@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type IconRef,
|
|
7
|
+
nodeRegistry,
|
|
8
|
+
type ParamAction,
|
|
9
|
+
type ParamField,
|
|
10
|
+
useScene,
|
|
11
|
+
type ZoneNode,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
|
+
import { Icon } from '@iconify/react'
|
|
15
|
+
import { Move, Trash2 } from 'lucide-react'
|
|
16
|
+
import { type ComponentType, lazy, Suspense, useCallback } from 'react'
|
|
17
|
+
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
18
|
+
import { collectZoneContentIds } from '../../../lib/zone-content'
|
|
19
|
+
import useEditor from '../../../store/use-editor'
|
|
20
|
+
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
21
|
+
import { PanelSection } from '../controls/panel-section'
|
|
22
|
+
import { SegmentedControl } from '../controls/segmented-control'
|
|
23
|
+
import { SliderControl } from '../controls/slider-control'
|
|
24
|
+
import { ToggleControl } from '../controls/toggle-control'
|
|
25
|
+
import { InspectorFooterContext, PanelWrapper } from './panel-wrapper'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Auto-derived right-panel inspector for any registry-backed node.
|
|
29
|
+
*
|
|
30
|
+
* Reads `definition.parametrics` from the registry and renders one
|
|
31
|
+
* `<PanelSection>` per group, one control per field. Field kinds supported:
|
|
32
|
+
* - `number` → SliderControl with min/max/step/unit from the descriptor
|
|
33
|
+
* - `enum` → dark-themed `<select>`
|
|
34
|
+
* - `color` → native color picker + hex input
|
|
35
|
+
* - `vec3` → three SliderControls for X / Y / Z
|
|
36
|
+
*
|
|
37
|
+
* Generic Actions section appends Move / Delete based on `capabilities`.
|
|
38
|
+
*
|
|
39
|
+
* Phase 4 will expand this with per-field `customEditor` support and a
|
|
40
|
+
* `parametrics.customPanel?` escape hatch for kinds whose parametric editor
|
|
41
|
+
* can't be auto-generated (topology editors etc.).
|
|
42
|
+
*/
|
|
43
|
+
export function ParametricInspector({
|
|
44
|
+
footer,
|
|
45
|
+
nodeId,
|
|
46
|
+
onClose,
|
|
47
|
+
}: { footer?: React.ReactNode; nodeId?: AnyNodeId; onClose?: () => void } = {}) {
|
|
48
|
+
const selectedIdFromSelection = useViewer((s) => s.selection.selectedIds[0]) as
|
|
49
|
+
| AnyNodeId
|
|
50
|
+
| undefined
|
|
51
|
+
const selectedId = nodeId ?? selectedIdFromSelection
|
|
52
|
+
const setSelection = useViewer((s) => s.setSelection)
|
|
53
|
+
// Subscribe only to the *type* — a string primitive that doesn't change
|
|
54
|
+
// when slider values change. Without this, every updateNode tick during
|
|
55
|
+
// a drag re-renders the entire panel + every field + every SliderControl.
|
|
56
|
+
// Per-field subscriptions live on FieldRenderer below.
|
|
57
|
+
const nodeType = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
|
|
58
|
+
|
|
59
|
+
const def = nodeType ? nodeRegistry.get(nodeType) : undefined
|
|
60
|
+
const parametrics = def?.parametrics
|
|
61
|
+
|
|
62
|
+
const handleUpdate = useCallback(
|
|
63
|
+
(patch: Partial<AnyNode>) => {
|
|
64
|
+
if (!selectedId) return
|
|
65
|
+
const scene = useScene.getState()
|
|
66
|
+
const node = scene.nodes[selectedId]
|
|
67
|
+
if (parametrics?.derive && node) {
|
|
68
|
+
const next = { ...node, ...patch } as AnyNode
|
|
69
|
+
patch = { ...patch, ...parametrics.derive(next, patch) }
|
|
70
|
+
}
|
|
71
|
+
// Bundle the edited node + any reconcile follow-ups into ONE
|
|
72
|
+
// updateNodes call so a single inspector edit is a single undo step.
|
|
73
|
+
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = [{ id: selectedId, data: patch }]
|
|
74
|
+
if (parametrics?.reconcile && node) {
|
|
75
|
+
const next = { ...node, ...patch } as AnyNode
|
|
76
|
+
updates.push(...parametrics.reconcile(node as AnyNode, next))
|
|
77
|
+
}
|
|
78
|
+
scene.updateNodes(updates)
|
|
79
|
+
},
|
|
80
|
+
[selectedId, parametrics],
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
const clearSelection = useCallback(() => {
|
|
84
|
+
if (onClose) {
|
|
85
|
+
onClose()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
setSelection({ selectedIds: [] })
|
|
89
|
+
}, [onClose, setSelection])
|
|
90
|
+
|
|
91
|
+
const handleMove = useCallback(() => {
|
|
92
|
+
if (!selectedId) return
|
|
93
|
+
const node = useScene.getState().nodes[selectedId]
|
|
94
|
+
if (!node) return
|
|
95
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
96
|
+
useEditor.getState().setMovingNode(node as any)
|
|
97
|
+
clearSelection()
|
|
98
|
+
}, [selectedId, clearSelection])
|
|
99
|
+
|
|
100
|
+
const handleDelete = useCallback(
|
|
101
|
+
(withZoneContent = false) => {
|
|
102
|
+
if (!selectedId) return
|
|
103
|
+
const scene = useScene.getState()
|
|
104
|
+
const node = scene.nodes[selectedId]
|
|
105
|
+
if (!node) return
|
|
106
|
+
|
|
107
|
+
const ids =
|
|
108
|
+
withZoneContent && node.type === 'zone'
|
|
109
|
+
? [selectedId, ...collectZoneContentIds(scene.nodes, node as ZoneNode)]
|
|
110
|
+
: [selectedId]
|
|
111
|
+
|
|
112
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
113
|
+
scene.deleteNodes(Array.from(new Set(ids)))
|
|
114
|
+
clearSelection()
|
|
115
|
+
},
|
|
116
|
+
[selectedId, clearSelection],
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if (!selectedId || !def || !parametrics) return null
|
|
120
|
+
|
|
121
|
+
// `parametrics.customPanel` escape hatch — kind owns its panel
|
|
122
|
+
// entirely (loaded lazily so the bundle isn't eager). Used by kinds
|
|
123
|
+
// whose editor has non-parametric concerns (slab holes list, ceiling
|
|
124
|
+
// height presets, etc.) until per-field `customEditor` + missing
|
|
125
|
+
// field kinds (list/action/computed) graduate the auto-derived
|
|
126
|
+
// panel to cover them.
|
|
127
|
+
if (parametrics.customPanel) {
|
|
128
|
+
const CustomPanel = resolveCustomPanel(parametrics.customPanel)
|
|
129
|
+
// Custom panels render their own `<PanelWrapper>` and don't thread a
|
|
130
|
+
// `footer` prop, so hand the host footer down via context.
|
|
131
|
+
return (
|
|
132
|
+
<InspectorFooterContext.Provider value={footer}>
|
|
133
|
+
<Suspense fallback={null}>
|
|
134
|
+
<CustomPanel />
|
|
135
|
+
</Suspense>
|
|
136
|
+
</InspectorFooterContext.Provider>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const presentation = def.presentation
|
|
141
|
+
const title = presentation?.label ?? nodeType ?? ''
|
|
142
|
+
const iconNode = renderIcon(presentation?.icon)
|
|
143
|
+
const canMove = !!def.capabilities.movable
|
|
144
|
+
const canDelete = def.capabilities.deletable !== false
|
|
145
|
+
const isZone = nodeType === 'zone'
|
|
146
|
+
|
|
147
|
+
const TrailingSection = parametrics.trailingSection
|
|
148
|
+
? resolveCustomPanel(parametrics.trailingSection)
|
|
149
|
+
: null
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<PanelWrapper
|
|
153
|
+
footer={footer}
|
|
154
|
+
icon={iconNode}
|
|
155
|
+
onClose={clearSelection}
|
|
156
|
+
title={title}
|
|
157
|
+
width={320}
|
|
158
|
+
>
|
|
159
|
+
{parametrics.groups.map((group, gi) => (
|
|
160
|
+
<PanelSection key={`group-${gi}`} title={group.label}>
|
|
161
|
+
{group.fields.map((field, fi) => (
|
|
162
|
+
<FieldRenderer
|
|
163
|
+
key={`field-${gi}-${fi}-${String(field.key)}`}
|
|
164
|
+
field={field as ParamField<AnyNode>}
|
|
165
|
+
nodeId={selectedId}
|
|
166
|
+
onUpdate={handleUpdate}
|
|
167
|
+
/>
|
|
168
|
+
))}
|
|
169
|
+
</PanelSection>
|
|
170
|
+
))}
|
|
171
|
+
{TrailingSection && (
|
|
172
|
+
<Suspense fallback={null}>
|
|
173
|
+
<TrailingSection />
|
|
174
|
+
</Suspense>
|
|
175
|
+
)}
|
|
176
|
+
{(canMove || canDelete || (parametrics.actions && parametrics.actions.length > 0)) && (
|
|
177
|
+
<PanelSection title="Actions">
|
|
178
|
+
<ActionGroup className={isZone ? 'flex-col' : undefined}>
|
|
179
|
+
{canMove && (
|
|
180
|
+
<ActionButton icon={<Move className="h-4 w-4" />} label="Move" onClick={handleMove} />
|
|
181
|
+
)}
|
|
182
|
+
{parametrics.actions?.map((action, i) => (
|
|
183
|
+
<ParamActionButton action={action} key={`paramaction-${i}`} nodeId={selectedId} />
|
|
184
|
+
))}
|
|
185
|
+
{canDelete &&
|
|
186
|
+
(isZone ? (
|
|
187
|
+
<>
|
|
188
|
+
<ActionButton
|
|
189
|
+
className="w-full flex-none"
|
|
190
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
191
|
+
label="Delete"
|
|
192
|
+
onClick={() => handleDelete(false)}
|
|
193
|
+
/>
|
|
194
|
+
<ActionButton
|
|
195
|
+
className="w-full flex-none"
|
|
196
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
197
|
+
label="Delete with contents"
|
|
198
|
+
onClick={() => handleDelete(true)}
|
|
199
|
+
/>
|
|
200
|
+
</>
|
|
201
|
+
) : (
|
|
202
|
+
<ActionButton
|
|
203
|
+
className="border-red-500/40 text-red-200 hover:bg-red-500/15"
|
|
204
|
+
icon={<Trash2 className="h-4 w-4" />}
|
|
205
|
+
label="Delete"
|
|
206
|
+
onClick={() => handleDelete()}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</ActionGroup>
|
|
210
|
+
</PanelSection>
|
|
211
|
+
)}
|
|
212
|
+
</PanelWrapper>
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// One inspector action button. Subscribes to `enabledIf`'s boolean result
|
|
217
|
+
// (same pattern as FieldRenderer's `visible`) so the disabled state stays
|
|
218
|
+
// live as the scene mutates — `===` on the boolean keeps unrelated ticks
|
|
219
|
+
// from re-rendering it. The click handler re-reads the live node so the
|
|
220
|
+
// handler always acts on current state.
|
|
221
|
+
function ParamActionButton({ action, nodeId }: { action: ParamAction<AnyNode>; nodeId: AnyNodeId }) {
|
|
222
|
+
const disabled = useScene((s) => {
|
|
223
|
+
if (!action.enabledIf) return false
|
|
224
|
+
const n = s.nodes[nodeId]
|
|
225
|
+
return n ? !action.enabledIf(n as AnyNode) : false
|
|
226
|
+
})
|
|
227
|
+
return (
|
|
228
|
+
<ActionButton
|
|
229
|
+
className={disabled ? 'opacity-40 pointer-events-none' : ''}
|
|
230
|
+
icon={
|
|
231
|
+
action.iconSrc ? (
|
|
232
|
+
<img alt="" className="h-4 w-4 shrink-0 object-contain" src={action.iconSrc} />
|
|
233
|
+
) : undefined
|
|
234
|
+
}
|
|
235
|
+
label={action.label}
|
|
236
|
+
onClick={() => {
|
|
237
|
+
const live = useScene.getState().nodes[nodeId]
|
|
238
|
+
if (live) action.onClick(live as AnyNode)
|
|
239
|
+
}}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function renderIcon(ref: IconRef | undefined): React.ReactNode | undefined {
|
|
245
|
+
if (!ref) return undefined
|
|
246
|
+
if (ref.kind === 'url') {
|
|
247
|
+
// Plain <img> here so the inspector doesn't pull in next/image's
|
|
248
|
+
// server-only requirements (the file is `'use client'`). Same
|
|
249
|
+
// 16x16 box the legacy panels use.
|
|
250
|
+
return <img alt="" className="h-4 w-4 shrink-0 object-contain" src={ref.src} />
|
|
251
|
+
}
|
|
252
|
+
if (ref.kind === 'iconify') {
|
|
253
|
+
return <Icon height={16} icon={ref.name} width={16} />
|
|
254
|
+
}
|
|
255
|
+
if (ref.kind === 'svg') {
|
|
256
|
+
return (
|
|
257
|
+
<svg height={16} viewBox={ref.viewBox} width={16}>
|
|
258
|
+
<path d={ref.path} fill="currentColor" />
|
|
259
|
+
</svg>
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
// `component`: lazy-loaded custom icon component. Suspense-safe.
|
|
263
|
+
const LazyIcon = lazy(ref.module)
|
|
264
|
+
return (
|
|
265
|
+
<Suspense fallback={null}>
|
|
266
|
+
<LazyIcon />
|
|
267
|
+
</Suspense>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Cache lazy custom panel components by their loader so React.lazy isn't
|
|
272
|
+
// re-invoked across renders.
|
|
273
|
+
const customPanelCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
|
274
|
+
|
|
275
|
+
function resolveCustomPanel(loader: () => Promise<{ default: ComponentType<any> }>): ComponentType {
|
|
276
|
+
const cached = customPanelCache.get(loader)
|
|
277
|
+
if (cached) return cached
|
|
278
|
+
const Comp = lazy(loader)
|
|
279
|
+
customPanelCache.set(loader, Comp as ComponentType)
|
|
280
|
+
return Comp as ComponentType
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Per-field renderers ─────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
interface FieldRendererProps {
|
|
286
|
+
field: ParamField<AnyNode>
|
|
287
|
+
nodeId: AnyNodeId
|
|
288
|
+
onUpdate: (patch: Partial<AnyNode>) => void
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function FieldRenderer({ field, nodeId, onUpdate }: FieldRendererProps) {
|
|
292
|
+
const key = String(field.key)
|
|
293
|
+
// Subscribe only to this field's value. Zustand compares with ===, so when
|
|
294
|
+
// another field on the same node changes (which produces a new node object
|
|
295
|
+
// reference), this primitive value stays equal and the field doesn't
|
|
296
|
+
// re-render. Vec3 arrays get a new reference only when the array itself
|
|
297
|
+
// changes — same outcome.
|
|
298
|
+
const value = useScene((s) => {
|
|
299
|
+
const n = s.nodes[nodeId]
|
|
300
|
+
return n ? (n as Record<string, unknown>)[key] : undefined
|
|
301
|
+
})
|
|
302
|
+
// visibleIf may consult other fields on the node — subscribe to its boolean
|
|
303
|
+
// result so we re-evaluate when relevant.
|
|
304
|
+
const visible = useScene((s) => {
|
|
305
|
+
const visibleIf = (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf
|
|
306
|
+
if (!visibleIf) return true
|
|
307
|
+
const n = s.nodes[nodeId]
|
|
308
|
+
return n ? visibleIf(n as AnyNode) : false
|
|
309
|
+
})
|
|
310
|
+
if (!visible) return null
|
|
311
|
+
|
|
312
|
+
switch (field.kind) {
|
|
313
|
+
case 'number': {
|
|
314
|
+
const num = typeof value === 'number' ? value : 0
|
|
315
|
+
const step = field.step ?? 0.01
|
|
316
|
+
const precision = precisionForStep(step)
|
|
317
|
+
return (
|
|
318
|
+
<SliderControl
|
|
319
|
+
label={prettifyKey(key)}
|
|
320
|
+
max={field.max}
|
|
321
|
+
min={field.min}
|
|
322
|
+
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
323
|
+
precision={precision}
|
|
324
|
+
step={step}
|
|
325
|
+
unit={field.unit ?? ''}
|
|
326
|
+
value={num}
|
|
327
|
+
/>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
case 'boolean': {
|
|
332
|
+
const checked = value === true
|
|
333
|
+
return (
|
|
334
|
+
<ToggleControl
|
|
335
|
+
checked={checked}
|
|
336
|
+
label={prettifyKey(key)}
|
|
337
|
+
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
338
|
+
/>
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
case 'enum': {
|
|
343
|
+
const str = typeof value === 'string' ? value : (field.options[0] ?? '')
|
|
344
|
+
if (field.display === 'segmented') {
|
|
345
|
+
return (
|
|
346
|
+
<SegmentedControl
|
|
347
|
+
onChange={(next) => onUpdate({ [key]: next } as Partial<AnyNode>)}
|
|
348
|
+
options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
|
|
349
|
+
value={str}
|
|
350
|
+
/>
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
return (
|
|
354
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
355
|
+
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
356
|
+
<select
|
|
357
|
+
className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
358
|
+
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
359
|
+
value={str}
|
|
360
|
+
>
|
|
361
|
+
{field.options.map((opt) => (
|
|
362
|
+
<option key={opt} value={opt}>
|
|
363
|
+
{prettifyEnumValue(opt)}
|
|
364
|
+
</option>
|
|
365
|
+
))}
|
|
366
|
+
</select>
|
|
367
|
+
</div>
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'color': {
|
|
372
|
+
const str = typeof value === 'string' ? value : '#888888'
|
|
373
|
+
return (
|
|
374
|
+
<div className="flex items-center justify-between px-3 py-2">
|
|
375
|
+
<span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
|
|
376
|
+
<div className="flex items-center gap-2">
|
|
377
|
+
<input
|
|
378
|
+
className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
|
|
379
|
+
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
380
|
+
type="color"
|
|
381
|
+
value={str}
|
|
382
|
+
/>
|
|
383
|
+
<input
|
|
384
|
+
className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
|
|
385
|
+
onChange={(e) => onUpdate({ [key]: e.target.value } as Partial<AnyNode>)}
|
|
386
|
+
type="text"
|
|
387
|
+
value={str}
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
case 'vec3': {
|
|
395
|
+
const v = Array.isArray(value) && value.length >= 3
|
|
396
|
+
? (value as [number, number, number])
|
|
397
|
+
: [0, 0, 0]
|
|
398
|
+
const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
|
|
399
|
+
{ label: 'X', index: 0 },
|
|
400
|
+
{ label: 'Y', index: 1 },
|
|
401
|
+
{ label: 'Z', index: 2 },
|
|
402
|
+
]
|
|
403
|
+
return (
|
|
404
|
+
<>
|
|
405
|
+
{axes.map(({ label, index }) => {
|
|
406
|
+
// v is a [number, number, number] tuple; the explicit local
|
|
407
|
+
// resolves TS's noUncheckedIndexedAccess concern that v[index]
|
|
408
|
+
// could be undefined.
|
|
409
|
+
const axisValue = v[index] ?? 0
|
|
410
|
+
return (
|
|
411
|
+
<SliderControl
|
|
412
|
+
key={`${key}-${label}`}
|
|
413
|
+
label={label}
|
|
414
|
+
max={axisValue + 5}
|
|
415
|
+
min={axisValue - 5}
|
|
416
|
+
onChange={(next) => {
|
|
417
|
+
const updated = [...v] as [number, number, number]
|
|
418
|
+
updated[index] = next
|
|
419
|
+
onUpdate({ [key]: updated } as Partial<AnyNode>)
|
|
420
|
+
}}
|
|
421
|
+
precision={2}
|
|
422
|
+
step={0.05}
|
|
423
|
+
unit="m"
|
|
424
|
+
value={Math.round(axisValue * 100) / 100}
|
|
425
|
+
/>
|
|
426
|
+
)
|
|
427
|
+
})}
|
|
428
|
+
</>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
case 'custom':
|
|
433
|
+
// The field owns its rendering and update logic — used for
|
|
434
|
+
// derived values (length from start/end), dynamic-bounded
|
|
435
|
+
// sliders (curve sagitta), composed editors.
|
|
436
|
+
return <CustomFieldRenderer Comp={field.component} nodeId={nodeId} onUpdate={onUpdate} />
|
|
437
|
+
|
|
438
|
+
default:
|
|
439
|
+
// material / ref / unrecognized kinds — not implemented in v1.
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function CustomFieldRenderer({
|
|
445
|
+
Comp,
|
|
446
|
+
nodeId,
|
|
447
|
+
onUpdate,
|
|
448
|
+
}: {
|
|
449
|
+
Comp: ComponentType<{ node: AnyNode; onUpdate: (patch: Partial<AnyNode>) => void }>
|
|
450
|
+
nodeId: AnyNodeId
|
|
451
|
+
onUpdate: (patch: Partial<AnyNode>) => void
|
|
452
|
+
}) {
|
|
453
|
+
// Subscribe to the full node — the custom editor may read any
|
|
454
|
+
// field. Tools that don't want this churn should write narrower
|
|
455
|
+
// selectors inside Comp itself.
|
|
456
|
+
const node = useScene((s) => s.nodes[nodeId])
|
|
457
|
+
if (!node) return null
|
|
458
|
+
return <Comp node={node} onUpdate={onUpdate} />
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── helpers ─────────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
function precisionForStep(step: number): number {
|
|
464
|
+
if (step <= 0) return 0
|
|
465
|
+
return Math.max(0, Math.ceil(-Math.log10(step)))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function prettifyKey(key: string): string {
|
|
469
|
+
// 'bracketStyle' → 'Bracket style'
|
|
470
|
+
const spaced = key.replace(/([A-Z])/g, ' $1').toLowerCase()
|
|
471
|
+
return spaced.charAt(0).toUpperCase() + spaced.slice(1)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function prettifyEnumValue(value: string): string {
|
|
475
|
+
// 'minimal' → 'Minimal'; 'roof-segment' → 'Roof segment'
|
|
476
|
+
return value
|
|
477
|
+
.split(/[-_\s]/)
|
|
478
|
+
.map((word, i) =>
|
|
479
|
+
i === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word.toLowerCase(),
|
|
480
|
+
)
|
|
481
|
+
.join(' ')
|
|
482
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
23
23
|
import { guideEmitter } from '../../../lib/guide-events'
|
|
24
24
|
import { getGuideImageName } from '../../../lib/local-guide-image'
|
|
25
|
+
import { cn } from '../../../lib/utils'
|
|
25
26
|
import useEditor from '../../../store/use-editor'
|
|
26
27
|
import { ActionButton, ActionGroup } from '../controls/action-button'
|
|
27
28
|
import { PanelSection } from '../controls/panel-section'
|
|
@@ -60,6 +61,9 @@ export function ReferencePanel() {
|
|
|
60
61
|
? (s.nodes[selectedReferenceId as AnyNode['id']] as ReferenceNode | undefined)
|
|
61
62
|
: undefined,
|
|
62
63
|
)
|
|
64
|
+
const isScaleFlowActive = useEditor(
|
|
65
|
+
(s) => s.referenceScaleActiveGuideId !== null && s.referenceScaleActiveGuideId === node?.id,
|
|
66
|
+
)
|
|
63
67
|
|
|
64
68
|
const handleUpdate = useCallback(
|
|
65
69
|
(updates: Partial<ReferenceNode>) => {
|
|
@@ -98,6 +102,9 @@ export function ReferencePanel() {
|
|
|
98
102
|
} as Partial<GuideNode>,
|
|
99
103
|
)
|
|
100
104
|
setGuideScaleReferenceVisible(selectedReferenceId, true)
|
|
105
|
+
// The new image starts uncalibrated — drop the calibration auto-lock
|
|
106
|
+
// so it can be resized/rotated right away.
|
|
107
|
+
setGuideLocked(selectedReferenceId, false)
|
|
101
108
|
} catch {
|
|
102
109
|
setReplaceError('Could not replace that image.')
|
|
103
110
|
} finally {
|
|
@@ -123,6 +130,13 @@ export function ReferencePanel() {
|
|
|
123
130
|
return
|
|
124
131
|
}
|
|
125
132
|
|
|
133
|
+
// The scale line is drawn on the 2D plan — starting from a 3D-only view
|
|
134
|
+
// would arm the flow invisibly inside the hidden floorplan panel.
|
|
135
|
+
const editor = useEditor.getState()
|
|
136
|
+
if (editor.viewMode === '3d') {
|
|
137
|
+
editor.setViewMode('2d')
|
|
138
|
+
}
|
|
139
|
+
|
|
126
140
|
guideEmitter.emit('guide:set-reference-scale', { guideId: node.id })
|
|
127
141
|
}, [node])
|
|
128
142
|
|
|
@@ -233,33 +247,54 @@ export function ReferencePanel() {
|
|
|
233
247
|
|
|
234
248
|
<PanelSection title="Reference Scale">
|
|
235
249
|
<div className="flex items-center gap-2 rounded-md border border-border/50 bg-background/40 px-2.5 py-2 text-sm">
|
|
236
|
-
<Ruler
|
|
250
|
+
<Ruler
|
|
251
|
+
className={cn(
|
|
252
|
+
'h-4 w-4 shrink-0',
|
|
253
|
+
node.scaleReference ? 'text-primary' : 'text-amber-600 dark:text-amber-400',
|
|
254
|
+
)}
|
|
255
|
+
/>
|
|
237
256
|
<span className="truncate text-muted-foreground">{scaleStatus}</span>
|
|
238
257
|
</div>
|
|
239
258
|
|
|
240
|
-
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
259
|
+
{!node.scaleReference && (
|
|
260
|
+
<p className="px-0.5 text-muted-foreground text-xs leading-snug">
|
|
261
|
+
{isScaleFlowActive
|
|
262
|
+
? 'Click both ends of a known distance on the plan, then type its real length.'
|
|
263
|
+
: 'Draw a line over a known dimension on the plan, then type its real length to scale the image exactly.'}
|
|
264
|
+
</p>
|
|
265
|
+
)}
|
|
247
266
|
|
|
248
267
|
<ActionGroup>
|
|
249
268
|
<ActionButton
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
label="Clear Scale"
|
|
260
|
-
onClick={() => handleUpdate({ scaleReference: null } as Partial<GuideNode>)}
|
|
269
|
+
className={cn(
|
|
270
|
+
!node.scaleReference &&
|
|
271
|
+
!isScaleFlowActive &&
|
|
272
|
+
'border-primary/50 bg-primary/15 text-primary hover:bg-primary/25 active:bg-primary/25',
|
|
273
|
+
)}
|
|
274
|
+
label={
|
|
275
|
+
isScaleFlowActive ? 'Cancel' : node.scaleReference ? 'Edit Scale' : 'Set Scale'
|
|
276
|
+
}
|
|
277
|
+
onClick={isScaleFlowActive ? handleCancelScale : handleStartScale}
|
|
261
278
|
/>
|
|
262
279
|
</ActionGroup>
|
|
280
|
+
|
|
281
|
+
{node.scaleReference && (
|
|
282
|
+
<ActionGroup>
|
|
283
|
+
<ActionButton
|
|
284
|
+
label={scaleReferenceVisible ? 'Hide Scale' : 'Show Scale'}
|
|
285
|
+
onClick={() => setGuideScaleReferenceVisible(node.id, !scaleReferenceVisible)}
|
|
286
|
+
/>
|
|
287
|
+
<ActionButton
|
|
288
|
+
label="Clear Scale"
|
|
289
|
+
onClick={() => {
|
|
290
|
+
handleUpdate({ scaleReference: null } as Partial<GuideNode>)
|
|
291
|
+
// Calibrating auto-locked the guide; clearing the scale
|
|
292
|
+
// returns it to a freely-editable reference.
|
|
293
|
+
setGuideLocked(node.id, false)
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</ActionGroup>
|
|
297
|
+
)}
|
|
263
298
|
</PanelSection>
|
|
264
299
|
|
|
265
300
|
<PanelSection title="Quick Actions">
|
|
@@ -197,15 +197,19 @@ function DropdownMenuSubContent({
|
|
|
197
197
|
className,
|
|
198
198
|
...props
|
|
199
199
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
|
200
|
+
// Portalled so the side-opening submenu escapes the parent content's
|
|
201
|
+
// `overflow-x-hidden` clip (otherwise it renders but is invisible).
|
|
200
202
|
return (
|
|
201
|
-
<DropdownMenuPrimitive.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
203
|
+
<DropdownMenuPrimitive.Portal>
|
|
204
|
+
<DropdownMenuPrimitive.SubContent
|
|
205
|
+
className={cn(
|
|
206
|
+
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
|
|
207
|
+
className,
|
|
208
|
+
)}
|
|
209
|
+
data-slot="dropdown-menu-sub-content"
|
|
210
|
+
{...props}
|
|
211
|
+
/>
|
|
212
|
+
</DropdownMenuPrimitive.Portal>
|
|
209
213
|
)
|
|
210
214
|
}
|
|
211
215
|
|