| name | viewport-highlighter |
| description | Identify and highlight viewports on construction drawing sheets using vision. Detects view boundaries, titles, scales, and view types. Creates viewport overlays via AgentCM API. Requires AgentCM (.construction/ directory). Triggers: 'highlight viewports', 'find views'.
|
Viewport Highlighter
Purpose
Automate viewport segmentation of construction drawing sheets. Each sheet
typically contains 1-12+ distinct views (floor plans, sections, elevations,
details, schedules). This skill identifies all views, determines their
boundaries, extracts metadata (title, detail number, scale, type), and
creates viewport highlights — producing the same result as a user manually
drawing and labeling each viewport in the UI.
Does NOT: modify existing viewports, delete viewports, or change element
data. Only creates new viewport highlights and populates their metadata.
Step 0: Verify AgentCM Mode
This skill requires AgentCM. It writes viewport overlays through the AgentCM REST API and has no standalone output path.
Check for .construction/ directory at the project root.
If .construction/ is absent, stop immediately and tell the user:
"viewport-highlighter requires an AgentCM project — .construction/ directory not found. This skill submits viewport overlays through the AgentCM API and cannot operate without it. Open this project in AgentCM first, then re-run."
If .construction/ exists:
- Read
.construction/CLAUDE.md for project context
- Read
.construction/database.yaml for query_command, project_id, api_url
- Read
.construction/index/sheet_index.yaml for sheet inventory
- Sheet images at
.construction/rasters/{sheet_number}.png
- OCR data queryable via
extracted_items table in PostgreSQL
- Write viewports via REST API
Step 1: User Scopes the Task
User provides sheet scope:
- Specific sheets: "A2.01, A2.02, A5.01"
- By discipline: "all architectural sheets"
- All sheets: "every sheet in the set"
Optional: user can specify which view types to look for (e.g., "only floor
plans and sections"). Default: identify ALL views on each sheet.
Before proceeding: Confirm the sheet list and any filters with the user.
Show the count of sheets to process.
Step 2: Vision — Identify View Boundaries
For each sheet in scope, rasterize to PNG (if not already available) and
examine with vision.
${CLAUDE_SKILL_DIR}/../../bin/construction-python \
${CLAUDE_SKILL_DIR}/../../scripts/pdf/rasterize_page.py \
"{pdf_path}" {page_index} --dpi 200 --output /tmp/{sheet_number}.png
Vision task: Examine the full sheet image. Identify every distinct view
(drawing area) on the sheet. For each view, extract:
| Field | What to look for | Example |
|---|
| Title | View title bar text (usually centered below the view) | "FIRST FLOOR PLAN" |
| Detail Number | Number in the detail bubble or title bar | "1", "A2.01", "3/A5" |
| Scale | Scale annotation near the title bar | 1/8" = 1'-0", 1/4" = 1'-0", "NTS" |
| View Type | Classification of the drawing content | plan, section, elevation, detail, schedule |
| Bounding Region | Approximate rectangle enclosing the entire view (normalized 0-1) | {x: 0.02, y: 0.05, w: 0.48, h: 0.65} |
View boundary detection signals
Use these visual cues to determine where one view ends and another begins:
- Heavy border lines — thick lines separating drawing areas
- Title bars — horizontal bars with view name, scale, detail number
- Detail bubbles — circles or hexagons with detail/sheet reference
- Whitespace gaps — clear separations between drawing content
- Grid systems — column/row grids define the extents of a plan view
- Section cut lines — long dash-dot lines with directional arrows
- Match lines — indicate where a plan continues on another sheet
View type classification
| View Type | extractionScope value | Signals |
|---|
| Floor plan | plan | Grid lines, room names/numbers, dimension strings, north arrow |
| Enlarged plan | plan | "ENLARGED" in title, larger scale than base plan, room detail |
| Section | section | Section cut reference (e.g., "SECTION A-A"), vertical layers, material hatching |
| Elevation | elevation | "ELEVATION" in title, facade view, material callouts, floor lines |
| Detail | detail | Detail bubble reference, large scale (3"=1'-0"), construction assembly closeup |
| Schedule | schedule | Tabular grid, column headers, row data (door schedule, finish schedule) |
| Diagram | other | Riser diagrams, single-line diagrams, flow diagrams |
Bounding region estimation from vision
When estimating the bounding region as normalized 0-1 coordinates:
- x = left edge of view content / sheet width (0.0 = left edge)
- y = top edge of view content / sheet height (0.0 = top edge)
- width = view content width / sheet width
- height = view content height / sheet height (include title bar)
Include the title bar and any associated notes/legends within the view.
Exclude the sheet title block (typically bottom-right corner).
Margins: Add ~1% padding on each side to avoid clipping content.
Multi-view sheet layout patterns
Common construction sheet layouts to expect:
- Single view: One large plan fills most of the sheet
- 2-up vertical: Two views stacked (e.g., floor plan top, reflected ceiling bottom)
- 2-up horizontal: Two views side by side (e.g., two elevations)
- Grid layout: 4-6 details arranged in a 2x3 or 3x2 grid
- Mixed: Large plan on left, 2-3 sections/details stacked on right
- Schedule + details: Schedule table in upper portion, details below
Step 3: OCR Anchor Verification
For each vision-identified view, verify and refine metadata using OCR data.
Title verification
Search for the view title text in extracted_items:
{query_command} -c "SELECT id, text, x_min, y_min, x_max, y_max
FROM extracted_items
WHERE sheet_id = '{sheet_id}'
AND text ILIKE '%{distinctive_title_word}%'
ORDER BY y_max DESC"
The title bar is typically near the bottom of the view content area.
Use the title's y-coordinate to refine the view's bottom boundary.
Scale verification
Search for scale text near the title:
{query_command} -c "SELECT id, text, x_min, y_min, x_max, y_max
FROM extracted_items
WHERE sheet_id = '{sheet_id}'
AND text ILIKE '%SCALE%'
AND y_min BETWEEN {title_y - 0.02} AND {title_y + 0.02}
AND x_min BETWEEN {title_x - 0.15} AND {title_x + 0.15}"
Common scale text patterns:
SCALE: 1/8" = 1'-0"
1/4" = 1'-0"
SCALE: NTS (not to scale)
3" = 1'-0"
Detail number verification
Search for detail number in title bar area or detail bubbles:
{query_command} -c "SELECT id, text, x_min, y_min, x_max, y_max
FROM extracted_items
WHERE sheet_id = '{sheet_id}'
AND y_min BETWEEN {title_y - 0.02} AND {title_y + 0.02}
AND x_min BETWEEN {title_x - 0.10} AND {title_x + 0.10}
AND text ~ '^[0-9A-Z]'"
Detail number formats: 1, 2, A, A2.01, 3/A5, 1/A5.01.
Step 4: Bounding Region Refinement
Refine vision-estimated boundaries using OCR element positions.
Query all extracted items within and near the estimated viewport area:
{query_command} -c "SELECT x_min, y_min, x_max, y_max
FROM extracted_items
WHERE sheet_id = '{sheet_id}'
AND (x_min + x_max) / 2 BETWEEN {est_x - 0.02} AND {est_x + est_w + 0.02}
AND (y_min + y_max) / 2 BETWEEN {est_y - 0.02} AND {est_y + est_h + 0.02}"
Use the extremes of contained items + 1% padding to set the final bounds.
Overlap prevention
After refining all viewports on a sheet, check for overlaps:
- If two viewports overlap, shrink the boundary of the one with fewer
contained elements at the overlapping edge
- Adjacent viewports should have a gap of 0.5-2% of sheet dimension
Validation checklist
Before creating each viewport, verify:
Step 5: Submit Viewports for Review
Submit viewport suggestions
Submit all viewports for a sheet as pending suggestions via the
viewport suggestion ingest endpoint. The user reviews and approves them
in the Group Review Gallery before they become live viewports.
curl -s --fail-with-body -X POST "{api_url}/projects/{project_id}/viewport-suggestions/ingest" \
-H "Content-Type: application/json" \
-d '{
"sheet_id": "{sheet_id}",
"viewports": [
{
"title": "FIRST FLOOR PLAN",
"detail_number": "1",
"scale_text": "1/8\" = 1'"'"'-0\"",
"extraction_scope": "plan",
"bounding_region": {"x": 0.02, "y": 0.05, "width": 0.48, "height": 0.65},
"confidence": 0.85,
"element_ids": []
},
{
"title": "BUILDING SECTION A-A",
"detail_number": "A",
"scale_text": "1/4\" = 1'"'"'-0\"",
"extraction_scope": "section",
"bounding_region": {"x": 0.52, "y": 0.05, "width": 0.46, "height": 0.45},
"confidence": 0.80,
"element_ids": []
}
]
}'
Response: Returns suggestion_ids for the created pending cards.
Processing order: Submit all viewports for one sheet in a single
POST. The system creates group_suggestions rows with status: pending
and proposedType: viewport_highlight. Crop images are generated when
the user opens the Group Review Gallery.
Confidence scoring:
- 0.90+ = title, scale, and detail number all OCR-verified
- 0.70–0.89 = vision-identified with partial OCR verification
- 0.50–0.69 = vision-only, no OCR anchors found
Review flow
Viewport suggestions appear in the Group Review Gallery as cards
with cropped raster previews. The user can:
- Edit title, detail number, scale text, and view type
- Adjust bounding region (future: via canvas interaction)
- Accept → promotes to a live viewport in
graph_views with containment
rebuild and crop generation
- Reject → suggestion archived, no viewport created
Title formatting conventions
When setting viewport titles, follow these conventions:
- Use the exact title text from the drawing (preserve case)
- If the title includes the sheet number reference (e.g., "1/A5.01"),
put only the detail number portion in
detail_number and the full
title in title
- Common abbreviations to preserve: "FLR", "CLG", "ELEV", "TYP"
Step 6: Verification & Markup
Sheet markup (REQUIRED for all modes)
Mark up each processed sheet with viewport boundary rectangles to show
exactly what was identified.
${CLAUDE_SKILL_DIR}/../../bin/construction-python \
${CLAUDE_SKILL_DIR}/scripts/markup_viewports.py \
--base "{sheet_image_path}" \
--items "{items_json_path}" \
--output "{output_path}" \
--color "amber" \
--label-style "titled"
Items JSON format (pixel coordinates):
[
{
"x": 100, "y": 200,
"width": 4000, "height": 3200,
"shape": "rect",
"label": "1 - FIRST FLOOR PLAN (plan)"
},
{
"x": 4200, "y": 200,
"width": 3800, "height": 1500,
"shape": "rect",
"label": "A - BUILDING SECTION (section)"
}
]
Verification report
After processing all sheets, produce a summary:
VIEWPORT HIGHLIGHTING SUMMARY
==============================
Sheets processed: 12
Total viewports created: 47
Sheet | Views | Types
-----------|-------|------------------
A2.01 | 3 | plan, section, section
A2.02 | 2 | plan, plan
A5.01 | 8 | detail (x8)
A5.02 | 6 | detail (x4), section (x2)
...
Element containment:
A2.01 / FIRST FLOOR PLAN: 342 elements
A2.01 / BUILDING SECTION A: 87 elements
...
Vision verification (recommended)
After creating all viewports on a sheet, re-examine the marked-up image
to verify:
- All views on the sheet are captured (no missing viewports)
- Bounding regions accurately encompass view content
- No significant overlap between viewports
- Title, detail number, and scale text are correct
If issues are found, PATCH the viewport to correct:
curl -s --fail-with-body -X PATCH "{api_url}/projects/{project_id}/viewports/{viewport_id}" \
-H "Content-Type: application/json" \
-d '{"title": "CORRECTED TITLE", "boundingRegion": {...}}'
Common Pitfalls
-
Title block is not a viewport. Every sheet has a title block
(bottom-right, ~15% of sheet). Do NOT create a viewport for it.
-
Revision blocks are not viewports. Revision history areas
(right edge) should be excluded.
-
Key plans are not primary viewports. Small orientation diagrams
showing which area of the building is depicted — skip these unless
the user specifically requests them.
-
Matchline continuation. When a floor plan spans two sheets
(A2.01 and A2.02 split at a matchline), each sheet gets its own
viewport — they are separate views even though they show one plan.
-
Schedule views. Tabular schedules (door schedule, finish schedule)
ARE viewports. Set extractionScope: "schedule".
-
North arrows and legends. Include these within the parent view's
bounding region, not as separate viewports.
-
Overlapping views. Some sheets show plans with section cuts and
enlarged areas overlaid. The base plan is the viewport — don't create
separate viewports for the section cut lines themselves.
-
Scale = "AS NOTED" or "NTS". Some sheets have multiple scales
or no scale. Set scaleText to the literal text found.
API Reference
| Method | Endpoint | Purpose |
|---|
| POST | /api/projects/{id}/viewport-suggestions/ingest | Primary — submit viewport suggestions for review |
| GET | /api/projects/{id}/sheets/{sheetId}/viewports | List existing (promoted) viewports |
| POST | /api/projects/{id}/sheets/{sheetId}/viewports | Direct create (manual/bypass review) |
| PATCH | /api/projects/{id}/viewports/{viewportId} | Update metadata (title, bounds, scale, scope) |
| GET | /api/projects/{id}/viewports/{viewportId}/elements | Get contained element IDs + count |
| GET | /api/projects/{id}/viewports/{viewportId}/crop | Fetch auto-generated raster crop PNG |
| DELETE | /api/projects/{id}/viewports/{viewportId} | Remove viewport (use only if user requests) |
ViewportSuggestionIngestBody (primary endpoint)
{
"sheet_id": "uuid",
"viewports": [
{
"title": "string (required)",
"detail_number": "string (optional)",
"scale_text": "string (optional)",
"extraction_scope": "plan|section|elevation|detail|schedule|other (optional)",
"bounding_region": { "x": 0.0, "y": 0.0, "width": 0.5, "height": 0.5 },
"confidence": 0.85,
"element_ids": ["optional array of extracted_item IDs within viewport"]
}
]
}
CreateViewportBody (direct create — bypass review)
{
"boundingRegion": { "x": 0.0, "y": 0.0, "width": 0.5, "height": 0.5 },
"title": "string (required)",
"detailNumber": "string (optional)",
"extractionScope": "plan|section|elevation|detail|schedule|other (optional)"
}
GraphViewport (response)
{
"id": "uuid",
"sheetId": "uuid",
"title": "FIRST FLOOR PLAN",
"detailNumber": "1",
"scaleText": "1/8\" = 1'-0\"",
"boundingRegion": { "x": 0.02, "y": 0.05, "width": 0.48, "height": 0.65 },
"centroid": [0.26, 0.375],
"bboxSource": "user",
"elementCount": 342,
"extractionScope": "plan",
"userCreated": true,
"rasterStorageKey": "{projectId}/viewports/{viewportId}.png",
"createdAt": "2026-04-06T18:30:00Z",
"updatedAt": "2026-04-06T18:30:00Z"
}
All coordinates are normalized 0-1. Origin is top-left. Multiply by
image pixel dimensions to convert to pixel coordinates for markup.
Idempotency
Before submitting viewport suggestions for a sheet, check for existing
viewports (already promoted/live):
existing=$(curl -s --fail-with-body "{api_url}/projects/{project_id}/sheets/{sheet_id}/viewports")
If viewports already exist on the sheet:
- Report them to the user
- Ask whether to skip the sheet, replace existing viewports, or add alongside
- If replacing: DELETE each existing viewport first, then submit new suggestions
- Never silently duplicate viewports
Pending suggestions (not yet reviewed) can be safely re-submitted — the
ingest endpoint creates new suggestion rows each time. The user resolves
duplicates during review in the Group Review Gallery.
Allowed Scripts
Allowed scripts — exhaustive list. Only execute these scripts during this skill:
../../scripts/pdf/rasterize_page.py — rasterize a sheet PDF page to PNG for vision
scripts/markup_viewports.py — overlay viewport boundary rectangles on a sheet image