{
  "schemaVersion": "1.0",
  "item": {
    "slug": "drawing-analyzer",
    "name": "Drawing Analyzer",
    "source": "tencent",
    "type": "skill",
    "category": "AI 智能",
    "sourceUrl": "https://clawhub.ai/datadrivenconstruction/drawing-analyzer",
    "canonicalUrl": "https://clawhub.ai/datadrivenconstruction/drawing-analyzer",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/drawing-analyzer",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=drawing-analyzer",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "claw.json",
      "instructions.md",
      "SKILL.md"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-04-30T16:55:25.780Z",
      "expiresAt": "2026-05-07T16:55:25.780Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=network",
        "contentDisposition": "attachment; filename=\"network-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/drawing-analyzer"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/drawing-analyzer",
    "agentPageUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent",
    "manifestUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "Overview",
        "body": "Analyze construction drawings (PDF, DWG) to extract dimensions, annotations, symbols, title block data, and support automated quantity takeoff and design review."
      },
      {
        "title": "Business Case",
        "body": "Drawing analysis automation enables:\n\nFaster Takeoffs: Extract quantities from drawings\nQuality Control: Verify drawing completeness\nData Extraction: Pull metadata for project systems\nDesign Review: Automated checking against standards"
      },
      {
        "title": "Technical Implementation",
        "body": "from dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional, Tuple\nimport re\nimport pdfplumber\nfrom pathlib import Path\n\n@dataclass\nclass TitleBlockData:\n    project_name: str\n    project_number: str\n    sheet_number: str\n    sheet_title: str\n    discipline: str\n    scale: str\n    date: str\n    revision: str\n    drawn_by: str\n    checked_by: str\n    approved_by: str\n\n@dataclass\nclass Dimension:\n    value: float\n    unit: str\n    dimension_type: str  # linear, angular, radial\n    location: Tuple[float, float]\n    associated_text: str\n\n@dataclass\nclass Annotation:\n    text: str\n    annotation_type: str  # note, callout, tag, keynote\n    location: Tuple[float, float]\n    references: List[str]\n\n@dataclass\nclass Symbol:\n    symbol_type: str  # door, window, equipment, etc.\n    tag: str\n    location: Tuple[float, float]\n    properties: Dict[str, Any]\n\n@dataclass\nclass DrawingAnalysisResult:\n    file_name: str\n    title_block: Optional[TitleBlockData]\n    dimensions: List[Dimension]\n    annotations: List[Annotation]\n    symbols: List[Symbol]\n    scale_factor: float\n    drawing_area: Tuple[float, float]\n    quality_issues: List[str]\n\nclass DrawingAnalyzer:\n    \"\"\"Analyze construction drawings for data extraction.\"\"\"\n\n    # Common dimension patterns\n    DIMENSION_PATTERNS = [\n        r\"(\\d+'-\\s*\\d+(?:\\s*\\d+/\\d+)?\\\"?)\",  # Feet-inches: 10'-6\", 10' - 6 1/2\"\n        r\"(\\d+(?:\\.\\d+)?)\\s*(?:mm|cm|m|ft|in)\",  # Metric/imperial with unit\n        r\"(\\d+'-\\d+\\\")\",  # Compact feet-inches\n        r\"(\\d+)\\s*(?:SF|LF|CY|EA)\",  # Quantity dimensions\n    ]\n\n    # Common annotation patterns\n    ANNOTATION_PATTERNS = {\n        'keynote': r'^\\d{1,2}[A-Z]?$',  # 1A, 12, 5B\n        'room_tag': r'^(?:RM|ROOM)\\s*\\d+',\n        'door_tag': r'^[A-Z]?\\d{2,3}[A-Z]?$',\n        'grid_line': r'^[A-Z]$|^\\d+$',\n        'elevation': r'^(?:EL|ELEV)\\.?\\s*\\d+',\n        'detail_ref': r'^\\d+/[A-Z]\\d+',\n    }\n\n    # Scale patterns\n    SCALE_PATTERNS = [\n        r\"SCALE:\\s*(\\d+(?:/\\d+)?)\\s*[\\\"']\\s*=\\s*(\\d+)\\s*['\\-]\",  # 1/4\" = 1'-0\"\n        r\"(\\d+):(\\d+)\",  # 1:100\n        r\"NTS|NOT TO SCALE\",\n    ]\n\n    def __init__(self):\n        self.results: Dict[str, DrawingAnalysisResult] = {}\n\n    def analyze_pdf_drawing(self, pdf_path: str) -> DrawingAnalysisResult:\n        \"\"\"Analyze a PDF drawing.\"\"\"\n        path = Path(pdf_path)\n\n        all_text = \"\"\n        dimensions = []\n        annotations = []\n        symbols = []\n        quality_issues = []\n\n        with pdfplumber.open(pdf_path) as pdf:\n            for page in pdf.pages:\n                # Extract text\n                text = page.extract_text() or \"\"\n                all_text += text + \"\\n\"\n\n                # Extract dimensions\n                page_dims = self._extract_dimensions(text)\n                dimensions.extend(page_dims)\n\n                # Extract annotations\n                page_annots = self._extract_annotations(text)\n                annotations.extend(page_annots)\n\n                # Extract from tables (often contain schedules)\n                tables = page.extract_tables()\n                for table in tables:\n                    symbols.extend(self._parse_schedule_table(table))\n\n        # Parse title block\n        title_block = self._extract_title_block(all_text)\n\n        # Determine scale\n        scale_factor = self._determine_scale(all_text)\n\n        # Quality checks\n        quality_issues = self._check_drawing_quality(\n            title_block, dimensions, annotations\n        )\n\n        result = DrawingAnalysisResult(\n            file_name=path.name,\n            title_block=title_block,\n            dimensions=dimensions,\n            annotations=annotations,\n            symbols=symbols,\n            scale_factor=scale_factor,\n            drawing_area=(0, 0),  # Would need image analysis\n            quality_issues=quality_issues\n        )\n\n        self.results[path.name] = result\n        return result\n\n    def _extract_dimensions(self, text: str) -> List[Dimension]:\n        \"\"\"Extract dimensions from text.\"\"\"\n        dimensions = []\n\n        for pattern in self.DIMENSION_PATTERNS:\n            matches = re.findall(pattern, text)\n            for match in matches:\n                value, unit = self._parse_dimension_value(match)\n                if value > 0:\n                    dimensions.append(Dimension(\n                        value=value,\n                        unit=unit,\n                        dimension_type='linear',\n                        location=(0, 0),\n                        associated_text=match\n                    ))\n\n        return dimensions\n\n    def _parse_dimension_value(self, dim_text: str) -> Tuple[float, str]:\n        \"\"\"Parse dimension text to value and unit.\"\"\"\n        dim_text = dim_text.strip()\n\n        # Feet and inches: 10'-6\"\n        ft_in_match = re.match(r\"(\\d+)'[-\\s]*(\\d+)?(?:\\s*(\\d+)/(\\d+))?\\\"?\", dim_text)\n        if ft_in_match:\n            feet = int(ft_in_match.group(1))\n            inches = int(ft_in_match.group(2) or 0)\n            if ft_in_match.group(3) and ft_in_match.group(4):\n                inches += int(ft_in_match.group(3)) / int(ft_in_match.group(4))\n            return feet * 12 + inches, 'in'\n\n        # Metric with unit\n        metric_match = re.match(r\"(\\d+(?:\\.\\d+)?)\\s*(mm|cm|m)\", dim_text)\n        if metric_match:\n            return float(metric_match.group(1)), metric_match.group(2)\n\n        # Just a number\n        num_match = re.match(r\"(\\d+(?:\\.\\d+)?)\", dim_text)\n        if num_match:\n            return float(num_match.group(1)), ''\n\n        return 0, ''\n\n    def _extract_annotations(self, text: str) -> List[Annotation]:\n        \"\"\"Extract annotations from text.\"\"\"\n        annotations = []\n        lines = text.split('\\n')\n\n        for line in lines:\n            line = line.strip()\n            if not line:\n                continue\n\n            for annot_type, pattern in self.ANNOTATION_PATTERNS.items():\n                if re.match(pattern, line, re.IGNORECASE):\n                    annotations.append(Annotation(\n                        text=line,\n                        annotation_type=annot_type,\n                        location=(0, 0),\n                        references=[]\n                    ))\n                    break\n\n            # General notes\n            if line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):\n                annotations.append(Annotation(\n                    text=line,\n                    annotation_type='note',\n                    location=(0, 0),\n                    references=[]\n                ))\n\n        return annotations\n\n    def _extract_title_block(self, text: str) -> Optional[TitleBlockData]:\n        \"\"\"Extract title block information.\"\"\"\n        # Common title block patterns\n        patterns = {\n            'project_name': r'PROJECT(?:\\s*NAME)?:\\s*(.+?)(?:\\n|$)',\n            'project_number': r'(?:PROJECT\\s*)?(?:NO|NUMBER|#)\\.?:\\s*(\\S+)',\n            'sheet_number': r'SHEET(?:\\s*NO)?\\.?:\\s*([A-Z]?\\d+(?:\\.\\d+)?)',\n            'sheet_title': r'SHEET\\s*TITLE:\\s*(.+?)(?:\\n|$)',\n            'scale': r'SCALE:\\s*(.+?)(?:\\n|$)',\n            'date': r'DATE:\\s*(\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4})',\n            'revision': r'REV(?:ISION)?\\.?:\\s*(\\S+)',\n            'drawn_by': r'(?:DRAWN|DRN)\\s*(?:BY)?:\\s*(\\S+)',\n            'checked_by': r'(?:CHECKED|CHK)\\s*(?:BY)?:\\s*(\\S+)',\n        }\n\n        extracted = {}\n        for field, pattern in patterns.items():\n            match = re.search(pattern, text, re.IGNORECASE)\n            extracted[field] = match.group(1).strip() if match else ''\n\n        # Determine discipline from sheet number\n        sheet_num = extracted.get('sheet_number', '')\n        discipline = ''\n        if sheet_num:\n            prefix = sheet_num[0].upper() if sheet_num[0].isalpha() else ''\n            discipline_map = {\n                'A': 'Architectural', 'S': 'Structural', 'M': 'Mechanical',\n                'E': 'Electrical', 'P': 'Plumbing', 'C': 'Civil',\n                'L': 'Landscape', 'I': 'Interior', 'F': 'Fire Protection'\n            }\n            discipline = discipline_map.get(prefix, '')\n\n        return TitleBlockData(\n            project_name=extracted.get('project_name', ''),\n            project_number=extracted.get('project_number', ''),\n            sheet_number=sheet_num,\n            sheet_title=extracted.get('sheet_title', ''),\n            discipline=discipline,\n            scale=extracted.get('scale', ''),\n            date=extracted.get('date', ''),\n            revision=extracted.get('revision', ''),\n            drawn_by=extracted.get('drawn_by', ''),\n            checked_by=extracted.get('checked_by', ''),\n            approved_by=''\n        )\n\n    def _parse_schedule_table(self, table: List[List]) -> List[Symbol]:\n        \"\"\"Parse schedule table to extract symbols/elements.\"\"\"\n        symbols = []\n\n        if not table or len(table) < 2:\n            return symbols\n\n        # First row is usually headers\n        headers = [str(cell).lower() if cell else '' for cell in table[0]]\n\n        # Find key columns\n        tag_col = next((i for i, h in enumerate(headers) if 'tag' in h or 'mark' in h or 'no' in h), 0)\n        type_col = next((i for i, h in enumerate(headers) if 'type' in h or 'size' in h), -1)\n\n        for row in table[1:]:\n            if len(row) > tag_col and row[tag_col]:\n                tag = str(row[tag_col]).strip()\n                symbol_type = str(row[type_col]).strip() if type_col >= 0 and len(row) > type_col else ''\n\n                if tag:\n                    props = {}\n                    for i, header in enumerate(headers):\n                        if i < len(row) and row[i]:\n                            props[header] = str(row[i])\n\n                    symbols.append(Symbol(\n                        symbol_type=symbol_type or 'unknown',\n                        tag=tag,\n                        location=(0, 0),\n                        properties=props\n                    ))\n\n        return symbols\n\n    def _determine_scale(self, text: str) -> float:\n        \"\"\"Determine drawing scale factor.\"\"\"\n        for pattern in self.SCALE_PATTERNS:\n            match = re.search(pattern, text, re.IGNORECASE)\n            if match:\n                if 'NTS' in match.group(0).upper():\n                    return 0  # Not to scale\n\n                if '=' in match.group(0):\n                    # Imperial: 1/4\" = 1'-0\"\n                    return self._parse_imperial_scale(match.group(0))\n                else:\n                    # Metric: 1:100\n                    return 1 / float(match.group(2))\n\n        return 1.0  # Default\n\n    def _parse_imperial_scale(self, scale_text: str) -> float:\n        \"\"\"Parse imperial scale to factor.\"\"\"\n        match = re.search(r'(\\d+)(?:/(\\d+))?\\s*[\"\\']?\\s*=\\s*(\\d+)', scale_text)\n        if match:\n            numerator = float(match.group(1))\n            denominator = float(match.group(2)) if match.group(2) else 1\n            feet = float(match.group(3))\n            inches_per_foot = (numerator / denominator)\n            return inches_per_foot / (feet * 12)\n        return 1.0\n\n    def _check_drawing_quality(self, title_block: TitleBlockData,\n                                dimensions: List, annotations: List) -> List[str]:\n        \"\"\"Check drawing for quality issues.\"\"\"\n        issues = []\n\n        if title_block:\n            if not title_block.project_number:\n                issues.append(\"Missing project number in title block\")\n            if not title_block.sheet_number:\n                issues.append(\"Missing sheet number\")\n            if not title_block.scale:\n                issues.append(\"Missing scale indication\")\n            if not title_block.date:\n                issues.append(\"Missing date\")\n\n        if len(dimensions) == 0:\n            issues.append(\"No dimensions found - verify drawing content\")\n\n        # Check for typical construction notes\n        note_types = [a.annotation_type for a in annotations]\n        if 'note' not in note_types:\n            issues.append(\"No general notes found\")\n\n        return issues\n\n    def generate_drawing_index(self, results: List[DrawingAnalysisResult]) -> str:\n        \"\"\"Generate drawing index from multiple analyzed drawings.\"\"\"\n        lines = [\"# Drawing Index\", \"\"]\n        lines.append(\"| Sheet | Title | Discipline | Scale | Rev |\")\n        lines.append(\"|-------|-------|------------|-------|-----|\")\n\n        for result in sorted(results, key=lambda r: r.title_block.sheet_number if r.title_block else ''):\n            if result.title_block:\n                tb = result.title_block\n                lines.append(f\"| {tb.sheet_number} | {tb.sheet_title} | {tb.discipline} | {tb.scale} | {tb.revision} |\")\n\n        return \"\\n\".join(lines)\n\n    def generate_report(self, result: DrawingAnalysisResult) -> str:\n        \"\"\"Generate analysis report for a drawing.\"\"\"\n        lines = [\"# Drawing Analysis Report\", \"\"]\n        lines.append(f\"**File:** {result.file_name}\")\n\n        if result.title_block:\n            tb = result.title_block\n            lines.append(\"\")\n            lines.append(\"## Title Block\")\n            lines.append(f\"- **Project:** {tb.project_name}\")\n            lines.append(f\"- **Project No:** {tb.project_number}\")\n            lines.append(f\"- **Sheet:** {tb.sheet_number}\")\n            lines.append(f\"- **Title:** {tb.sheet_title}\")\n            lines.append(f\"- **Discipline:** {tb.discipline}\")\n            lines.append(f\"- **Scale:** {tb.scale}\")\n            lines.append(f\"- **Date:** {tb.date}\")\n            lines.append(f\"- **Revision:** {tb.revision}\")\n\n        lines.append(\"\")\n        lines.append(\"## Content Summary\")\n        lines.append(f\"- **Dimensions Found:** {len(result.dimensions)}\")\n        lines.append(f\"- **Annotations Found:** {len(result.annotations)}\")\n        lines.append(f\"- **Symbols/Elements:** {len(result.symbols)}\")\n\n        if result.quality_issues:\n            lines.append(\"\")\n            lines.append(\"## Quality Issues\")\n            for issue in result.quality_issues:\n                lines.append(f\"- ⚠️ {issue}\")\n\n        if result.symbols:\n            lines.append(\"\")\n            lines.append(\"## Elements Found\")\n            for symbol in result.symbols[:20]:\n                lines.append(f\"- {symbol.tag}: {symbol.symbol_type}\")\n\n        return \"\\n\".join(lines)"
      },
      {
        "title": "Quick Start",
        "body": "# Initialize analyzer\nanalyzer = DrawingAnalyzer()\n\n# Analyze a drawing\nresult = analyzer.analyze_pdf_drawing(\"A101_Floor_Plan.pdf\")\n\n# Check title block\nif result.title_block:\n    print(f\"Sheet: {result.title_block.sheet_number}\")\n    print(f\"Title: {result.title_block.sheet_title}\")\n    print(f\"Scale: {result.title_block.scale}\")\n\n# Review extracted data\nprint(f\"Dimensions: {len(result.dimensions)}\")\nprint(f\"Annotations: {len(result.annotations)}\")\nprint(f\"Symbols: {len(result.symbols)}\")\n\n# Check quality\nfor issue in result.quality_issues:\n    print(f\"Issue: {issue}\")\n\n# Generate report\nreport = analyzer.generate_report(result)\nprint(report)"
      },
      {
        "title": "Dependencies",
        "body": "pip install pdfplumber"
      }
    ],
    "body": "Drawing Analyzer for Construction\nOverview\n\nAnalyze construction drawings (PDF, DWG) to extract dimensions, annotations, symbols, title block data, and support automated quantity takeoff and design review.\n\nBusiness Case\n\nDrawing analysis automation enables:\n\nFaster Takeoffs: Extract quantities from drawings\nQuality Control: Verify drawing completeness\nData Extraction: Pull metadata for project systems\nDesign Review: Automated checking against standards\nTechnical Implementation\nfrom dataclasses import dataclass, field\nfrom typing import List, Dict, Any, Optional, Tuple\nimport re\nimport pdfplumber\nfrom pathlib import Path\n\n@dataclass\nclass TitleBlockData:\n    project_name: str\n    project_number: str\n    sheet_number: str\n    sheet_title: str\n    discipline: str\n    scale: str\n    date: str\n    revision: str\n    drawn_by: str\n    checked_by: str\n    approved_by: str\n\n@dataclass\nclass Dimension:\n    value: float\n    unit: str\n    dimension_type: str  # linear, angular, radial\n    location: Tuple[float, float]\n    associated_text: str\n\n@dataclass\nclass Annotation:\n    text: str\n    annotation_type: str  # note, callout, tag, keynote\n    location: Tuple[float, float]\n    references: List[str]\n\n@dataclass\nclass Symbol:\n    symbol_type: str  # door, window, equipment, etc.\n    tag: str\n    location: Tuple[float, float]\n    properties: Dict[str, Any]\n\n@dataclass\nclass DrawingAnalysisResult:\n    file_name: str\n    title_block: Optional[TitleBlockData]\n    dimensions: List[Dimension]\n    annotations: List[Annotation]\n    symbols: List[Symbol]\n    scale_factor: float\n    drawing_area: Tuple[float, float]\n    quality_issues: List[str]\n\nclass DrawingAnalyzer:\n    \"\"\"Analyze construction drawings for data extraction.\"\"\"\n\n    # Common dimension patterns\n    DIMENSION_PATTERNS = [\n        r\"(\\d+'-\\s*\\d+(?:\\s*\\d+/\\d+)?\\\"?)\",  # Feet-inches: 10'-6\", 10' - 6 1/2\"\n        r\"(\\d+(?:\\.\\d+)?)\\s*(?:mm|cm|m|ft|in)\",  # Metric/imperial with unit\n        r\"(\\d+'-\\d+\\\")\",  # Compact feet-inches\n        r\"(\\d+)\\s*(?:SF|LF|CY|EA)\",  # Quantity dimensions\n    ]\n\n    # Common annotation patterns\n    ANNOTATION_PATTERNS = {\n        'keynote': r'^\\d{1,2}[A-Z]?$',  # 1A, 12, 5B\n        'room_tag': r'^(?:RM|ROOM)\\s*\\d+',\n        'door_tag': r'^[A-Z]?\\d{2,3}[A-Z]?$',\n        'grid_line': r'^[A-Z]$|^\\d+$',\n        'elevation': r'^(?:EL|ELEV)\\.?\\s*\\d+',\n        'detail_ref': r'^\\d+/[A-Z]\\d+',\n    }\n\n    # Scale patterns\n    SCALE_PATTERNS = [\n        r\"SCALE:\\s*(\\d+(?:/\\d+)?)\\s*[\\\"']\\s*=\\s*(\\d+)\\s*['\\-]\",  # 1/4\" = 1'-0\"\n        r\"(\\d+):(\\d+)\",  # 1:100\n        r\"NTS|NOT TO SCALE\",\n    ]\n\n    def __init__(self):\n        self.results: Dict[str, DrawingAnalysisResult] = {}\n\n    def analyze_pdf_drawing(self, pdf_path: str) -> DrawingAnalysisResult:\n        \"\"\"Analyze a PDF drawing.\"\"\"\n        path = Path(pdf_path)\n\n        all_text = \"\"\n        dimensions = []\n        annotations = []\n        symbols = []\n        quality_issues = []\n\n        with pdfplumber.open(pdf_path) as pdf:\n            for page in pdf.pages:\n                # Extract text\n                text = page.extract_text() or \"\"\n                all_text += text + \"\\n\"\n\n                # Extract dimensions\n                page_dims = self._extract_dimensions(text)\n                dimensions.extend(page_dims)\n\n                # Extract annotations\n                page_annots = self._extract_annotations(text)\n                annotations.extend(page_annots)\n\n                # Extract from tables (often contain schedules)\n                tables = page.extract_tables()\n                for table in tables:\n                    symbols.extend(self._parse_schedule_table(table))\n\n        # Parse title block\n        title_block = self._extract_title_block(all_text)\n\n        # Determine scale\n        scale_factor = self._determine_scale(all_text)\n\n        # Quality checks\n        quality_issues = self._check_drawing_quality(\n            title_block, dimensions, annotations\n        )\n\n        result = DrawingAnalysisResult(\n            file_name=path.name,\n            title_block=title_block,\n            dimensions=dimensions,\n            annotations=annotations,\n            symbols=symbols,\n            scale_factor=scale_factor,\n            drawing_area=(0, 0),  # Would need image analysis\n            quality_issues=quality_issues\n        )\n\n        self.results[path.name] = result\n        return result\n\n    def _extract_dimensions(self, text: str) -> List[Dimension]:\n        \"\"\"Extract dimensions from text.\"\"\"\n        dimensions = []\n\n        for pattern in self.DIMENSION_PATTERNS:\n            matches = re.findall(pattern, text)\n            for match in matches:\n                value, unit = self._parse_dimension_value(match)\n                if value > 0:\n                    dimensions.append(Dimension(\n                        value=value,\n                        unit=unit,\n                        dimension_type='linear',\n                        location=(0, 0),\n                        associated_text=match\n                    ))\n\n        return dimensions\n\n    def _parse_dimension_value(self, dim_text: str) -> Tuple[float, str]:\n        \"\"\"Parse dimension text to value and unit.\"\"\"\n        dim_text = dim_text.strip()\n\n        # Feet and inches: 10'-6\"\n        ft_in_match = re.match(r\"(\\d+)'[-\\s]*(\\d+)?(?:\\s*(\\d+)/(\\d+))?\\\"?\", dim_text)\n        if ft_in_match:\n            feet = int(ft_in_match.group(1))\n            inches = int(ft_in_match.group(2) or 0)\n            if ft_in_match.group(3) and ft_in_match.group(4):\n                inches += int(ft_in_match.group(3)) / int(ft_in_match.group(4))\n            return feet * 12 + inches, 'in'\n\n        # Metric with unit\n        metric_match = re.match(r\"(\\d+(?:\\.\\d+)?)\\s*(mm|cm|m)\", dim_text)\n        if metric_match:\n            return float(metric_match.group(1)), metric_match.group(2)\n\n        # Just a number\n        num_match = re.match(r\"(\\d+(?:\\.\\d+)?)\", dim_text)\n        if num_match:\n            return float(num_match.group(1)), ''\n\n        return 0, ''\n\n    def _extract_annotations(self, text: str) -> List[Annotation]:\n        \"\"\"Extract annotations from text.\"\"\"\n        annotations = []\n        lines = text.split('\\n')\n\n        for line in lines:\n            line = line.strip()\n            if not line:\n                continue\n\n            for annot_type, pattern in self.ANNOTATION_PATTERNS.items():\n                if re.match(pattern, line, re.IGNORECASE):\n                    annotations.append(Annotation(\n                        text=line,\n                        annotation_type=annot_type,\n                        location=(0, 0),\n                        references=[]\n                    ))\n                    break\n\n            # General notes\n            if line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):\n                annotations.append(Annotation(\n                    text=line,\n                    annotation_type='note',\n                    location=(0, 0),\n                    references=[]\n                ))\n\n        return annotations\n\n    def _extract_title_block(self, text: str) -> Optional[TitleBlockData]:\n        \"\"\"Extract title block information.\"\"\"\n        # Common title block patterns\n        patterns = {\n            'project_name': r'PROJECT(?:\\s*NAME)?:\\s*(.+?)(?:\\n|$)',\n            'project_number': r'(?:PROJECT\\s*)?(?:NO|NUMBER|#)\\.?:\\s*(\\S+)',\n            'sheet_number': r'SHEET(?:\\s*NO)?\\.?:\\s*([A-Z]?\\d+(?:\\.\\d+)?)',\n            'sheet_title': r'SHEET\\s*TITLE:\\s*(.+?)(?:\\n|$)',\n            'scale': r'SCALE:\\s*(.+?)(?:\\n|$)',\n            'date': r'DATE:\\s*(\\d{1,2}[/-]\\d{1,2}[/-]\\d{2,4})',\n            'revision': r'REV(?:ISION)?\\.?:\\s*(\\S+)',\n            'drawn_by': r'(?:DRAWN|DRN)\\s*(?:BY)?:\\s*(\\S+)',\n            'checked_by': r'(?:CHECKED|CHK)\\s*(?:BY)?:\\s*(\\S+)',\n        }\n\n        extracted = {}\n        for field, pattern in patterns.items():\n            match = re.search(pattern, text, re.IGNORECASE)\n            extracted[field] = match.group(1).strip() if match else ''\n\n        # Determine discipline from sheet number\n        sheet_num = extracted.get('sheet_number', '')\n        discipline = ''\n        if sheet_num:\n            prefix = sheet_num[0].upper() if sheet_num[0].isalpha() else ''\n            discipline_map = {\n                'A': 'Architectural', 'S': 'Structural', 'M': 'Mechanical',\n                'E': 'Electrical', 'P': 'Plumbing', 'C': 'Civil',\n                'L': 'Landscape', 'I': 'Interior', 'F': 'Fire Protection'\n            }\n            discipline = discipline_map.get(prefix, '')\n\n        return TitleBlockData(\n            project_name=extracted.get('project_name', ''),\n            project_number=extracted.get('project_number', ''),\n            sheet_number=sheet_num,\n            sheet_title=extracted.get('sheet_title', ''),\n            discipline=discipline,\n            scale=extracted.get('scale', ''),\n            date=extracted.get('date', ''),\n            revision=extracted.get('revision', ''),\n            drawn_by=extracted.get('drawn_by', ''),\n            checked_by=extracted.get('checked_by', ''),\n            approved_by=''\n        )\n\n    def _parse_schedule_table(self, table: List[List]) -> List[Symbol]:\n        \"\"\"Parse schedule table to extract symbols/elements.\"\"\"\n        symbols = []\n\n        if not table or len(table) < 2:\n            return symbols\n\n        # First row is usually headers\n        headers = [str(cell).lower() if cell else '' for cell in table[0]]\n\n        # Find key columns\n        tag_col = next((i for i, h in enumerate(headers) if 'tag' in h or 'mark' in h or 'no' in h), 0)\n        type_col = next((i for i, h in enumerate(headers) if 'type' in h or 'size' in h), -1)\n\n        for row in table[1:]:\n            if len(row) > tag_col and row[tag_col]:\n                tag = str(row[tag_col]).strip()\n                symbol_type = str(row[type_col]).strip() if type_col >= 0 and len(row) > type_col else ''\n\n                if tag:\n                    props = {}\n                    for i, header in enumerate(headers):\n                        if i < len(row) and row[i]:\n                            props[header] = str(row[i])\n\n                    symbols.append(Symbol(\n                        symbol_type=symbol_type or 'unknown',\n                        tag=tag,\n                        location=(0, 0),\n                        properties=props\n                    ))\n\n        return symbols\n\n    def _determine_scale(self, text: str) -> float:\n        \"\"\"Determine drawing scale factor.\"\"\"\n        for pattern in self.SCALE_PATTERNS:\n            match = re.search(pattern, text, re.IGNORECASE)\n            if match:\n                if 'NTS' in match.group(0).upper():\n                    return 0  # Not to scale\n\n                if '=' in match.group(0):\n                    # Imperial: 1/4\" = 1'-0\"\n                    return self._parse_imperial_scale(match.group(0))\n                else:\n                    # Metric: 1:100\n                    return 1 / float(match.group(2))\n\n        return 1.0  # Default\n\n    def _parse_imperial_scale(self, scale_text: str) -> float:\n        \"\"\"Parse imperial scale to factor.\"\"\"\n        match = re.search(r'(\\d+)(?:/(\\d+))?\\s*[\"\\']?\\s*=\\s*(\\d+)', scale_text)\n        if match:\n            numerator = float(match.group(1))\n            denominator = float(match.group(2)) if match.group(2) else 1\n            feet = float(match.group(3))\n            inches_per_foot = (numerator / denominator)\n            return inches_per_foot / (feet * 12)\n        return 1.0\n\n    def _check_drawing_quality(self, title_block: TitleBlockData,\n                                dimensions: List, annotations: List) -> List[str]:\n        \"\"\"Check drawing for quality issues.\"\"\"\n        issues = []\n\n        if title_block:\n            if not title_block.project_number:\n                issues.append(\"Missing project number in title block\")\n            if not title_block.sheet_number:\n                issues.append(\"Missing sheet number\")\n            if not title_block.scale:\n                issues.append(\"Missing scale indication\")\n            if not title_block.date:\n                issues.append(\"Missing date\")\n\n        if len(dimensions) == 0:\n            issues.append(\"No dimensions found - verify drawing content\")\n\n        # Check for typical construction notes\n        note_types = [a.annotation_type for a in annotations]\n        if 'note' not in note_types:\n            issues.append(\"No general notes found\")\n\n        return issues\n\n    def generate_drawing_index(self, results: List[DrawingAnalysisResult]) -> str:\n        \"\"\"Generate drawing index from multiple analyzed drawings.\"\"\"\n        lines = [\"# Drawing Index\", \"\"]\n        lines.append(\"| Sheet | Title | Discipline | Scale | Rev |\")\n        lines.append(\"|-------|-------|------------|-------|-----|\")\n\n        for result in sorted(results, key=lambda r: r.title_block.sheet_number if r.title_block else ''):\n            if result.title_block:\n                tb = result.title_block\n                lines.append(f\"| {tb.sheet_number} | {tb.sheet_title} | {tb.discipline} | {tb.scale} | {tb.revision} |\")\n\n        return \"\\n\".join(lines)\n\n    def generate_report(self, result: DrawingAnalysisResult) -> str:\n        \"\"\"Generate analysis report for a drawing.\"\"\"\n        lines = [\"# Drawing Analysis Report\", \"\"]\n        lines.append(f\"**File:** {result.file_name}\")\n\n        if result.title_block:\n            tb = result.title_block\n            lines.append(\"\")\n            lines.append(\"## Title Block\")\n            lines.append(f\"- **Project:** {tb.project_name}\")\n            lines.append(f\"- **Project No:** {tb.project_number}\")\n            lines.append(f\"- **Sheet:** {tb.sheet_number}\")\n            lines.append(f\"- **Title:** {tb.sheet_title}\")\n            lines.append(f\"- **Discipline:** {tb.discipline}\")\n            lines.append(f\"- **Scale:** {tb.scale}\")\n            lines.append(f\"- **Date:** {tb.date}\")\n            lines.append(f\"- **Revision:** {tb.revision}\")\n\n        lines.append(\"\")\n        lines.append(\"## Content Summary\")\n        lines.append(f\"- **Dimensions Found:** {len(result.dimensions)}\")\n        lines.append(f\"- **Annotations Found:** {len(result.annotations)}\")\n        lines.append(f\"- **Symbols/Elements:** {len(result.symbols)}\")\n\n        if result.quality_issues:\n            lines.append(\"\")\n            lines.append(\"## Quality Issues\")\n            for issue in result.quality_issues:\n                lines.append(f\"- ⚠️ {issue}\")\n\n        if result.symbols:\n            lines.append(\"\")\n            lines.append(\"## Elements Found\")\n            for symbol in result.symbols[:20]:\n                lines.append(f\"- {symbol.tag}: {symbol.symbol_type}\")\n\n        return \"\\n\".join(lines)\n\nQuick Start\n# Initialize analyzer\nanalyzer = DrawingAnalyzer()\n\n# Analyze a drawing\nresult = analyzer.analyze_pdf_drawing(\"A101_Floor_Plan.pdf\")\n\n# Check title block\nif result.title_block:\n    print(f\"Sheet: {result.title_block.sheet_number}\")\n    print(f\"Title: {result.title_block.sheet_title}\")\n    print(f\"Scale: {result.title_block.scale}\")\n\n# Review extracted data\nprint(f\"Dimensions: {len(result.dimensions)}\")\nprint(f\"Annotations: {len(result.annotations)}\")\nprint(f\"Symbols: {len(result.symbols)}\")\n\n# Check quality\nfor issue in result.quality_issues:\n    print(f\"Issue: {issue}\")\n\n# Generate report\nreport = analyzer.generate_report(result)\nprint(report)\n\nDependencies\npip install pdfplumber"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/datadrivenconstruction/drawing-analyzer",
    "publisherUrl": "https://clawhub.ai/datadrivenconstruction/drawing-analyzer",
    "owner": "datadrivenconstruction",
    "version": "2.1.0",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/drawing-analyzer",
    "downloadUrl": "https://openagent3.xyz/downloads/drawing-analyzer",
    "agentUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent",
    "manifestUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/drawing-analyzer/agent.md"
  }
}