@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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
DoorNode,
|
|
5
|
+
LevelNode,
|
|
6
|
+
reconcileSceneStructure,
|
|
7
|
+
SlabNode,
|
|
8
|
+
WallNode,
|
|
9
|
+
ZoneNode,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { paintScopeRole, resolvePaintScopeTargets } from './paint-scope'
|
|
12
|
+
import {
|
|
13
|
+
mergePaintSurfaces,
|
|
14
|
+
paintSurfaceMeshes,
|
|
15
|
+
plateAffectedSurfaces,
|
|
16
|
+
platePreviewSurfaces,
|
|
17
|
+
} from './plate-paint-affected'
|
|
18
|
+
|
|
19
|
+
const square = (x: number): [number, number][] => [
|
|
20
|
+
[x, 0],
|
|
21
|
+
[x + 4, 0],
|
|
22
|
+
[x + 4, 4],
|
|
23
|
+
[x, 4],
|
|
24
|
+
]
|
|
25
|
+
const level = LevelNode.parse({ id: 'level_paint' })
|
|
26
|
+
const kitchen = ZoneNode.parse({
|
|
27
|
+
id: 'zone_kitchen',
|
|
28
|
+
name: 'Kitchen',
|
|
29
|
+
parentId: level.id,
|
|
30
|
+
spaceRole: 'room',
|
|
31
|
+
polygon: square(0),
|
|
32
|
+
})
|
|
33
|
+
const hall = ZoneNode.parse({
|
|
34
|
+
id: 'zone_hall',
|
|
35
|
+
name: 'Hall',
|
|
36
|
+
parentId: level.id,
|
|
37
|
+
spaceRole: 'room',
|
|
38
|
+
polygon: square(4),
|
|
39
|
+
floorStepFinish: 'library:oak',
|
|
40
|
+
floorEdgeFinish: 'library:stone',
|
|
41
|
+
})
|
|
42
|
+
const base = SlabNode.parse({
|
|
43
|
+
id: 'slab_base',
|
|
44
|
+
parentId: level.id,
|
|
45
|
+
boundary: 'auto',
|
|
46
|
+
plateRole: 'base',
|
|
47
|
+
polygon: [
|
|
48
|
+
[0, 0],
|
|
49
|
+
[8, 0],
|
|
50
|
+
[8, 4],
|
|
51
|
+
[0, 4],
|
|
52
|
+
],
|
|
53
|
+
zoneIds: [kitchen.id, hall.id],
|
|
54
|
+
})
|
|
55
|
+
const platform = SlabNode.parse({
|
|
56
|
+
id: 'slab_platform',
|
|
57
|
+
parentId: level.id,
|
|
58
|
+
boundary: 'auto',
|
|
59
|
+
plateRole: 'platform',
|
|
60
|
+
polygon: square(4),
|
|
61
|
+
zoneIds: [hall.id],
|
|
62
|
+
})
|
|
63
|
+
const manual = SlabNode.parse({ id: 'slab_manual', parentId: level.id, polygon: square(20) })
|
|
64
|
+
const nodes = Object.fromEntries(
|
|
65
|
+
[level, kitchen, hall, base, platform, manual].map((n) => [n.id, n]),
|
|
66
|
+
) as Record<string, AnyNode>
|
|
67
|
+
|
|
68
|
+
const keys = (surfaces: ReturnType<typeof plateAffectedSurfaces>) =>
|
|
69
|
+
(surfaces ?? []).map((s) => `${s.nodeId}:${s.role}`).sort()
|
|
70
|
+
|
|
71
|
+
describe('what one paint click changes on the floor plates (point 7)', () => {
|
|
72
|
+
test('a room’s steps are one finish on every plate of the level', () => {
|
|
73
|
+
expect(keys(plateAffectedSurfaces(nodes, base, 'step:zone_kitchen'))).toEqual([
|
|
74
|
+
'slab_base:step:zone_kitchen',
|
|
75
|
+
'slab_platform:step:zone_kitchen',
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('a room floor with painted steps or a region paint stands alone', () => {
|
|
80
|
+
expect(keys(plateAffectedSurfaces(nodes, platform, 'room:zone_hall'))).toEqual([
|
|
81
|
+
'slab_base:room:zone_hall',
|
|
82
|
+
'slab_platform:room:zone_hall',
|
|
83
|
+
])
|
|
84
|
+
expect(keys(plateAffectedSurfaces(nodes, base, 'room:zone_kitchen/region_1'))).toEqual([
|
|
85
|
+
'slab_base:room:zone_kitchen/region_1',
|
|
86
|
+
'slab_platform:room:zone_kitchen/region_1',
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('the footprint edge band carries the edges of its rooms that have no edge finish', () => {
|
|
91
|
+
expect(keys(plateAffectedSurfaces(nodes, base, 'edge'))).toEqual([
|
|
92
|
+
'slab_base:edge',
|
|
93
|
+
'slab_base:edge:zone_kitchen',
|
|
94
|
+
'slab_platform:edge:zone_kitchen',
|
|
95
|
+
])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('other plate faces are just themselves; a hand-drawn slab keeps its own path', () => {
|
|
99
|
+
expect(keys(plateAffectedSurfaces(nodes, base, 'riser'))).toEqual(['slab_base:riser'])
|
|
100
|
+
expect(plateAffectedSurfaces(nodes, manual, 'surface')).toBeNull()
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
/** A | B | C in a row, a door in each dividing wall, B raised: one step per door. */
|
|
105
|
+
function doorwayRow(): Record<string, AnyNode> {
|
|
106
|
+
const ring = (x0: number, x1: number): [number, number][] => [
|
|
107
|
+
[x0, 0],
|
|
108
|
+
[x1, 0],
|
|
109
|
+
[x1, 4],
|
|
110
|
+
[x0, 4],
|
|
111
|
+
]
|
|
112
|
+
const outline = ring(0, 12)
|
|
113
|
+
const walls = outline.map((start, i) =>
|
|
114
|
+
WallNode.parse({ parentId: 'level_row', start, end: outline[(i + 1) % 4], thickness: 0.2 }),
|
|
115
|
+
)
|
|
116
|
+
const doors = ['door_ab', 'door_bc'].map((id) =>
|
|
117
|
+
DoorNode.parse({
|
|
118
|
+
id,
|
|
119
|
+
parentId: `wall_${id}`,
|
|
120
|
+
wallId: `wall_${id}`,
|
|
121
|
+
width: 1,
|
|
122
|
+
height: 2,
|
|
123
|
+
position: [2, 1, 0],
|
|
124
|
+
}),
|
|
125
|
+
)
|
|
126
|
+
const dividers = [4, 8].map((x, i) =>
|
|
127
|
+
WallNode.parse({
|
|
128
|
+
id: `wall_${doors[i]!.id}`,
|
|
129
|
+
parentId: 'level_row',
|
|
130
|
+
start: [x, 0],
|
|
131
|
+
end: [x, 4],
|
|
132
|
+
thickness: 0.2,
|
|
133
|
+
children: [doors[i]!.id],
|
|
134
|
+
}),
|
|
135
|
+
)
|
|
136
|
+
const zones = ['a', 'b', 'c'].map((name, i) =>
|
|
137
|
+
ZoneNode.parse({
|
|
138
|
+
id: `zone_${name}`,
|
|
139
|
+
parentId: 'level_row',
|
|
140
|
+
name,
|
|
141
|
+
spaceRole: 'room',
|
|
142
|
+
enclosureStatus: 'enclosed',
|
|
143
|
+
polygon: ring(i * 4, (i + 1) * 4),
|
|
144
|
+
floor: { elevation: i === 1 ? 0.3 : 0.05 },
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
const children = [...walls, ...dividers, ...zones]
|
|
148
|
+
const level = LevelNode.parse({ id: 'level_row', height: 3, children: children.map((n) => n.id) })
|
|
149
|
+
let minted = 0
|
|
150
|
+
return reconcileSceneStructure({
|
|
151
|
+
nodes: Object.fromEntries([level, ...children, ...doors].map((n) => [n.id, n])),
|
|
152
|
+
mintId: (kind) => `${kind}_row${++minted}`,
|
|
153
|
+
}).nodes as Record<string, AnyNode>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// B is raised between A and C, a door on each side: B owns one step per door.
|
|
157
|
+
describe('doorway steps: hover, preview and commit are the same set', () => {
|
|
158
|
+
const row = doorwayRow()
|
|
159
|
+
const plates = Object.values(row).filter(
|
|
160
|
+
(node): node is SlabNode => node.type === 'slab' && node.boundary === 'auto',
|
|
161
|
+
)
|
|
162
|
+
const plate = plates.find((slab) => slab.plateRole === 'platform')!
|
|
163
|
+
const onPlates = (...roles: string[]) =>
|
|
164
|
+
plates.flatMap((slab) => roles.map((role) => `${slab.id}:${role}`)).sort()
|
|
165
|
+
type Obj = {
|
|
166
|
+
userData: Record<string, unknown>
|
|
167
|
+
isMesh?: boolean
|
|
168
|
+
traverse: (visit: (o: Obj) => void) => void
|
|
169
|
+
}
|
|
170
|
+
const mesh = (paintRole: string): Obj => ({
|
|
171
|
+
isMesh: true,
|
|
172
|
+
userData: { paintRole, __fromGeometry: true },
|
|
173
|
+
traverse(visit) {
|
|
174
|
+
visit(this)
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
const floorB = mesh('room:zone_b')
|
|
178
|
+
const stepAB = mesh('step:zone_b/door_ab')
|
|
179
|
+
const stepBC = mesh('step:zone_b/door_bc')
|
|
180
|
+
const root: Obj = {
|
|
181
|
+
userData: {},
|
|
182
|
+
traverse(visit) {
|
|
183
|
+
for (const child of [floorB, stepAB, stepBC]) visit(child)
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
const outlined = (surfaces: ReturnType<typeof plateAffectedSurfaces>) =>
|
|
187
|
+
paintSurfaceMeshes(surfaces!, (id) => (id === plate.id ? root : null))
|
|
188
|
+
const targets = (role: string, scope: 'single' | 'room') =>
|
|
189
|
+
resolvePaintScopeTargets({
|
|
190
|
+
node: plate,
|
|
191
|
+
role,
|
|
192
|
+
scope,
|
|
193
|
+
nodes: row,
|
|
194
|
+
spaces: {},
|
|
195
|
+
slotRolesOf: () => [],
|
|
196
|
+
})
|
|
197
|
+
.map((target) => `${target.nodeId}:${target.role}`)
|
|
198
|
+
.sort()
|
|
199
|
+
|
|
200
|
+
test('one step: that doorway only, on every plate', () => {
|
|
201
|
+
const role = 'step:zone_b/door_bc'
|
|
202
|
+
expect(paintScopeRole(plate, role, 'single')).toBe(role)
|
|
203
|
+
expect(targets(role, 'single')).toEqual(onPlates(role))
|
|
204
|
+
const affected = plateAffectedSurfaces(row, plate, role)
|
|
205
|
+
expect(keys(affected)).toEqual(onPlates(role))
|
|
206
|
+
expect(outlined(affected)).toEqual([stepBC])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('the room scope: every step of the room', () => {
|
|
210
|
+
const role = paintScopeRole(plate, 'step:zone_b/door_bc', 'room')
|
|
211
|
+
expect(role).toBe('step:zone_b')
|
|
212
|
+
expect(targets('step:zone_b/door_bc', 'room')).toEqual(onPlates(role))
|
|
213
|
+
expect(outlined(plateAffectedSurfaces(row, plate, role))).toEqual([stepAB, stepBC])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('a floor paint carries only the steps that follow the floor', () => {
|
|
217
|
+
expect(keys(plateAffectedSurfaces(row, plate, 'room:zone_b'))).toEqual(
|
|
218
|
+
onPlates('room:zone_b', 'step:zone_b/door_ab', 'step:zone_b/door_bc'),
|
|
219
|
+
)
|
|
220
|
+
const painted = {
|
|
221
|
+
...row,
|
|
222
|
+
zone_b: { ...row.zone_b!, floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }] },
|
|
223
|
+
} as Record<string, AnyNode>
|
|
224
|
+
const affected = plateAffectedSurfaces(painted, plate, 'room:zone_b')
|
|
225
|
+
expect(keys(affected)).toEqual(onPlates('room:zone_b', 'step:zone_b/door_ab'))
|
|
226
|
+
expect(outlined(affected)).toEqual([floorB, stepAB])
|
|
227
|
+
// The preview is the click targets plus the fallbacks, once each.
|
|
228
|
+
expect(mergePaintSurfaces([{ nodeId: plate.id, role: 'room:zone_b' }], affected)).toHaveLength(
|
|
229
|
+
affected!.length,
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
describe('the whole room: what it paints, erases, and where it previews', () => {
|
|
235
|
+
const row = doorwayRow()
|
|
236
|
+
const plates = Object.values(row).filter(
|
|
237
|
+
(node): node is SlabNode => node.type === 'slab' && node.boundary === 'auto',
|
|
238
|
+
)
|
|
239
|
+
const plate = plates.find((slab) => slab.plateRole === 'platform')!
|
|
240
|
+
const onPlates = (...roles: string[]) =>
|
|
241
|
+
plates.flatMap((slab) => roles.map((role) => `${slab.id}:${role}`)).sort()
|
|
242
|
+
const zoneB = row.zone_b as ZoneNode
|
|
243
|
+
const withB = (patch: Partial<ZoneNode>) =>
|
|
244
|
+
({ ...row, zone_b: { ...zoneB, ...patch } }) as Record<string, AnyNode>
|
|
245
|
+
|
|
246
|
+
test('painted, the whole room is its floor, its painted parts and the steps that follow it', () => {
|
|
247
|
+
const rug = withB({
|
|
248
|
+
floor: {
|
|
249
|
+
...zoneB.floor,
|
|
250
|
+
regions: [{ id: 'rug', polygon: square(4), finish: 'library:red' }],
|
|
251
|
+
},
|
|
252
|
+
} as Partial<ZoneNode>)
|
|
253
|
+
expect(keys(plateAffectedSurfaces(rug, plate, 'room:zone_b/*'))).toEqual(
|
|
254
|
+
onPlates('room:zone_b', 'room:zone_b/rug', 'step:zone_b/door_ab', 'step:zone_b/door_bc'),
|
|
255
|
+
)
|
|
256
|
+
const nodes = withB({ floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }] })
|
|
257
|
+
expect(keys(plateAffectedSurfaces(nodes, plate, 'room:zone_b/*'))).toEqual(
|
|
258
|
+
onPlates('room:zone_b', 'step:zone_b/door_ab'),
|
|
259
|
+
)
|
|
260
|
+
// A room step finish holds the steps: the floor goes alone.
|
|
261
|
+
expect(
|
|
262
|
+
keys(plateAffectedSurfaces(withB({ floorStepFinish: 'library:y' }), plate, 'room:zone_b/*')),
|
|
263
|
+
).toEqual(onPlates('room:zone_b'))
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('erased, it also clears its painted parts and its edge; doorway paint stays', () => {
|
|
267
|
+
const nodes = withB({
|
|
268
|
+
floorStepFinish: 'library:y',
|
|
269
|
+
floorEdgeFinish: 'library:stone',
|
|
270
|
+
floorStepOverrides: [{ key: 'door_bc', finish: 'library:x' }],
|
|
271
|
+
floor: {
|
|
272
|
+
...zoneB.floor,
|
|
273
|
+
finish: 'library:oak',
|
|
274
|
+
regions: [{ id: 'rug', polygon: square(4), finish: 'library:red' }],
|
|
275
|
+
},
|
|
276
|
+
} as Partial<ZoneNode>)
|
|
277
|
+
expect(keys(plateAffectedSurfaces(nodes, plate, 'room:zone_b/*', { erasing: true }))).toEqual(
|
|
278
|
+
onPlates('edge:zone_b', 'room:zone_b', 'room:zone_b/rug', 'step:zone_b/door_ab'),
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('a floor previews on every plate; its steps are drawn by that preview', () => {
|
|
283
|
+
expect(keys(platePreviewSurfaces(row, plate, 'room:zone_b'))).toEqual(onPlates('room:zone_b'))
|
|
284
|
+
expect(keys(platePreviewSurfaces(row, plate, 'room:zone_b/*'))).toEqual(
|
|
285
|
+
onPlates('room:zone_b/*'),
|
|
286
|
+
)
|
|
287
|
+
expect(keys(platePreviewSurfaces(row, plate, 'step:zone_b'))).toEqual(onPlates('step:zone_b'))
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe("a footprint's edge band previews where its rooms' edges are drawn", () => {
|
|
292
|
+
test('the band itself, and the plates drawing the edges that carry it', () => {
|
|
293
|
+
expect(keys(platePreviewSurfaces(nodes, base, 'edge'))).toEqual([
|
|
294
|
+
'slab_base:edge',
|
|
295
|
+
'slab_platform:edge',
|
|
296
|
+
])
|
|
297
|
+
expect(keys(platePreviewSurfaces(nodes, base, 'foundation'))).toEqual(['slab_base:foundation'])
|
|
298
|
+
expect(platePreviewSurfaces(nodes, manual, 'surface')).toBeNull()
|
|
299
|
+
})
|
|
300
|
+
})
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
floorStepKeysOf,
|
|
5
|
+
floorStepOverrideFor,
|
|
6
|
+
floorStepRole,
|
|
7
|
+
floorStepRoleCovers,
|
|
8
|
+
parseCeilingRegionRole,
|
|
9
|
+
parseFloorStepRole,
|
|
10
|
+
parseRoomFinishRole,
|
|
11
|
+
roomFinishRole,
|
|
12
|
+
type SlabNode,
|
|
13
|
+
type ZoneNode,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
import { roomFootprint } from './floor-footprints'
|
|
17
|
+
|
|
18
|
+
export type PaintSurface = { nodeId: AnyNodeId; role: string }
|
|
19
|
+
|
|
20
|
+
const isFloorPlate = (node: AnyNode | undefined): node is SlabNode =>
|
|
21
|
+
node?.type === 'slab' && node.boundary === 'auto'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Every floor-plate surface one paint click changes: the painted surface and
|
|
25
|
+
* the surfaces whose finish falls back to it. A room's floor carries its steps
|
|
26
|
+
* until they are painted on their own (room-wide or per doorway); the
|
|
27
|
+
* footprint's edge band carries the edges of the rooms on it that have no edge
|
|
28
|
+
* finish; a step paints its doorway, or in the room scope (`step:<zoneId>`)
|
|
29
|
+
* every step of the room, wherever they are drawn. A region paint changes only
|
|
30
|
+
* the region. The whole room (`room:<zoneId>/*`) paints its floor, its painted
|
|
31
|
+
* parts and the steps that follow it; erased, it also clears its steps and edge.
|
|
32
|
+
*
|
|
33
|
+
* This set is the hover outline; `platePreviewSurfaces` previews it, so what
|
|
34
|
+
* lights up is exactly what the click changes. Null for anything but a floor
|
|
35
|
+
* plate.
|
|
36
|
+
*/
|
|
37
|
+
export function plateAffectedSurfaces(
|
|
38
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
39
|
+
node: AnyNode,
|
|
40
|
+
role: string,
|
|
41
|
+
options: { erasing?: boolean } = {},
|
|
42
|
+
): PaintSurface[] | null {
|
|
43
|
+
if (!isFloorPlate(node)) return null
|
|
44
|
+
const plates = Object.values(nodes).filter(
|
|
45
|
+
(other): other is SlabNode => isFloorPlate(other) && other.parentId === node.parentId,
|
|
46
|
+
)
|
|
47
|
+
const onEveryPlate = (paintRole: string) =>
|
|
48
|
+
plates.map((plate) => ({ nodeId: plate.id as AnyNodeId, role: paintRole }))
|
|
49
|
+
const zoneOf = (zoneId: string) => {
|
|
50
|
+
const zone = nodes[zoneId]
|
|
51
|
+
return zone?.type === 'zone' ? zone : null
|
|
52
|
+
}
|
|
53
|
+
// The room's steps that no doorway paint holds: the ones its floor carries.
|
|
54
|
+
const followingSteps = (zone: ZoneNode) => {
|
|
55
|
+
const zones = Object.values(nodes).filter(
|
|
56
|
+
(other): other is ZoneNode => other.type === 'zone' && other.parentId === zone.parentId,
|
|
57
|
+
)
|
|
58
|
+
return floorStepKeysOf(nodes, zone.parentId ?? '', zone.id)
|
|
59
|
+
.filter((key) => !floorStepOverrideFor(zone, key, null, zones))
|
|
60
|
+
.flatMap((key) => onEveryPlate(floorStepRole(zone.id, key)))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (parseFloorStepRole(role)) return onEveryPlate(role)
|
|
64
|
+
|
|
65
|
+
const floor = parseRoomFinishRole(role)
|
|
66
|
+
if (floor) {
|
|
67
|
+
const zone = zoneOf(floor.zoneId)
|
|
68
|
+
if (floor.regionId === '*') {
|
|
69
|
+
const surfaces = onEveryPlate(roomFinishRole(floor.zoneId))
|
|
70
|
+
if (!zone) return surfaces
|
|
71
|
+
for (const region of zone.floor?.regions ?? [])
|
|
72
|
+
surfaces.push(...onEveryPlate(roomFinishRole(zone.id, region.id)))
|
|
73
|
+
if (!options.erasing)
|
|
74
|
+
return zone.floorStepFinish ? surfaces : [...surfaces, ...followingSteps(zone)]
|
|
75
|
+
surfaces.push(...followingSteps(zone))
|
|
76
|
+
if (zone.floorEdgeFinish) surfaces.push(...onEveryPlate(`edge:${zone.id}`))
|
|
77
|
+
return surfaces
|
|
78
|
+
}
|
|
79
|
+
const surfaces = onEveryPlate(role)
|
|
80
|
+
if (floor.regionId || !zone || zone.floorStepFinish) return surfaces
|
|
81
|
+
return [...surfaces, ...followingSteps(zone)]
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (role === 'edge' && node.plateRole === 'base') {
|
|
85
|
+
const surfaces: PaintSurface[] = [{ nodeId: node.id as AnyNodeId, role }]
|
|
86
|
+
for (const zone of Object.values(nodes)) {
|
|
87
|
+
if (zone.type !== 'zone' || zone.parentId !== node.parentId || zone.floorEdgeFinish) continue
|
|
88
|
+
if (roomFootprint(nodes as Record<string, AnyNode>, zone.id)?.id !== node.id) continue
|
|
89
|
+
surfaces.push(...onEveryPlate(`edge:${zone.id}`))
|
|
90
|
+
}
|
|
91
|
+
return surfaces
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [{ nodeId: node.id as AnyNodeId, role }]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The (plate, role) pairs whose paint preview draws `plateAffectedSurfaces`.
|
|
99
|
+
* A surface that follows another (a room's unpainted steps, the room edges on
|
|
100
|
+
* a footprint's band) is drawn by the preview of the surface it follows on the
|
|
101
|
+
* same plate: only that preview knows what the follower shows once its leader
|
|
102
|
+
* is erased. So a floor and a whole room preview on every plate, and a
|
|
103
|
+
* footprint's edge band on itself and on the plates drawing its rooms' edges.
|
|
104
|
+
*/
|
|
105
|
+
export function platePreviewSurfaces(
|
|
106
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
107
|
+
node: AnyNode,
|
|
108
|
+
role: string,
|
|
109
|
+
): PaintSurface[] | null {
|
|
110
|
+
if (!isFloorPlate(node)) return null
|
|
111
|
+
if (parseFloorStepRole(role) || parseRoomFinishRole(role))
|
|
112
|
+
return Object.values(nodes)
|
|
113
|
+
.filter((other) => isFloorPlate(other) && other.parentId === node.parentId)
|
|
114
|
+
.map((plate) => ({ nodeId: plate.id as AnyNodeId, role }))
|
|
115
|
+
if (role === 'edge' && node.plateRole === 'base')
|
|
116
|
+
return mergePaintSurfaces(
|
|
117
|
+
[{ nodeId: node.id as AnyNodeId, role }],
|
|
118
|
+
(plateAffectedSurfaces(nodes, node, role) ?? []).map((surface) => ({
|
|
119
|
+
nodeId: surface.nodeId,
|
|
120
|
+
role,
|
|
121
|
+
})),
|
|
122
|
+
)
|
|
123
|
+
return [{ nodeId: node.id as AnyNodeId, role }]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A ceiling's painted part: the click changes that region and nothing else. */
|
|
127
|
+
export function ceilingAffectedSurfaces(node: AnyNode, role: string): PaintSurface[] | null {
|
|
128
|
+
return node.type === 'ceiling' && parseCeilingRegionRole(role)
|
|
129
|
+
? [{ nodeId: node.id as AnyNodeId, role }]
|
|
130
|
+
: null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The targets a preview paints: the click's own targets plus what falls back to them. */
|
|
134
|
+
export function mergePaintSurfaces(
|
|
135
|
+
first: readonly PaintSurface[],
|
|
136
|
+
second: readonly PaintSurface[] | null,
|
|
137
|
+
): PaintSurface[] {
|
|
138
|
+
const merged = new Map<string, PaintSurface>()
|
|
139
|
+
for (const surface of [...first, ...(second ?? [])])
|
|
140
|
+
merged.set(`${surface.nodeId}:${surface.role}`, surface)
|
|
141
|
+
return [...merged.values()]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The meshes of `surfaces` under their registered roots: what the hover
|
|
146
|
+
* outline draws instead of the whole plate.
|
|
147
|
+
*/
|
|
148
|
+
export function paintSurfaceMeshes<
|
|
149
|
+
T extends { userData: Record<string, unknown>; traverse: (visit: (object: T) => void) => void },
|
|
150
|
+
>(surfaces: readonly PaintSurface[], rootOf: (nodeId: AnyNodeId) => T | null | undefined): T[] {
|
|
151
|
+
const byNode = new Map<AnyNodeId, Set<string>>()
|
|
152
|
+
for (const { nodeId, role } of surfaces) {
|
|
153
|
+
const roles = byNode.get(nodeId) ?? new Set<string>()
|
|
154
|
+
roles.add(role)
|
|
155
|
+
byNode.set(nodeId, roles)
|
|
156
|
+
}
|
|
157
|
+
const meshes: T[] = []
|
|
158
|
+
for (const [nodeId, roles] of byNode) {
|
|
159
|
+
rootOf(nodeId)?.traverse((object) => {
|
|
160
|
+
const { paintRole, slotId, __fromGeometry } = object.userData as {
|
|
161
|
+
paintRole?: string
|
|
162
|
+
slotId?: string | null
|
|
163
|
+
__fromGeometry?: boolean
|
|
164
|
+
}
|
|
165
|
+
if ((object as { isMesh?: boolean }).isMesh !== true || __fromGeometry !== true) return
|
|
166
|
+
const role = paintRole ?? slotId
|
|
167
|
+
if (role && (roles.has(role) || [...roles].some((wide) => floorStepRoleCovers(wide, role))))
|
|
168
|
+
meshes.push(object)
|
|
169
|
+
})
|
|
170
|
+
}
|
|
171
|
+
return meshes
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** The floor-plate surfaces the paint hover outlines (null: outline the hovered node as usual). */
|
|
175
|
+
export const usePaintOutline = create<{ surfaces: PaintSurface[] | null }>(() => ({
|
|
176
|
+
surfaces: null,
|
|
177
|
+
}))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ComponentType } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* What a plugin's tool answers in a chat. Images travel by reference (a host-stored asset), never
|
|
5
|
+
* as bytes, so a transcript stays small; a refusal is `error` with its stable `code`.
|
|
6
|
+
*/
|
|
7
|
+
export type ChatToolOutput = {
|
|
8
|
+
text?: string
|
|
9
|
+
data?: Record<string, unknown>
|
|
10
|
+
images?: readonly { ref: string; alt?: string }[]
|
|
11
|
+
error?: string
|
|
12
|
+
code?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What a plugin's card renders for one of its tool calls, from the persisted call alone. `actions`
|
|
17
|
+
* are the host's: open one of the editor's views, select nodes, send a prompt to the chat.
|
|
18
|
+
*/
|
|
19
|
+
export type ChatToolCardProps = {
|
|
20
|
+
toolName: string
|
|
21
|
+
input: unknown
|
|
22
|
+
output: ChatToolOutput | undefined
|
|
23
|
+
state: 'running' | 'done' | 'error' | 'cancelled'
|
|
24
|
+
actions: {
|
|
25
|
+
openView(viewId: string, options?: { beside?: boolean }): void
|
|
26
|
+
select(nodeIds: readonly string[]): void
|
|
27
|
+
send(prompt: string): void
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A plugin's contribution to a host's chat, exported from its browser entry beside its panels and
|
|
33
|
+
* views; its tool contracts are its `./agent-tools` export. Which tools a turn offers, and to whom,
|
|
34
|
+
* is the host's decision.
|
|
35
|
+
*/
|
|
36
|
+
export type PluginChatExtension = {
|
|
37
|
+
pluginId: string
|
|
38
|
+
/** Runs the plugin's `runsIn: 'editor'` tools in the editor tab. */
|
|
39
|
+
execute?: (
|
|
40
|
+
name: string,
|
|
41
|
+
input: unknown,
|
|
42
|
+
context: { projectId: string; signal: AbortSignal },
|
|
43
|
+
) => Promise<ChatToolOutput>
|
|
44
|
+
/** A card per tool name, loaded lazily; a tool without one shows as a plain tool row. */
|
|
45
|
+
cards?: Readonly<Record<string, () => Promise<{ default: ComponentType<ChatToolCardProps> }>>>
|
|
46
|
+
/** Entries for the composer's `/` menu: picking one sends `prompt`. */
|
|
47
|
+
actions?: readonly { id: string; label: string; hint: string; prompt: string }[]
|
|
48
|
+
/** File extensions or MIME types the plugin's tools read, which the composer then accepts. */
|
|
49
|
+
accepts?: readonly string[]
|
|
50
|
+
}
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
type EditorHostPanel,
|
|
4
4
|
editorHostPanelRegistry,
|
|
5
5
|
managedPluginIds,
|
|
6
|
+
pluginInstallLocks,
|
|
6
7
|
registerEditorHostPanel,
|
|
8
|
+
setPluginInstallLocks,
|
|
7
9
|
showsPluginManager,
|
|
8
10
|
} from './plugin-panels'
|
|
9
11
|
|
|
@@ -108,3 +110,26 @@ describe('showsPluginManager', () => {
|
|
|
108
110
|
}
|
|
109
111
|
})
|
|
110
112
|
})
|
|
113
|
+
|
|
114
|
+
describe('pluginInstallLocks', () => {
|
|
115
|
+
afterEach(() => setPluginInstallLocks({}))
|
|
116
|
+
|
|
117
|
+
test('replaces the whole lock set and notifies subscribers with a new snapshot', () => {
|
|
118
|
+
let notified = 0
|
|
119
|
+
const unsubscribe = pluginInstallLocks.subscribe(() => {
|
|
120
|
+
notified += 1
|
|
121
|
+
})
|
|
122
|
+
const before = pluginInstallLocks.getSnapshot()
|
|
123
|
+
setPluginInstallLocks({
|
|
124
|
+
'pascal:architect': { actionLabel: 'Upgrade', onAction: () => {}, reason: 'Pro plan' },
|
|
125
|
+
})
|
|
126
|
+
const after = pluginInstallLocks.getSnapshot()
|
|
127
|
+
|
|
128
|
+
expect(after).not.toBe(before)
|
|
129
|
+
expect(Object.keys(after)).toEqual(['pascal:architect'])
|
|
130
|
+
setPluginInstallLocks({})
|
|
131
|
+
expect(pluginInstallLocks.getSnapshot()).toEqual({})
|
|
132
|
+
expect(notified).toBe(2)
|
|
133
|
+
unsubscribe()
|
|
134
|
+
})
|
|
135
|
+
})
|
package/src/lib/plugin-panels.ts
CHANGED
|
@@ -17,6 +17,48 @@ export type EditorHostPanel = {
|
|
|
17
17
|
}
|
|
18
18
|
pluginUrl?: string
|
|
19
19
|
defaultInstalled?: boolean
|
|
20
|
+
/** Short label shown next to the plugin name in the manager, e.g. "Pro". */
|
|
21
|
+
badge?: string
|
|
22
|
+
/** Directory grouping, e.g. "Outdoor & site". Uncategorised plugins list under "Other". */
|
|
23
|
+
category?: string
|
|
24
|
+
/** One line for the directory card; `description` is the full text on its detail. */
|
|
25
|
+
summary?: string
|
|
26
|
+
/** What the plugin reaches beyond the editor — the host's policy, shown before install. */
|
|
27
|
+
access?: readonly PluginAccess[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One thing a plugin can reach once installed: a host service, an outside account, shared data. */
|
|
31
|
+
export type PluginAccess = {
|
|
32
|
+
/** Picks the glyph: AI calls, media models, data pulls, an outside account, sharing. */
|
|
33
|
+
kind: 'ai' | 'media' | 'data' | 'account' | 'sharing'
|
|
34
|
+
label: string
|
|
35
|
+
detail: string
|
|
36
|
+
/** Calls spend the project's credits; the card sums these into one "Uses credits" chip. */
|
|
37
|
+
usesCredits?: boolean
|
|
38
|
+
/** Card chip for an access that spends no credits, e.g. "Mint account". Defaults to `label`. */
|
|
39
|
+
chip?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Host-owned facts about the open project that the plugin directory shows: its
|
|
44
|
+
* name, the credits plugin calls spend, and the house rules under the
|
|
45
|
+
* categories ("Installing is free", …).
|
|
46
|
+
*/
|
|
47
|
+
export type PluginDirectoryContext = {
|
|
48
|
+
projectName?: string
|
|
49
|
+
credits?: { remaining: number } | { unlimited: true } | null
|
|
50
|
+
notes?: readonly string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A host-owned reason why a plugin cannot be installed right now (a plan the
|
|
55
|
+
* account doesn't have, for instance). The manager swaps its Install button
|
|
56
|
+
* for `actionLabel`; uninstalling is never locked.
|
|
57
|
+
*/
|
|
58
|
+
export type PluginInstallLock = {
|
|
59
|
+
reason: string
|
|
60
|
+
actionLabel: string
|
|
61
|
+
onAction: () => void
|
|
20
62
|
}
|
|
21
63
|
|
|
22
64
|
function isDevMode(): boolean {
|
|
@@ -86,6 +128,58 @@ class EditorHostPanelRegistryImpl {
|
|
|
86
128
|
|
|
87
129
|
export const editorHostPanelRegistry = new EditorHostPanelRegistryImpl()
|
|
88
130
|
|
|
131
|
+
class PluginInstallLocksImpl {
|
|
132
|
+
private locks: Readonly<Record<string, PluginInstallLock>> = {}
|
|
133
|
+
private readonly listeners = new Set<() => void>()
|
|
134
|
+
|
|
135
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
136
|
+
this.listeners.add(onChange)
|
|
137
|
+
return () => {
|
|
138
|
+
this.listeners.delete(onChange)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
getSnapshot = (): Readonly<Record<string, PluginInstallLock>> => this.locks
|
|
143
|
+
|
|
144
|
+
set(locks: Record<string, PluginInstallLock>): void {
|
|
145
|
+
this.locks = { ...locks }
|
|
146
|
+
for (const listener of this.listeners) listener()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const pluginInstallLocks = new PluginInstallLocksImpl()
|
|
151
|
+
|
|
152
|
+
class PluginDirectoryContextImpl {
|
|
153
|
+
private context: PluginDirectoryContext = {}
|
|
154
|
+
private readonly listeners = new Set<() => void>()
|
|
155
|
+
|
|
156
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
157
|
+
this.listeners.add(onChange)
|
|
158
|
+
return () => {
|
|
159
|
+
this.listeners.delete(onChange)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
getSnapshot = (): PluginDirectoryContext => this.context
|
|
164
|
+
|
|
165
|
+
set(context: PluginDirectoryContext): void {
|
|
166
|
+
this.context = context
|
|
167
|
+
for (const listener of this.listeners) listener()
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const pluginDirectoryContext = new PluginDirectoryContextImpl()
|
|
172
|
+
|
|
173
|
+
/** Replaces what the plugin directory knows about the open project. */
|
|
174
|
+
export function setPluginDirectoryContext(context: PluginDirectoryContext): void {
|
|
175
|
+
pluginDirectoryContext.set(context)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Replaces every install lock, keyed by `pluginId`. Pass `{}` to clear. */
|
|
179
|
+
export function setPluginInstallLocks(locks: Record<string, PluginInstallLock>): void {
|
|
180
|
+
pluginInstallLocks.set(locks)
|
|
181
|
+
}
|
|
182
|
+
|
|
89
183
|
export function registerEditorHostPanel(panel: EditorHostPanel): void {
|
|
90
184
|
editorHostPanelRegistry.registerPanel(panel)
|
|
91
185
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { nodeRegistry } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export const REGISTERED_DRAFT_SNAP_EXTENSION = 'pascal:editor/draft-snap'
|
|
4
|
+
|
|
5
|
+
export function snapRegisteredDraftPoint<TArgs, TResult>(
|
|
6
|
+
kind: string,
|
|
7
|
+
args: TArgs,
|
|
8
|
+
fallback: TResult,
|
|
9
|
+
): TResult {
|
|
10
|
+
const snap = nodeRegistry.get(kind)?.extensions?.[REGISTERED_DRAFT_SNAP_EXTENSION]
|
|
11
|
+
return typeof snap === 'function' ? (snap as (args: TArgs) => TResult)(args) : fallback
|
|
12
|
+
}
|