@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
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
emitter,
|
|
4
|
+
useScene,
|
|
5
|
+
type ZoneNode,
|
|
6
|
+
} from '@pascal-app/core'
|
|
2
7
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
|
-
import { Camera, Hexagon, Trash2 } from 'lucide-react'
|
|
8
|
+
import { Camera, Hexagon, Save, Trash2 } from 'lucide-react'
|
|
4
9
|
import { useState } from 'react'
|
|
10
|
+
import { sfxEmitter } from './../../../../../lib/sfx-bus'
|
|
11
|
+
import { collectZoneContentIds } from './../../../../../lib/zone-content'
|
|
5
12
|
import { ColorDot } from './../../../../../components/ui/primitives/color-dot'
|
|
6
13
|
import {
|
|
7
14
|
Popover,
|
|
@@ -10,6 +17,8 @@ import {
|
|
|
10
17
|
} from './../../../../../components/ui/primitives/popover'
|
|
11
18
|
import { cn } from './../../../../../lib/utils'
|
|
12
19
|
import useEditor from './../../../../../store/use-editor'
|
|
20
|
+
import { ActionButton } from '../../../controls/action-button'
|
|
21
|
+
import { PanelSection } from '../../../controls/panel-section'
|
|
13
22
|
|
|
14
23
|
function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
15
24
|
const [cameraPopoverOpen, setCameraPopoverOpen] = useState(false)
|
|
@@ -26,6 +35,7 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
|
26
35
|
|
|
27
36
|
const handleDelete = (e: React.MouseEvent) => {
|
|
28
37
|
e.stopPropagation()
|
|
38
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
29
39
|
deleteNode(zone.id)
|
|
30
40
|
if (isSelected) {
|
|
31
41
|
setSelection({ zoneId: null })
|
|
@@ -125,6 +135,8 @@ function ZoneItem({ zone }: { zone: ZoneNode }) {
|
|
|
125
135
|
export function ZonePanel() {
|
|
126
136
|
const nodes = useScene((state) => state.nodes)
|
|
127
137
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
138
|
+
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
|
139
|
+
const setSelection = useViewer((state) => state.setSelection)
|
|
128
140
|
const setPhase = useEditor((state) => state.setPhase)
|
|
129
141
|
const setMode = useEditor((state) => state.setMode)
|
|
130
142
|
const setTool = useEditor((state) => state.setTool)
|
|
@@ -133,6 +145,7 @@ export function ZonePanel() {
|
|
|
133
145
|
const levelZones = Object.values(nodes).filter(
|
|
134
146
|
(node): node is ZoneNode => node.type === 'zone' && node.parentId === currentLevelId,
|
|
135
147
|
)
|
|
148
|
+
const selectedZone = levelZones.find((zone) => zone.id === selectedZoneId)
|
|
136
149
|
|
|
137
150
|
const handleAddZone = () => {
|
|
138
151
|
if (currentLevelId) {
|
|
@@ -142,6 +155,17 @@ export function ZonePanel() {
|
|
|
142
155
|
}
|
|
143
156
|
}
|
|
144
157
|
|
|
158
|
+
const deleteSelectedZone = (withContent: boolean) => {
|
|
159
|
+
if (!selectedZone) return
|
|
160
|
+
const scene = useScene.getState()
|
|
161
|
+
const ids = withContent
|
|
162
|
+
? [selectedZone.id as AnyNodeId, ...collectZoneContentIds(scene.nodes, selectedZone)]
|
|
163
|
+
: [selectedZone.id as AnyNodeId]
|
|
164
|
+
sfxEmitter.emit('sfx:structure-delete')
|
|
165
|
+
scene.deleteNodes(Array.from(new Set(ids)))
|
|
166
|
+
setSelection({ selectedIds: [], zoneId: null })
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
if (!currentLevelId) {
|
|
146
170
|
return (
|
|
147
171
|
<div className="px-3 py-4 text-muted-foreground text-sm">
|
|
@@ -162,6 +186,31 @@ export function ZonePanel() {
|
|
|
162
186
|
) : (
|
|
163
187
|
levelZones.map((zone) => <ZoneItem key={zone.id} zone={zone} />)
|
|
164
188
|
)}
|
|
189
|
+
{selectedZone ? (
|
|
190
|
+
<PanelSection className="mt-2 border-t" title="Actions">
|
|
191
|
+
<ActionButton
|
|
192
|
+
className="w-full flex-none"
|
|
193
|
+
icon={<Save className="h-4 w-4" />}
|
|
194
|
+
label="Save to catalog"
|
|
195
|
+
onClick={() => emitter.emit('room-preset:create', { zoneId: selectedZone.id })}
|
|
196
|
+
type="button"
|
|
197
|
+
/>
|
|
198
|
+
<ActionButton
|
|
199
|
+
className="w-full flex-none"
|
|
200
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
201
|
+
label="Delete"
|
|
202
|
+
onClick={() => deleteSelectedZone(false)}
|
|
203
|
+
type="button"
|
|
204
|
+
/>
|
|
205
|
+
<ActionButton
|
|
206
|
+
className="w-full flex-none"
|
|
207
|
+
icon={<Trash2 className="h-4 w-4 text-red-400" />}
|
|
208
|
+
label="Delete with contents"
|
|
209
|
+
onClick={() => deleteSelectedZone(true)}
|
|
210
|
+
type="button"
|
|
211
|
+
/>
|
|
212
|
+
</PanelSection>
|
|
213
|
+
) : null}
|
|
165
214
|
</div>
|
|
166
215
|
)
|
|
167
216
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import type { ReactNode } from 'react'
|
|
4
|
+
import { editorHostPanelRegistry } from '../../../lib/plugin-panels'
|
|
5
|
+
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
4
6
|
import { cn } from './../../../lib/utils'
|
|
7
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../primitives/tooltip'
|
|
5
8
|
|
|
6
9
|
export type SidebarTab = {
|
|
7
10
|
id: string
|
|
8
11
|
label: string
|
|
9
12
|
mobileDefaultSnap?: number
|
|
10
13
|
mobileIcon?: ReactNode
|
|
14
|
+
/** Desktop icon shown in the vertical rail (v2 layout). */
|
|
15
|
+
icon?: ReactNode
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
interface TabBarProps {
|
|
@@ -30,7 +35,11 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
|
30
35
|
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground',
|
|
31
36
|
)}
|
|
32
37
|
key={tab.id}
|
|
33
|
-
onClick={() =>
|
|
38
|
+
onClick={() => {
|
|
39
|
+
triggerSFX('sfx:menu-click')
|
|
40
|
+
onTabChange(tab.id)
|
|
41
|
+
}}
|
|
42
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
34
43
|
type="button"
|
|
35
44
|
>
|
|
36
45
|
{tab.label}
|
|
@@ -40,3 +49,68 @@ export function TabBar({ tabs, activeTab, onTabChange }: TabBarProps) {
|
|
|
40
49
|
</div>
|
|
41
50
|
)
|
|
42
51
|
}
|
|
52
|
+
|
|
53
|
+
interface IconRailProps {
|
|
54
|
+
tabs: SidebarTab[]
|
|
55
|
+
/** Highlighted tab. Stays highlighted while the panel is collapsed. */
|
|
56
|
+
activeTab: string
|
|
57
|
+
/** True when the panel beside the rail is collapsed. */
|
|
58
|
+
collapsed: boolean
|
|
59
|
+
/** Clicking a rail icon: switch tab, or toggle the panel (see layout). */
|
|
60
|
+
onIconClick: (id: string) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Vertical icon rail for the v2 left column. Always visible (even when the
|
|
65
|
+
* panel is collapsed) so the user can reopen the panel by clicking an icon.
|
|
66
|
+
* The label renders as a hover tooltip on the right.
|
|
67
|
+
*/
|
|
68
|
+
export function IconRail({ tabs, activeTab, collapsed, onIconClick }: IconRailProps) {
|
|
69
|
+
const pluginPanelIds = new Set(
|
|
70
|
+
editorHostPanelRegistry.getSnapshot().flatMap((panel) =>
|
|
71
|
+
panel.pluginId ? [panel.id] : [],
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
const defaultTabs = tabs.filter((tab) => !pluginPanelIds.has(tab.id) && tab.id !== 'plugins')
|
|
75
|
+
const pluginTabs = tabs.filter((tab) => pluginPanelIds.has(tab.id) || tab.id === 'plugins')
|
|
76
|
+
|
|
77
|
+
const renderTab = (tab: SidebarTab) => {
|
|
78
|
+
const showActive = activeTab === tab.id && !collapsed
|
|
79
|
+
return (
|
|
80
|
+
<Tooltip key={tab.id}>
|
|
81
|
+
<TooltipTrigger asChild>
|
|
82
|
+
<button
|
|
83
|
+
className={cn(
|
|
84
|
+
'group flex h-11 w-11 items-center justify-center rounded-xl transition-all duration-200 [&_img]:transition-[opacity,filter] [&_img]:duration-200',
|
|
85
|
+
showActive
|
|
86
|
+
? 'bg-accent text-foreground shadow-sm [&_img]:opacity-100 [&_img]:grayscale-0'
|
|
87
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground [&_img]:opacity-60 [&_img]:grayscale hover:[&_img]:opacity-100 hover:[&_img]:grayscale-0',
|
|
88
|
+
)}
|
|
89
|
+
onClick={() => {
|
|
90
|
+
triggerSFX('sfx:menu-click')
|
|
91
|
+
onIconClick(tab.id)
|
|
92
|
+
}}
|
|
93
|
+
onMouseEnter={() => triggerSFX('sfx:menu-hover')}
|
|
94
|
+
type="button"
|
|
95
|
+
>
|
|
96
|
+
{tab.icon ?? tab.label.charAt(0)}
|
|
97
|
+
</button>
|
|
98
|
+
</TooltipTrigger>
|
|
99
|
+
<TooltipContent side="right">{tab.label}</TooltipContent>
|
|
100
|
+
</Tooltip>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<TooltipProvider delayDuration={0} disableHoverableContent>
|
|
106
|
+
<div className="flex h-full w-14 shrink-0 flex-col items-center gap-1 border-border/50 border-r py-2">
|
|
107
|
+
{defaultTabs.map(renderTab)}
|
|
108
|
+
{pluginTabs.length > 0 && (
|
|
109
|
+
<div className="mt-1 flex w-11 flex-col items-center gap-1 border-border/70 border-t pt-2">
|
|
110
|
+
{pluginTabs.map(renderTab)}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
</TooltipProvider>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@iconify/react'
|
|
4
|
+
import { type IconRef, useScene } from '@pascal-app/core'
|
|
5
|
+
import { Plus } from 'lucide-react'
|
|
6
|
+
import {
|
|
7
|
+
type ComponentType,
|
|
8
|
+
lazy,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
Suspense,
|
|
11
|
+
useEffect,
|
|
12
|
+
useSyncExternalStore,
|
|
13
|
+
} from 'react'
|
|
14
|
+
import useEditor from '../../../store/use-editor'
|
|
15
|
+
import { editorHostPanelRegistry, type EditorHostPanel } from '../../../lib/plugin-panels'
|
|
16
|
+
import { ErrorBoundary } from '../primitives/error-boundary'
|
|
17
|
+
import type { ExtraPanel } from './icon-rail'
|
|
18
|
+
import { PluginsPanel } from './panels/plugins-panel'
|
|
19
|
+
|
|
20
|
+
const pluginsManagerPanel: ExtraPanel = {
|
|
21
|
+
id: 'plugins',
|
|
22
|
+
label: 'Plugins',
|
|
23
|
+
icon: <Plus className="h-5 w-5" />,
|
|
24
|
+
component: PluginsPanel,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Resolve a plugin's {@link IconRef} into a rail-sized React node. Mirrors the
|
|
28
|
+
* inspector's `renderIcon`, sized for the 24px icon-rail button. */
|
|
29
|
+
function renderIconRef(ref: IconRef): ReactNode {
|
|
30
|
+
if (ref.kind === 'url') {
|
|
31
|
+
return <img alt="" className="h-5 w-5 object-contain" src={ref.src} />
|
|
32
|
+
}
|
|
33
|
+
if (ref.kind === 'iconify') {
|
|
34
|
+
return <Icon height={20} icon={ref.name} width={20} />
|
|
35
|
+
}
|
|
36
|
+
if (ref.kind === 'svg') {
|
|
37
|
+
return (
|
|
38
|
+
<svg height={20} viewBox={ref.viewBox} width={20}>
|
|
39
|
+
<path d={ref.path} fill="currentColor" />
|
|
40
|
+
</svg>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
const LazyIcon = lazy(ref.module)
|
|
44
|
+
return (
|
|
45
|
+
<Suspense fallback={null}>
|
|
46
|
+
<LazyIcon />
|
|
47
|
+
</Suspense>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function PluginPanelCrashed({ label }: { label: string }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="flex flex-col gap-2 p-4 text-sm">
|
|
54
|
+
<p className="font-medium text-sidebar-foreground">"{label}" plugin crashed</p>
|
|
55
|
+
<p className="text-sidebar-foreground/50 text-xs">
|
|
56
|
+
This panel hit an error and was unloaded for this session. The rest of the editor is
|
|
57
|
+
unaffected — reload to try again.
|
|
58
|
+
</p>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// `React.lazy` must be called once per loader so the resolved component keeps a
|
|
64
|
+
// stable identity across renders (otherwise switching panels remounts the tree
|
|
65
|
+
// every time the sidebar re-renders). Cache the wrapped component by its loader.
|
|
66
|
+
const wrappedPanelCache = new WeakMap<EditorHostPanel['component'], ComponentType>()
|
|
67
|
+
|
|
68
|
+
function resolvePanelComponent(panel: EditorHostPanel): ComponentType {
|
|
69
|
+
const cached = wrappedPanelCache.get(panel.component)
|
|
70
|
+
if (cached) return cached
|
|
71
|
+
const Lazy = lazy(panel.component)
|
|
72
|
+
const Wrapped: ComponentType = () => (
|
|
73
|
+
<ErrorBoundary fallback={<PluginPanelCrashed label={panel.label} />}>
|
|
74
|
+
<Suspense fallback={<div className="p-4 text-sidebar-foreground/50 text-sm">Loading…</div>}>
|
|
75
|
+
<Lazy />
|
|
76
|
+
</Suspense>
|
|
77
|
+
</ErrorBoundary>
|
|
78
|
+
)
|
|
79
|
+
Wrapped.displayName = `PluginPanel(${panel.id})`
|
|
80
|
+
wrappedPanelCache.set(panel.component, Wrapped)
|
|
81
|
+
return Wrapped
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Merge host-registered panels (from the observable {@link editorHostPanelRegistry})
|
|
86
|
+
* with the host's `extraPanels`, returning the combined list the icon rail and
|
|
87
|
+
* panel-content area render. Host panels keep their leading order and win on id
|
|
88
|
+
* collisions. Subscribes to the registry so a panel that registers after the
|
|
89
|
+
* first render makes the rail re-render.
|
|
90
|
+
*
|
|
91
|
+
* Panels are filtered by the current workspace: a panel surfaces only in the
|
|
92
|
+
* workspaces it declares (`EditorHostPanel.workspaces`, default `['edit']`), so an
|
|
93
|
+
* authoring panel like Nature doesn't ride into the studio rail.
|
|
94
|
+
*/
|
|
95
|
+
export function useHostPanels(hostPanels?: ExtraPanel[]): ExtraPanel[] {
|
|
96
|
+
const registered = useSyncExternalStore(
|
|
97
|
+
editorHostPanelRegistry.subscribe,
|
|
98
|
+
editorHostPanelRegistry.getSnapshot,
|
|
99
|
+
editorHostPanelRegistry.getSnapshot,
|
|
100
|
+
)
|
|
101
|
+
const workspaceMode = useEditor((s) => s.workspaceMode)
|
|
102
|
+
const installedPlugins = useScene((s) => s.installedPlugins)
|
|
103
|
+
const hostIds = new Set(hostPanels?.map((p) => p.id))
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const scene = useScene.getState()
|
|
107
|
+
if (scene.hasExplicitPluginInstallState) return
|
|
108
|
+
const defaults = editorHostPanelRegistry.getDefaultInstalledPluginIds()
|
|
109
|
+
if (defaults.every((pluginId) => scene.installedPlugins.includes(pluginId))) return
|
|
110
|
+
scene.setInstalledPlugins([...scene.installedPlugins, ...defaults], { explicit: false })
|
|
111
|
+
}, [registered])
|
|
112
|
+
|
|
113
|
+
const fromRegistry = registered
|
|
114
|
+
.filter(
|
|
115
|
+
(p) =>
|
|
116
|
+
!hostIds.has(p.id) &&
|
|
117
|
+
(p.workspaces ?? ['edit']).includes(workspaceMode) &&
|
|
118
|
+
(!p.pluginId || installedPlugins.includes(p.pluginId)),
|
|
119
|
+
)
|
|
120
|
+
.map(
|
|
121
|
+
(p): ExtraPanel => ({
|
|
122
|
+
id: p.id,
|
|
123
|
+
label: p.label,
|
|
124
|
+
icon: renderIconRef(p.icon),
|
|
125
|
+
component: resolvePanelComponent(p),
|
|
126
|
+
pluginId: p.pluginId,
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
const manager =
|
|
130
|
+
workspaceMode === 'edit' && !hostIds.has(pluginsManagerPanel.id) ? [pluginsManagerPanel] : []
|
|
131
|
+
return [...(hostPanels ?? []), ...fromRegistry, ...manager]
|
|
132
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import { resolveAssetSnapTarget, resolveNodeSnapTarget } from './snap-target-badge'
|
|
4
|
+
|
|
5
|
+
describe('resolveAssetSnapTarget', () => {
|
|
6
|
+
test('maps wall-hosted catalog assets to a wall badge', () => {
|
|
7
|
+
expect(resolveAssetSnapTarget('wall')).toBe('wall')
|
|
8
|
+
expect(resolveAssetSnapTarget('wall-side')).toBe('wall')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('maps ceiling-hosted catalog assets to a ceiling badge', () => {
|
|
12
|
+
expect(resolveAssetSnapTarget('ceiling')).toBe('ceiling')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('does not badge floor assets', () => {
|
|
16
|
+
expect(resolveAssetSnapTarget(undefined)).toBeNull()
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('resolveNodeSnapTarget', () => {
|
|
21
|
+
test('prefers roof attachment when a node is hosted by a roof segment', () => {
|
|
22
|
+
const node = {
|
|
23
|
+
id: 'window_1',
|
|
24
|
+
type: 'window',
|
|
25
|
+
roofSegmentId: 'roof-segment_1',
|
|
26
|
+
} as unknown as AnyNode
|
|
27
|
+
|
|
28
|
+
expect(resolveNodeSnapTarget(node)).toBe('roof')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('badges gutter-hosted downspouts as roof accessories', () => {
|
|
32
|
+
const node = {
|
|
33
|
+
id: 'downspout_1',
|
|
34
|
+
type: 'downspout',
|
|
35
|
+
gutterId: 'gutter_1',
|
|
36
|
+
} as unknown as AnyNode
|
|
37
|
+
|
|
38
|
+
expect(resolveNodeSnapTarget(node)).toBe('roof')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AnyNode, AssetInput } from '@pascal-app/core'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cn } from '../../lib/utils'
|
|
4
|
+
|
|
5
|
+
export type SnapTarget = 'wall' | 'ceiling' | 'roof'
|
|
6
|
+
export type SnapTargetBadgeSize = 'tile' | 'tree'
|
|
7
|
+
|
|
8
|
+
const SNAP_TARGET_ICONS: Record<SnapTarget, string> = {
|
|
9
|
+
wall: '/icons/wall.webp',
|
|
10
|
+
ceiling: '/icons/ceiling.webp',
|
|
11
|
+
roof: '/icons/roof.webp',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SNAP_TARGET_LABELS: Record<SnapTarget, string> = {
|
|
15
|
+
wall: 'Wall attachment',
|
|
16
|
+
ceiling: 'Ceiling attachment',
|
|
17
|
+
roof: 'Roof attachment',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SNAP_TARGET_BADGE_SIZE_CLASSES: Record<SnapTargetBadgeSize, string> = {
|
|
21
|
+
tile: 'h-6 w-6 rounded-md',
|
|
22
|
+
tree: 'h-3.5 w-3.5 rounded-[3px]',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SNAP_TARGET_ICON_SIZE_CLASSES: Record<SnapTargetBadgeSize, string> = {
|
|
26
|
+
tile: 'h-[18px] w-[18px]',
|
|
27
|
+
tree: 'h-2.5 w-2.5',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveAssetSnapTarget(attachTo: AssetInput['attachTo']): SnapTarget | null {
|
|
31
|
+
if (attachTo === 'wall' || attachTo === 'wall-side') return 'wall'
|
|
32
|
+
if (attachTo === 'ceiling') return 'ceiling'
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resolveNodeSnapTarget(node: AnyNode | null | undefined): SnapTarget | null {
|
|
37
|
+
if (!node) return null
|
|
38
|
+
if ('roofSegmentId' in node && typeof node.roofSegmentId === 'string') return 'roof'
|
|
39
|
+
if (node.type === 'downspout') return 'roof'
|
|
40
|
+
if (node.type === 'door' || node.type === 'window') return 'wall'
|
|
41
|
+
if (node.type === 'item') return resolveAssetSnapTarget(node.asset?.attachTo)
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function SnapTargetBadge({
|
|
46
|
+
className,
|
|
47
|
+
size = 'tile',
|
|
48
|
+
target,
|
|
49
|
+
}: {
|
|
50
|
+
className?: string
|
|
51
|
+
size?: SnapTargetBadgeSize
|
|
52
|
+
target: SnapTarget
|
|
53
|
+
}) {
|
|
54
|
+
return (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
'flex items-center justify-center bg-black/65 ring-1 ring-white/20',
|
|
58
|
+
SNAP_TARGET_BADGE_SIZE_CLASSES[size],
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<img
|
|
63
|
+
alt={SNAP_TARGET_LABELS[target]}
|
|
64
|
+
className={cn('object-contain', SNAP_TARGET_ICON_SIZE_CLASSES[size])}
|
|
65
|
+
src={SNAP_TARGET_ICONS[target]}
|
|
66
|
+
/>
|
|
67
|
+
</span>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function SnapTargetIcon({
|
|
72
|
+
children,
|
|
73
|
+
target,
|
|
74
|
+
}: {
|
|
75
|
+
children: ReactNode
|
|
76
|
+
target: SnapTarget
|
|
77
|
+
}) {
|
|
78
|
+
return (
|
|
79
|
+
<span className="relative inline-flex h-5 w-5 items-center justify-center">
|
|
80
|
+
{children}
|
|
81
|
+
<SnapTargetBadge
|
|
82
|
+
className="-right-1.5 -bottom-1.5 absolute"
|
|
83
|
+
size="tree"
|
|
84
|
+
target={target}
|
|
85
|
+
/>
|
|
86
|
+
</span>
|
|
87
|
+
)
|
|
88
|
+
}
|