Skip to content
Mapping

3D & massing

How the map draws volume: existing buildings, the planning envelope and a generated proposal, each at its own opacity. Numbers measured from the shipping map.

Buildings fade in

Context buildings are fill-extrusions whose opacity is interpolated on zoom: 0 at 14.5, 0.75 at 15.5.

z 14.5 · opacity 0z 15.5 · opacity 0.75
The fadeOne zoom level, six steps. Illustrative geometry; the opacities and stops are the map's.
Code
import { fabric } from "@gridd/ui/cartography"

map.addLayer({
  id: "buildings-ext",
  type: "fill-extrusion",
  source: "buildings",
  "source-layer": "bldg",
  minzoom: 14.5,
  paint: {
    "fill-extrusion-color": fabric().building,
    // interpolated on ZOOM, not a transition: it tracks the camera in both
    // directions and settles on the 0.75 the layer has always rendered at
    "fill-extrusion-opacity": ["interpolate", ["linear"], ["zoom"], 14.5, 0, 15.5, 0.75],
    "fill-extrusion-height": ["coalesce", ["get", "h"], 3],
    "fill-extrusion-vertical-gradient": true
  }
})

The solidity ladder

Opacity is rank. The thing being proposed is the most solid thing on the map.

Proposal 0.9
Context 0.75
Envelope 0.42
Opacity is rankProposal 0.9 (teal) · context buildings 0.75 (neutral) · the planning envelope 0.42, dropping to 0.14 while a proposal sits inside it.
Code
import { massing } from "@gridd/ui/cartography"
import { token } from "@gridd/ui/tokens"

// the planning envelope — the selection accent, at 0.42
map.addLayer({
  id: "envelope-ext",
  type: "fill-extrusion",
  source: "envelope",
  paint: {
    "fill-extrusion-color": token("--gd-accent", theme),
    "fill-extrusion-opacity": 0.42,
    "fill-extrusion-base": ["get", "base"],
    "fill-extrusion-height": ["get", "top"],
    "fill-extrusion-vertical-gradient": true
  }
})

// the proposal — teal, alternating by storey parity, at 0.9. Inserted BELOW
// the envelope so it writes depth first and the ghosted envelope blends over it.
map.addLayer({
  id: "massing-ext",
  type: "fill-extrusion",
  source: "massing",
  paint: {
    "fill-extrusion-color": [
      "case", ["==", ["%", ["coalesce", ["get", "storey"], 1], 2], 0],
      massing().b, massing().a
    ],
    "fill-extrusion-opacity": 0.9,
    "fill-extrusion-vertical-gradient": true
  }
}, "envelope-foot")

// a proposal lands: ghost the envelope, hide the existing stock
map.setPaintProperty("envelope-ext", "fill-extrusion-opacity", proposal ? 0.14 : 0.42)
  • 0.9 — the PROPOSAL: GRIDD's generated best-fit massing, in the cartography teal (#17A6A0, alternating with #0F7D78 by storey parity).
  • 0.75 — context buildings at full zoom, in the neutral stock colour.
  • 0.42 — the 3D planning envelope, in the selection accent. It ghosts to 0.14 the moment a proposal is placed inside it, and the existing building stock hides outright.

Rules

  • Fade extrusions in over a full zoom level — 0 at 14.5, 0.75 at 15.5. Interpolate on zoom, not on a transition.
  • Default a missing height to one storeycoalesce(h, 3).
  • Turn fill-extrusion-vertical-gradient on for EVERY extrusion, including the proposal.
  • Draw context buildings in the neutral stock colour (#E8E9ED). Buildings are furniture, never a signal.
  • Draw the proposal in the cartography teal, never the ramp's pass-green.
  • Ghost the envelope to 0.14 and hide the building stock while a proposal is on the lot.
  • Draw easements BEFORE the envelope, so the carved-out box reads as clear of them.
  • Land every ordinary camera move flat and north-up. Leaving 3D is one ease, 600–700 ms.
  • Do not force pitch 0 while an envelope is up. The reframe owns tilt exactly then.

Why this way

The reasoning behind the decisions above. Read it once; it is what stops the next person undoing them.

Why buildings fade over a whole level

A city of prisms popping into one frame is the worst frame the map can paint. A full level of fade means no frame is the arrival frame — the Google-Maps building entrance. It is zoom-interpolated rather than a CSS-style transition so it tracks the camera continuously in both directions, and settles on the same 0.75 the layer has always rendered at.

Opacity is rank

The thing being proposed is the most solid thing on the map and everything else yields to it. Context at 0.75 is solid enough to read and transparent enough to see a boundary through; the envelope at 0.42 is a rule volume you are meant to look INTO. When a proposal arrives the envelope drops to 0.14 and the existing stock hides outright, because at that moment the question has narrowed to one building and everything else is in the way of it.

Why the proposal is teal and not green

Green borrows the assessment ramp's verdict vocabulary, and a generated massing is a study model, not a verdict. The teal also clears the warm existing stock and the indigo envelope it sits inside, so a proposal reads at a glance. The two teal shades alternate by storey parity so a stack of levels is legible as distinct boxes rather than one fused block.

The massing layer is inserted BELOW the envelope layers on purpose: extrusion layers depth-test against earlier-drawn geometry, so the massing has to write depth first and let the ghosted envelope blend over it — otherwise the envelope wall occludes the very thing it is meant to contain.

The building colour, and why it is not pure white

#E8E9ED: a near-white, deliberately not pure white. The layer is semi-transparent so a parcel's fill still reads through it, and pure white picks up a cold tint from whatever is beneath it. Buildings and boundaries are context; the palette keeps them quiet on purpose.

One storey is an honest default, not a guess

Height is coalesce(h, 3) — where the storeys model has no answer, a building is one storey. In a detached-housing fabric that is the honest answer rather than a guess dressed as data, and it fails in the direction that under-claims.

One sky

fill-extrusion-vertical-gradient is on for every extrusion — the light comes from one place, including on the proposal. An unlit proposal inside lit context reads as a cut-out rather than as a building.

2D is the working view

3D is a mode the user enters, not a state the map drifts into. Pitch 0 and bearing 0 ride along on every fly, fit and reset; leaving 3D is one ease, 600–700 ms, the same duration class as every other mode reset (Camera & movement). The single exception is that while an envelope is up the reframe owns tilt — forcing pitch 0 before re-tilting paints a one-frame bounce.

The envelope itself is drawn as foot plus walls from the parcel's own base/top attributes: the rule volume is geometry, not decoration. Easements are drawn before it so the carved-out box reads as clear of them.

Navigate

Esc