Boundary import + layout model, metric elevation migration, content toggles, segment slope - #1792
Merged
Merged
Conversation
default building lot widths need work
showBuildings is an import-time conversion argument on loadAndParseStreetmixURL (not a schema prop). Parity harness drives both legacy and managed with a unified SHOW_BUILDINGS constant. Known issue: the managed parity trigger now calls loadAndParseStreetmixURL before street-align/street-ground load, racing under the full sequential run (pageerrors + buildings leakage). See docs/managed-street-buildings-wip.md.
Elevation: replace the deprecated integer `level` (1 level == 0.15m curb height) with metric `elevation` everywhere, matching Streetmix schema 33+: - street-segment schema: `elevation` (number, meters) replaces `level` (int); TYPES presets updated (sidewalk/building 1 -> 0.15) - calculateHeight() takes meters directly; new levelToElevation() and migrateSegmentLevelToElevation() helpers; CURB_HEIGHT kept only as the level<->meters migration constant - convertStreetValues() now normalizes pre-33 integer levels to meters and passes 33+ metric elevation through (previously the reverse, lossy) - legacy `street` parser derives its integer-level lookup indices locally via metricElevationToLevel() - serialization writes `elevation` only; legacy saved scenes are migrated at load time in createEntityFromObj (prop-string and object forms) and in parseStreetObject for legacy json-blobs - editor sweep: SegmentAdd/Update commands, AI tool schemas + prompt examples, defaultStreets presets, segment sidebar (Elevation (m)) Boundary parity (#1775, part of #1286/#1720): import building edges from the canonical schemaVersion 34+ `boundary` object, falling back to the deprecated flat leftBuildingVariant/rightBuildingVariant fields: - new getBoundaryFromStreetData() normalizes both shapes ({ variant, floors, elevation-in-meters }) - managed-street building import applies boundary elevation (meters) to the building segment and persists `floors` as a new street-segment schema property (metadata only; does not drive model height yet) - legacy streetmix-loader path reads the boundary variant too - unknown boundary variants still warn and are skipped Tests: v34 boundary fixture (coastal-boundary-street), metric elevation math, level->elevation migration, boundary normalization/fallback, and updated schema-version expectations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
showBuildings is now a managed-street schema property (default true), available at layer creation time (parity harness, AddLayerPanel, AI tools via entityUpdate) and as a Buildings checkbox in the managed street sidebar. Semantics: imports always create the boundary building segments when the source has boundary data; the property only controls whether they show. Toggling is realtime and non-destructive — off hides the building segments (visible: false, they stay in the DOM and serialize with the scene), on restores them. New street-layout-utils.getLayoutSegments() makes street-align, street-ground, and street-label treat hidden buildings as absent, so toggling off re-centers the street, shrinks the ground slab, and drops the building width labels — identical to a street imported without buildings. Non-building segments are always included in layout regardless of visibility so hiding a lane doesn't reflow the street. loadAndParseStreetmixURL keeps its optional showBuildings argument for direct callers but now writes it back to the component property so state stays consistent; the parity harness passes the toggle as component state instead. Verified end-to-end in headless Chromium against the dev server with the v34 boundary fixture: toggle off re-centers the travelled way (sidewalk x -4.5 -> -7.5) and shrinks the ground (31.4m -> 17.4m street width), toggle on restores exactly; saved JSON carries showBuildings plus the hidden state; legacy level json-blobs still migrate to metric elevation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
…lope Toggles (from the legacy street component prototype, minus globalAnimated which has no managed equivalent yet): showGround, showStriping, and showVehicles join showBuildings as managed-street schema properties with sidebar rows. All are realtime and non-destructive — content is always generated, the toggles only control visibility. Vehicles are identified by their mixin's catalog category (same approach as the legacy street-utils helpers, now scoped to the street entity); striping targets the planes created by street-generated-striping; ground hides the street-ground dirtbox. A subtree observer (no-op while toggles are on) re-applies visibility to late-generated content, so clones regenerated after a segment edit stay hidden while a toggle is off. The creation functions and AI tools that already passed showVehicles/showStriping into managed-street now actually take effect instead of being silently dropped. Segment slope (#1774, coastmix schema v34): parse slope { on, values } where values are [start, end] elevations in meters; flat for slope.on false (the seeded v34 default), so nothing changes for existing streets. New getSegmentSlope() normalizes/validates the upstream shape and calculateSlopedHeights() computes the mean surface height plus per-edge deltas; the below-box geometry displaces its top-face vertices so the surface tilts between the two edge heights while the box bottom stays on the dirt layer. Generated content sits at the mean elevation for now. slope/slopeStart/slopeEnd round-trip through save/load and getManagedStreetJSON. Verified end-to-end in headless Chromium: sloped fixture segment renders at mean height 0.225m with top face tilting +0.075/-0.075 across its width; striping (3), vehicles (12), and ground toggle off/on in realtime while 22 pedestrians stay untouched; clones regenerated with showVehicles off are created hidden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
Conflict resolutions: - src/tested/* and test/core/*: main converted these modules to ESM; kept this branch's content (metric elevation, boundary, slope helpers and their tests) in main's export/import style - aframe-streetmix-parsers.js: inlined the metric->integer level derivation instead of requiring the now-ESM streetmix-utils (this deprecated CJS parser must stay require()-able by the mocha tests) - StreetSegmentSidebar.jsx: kept the Elevation (m) row, adopted main's react-intl label pattern (segmentSidebar.elevation replaces segmentSidebar.curbLevel across the locale files) - compare-imports.mjs: main moved the parity harness to Playwright; kept the SHOW_BUILDINGS parameterization for both import paths and adapted the evaluate/waitForFunction calls to Playwright signatures - editor/i18n/config.js: brace a long single-line if that main committed unformatted — lint-staged's prettier pass rewraps it into a curly violation that blocked any commit staging the file Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
wobble-geometry-box.play() read this.el.object3D.children[0].geometry —
positional indexing that breaks when child entities attach their Groups
before the surface mesh exists. After main's generated-clones changes,
the seawall clones on a waterfront building segment land in object3D
ahead of the water surface mesh, so children[0] had no geometry and the
throw aborted entity load (the managed street's import promise then
never resolved). Use getObject3D('mesh') instead, retrying via
object3dset when the mesh isn't built yet, and guard tick until the
waves exist.
Found by re-running the headless-Chromium verification after merging
main: the coastal fixture import hung with the waterfront building stuck
in pendingEntities. All checks pass again post-fix (slope tilt, content
toggles, buildings toggle, legacy level migration).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
Negative elevations (below road level) are intentionally out of scope for now. calculateHeight already clamped them to the base surface depth at render time, but the intent was implicit and negatives could still ride along in data. Codify it: - elevation / slopeStart / slopeEnd schema get min: 0 (editor number inputs stop at road level) - levelToElevation clamps negative legacy levels to 0, so a saved level: -1 migrates to elevation: 0 — the same rendered result it had before, now with clean data - comments on the schema and the calculateHeight clamp state the decision; the AI segment tool schema says negatives are unsupported - tests updated to assert the clamped values Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
The layers-panel eye icon and the managed-street showBuildings toggle both write the building segment's `visible` component, but only the sidebar path re-ran layout. Re-showing a building via the eye while showBuildings was off made it appear at the stale position it held before the street re-centered — on top of the travelled way. Treat any change to a building segment's `visible` as a layout change: each segment gets a componentchanged listener (the event does not bubble, so a parent listener never fires) attached at managed-street init for saved scenes and via the child mutation observer for segments added later. The handler re-emits segments-changed so street-align, street-ground, and street-label recompute — the re-shown building slots in at the street edge and the ground/labels grow to match; hiding one building via the eye symmetrically shrinks the layout. Bulk updates from updateBuildingVisibility suppress the per-segment handler and emit one event as before. Verified in headless Chromium: sidebar off then eye-on left building repositions it (x -10.7 -> -8.7), re-centers the street (sidewalk -7.5 -> -2.5) and grows the ground 17.4m -> 27.4m (street + that one building) with edges exactly adjacent; eye-off shrinks back. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
Rework how flanking land use relates to street layout, and rename the segment type from 'building' to 'boundary' (the better term for the wider set it covers: buildings, waterfront, fences, parking lots). Layout model: the travelled way is the only layout input. street-align centers the travelled way alone; street-ground and street-label span it alone; boundary segments are positioned OUTSIDE the travelled way's edges by their `side` property (stacking outward if multiple), whether visible or not. Consequences, all verified live: - adding, removing, hiding, or showing a boundary never moves the street — the toggle and the scene-graph eye are pure visibility, so the previous eye-toggle re-layout machinery (per-segment componentchanged watchers) is deleted, not just bypassed - boundaries render at the street ends regardless of their position in the segment list — ends-only by construction (structural editing rules remain with #1751) - this also anticipates the future centerline mechanism: the travelled way will anchor to real-world centerline points, boundaries stay edge-derived Rename: street-segment type 'boundary' replaces 'building' (TYPES.boundary, schema oneOf, side/variant behavior checks, sidebar gates + Boundary Variant label with locale updates, AI tool schemas, default street presets, StreetPlan Buildings mapping, import creator + "Boundary • variant (side)" layer names). managed-street's toggle is now `showBoundaries`. Load-time migrations cover saved scenes: `type: building` -> `type: boundary` and `showBuildings` -> `showBoundaries` (string + object forms, unit-tested); parseStreetObject accepts legacy 'building' in json-blobs. The legacy street component's processBuildings path is untouched. Verified in headless Chromium: with a 17.4m travelled way the sidewalk sits at x=-7.5 and ground stays 17.4m across all states (boundaries on, off, one eyed-on, and a boundary reparented into the middle of the DOM); left/right boundaries hold x=-13.7/+10.7 throughout; a saved scene with 'type: building; level: 1' + 'showBuildings: false' loads as a boundary at elevation 0.15 with showBoundaries false. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs
The live streetmix.net export has no sloped segments, so the synthesized coastal fixture stays as the slope coverage; the real export covers the boundary object and metric elevations against production data.
Collaborator
Author
Manual test plan (pre-merge)Setup: 1. Streetmix import with boundaries
2. Layout invariance (the core behavioral guarantee)
3. Content toggles
4. Elevation editing
5. Slope (import + round-trip only)
6. Legacy scene migration (back-compat)
7. Regression sweep
Pre-merge/deploy checklist
Deferred (tracked, not blockers): floors → model height mapping proposal, slope editing UI, structural ends-only drag rules (#1751). |
street-segment only emitted segment-changed for width/length, and managed-street only relayed width changes, so converting a segment to/from boundary or flipping a boundary's side left the layout stale until the next width edit. Both properties now trigger the same segments-changed re-layout path; generated-content listeners already self-guard on width/length so no spurious clone regeneration. Verified live: type change alone re-centers the travelled way and moves the boundary outside the edge; side flip alone jumps it across; converting back restores the original layout.
kfarr
marked this pull request as ready for review
July 5, 2026 06:45
This was referenced Jul 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #1733 (includes its managed-street building import work, rebuilt on the canonical boundary object). Addresses #1775 and #1774; part of the building-parity epic #1286 / #1720; lays groundwork for #1751. Up to date with
main.Boundary layout model (
building→boundary)The segment type is renamed
boundary— the better term for the set it covers (buildings, waterfront, fences, parking lots). More importantly, the layout model changed:street-aligncenters the travelled way alone;street-groundandstreet-labelspan it alone. Boundary segments are positioned outside the travelled way's edges by theirsideproperty (stacking outward if multiple), whether visible or not.type: building→type: boundaryandshowBuildings→showBoundaries(string + object forms, unit-tested); json-blobs accepting legacybuildingtoo. The legacystreetcomponent'sprocessBuildingspath is untouched.Elevation: integer
level→ metricelevationStreetmix schema 33+ stores elevation in meters; we were down-converting to a lossy integer "curb level". Killed before beta exit:
street-segment.elevation(number, meters) replaceslevel(int); presets updated (1→0.15);calculateHeight()takes meters;CURB_HEIGHTkept only as the migration constantconvertStreetValues()normalizes pre-33 integer levels to meters; 33+ passes throughmigrateSegmentLevelToElevationincreateEntityFromObj,levelToElevationfallback inparseStreetObject)min: 0, render clamp, legacy negative levels migrate to0Boundary object import (#1775)
getBoundaryFromStreetData()reads the canonical v34boundary.left/right(variant/floors/elevationin meters) with flat-field fallback; boundary wins when both presentelevationapplies to the segment (coastmix raised edges import correctly);floorspersists via a new schema prop (metadata only; height mapping is an open question)streetmix-loaderreads the boundary variant tooToggles:
showBoundaries,showGround,showStriping,showVehiclesMigrated from the legacy
streetprototype (skippingglobalAnimated) as managed-street schema props with sidebar rows. Realtime, non-destructive, visibility-only:Segment slope (#1774, coastmix v34)
getSegmentSlope()parsesslope: {on, values: [start, end]}(meters); seeded v34 defaults stay flatbelow-boxtilts its top face between the two edge heights; entity sits at mean height (generated content at mean elevation); round-trips through save/loadvalues[0]→ segment start (−x) edge; confirm against a live coastmix exportDrive-by fix
wobble-geometry-box.play()usedobject3D.children[0].geometry— crashes entity load when clone Groups attach before the surface mesh (waterfront boundaries after main's clone changes). NowgetObject3D('mesh')+object3dsetretry.Verification (headless Chromium against the dev server)
type: building; level: 1+showBuildings: falseloads as aboundaryatelevation: 0.15withshowBoundaries: falseTests
coastal-boundary-street.streetmix.json(synthesized; keeps slope coverage) +kfarr-demo-street-v34.streetmix.json(real streetmix.net export of kfarr/3, fetched 2026-07-04 — covers the boundary object and metric elevations against production data; the live export has no sloped segments, hence keeping the synthesized one)test:componentscoastal-boundary-streetis the closest match of all at 2.37% pixel diff; higher outliers (marina-parking 19%, kfarr-demo-v34 16.6%) are the known parking non-parity and random clone placement noise on content-heavy streetsOpen questions for review
floors→ model height mapping (proposal: pick models by floor count from catalog as follow-up)compound-wallaliases seawall;beach/other new coastmix variants unmapped (warn-and-skip); boundaryidnot persistedelevation/boundary) will not render correctly if opened by a pre-PR build (segments flatten to level 0, unknown segment type) — relevant to rollbacks and self-hosted older deployments🤖 Generated with Claude Code
https://claude.ai/code/session_014eU9iRHukcxHhvXxDFjeSs