skill
v1.0.04· MITZoning Envelope
Generate interactive 3D zoning envelope viewers from zoning analysis reports. Requires a zoning analysis report as input.
Install
Copy install prompt for AI
Natural-language prompt you can paste into any Claude conversation. Claude Code installs it to ~/.claude/skills/zoning-envelope/; claude.ai and Claude Desktop adopt it for the current chat and tell you how to save it permanently.
Copy install command
curl -fsSL https://spellbook.build/i/zoning-envelope | bash
Drops the file into ~/.claude/skills/zoning-envelope/SKILL.md. The script is plain shell — inspect it by dropping the | bash.
Download skill folder
Download .zipA zoning-envelope/ folder with SKILL.md (and any supporting files) — unzip into ~/.claude/skills/.
Benchmarks
This skill isn't in a benchmarked category yet. Set metadata.type in the frontmatter to activate scorecards.
Skill content
---
name: zoning-envelope
description: Generate interactive 3D zoning envelope viewers from zoning analysis reports. Requires a zoning analysis report as input.
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- AskUserQuestion
---
# /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`. This skill is a renderer — it does not perform zoning calculations.
## Project Dossier
If `PROJECT.md` exists in the working directory, read it before fetching — lot geometry and bulk controls may already be on file from /zoning-analysis-nyc. After completing, append a one-line envelope summary and the viewer file path to its **Zoning** section. Update values in place (the dossier holds current state, not history), every entry with a source and date. No `PROJECT.md`? Skip silently — or mention `/project-dossier init` if the user is clearly starting a project.
## Usage
```
/zoning-envelope path/to/zoning-analysis.md
/zoning-envelope 250 hudson
/zoning-envelope
```
## Step 1: Find the Report
### If a `.md` path is provided
Read the file directly.
### If a search term is provided (address, etc.)
Search for matching zoning analysis reports in the 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` file in the current working directory. If found, confirm with the user. If not found, tell the user:
> No zoning analysis report found. Run `/zoning-analysis-nyc` first, then come back with `/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:
```json
{
"lot_poly": [[x, y], ...],
"unit": "ft",
"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:
- `LOT_POLY` — the lot boundary polygon in local units
- `UNIT` — "ft"
- `VOLUMES` — array of volumes to extrude, each with inset distance, height range, label
- `HEIGHT_CAP` — max height for the amber cap plane
- `INFO` — title, zone, id, area for the overlay panels
- `STATS` — key/value pairs for the parameters panel
- `SCENARIOS` — if present, multi-scenario toggle data
**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 | `#f5f3ef` | 1.0 |
| Ground (lot) | `#dcd7cd` | 0.5 |
| Lot boundary | `#2c2c2c` | 1.0 |
| Setback zones | `#c85a50` | 0.2 |
| Base volume faces | `#6ba0c5` | 0.08–0.10 |
| Base volume edges | `#6ba0c5` | 0.30–0.35 |
| Tower/upper volume | `#6ba0c5` | 0.05 |
| Height cap / sky plane | `#e8a849` | 0.10–0.15 |
| Labels | `#333333` | 1.0 (canvas sprites) |
| Grid | `#d0ccc4` | 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
```html
<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:
**`signedArea(poly)`** — Returns signed area. Positive = CCW winding.
**`insetPolygon(poly, d)`** — Shrinks polygon inward by distance `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.
**`triangulate(poly)`** — Ear-clipping triangulation for arbitrary simple polygons. Returns index array for `BufferGeometry.setIndex()`.
**`extrudePolygon(poly, hBottom, hTop, color, opacity)`** — Returns a `THREE.Group` containing:
1. `BufferGeometry` with triangulated top/bottom faces + side quads
2. Wireframe edges: top ring, bottom ring, vertical edges at each vertex
**`groundPolygon(poly, color, opacity, yOffset)`** — Triangulated flat polygon at a given Y height.
**`centroid(poly)`** — Returns `[cx, cz]` for camera targeting and label placement.
**`createTextSprite(text, options)`** — Creates a `THREE.Sprite` with canvas-rendered text. Options: `fontSize`, `color`, `bgColor`.
### Scene Setup
```javascript
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 townhouse lots (~25 ft) and large assembled sites (170 ft+).
### Geometry Pipeline
For each generated file:
1. **Fix polygon winding** to CCW using `signedArea`
2. **Lot ground plane**: `groundPolygon(lotPoly, ...)` + outline + vertex markers (small spheres)
3. **Edge labels**: On edges > 20 units, place a `createTextSprite` at the edge midpoint, offset outward
4. **Setback zone**: Render lot polygon as red → overlay inset polygon as lot color. Draw inset as dashed line.
5. **Volumes**: For each volume in the model, compute inset if needed, then `extrudePolygon(...)`
6. **Height cap**: `groundPolygon(topPoly, amber, ...)` at max height
7. **Height labels**: Dashed vertical lines + text sprites at key heights
8. **Street label**: At the street-facing edge (Z ≈ 0 or the edge closest to the origin)
9. **Area label**: At lot centroid
10. **Grid**: `THREE.GridHelper` scaled to lot size, subtle opacity
### Multi-Scenario Support
If `SCENARIOS` is populated (multi-lot analysis with apareadas, unified, etc.):
1. Create a `THREE.Group` per scenario
2. Add toggle buttons in a `#scenario-bar` div (top-left, below title)
3. `showScenario(key)` function:
- Toggle group visibility
- Update stats panel content
- Update legend if needed
4. Active button gets `.active` class (dark background)
5. Lot dividers show/hide based on whether lots are unified
### Reference Implementations
| File | Pattern |
|------|---------|
| `./zoning-envelope-250-hudson-st.html` | NYC exact polygon, contextual base+tower |
Use this as the code baseline.
## Step 5: Save File
Save the HTML next to the source report with `zoning-envelope-` prefix and the same slug:
- `zoning-analysis-250-hudson-st.md` → `zoning-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. The `unit` field in the Envelope Data block determines all labels and scaling.
- **Camera:** Position proportional to max lot dimension. `PerspectiveCamera(35)` with OrbitControls.
- **Multi-lot:** When the report includes `scenarios`, generate toggle buttons. Use simplified rectangles if individual lot polygons are not available in the report.