@pascal-app/editor 1.0.6 → 1.0.7
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 +18 -15
- package/src/components/editor/action-menu-button.tsx +4 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
- package/src/components/editor/bake-thumbnail.tsx +3 -2
- package/src/components/editor/custom-camera-controls.tsx +20 -8
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +72 -61
- package/src/components/editor/floating-action-menu.tsx +27 -9
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.test.ts +220 -0
- package/src/components/editor/floorplan-panel.tsx +285 -132
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/grid.tsx +24 -15
- package/src/components/editor/group-actions.ts +19 -1
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +27 -2
- package/src/components/editor/group-selection-box-3d.tsx +4 -1
- package/src/components/editor/group-transform-shared.ts +20 -0
- package/src/components/editor/index.tsx +130 -20
- package/src/components/editor/inspector-extension-actions.tsx +36 -0
- package/src/components/editor/localized-html.tsx +22 -0
- package/src/components/editor/node-action-menu.tsx +46 -12
- package/src/components/editor/node-arrow-handles.tsx +1 -2
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/registry-action-contributions.test.tsx +67 -6
- package/src/components/editor/registry-action-contributions.tsx +115 -15
- package/src/components/editor/riser-diagram-panel.tsx +59 -9
- package/src/components/editor/room-controls.tsx +1 -1
- package/src/components/editor/room-handles-2d.tsx +156 -6
- package/src/components/editor/room-handles.tsx +241 -51
- package/src/components/editor/room-highlight.tsx +2 -0
- package/src/components/editor/room-labels.tsx +29 -15
- package/src/components/editor/selection-manager.tsx +15 -102
- package/src/components/editor/separator-end-grip.tsx +74 -0
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/thumbnail-generator.test.ts +15 -6
- package/src/components/editor/thumbnail-generator.tsx +26 -1
- package/src/components/editor/use-floorplan-background-placement.ts +55 -52
- package/src/components/editor/views/view-bar.tsx +111 -26
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +5 -1
- package/src/components/editor/wall-pull-grip.tsx +193 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
- package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
- package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
- package/src/components/procedural-items/inspector.tsx +59 -12
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
- package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
- package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/placement-strategies.test.ts +64 -1
- package/src/components/tools/item/placement-strategies.ts +5 -9
- package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
- package/src/components/tools/registry/item-surface-move.test.ts +11 -0
- package/src/components/tools/select/select-candidates.test.ts +45 -0
- package/src/components/tools/select/select-candidates.ts +13 -2
- package/src/components/tools/shared/cursor-sphere.tsx +10 -3
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/tools/wall/wall-drafting.test.ts +299 -23
- package/src/components/tools/wall/wall-drafting.ts +132 -24
- package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
- package/src/components/tools/wall/wall-pull-snap.ts +563 -0
- package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
- package/src/components/tools/zone/zone-tool.tsx +3 -3
- package/src/components/ui/action-menu/action-button.tsx +2 -0
- package/src/components/ui/action-menu/camera-actions.tsx +8 -7
- package/src/components/ui/action-menu/control-modes.tsx +9 -5
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
- package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
- package/src/components/ui/action-menu/index.tsx +1 -0
- package/src/components/ui/action-menu/measurement-control.tsx +35 -29
- package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
- package/src/components/ui/action-menu/structure-tools.tsx +25 -23
- package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
- package/src/components/ui/action-menu/view-toggles.tsx +256 -51
- package/src/components/ui/build-panel/build-panel.tsx +163 -25
- package/src/components/ui/command-palette/editor-commands.tsx +38 -11
- package/src/components/ui/controls/finish-swatch.tsx +45 -19
- package/src/components/ui/controls/material-picker.tsx +24 -17
- package/src/components/ui/controls/metric-control.test.ts +52 -0
- package/src/components/ui/controls/metric-control.tsx +62 -24
- package/src/components/ui/controls/segmented-control.tsx +5 -2
- package/src/components/ui/controls/slider-control.test.tsx +23 -0
- package/src/components/ui/controls/slider-control.tsx +30 -14
- package/src/components/ui/floating-level-selector.tsx +100 -48
- package/src/components/ui/helpers/building-helper.tsx +5 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
- package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
- package/src/components/ui/helpers/helper-manager.tsx +82 -57
- package/src/components/ui/helpers/item-helper.tsx +6 -4
- package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
- package/src/components/ui/level-duplicate-dialog.tsx +32 -23
- package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
- package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
- package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
- package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
- package/src/components/ui/panels/inspector-extensions.tsx +66 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
- package/src/components/ui/panels/multi-field-value.test.ts +27 -0
- package/src/components/ui/panels/multi-field-value.ts +20 -1
- package/src/components/ui/panels/multi-height-mode.tsx +8 -7
- package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
- package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
- package/src/components/ui/panels/node-display.ts +35 -24
- package/src/components/ui/panels/panel-manager.tsx +5 -3
- package/src/components/ui/panels/panel-wrapper.tsx +22 -109
- package/src/components/ui/panels/parametric-field-control.tsx +13 -10
- package/src/components/ui/panels/parametric-inspector.tsx +18 -12
- package/src/components/ui/panels/reference-panel.tsx +51 -35
- package/src/components/ui/panels/room-construction-rows.tsx +464 -65
- package/src/components/ui/panels/room-name-options.test.ts +36 -3
- package/src/components/ui/panels/room-name-options.ts +110 -11
- package/src/components/ui/panels/room-openings-field.tsx +21 -12
- package/src/components/ui/panels/room-panel.tsx +206 -99
- package/src/components/ui/panels/shape-choice.tsx +7 -5
- package/src/components/ui/panels/wall-region-list.tsx +7 -7
- package/src/components/ui/primitives/shortcut-token.tsx +10 -6
- package/src/components/ui/sidebar/icon-rail.tsx +11 -9
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
- package/src/components/ui/sidebar/tab-bar.tsx +4 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
- package/src/components/ui/snap-target-badge.tsx +7 -6
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
- package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
- package/src/components/viewer/floorplan-compass-button.tsx +12 -3
- package/src/components/viewer/floorplan-preview.tsx +27 -19
- package/src/components/viewer/render-settings-menu.tsx +294 -0
- package/src/components/viewer/sky-control.tsx +710 -0
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
- package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
- package/src/components/viewer/viewer-controls-bar.tsx +278 -260
- package/src/components/viewer/viewer-hover-label.tsx +12 -4
- package/src/components/viewer/viewer-rooms.tsx +61 -4
- package/src/components/viewer/viewer-scene-header.tsx +28 -17
- package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
- package/src/components/viewer-overlay.tsx +8 -0
- package/src/components/walkthrough-hud.tsx +75 -2
- package/src/hooks/use-keyboard.test.ts +85 -4
- package/src/hooks/use-keyboard.ts +65 -7
- package/src/hooks/use-selected-room.ts +12 -1
- package/src/i18n/floor-footprint-name.ts +30 -0
- package/src/i18n/level-name.ts +39 -0
- package/src/i18n/locales/en/messages.po +15117 -0
- package/src/i18n/locales/en/messages.ts +1 -0
- package/src/i18n/locales/en-XA/messages.ts +1 -0
- package/src/i18n/locales/es/messages.po +37501 -0
- package/src/i18n/locales/es/messages.ts +1 -0
- package/src/i18n/locales/fr/messages.po +37501 -0
- package/src/i18n/locales/fr/messages.ts +1 -0
- package/src/i18n/locales/ja/messages.po +37501 -0
- package/src/i18n/locales/ja/messages.ts +1 -0
- package/src/i18n/locales/zh-CN/messages.po +37501 -0
- package/src/i18n/locales/zh-CN/messages.ts +1 -0
- package/src/i18n/locales.test.ts +63 -0
- package/src/i18n/locales.ts +32 -0
- package/src/i18n/message-slot.tsx +25 -0
- package/src/i18n/node-messages.ts +169 -0
- package/src/i18n/pseudo-locale.ts +17 -0
- package/src/i18n/published-entry.test.ts +17 -0
- package/src/i18n/scene-theme-name.ts +16 -0
- package/src/index.tsx +156 -4
- package/src/lib/add-storey.test.ts +3 -3
- package/src/lib/contextual-help.test.ts +20 -1
- package/src/lib/contextual-help.ts +95 -19
- package/src/lib/continuation.ts +38 -11
- package/src/lib/floor-footprints.test.ts +2 -1
- package/src/lib/floor-foundation-support.test.ts +7 -3
- package/src/lib/floorplan/dimension-drive.test.ts +36 -2
- package/src/lib/floorplan/dimension-drive.ts +68 -3
- package/src/lib/floorplan/floorplan-export.test.ts +16 -5
- package/src/lib/floorplan/floorplan-export.tsx +13 -16
- package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
- package/src/lib/floorplan/floorplan-extension.ts +37 -18
- package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
- package/src/lib/floorplan/floorplan-mode.ts +14 -12
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
- package/src/lib/floorplan/floorplan-readonly.ts +3 -3
- package/src/lib/floorplan/schedules.ts +10 -6
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
- package/src/lib/geometry-script/author.test.ts +114 -0
- package/src/lib/geometry-script/author.ts +55 -23
- package/src/lib/geometry-script/client.ts +3 -0
- package/src/lib/geometry-script/finish-scripts.ts +7 -7
- package/src/lib/glb-export.test.ts +327 -0
- package/src/lib/glb-export.ts +70 -2
- package/src/lib/grid-frame.test.ts +56 -3
- package/src/lib/grid-frame.ts +28 -1
- package/src/lib/history.test.ts +2 -1
- package/src/lib/hud-title.ts +48 -24
- package/src/lib/ifc-export.test.ts +41 -1
- package/src/lib/inspector-card-mode.test.ts +3 -31
- package/src/lib/inspector-card-mode.ts +5 -10
- package/src/lib/inspector-expanded.test.ts +13 -0
- package/src/lib/inspector-expanded.ts +13 -1
- package/src/lib/keyboard-pan.test.ts +7 -0
- package/src/lib/keyboard-pan.ts +9 -2
- package/src/lib/layer-thickness.test.ts +22 -0
- package/src/lib/layer-thickness.ts +79 -0
- package/src/lib/lot/drop-in.test.ts +59 -1
- package/src/lib/lot/drop-in.ts +42 -50
- package/src/lib/material-swatch-style.ts +24 -0
- package/src/lib/measurement-parser.ts +7 -3
- package/src/lib/measurements.test.ts +17 -0
- package/src/lib/measurements.ts +20 -5
- package/src/lib/mezzanine-selection.test.ts +3 -1
- package/src/lib/orbit-near-plane.test.ts +12 -0
- package/src/lib/orbit-near-plane.ts +13 -0
- package/src/lib/paint-regions.test.ts +8 -5
- package/src/lib/paint-scope.test.ts +53 -0
- package/src/lib/paint-scope.ts +18 -4
- package/src/lib/panel-rows.ts +17 -8
- package/src/lib/reference-scale.ts +83 -0
- package/src/lib/room-built-on.test.ts +26 -12
- package/src/lib/room-built-on.ts +20 -2
- package/src/lib/room-construction.test.ts +28 -1
- package/src/lib/room-construction.ts +14 -0
- package/src/lib/room-controls-phase6.test.ts +140 -0
- package/src/lib/room-handle-drag.ts +557 -42
- package/src/lib/room-name-catalog.test.ts +8 -0
- package/src/lib/room-name-catalog.ts +8 -1
- package/src/lib/room-name-messages.test.ts +132 -0
- package/src/lib/room-name-messages.ts +147 -0
- package/src/lib/room-name-storage.test.ts +334 -0
- package/src/lib/room-push-dimensions.test.ts +37 -0
- package/src/lib/room-push-dimensions.ts +226 -2
- package/src/lib/room-selection.test.ts +23 -6
- package/src/lib/scene-clipboard.test.ts +70 -49
- package/src/lib/scene-clipboard.ts +9 -4
- package/src/lib/selection-cycle.ts +63 -0
- package/src/lib/separator-edit.test.ts +431 -0
- package/src/lib/separator-edit.ts +179 -0
- package/src/lib/separator-lines.ts +38 -0
- package/src/lib/sfx-bus.ts +27 -1
- package/src/lib/sfx-player.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +5 -1
- package/src/lib/tool-shortcuts.ts +130 -0
- package/src/lib/viewer-selection.test.ts +28 -24
- package/src/lib/viewer-selection.ts +20 -5
- package/src/lib/wall-draw-variant.test.ts +7 -6
- package/src/lib/wall-draw-variant.ts +107 -6
- package/src/lib/wall-nudge.test.ts +44 -0
- package/src/lib/wall-nudge.ts +131 -0
- package/src/lib/wall-parallel-snap.test.ts +83 -0
- package/src/lib/wall-parallel-snap.ts +176 -0
- package/src/lib/wall-placement-pointer.test.ts +23 -0
- package/src/lib/wall-placement-pointer.ts +18 -0
- package/src/lib/wall-pull.test.ts +245 -0
- package/src/lib/wall-pull.ts +209 -0
- package/src/lib/wall-push-line-up.test.ts +132 -0
- package/src/lib/wall-region-list.ts +14 -5
- package/src/lib/wall-run-snap.test.ts +188 -0
- package/src/lib/wall-run-snap.ts +83 -0
- package/src/lib/wall-type.test.ts +125 -0
- package/src/lib/wall-type.ts +119 -0
- package/src/lib/window-interaction.test.ts +42 -0
- package/src/lib/window-interaction.ts +47 -5
- package/src/lib/world-grid-snap.test.ts +139 -2
- package/src/lib/world-grid-snap.ts +102 -105
- package/src/store/use-alignment-guides.ts +27 -1
- package/src/store/use-editor.tsx +48 -0
- package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
- package/src/store/use-floorplan-annotation-visibility.ts +47 -22
- package/src/store/use-wall-snap-indicator.ts +15 -2
|
@@ -139,4 +139,49 @@ describe('selectable candidates', () => {
|
|
|
139
139
|
})
|
|
140
140
|
expect(collectSelectableCandidateIds().sort()).toEqual(['item_pick', 'wall_pick'])
|
|
141
141
|
})
|
|
142
|
+
|
|
143
|
+
test("a box leaves a room's automatic floor plate and ceiling to the room, keeping what hangs from it", () => {
|
|
144
|
+
const restore = nodeRegistry._snapshot()
|
|
145
|
+
try {
|
|
146
|
+
for (const kind of ['slab', 'ceiling', 'box-select-test-light'] as const)
|
|
147
|
+
if (!nodeRegistry.has(kind))
|
|
148
|
+
registerNode({
|
|
149
|
+
kind,
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
152
|
+
category: 'structure',
|
|
153
|
+
defaults: () => ({}),
|
|
154
|
+
capabilities: { selectable: {} },
|
|
155
|
+
} as AnyNodeDefinition)
|
|
156
|
+
useScene.setState({
|
|
157
|
+
nodes: {
|
|
158
|
+
level_test: {
|
|
159
|
+
id: 'level_test',
|
|
160
|
+
type: 'level',
|
|
161
|
+
parentId: 'building_test',
|
|
162
|
+
children: ['slab_shared', 'slab_drawn', 'ceiling_auto'],
|
|
163
|
+
},
|
|
164
|
+
slab_shared: {
|
|
165
|
+
id: 'slab_shared',
|
|
166
|
+
type: 'slab',
|
|
167
|
+
parentId: 'level_test',
|
|
168
|
+
boundary: 'auto',
|
|
169
|
+
plateRole: 'base',
|
|
170
|
+
},
|
|
171
|
+
slab_drawn: { id: 'slab_drawn', type: 'slab', parentId: 'level_test' },
|
|
172
|
+
ceiling_auto: {
|
|
173
|
+
id: 'ceiling_auto',
|
|
174
|
+
type: 'ceiling',
|
|
175
|
+
parentId: 'level_test',
|
|
176
|
+
boundary: 'auto',
|
|
177
|
+
children: ['light_hung'],
|
|
178
|
+
},
|
|
179
|
+
light_hung: { id: 'light_hung', type: 'box-select-test-light', parentId: 'ceiling_auto' },
|
|
180
|
+
} as unknown as Record<string, AnyNode>,
|
|
181
|
+
})
|
|
182
|
+
expect(collectSelectableCandidateIds().sort()).toEqual(['light_hung', 'slab_drawn'])
|
|
183
|
+
} finally {
|
|
184
|
+
restore()
|
|
185
|
+
}
|
|
186
|
+
})
|
|
142
187
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
isDerivedNode,
|
|
4
5
|
isRegistrySelectable,
|
|
5
6
|
type LevelNode,
|
|
6
7
|
nodeRegistry,
|
|
@@ -16,6 +17,14 @@ function isVisibleSelectableNode(node: AnyNode): boolean {
|
|
|
16
17
|
return isRegistrySelectable(node.type)
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Derived construction (a room's automatic floor plate or ceiling) belongs to its rooms, so a box
|
|
22
|
+
* never takes it along: a box round a porch would otherwise catch the house's shared floor plate,
|
|
23
|
+
* and deleting the selection took the floor out from under every room on it. A click still selects
|
|
24
|
+
* one on its own.
|
|
25
|
+
*/
|
|
26
|
+
const boxSelectable = (node: AnyNode) => !isDerivedNode(node)
|
|
27
|
+
|
|
19
28
|
export function collectSelectableCandidateIds(): string[] {
|
|
20
29
|
const { levelId } = useViewer.getState().selection
|
|
21
30
|
const { nodes } = useScene.getState()
|
|
@@ -43,7 +52,8 @@ export function collectSelectableCandidateIds(): string[] {
|
|
|
43
52
|
)
|
|
44
53
|
return
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
// A ceiling's hung lights and fans are still the box's; the ceiling is not.
|
|
56
|
+
if (isRegistrySelectable(node.type) && boxSelectable(node)) {
|
|
47
57
|
addNode(node)
|
|
48
58
|
}
|
|
49
59
|
|
|
@@ -70,7 +80,8 @@ export function collectSelectableCandidateIds(): string[] {
|
|
|
70
80
|
|
|
71
81
|
const buildingId = resolveBuildingForLevel(levelId as AnyNodeId, nodes)
|
|
72
82
|
for (const node of Object.values(nodes)) {
|
|
73
|
-
if (!node || node.type === 'level' || !isVisibleSelectableNode(node))
|
|
83
|
+
if (!node || node.type === 'level' || !isVisibleSelectableNode(node) || !boxSelectable(node))
|
|
84
|
+
continue
|
|
74
85
|
|
|
75
86
|
const def = nodeRegistry.get(node.type)
|
|
76
87
|
const isBuildingScoped = def?.floorplanScope === 'building'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type MessageDescriptor, usePascalI18n } from '@pascal-app/core'
|
|
2
2
|
import type { ThreeElements } from '@react-three/fiber'
|
|
3
3
|
import { forwardRef } from 'react'
|
|
4
4
|
import { CircleGeometry, CylinderGeometry, type Group, SphereGeometry } from 'three'
|
|
@@ -8,6 +8,7 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
8
8
|
import { wallCursorIcon } from '../../../lib/tool-cursor-icon'
|
|
9
9
|
import useEditor from '../../../store/use-editor'
|
|
10
10
|
import useWallSnapIndicator from '../../../store/use-wall-snap-indicator'
|
|
11
|
+
import { Html } from '../../editor/localized-html'
|
|
11
12
|
|
|
12
13
|
interface CursorSphereProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
13
14
|
color?: string
|
|
@@ -51,6 +52,7 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
51
52
|
},
|
|
52
53
|
ref,
|
|
53
54
|
) {
|
|
55
|
+
const i18n = usePascalI18n()
|
|
54
56
|
const tool = useEditor((s) => s.tool)
|
|
55
57
|
const mode = useEditor((s) => s.mode)
|
|
56
58
|
const catalogCategory = useEditor((s) => s.catalogCategory)
|
|
@@ -62,7 +64,8 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
62
64
|
const wallMode = useEditor((s) => s.continuationByContext.wall)
|
|
63
65
|
|
|
64
66
|
// Find the icon for the current tool — the wall shows its Rooms variant.
|
|
65
|
-
let activeToolConfig: { label: string; iconSrc: string } | null | undefined =
|
|
67
|
+
let activeToolConfig: { label: string | MessageDescriptor; iconSrc: string } | null | undefined =
|
|
68
|
+
null
|
|
66
69
|
if (mode === 'build' && tool) {
|
|
67
70
|
if (tool === 'wall') {
|
|
68
71
|
activeToolConfig = wallCursorIcon(wallMode)
|
|
@@ -162,7 +165,11 @@ export const CursorSphere = forwardRef<Group, CursorSphereProps>(function Cursor
|
|
|
162
165
|
{tooltipContent || (
|
|
163
166
|
// eslint-disable-next-line @next/next/no-img-element
|
|
164
167
|
<img
|
|
165
|
-
alt={
|
|
168
|
+
alt={
|
|
169
|
+
typeof activeToolConfig!.label === 'string'
|
|
170
|
+
? activeToolConfig!.label
|
|
171
|
+
: i18n._(activeToolConfig!.label)
|
|
172
|
+
}
|
|
166
173
|
src={activeToolConfig!.iconSrc}
|
|
167
174
|
style={{
|
|
168
175
|
width: '100%',
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
|
-
import { Html } from '@react-three/drei'
|
|
6
5
|
import { useEffect, useMemo } from 'react'
|
|
7
6
|
import { PlaneGeometry } from 'three'
|
|
8
7
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
type LinearUnit,
|
|
14
13
|
type MetricNotation,
|
|
15
14
|
} from '../../../lib/measurements'
|
|
15
|
+
import { Html } from '../../editor/localized-html'
|
|
16
16
|
import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
|
|
17
17
|
|
|
18
18
|
const VALID_COLOR = 0x22_c5_5e // green-500
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
-
import { Html } from '@react-three/drei'
|
|
5
4
|
import { useLayoutEffect, useMemo } from 'react'
|
|
6
5
|
import { BufferGeometry, Float32BufferAttribute, Line as ThreeLine } from 'three'
|
|
7
6
|
import { LineBasicNodeMaterial } from 'three/webgpu'
|
|
8
7
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
9
8
|
import type { PlacementPreviewDimension } from '../../../store/use-placement-preview'
|
|
10
9
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
10
|
+
import { Html } from '../../editor/localized-html'
|
|
11
11
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
12
12
|
|
|
13
13
|
const DIMENSION_COLOR = 0x63_66_f1
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
useLiveTerrain,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
12
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
|
-
import { Html } from '@react-three/drei'
|
|
14
13
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
15
14
|
import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
|
16
15
|
import {
|
|
@@ -25,6 +24,7 @@ import { EDITOR_LAYER } from '../../../lib/constants'
|
|
|
25
24
|
import { sculptFieldForSite, terrainPointInsideSite } from '../../../lib/terrain-sculpt'
|
|
26
25
|
import { brushRingColor } from '../../../lib/terrain-verb-color'
|
|
27
26
|
import useEditor from '../../../store/use-editor'
|
|
27
|
+
import { Html } from '../../editor/localized-html'
|
|
28
28
|
import { formatMeasurement } from '../../editor/measurement-pill'
|
|
29
29
|
|
|
30
30
|
/** Segments in the cursor ring. 64 reads as a circle at any usable brush size. */
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createTerrainField,
|
|
10
10
|
DoorNode as DoorSchema,
|
|
11
11
|
encodeTerrainField,
|
|
12
|
+
findOpenWallEnds,
|
|
12
13
|
flattenPatch,
|
|
13
14
|
GROUND_SUPPORT_ID,
|
|
14
15
|
getFloorPlacedElevation,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
resolveTerrainWallConstructionOptions,
|
|
20
21
|
runAsSingleSceneHistoryStep,
|
|
21
22
|
SlabNode,
|
|
23
|
+
snapWorldXZToBuildingLocal,
|
|
22
24
|
spatialGridManager,
|
|
23
25
|
useScene,
|
|
24
26
|
type WallNode,
|
|
@@ -30,11 +32,15 @@ import {
|
|
|
30
32
|
migrateRoomZones,
|
|
31
33
|
} from '@pascal-app/core/scene-migrations'
|
|
32
34
|
import { useViewer } from '@pascal-app/viewer'
|
|
33
|
-
import
|
|
35
|
+
import type { SnappingMode } from '../../../lib/snapping-mode'
|
|
36
|
+
import { snapBuildingLocalToWorldGrid } from '../../../lib/world-grid-snap'
|
|
37
|
+
import useEditor, { isAngleSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
34
38
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
35
39
|
import {
|
|
36
40
|
createWallOnCurrentLevel,
|
|
41
|
+
getSegmentGridStep,
|
|
37
42
|
resolveEndpointWallSplit,
|
|
43
|
+
snapWallDraftPoint,
|
|
38
44
|
snapWallDraftPointDetailed,
|
|
39
45
|
} from './wall-drafting'
|
|
40
46
|
import type { WallPlanPoint } from './wall-snap-geometry'
|
|
@@ -758,27 +764,64 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
758
764
|
expect(levelWalls()).toHaveLength(2)
|
|
759
765
|
})
|
|
760
766
|
|
|
761
|
-
test('
|
|
762
|
-
|
|
767
|
+
test.each(['grid', 'lines', 'angles', 'off'] as const)(
|
|
768
|
+
'%s drafting splits the host at a mid-wall junction',
|
|
769
|
+
(mode) => {
|
|
770
|
+
useEditor.getState().setSnappingMode('wall', mode)
|
|
771
|
+
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
772
|
+
|
|
773
|
+
expect(created?.end).toEqual([2, 0])
|
|
774
|
+
expect(useScene.getState().nodes['wall_a' as AnyNodeId]).toBeUndefined()
|
|
775
|
+
expect(
|
|
776
|
+
levelWalls()
|
|
777
|
+
.filter((wall) => wall.id !== created?.id)
|
|
778
|
+
.map(({ start, end }) => [start, end]),
|
|
779
|
+
).toEqual([
|
|
780
|
+
[
|
|
781
|
+
[0, 0],
|
|
782
|
+
[2, 0],
|
|
783
|
+
],
|
|
784
|
+
[
|
|
785
|
+
[2, 0],
|
|
786
|
+
[4, 0],
|
|
787
|
+
],
|
|
788
|
+
])
|
|
789
|
+
},
|
|
790
|
+
)
|
|
763
791
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
792
|
+
test('a wall drawn exactly over one built another way takes its place', () => {
|
|
793
|
+
// The seed wall is plain; the wall tool draws a thicker one.
|
|
794
|
+
const previousDefaults = useEditor.getState().toolDefaults
|
|
795
|
+
useEditor.setState({ toolDefaults: { ...previousDefaults, wall: { thickness: 0.3 } } } as never)
|
|
796
|
+
try {
|
|
797
|
+
const drawn = createWallOnCurrentLevel([0, 0], [4, 0])
|
|
798
|
+
expect(drawn).not.toBeNull()
|
|
799
|
+
expect(useScene.getState().nodes['wall_a' as AnyNodeId]).toBeUndefined()
|
|
800
|
+
expect(levelWalls().map((wall) => wall.id)).toEqual([drawn!.id])
|
|
801
|
+
} finally {
|
|
802
|
+
useEditor.setState({ toolDefaults: previousDefaults } as never)
|
|
803
|
+
}
|
|
774
804
|
})
|
|
775
805
|
|
|
776
|
-
test('
|
|
806
|
+
test('a wall drawn exactly over one built the same is refused', () => {
|
|
777
807
|
expect(createWallOnCurrentLevel([0, 0], [4, 0])).toBeNull()
|
|
778
808
|
expect(levelWalls()).toHaveLength(1)
|
|
779
809
|
})
|
|
780
810
|
|
|
781
|
-
test('
|
|
811
|
+
test('a wall drawn on along part of another takes that stretch over', () => {
|
|
812
|
+
const drawn = createWallOnCurrentLevel([2, 0], [6, 0])
|
|
813
|
+
expect(drawn).not.toBeNull()
|
|
814
|
+
const walls = levelWalls()
|
|
815
|
+
// The seed wall keeps [0, 2]; nothing else runs along [2, 4].
|
|
816
|
+
expect(walls).toHaveLength(2)
|
|
817
|
+
const seed = walls.find((wall) => wall.id !== drawn!.id)!
|
|
818
|
+
expect([seed.start, seed.end]).toEqual([
|
|
819
|
+
[0, 0],
|
|
820
|
+
[2, 0],
|
|
821
|
+
])
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
test('grid mode: endpoint resolved onto a wall body splits its host', () => {
|
|
782
825
|
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
783
826
|
|
|
784
827
|
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
@@ -798,7 +841,7 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
798
841
|
expect(levelWalls()).toHaveLength(2)
|
|
799
842
|
})
|
|
800
843
|
|
|
801
|
-
test('
|
|
844
|
+
test('a T into a wall rehosts its door at the same position', () => {
|
|
802
845
|
const door = DoorSchema.parse({
|
|
803
846
|
position: [1, 1.05, 0],
|
|
804
847
|
parentId: 'wall_a',
|
|
@@ -809,12 +852,12 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
809
852
|
const created = createWallOnCurrentLevel([2, 2], [2, 0])
|
|
810
853
|
|
|
811
854
|
expect(created?.end).toEqual([2, 0])
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
expect(
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
855
|
+
const kept = useScene.getState().nodes[door.id as AnyNodeId] as DoorSchema
|
|
856
|
+
expect(kept.parentId).not.toBe('wall_a')
|
|
857
|
+
expect(kept.position[0]).toBe(1)
|
|
858
|
+
expect((useScene.getState().nodes[kept.parentId as AnyNodeId] as WallNode).children).toContain(
|
|
859
|
+
door.id,
|
|
860
|
+
)
|
|
818
861
|
})
|
|
819
862
|
|
|
820
863
|
test('a splitting commit lands as a single undo step', () => {
|
|
@@ -826,7 +869,7 @@ describe('createWallOnCurrentLevel', () => {
|
|
|
826
869
|
expect(useScene.temporal.getState().pastStates.length - before).toBe(1)
|
|
827
870
|
})
|
|
828
871
|
|
|
829
|
-
test('a crossing splits both
|
|
872
|
+
test('a crossing splits both walls in one undo step', () => {
|
|
830
873
|
const before = useScene.temporal.getState().pastStates.length
|
|
831
874
|
|
|
832
875
|
const created = createWallOnCurrentLevel([2, -2], [2, 2])
|
|
@@ -1095,4 +1138,237 @@ describe('snapWallDraftPointDetailed', () => {
|
|
|
1095
1138
|
expect(freed.point).toEqual([2, 0])
|
|
1096
1139
|
expect(freed.snap).toBeNull()
|
|
1097
1140
|
})
|
|
1141
|
+
|
|
1142
|
+
test('an end aimed into an L corner block joins the corner and reads as one', () => {
|
|
1143
|
+
const south = { ...makeWall([0, 0], [4, 0], 'wall_south'), thickness: 0.2 }
|
|
1144
|
+
const west = { ...makeWall([0, 4], [0, 0], 'wall_west'), thickness: 0.2 }
|
|
1145
|
+
const result = snapWallDraftPointDetailed({
|
|
1146
|
+
point: [0.08, 0.08],
|
|
1147
|
+
walls: [south, west],
|
|
1148
|
+
start: [2, 3],
|
|
1149
|
+
magnetic: false,
|
|
1150
|
+
step: 0,
|
|
1151
|
+
captureWallBodies: true,
|
|
1152
|
+
})
|
|
1153
|
+
expect(result.point).toEqual([0, 0])
|
|
1154
|
+
expect(result.snap).toBe('endpoint')
|
|
1155
|
+
expect(result.targetWallIds).toEqual(['wall_south', 'wall_west'])
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
// The 3D tool snaps to the floor below as a reference but can't join it: an
|
|
1159
|
+
// end by a lower wall's face stays put, while its corner still snaps.
|
|
1160
|
+
test('walls left out of bodyWalls never capture an end, but still snap corners', () => {
|
|
1161
|
+
const lower = { ...makeWall([0, 0.4], [4, 0.4], 'wall_lower'), thickness: 0.2 }
|
|
1162
|
+
const snap = (point: WallPlanPoint, bodyWalls?: WallNode[]) =>
|
|
1163
|
+
snapWallDraftPointDetailed({
|
|
1164
|
+
point,
|
|
1165
|
+
walls: [lower],
|
|
1166
|
+
start: [1, 3],
|
|
1167
|
+
magnetic: false,
|
|
1168
|
+
step: 0,
|
|
1169
|
+
captureWallBodies: true,
|
|
1170
|
+
bodyWalls,
|
|
1171
|
+
})
|
|
1172
|
+
|
|
1173
|
+
const captured = snap([1, 0.5])
|
|
1174
|
+
expect(captured.snap).toBe('wall')
|
|
1175
|
+
expect(captured.point[1]).toBeCloseTo(0.4, 12)
|
|
1176
|
+
|
|
1177
|
+
const free = snap([1, 0.5], [])
|
|
1178
|
+
expect(free.point).toEqual([1, 0.5])
|
|
1179
|
+
expect(free.snap).toBeNull()
|
|
1180
|
+
|
|
1181
|
+
const corner = snap([0.02, 0.42], [])
|
|
1182
|
+
expect(corner.point).toEqual([0, 0.4])
|
|
1183
|
+
expect(corner.snap).toBe('endpoint')
|
|
1184
|
+
})
|
|
1098
1185
|
})
|
|
1186
|
+
|
|
1187
|
+
// The site planner sets a house on its lot turned and offset. Its walls are
|
|
1188
|
+
// drawn square to the house, in building-local metres. An interior wall drawn
|
|
1189
|
+
// across it has to meet both exterior walls exactly and stay square to the
|
|
1190
|
+
// house however the building is turned, in 3D and on the plan alike.
|
|
1191
|
+
const HOUSE_BUILDING_ID = 'building_lot'
|
|
1192
|
+
// A 30' × 24' house of 8" walls, 2' × 1' off the building origin, so no wall
|
|
1193
|
+
// line or face lies on the 0.5 m lattice.
|
|
1194
|
+
const HOUSE_MIN: WallPlanPoint = [0.6096, 0.3048]
|
|
1195
|
+
const HOUSE_MAX: WallPlanPoint = [9.7536, 7.62]
|
|
1196
|
+
const EXTERIOR_THICKNESS = 0.2032
|
|
1197
|
+
// Where a builder aims an interior wall's end: just inside the exterior
|
|
1198
|
+
// wall's interior face (1.18" off it).
|
|
1199
|
+
const FACE_AIM = EXTERIOR_THICKNESS / 2 + 0.03
|
|
1200
|
+
|
|
1201
|
+
const BUILDING_POSES: Array<{
|
|
1202
|
+
label: string
|
|
1203
|
+
yaw: number
|
|
1204
|
+
position: [number, number, number]
|
|
1205
|
+
}> = [
|
|
1206
|
+
{ label: 'an unturned building at the origin', yaw: 0, position: [0, 0, 0] },
|
|
1207
|
+
{
|
|
1208
|
+
label: 'a building turned 17° and offset on its lot',
|
|
1209
|
+
yaw: (17 * Math.PI) / 180,
|
|
1210
|
+
position: [12.37, 0, -8.91],
|
|
1211
|
+
},
|
|
1212
|
+
]
|
|
1213
|
+
|
|
1214
|
+
function seedHouse({ yaw, position }: { yaw: number; position: [number, number, number] }) {
|
|
1215
|
+
const [x0, z0] = HOUSE_MIN
|
|
1216
|
+
const [x1, z1] = HOUSE_MAX
|
|
1217
|
+
const walls = [
|
|
1218
|
+
makeWall([x0, z0], [x1, z0], 'wall_south'),
|
|
1219
|
+
makeWall([x1, z0], [x1, z1], 'wall_east'),
|
|
1220
|
+
makeWall([x1, z1], [x0, z1], 'wall_north'),
|
|
1221
|
+
makeWall([x0, z1], [x0, z0], 'wall_west'),
|
|
1222
|
+
].map((wall) => ({ ...wall, thickness: EXTERIOR_THICKNESS }))
|
|
1223
|
+
const building = BuildingNode.parse({
|
|
1224
|
+
id: HOUSE_BUILDING_ID,
|
|
1225
|
+
position,
|
|
1226
|
+
rotation: [0, yaw, 0],
|
|
1227
|
+
children: [LEVEL_ID],
|
|
1228
|
+
})
|
|
1229
|
+
const level = LevelSchema.parse({ id: LEVEL_ID, parentId: building.id, level: 0, height: 2.5 })
|
|
1230
|
+
useScene.setState({
|
|
1231
|
+
nodes: Object.fromEntries([
|
|
1232
|
+
[building.id, building],
|
|
1233
|
+
[level.id, { ...level, children: walls.map((wall) => wall.id) }],
|
|
1234
|
+
...walls.map((wall) => [wall.id, wall] as const),
|
|
1235
|
+
]),
|
|
1236
|
+
rootNodeIds: [building.id],
|
|
1237
|
+
dirtyNodes: new Set(),
|
|
1238
|
+
collections: {},
|
|
1239
|
+
} as never)
|
|
1240
|
+
useViewer.setState({
|
|
1241
|
+
selection: {
|
|
1242
|
+
buildingId: HOUSE_BUILDING_ID,
|
|
1243
|
+
levelId: LEVEL_ID,
|
|
1244
|
+
zoneId: null,
|
|
1245
|
+
selectedIds: [],
|
|
1246
|
+
},
|
|
1247
|
+
} as never)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
for (const pose of BUILDING_POSES) {
|
|
1251
|
+
describe(`an interior wall drawn across a house in ${pose.label}`, () => {
|
|
1252
|
+
const [, z0] = HOUSE_MIN
|
|
1253
|
+
const [, z1] = HOUSE_MAX
|
|
1254
|
+
|
|
1255
|
+
beforeEach(() => {
|
|
1256
|
+
useEditor.getState().setGridSnapStep(0.5)
|
|
1257
|
+
// A reshaping-endpoint scope resolves to the 'wall' snap context
|
|
1258
|
+
// without the node registry, as in `createWallOnCurrentLevel` above.
|
|
1259
|
+
useInteractionScope
|
|
1260
|
+
.getState()
|
|
1261
|
+
.begin({ kind: 'reshaping', nodeId: 'wall_south', reshape: 'endpoint', driver: 'tool' })
|
|
1262
|
+
seedHouse(pose)
|
|
1263
|
+
useScene.temporal.getState().clear()
|
|
1264
|
+
useScene.temporal.getState().resume()
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
afterEach(() => {
|
|
1268
|
+
useEditor.getState().setSnappingMode('wall', 'grid')
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
const setMode = (mode: SnappingMode) => useEditor.getState().setSnappingMode('wall', mode)
|
|
1272
|
+
|
|
1273
|
+
// The cursor as a grid event hands it over: a hit on the world plane,
|
|
1274
|
+
// taken into the building's frame.
|
|
1275
|
+
const cursorAt = (local: WallPlanPoint): WallPlanPoint => {
|
|
1276
|
+
const cos = Math.cos(pose.yaw)
|
|
1277
|
+
const sin = Math.sin(pose.yaw)
|
|
1278
|
+
const worldX = pose.position[0] + local[0] * cos + local[1] * sin
|
|
1279
|
+
const worldZ = pose.position[2] - local[0] * sin + local[1] * cos
|
|
1280
|
+
return snapWorldXZToBuildingLocal(worldX, worldZ, pose.position, pose.yaw, 0).local
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// The 3D wall tool's snap call.
|
|
1284
|
+
const snapIn3d = (point: WallPlanPoint, start?: WallPlanPoint, captureWallBodies = true) =>
|
|
1285
|
+
snapWallDraftPointDetailed({
|
|
1286
|
+
point,
|
|
1287
|
+
walls: levelWalls(),
|
|
1288
|
+
start,
|
|
1289
|
+
angleSnap: start !== undefined && isAngleSnapActive(),
|
|
1290
|
+
magnetic: isMagneticSnapActive(),
|
|
1291
|
+
captureWallBodies,
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
// The plan's click: the same cursor through the plan's grid callback.
|
|
1295
|
+
const snapOnPlan = (point: WallPlanPoint, start?: WallPlanPoint) =>
|
|
1296
|
+
snapWallDraftPoint({
|
|
1297
|
+
point,
|
|
1298
|
+
walls: levelWalls(),
|
|
1299
|
+
start,
|
|
1300
|
+
angleSnap: start !== undefined && isAngleSnapActive(),
|
|
1301
|
+
magnetic: isMagneticSnapActive(),
|
|
1302
|
+
gridSnap: (p) => snapBuildingLocalToWorldGrid(p, getSegmentGridStep()),
|
|
1303
|
+
captureWallBodies: true,
|
|
1304
|
+
})
|
|
1305
|
+
|
|
1306
|
+
const partitionOpenEnds = (partition: WallNode) =>
|
|
1307
|
+
findOpenWallEnds(useScene.getState().nodes, LEVEL_ID).filter(
|
|
1308
|
+
(end) => end.wallId === partition.id,
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
test('grid: both ends land on the exterior walls, square to the house, with no gap', () => {
|
|
1312
|
+
setMode('grid')
|
|
1313
|
+
const startAim = cursorAt([4.2, z0 + FACE_AIM])
|
|
1314
|
+
const start = snapIn3d(startAim)
|
|
1315
|
+
expect(start.snap).toBe('wall')
|
|
1316
|
+
expect(start.targetWallIds).toEqual(['wall_south'])
|
|
1317
|
+
expect(start.point[0]).toBeCloseTo(4, 9)
|
|
1318
|
+
expect(start.point[1]).toBeCloseTo(z0, 9)
|
|
1319
|
+
|
|
1320
|
+
// The cursor drifts a little sideways on its way across the house.
|
|
1321
|
+
const endAim = cursorAt([4.1, z1 - FACE_AIM])
|
|
1322
|
+
const end = snapIn3d(endAim, start.point)
|
|
1323
|
+
expect(end.snap).toBe('wall')
|
|
1324
|
+
expect(end.targetWallIds).toEqual(['wall_north'])
|
|
1325
|
+
expect(end.point[0]).toBeCloseTo(start.point[0], 9)
|
|
1326
|
+
expect(end.point[1]).toBeCloseTo(z1, 9)
|
|
1327
|
+
|
|
1328
|
+
// The plan snaps the same cursor to the same points.
|
|
1329
|
+
expect(snapOnPlan(startAim)).toEqual(start.point)
|
|
1330
|
+
expect(snapOnPlan(endAim, start.point)).toEqual(end.point)
|
|
1331
|
+
|
|
1332
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1333
|
+
expect(partition).not.toBeNull()
|
|
1334
|
+
expect(partitionOpenEnds(partition!)).toEqual([])
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1337
|
+
test('ortho holds the wall square to the house and lands it on the far wall', () => {
|
|
1338
|
+
setMode('angles')
|
|
1339
|
+
const startAim = cursorAt([4.2, z0 + FACE_AIM])
|
|
1340
|
+
const start = snapIn3d(startAim)
|
|
1341
|
+
expect(start.point[0]).toBeCloseTo(4.2, 9)
|
|
1342
|
+
expect(start.point[1]).toBeCloseTo(z0, 9)
|
|
1343
|
+
|
|
1344
|
+
// 4° off square: the 15° lock squares it to the house.
|
|
1345
|
+
const endAim = cursorAt([3.7, z1 - FACE_AIM])
|
|
1346
|
+
const end = snapIn3d(endAim, start.point)
|
|
1347
|
+
expect(end.snap).toBe('wall')
|
|
1348
|
+
expect(end.point[0]).toBeCloseTo(start.point[0], 9)
|
|
1349
|
+
expect(end.point[1]).toBeCloseTo(z1, 9)
|
|
1350
|
+
|
|
1351
|
+
const onPlan = snapOnPlan(endAim, start.point)
|
|
1352
|
+
expect(onPlan[0]).toBeCloseTo(end.point[0], 9)
|
|
1353
|
+
expect(onPlan[1]).toBeCloseTo(end.point[1], 9)
|
|
1354
|
+
|
|
1355
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1356
|
+
expect(partition).not.toBeNull()
|
|
1357
|
+
expect(partitionOpenEnds(partition!)).toEqual([])
|
|
1358
|
+
})
|
|
1359
|
+
|
|
1360
|
+
test('without the body snap the lattice stops the end short of the face: an open gap', () => {
|
|
1361
|
+
setMode('grid')
|
|
1362
|
+
const start = snapIn3d(cursorAt([4.2, z0 + FACE_AIM]))
|
|
1363
|
+
const end = snapIn3d(cursorAt([4.1, z1 - FACE_AIM]), start.point, false)
|
|
1364
|
+
// The lattice line under the cursor, 0.72" short of the north wall's face.
|
|
1365
|
+
expect(end.snap).toBeNull()
|
|
1366
|
+
expect(end.point[1]).toBeCloseTo(7.5, 9)
|
|
1367
|
+
|
|
1368
|
+
const partition = createWallOnCurrentLevel(start.point, end.point)
|
|
1369
|
+
const [open] = partitionOpenEnds(partition!)
|
|
1370
|
+
expect(open?.reason).toBe('gap')
|
|
1371
|
+
expect(open?.gap).toBeCloseTo(z1 - EXTERIOR_THICKNESS / 2 - 7.5, 3)
|
|
1372
|
+
})
|
|
1373
|
+
})
|
|
1374
|
+
}
|