@pascal-app/editor 1.0.1 → 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 +16 -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 +221 -128
- 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 +37 -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 +536 -487
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/grid.tsx +31 -7
- package/src/components/editor/group-actions.ts +59 -11
- package/src/components/editor/group-floating-action-menu.tsx +19 -9
- package/src/components/editor/group-move-3d.ts +4 -6
- package/src/components/editor/group-rotate-handle.tsx +81 -35
- package/src/components/editor/group-selection-box-3d.tsx +25 -17
- package/src/components/editor/group-transform-shared.test.ts +126 -1
- package/src/components/editor/group-transform-shared.ts +79 -73
- package/src/components/editor/handles/handle-arrow.tsx +72 -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 +272 -131
- package/src/components/editor/index.tsx +316 -176
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +70 -70
- package/src/components/editor/node-arrow-handles.tsx +167 -14
- 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 +132 -0
- 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 +114 -163
- package/src/components/editor/wall-opening-highlights.tsx +2 -3
- 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-group-move.tsx +17 -24
- 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-registered-tool-layer.tsx +33 -12
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +242 -126
- 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-measurement.tsx +166 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +87 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +355 -245
- 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 +31 -66
- package/src/components/tools/__fixtures__/hosting.ts +10 -0
- package/src/components/tools/item/item-surface-placement.test.ts +3 -9
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +3 -9
- package/src/components/tools/item/placement-history.test.tsx +474 -0
- package/src/components/tools/item/placement-strategies.ts +63 -47
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +3 -3
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +207 -97
- package/src/components/tools/item/use-placement-coordinator.tsx +251 -111
- 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/item-surface-move.test.ts +10 -2
- package/src/components/tools/registry/item-surface-move.ts +46 -24
- package/src/components/tools/registry/item-surface-resolver.test.ts +2 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +26 -10
- 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/affordance-dispatch.test.ts +24 -0
- package/src/components/tools/shared/affordance-dispatch.ts +13 -0
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/shared/draft-measurement-label.tsx +38 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +27 -0
- package/src/components/tools/shared/pointer-support-cap.ts +17 -5
- package/src/components/tools/shared/surface-rejection.test.tsx +5 -4
- package/src/components/tools/shared/surface-rejection.tsx +14 -0
- 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 +82 -23
- 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 +101 -91
- package/src/components/ui/controls/material-picker.tsx +8 -67
- 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/controls/slider-control.tsx +76 -31
- 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 +348 -19
- package/src/components/ui/helpers/item-helper.tsx +8 -0
- package/src/components/ui/helpers/registered-tool-helper.tsx +10 -33
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +5 -30
- 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-height-mode.tsx +27 -21
- 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 +7 -19
- 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/shortcut-token.tsx +4 -0
- 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/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +615 -372
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +7 -7
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- 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-visibility.test.ts +35 -0
- package/src/components/viewer/floorplan-preview-visibility.ts +19 -0
- package/src/components/viewer/floorplan-preview.tsx +6 -18
- 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 +9 -9
- package/src/hooks/use-auto-save.integration.test.tsx +311 -0
- package/src/hooks/use-auto-save.ts +55 -14
- package/src/hooks/use-ceiling-events.test.ts +1 -0
- package/src/hooks/use-ceiling-events.ts +2 -0
- package/src/hooks/use-grid-events.test.ts +2 -0
- package/src/hooks/use-grid-events.ts +16 -11
- package/src/hooks/use-keyboard.test.ts +182 -0
- package/src/hooks/use-keyboard.ts +122 -91
- 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 +306 -39
- package/src/lib/catalog-panel-model.ts +39 -0
- package/src/lib/catalog-selection.test.ts +27 -0
- package/src/lib/catalog-selection.ts +8 -0
- 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/contextual-help.test.ts +27 -1
- package/src/lib/contextual-help.ts +16 -9
- 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 +15 -9
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +48 -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 +191 -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 +544 -16
- package/src/lib/glb-export.ts +256 -125
- package/src/lib/grid-frame.test.ts +19 -0
- package/src/lib/grid-frame.ts +18 -0
- 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 +27 -1
- package/src/lib/keyboard-pan.test.ts +184 -0
- package/src/lib/keyboard-pan.ts +238 -0
- 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 +74 -0
- package/src/lib/material-paint-panel-model.ts +80 -0
- 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/overlay-transform.test.ts +28 -0
- package/src/lib/overlay-transform.ts +12 -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 +96 -0
- package/src/lib/panel-tool-options.test.ts +34 -0
- package/src/lib/panel-tool-options.ts +46 -0
- package/src/lib/parametric-node-update.ts +37 -0
- 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 +87 -0
- package/src/lib/scene.ts +39 -45
- 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 +17 -2
- package/src/lib/snapping-mode.ts +19 -7
- package/src/lib/spatial-drag-plane.test.ts +53 -0
- package/src/lib/spatial-drag-plane.ts +19 -0
- package/src/lib/spatial-pointer-input.test.ts +72 -0
- package/src/lib/spatial-pointer-input.ts +62 -0
- package/src/lib/spatial-snap.test.ts +47 -0
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +213 -0
- package/src/lib/terrain-panel-model.ts +117 -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/world-grid-snap.test.ts +48 -0
- package/src/lib/world-grid-snap.ts +9 -5
- 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/rotation-snapping.test.ts +53 -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 +442 -323
- package/src/store/use-floorplan-draft-preview.ts +23 -1
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-interaction-scope.ts +155 -6
- 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
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ItemNode,
|
|
8
8
|
LevelNode,
|
|
9
9
|
nodeRegistry,
|
|
10
|
-
registerNode,
|
|
11
10
|
type SurfacePlacement,
|
|
12
11
|
sceneRegistry,
|
|
13
12
|
useLiveTransforms,
|
|
@@ -17,6 +16,7 @@ import { Group, Vector3 } from 'three'
|
|
|
17
16
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
18
17
|
import useEditor from '../../../store/use-editor'
|
|
19
18
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
19
|
+
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
20
20
|
import { createRegistryItemSurfaceMove } from './item-surface-move'
|
|
21
21
|
|
|
22
22
|
const level = LevelNode.parse({ id: 'level_resolver' })
|
|
@@ -69,7 +69,7 @@ beforeEach(() => {
|
|
|
69
69
|
savedRoots = useScene.getState().rootNodeIds
|
|
70
70
|
restoreRegistry = nodeRegistry._snapshot()
|
|
71
71
|
nodeRegistry._reset()
|
|
72
|
-
|
|
72
|
+
registerHostingTestNode({
|
|
73
73
|
kind: 'item',
|
|
74
74
|
schemaVersion: 1,
|
|
75
75
|
schema: ItemNode,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createSceneApi,
|
|
14
14
|
emitter,
|
|
15
15
|
findLevelAncestorId,
|
|
16
|
+
floorPlacedCollides,
|
|
16
17
|
footprintAABBFrom,
|
|
17
18
|
type GridEvent,
|
|
18
19
|
type GroupMoveSnapResult,
|
|
@@ -42,7 +43,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
42
43
|
import { type Group, Vector3 } from 'three'
|
|
43
44
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
44
45
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
45
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
isFreshPlacementMetadata,
|
|
48
|
+
stripPlacementMetadataFlags,
|
|
49
|
+
} from '../../../lib/placement-metadata'
|
|
46
50
|
import {
|
|
47
51
|
offsetPlanPositionByLocalCenter,
|
|
48
52
|
resolvePrioritizedPlanarCursorPosition,
|
|
@@ -64,6 +68,7 @@ import useEditor, {
|
|
|
64
68
|
isMagneticSnapActive,
|
|
65
69
|
} from '../../../store/use-editor'
|
|
66
70
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
71
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
67
72
|
import { swallowNextClick } from '../../editor/node-arrow-handles'
|
|
68
73
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
69
74
|
import { DragBoundingBox } from '../shared/drag-bounding-box'
|
|
@@ -218,7 +223,14 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
218
223
|
*/
|
|
219
224
|
type ClickTriggerEvent = GridEvent | NodeEvent<AnyNode>
|
|
220
225
|
|
|
221
|
-
export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
226
|
+
export function MoveRegistryNodeTool({ node: source }: { node: AnyNode }) {
|
|
227
|
+
const node = useMemo(
|
|
228
|
+
() =>
|
|
229
|
+
isFreshPlacementMetadata(source.metadata)
|
|
230
|
+
? (useScene.getState().nodes[source.id] ?? source)
|
|
231
|
+
: source,
|
|
232
|
+
[source],
|
|
233
|
+
)
|
|
222
234
|
const itemSurfaceMove = useMemo(() => createRegistryItemSurfaceMove(node), [node])
|
|
223
235
|
const suppressRaycastsRef = useRef<(() => void) | null>(null)
|
|
224
236
|
const previewGroupRef = useRef<Group>(null)
|
|
@@ -329,7 +341,8 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
329
341
|
// position isn't in the level frame the spatial grid indexes. They may
|
|
330
342
|
// still provide a parent-frame collision check and use the same bounds box.
|
|
331
343
|
const collides =
|
|
332
|
-
!frameParent &&
|
|
344
|
+
!frameParent &&
|
|
345
|
+
floorPlacedCollides(nodeRegistry.get(node.type)?.capabilities?.floorPlaced, node)
|
|
333
346
|
// Snapshot the scene once at drag-start — bounds depend on `node` (locked
|
|
334
347
|
// for the lifetime of this tool) and any sibling state the kind reads. If a
|
|
335
348
|
// future kind needs live sibling state mid-drag, switch to a subscribed
|
|
@@ -432,6 +445,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
432
445
|
}, [])
|
|
433
446
|
|
|
434
447
|
useEffect(() => {
|
|
448
|
+
const ownsSubtree = useInteractionScope.getState().adoptSubtree(node.id)
|
|
435
449
|
useScene.temporal.getState().pause()
|
|
436
450
|
const movementSfx = createMovementSfxTick()
|
|
437
451
|
dragAnchorRef.current = null
|
|
@@ -1114,11 +1128,13 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1114
1128
|
} as Partial<AnyNode>
|
|
1115
1129
|
|
|
1116
1130
|
if (isNew) {
|
|
1117
|
-
const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data)
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1131
|
+
const finalId = commitFreshPlacementSubtree(node.id as AnyNodeId, data, (reason) => {
|
|
1132
|
+
setSurfaceRejection(reason)
|
|
1133
|
+
setValid(false)
|
|
1134
|
+
})
|
|
1135
|
+
if (!finalId) return
|
|
1136
|
+
committed = true
|
|
1137
|
+
committedId = finalId
|
|
1122
1138
|
} else {
|
|
1123
1139
|
// Fold the connected-ductwork follow-updates into the SAME
|
|
1124
1140
|
// batch as the moved node so the whole thing is one undo step.
|
|
@@ -1339,7 +1355,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1339
1355
|
clearParentFramePreview()
|
|
1340
1356
|
if (isNew) {
|
|
1341
1357
|
updateSurfaceNode(node.id, {}, null)
|
|
1342
|
-
useScene.getState().deleteNode(node.id
|
|
1358
|
+
if (!ownsSubtree) useScene.getState().deleteNode(node.id)
|
|
1343
1359
|
} else {
|
|
1344
1360
|
itemSurfaceMove?.restore()
|
|
1345
1361
|
applyMeshPose(originalPosition, originalRotationY)
|
|
@@ -1376,7 +1392,7 @@ export function MoveRegistryNodeTool({ node }: { node: AnyNode }) {
|
|
|
1376
1392
|
// unmount / commit paths uniformly.
|
|
1377
1393
|
useAlignmentGuides.getState().clear()
|
|
1378
1394
|
const finalisedBy2D = useEditor.getState().movingNodeOrigin === '2d'
|
|
1379
|
-
if (!committed && !finalisedBy2D && (!isNew || itemSurfaceMove)) {
|
|
1395
|
+
if (!committed && !finalisedBy2D && (!isNew || (!ownsSubtree && itemSurfaceMove))) {
|
|
1380
1396
|
useLiveTransforms.getState().clear(node.id)
|
|
1381
1397
|
clearConnectivityOverrides()
|
|
1382
1398
|
clearParentFramePreview()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sceneRegistry, useScene
|
|
1
|
+
import { sceneRegistry, useScene } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { useThree } from '@react-three/fiber'
|
|
4
4
|
import { useCallback, useEffect, useRef } from 'react'
|
|
@@ -248,18 +248,8 @@ function collectNodeIdsInScreenRect(
|
|
|
248
248
|
|
|
249
249
|
function commitBoxSelection(ids: string[], event: PointerEvent) {
|
|
250
250
|
const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
|
|
251
|
-
const { phase, structureLayer } = useEditor.getState()
|
|
252
251
|
const viewer = useViewer.getState()
|
|
253
252
|
|
|
254
|
-
if (phase === 'structure' && structureLayer === 'zones') {
|
|
255
|
-
if (ids.length > 0) {
|
|
256
|
-
viewer.setSelection({ zoneId: ids[0] as ZoneNode['id'] })
|
|
257
|
-
} else if (!shouldAppend) {
|
|
258
|
-
viewer.setSelection({ zoneId: null })
|
|
259
|
-
}
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
|
|
263
253
|
if (shouldAppend) {
|
|
264
254
|
viewer.setSelection({
|
|
265
255
|
selectedIds: Array.from(new Set([...viewer.selection.selectedIds, ...ids])),
|
|
@@ -274,7 +264,7 @@ export const BoxSelectTool: React.FC = () => {
|
|
|
274
264
|
const phase = useEditor((s) => s.phase)
|
|
275
265
|
const mode = useEditor((s) => s.mode)
|
|
276
266
|
const selectionTool = useEditor((s) => s.floorplanSelectionTool)
|
|
277
|
-
const isActive = mode === 'select' &&
|
|
267
|
+
const isActive = mode === 'select' && phase === 'building'
|
|
278
268
|
|
|
279
269
|
if (!isActive) return null
|
|
280
270
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { emitter, type GridEvent } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { act } from '@react-three/fiber'
|
|
5
|
+
import { cancelActiveTool } from '../../../hooks/use-keyboard'
|
|
6
|
+
import { selectionEnabled } from '../../../lib/interaction/scope'
|
|
7
|
+
import useEditor from '../../../store/use-editor'
|
|
8
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
9
|
+
import { withSelectionHarness } from '../../../test-utils/selection-harness'
|
|
10
|
+
import { PlaneBoxSelectTool } from './plane-box-select-tool'
|
|
11
|
+
|
|
12
|
+
for (const exit of ['Escape', 'unmount', 'pointercancel'] as const) {
|
|
13
|
+
test(`plane marquee resets its drag and scope on ${exit}`, async () => {
|
|
14
|
+
await withSelectionHarness(async ({ render, canvas }) => {
|
|
15
|
+
useEditor.setState({
|
|
16
|
+
phase: 'building',
|
|
17
|
+
mode: 'select',
|
|
18
|
+
floorplanSelectionTool: 'marquee',
|
|
19
|
+
isFloorplanHovered: true,
|
|
20
|
+
})
|
|
21
|
+
useViewer.setState({ inputDragging: false, cameraDragging: false })
|
|
22
|
+
useInteractionScope.getState().end()
|
|
23
|
+
await render(<PlaneBoxSelectTool />)
|
|
24
|
+
await act(async () => {
|
|
25
|
+
canvas.dispatchEvent(
|
|
26
|
+
Object.assign(new Event('pointerdown'), { button: 0, clientX: 200, clientY: 200 }),
|
|
27
|
+
)
|
|
28
|
+
emitter.emit('grid:move', {
|
|
29
|
+
position: [4, 0, 4],
|
|
30
|
+
nativeEvent: { clientX: 800, clientY: 800 },
|
|
31
|
+
} as GridEvent)
|
|
32
|
+
})
|
|
33
|
+
expect(useInteractionScope.getState().scope.kind).toBe('box-select')
|
|
34
|
+
if (exit === 'unmount') await render(null)
|
|
35
|
+
else
|
|
36
|
+
await act(async () => {
|
|
37
|
+
if (exit === 'Escape') cancelActiveTool()
|
|
38
|
+
else window.dispatchEvent(new Event('pointercancel'))
|
|
39
|
+
})
|
|
40
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
|
|
41
|
+
expect(useViewer.getState().previewSelectedIds).toEqual([])
|
|
42
|
+
if (exit === 'Escape') expect(useEditor.getState().floorplanSelectionTool).toBe('marquee')
|
|
43
|
+
await act(async () => {
|
|
44
|
+
emitter.emit('grid:move', {
|
|
45
|
+
position: [6, 0, 6],
|
|
46
|
+
nativeEvent: { clientX: 900, clientY: 900 },
|
|
47
|
+
} as GridEvent)
|
|
48
|
+
canvas.dispatchEvent(
|
|
49
|
+
Object.assign(new Event('pointerup'), { button: 0, clientX: 900, clientY: 900 }),
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test('plane marquee cleanup cannot release a replacement box-select owner', async () => {
|
|
58
|
+
await withSelectionHarness(async ({ render, canvas }) => {
|
|
59
|
+
useEditor.setState({ isFloorplanHovered: true })
|
|
60
|
+
useViewer.setState({ inputDragging: false, cameraDragging: false })
|
|
61
|
+
await render(<PlaneBoxSelectTool />)
|
|
62
|
+
await act(async () => {
|
|
63
|
+
canvas.dispatchEvent(
|
|
64
|
+
Object.assign(new Event('pointerdown'), { button: 0, clientX: 200, clientY: 200 }),
|
|
65
|
+
)
|
|
66
|
+
emitter.emit('grid:move', {
|
|
67
|
+
position: [4, 0, 4],
|
|
68
|
+
nativeEvent: { clientX: 800, clientY: 800 },
|
|
69
|
+
} as GridEvent)
|
|
70
|
+
})
|
|
71
|
+
const ownGesture = useInteractionScope.getState().gesture
|
|
72
|
+
useInteractionScope.getState().begin({ kind: 'box-select' })
|
|
73
|
+
const replacement = useInteractionScope.getState().gesture
|
|
74
|
+
expect(replacement).not.toBe(ownGesture)
|
|
75
|
+
await render(null)
|
|
76
|
+
expect(useInteractionScope.getState().scope.kind).toBe('box-select')
|
|
77
|
+
expect(useInteractionScope.getState().gesture).toBe(replacement)
|
|
78
|
+
})
|
|
79
|
+
})
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import '../../../three-types'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import {
|
|
5
|
-
type AnyNodeId,
|
|
6
|
-
emitter,
|
|
7
|
-
type GridEvent,
|
|
8
|
-
sceneRegistry,
|
|
9
|
-
useScene,
|
|
10
|
-
type ZoneNode,
|
|
11
|
-
} from '@pascal-app/core'
|
|
4
|
+
import { type AnyNodeId, emitter, type GridEvent, sceneRegistry, useScene } from '@pascal-app/core'
|
|
12
5
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
6
|
import { useThree } from '@react-three/fiber'
|
|
14
7
|
import { useCallback, useEffect, useRef } from 'react'
|
|
@@ -26,9 +19,10 @@ import {
|
|
|
26
19
|
Vector2,
|
|
27
20
|
Vector3,
|
|
28
21
|
} from 'three'
|
|
22
|
+
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
29
23
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
30
24
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
31
|
-
import
|
|
25
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
32
26
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
33
27
|
import { isBoxSelectPointerSuppressed, markBoxSelectHandled } from './box-select-state'
|
|
34
28
|
import { collectSelectableCandidateIds } from './select-candidates'
|
|
@@ -297,6 +291,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
|
|
|
297
291
|
const currentPoint = useRef(new Vector3())
|
|
298
292
|
const pointerDown = useRef(false)
|
|
299
293
|
const isDragging = useRef(false)
|
|
294
|
+
const ownedGesture = useRef<object | null>(null)
|
|
300
295
|
const startClientX = useRef(0)
|
|
301
296
|
const startClientY = useRef(0)
|
|
302
297
|
const gridY = useRef(0)
|
|
@@ -320,11 +315,36 @@ export const PlaneBoxSelectTool: React.FC = () => {
|
|
|
320
315
|
const resetDrag = useCallback(() => {
|
|
321
316
|
pointerDown.current = false
|
|
322
317
|
isDragging.current = false
|
|
323
|
-
|
|
318
|
+
const gesture = ownedGesture.current
|
|
319
|
+
if (gesture)
|
|
320
|
+
useInteractionScope.getState().endIf(() => useInteractionScope.getState().gesture === gesture)
|
|
321
|
+
ownedGesture.current = null
|
|
322
|
+
previousGridPosition.current = null
|
|
323
|
+
if (rectFillRef.current) rectFillRef.current.visible = false
|
|
324
324
|
outlineRef.current.visible = false
|
|
325
325
|
syncPreviewSelectedIds([])
|
|
326
326
|
}, [syncPreviewSelectedIds])
|
|
327
327
|
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const onCancel = () => {
|
|
330
|
+
if (!pointerDown.current) return
|
|
331
|
+
if (
|
|
332
|
+
(!ownedGesture.current && useInteractionScope.getState().scope.kind === 'idle') ||
|
|
333
|
+
(ownedGesture.current && useInteractionScope.getState().gesture === ownedGesture.current)
|
|
334
|
+
) {
|
|
335
|
+
markToolCancelConsumed()
|
|
336
|
+
}
|
|
337
|
+
resetDrag()
|
|
338
|
+
}
|
|
339
|
+
emitter.on('tool:cancel', onCancel)
|
|
340
|
+
window.addEventListener('pointercancel', resetDrag)
|
|
341
|
+
return () => {
|
|
342
|
+
emitter.off('tool:cancel', onCancel)
|
|
343
|
+
window.removeEventListener('pointercancel', resetDrag)
|
|
344
|
+
resetDrag()
|
|
345
|
+
}
|
|
346
|
+
}, [resetDrag])
|
|
347
|
+
|
|
328
348
|
const raycastToGround = useCallback(
|
|
329
349
|
(event: PointerEvent): Vector3 | null => {
|
|
330
350
|
const rect = gl.domElement.getBoundingClientRect()
|
|
@@ -342,12 +362,11 @@ export const PlaneBoxSelectTool: React.FC = () => {
|
|
|
342
362
|
useEffect(() => {
|
|
343
363
|
const outline = outlineRef.current
|
|
344
364
|
return () => {
|
|
345
|
-
|
|
346
|
-
setPreviewSelectedIds([])
|
|
365
|
+
resetDrag()
|
|
347
366
|
outline.geometry.dispose()
|
|
348
367
|
;(outline.material as LineBasicMaterial).dispose()
|
|
349
368
|
}
|
|
350
|
-
}, [
|
|
369
|
+
}, [resetDrag])
|
|
351
370
|
|
|
352
371
|
useEffect(() => {
|
|
353
372
|
const unsubscribe = useViewer.subscribe((state) => {
|
|
@@ -440,15 +459,7 @@ export const PlaneBoxSelectTool: React.FC = () => {
|
|
|
440
459
|
|
|
441
460
|
const ids = collectNodeIdsInPlaneBounds(bounds)
|
|
442
461
|
const shouldAppend = event.metaKey || event.ctrlKey || event.shiftKey
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if (phase === 'structure' && structureLayer === 'zones') {
|
|
446
|
-
if (ids.length > 0) {
|
|
447
|
-
useViewer.getState().setSelection({ zoneId: ids[0] as ZoneNode['id'] })
|
|
448
|
-
} else if (!shouldAppend) {
|
|
449
|
-
useViewer.getState().setSelection({ zoneId: null })
|
|
450
|
-
}
|
|
451
|
-
} else if (shouldAppend) {
|
|
462
|
+
if (shouldAppend) {
|
|
452
463
|
const currentIds = useViewer.getState().selection.selectedIds
|
|
453
464
|
useViewer.getState().setSelection({
|
|
454
465
|
selectedIds: Array.from(new Set([...currentIds, ...ids])),
|
|
@@ -507,6 +518,8 @@ export const PlaneBoxSelectTool: React.FC = () => {
|
|
|
507
518
|
const dy = nativeEvent.clientY - startClientY.current
|
|
508
519
|
if (!isDragging.current && Math.hypot(dx, dy) >= DRAG_THRESHOLD_PX) {
|
|
509
520
|
isDragging.current = true
|
|
521
|
+
useInteractionScope.getState().begin({ kind: 'box-select' })
|
|
522
|
+
ownedGesture.current = useInteractionScope.getState().gesture
|
|
510
523
|
}
|
|
511
524
|
|
|
512
525
|
if (isDragging.current && rectFillRef.current && outlineRef.current) {
|
|
@@ -47,10 +47,7 @@ describe('selectable candidates', () => {
|
|
|
47
47
|
},
|
|
48
48
|
previewSelectedIds: [],
|
|
49
49
|
})
|
|
50
|
-
useEditor.setState({
|
|
51
|
-
phase: 'structure',
|
|
52
|
-
structureLayer: 'elements',
|
|
53
|
-
})
|
|
50
|
+
useEditor.setState({ phase: 'building' })
|
|
54
51
|
})
|
|
55
52
|
|
|
56
53
|
test('includes building-scoped elevators for the active level building', () => {
|
|
@@ -106,4 +103,40 @@ describe('selectable candidates', () => {
|
|
|
106
103
|
|
|
107
104
|
expect(collectSelectableCandidateIds()).toContain('elevator_test')
|
|
108
105
|
})
|
|
106
|
+
|
|
107
|
+
test('Building box selection includes structural and furnishing kinds on the active level', () => {
|
|
108
|
+
for (const category of ['structure', 'furnish'] as const) {
|
|
109
|
+
const kind = `box-select-test-${category}`
|
|
110
|
+
if (!nodeRegistry.has(kind)) {
|
|
111
|
+
registerNode({
|
|
112
|
+
kind,
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
115
|
+
category,
|
|
116
|
+
defaults: () => ({}),
|
|
117
|
+
capabilities: { selectable: {} },
|
|
118
|
+
} as AnyNodeDefinition)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
useScene.setState({
|
|
122
|
+
nodes: {
|
|
123
|
+
level_test: {
|
|
124
|
+
id: 'level_test',
|
|
125
|
+
type: 'level',
|
|
126
|
+
parentId: 'building_test',
|
|
127
|
+
children: ['wall_pick', 'item_pick', 'item_hidden'],
|
|
128
|
+
},
|
|
129
|
+
wall_pick: { id: 'wall_pick', type: 'box-select-test-structure', parentId: 'level_test' },
|
|
130
|
+
item_pick: { id: 'item_pick', type: 'box-select-test-furnish', parentId: 'level_test' },
|
|
131
|
+
item_hidden: {
|
|
132
|
+
id: 'item_hidden',
|
|
133
|
+
type: 'box-select-test-furnish',
|
|
134
|
+
parentId: 'level_test',
|
|
135
|
+
visible: false,
|
|
136
|
+
},
|
|
137
|
+
item_other: { id: 'item_other', type: 'box-select-test-furnish', parentId: 'level_other' },
|
|
138
|
+
} as unknown as Record<string, AnyNode>,
|
|
139
|
+
})
|
|
140
|
+
expect(collectSelectableCandidateIds().sort()).toEqual(['item_pick', 'wall_pick'])
|
|
141
|
+
})
|
|
109
142
|
})
|
|
@@ -19,17 +19,29 @@ function isVisibleSelectableNode(node: AnyNode): boolean {
|
|
|
19
19
|
export function collectSelectableCandidateIds(): string[] {
|
|
20
20
|
const { levelId } = useViewer.getState().selection
|
|
21
21
|
const { nodes } = useScene.getState()
|
|
22
|
-
const { phase
|
|
22
|
+
const { phase } = useEditor.getState()
|
|
23
23
|
const result: string[] = []
|
|
24
24
|
const seen = new Set<string>()
|
|
25
25
|
const addNode = (node: AnyNode | undefined) => {
|
|
26
|
-
if (
|
|
26
|
+
if (
|
|
27
|
+
!node ||
|
|
28
|
+
node.type === 'zone' ||
|
|
29
|
+
seen.has(node.id) ||
|
|
30
|
+
(node as { visible?: boolean }).visible === false
|
|
31
|
+
)
|
|
32
|
+
return
|
|
27
33
|
seen.add(node.id)
|
|
28
34
|
result.push(node.id)
|
|
29
35
|
}
|
|
30
36
|
const visitLevelDescendant = (id: AnyNodeId) => {
|
|
31
37
|
const node = nodes[id]
|
|
32
|
-
if (
|
|
38
|
+
if (
|
|
39
|
+
!node ||
|
|
40
|
+
node.type === 'zone' ||
|
|
41
|
+
seen.has(node.id) ||
|
|
42
|
+
(node as { visible?: boolean }).visible === false
|
|
43
|
+
)
|
|
44
|
+
return
|
|
33
45
|
|
|
34
46
|
if (isRegistrySelectable(node.type)) {
|
|
35
47
|
addNode(node)
|
|
@@ -52,14 +64,6 @@ export function collectSelectableCandidateIds(): string[] {
|
|
|
52
64
|
const levelNode = nodes[levelId as AnyNodeId] as LevelNode | undefined
|
|
53
65
|
if (levelNode?.type !== 'level') return []
|
|
54
66
|
|
|
55
|
-
if (phase === 'structure' && structureLayer === 'zones') {
|
|
56
|
-
for (const childId of levelNode.children) {
|
|
57
|
-
const node = nodes[childId as AnyNodeId]
|
|
58
|
-
if (node?.type === 'zone') addNode(node)
|
|
59
|
-
}
|
|
60
|
-
return result
|
|
61
|
-
}
|
|
62
|
-
|
|
63
67
|
for (const childId of levelNode.children) {
|
|
64
68
|
visitLevelDescendant(childId as AnyNodeId)
|
|
65
69
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { afterAll, afterEach, expect, spyOn, test } from 'bun:test'
|
|
2
|
+
import { type AnyNodeDefinition, nodeRegistry } from '@pascal-app/core'
|
|
3
|
+
import { getRegistryAffordanceTool, preloadRegistryAffordanceTools } from './affordance-dispatch'
|
|
4
|
+
|
|
5
|
+
const get = spyOn(nodeRegistry, 'get')
|
|
6
|
+
afterEach(() => get.mockReset())
|
|
7
|
+
afterAll(() => get.mockRestore())
|
|
8
|
+
|
|
9
|
+
test('a preloaded drag tool mounts directly without first-gesture suspense', async () => {
|
|
10
|
+
const Tool = () => null
|
|
11
|
+
const loader = async () => ({ default: Tool })
|
|
12
|
+
get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
|
|
13
|
+
await preloadRegistryAffordanceTools('wall')
|
|
14
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
|
|
15
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(Tool)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('preloading does not change an already mounted lazy component identity', async () => {
|
|
19
|
+
const loader = async () => ({ default: () => null })
|
|
20
|
+
get.mockReturnValue({ affordanceTools: { move: loader } } as unknown as AnyNodeDefinition)
|
|
21
|
+
const mounted = getRegistryAffordanceTool('wall', 'move')
|
|
22
|
+
await preloadRegistryAffordanceTools('wall')
|
|
23
|
+
expect(getRegistryAffordanceTool('wall', 'move')).toBe(mounted)
|
|
24
|
+
})
|
|
@@ -15,6 +15,19 @@ import { type ComponentType, lazy } from 'react'
|
|
|
15
15
|
*/
|
|
16
16
|
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType>()
|
|
17
17
|
|
|
18
|
+
export async function preloadRegistryAffordanceTools(kind: string): Promise<void> {
|
|
19
|
+
const tools = nodeRegistry.get(kind)?.affordanceTools
|
|
20
|
+
await Promise.allSettled(
|
|
21
|
+
Object.values(tools ?? {}).map(async (loader) => {
|
|
22
|
+
if (!loader || lazyToolCache.has(loader)) return
|
|
23
|
+
const module = (await loader()) as { default: ComponentType<any> }
|
|
24
|
+
// Keep an already-mounted lazy component stable. Otherwise use the resolved
|
|
25
|
+
// component directly so grabbing a preloaded handle never suspends its tool.
|
|
26
|
+
if (!lazyToolCache.has(loader)) lazyToolCache.set(loader, module.default)
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
export function getRegistryAffordanceTool(
|
|
19
32
|
kind: string,
|
|
20
33
|
affordance: string,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Html } from '@react-three/drei'
|
|
2
2
|
import type { ThreeElements } from '@react-three/fiber'
|
|
3
3
|
import { forwardRef } from 'react'
|
|
4
|
-
import type
|
|
4
|
+
import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
|
|
5
5
|
import { furnishTools } from '../../../components/ui/action-menu/furnish-tools'
|
|
6
6
|
import { tools } from '../../../components/ui/action-menu/structure-tools'
|
|
7
7
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
8
|
+
import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
|
|
8
9
|
import useEditor from '../../../store/use-editor'
|
|
9
10
|
import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
|
|
10
11
|
|
|
@@ -25,6 +26,19 @@ interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
|
25
26
|
tooltipContent?: React.ReactNode
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
// One geometry each for the life of the module: the vertical line used to
|
|
30
|
+
// be a `<cylinderGeometry args={[…, height, 8]}>` rebuilt on EVERY height
|
|
31
|
+
// change (the cursor's drop to the floor moves with the pointer), and the
|
|
32
|
+
// ring / dot / tip were built and disposed on every toggle — WebGPU then
|
|
33
|
+
// drew a frame whose vertex buffers had just been destroyed ("Vertex
|
|
34
|
+
// buffer slot 0 required by [RenderPipeline "renderPipeline_MeshBasicMaterial_…"]
|
|
35
|
+
// was not set … DrawIndexed(96 …)" — the 8-segment cylinder's 96 indices).
|
|
36
|
+
// The line is a unit cylinder scaled to `height` instead.
|
|
37
|
+
const DOT_GEOMETRY = new CircleGeometry(0.06, 32)
|
|
38
|
+
const RING_GEOMETRY = new CircleGeometry(0.2, 32)
|
|
39
|
+
const LINE_GEOMETRY = new CylinderGeometry(0.01, 0.01, 1, 8)
|
|
40
|
+
const TIP_GEOMETRY = new SphereGeometry(0.08, 20, 14)
|
|
41
|
+
|
|
28
42
|
export const CursorSphere = forwardRef<Group, CursorSphereProps>(function CursorSphere(
|
|
29
43
|
{
|
|
30
44
|
color = '#818cf8',
|
|
@@ -45,11 +59,14 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
45
59
|
// (green square at a corner); hide the cursor's ground dot/ring so it
|
|
46
60
|
// doesn't sit on top of that glyph.
|
|
47
61
|
const isSnapping = useWallSnapIndicator((s) => s.point !== null)
|
|
62
|
+
const wallMode = useEditor((s) => s.continuationByContext.wall)
|
|
48
63
|
|
|
49
|
-
// Find the icon for the current tool
|
|
50
|
-
let activeToolConfig = null
|
|
64
|
+
// Find the icon for the current tool — the wall shows its Rooms variant.
|
|
65
|
+
let activeToolConfig: { label: string; iconSrc: string } | null | undefined = null
|
|
51
66
|
if (mode === 'build' && tool) {
|
|
52
|
-
if (tool === '
|
|
67
|
+
if (tool === 'wall') {
|
|
68
|
+
activeToolConfig = wallCursorIcon(wallMode)
|
|
69
|
+
} else if (tool === 'item' && catalogCategory) {
|
|
53
70
|
activeToolConfig = furnishTools.find((t) => t.catalogCategory === catalogCategory)
|
|
54
71
|
} else {
|
|
55
72
|
activeToolConfig = tools.find((t) => t.id === tool)
|
|
@@ -67,8 +84,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
67
84
|
<group rotation={[-Math.PI / 2, 0, 0]}>
|
|
68
85
|
{/* Center dot — at the ground unless the placement point is elevated */}
|
|
69
86
|
{!dotAtTip && (
|
|
70
|
-
<mesh layers={EDITOR_LAYER} renderOrder={2}>
|
|
71
|
-
<circleGeometry args={[0.06, 32]} />
|
|
87
|
+
<mesh geometry={DOT_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
|
|
72
88
|
<meshBasicMaterial
|
|
73
89
|
color={color}
|
|
74
90
|
depthTest={false}
|
|
@@ -80,8 +96,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
80
96
|
)}
|
|
81
97
|
|
|
82
98
|
{/* Outer ring / glow */}
|
|
83
|
-
<mesh layers={EDITOR_LAYER} renderOrder={2}>
|
|
84
|
-
<circleGeometry args={[0.2, 32]} />
|
|
99
|
+
<mesh geometry={RING_GEOMETRY} layers={EDITOR_LAYER} renderOrder={2}>
|
|
85
100
|
<meshBasicMaterial
|
|
86
101
|
color={color}
|
|
87
102
|
depthTest={false}
|
|
@@ -95,8 +110,13 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
95
110
|
|
|
96
111
|
{/* Vertical line */}
|
|
97
112
|
{height > 0 && (
|
|
98
|
-
<mesh
|
|
99
|
-
|
|
113
|
+
<mesh
|
|
114
|
+
geometry={LINE_GEOMETRY}
|
|
115
|
+
layers={EDITOR_LAYER}
|
|
116
|
+
position={[0, height / 2, 0]}
|
|
117
|
+
renderOrder={2}
|
|
118
|
+
scale={[1, height, 1]}
|
|
119
|
+
>
|
|
100
120
|
<meshBasicMaterial
|
|
101
121
|
color={color}
|
|
102
122
|
depthTest={false}
|
|
@@ -110,8 +130,12 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
110
130
|
{/* Bright marker dot at the tip of the line — the actual placement
|
|
111
131
|
point, riding at the cursor while the line drops to the floor. */}
|
|
112
132
|
{dotAtTip && height > 0 && (
|
|
113
|
-
<mesh
|
|
114
|
-
|
|
133
|
+
<mesh
|
|
134
|
+
geometry={TIP_GEOMETRY}
|
|
135
|
+
layers={EDITOR_LAYER}
|
|
136
|
+
position={[0, height, 0]}
|
|
137
|
+
renderOrder={2}
|
|
138
|
+
>
|
|
115
139
|
<meshBasicMaterial color={color} depthTest={false} depthWrite={false} />
|
|
116
140
|
</mesh>
|
|
117
141
|
)}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Html } from '@react-three/drei'
|
|
4
|
+
|
|
5
|
+
/** Lift of a draft's readout above the top of what it measures, in metres. */
|
|
6
|
+
export const DRAFT_LABEL_Y_OFFSET = 0.22
|
|
7
|
+
|
|
8
|
+
/** Screen-space readout for a draft in 3D (lengths, angles, split segments). */
|
|
9
|
+
export function DraftMeasurementLabel({
|
|
10
|
+
color,
|
|
11
|
+
label,
|
|
12
|
+
position,
|
|
13
|
+
shadowColor,
|
|
14
|
+
}: {
|
|
15
|
+
color: string
|
|
16
|
+
label: string
|
|
17
|
+
position: [number, number, number]
|
|
18
|
+
shadowColor: string
|
|
19
|
+
}) {
|
|
20
|
+
return (
|
|
21
|
+
<Html
|
|
22
|
+
center
|
|
23
|
+
position={position}
|
|
24
|
+
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
|
25
|
+
zIndexRange={[100, 0]}
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
className="whitespace-nowrap font-bold font-mono text-[15px]"
|
|
29
|
+
style={{
|
|
30
|
+
color,
|
|
31
|
+
textShadow: `-1.5px -1.5px 0 ${shadowColor}, 1.5px -1.5px 0 ${shadowColor}, -1.5px 1.5px 0 ${shadowColor}, 1.5px 1.5px 0 ${shadowColor}, 0 0 4px ${shadowColor}, 0 0 4px ${shadowColor}`,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{label}
|
|
35
|
+
</div>
|
|
36
|
+
</Html>
|
|
37
|
+
)
|
|
38
|
+
}
|