Browse skills
skill
v1.0.04· MIT

Zoning 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 .zip

A 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.