@pascal-app/editor 1.0.3 → 1.0.4
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 +11 -9
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +315 -4
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/custom-camera-controls.tsx +202 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +36 -25
- package/src/components/editor/editor-layout-v2.tsx +131 -58
- package/src/components/editor/export-manager.tsx +34 -8
- package/src/components/editor/first-person/build-collider-world.test.ts +51 -0
- package/src/components/editor/first-person/build-collider-world.ts +6 -2
- package/src/components/editor/first-person/mechanism-targets.test.ts +75 -0
- package/src/components/editor/first-person/mechanism-targets.ts +36 -0
- package/src/components/editor/first-person-controls.tsx +205 -2
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +442 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-panel.tsx +344 -342
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +54 -2
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +294 -169
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +225 -0
- package/src/components/editor/room-labels.test.ts +357 -0
- package/src/components/editor/room-labels.tsx +990 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +451 -317
- package/src/components/editor/slab-hole-highlights.tsx +3 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +4 -0
- package/src/components/editor/snapshot-capture.test.ts +52 -4
- package/src/components/editor/snapshot-capture.ts +30 -5
- package/src/components/editor/thumbnail-generator.test.ts +308 -0
- package/src/components/editor/thumbnail-generator.tsx +1035 -229
- package/src/components/editor/use-floorplan-background-placement.ts +31 -11
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +670 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.tsx +220 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +3 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +113 -102
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +291 -227
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +8 -4
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +93 -46
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +3 -1
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/stair/stair-defaults.ts +0 -3
- package/src/components/tools/stair/stair-tool.tsx +90 -42
- package/src/components/tools/tool-manager.tsx +46 -20
- package/src/components/tools/wall/wall-drafting.test.ts +19 -4
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +3 -0
- package/src/components/ui/action-menu/camera-actions.tsx +2 -1
- package/src/components/ui/action-menu/control-modes.tsx +2 -35
- package/src/components/ui/action-menu/measurement-control.tsx +3 -10
- package/src/components/ui/build-panel/build-panel.tsx +235 -0
- package/src/components/ui/command-palette/editor-commands.tsx +4 -30
- package/src/components/ui/command-palette/index.tsx +5 -24
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/floating-level-selector.tsx +9 -14
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +154 -70
- package/src/components/ui/helpers/helper-manager.test.tsx +69 -0
- package/src/components/ui/helpers/helper-manager.tsx +325 -14
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +20 -14
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +2 -2
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +43 -4
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +78 -36
- package/src/components/ui/panels/parametric-field-control.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +4 -4
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +725 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +2 -0
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +145 -0
- package/src/components/ui/sidebar/icon-rail.tsx +5 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +31 -4
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +176 -211
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +0 -27
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +0 -2
- package/src/components/ui/sidebar/tab-bar.tsx +10 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +175 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +123 -9
- package/src/hooks/use-auto-save.ts +34 -8
- package/src/hooks/use-keyboard.test.ts +178 -3
- package/src/hooks/use-keyboard.ts +89 -71
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +48 -0
- package/src/hooks/use-placement-notice.ts +42 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +246 -39
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +48 -0
- package/src/lib/editor-views.ts +144 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +283 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +339 -18
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +138 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +56 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +100 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +779 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +232 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +156 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +275 -0
- package/src/lib/floorplan/site-plan/geometry.ts +557 -0
- package/src/lib/floorplan/site-plan/index.ts +71 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +366 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1083 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +379 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +69 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/fresh-planar-placement.test.ts +36 -0
- package/src/lib/fresh-planar-placement.ts +59 -36
- package/src/lib/geometry-script/author.ts +152 -0
- package/src/lib/geometry-script/client.ts +152 -0
- package/src/lib/geometry-script/protocol.ts +12 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +485 -0
- package/src/lib/glb-export.ts +237 -119
- package/src/lib/history.test.ts +6 -0
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +18 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +186 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +407 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +165 -0
- package/src/lib/orbit-floor-clearance.ts +106 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +111 -0
- package/src/lib/paint-pass-through.ts +76 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +266 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +1 -1
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +50 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +266 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +973 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +364 -0
- package/src/lib/scene-clipboard.ts +229 -48
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +53 -74
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +569 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +13 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +175 -0
- package/src/store/view-layout.ts +196 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
Redo2,
|
|
32
32
|
Sparkles,
|
|
33
33
|
Square,
|
|
34
|
-
SquareStack,
|
|
35
34
|
Sun,
|
|
36
35
|
Trash2,
|
|
37
36
|
Undo2,
|
|
@@ -40,6 +39,7 @@ import {
|
|
|
40
39
|
import { useEffect } from 'react'
|
|
41
40
|
import { getHistoryCommandState, runRedo, runUndo } from '../../../lib/history'
|
|
42
41
|
import { deleteLevelWithFallbackSelection } from '../../../lib/level-selection'
|
|
42
|
+
import { getSavedSceneDocument } from '../../../lib/scene'
|
|
43
43
|
import { createUnitInBuilding, enterUnitFocus, leaveUnitFocus } from '../../../lib/units'
|
|
44
44
|
import { useCommandRegistry } from '../../../store/use-command-registry'
|
|
45
45
|
import type { StructureTool } from '../../../store/use-editor'
|
|
@@ -50,10 +50,8 @@ export function EditorCommands() {
|
|
|
50
50
|
const register = useCommandRegistry((s) => s.register)
|
|
51
51
|
const { navigateTo, setInputValue, setOpen } = useCommandPalette()
|
|
52
52
|
|
|
53
|
-
const setPhase = useEditor((s) => s.setPhase)
|
|
54
53
|
const armToolMode = useEditor((s) => s.armToolMode)
|
|
55
54
|
const armMaterialPaint = useEditor((s) => s.armMaterialPaint)
|
|
56
|
-
const setStructureLayer = useEditor((s) => s.setStructureLayer)
|
|
57
55
|
const isPreviewMode = useEditor((s) => s.isPreviewMode)
|
|
58
56
|
const setPreviewMode = useEditor((s) => s.setPreviewMode)
|
|
59
57
|
|
|
@@ -79,12 +77,7 @@ export function EditorCommands() {
|
|
|
79
77
|
)
|
|
80
78
|
|
|
81
79
|
const activateTool = (tool: StructureTool) => {
|
|
82
|
-
run(() => {
|
|
83
|
-
setPhase('structure')
|
|
84
|
-
if (tool === 'zone') setStructureLayer('zones')
|
|
85
|
-
else setStructureLayer('elements')
|
|
86
|
-
armToolMode({ mode: 'build', tool })
|
|
87
|
-
})
|
|
80
|
+
run(() => armToolMode({ mode: 'build', tool }))
|
|
88
81
|
}
|
|
89
82
|
|
|
90
83
|
return register([
|
|
@@ -175,11 +168,7 @@ export function EditorCommands() {
|
|
|
175
168
|
keywords: ['paint', 'material', 'texture', 'bucket', 'surface'],
|
|
176
169
|
shortcut: ['P'],
|
|
177
170
|
execute: () =>
|
|
178
|
-
run(() =>
|
|
179
|
-
setPhase('structure')
|
|
180
|
-
setStructureLayer('elements')
|
|
181
|
-
armMaterialPaint()
|
|
182
|
-
}),
|
|
171
|
+
run(() => armMaterialPaint()),
|
|
183
172
|
},
|
|
184
173
|
{
|
|
185
174
|
id: 'editor.mode.terrain-sculpt',
|
|
@@ -315,19 +304,6 @@ export function EditorCommands() {
|
|
|
315
304
|
navigate: true,
|
|
316
305
|
execute: () => navigateTo('wall-mode'),
|
|
317
306
|
},
|
|
318
|
-
{
|
|
319
|
-
id: 'editor.viewer.level-mode',
|
|
320
|
-
label: 'Level Mode',
|
|
321
|
-
group: 'Viewer Controls',
|
|
322
|
-
icon: <SquareStack className="h-4 w-4" />,
|
|
323
|
-
keywords: ['level', 'floor', 'exploded', 'stacked', 'solo'],
|
|
324
|
-
badge: () => {
|
|
325
|
-
const mode = useViewer.getState().levelMode
|
|
326
|
-
return { manual: 'Manual', stacked: 'Stacked', exploded: 'Exploded', solo: 'Solo' }[mode]
|
|
327
|
-
},
|
|
328
|
-
navigate: true,
|
|
329
|
-
execute: () => navigateTo('level-mode'),
|
|
330
|
-
},
|
|
331
307
|
{
|
|
332
308
|
id: 'editor.viewer.camera-mode',
|
|
333
309
|
label: () => {
|
|
@@ -422,7 +398,7 @@ export function EditorCommands() {
|
|
|
422
398
|
keywords: ['export', 'download', 'json', 'save', 'data'],
|
|
423
399
|
execute: () =>
|
|
424
400
|
run(() => {
|
|
425
|
-
const { nodes, rootNodeIds } =
|
|
401
|
+
const { nodes, rootNodeIds } = getSavedSceneDocument()
|
|
426
402
|
const blob = new Blob([JSON.stringify({ nodes, rootNodeIds }, null, 2)], {
|
|
427
403
|
type: 'application/json',
|
|
428
404
|
})
|
|
@@ -476,10 +452,8 @@ export function EditorCommands() {
|
|
|
476
452
|
navigateTo,
|
|
477
453
|
setInputValue,
|
|
478
454
|
setOpen,
|
|
479
|
-
setPhase,
|
|
480
455
|
armToolMode,
|
|
481
456
|
armMaterialPaint,
|
|
482
|
-
setStructureLayer,
|
|
483
457
|
isPreviewMode,
|
|
484
458
|
setPreviewMode,
|
|
485
459
|
exportScene,
|
|
@@ -149,7 +149,6 @@ function OptionItem({
|
|
|
149
149
|
// ---------------------------------------------------------------------------
|
|
150
150
|
const PAGE_LABEL: Record<string, string> = {
|
|
151
151
|
'wall-mode': 'Wall Mode',
|
|
152
|
-
'level-mode': 'Level Mode',
|
|
153
152
|
'rename-level': 'Rename Level',
|
|
154
153
|
'goto-level': 'Go to Level',
|
|
155
154
|
}
|
|
@@ -204,8 +203,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
204
203
|
|
|
205
204
|
const wallMode = useViewer((s) => s.wallMode)
|
|
206
205
|
const setWallMode = useViewer((s) => s.setWallMode)
|
|
207
|
-
const levelMode = useViewer((s) => s.levelMode)
|
|
208
|
-
const setLevelMode = useViewer((s) => s.setLevelMode)
|
|
209
206
|
|
|
210
207
|
const allLevels = useScene(
|
|
211
208
|
useShallow((s) =>
|
|
@@ -250,12 +247,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
250
247
|
down: 'Down',
|
|
251
248
|
translucent: 'Translucent',
|
|
252
249
|
}
|
|
253
|
-
const levelModeLabel: Record<'manual' | 'stacked' | 'exploded' | 'solo', string> = {
|
|
254
|
-
manual: 'Manual',
|
|
255
|
-
stacked: 'Stacked',
|
|
256
|
-
exploded: 'Exploded',
|
|
257
|
-
solo: 'Solo',
|
|
258
|
-
}
|
|
259
250
|
|
|
260
251
|
const confirmRename = () => {
|
|
261
252
|
if (!(activeLevelId && inputValue.trim())) return
|
|
@@ -364,7 +355,11 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
364
355
|
keywords={action.keywords}
|
|
365
356
|
label={action.label}
|
|
366
357
|
navigate={action.navigate}
|
|
367
|
-
|
|
358
|
+
// A registered command that does not navigate within the
|
|
359
|
+
// palette closes it, like the built-ins — otherwise the
|
|
360
|
+
// dialog overlay keeps swallowing clicks under a full-screen
|
|
361
|
+
// surface the command opened (Sheets).
|
|
362
|
+
onSelect={() => (action.navigate ? action.execute() : run(() => action.execute()))}
|
|
368
363
|
shortcut={action.shortcut}
|
|
369
364
|
/>
|
|
370
365
|
))}
|
|
@@ -385,20 +380,6 @@ export function CommandPalette({ emptyAction }: { emptyAction?: CommandPaletteEm
|
|
|
385
380
|
</Command.Group>
|
|
386
381
|
)}
|
|
387
382
|
|
|
388
|
-
{/* ── Level Mode sub-page ───────────────────────────────────── */}
|
|
389
|
-
{page === 'level-mode' && (
|
|
390
|
-
<Command.Group heading="Level Mode">
|
|
391
|
-
{(['stacked', 'exploded', 'solo'] as const).map((mode) => (
|
|
392
|
-
<OptionItem
|
|
393
|
-
isActive={levelMode === mode}
|
|
394
|
-
key={mode}
|
|
395
|
-
label={levelModeLabel[mode]}
|
|
396
|
-
onSelect={() => run(() => setLevelMode(mode))}
|
|
397
|
-
/>
|
|
398
|
-
))}
|
|
399
|
-
</Command.Group>
|
|
400
|
-
)}
|
|
401
|
-
|
|
402
383
|
{/* ── Go to Level sub-page ──────────────────────────────────── */}
|
|
403
384
|
{page === 'goto-level' && (
|
|
404
385
|
<Command.Group heading="Go to Level">
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
getCatalogMaterialById,
|
|
6
|
+
getLibraryMaterialIdFromRef,
|
|
7
|
+
getSceneMaterialIdFromRef,
|
|
8
|
+
nodeRegistry,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../primitives/popover'
|
|
13
|
+
import { MaterialPicker } from './material-picker'
|
|
14
|
+
|
|
15
|
+
type Swatch = { label: string; color: string | null; image: string | null }
|
|
16
|
+
|
|
17
|
+
/** How a finish reference reads in a panel: its catalog tile, its colour, or its scene material. */
|
|
18
|
+
function useFinishSwatch(ref: string | undefined, fallback: string): Swatch {
|
|
19
|
+
const materials = useScene((state) => state.materials)
|
|
20
|
+
if (!ref) return { ...describe(fallback, materials), label: 'Default' }
|
|
21
|
+
return describe(ref, materials)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function describe(ref: string, materials: ReturnType<typeof useScene.getState>['materials']) {
|
|
25
|
+
const library = getCatalogMaterialById(getLibraryMaterialIdFromRef(ref) ?? undefined)
|
|
26
|
+
if (library)
|
|
27
|
+
return {
|
|
28
|
+
label: library.label,
|
|
29
|
+
color: library.previewColor ?? null,
|
|
30
|
+
image: library.previewThumbnailUrl ?? null,
|
|
31
|
+
}
|
|
32
|
+
const sceneId = getSceneMaterialIdFromRef(ref)
|
|
33
|
+
const scene = sceneId ? materials[sceneId as keyof typeof materials] : undefined
|
|
34
|
+
if (scene) return { label: scene.name, color: scene.material.properties?.color ?? null, image: null }
|
|
35
|
+
if (ref.startsWith('#')) return { label: ref.toUpperCase(), color: ref, image: null }
|
|
36
|
+
return { label: 'Default', color: null, image: null }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Paints one surface of a node from its panel, exactly as the paint tool does:
|
|
41
|
+
* the kind's own paint commit for that role (same write, one undo step).
|
|
42
|
+
*/
|
|
43
|
+
export function paintSurface(node: AnyNode, role: string, materialPreset: string) {
|
|
44
|
+
const paint = nodeRegistry.get(node.type)?.capabilities?.paint
|
|
45
|
+
if (!paint) return
|
|
46
|
+
const args = { node, role, material: undefined, materialPreset }
|
|
47
|
+
if (paint.commit) paint.commit(args)
|
|
48
|
+
else useScene.getState().updateNode(node.id, paint.buildPatch(args) as Partial<AnyNode>)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A finish row: the swatch and its name. A click opens the paint tool's
|
|
53
|
+
* material picker; a pick paints the surface through the paint tool's path.
|
|
54
|
+
*/
|
|
55
|
+
export function FinishSwatch({
|
|
56
|
+
label,
|
|
57
|
+
node,
|
|
58
|
+
role,
|
|
59
|
+
value,
|
|
60
|
+
fallback,
|
|
61
|
+
hint,
|
|
62
|
+
}: {
|
|
63
|
+
label: string
|
|
64
|
+
node: AnyNode
|
|
65
|
+
/** The paint role of the surface (`edge`, `foundation`, …). */
|
|
66
|
+
role: string
|
|
67
|
+
/** The authored finish reference; absent shows `fallback` as "Default". */
|
|
68
|
+
value: string | undefined
|
|
69
|
+
fallback: string
|
|
70
|
+
/** Tooltip on the row. */
|
|
71
|
+
hint?: string
|
|
72
|
+
}) {
|
|
73
|
+
const [open, setOpen] = useState(false)
|
|
74
|
+
const swatch = useFinishSwatch(value, fallback)
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex min-h-8 items-center justify-between gap-3" title={hint}>
|
|
77
|
+
<span className="text-muted-foreground text-xs">{label}</span>
|
|
78
|
+
<Popover onOpenChange={setOpen} open={open}>
|
|
79
|
+
<PopoverTrigger asChild>
|
|
80
|
+
<button
|
|
81
|
+
aria-label={`${label}: ${swatch.label}`}
|
|
82
|
+
className="flex min-w-0 items-center gap-2 rounded-full border border-border/50 py-0.5 pr-2.5 pl-1 text-xs transition-colors hover:bg-accent"
|
|
83
|
+
data-finish-swatch={label}
|
|
84
|
+
type="button"
|
|
85
|
+
>
|
|
86
|
+
<span
|
|
87
|
+
aria-hidden
|
|
88
|
+
className="size-4 shrink-0 overflow-hidden rounded-full border border-border/50"
|
|
89
|
+
style={{ backgroundColor: swatch.color ?? 'transparent' }}
|
|
90
|
+
>
|
|
91
|
+
{swatch.image && <img alt="" className="size-full object-cover" src={swatch.image} />}
|
|
92
|
+
</span>
|
|
93
|
+
<span className="truncate">{swatch.label}</span>
|
|
94
|
+
</button>
|
|
95
|
+
</PopoverTrigger>
|
|
96
|
+
<PopoverContent align="end" className="flex h-96 w-80 flex-col p-2" side="left">
|
|
97
|
+
<MaterialPicker
|
|
98
|
+
onSelectMaterialPreset={(ref) => {
|
|
99
|
+
const live = useScene.getState().nodes[node.id as keyof ReturnType<typeof useScene.getState>['nodes']]
|
|
100
|
+
if (live) paintSurface(live, role, ref)
|
|
101
|
+
setOpen(false)
|
|
102
|
+
}}
|
|
103
|
+
selectOnCategoryChange={false}
|
|
104
|
+
selectedMaterialPreset={value}
|
|
105
|
+
/>
|
|
106
|
+
</PopoverContent>
|
|
107
|
+
</Popover>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { type SceneMaterialId } from '@pascal-app/core'
|
|
4
|
-
import { Eraser, Plus,
|
|
4
|
+
import { Eraser, PaintBucket, Pentagon, Pipette, Plus, Square } from 'lucide-react'
|
|
5
5
|
import { useState } from 'react'
|
|
6
6
|
import { useMaterialPaintPanelModel } from '../../../lib/material-paint-panel-model'
|
|
7
|
+
import { type PaintMode, usePaintRegionMode } from '../../../lib/paint-region-mode'
|
|
8
|
+
import useEditor, { armMaterialPaint } from '../../../store/use-editor'
|
|
7
9
|
import { Button } from '../primitives/button'
|
|
8
10
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
|
|
9
11
|
import { MaterialPicker } from './material-picker'
|
|
10
12
|
import { SceneMaterialList } from './scene-material-list'
|
|
13
|
+
import { SegmentedControl } from './segmented-control'
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Material picker for paint mode. Embedders render this wherever paint controls
|
|
14
17
|
* belong (the community editor places it in the Build sidebar while paint mode
|
|
15
18
|
* is active). It fills its container's height and lays out as three bands: a
|
|
16
|
-
* fixed
|
|
19
|
+
* fixed mode header, a single scrolling catalog grid, and a fixed
|
|
17
20
|
* scene-material footer (always visible, with a `+` to add a custom material).
|
|
18
21
|
*/
|
|
19
22
|
export type MaterialPaintPanelProps = {
|
|
@@ -21,37 +24,103 @@ export type MaterialPaintPanelProps = {
|
|
|
21
24
|
onCreateMaterialRequest?: () => void
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
const PAINT_MODE_OPTIONS: { value: PaintMode; label: string; icon: typeof PaintBucket }[] = [
|
|
28
|
+
{ value: 'surface', label: 'Whole surface: a click paints the surface under it', icon: PaintBucket },
|
|
29
|
+
{
|
|
30
|
+
value: 'rectangle',
|
|
31
|
+
label: 'Rectangle: press anywhere on a wall, floor or ceiling, drag to the opposite corner',
|
|
32
|
+
icon: Square,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'polygon',
|
|
36
|
+
label: 'Polygon: click the points of a shape on a floor or ceiling',
|
|
37
|
+
icon: Pentagon,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: 'erase',
|
|
41
|
+
label: 'Erase: click a painted part to remove it, or a painted surface to reset it',
|
|
42
|
+
icon: Eraser,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: 'pick',
|
|
46
|
+
label: 'Pick: click a surface to paint with its material (or hold Alt / Option)',
|
|
47
|
+
icon: Pipette,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Paint the whole surface, draw part of a wall, floor or ceiling first, erase, or
|
|
53
|
+
* pick a material off a surface. Erase
|
|
54
|
+
* is the eraser the paint path already knows (a click writes "no finish"), so a
|
|
55
|
+
* painted region goes and a whole-surface paint returns to its default. The row
|
|
56
|
+
* shows a choice only while paint mode is on; any pick (erase included) turns it
|
|
57
|
+
* on, so nothing here needs a colour first.
|
|
58
|
+
*/
|
|
59
|
+
function PaintModeControl() {
|
|
60
|
+
const painting = useEditor((s) => s.mode === 'material-paint')
|
|
61
|
+
const mode = usePaintRegionMode((s) => s.mode)
|
|
62
|
+
const select = (next: PaintMode) => {
|
|
63
|
+
if (painting) usePaintRegionMode.getState().setMode(next)
|
|
64
|
+
else armMaterialPaint(undefined, next)
|
|
65
|
+
}
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className="shrink-0 space-y-1.5 pb-2"
|
|
69
|
+
data-paint-mode={painting ? mode : 'off'}
|
|
70
|
+
// A clicked option must not keep focus: the next shortcut (V) would light
|
|
71
|
+
// its focus ring and it would read as still chosen after paint mode ends.
|
|
72
|
+
onPointerUp={(event) => (event.target as HTMLElement).closest('button')?.blur()}
|
|
73
|
+
>
|
|
74
|
+
<div className="px-0.5 font-medium text-muted-foreground text-xs">
|
|
75
|
+
{painting && mode === 'erase' ? 'Erase' : painting && mode === 'pick' ? 'Pick material' : 'Paint'}
|
|
76
|
+
</div>
|
|
77
|
+
<SegmentedControl
|
|
78
|
+
mixed={!painting}
|
|
79
|
+
onChange={select}
|
|
80
|
+
options={PAINT_MODE_OPTIONS.map(({ value, label, icon: Icon }) => ({
|
|
81
|
+
value,
|
|
82
|
+
label: (
|
|
83
|
+
<Tooltip>
|
|
84
|
+
<TooltipTrigger asChild>
|
|
85
|
+
<span aria-label={label} className="flex items-center justify-center">
|
|
86
|
+
<Icon className="h-3.5 w-3.5" />
|
|
87
|
+
</span>
|
|
88
|
+
</TooltipTrigger>
|
|
89
|
+
<TooltipContent>{label}</TooltipContent>
|
|
90
|
+
</Tooltip>
|
|
91
|
+
),
|
|
92
|
+
}))}
|
|
93
|
+
value={mode}
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
24
99
|
export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPanelProps) {
|
|
25
|
-
const
|
|
100
|
+
const painting = useEditor((s) => s.mode === 'material-paint')
|
|
101
|
+
const erasing = usePaintRegionMode((s) => s.mode) === 'erase' && painting
|
|
102
|
+
const {
|
|
103
|
+
activePaintMaterial,
|
|
104
|
+
materialCount,
|
|
105
|
+
selectMaterial,
|
|
106
|
+
createCustomMaterial: createMaterial,
|
|
107
|
+
} = useMaterialPaintPanelModel()
|
|
26
108
|
const [autoEditMaterialId, setAutoEditMaterialId] = useState<SceneMaterialId | null>(null)
|
|
27
109
|
const createCustomMaterial = () => setAutoEditMaterialId(createMaterial())
|
|
28
110
|
|
|
29
111
|
return (
|
|
30
112
|
<div className="flex h-full min-h-0 w-full flex-col">
|
|
31
|
-
{/* Fixed:
|
|
32
|
-
<
|
|
33
|
-
<Button
|
|
34
|
-
aria-pressed={paintEraser}
|
|
35
|
-
className="flex-1"
|
|
36
|
-
onClick={() => setPaintEraser(!paintEraser)}
|
|
37
|
-
size="sm"
|
|
38
|
-
variant={paintEraser ? 'default' : 'outline'}
|
|
39
|
-
>
|
|
40
|
-
<Eraser />
|
|
41
|
-
Erase
|
|
42
|
-
</Button>
|
|
43
|
-
<Button
|
|
44
|
-
className="flex-1"
|
|
45
|
-
disabled={!canResetSelection}
|
|
46
|
-
onClick={resetSelection}
|
|
47
|
-
size="sm"
|
|
48
|
-
variant="outline"
|
|
49
|
-
>
|
|
50
|
-
<RotateCcw />
|
|
51
|
-
Reset all
|
|
52
|
-
</Button>
|
|
53
|
-
</div>
|
|
113
|
+
{/* Fixed: whole surface, part of a surface, or erase. */}
|
|
114
|
+
<PaintModeControl />
|
|
54
115
|
|
|
116
|
+
{/* Erasing paints nothing: no swatches to pick from, just what a click does. */}
|
|
117
|
+
{erasing ? (
|
|
118
|
+
<p className="px-0.5 py-1 text-muted-foreground text-xs" data-paint-erase-note>
|
|
119
|
+
Click a painted part of a wall, floor or ceiling to remove it, or a painted surface to reset it to
|
|
120
|
+
its default.
|
|
121
|
+
</p>
|
|
122
|
+
) : (
|
|
123
|
+
<>
|
|
55
124
|
{/* Scrolls: category tabs (fixed inside) + catalog grid (the scroll). */}
|
|
56
125
|
{/* A stable hook for host-app onboarding to point at. Static, and read
|
|
57
126
|
only from outside: nothing here depends on it. */}
|
|
@@ -94,6 +163,8 @@ export function MaterialPaintPanel({ onCreateMaterialRequest }: MaterialPaintPan
|
|
|
94
163
|
)}
|
|
95
164
|
</div>
|
|
96
165
|
</div>
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
97
168
|
</div>
|
|
98
169
|
)
|
|
99
170
|
}
|
|
@@ -14,6 +14,8 @@ export type MaterialPickerProps = {
|
|
|
14
14
|
nodeType?: MaterialTarget
|
|
15
15
|
hideSideControl?: boolean
|
|
16
16
|
onCreateMaterialRequest?: () => void
|
|
17
|
+
/** Whether switching category tabs selects the tab's first material (paint mode does). */
|
|
18
|
+
selectOnCategoryChange?: boolean
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
function getCategoryLabel(category: string) {
|
|
@@ -31,8 +33,9 @@ export function MaterialPicker({
|
|
|
31
33
|
onSelectMaterialPreset,
|
|
32
34
|
disabled = false,
|
|
33
35
|
onCreateMaterialRequest,
|
|
36
|
+
selectOnCategoryChange = true,
|
|
34
37
|
}: MaterialPickerProps) {
|
|
35
|
-
const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled)
|
|
38
|
+
const { selectedCategory, setSelectedCategory, sourceFilter, setSourceFilter, visibleSourceFilters, availableCategories, catalogItems, select: handleCatalogSelect } = useMaterialCatalogModel(selectedMaterialPreset, onSelectMaterialPreset, disabled, selectOnCategoryChange)
|
|
36
39
|
|
|
37
40
|
return (
|
|
38
41
|
<div
|
|
@@ -10,21 +10,13 @@ import {
|
|
|
10
10
|
} from '@pascal-app/core'
|
|
11
11
|
import { Copy, Paintbrush, Pencil, Trash2 } from 'lucide-react'
|
|
12
12
|
import { useEffect, useMemo, useState } from 'react'
|
|
13
|
+
import { sceneMaterialUsageCounts } from '../../../lib/scene-material-refs'
|
|
13
14
|
import useEditor from '../../../store/use-editor'
|
|
14
15
|
import { Button } from '../primitives/button'
|
|
15
16
|
import { Input } from '../primitives/input'
|
|
16
17
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../primitives/tooltip'
|
|
17
18
|
import { MaterialPropertiesEditor } from './material-properties-editor'
|
|
18
19
|
|
|
19
|
-
type SlotRecord = Record<string, string | undefined>
|
|
20
|
-
|
|
21
|
-
function getSlotRecord(node: unknown): SlotRecord | null {
|
|
22
|
-
if (!node || typeof node !== 'object' || !('slots' in node)) return null
|
|
23
|
-
const slots = (node as { slots?: unknown }).slots
|
|
24
|
-
if (!slots || typeof slots !== 'object' || Array.isArray(slots)) return null
|
|
25
|
-
return slots as SlotRecord
|
|
26
|
-
}
|
|
27
|
-
|
|
28
20
|
export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId | null }) {
|
|
29
21
|
const materials = useScene((state) => state.materials)
|
|
30
22
|
const nodes = useScene((state) => state.nodes)
|
|
@@ -40,29 +32,9 @@ export function SceneMaterialList({ autoEditId }: { autoEditId?: SceneMaterialId
|
|
|
40
32
|
[materials],
|
|
41
33
|
)
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const [id] of materialEntries) {
|
|
48
|
-
counts.set(id, 0)
|
|
49
|
-
refToId.set(toSceneMaterialRef(id), id)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
for (const node of Object.values(nodes)) {
|
|
53
|
-
const slots = getSlotRecord(node)
|
|
54
|
-
if (!slots) continue
|
|
55
|
-
|
|
56
|
-
for (const value of Object.values(slots)) {
|
|
57
|
-
if (typeof value !== 'string') continue
|
|
58
|
-
const materialId = refToId.get(value)
|
|
59
|
-
if (!materialId) continue
|
|
60
|
-
counts.set(materialId, (counts.get(materialId) ?? 0) + 1)
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return counts
|
|
65
|
-
}, [materialEntries, nodes])
|
|
35
|
+
// Every place a scene material can be used — slots and the room, wall and
|
|
36
|
+
// footprint finishes — counted by the same walker saving and pasting use.
|
|
37
|
+
const usageCounts = useMemo(() => sceneMaterialUsageCounts(nodes), [nodes])
|
|
66
38
|
|
|
67
39
|
return (
|
|
68
40
|
<div className="space-y-2">
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ChevronDown } from 'lucide-react'
|
|
4
|
+
import { cn } from '../../../lib/utils'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuRadioGroup,
|
|
9
|
+
DropdownMenuRadioItem,
|
|
10
|
+
DropdownMenuTrigger,
|
|
11
|
+
} from '../primitives/dropdown-menu'
|
|
12
|
+
|
|
13
|
+
interface SelectControlProps<T extends string> {
|
|
14
|
+
label: string
|
|
15
|
+
value: T
|
|
16
|
+
onChange: (value: T) => void
|
|
17
|
+
options: { label: string; value: T }[]
|
|
18
|
+
className?: string
|
|
19
|
+
mixed?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function SelectControl<T extends string>({
|
|
23
|
+
label,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
options,
|
|
27
|
+
className,
|
|
28
|
+
mixed = false,
|
|
29
|
+
}: SelectControlProps<T>) {
|
|
30
|
+
const current = mixed ? 'Mixed' : options.find((option) => option.value === value)?.label
|
|
31
|
+
return (
|
|
32
|
+
<DropdownMenu>
|
|
33
|
+
<DropdownMenuTrigger asChild>
|
|
34
|
+
<button
|
|
35
|
+
aria-label={label}
|
|
36
|
+
className={cn(
|
|
37
|
+
'group flex h-10 w-full items-center justify-between gap-2 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 text-sm transition-colors hover:bg-[#3e3e3e] data-[state=open]:bg-[#3e3e3e]',
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
type="button"
|
|
41
|
+
>
|
|
42
|
+
<span className="select-none text-muted-foreground transition-colors group-hover:text-foreground">
|
|
43
|
+
{label}
|
|
44
|
+
</span>
|
|
45
|
+
<span className="flex min-w-0 items-center gap-1.5 text-foreground">
|
|
46
|
+
<span className="truncate">{current}</span>
|
|
47
|
+
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
|
|
48
|
+
</span>
|
|
49
|
+
</button>
|
|
50
|
+
</DropdownMenuTrigger>
|
|
51
|
+
<DropdownMenuContent align="end" className="min-w-(--radix-dropdown-menu-trigger-width)">
|
|
52
|
+
<DropdownMenuRadioGroup
|
|
53
|
+
onValueChange={(next) => onChange(next as T)}
|
|
54
|
+
value={mixed ? '' : value}
|
|
55
|
+
>
|
|
56
|
+
{options.map((option) => (
|
|
57
|
+
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
|
58
|
+
{option.label}
|
|
59
|
+
</DropdownMenuRadioItem>
|
|
60
|
+
))}
|
|
61
|
+
</DropdownMenuRadioGroup>
|
|
62
|
+
</DropdownMenuContent>
|
|
63
|
+
</DropdownMenu>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -50,12 +50,13 @@ import { pasteSelectionAndPickUp } from '../editor/group-actions'
|
|
|
50
50
|
import {
|
|
51
51
|
buildLevelDuplicateCreateOps,
|
|
52
52
|
type LevelDuplicatePreset,
|
|
53
|
-
} from '
|
|
53
|
+
} from '@pascal-app/core/building'
|
|
54
54
|
import { getDefaultLevelName, getLevelDisplayName } from '@pascal-app/core'
|
|
55
55
|
import { deleteLevelWithFallbackSelection } from '../../lib/level-selection'
|
|
56
56
|
import { unitMemberLevels, leaveUnitFocus } from '../../lib/units'
|
|
57
57
|
import { useLinearDisplay } from '../../lib/use-linear-display'
|
|
58
58
|
import { cn } from '../../lib/utils'
|
|
59
|
+
import { useIsMobile } from '../../hooks/use-mobile'
|
|
59
60
|
import { ActionButton } from './controls/action-button'
|
|
60
61
|
import { SliderControl } from './controls/slider-control'
|
|
61
62
|
import { LevelDuplicateDialog } from './level-duplicate-dialog'
|
|
@@ -448,11 +449,13 @@ function UnitFocusChip() {
|
|
|
448
449
|
// ── Main component ──────────────────────────────────────────────────────────
|
|
449
450
|
|
|
450
451
|
export function FloatingLevelSelector() {
|
|
452
|
+
// Desktop's view bar is a row above the stage; mobile's floats over it.
|
|
453
|
+
const isMobile = useIsMobile()
|
|
451
454
|
const selectedBuildingId = useViewer((s) => s.selection.buildingId)
|
|
452
455
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
453
456
|
const setSelection = useViewer((s) => s.setSelection)
|
|
454
457
|
const createNode = useScene((s) => s.createNode)
|
|
455
|
-
const
|
|
458
|
+
const applyNodeChanges = useScene((s) => s.applyNodeChanges)
|
|
456
459
|
const updateNodes = useScene((s) => s.updateNodes)
|
|
457
460
|
|
|
458
461
|
const [deletingLevel, setDeletingLevel] = useState<LevelNode | null>(null)
|
|
@@ -560,29 +563,21 @@ export function FloatingLevelSelector() {
|
|
|
560
563
|
|
|
561
564
|
const handleDuplicateLevel = useCallback(
|
|
562
565
|
(level: LevelNode, preset: LevelDuplicatePreset = 'everything') => {
|
|
563
|
-
const { createOps, newLevelId,
|
|
566
|
+
const { createOps, newLevelId, updateOps } = buildLevelDuplicateCreateOps({
|
|
564
567
|
nodes: useScene.getState().nodes,
|
|
565
568
|
level,
|
|
566
569
|
levels,
|
|
567
570
|
preset,
|
|
568
571
|
})
|
|
569
572
|
|
|
570
|
-
|
|
571
|
-
updateNodes(
|
|
572
|
-
shiftedLevels.map((shiftedLevel) => ({
|
|
573
|
-
id: shiftedLevel.id as AnyNodeId,
|
|
574
|
-
data: { level: shiftedLevel.level } as Partial<AnyNode>,
|
|
575
|
-
})),
|
|
576
|
-
)
|
|
577
|
-
}
|
|
578
|
-
createNodes(createOps)
|
|
573
|
+
applyNodeChanges({ create: createOps, update: updateOps })
|
|
579
574
|
|
|
580
575
|
setSelection({
|
|
581
576
|
buildingId: resolvedBuildingId ?? undefined,
|
|
582
577
|
levelId: newLevelId as LevelNode['id'],
|
|
583
578
|
})
|
|
584
579
|
},
|
|
585
|
-
[
|
|
580
|
+
[applyNodeChanges, levels, resolvedBuildingId, setSelection],
|
|
586
581
|
)
|
|
587
582
|
|
|
588
583
|
const handlePasteToLevel = useCallback((level: LevelNode) => {
|
|
@@ -641,7 +636,7 @@ export function FloatingLevelSelector() {
|
|
|
641
636
|
|
|
642
637
|
return (
|
|
643
638
|
<>
|
|
644
|
-
<div className=
|
|
639
|
+
<div className={cn('pointer-events-auto absolute left-3 z-20', isMobile ? 'top-14' : 'top-3')}>
|
|
645
640
|
<div className="relative">
|
|
646
641
|
{/* Floating + at top edge */}
|
|
647
642
|
{!draggingLevelId && (
|
|
@@ -1,14 +1,17 @@
|
|
|
1
|
+
import type { HudTitle } from '../../../lib/hud-title'
|
|
1
2
|
import { ContextualHelperPanel } from './contextual-helper-panel'
|
|
2
3
|
|
|
3
4
|
interface BuildingHelperProps {
|
|
4
5
|
showRotate?: boolean
|
|
6
|
+
title?: HudTitle | null
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
// Rotate is one hint with both keys (R / T) — never two separate
|
|
8
10
|
// counterclockwise / clockwise rows — to match every other placement helper.
|
|
9
|
-
export function BuildingHelper({ showRotate }: BuildingHelperProps) {
|
|
11
|
+
export function BuildingHelper({ showRotate, title = null }: BuildingHelperProps) {
|
|
10
12
|
return (
|
|
11
13
|
<ContextualHelperPanel
|
|
14
|
+
title={title}
|
|
12
15
|
hints={[
|
|
13
16
|
{ keys: ['Left click'], label: 'Place building' },
|
|
14
17
|
...(showRotate ? [{ keys: ['R', 'T'], label: 'Rotate' }] : []),
|