Skills-for-architects zoning-envelope
Generate interactive 3D zoning envelope viewers from zoning analysis reports. Requires a zoning analysis report as input.
git clone https://github.com/AlpacaLabsLLC/skills-for-architects
T=$(mktemp -d) && git clone --depth=1 https://github.com/AlpacaLabsLLC/skills-for-architects "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/02-zoning-analysis/skills/zoning-envelope" ~/.claude/skills/alpacalabsllc-skills-for-architects-zoning-envelope && rm -rf "$T"
plugins/02-zoning-analysis/skills/zoning-envelope/SKILL.md/zoning-envelope — 3D Zoning Envelope Viewer
Generate an interactive 3D axonometric zoning envelope viewer as a self-contained HTML file. Uses Three.js with OrbitControls — opens in any browser, no dependencies.
Requires a zoning analysis report generated by
/zoning-analysis-nyc or /zoning-analysis-uruguay. This skill is a renderer — it does not perform zoning calculations.
Usage
/zoning-envelope path/to/zoning-analysis.md /zoning-envelope padron 350 /zoning-envelope 250 hudson /zoning-envelope
Step 1: Find the Report
If a .md
path is provided
.mdRead the file directly.
If a search term is provided (address, padrón number, etc.)
Search for matching zoning analysis reports in these locations:
(Uruguay)./
(NYC)./- Current working directory
Use Glob + Grep to find reports matching the search term. If multiple matches, show the options and ask the user to pick one.
If no argument is provided
Search for the most recently modified
zoning-analysis-*.md or padron-*.md file in the report directories. If found, confirm with the user. If not found, tell the user:
No zoning analysis report found. Run
or/zoning-analysis-nycfirst, then come back with/zoning-analysis-uruguay./zoning-envelope
If no Envelope Data block is found
If the report exists but lacks the
## Envelope Data JSON block (older report format), tell the user:
This report was generated before the Envelope Data format was added. Re-run the zoning analysis to get an updated report, or I can attempt to parse the tables (results may be approximate).
Step 2: Parse Envelope Data
Read the
## Envelope Data JSON block from the report — a fenced code block containing:
{ "lot_poly": [[x, y], ...], "unit": "ft" | "m", "setbacks": { "front": 6, "rear": 3, "lateral1": 3, "lateral2": 2 }, "volumes": [ { "type": "base", "inset": 20, "h_bottom": 0, "h_top": 85, "label": "base" }, { "type": "tower", "inset": 10, "h_bottom": 85, "h_top": 290, "label": "tower" } ], "height_cap": 290, "info": { "title": "...", "zone": "...", "id": "...", "area": "..." }, "stats": { "key": "value", ... }, "scenarios": null }
Step 3: Normalize to Envelope Model
From the parsed JSON, build the internal model:
— the lot boundary polygon in local unitsLOT_POLY
— "ft" or "m"UNIT
— array of volumes to extrude, each with inset distance, height range, labelVOLUMES
— max height for the amber cap planeHEIGHT_CAP
— title, zone, id, area for the overlay panelsINFO
— key/value pairs for the parameters panelSTATS
— if present, multi-scenario toggle dataSCENARIOS
Compute inset polygons using the
insetPolygon(poly, distance) function. For multi-volume envelopes (base + tower), compute the tower inset from the base inset (cumulative), not from the lot polygon — so the tower is always smaller than the base.
Compute volumes by extruding inset polygons between height intervals.
Step 3: Generate HTML
Build a self-contained HTML file following the design system below.
Design System
| Element | Color | Opacity |
|---|---|---|
| Background | | 1.0 |
| Ground (lot) | | 0.5 |
| Lot boundary | | 1.0 |
| Setback zones | | 0.2 |
| Base volume faces | | 0.08–0.10 |
| Base volume edges | | 0.30–0.35 |
| Tower/upper volume | | 0.05 |
| Height cap / sky plane | | 0.10–0.15 |
| Labels | | 1.0 (canvas sprites) |
| Grid | | 0.15 |
Typography: Helvetica Neue, 11px for overlay panels, canvas sprites for 3D labels.
Layout:
- Top-left: Title + address/zone
- Top-right: Parameters panel (stats)
- Bottom-left: Color legend
- Bottom-right: Controls hint
All materials:
transparent: true, depthWrite: false, side: DoubleSide.
CDN Import Map
<script type="importmap"> { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/" } } </script>
Required Utility Functions
Include these in every generated HTML:
— Returns signed area. Positive = CCW winding.signedArea(poly)
— Shrinks polygon inward by distance insetPolygon(poly, d)
d along edge-normal bisectors. Each vertex moves along the bisector of its two adjacent edge normals, with distance adjusted for the bisector angle. CRITICAL: The normal direction depends on polygon winding, which varies by data source (WGS84 vs EPSG:3857 produce opposite windings). The function MUST self-correct: after computing the inset, compare abs(signedArea(result)) against abs(signedArea(poly)). If the result is LARGER, negate the offset direction and recompute. This makes the function robust regardless of input winding.
— Ear-clipping triangulation for arbitrary simple polygons. Returns index array for triangulate(poly)
BufferGeometry.setIndex().
— Returns a extrudePolygon(poly, hBottom, hTop, color, opacity)
THREE.Group containing:
with triangulated top/bottom faces + side quadsBufferGeometry- Wireframe edges: top ring, bottom ring, vertical edges at each vertex
— Triangulated flat polygon at a given Y height.groundPolygon(poly, color, opacity, yOffset)
— Returns centroid(poly)
[cx, cz] for camera targeting and label placement.
— Creates a createTextSprite(text, options)
THREE.Sprite with canvas-rendered text. Options: fontSize, color, bgColor.
Scene Setup
renderer = WebGLRenderer({ antialias: true }) renderer.setClearColor(0xf5f3ef, 1) camera = PerspectiveCamera(35, aspect, 1, maxDim * 10) camera.position = centroid + [maxDim * 1.5, maxDim * 1.0, maxDim * 1.5] controls = OrbitControls with damping AmbientLight(0xffffff, 0.75) DirectionalLight(0xffffff, 0.35) from upper-right
Scale camera distance to the lot's maximum dimension so it works for both small Uruguay lots (30m) and large NYC lots (170ft).
Geometry Pipeline
For each generated file:
- Fix polygon winding to CCW using
signedArea - Lot ground plane:
+ outline + vertex markers (small spheres)groundPolygon(lotPoly, ...) - Edge labels: On edges > 20 units, place a
at the edge midpoint, offset outwardcreateTextSprite - Setback zone: Render lot polygon as red → overlay inset polygon as lot color. Draw inset as dashed line.
- Volumes: For each volume in the model, compute inset if needed, then
extrudePolygon(...) - Height cap:
at max heightgroundPolygon(topPoly, amber, ...) - Height labels: Dashed vertical lines + text sprites at key heights
- Street label: At the street-facing edge (Z ≈ 0 or the edge closest to the origin)
- Area label: At lot centroid
- Grid:
scaled to lot size, subtle opacityTHREE.GridHelper
Multi-Scenario Support
If
SCENARIOS is populated (multi-lot analysis with apareadas, unified, etc.):
- Create a
per scenarioTHREE.Group - Add toggle buttons in a
div (top-left, below title)#scenario-bar
function:showScenario(key)- Toggle group visibility
- Update stats panel content
- Update legend if needed
- Active button gets
class (dark background).active - Lot dividers show/hide based on whether lots are unified
Reference Implementations
| File | Pattern |
|---|---|
| NYC exact polygon, contextual base+tower |
| Uruguay exact polygon, single lot, flat height cap |
| Multi-lot, 4 scenario toggles |
Use these as the code baseline. Copy utility functions verbatim from the Padrón 444 implementation (cleanest version).
Step 5: Save File
Save the HTML next to the source report with
zoning-envelope- prefix and the same slug:
→padron-350-punta-del-este.mdzoning-envelope-padron-350-punta-del-este.html
→zoning-analysis-250-hudson-st.mdzoning-envelope-250-hudson-st.html
Open the file in the browser after saving.
Notes
- Dependency: This skill requires a zoning analysis report. It does not perform zoning lookups, coordinate conversion, or regulation parsing — that's the analysis skill's job.
- Units: NYC reports use feet, Uruguay uses meters. The
field in the Envelope Data block determines all labels and scaling.unit - Camera: Position proportional to max lot dimension.
with OrbitControls.PerspectiveCamera(35) - Multi-lot: When the report includes
, generate toggle buttons. Use simplified rectangles if individual lot polygons are not available in the report.scenarios