← 返回 Skills 市场
datadrivenconstruction

Drawing Analyzer

作者 datadrivenconstruction · GitHub ↗ · v2.1.0
darwinlinuxwin32 ⚠ suspicious
1585
总下载
0
收藏
3
当前安装
2
版本数
在 OpenClaw 中安装
/install drawing-analyzer
功能描述
Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation.
使用说明 (SKILL.md)

\r

Drawing Analyzer for Construction\r

\r

Overview\r

\r Analyze construction drawings (PDF, DWG) to extract dimensions, annotations, symbols, title block data, and support automated quantity takeoff and design review.\r \r

Business Case\r

\r Drawing analysis automation enables:\r

  • Faster Takeoffs: Extract quantities from drawings\r
  • Quality Control: Verify drawing completeness\r
  • Data Extraction: Pull metadata for project systems\r
  • Design Review: Automated checking against standards\r \r

Technical Implementation\r

\r

from dataclasses import dataclass, field\r
from typing import List, Dict, Any, Optional, Tuple\r
import re\r
import pdfplumber\r
from pathlib import Path\r
\r
@dataclass\r
class TitleBlockData:\r
    project_name: str\r
    project_number: str\r
    sheet_number: str\r
    sheet_title: str\r
    discipline: str\r
    scale: str\r
    date: str\r
    revision: str\r
    drawn_by: str\r
    checked_by: str\r
    approved_by: str\r
\r
@dataclass\r
class Dimension:\r
    value: float\r
    unit: str\r
    dimension_type: str  # linear, angular, radial\r
    location: Tuple[float, float]\r
    associated_text: str\r
\r
@dataclass\r
class Annotation:\r
    text: str\r
    annotation_type: str  # note, callout, tag, keynote\r
    location: Tuple[float, float]\r
    references: List[str]\r
\r
@dataclass\r
class Symbol:\r
    symbol_type: str  # door, window, equipment, etc.\r
    tag: str\r
    location: Tuple[float, float]\r
    properties: Dict[str, Any]\r
\r
@dataclass\r
class DrawingAnalysisResult:\r
    file_name: str\r
    title_block: Optional[TitleBlockData]\r
    dimensions: List[Dimension]\r
    annotations: List[Annotation]\r
    symbols: List[Symbol]\r
    scale_factor: float\r
    drawing_area: Tuple[float, float]\r
    quality_issues: List[str]\r
\r
class DrawingAnalyzer:\r
    """Analyze construction drawings for data extraction."""\r
\r
    # Common dimension patterns\r
    DIMENSION_PATTERNS = [\r
        r"(\d+'-\s*\d+(?:\s*\d+/\d+)?\"?)",  # Feet-inches: 10'-6", 10' - 6 1/2"\r
        r"(\d+(?:\.\d+)?)\s*(?:mm|cm|m|ft|in)",  # Metric/imperial with unit\r
        r"(\d+'-\d+\")",  # Compact feet-inches\r
        r"(\d+)\s*(?:SF|LF|CY|EA)",  # Quantity dimensions\r
    ]\r
\r
    # Common annotation patterns\r
    ANNOTATION_PATTERNS = {\r
        'keynote': r'^\d{1,2}[A-Z]?$',  # 1A, 12, 5B\r
        'room_tag': r'^(?:RM|ROOM)\s*\d+',\r
        'door_tag': r'^[A-Z]?\d{2,3}[A-Z]?$',\r
        'grid_line': r'^[A-Z]$|^\d+$',\r
        'elevation': r'^(?:EL|ELEV)\.?\s*\d+',\r
        'detail_ref': r'^\d+/[A-Z]\d+',\r
    }\r
\r
    # Scale patterns\r
    SCALE_PATTERNS = [\r
        r"SCALE:\s*(\d+(?:/\d+)?)\s*[\"']\s*=\s*(\d+)\s*['\-]",  # 1/4" = 1'-0"\r
        r"(\d+):(\d+)",  # 1:100\r
        r"NTS|NOT TO SCALE",\r
    ]\r
\r
    def __init__(self):\r
        self.results: Dict[str, DrawingAnalysisResult] = {}\r
\r
    def analyze_pdf_drawing(self, pdf_path: str) -> DrawingAnalysisResult:\r
        """Analyze a PDF drawing."""\r
        path = Path(pdf_path)\r
\r
        all_text = ""\r
        dimensions = []\r
        annotations = []\r
        symbols = []\r
        quality_issues = []\r
\r
        with pdfplumber.open(pdf_path) as pdf:\r
            for page in pdf.pages:\r
                # Extract text\r
                text = page.extract_text() or ""\r
                all_text += text + "\
"\r
\r
                # Extract dimensions\r
                page_dims = self._extract_dimensions(text)\r
                dimensions.extend(page_dims)\r
\r
                # Extract annotations\r
                page_annots = self._extract_annotations(text)\r
                annotations.extend(page_annots)\r
\r
                # Extract from tables (often contain schedules)\r
                tables = page.extract_tables()\r
                for table in tables:\r
                    symbols.extend(self._parse_schedule_table(table))\r
\r
        # Parse title block\r
        title_block = self._extract_title_block(all_text)\r
\r
        # Determine scale\r
        scale_factor = self._determine_scale(all_text)\r
\r
        # Quality checks\r
        quality_issues = self._check_drawing_quality(\r
            title_block, dimensions, annotations\r
        )\r
\r
        result = DrawingAnalysisResult(\r
            file_name=path.name,\r
            title_block=title_block,\r
            dimensions=dimensions,\r
            annotations=annotations,\r
            symbols=symbols,\r
            scale_factor=scale_factor,\r
            drawing_area=(0, 0),  # Would need image analysis\r
            quality_issues=quality_issues\r
        )\r
\r
        self.results[path.name] = result\r
        return result\r
\r
    def _extract_dimensions(self, text: str) -> List[Dimension]:\r
        """Extract dimensions from text."""\r
        dimensions = []\r
\r
        for pattern in self.DIMENSION_PATTERNS:\r
            matches = re.findall(pattern, text)\r
            for match in matches:\r
                value, unit = self._parse_dimension_value(match)\r
                if value > 0:\r
                    dimensions.append(Dimension(\r
                        value=value,\r
                        unit=unit,\r
                        dimension_type='linear',\r
                        location=(0, 0),\r
                        associated_text=match\r
                    ))\r
\r
        return dimensions\r
\r
    def _parse_dimension_value(self, dim_text: str) -> Tuple[float, str]:\r
        """Parse dimension text to value and unit."""\r
        dim_text = dim_text.strip()\r
\r
        # Feet and inches: 10'-6"\r
        ft_in_match = re.match(r"(\d+)'[-\s]*(\d+)?(?:\s*(\d+)/(\d+))?\"?", dim_text)\r
        if ft_in_match:\r
            feet = int(ft_in_match.group(1))\r
            inches = int(ft_in_match.group(2) or 0)\r
            if ft_in_match.group(3) and ft_in_match.group(4):\r
                inches += int(ft_in_match.group(3)) / int(ft_in_match.group(4))\r
            return feet * 12 + inches, 'in'\r
\r
        # Metric with unit\r
        metric_match = re.match(r"(\d+(?:\.\d+)?)\s*(mm|cm|m)", dim_text)\r
        if metric_match:\r
            return float(metric_match.group(1)), metric_match.group(2)\r
\r
        # Just a number\r
        num_match = re.match(r"(\d+(?:\.\d+)?)", dim_text)\r
        if num_match:\r
            return float(num_match.group(1)), ''\r
\r
        return 0, ''\r
\r
    def _extract_annotations(self, text: str) -> List[Annotation]:\r
        """Extract annotations from text."""\r
        annotations = []\r
        lines = text.split('\
')\r
\r
        for line in lines:\r
            line = line.strip()\r
            if not line:\r
                continue\r
\r
            for annot_type, pattern in self.ANNOTATION_PATTERNS.items():\r
                if re.match(pattern, line, re.IGNORECASE):\r
                    annotations.append(Annotation(\r
                        text=line,\r
                        annotation_type=annot_type,\r
                        location=(0, 0),\r
                        references=[]\r
                    ))\r
                    break\r
\r
            # General notes\r
            if line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):\r
                annotations.append(Annotation(\r
                    text=line,\r
                    annotation_type='note',\r
                    location=(0, 0),\r
                    references=[]\r
                ))\r
\r
        return annotations\r
\r
    def _extract_title_block(self, text: str) -> Optional[TitleBlockData]:\r
        """Extract title block information."""\r
        # Common title block patterns\r
        patterns = {\r
            'project_name': r'PROJECT(?:\s*NAME)?:\s*(.+?)(?:\
|$)',\r
            'project_number': r'(?:PROJECT\s*)?(?:NO|NUMBER|#)\.?:\s*(\S+)',\r
            'sheet_number': r'SHEET(?:\s*NO)?\.?:\s*([A-Z]?\d+(?:\.\d+)?)',\r
            'sheet_title': r'SHEET\s*TITLE:\s*(.+?)(?:\
|$)',\r
            'scale': r'SCALE:\s*(.+?)(?:\
|$)',\r
            'date': r'DATE:\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})',\r
            'revision': r'REV(?:ISION)?\.?:\s*(\S+)',\r
            'drawn_by': r'(?:DRAWN|DRN)\s*(?:BY)?:\s*(\S+)',\r
            'checked_by': r'(?:CHECKED|CHK)\s*(?:BY)?:\s*(\S+)',\r
        }\r
\r
        extracted = {}\r
        for field, pattern in patterns.items():\r
            match = re.search(pattern, text, re.IGNORECASE)\r
            extracted[field] = match.group(1).strip() if match else ''\r
\r
        # Determine discipline from sheet number\r
        sheet_num = extracted.get('sheet_number', '')\r
        discipline = ''\r
        if sheet_num:\r
            prefix = sheet_num[0].upper() if sheet_num[0].isalpha() else ''\r
            discipline_map = {\r
                'A': 'Architectural', 'S': 'Structural', 'M': 'Mechanical',\r
                'E': 'Electrical', 'P': 'Plumbing', 'C': 'Civil',\r
                'L': 'Landscape', 'I': 'Interior', 'F': 'Fire Protection'\r
            }\r
            discipline = discipline_map.get(prefix, '')\r
\r
        return TitleBlockData(\r
            project_name=extracted.get('project_name', ''),\r
            project_number=extracted.get('project_number', ''),\r
            sheet_number=sheet_num,\r
            sheet_title=extracted.get('sheet_title', ''),\r
            discipline=discipline,\r
            scale=extracted.get('scale', ''),\r
            date=extracted.get('date', ''),\r
            revision=extracted.get('revision', ''),\r
            drawn_by=extracted.get('drawn_by', ''),\r
            checked_by=extracted.get('checked_by', ''),\r
            approved_by=''\r
        )\r
\r
    def _parse_schedule_table(self, table: List[List]) -> List[Symbol]:\r
        """Parse schedule table to extract symbols/elements."""\r
        symbols = []\r
\r
        if not table or len(table) \x3C 2:\r
            return symbols\r
\r
        # First row is usually headers\r
        headers = [str(cell).lower() if cell else '' for cell in table[0]]\r
\r
        # Find key columns\r
        tag_col = next((i for i, h in enumerate(headers) if 'tag' in h or 'mark' in h or 'no' in h), 0)\r
        type_col = next((i for i, h in enumerate(headers) if 'type' in h or 'size' in h), -1)\r
\r
        for row in table[1:]:\r
            if len(row) > tag_col and row[tag_col]:\r
                tag = str(row[tag_col]).strip()\r
                symbol_type = str(row[type_col]).strip() if type_col >= 0 and len(row) > type_col else ''\r
\r
                if tag:\r
                    props = {}\r
                    for i, header in enumerate(headers):\r
                        if i \x3C len(row) and row[i]:\r
                            props[header] = str(row[i])\r
\r
                    symbols.append(Symbol(\r
                        symbol_type=symbol_type or 'unknown',\r
                        tag=tag,\r
                        location=(0, 0),\r
                        properties=props\r
                    ))\r
\r
        return symbols\r
\r
    def _determine_scale(self, text: str) -> float:\r
        """Determine drawing scale factor."""\r
        for pattern in self.SCALE_PATTERNS:\r
            match = re.search(pattern, text, re.IGNORECASE)\r
            if match:\r
                if 'NTS' in match.group(0).upper():\r
                    return 0  # Not to scale\r
\r
                if '=' in match.group(0):\r
                    # Imperial: 1/4" = 1'-0"\r
                    return self._parse_imperial_scale(match.group(0))\r
                else:\r
                    # Metric: 1:100\r
                    return 1 / float(match.group(2))\r
\r
        return 1.0  # Default\r
\r
    def _parse_imperial_scale(self, scale_text: str) -> float:\r
        """Parse imperial scale to factor."""\r
        match = re.search(r'(\d+)(?:/(\d+))?\s*["\']?\s*=\s*(\d+)', scale_text)\r
        if match:\r
            numerator = float(match.group(1))\r
            denominator = float(match.group(2)) if match.group(2) else 1\r
            feet = float(match.group(3))\r
            inches_per_foot = (numerator / denominator)\r
            return inches_per_foot / (feet * 12)\r
        return 1.0\r
\r
    def _check_drawing_quality(self, title_block: TitleBlockData,\r
                                dimensions: List, annotations: List) -> List[str]:\r
        """Check drawing for quality issues."""\r
        issues = []\r
\r
        if title_block:\r
            if not title_block.project_number:\r
                issues.append("Missing project number in title block")\r
            if not title_block.sheet_number:\r
                issues.append("Missing sheet number")\r
            if not title_block.scale:\r
                issues.append("Missing scale indication")\r
            if not title_block.date:\r
                issues.append("Missing date")\r
\r
        if len(dimensions) == 0:\r
            issues.append("No dimensions found - verify drawing content")\r
\r
        # Check for typical construction notes\r
        note_types = [a.annotation_type for a in annotations]\r
        if 'note' not in note_types:\r
            issues.append("No general notes found")\r
\r
        return issues\r
\r
    def generate_drawing_index(self, results: List[DrawingAnalysisResult]) -> str:\r
        """Generate drawing index from multiple analyzed drawings."""\r
        lines = ["# Drawing Index", ""]\r
        lines.append("| Sheet | Title | Discipline | Scale | Rev |")\r
        lines.append("|-------|-------|------------|-------|-----|")\r
\r
        for result in sorted(results, key=lambda r: r.title_block.sheet_number if r.title_block else ''):\r
            if result.title_block:\r
                tb = result.title_block\r
                lines.append(f"| {tb.sheet_number} | {tb.sheet_title} | {tb.discipline} | {tb.scale} | {tb.revision} |")\r
\r
        return "\
".join(lines)\r
\r
    def generate_report(self, result: DrawingAnalysisResult) -> str:\r
        """Generate analysis report for a drawing."""\r
        lines = ["# Drawing Analysis Report", ""]\r
        lines.append(f"**File:** {result.file_name}")\r
\r
        if result.title_block:\r
            tb = result.title_block\r
            lines.append("")\r
            lines.append("## Title Block")\r
            lines.append(f"- **Project:** {tb.project_name}")\r
            lines.append(f"- **Project No:** {tb.project_number}")\r
            lines.append(f"- **Sheet:** {tb.sheet_number}")\r
            lines.append(f"- **Title:** {tb.sheet_title}")\r
            lines.append(f"- **Discipline:** {tb.discipline}")\r
            lines.append(f"- **Scale:** {tb.scale}")\r
            lines.append(f"- **Date:** {tb.date}")\r
            lines.append(f"- **Revision:** {tb.revision}")\r
\r
        lines.append("")\r
        lines.append("## Content Summary")\r
        lines.append(f"- **Dimensions Found:** {len(result.dimensions)}")\r
        lines.append(f"- **Annotations Found:** {len(result.annotations)}")\r
        lines.append(f"- **Symbols/Elements:** {len(result.symbols)}")\r
\r
        if result.quality_issues:\r
            lines.append("")\r
            lines.append("## Quality Issues")\r
            for issue in result.quality_issues:\r
                lines.append(f"- ⚠️ {issue}")\r
\r
        if result.symbols:\r
            lines.append("")\r
            lines.append("## Elements Found")\r
            for symbol in result.symbols[:20]:\r
                lines.append(f"- {symbol.tag}: {symbol.symbol_type}")\r
\r
        return "\
".join(lines)\r
```\r
\r
## Quick Start\r
\r
```python\r
# Initialize analyzer\r
analyzer = DrawingAnalyzer()\r
\r
# Analyze a drawing\r
result = analyzer.analyze_pdf_drawing("A101_Floor_Plan.pdf")\r
\r
# Check title block\r
if result.title_block:\r
    print(f"Sheet: {result.title_block.sheet_number}")\r
    print(f"Title: {result.title_block.sheet_title}")\r
    print(f"Scale: {result.title_block.scale}")\r
\r
# Review extracted data\r
print(f"Dimensions: {len(result.dimensions)}")\r
print(f"Annotations: {len(result.annotations)}")\r
print(f"Symbols: {len(result.symbols)}")\r
\r
# Check quality\r
for issue in result.quality_issues:\r
    print(f"Issue: {issue}")\r
\r
# Generate report\r
report = analyzer.generate_report(result)\r
print(report)\r
```\r
\r
## Dependencies\r
\r
```bash\r
pip install pdfplumber\r
```\r
安全使用建议
This skill contains useful PDF parsing code but has gaps and inconsistencies. Before installing or running it: - Expect to need to install Python packages (pdfplumber and other libs referenced) manually; the skill does not provide an install script. - Confirm DWG/CAD support with the author — the included code only covers PDFs, so DWG files may not be handled. - Only provide non-sensitive sample drawings until you verify behavior. Because the skill has filesystem permission, it can read files you point it to — avoid supplying credentials or unrelated system paths. - Ask the publisher for an explicit install manifest (pip requirements or a tested runtime) and for clarification of what files the skill will access and any network endpoints it contacts. - If you require higher assurance, request a version with an explicit install spec, a minimal dependency list, or run it in an isolated environment (sandbox) first.
功能分析
Type: OpenClaw Skill Name: drawing-analyzer Version: 2.1.0 The OpenClaw skill 'drawing-analyzer' is designed to extract data from PDF construction drawings. The Python code in SKILL.md uses `pdfplumber` to read and parse PDF files, which necessitates file system access. This access is explicitly declared and justified by the `filesystem` permission in `claw.json`. There is no evidence of malicious intent, such as data exfiltration, unauthorized command execution, persistence mechanisms, or prompt injection attempts in any of the files. The instructions for the AI agent in `instructions.md` are clear and align with the skill's stated purpose, even including constraints to 'Only use data provided by the user or referenced in the skill'.
能力评估
Purpose & Capability
The name/description claim PDF and DWG analysis plus quantity takeoff. The included runtime code and examples only show PDF text extraction (pdfplumber) and no DWG handling or CAD library usage. Requesting only python3 while promising DWG/CAD features is disproportionate and misleading.
Instruction Scope
SKILL.md contains detailed Python code for opening user-supplied PDF file paths and extracting text/tables; that scope (reading user-provided drawing files) matches the stated purpose. However the instructions don't include clear runtime/install steps for required Python packages, and the prose ('process using methods in SKILL.md') could encourage the agent to execute arbitrary Python code derived from the doc — clarify allowed operations and explicit input sources.
Install Mechanism
This is instruction-only (no install spec). The code depends on third-party Python libraries (e.g., pdfplumber) but the skill does not declare how to install them. That mismatch means an agent or user may attempt ad-hoc pip installs or fail at runtime; lack of a vetted install source raises operational risk.
Credentials
The skill requests no environment variables or credentials. claw.json declares filesystem permission which is appropriate for reading drawings supplied by the user. No unrelated secrets or external credentials are requested.
Persistence & Privilege
always is false and the skill is user-invocable — normal. The skill does require filesystem access per metadata, which is expected for processing local drawings; it does not request elevated platform-wide privileges.
如何使用
  1. 确保已安装 OpenClaw(本地或 Docker 部署)
  2. 在对话框中输入安装命令:/install drawing-analyzer
  3. 安装完成后,直接呼叫该 Skill 的名称或使用 /drawing-analyzer 触发
  4. 根据 Skill 的参数说明提供必要输入,即可获得结构化输出
版本历史
v2.1.0
drawing-analyzer 2.1.0 - Added in-depth documentation and business case details in SKILL.md, describing supported drawing formats (PDF, DWG), extractable data (dimensions, annotations, symbols, title block), and automation use cases. - Clarified technical implementation and included Python code samples for how drawing analysis is performed. - Expanded metadata to specify binaries required (python3) and supported operating systems. - Updated homepage and description for improved clarity on supported features and automation capabilities.
v1.0.0
Drawing Analyzer 1.0.0 - Initial release. - Analyze construction drawings (PDF, DWG) to extract dimensions, annotations, symbols, and title block data. - Supports automated quantity takeoff and design review. - Extracts and parses common patterns for dimensions, annotations, and drawing metadata. - Identifies quality issues to assist with quality control and completeness checks.
元数据
Slug drawing-analyzer
版本 2.1.0
许可证
累计安装 3
当前安装数 3
历史版本数 2
常见问题

Drawing Analyzer 是什么?

Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation. 它是一个面向 Claude Code / OpenClaw 的 AI Agent Skill 插件,目前累计下载 1585 次。

如何安装 Drawing Analyzer?

在 OpenClaw 或 Claude Code 对话框中运行命令「/install drawing-analyzer」即可一键安装,无需额外配置。

Drawing Analyzer 是免费的吗?

是的,Drawing Analyzer 完全免费(开源免费),可自由下载、安装和使用。

Drawing Analyzer 支持哪些平台?

Drawing Analyzer 跨平台运行,可在任意部署了 OpenClaw / Claude Code 的环境中使用(darwin, linux, win32)。

谁开发了 Drawing Analyzer?

由 datadrivenconstruction(@datadrivenconstruction)开发并维护,当前版本 v2.1.0。

💬 留言讨论