editor: add measurement tool#491
Conversation
Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… rendering and metrics
…d chained measurements
# Conflicts: # packages/core/src/registry/types.ts # packages/editor/src/components/editor-2d/floorplan-registry-action-menu.tsx # packages/editor/src/components/editor-2d/renderers/floorplan-registry-layer.tsx # packages/editor/src/components/editor/node-arrow-handles.tsx # packages/editor/src/components/ui/sidebar/use-plugin-panels.tsx # packages/editor/src/lib/measurements.test.ts # packages/editor/src/lib/measurements.ts # packages/plugin-trees/src/index.ts
| const measurement = useMeasurementTool.getState() | ||
| measurement.setPreviewArea(null) | ||
| measurement.setPreviewPerimeter(null) | ||
| measurement.setPreviewSegment(null) |
There was a problem hiding this comment.
Grid move clears hover previews
Medium Severity
Measurement hover previews for nodes (distance, area, perimeter) don't appear in 2D. The grid:move handler clears previewArea, previewPerimeter, and previewSegment at the start, immediately wiping any previews set by pointermove events.
Reviewed by Cursor Bugbot for commit 5d87835. Configure here.
| measurement.commitAngle(point) | ||
| } else { | ||
| measurement.beginAngle('2d', point, target?.targetLine ?? null) | ||
| } |
There was a problem hiding this comment.
Collinear wall snaps zero angle
Medium Severity
When placing a 2D angle on a straight host (e.g. a wall), the second click can sit on the same ray as the reference leg. commitAngle then sets first and second to the same plan point, so angleBetweenMeasurements reports 0° instead of the corner angle users expect along that edge.
Reviewed by Cursor Bugbot for commit 5d87835. Configure here.
| measurement.commitAngle(point) | ||
| } else { | ||
| measurement.beginAngle('2d', point, target?.targetLine ?? null) | ||
| } |
There was a problem hiding this comment.
Angle start ignores guide lines
Low Severity
Starting a 2D angle passes only target?.targetLine into beginAngle. Parallel and perpendicular snaps expose their geometry on guideLine, so the reference leg falls back to the generic grid axis instead of the host edge the snap indicator already highlights.
Reviewed by Cursor Bugbot for commit 5d87835. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
There are 7 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ab131b0. Configure here.
| }, | ||
| ], | ||
| selectedId: id, | ||
| })) |
There was a problem hiding this comment.
Colinear angle collapses leg
Medium Severity
When committing an angle, if the second point falls on the reference line, referencePointForAngle can cause the first and second legs to align. This results in a degenerate 0° angle measurement, displaying an empty or misleading arc.
Reviewed by Cursor Bugbot for commit ab131b0. Configure here.
| hydrateMeasurements(sceneGraph.measurements) | ||
| } else { | ||
| useScene.getState().clearScene() | ||
| hydrateMeasurements(null) |
There was a problem hiding this comment.
Measurements dropped without field
High Severity
On load, editor state is restored only from sceneGraph.measurements, not from measurement nodes already in nodes. MeasurementSceneGraphSync then rebuilds scene nodes from an empty tool store and removes any persisted measurement_* entries, so saves missing the measurements field lose linear measurements silently.
Reviewed by Cursor Bugbot for commit ab131b0. Configure here.
|
|
||
| if (!parentId) { | ||
| rootNodeIds.push(id) | ||
| continue |
There was a problem hiding this comment.
Orphan measurements at scene root
Medium Severity
Linear measurements without snap attachments get parentId: null and are appended to rootNodeIds. They are not parented under the active level or building, so the scene tree, outliner, and hierarchy-aware features can misplace or hide them compared to other level content.
Reviewed by Cursor Bugbot for commit ab131b0. Configure here.
| }, [draggingSegmentEndpoint, segments]) | ||
|
|
||
| useEffect(() => () => syncLinearMeasurementSceneNodes([]), []) | ||
|
|
There was a problem hiding this comment.
Unmount strips scene measurement nodes
Medium Severity
MeasurementSceneGraphSync runs syncLinearMeasurementSceneNodes([]) on unmount, removing every measurement node from useScene while useMeasurementTool segments are unchanged. Remounts restore them, but any autosave or UI that reads the scene graph between unmount and resync can persist or show a graph missing synced measurement nodes even though the measurements store still has data.
Reviewed by Cursor Bugbot for commit ab131b0. Configure here.


What does this PR do?
Adds a registry-driven measurement tool for 2D and 3D editing, including distance, area, perimeter, and angle measurements; snap feedback and persisted measurement history; per-kind measurement definitions for built-in nodes; and consistent thinner measurement stroke styling across measurement overlays.
How to test
bun run lintandbun run check; both should complete without fixes.bun run check-typesandbun run build; both should complete successfully.Screenshots / screen recording
Not added yet — this is a visual/interactive measurement tool change.
Checklist
bun devbun checkto verify)mainbranchNote
Medium Risk
Large editor/scene-graph feature with new node type and broad parent
childrenschema updates; persistence and interaction paths need careful regression, but no auth or payment surface.Overview
Introduces a registry-driven measurement system so editors can draw and persist length, area, perimeter, and angle readouts in 2D floorplan and 3D, instead of ad hoc dimension UI only.
Schema & core: New
measurementnodes (MeasurementNode, attachments to geometry) can hang under levels, walls, slabs, stairs, cabinets, etc.NodeDefinition.measurementsupplies snap anchors, direct length, area, and perimeter per node kind. The event bus and scene registry gain measurement events and anodeIdsreverse map so nested R3F picks resolve to the owning scene node.Editor UX: Measure appears on the Build tab; the viewer menu adds a Measurements visibility toggle. A Measurement history panel controls mode, precision, chained measurements, snap kinds, and inline edits.
MeasurementSceneGraphSynckeeps the tool store and scene graph aligned for save/reload.2D:
FloorplanMeasurementToolLayerhandles grid clicks, snapping (walls, roofs, MEP, intersections, guides), endpoint drag, hover previews, and zoom-stable HTML pill labels (shared with 3D styling viaDimensionPillShell).Misc: Group bbox helpers depend on
meshEpochwithout biome ignore comments;next.configaddsallowedDevOriginsfor127.0.0.1.Reviewed by Cursor Bugbot for commit ab131b0. Bugbot is set up for automated code reviews on this repo. Configure here.