SkillAgentSearch skills...

drawing-analyzer

Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation.

Install / Use

npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill drawing-analyzer

Installs into whichever agent you are using.

About this skill
📄

SKILL.md

Installable skill definition

Quality Score

89/100

Category

Automation

Supported Platforms

Universal

Our assessment of drawing-analyzer

drawing-analyzer scores 89/100 on our quality scale, 1474th of 2,863 Automation skills we index.

Its SKILL.md is 16 KB long, well organised into 12 sections with 3 code examples: a thorough specification that gives an agent plenty to work with.

It has 333 GitHub stars, a meaningful sign that others use it.

Substance
30/30
Structure
18/20
Description
15/15
Adoption
11/20
Freshness
15/15

Maintenance, license and trust

  • The repository was last updated about 2 months ago, so drawing-analyzer is actively maintained.
  • It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
  • Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.

drawing-analyzer compared with similar skills

All 4 of these similar skills score higher than drawing-analyzer; compare them before choosing.

SkillScoreStarsUpdatedFormat
drawing-analyzer (this skill)by datadrivenconstruction893332mo agoSKILL.md
Agent-Reachby Panniantong100106.4k5d agoCLAUDE.md
taste-skillby Leonxlnx100105.7ktodayCLAUDE.md
headroomby headroomlabs-ai10078.3ktodayCLAUDE.md
Scraplingby D4Vinci10090.4ktodayMCP Server

Frequently asked questions

How do I install drawing-analyzer?
Run npx skills add datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill drawing-analyzer. The install tabs above show the steps for each supported agent.
Which AI agents does drawing-analyzer work with?
It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
Is drawing-analyzer safe to use?
It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
Is drawing-analyzer still maintained?
The repository was last updated about 2 months ago, so drawing-analyzer is actively maintained.

name: "drawing-analyzer" description: "Analyze construction drawings to extract dimensions, annotations, symbols, and metadata. Support quantity takeoff and design review automation." homepage: "https://datadrivenconstruction.io" metadata: {"openclaw": {"emoji": "📑", "os": ["darwin", "linux", "win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"]}}}

Drawing Analyzer for Construction

Overview

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

Business Case

Drawing analysis automation enables:

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

Technical Implementation

from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional, Tuple
import re
import pdfplumber
from pathlib import Path

@dataclass
class TitleBlockData:
    project_name: str
    project_number: str
    sheet_number: str
    sheet_title: str
    discipline: str
    scale: str
    date: str
    revision: str
    drawn_by: str
    checked_by: str
    approved_by: str

@dataclass
class Dimension:
    value: float
    unit: str
    dimension_type: str  # linear, angular, radial
    location: Tuple[float, float]
    associated_text: str

@dataclass
class Annotation:
    text: str
    annotation_type: str  # note, callout, tag, keynote
    location: Tuple[float, float]
    references: List[str]

@dataclass
class Symbol:
    symbol_type: str  # door, window, equipment, etc.
    tag: str
    location: Tuple[float, float]
    properties: Dict[str, Any]

@dataclass
class DrawingAnalysisResult:
    file_name: str
    title_block: Optional[TitleBlockData]
    dimensions: List[Dimension]
    annotations: List[Annotation]
    symbols: List[Symbol]
    scale_factor: float
    drawing_area: Tuple[float, float]
    quality_issues: List[str]

class DrawingAnalyzer:
    """Analyze construction drawings for data extraction."""

    # Common dimension patterns
    DIMENSION_PATTERNS = [
        r"(\d+'-\s*\d+(?:\s*\d+/\d+)?\"?)",  # Feet-inches: 10'-6", 10' - 6 1/2"
        r"(\d+(?:\.\d+)?)\s*(?:mm|cm|m|ft|in)",  # Metric/imperial with unit
        r"(\d+'-\d+\")",  # Compact feet-inches
        r"(\d+)\s*(?:SF|LF|CY|EA)",  # Quantity dimensions
    ]

    # Common annotation patterns
    ANNOTATION_PATTERNS = {
        'keynote': r'^\d{1,2}[A-Z]?$',  # 1A, 12, 5B
        'room_tag': r'^(?:RM|ROOM)\s*\d+',
        'door_tag': r'^[A-Z]?\d{2,3}[A-Z]?$',
        'grid_line': r'^[A-Z]$|^\d+$',
        'elevation': r'^(?:EL|ELEV)\.?\s*\d+',
        'detail_ref': r'^\d+/[A-Z]\d+',
    }

    # Scale patterns
    SCALE_PATTERNS = [
        r"SCALE:\s*(\d+(?:/\d+)?)\s*[\"']\s*=\s*(\d+)\s*['\-]",  # 1/4" = 1'-0"
        r"(\d+):(\d+)",  # 1:100
        r"NTS|NOT TO SCALE",
    ]

    def __init__(self):
        self.results: Dict[str, DrawingAnalysisResult] = {}

    def analyze_pdf_drawing(self, pdf_path: str) -> DrawingAnalysisResult:
        """Analyze a PDF drawing."""
        path = Path(pdf_path)

        all_text = ""
        dimensions = []
        annotations = []
        symbols = []
        quality_issues = []

        with pdfplumber.open(pdf_path) as pdf:
            for page in pdf.pages:
                # Extract text
                text = page.extract_text() or ""
                all_text += text + "\n"

                # Extract dimensions
                page_dims = self._extract_dimensions(text)
                dimensions.extend(page_dims)

                # Extract annotations
                page_annots = self._extract_annotations(text)
                annotations.extend(page_annots)

                # Extract from tables (often contain schedules)
                tables = page.extract_tables()
                for table in tables:
                    symbols.extend(self._parse_schedule_table(table))

        # Parse title block
        title_block = self._extract_title_block(all_text)

        # Determine scale
        scale_factor = self._determine_scale(all_text)

        # Quality checks
        quality_issues = self._check_drawing_quality(
            title_block, dimensions, annotations
        )

        result = DrawingAnalysisResult(
            file_name=path.name,
            title_block=title_block,
            dimensions=dimensions,
            annotations=annotations,
            symbols=symbols,
            scale_factor=scale_factor,
            drawing_area=(0, 0),  # Would need image analysis
            quality_issues=quality_issues
        )

        self.results[path.name] = result
        return result

    def _extract_dimensions(self, text: str) -> List[Dimension]:
        """Extract dimensions from text."""
        dimensions = []

        for pattern in self.DIMENSION_PATTERNS:
            matches = re.findall(pattern, text)
            for match in matches:
                value, unit = self._parse_dimension_value(match)
                if value > 0:
                    dimensions.append(Dimension(
                        value=value,
                        unit=unit,
                        dimension_type='linear',
                        location=(0, 0),
                        associated_text=match
                    ))

        return dimensions

    def _parse_dimension_value(self, dim_text: str) -> Tuple[float, str]:
        """Parse dimension text to value and unit."""
        dim_text = dim_text.strip()

        # Feet and inches: 10'-6"
        ft_in_match = re.match(r"(\d+)'[-\s]*(\d+)?(?:\s*(\d+)/(\d+))?\"?", dim_text)
        if ft_in_match:
            feet = int(ft_in_match.group(1))
            inches = int(ft_in_match.group(2) or 0)
            if ft_in_match.group(3) and ft_in_match.group(4):
                inches += int(ft_in_match.group(3)) / int(ft_in_match.group(4))
            return feet * 12 + inches, 'in'

        # Metric with unit
        metric_match = re.match(r"(\d+(?:\.\d+)?)\s*(mm|cm|m)", dim_text)
        if metric_match:
            return float(metric_match.group(1)), metric_match.group(2)

        # Just a number
        num_match = re.match(r"(\d+(?:\.\d+)?)", dim_text)
        if num_match:
            return float(num_match.group(1)), ''

        return 0, ''

    def _extract_annotations(self, text: str) -> List[Annotation]:
        """Extract annotations from text."""
        annotations = []
        lines = text.split('\n')

        for line in lines:
            line = line.strip()
            if not line:
                continue

            for annot_type, pattern in self.ANNOTATION_PATTERNS.items():
                if re.match(pattern, line, re.IGNORECASE):
                    annotations.append(Annotation(
                        text=line,
                        annotation_type=annot_type,
                        location=(0, 0),
                        references=[]
                    ))
                    break

            # General notes
            if line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):
                annotations.append(Annotation(
                    text=line,
                    annotation_type='note',
                    location=(0, 0),
                    references=[]
                ))

        return annotations

    def _extract_title_block(self, text: str) -> Optional[TitleBlockData]:
        """Extract title block information."""
        # Common title block patterns
        patterns = {
            'project_name': r'PROJECT(?:\s*NAME)?:\s*(.+?)(?:\n|$)',
            'project_number': r'(?:PROJECT\s*)?(?:NO|NUMBER|#)\.?:\s*(\S+)',
            'sheet_number': r'SHEET(?:\s*NO)?\.?:\s*([A-Z]?\d+(?:\.\d+)?)',
            'sheet_title': r'SHEET\s*TITLE:\s*(.+?)(?:\n|$)',
            'scale': r'SCALE:\s*(.+?)(?:\n|$)',
            'date': r'DATE:\s*(\d{1,2}[/-]\d{1,2}[/-]\d{2,4})',
            'revision': r'REV(?:ISION)?\.?:\s*(\S+)',
            'drawn_by': r'(?:DRAWN|DRN)\s*(?:BY)?:\s*(\S+)',
            'checked_by': r'(?:CHECKED|CHK)\s*(?:BY)?:\s*(\S+)',
        }

        extracted = {}
        for field, pattern in patterns.items():
            match = re.search(pattern, text, re.IGNORECASE)
            extracted[field] = match.group(1).strip() if match else ''

        # Determine discipline from sheet number
        sheet_num = extracted.get('sheet_number', '')
        discipline = ''
        if sheet_num:
            prefix = sheet_num[0].upper() if sheet_num[0].isalpha() else ''
            discipline_map = {
                'A': 'Architectural', 'S': 'Structural', 'M': 'Mechanical',
                'E': 'Electrical', 'P': 'Plumbing', 'C': 'Civil',
                'L': 'Landscape', 'I': 'Interior', 'F': 'Fire Protection'
            }
            discipline = discipline_map.get(prefix, '')

        return TitleBlockData(
            project_name=extracted.get('project_name', ''),
            project_number=extracted.get('project_number', ''),
            sheet_number=sheet_num,
            sheet_title=extracted.get('sheet_title', ''),
            discipline=discipline,
            scale=extracted.get('scale', ''),
            date=extracted.get('date', ''),
            revision=extracted.get('revision', ''),
            drawn_by=extracted.get('drawn_by', ''),
            checked_by=extracted.get('checked_by', ''),
            approved_by=''
        )

    def _parse_schedule_table(self, table: List[List]) -> List[Symbol]:
        """Parse schedule table to extract symbols/elements."""
        symbols = []

        if not table or len(table) < 2:
            return symbols

        # First row is usually headers
        headers = [str(cell).lower() if cell else '' for cell in table[0]]

        # Find key columns
        tag_col = next((i for i, h in enumerate(headers) if 'tag' in h or 'mark' in h or 'no' in h), 0)
        type_col = next((i for i, h in enumerate(headers) if 'type' in h or 'size' in h), -1)

        for row in table[1:]:
            if len(row) > tag_col and row[tag_col]:
                tag = str(row[tag_col]).strip()
                symbol_type = str(row[type_col]).strip() if type_col >= 0 and len(row) > type_col else ''

                if tag:
                    props = {}
                    for i, header in enumerate(headers):
                        if i < len(row) and row[i]:
                            props[header] = str(row[i])

                    symbols.append(Symbol(
                        symbol_type=symbol_type or 'unknown',
                        tag=tag,
                        location=(0, 0),
                        properties=props
                    ))

        return symbols

    def _determine_scale(self, text: str) -> float:
        """Determine drawing scale factor."""
        for pattern in self.SCALE_PATTERNS:
            match = re.search(pattern, text, re.IGNORECASE)
            if match:
                if 'NTS' in match.group(0).upper():
                    return 0  # Not to scale

                if '=' in match.group(0):
                    # Imperial: 1/4" = 1'-0"
                    return self._parse_imperial_scale(match.group(0))
                else:
                    # Metric: 1:100
                    return 1 / float(match.group(2))

        return 1.0  # Default

    def _parse_imperial_scale(self, scale_text: str) -> float:
        """Parse imperial scale to factor."""
        match = re.search(r'(\d+)(?:/(\d+))?\s*["\']?\s*=\s*(\d+)', scale_text)
        if match:
            numerator = float(match.group(1))
            denominator = float(match.group(2)) if match.group(2) else 1
            feet = float(match.group(3))
            inches_per_foot = (numerator / denominator)
            return inches_per_foot / (feet * 12)
        return 1.0

 

Truncated for display — read the full file on GitHub.

Related Skills

View on GitHub
GitHub Stars333
CategoryAutomation
Updated2mo ago
Forks82

Languages

Python

Trust signals

100/100

From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.

No cautions