<< All versions
Skill v1.0.0
currentAutomated scan100/100datadrivenconstruction/ddc_skills_for_ai_agents_in_construction/drawing-analyzer
──Details
PublishedSeptember 29, 2026 at 07:01 AM
Content Hashsha256:ca15ca72c52d8e9f...
Git SHAce45bbfbdd63
──Files
Files (1 file, 15.6 KB)
SKILL.md15.6 KBactive
SKILL.md · 457 lines · 15.6 KB
version: "1.0.0" 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
python
from dataclasses import dataclass, fieldfrom typing import List, Dict, Any, Optional, Tupleimport reimport pdfplumberfrom pathlib import Path@dataclassclass TitleBlockData:project_name: strproject_number: strsheet_number: strsheet_title: strdiscipline: strscale: strdate: strrevision: strdrawn_by: strchecked_by: strapproved_by: str@dataclassclass Dimension:value: floatunit: strdimension_type: str # linear, angular, radiallocation: Tuple[float, float]associated_text: str@dataclassclass Annotation:text: strannotation_type: str # note, callout, tag, keynotelocation: Tuple[float, float]references: List[str]@dataclassclass Symbol:symbol_type: str # door, window, equipment, etc.tag: strlocation: Tuple[float, float]properties: Dict[str, Any]@dataclassclass DrawingAnalysisResult:file_name: strtitle_block: Optional[TitleBlockData]dimensions: List[Dimension]annotations: List[Annotation]symbols: List[Symbol]scale_factor: floatdrawing_area: Tuple[float, float]quality_issues: List[str]class DrawingAnalyzer:"""Analyze construction drawings for data extraction."""# Common dimension patternsDIMENSION_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 unitr"(\d+'-\d+\")", # Compact feet-inchesr"(\d+)\s*(?:SF|LF|CY|EA)", # Quantity dimensions]# Common annotation patternsANNOTATION_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 patternsSCALE_PATTERNS = [r"SCALE:\s*(\d+(?:/\d+)?)\s*[\"']\s*=\s*(\d+)\s*['\-]", # 1/4" = 1'-0"r"(\d+):(\d+)", # 1:100r"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 texttext = page.extract_text() or ""all_text += text + "\n"# Extract dimensionspage_dims = self._extract_dimensions(text)dimensions.extend(page_dims)# Extract annotationspage_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 blocktitle_block = self._extract_title_block(all_text)# Determine scalescale_factor = self._determine_scale(all_text)# Quality checksquality_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 analysisquality_issues=quality_issues)self.results[path.name] = resultreturn resultdef _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 dimensionsdef _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 unitmetric_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 numbernum_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:continuefor 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 notesif line.startswith(('NOTE:', 'SEE ', 'REFER TO', 'TYP', 'U.N.O.')):annotations.append(Annotation(text=line,annotation_type='note',location=(0, 0),references=[]))return annotationsdef _extract_title_block(self, text: str) -> Optional[TitleBlockData]:"""Extract title block information."""# Common title block patternspatterns = {'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 numbersheet_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 headersheaders = [str(cell).lower() if cell else '' for cell in table[0]]# Find key columnstag_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 symbolsdef _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 scaleif '=' in match.group(0):# Imperial: 1/4" = 1'-0"return self._parse_imperial_scale(match.group(0))else:# Metric: 1:100return 1 / float(match.group(2))return 1.0 # Defaultdef _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 1feet = float(match.group(3))inches_per_foot = (numerator / denominator)return inches_per_foot / (feet * 12)return 1.0def _check_drawing_quality(self, title_block: TitleBlockData,dimensions: List, annotations: List) -> List[str]:"""Check drawing for quality issues."""issues = []if title_block:if not title_block.project_number:issues.append("Missing project number in title block")if not title_block.sheet_number:issues.append("Missing sheet number")if not title_block.scale:issues.append("Missing scale indication")if not title_block.date:issues.append("Missing date")if len(dimensions) == 0:issues.append("No dimensions found - verify drawing content")# Check for typical construction notesnote_types = [a.annotation_type for a in annotations]if 'note' not in note_types:issues.append("No general notes found")return issuesdef generate_drawing_index(self, results: List[DrawingAnalysisResult]) -> str:"""Generate drawing index from multiple analyzed drawings."""lines = ["# Drawing Index", ""]lines.append("| Sheet | Title | Discipline | Scale | Rev |")lines.append("|-------|-------|------------|-------|-----|")for result in sorted(results, key=lambda r: r.title_block.sheet_number if r.title_block else ''):if result.title_block:tb = result.title_blocklines.append(f"| {tb.sheet_number} | {tb.sheet_title} | {tb.discipline} | {tb.scale} | {tb.revision} |")return "\n".join(lines)def generate_report(self, result: DrawingAnalysisResult) -> str:"""Generate analysis report for a drawing."""lines = ["# Drawing Analysis Report", ""]lines.append(f"**File:** {result.file_name}")if result.title_block:tb = result.title_blocklines.append("")lines.append("## Title Block")lines.append(f"- **Project:** {tb.project_name}")lines.append(f"- **Project No:** {tb.project_number}")lines.append(f"- **Sheet:** {tb.sheet_number}")lines.append(f"- **Title:** {tb.sheet_title}")lines.append(f"- **Discipline:** {tb.discipline}")lines.append(f"- **Scale:** {tb.scale}")lines.append(f"- **Date:** {tb.date}")lines.append(f"- **Revision:** {tb.revision}")lines.append("")lines.append("## Content Summary")lines.append(f"- **Dimensions Found:** {len(result.dimensions)}")lines.append(f"- **Annotations Found:** {len(result.annotations)}")lines.append(f"- **Symbols/Elements:** {len(result.symbols)}")if result.quality_issues:lines.append("")lines.append("## Quality Issues")for issue in result.quality_issues:lines.append(f"- ⚠️ {issue}")if result.symbols:lines.append("")lines.append("## Elements Found")for symbol in result.symbols[:20]:lines.append(f"- {symbol.tag}: {symbol.symbol_type}")return "\n".join(lines)
Quick Start
python
# Initialize analyzeranalyzer = DrawingAnalyzer()# Analyze a drawingresult = analyzer.analyze_pdf_drawing("A101_Floor_Plan.pdf")# Check title blockif result.title_block:print(f"Sheet: {result.title_block.sheet_number}")print(f"Title: {result.title_block.sheet_title}")print(f"Scale: {result.title_block.scale}")# Review extracted dataprint(f"Dimensions: {len(result.dimensions)}")print(f"Annotations: {len(result.annotations)}")print(f"Symbols: {len(result.symbols)}")# Check qualityfor issue in result.quality_issues:print(f"Issue: {issue}")# Generate reportreport = analyzer.generate_report(result)print(report)
Dependencies
bash
pip install pdfplumber