@pascal-app/editor 1.0.5 → 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 +20 -17
- 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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { BuildingNode, CeilingNode, LevelNode, useScene } from '@pascal-app/core'
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { act, createRoot } from '@react-three/fiber'
|
|
5
|
+
import { createElement, Fragment } from 'react'
|
|
6
|
+
import { OrthographicCamera, type WebGLRenderer } from 'three'
|
|
7
|
+
import useEditor, { normalizePersistedEditorLayoutState } from '../../../store/use-editor'
|
|
8
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
9
|
+
import { CeilingSystem, CeilingTopsSync } from './ceiling-system'
|
|
10
|
+
|
|
11
|
+
test('ceilings start on for the roof level and off for the floor plans, each kept as set', () => {
|
|
12
|
+
const fresh = normalizePersistedEditorLayoutState(undefined)
|
|
13
|
+
expect(fresh.showRoofCeilings).toBe(true)
|
|
14
|
+
expect(fresh.showCeilings).toBe(false)
|
|
15
|
+
const turned = normalizePersistedEditorLayoutState({
|
|
16
|
+
showRoofCeilings: false,
|
|
17
|
+
showCeilings: true,
|
|
18
|
+
})
|
|
19
|
+
expect(turned.showRoofCeilings).toBe(false)
|
|
20
|
+
expect(turned.showCeilings).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('the level’s ceilings setting draws the ceiling tops, stepping aside to place on a ceiling', async () => {
|
|
24
|
+
const previous = {
|
|
25
|
+
scene: useScene.getState(),
|
|
26
|
+
viewer: useViewer.getState(),
|
|
27
|
+
editor: useEditor.getState(),
|
|
28
|
+
scope: useInteractionScope.getState(),
|
|
29
|
+
}
|
|
30
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
31
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
32
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
33
|
+
const makeCanvas = () =>
|
|
34
|
+
Object.assign(new EventTarget(), {
|
|
35
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
|
|
36
|
+
}) as unknown as HTMLCanvasElement
|
|
37
|
+
const canvas = makeCanvas()
|
|
38
|
+
const previewCanvas = makeCanvas()
|
|
39
|
+
const root = createRoot(canvas)
|
|
40
|
+
const previewRoot = createRoot(previewCanvas)
|
|
41
|
+
const building = BuildingNode.parse({})
|
|
42
|
+
const storey = LevelNode.parse({ parentId: building.id, level: 0, height: 3 })
|
|
43
|
+
const roof = LevelNode.parse({
|
|
44
|
+
parentId: building.id,
|
|
45
|
+
level: 1,
|
|
46
|
+
height: 3,
|
|
47
|
+
metadata: { role: 'roof' },
|
|
48
|
+
})
|
|
49
|
+
building.children = [storey.id, roof.id]
|
|
50
|
+
const ceiling = CeilingNode.parse({
|
|
51
|
+
parentId: storey.id,
|
|
52
|
+
height: 2.7,
|
|
53
|
+
polygon: [
|
|
54
|
+
[0, 0],
|
|
55
|
+
[4, 0],
|
|
56
|
+
[4, 4],
|
|
57
|
+
[0, 4],
|
|
58
|
+
],
|
|
59
|
+
})
|
|
60
|
+
const select = (levelId: typeof storey.id) =>
|
|
61
|
+
act(async () =>
|
|
62
|
+
useViewer.setState({
|
|
63
|
+
selection: { buildingId: building.id, levelId, zoneId: null, selectedIds: [] },
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
const tops = () => useViewer.getState().showCeilingTops
|
|
67
|
+
try {
|
|
68
|
+
useScene.setState({
|
|
69
|
+
nodes: {
|
|
70
|
+
[building.id]: building,
|
|
71
|
+
[storey.id]: storey,
|
|
72
|
+
[roof.id]: roof,
|
|
73
|
+
[ceiling.id]: ceiling,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
useEditor.setState({
|
|
77
|
+
...normalizePersistedEditorLayoutState(undefined),
|
|
78
|
+
tool: null,
|
|
79
|
+
selectedItem: null,
|
|
80
|
+
isPreviewMode: false,
|
|
81
|
+
isCaptureMode: false,
|
|
82
|
+
})
|
|
83
|
+
useInteractionScope.setState({ scope: { kind: 'idle' } })
|
|
84
|
+
useViewer.setState({ showCeilingTops: false, hoveredId: null })
|
|
85
|
+
await select(roof.id)
|
|
86
|
+
const configure = (target: typeof root, domElement: HTMLCanvasElement) =>
|
|
87
|
+
target.configure({
|
|
88
|
+
gl: {
|
|
89
|
+
domElement,
|
|
90
|
+
render() {},
|
|
91
|
+
setSize() {},
|
|
92
|
+
setPixelRatio() {},
|
|
93
|
+
} as unknown as WebGLRenderer,
|
|
94
|
+
camera: Object.assign(new OrthographicCamera(), { manual: true }),
|
|
95
|
+
frameloop: 'never',
|
|
96
|
+
dpr: 1,
|
|
97
|
+
size: { width: 100, height: 100, top: 0, left: 0 },
|
|
98
|
+
})
|
|
99
|
+
await configure(root, canvas)
|
|
100
|
+
await act(async () =>
|
|
101
|
+
root.render(
|
|
102
|
+
createElement(Fragment, null, createElement(CeilingTopsSync), createElement(CeilingSystem)),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
expect(tops()).toBe(true)
|
|
106
|
+
|
|
107
|
+
// A second viewer's ceiling system (the preview's) going away leaves the tops as set.
|
|
108
|
+
await configure(previewRoot, previewCanvas)
|
|
109
|
+
await act(async () => previewRoot.render(createElement(CeilingSystem)))
|
|
110
|
+
await act(async () => previewRoot.unmount())
|
|
111
|
+
expect(tops()).toBe(true)
|
|
112
|
+
|
|
113
|
+
await select(storey.id)
|
|
114
|
+
expect(tops()).toBe(false)
|
|
115
|
+
await act(async () => useEditor.getState().setShowCeilings(true))
|
|
116
|
+
expect(tops()).toBe(true)
|
|
117
|
+
await act(async () => useEditor.setState({ tool: 'ceiling' }))
|
|
118
|
+
expect(tops()).toBe(false)
|
|
119
|
+
await act(async () => useEditor.setState({ tool: null, isPreviewMode: true }))
|
|
120
|
+
expect(tops()).toBe(false)
|
|
121
|
+
await act(async () => useEditor.setState({ isPreviewMode: false }))
|
|
122
|
+
expect(tops()).toBe(true)
|
|
123
|
+
|
|
124
|
+
// The roof level keeps its own setting.
|
|
125
|
+
await select(roof.id)
|
|
126
|
+
await act(async () => useEditor.getState().setShowRoofCeilings(false))
|
|
127
|
+
expect(tops()).toBe(false)
|
|
128
|
+
expect(useEditor.getState().showCeilings).toBe(true)
|
|
129
|
+
|
|
130
|
+
await act(async () => useEditor.getState().setShowRoofCeilings(true))
|
|
131
|
+
expect(tops()).toBe(true)
|
|
132
|
+
await act(async () => root.unmount())
|
|
133
|
+
expect(tops()).toBe(false)
|
|
134
|
+
} finally {
|
|
135
|
+
useScene.setState(previous.scene, true)
|
|
136
|
+
useViewer.setState(previous.viewer, true)
|
|
137
|
+
useEditor.setState(previous.editor, true)
|
|
138
|
+
useInteractionScope.setState(previous.scope, true)
|
|
139
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
140
|
+
}
|
|
141
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
1
|
+
import { type AnyNodeId, sceneRegistry, servesAsRoofLevel, useScene } from '@pascal-app/core'
|
|
2
2
|
import { isProceduralItem } from '@pascal-app/core/procedural-items'
|
|
3
3
|
import { useViewer } from '@pascal-app/viewer'
|
|
4
4
|
import { useEffect } from 'react'
|
|
@@ -74,10 +74,59 @@ function setCeilingGridHighlighted(ceilingGrid: Mesh, highlighted: boolean) {
|
|
|
74
74
|
delete userData[CEILING_GRID_HIGHLIGHT_MATERIAL_KEY]
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
/**
|
|
78
|
+
* The "Ceilings" setting that applies on `levelId`: the roof level's, on until turned off; the
|
|
79
|
+
* floor plans', off until turned on. On, ceilings read as solid from above too.
|
|
80
|
+
*/
|
|
81
|
+
export function useCeilingsSetting(levelId: string | null | undefined): {
|
|
82
|
+
onRoofLevel: boolean
|
|
83
|
+
shown: boolean
|
|
84
|
+
setShown: (show: boolean) => void
|
|
85
|
+
} {
|
|
86
|
+
const onRoofLevel = useScene((state) => {
|
|
87
|
+
const level = levelId ? state.nodes[levelId as AnyNodeId] : undefined
|
|
88
|
+
return level?.type === 'level' && servesAsRoofLevel(state.nodes, level)
|
|
89
|
+
})
|
|
90
|
+
const shown = useEditor((state) => (onRoofLevel ? state.showRoofCeilings : state.showCeilings))
|
|
91
|
+
const setRoof = useEditor((state) => state.setShowRoofCeilings)
|
|
92
|
+
const setFloor = useEditor((state) => state.setShowCeilings)
|
|
93
|
+
return { onRoofLevel, shown, setShown: onRoofLevel ? setRoof : setFloor }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function useIsCeilingToolActive(): boolean {
|
|
78
97
|
const tool = useEditor((state) => state.tool)
|
|
79
98
|
const selectedItem = useEditor((state) => state.selectedItem)
|
|
80
99
|
const movingNode = useMovingNode()
|
|
100
|
+
return (
|
|
101
|
+
tool === 'ceiling' ||
|
|
102
|
+
selectedItem?.attachTo === 'ceiling' ||
|
|
103
|
+
(movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
|
|
104
|
+
(isProceduralItem(movingNode) && movingNode.recipe.mounting?.attachTo === 'ceiling')
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Whether the viewer draws ceiling tops (`showCeilingTops`), from the level's Ceilings setting.
|
|
110
|
+
* Mounted once by the editor, outside the canvases: the editor and preview viewers each mount a
|
|
111
|
+
* `CeilingSystem`, and two writers each turning the tops off on unmount raced.
|
|
112
|
+
*/
|
|
113
|
+
export const CeilingTopsSync = () => {
|
|
114
|
+
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
115
|
+
const ceilingsShown = useCeilingsSetting(activeLevelId).shown
|
|
116
|
+
const isPreviewMode = useEditor((state) => state.isPreviewMode)
|
|
117
|
+
const isCaptureMode = useEditor((state) => state.isCaptureMode)
|
|
118
|
+
const isCeilingToolActive = useIsCeilingToolActive()
|
|
119
|
+
// Placing on a ceiling looks through it from above, so the tops step aside; preview and
|
|
120
|
+
// captures keep the open look.
|
|
121
|
+
const showTops = ceilingsShown && !isCeilingToolActive && !isPreviewMode && !isCaptureMode
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
useViewer.getState().setShowCeilingTops(showTops)
|
|
124
|
+
return () => useViewer.getState().setShowCeilingTops(false)
|
|
125
|
+
}, [showTops])
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const CeilingSystem = () => {
|
|
81
130
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
82
131
|
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
83
132
|
const hoveredId = useViewer((state) => state.hoveredId)
|
|
@@ -87,6 +136,7 @@ export const CeilingSystem = () => {
|
|
|
87
136
|
// reveal is gated here, on the consumer.
|
|
88
137
|
const inputDragging = useViewer((state) => state.inputDragging)
|
|
89
138
|
const scopeIdle = useInteractionScope((state) => state.scope.kind === 'idle')
|
|
139
|
+
const isCeilingToolActive = useIsCeilingToolActive()
|
|
90
140
|
|
|
91
141
|
useEffect(() => {
|
|
92
142
|
const nodes = useScene.getState().nodes
|
|
@@ -96,12 +146,6 @@ export const CeilingSystem = () => {
|
|
|
96
146
|
|
|
97
147
|
const levelsToShowCeilings = new Set<string>()
|
|
98
148
|
|
|
99
|
-
const isCeilingToolActive =
|
|
100
|
-
tool === 'ceiling' ||
|
|
101
|
-
selectedItem?.attachTo === 'ceiling' ||
|
|
102
|
-
(movingNode?.type === 'item' && movingNode?.asset?.attachTo === 'ceiling') ||
|
|
103
|
-
(isProceduralItem(movingNode) && movingNode.recipe.mounting?.attachTo === 'ceiling')
|
|
104
|
-
|
|
105
149
|
if (isCeilingToolActive && activeLevelId) {
|
|
106
150
|
levelsToShowCeilings.add(activeLevelId)
|
|
107
151
|
}
|
|
@@ -162,15 +206,6 @@ export const CeilingSystem = () => {
|
|
|
162
206
|
}
|
|
163
207
|
}
|
|
164
208
|
})
|
|
165
|
-
}, [
|
|
166
|
-
tool,
|
|
167
|
-
selectedItem,
|
|
168
|
-
movingNode,
|
|
169
|
-
selectedIds,
|
|
170
|
-
activeLevelId,
|
|
171
|
-
hoveredId,
|
|
172
|
-
scopeIdle,
|
|
173
|
-
inputDragging,
|
|
174
|
-
])
|
|
209
|
+
}, [isCeilingToolActive, selectedIds, activeLevelId, hoveredId, scopeIdle, inputDragging])
|
|
175
210
|
return null
|
|
176
211
|
}
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
emitter,
|
|
6
|
+
type GridEvent,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
usePascalI18n,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
4
11
|
import {
|
|
5
12
|
CursorSphere,
|
|
6
13
|
clearPlacementSurface,
|
|
@@ -17,11 +24,12 @@ import {
|
|
|
17
24
|
useEditor,
|
|
18
25
|
useInteractionScope,
|
|
19
26
|
} from '@pascal-app/editor'
|
|
20
|
-
import { Html } from '@react-three/drei'
|
|
21
27
|
import { useThree } from '@react-three/fiber'
|
|
22
28
|
import { type RefObject, useCallback, useEffect, useRef, useState } from 'react'
|
|
23
29
|
import { createPortal } from 'react-dom'
|
|
24
30
|
import { type Group, Vector3 } from 'three'
|
|
31
|
+
import { messageWithSlot } from '../../../i18n/message-slot'
|
|
32
|
+
import { Html } from '../../editor/localized-html'
|
|
25
33
|
import type { RunSurfaceBounds, RunSurfaceTarget } from './distribution-run-contract'
|
|
26
34
|
import { clearDrawAlignment } from './draw-alignment'
|
|
27
35
|
import type { RunBodyHit, ScenePort } from './ports'
|
|
@@ -1197,6 +1205,7 @@ export function DistributionRunCursor({
|
|
|
1197
1205
|
onLengthInputChange?: (value: string) => void
|
|
1198
1206
|
onDirectionSelect?: (direction: RunPoint) => void
|
|
1199
1207
|
}) {
|
|
1208
|
+
const i18n = usePascalI18n()
|
|
1200
1209
|
const lengthFieldRef = useRef<HTMLInputElement>(null)
|
|
1201
1210
|
useEffect(() => {
|
|
1202
1211
|
if (start) lengthFieldRef.current?.focus()
|
|
@@ -1284,30 +1293,45 @@ export function DistributionRunCursor({
|
|
|
1284
1293
|
<DimensionPill parts={parts} primary={primary} unit={unit} />
|
|
1285
1294
|
{start ? (
|
|
1286
1295
|
<label className="rounded-full border border-border/60 bg-background/90 px-3 py-1 text-[11px] tabular-nums text-muted-foreground shadow-sm backdrop-blur">
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
) {
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1296
|
+
{messageWithSlot(
|
|
1297
|
+
i18n,
|
|
1298
|
+
/* i18n */ {
|
|
1299
|
+
id: 'editor.tools.distribution.distributionRunTool.lengthField',
|
|
1300
|
+
message: 'Length: {field} m',
|
|
1301
|
+
},
|
|
1302
|
+
'field',
|
|
1303
|
+
<input
|
|
1304
|
+
className="w-20 bg-transparent text-center text-foreground outline-none"
|
|
1305
|
+
data-run-length-input
|
|
1306
|
+
inputMode="decimal"
|
|
1307
|
+
onKeyDown={(event) => {
|
|
1308
|
+
if (
|
|
1309
|
+
event.key === 'Backspace' ||
|
|
1310
|
+
event.key === 'Delete' ||
|
|
1311
|
+
event.key === 'ArrowLeft' ||
|
|
1312
|
+
event.key === 'ArrowRight'
|
|
1313
|
+
) {
|
|
1314
|
+
event.stopPropagation()
|
|
1315
|
+
}
|
|
1316
|
+
}}
|
|
1317
|
+
onChange={(event) => onLengthInputChange?.(event.target.value)}
|
|
1318
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
1319
|
+
placeholder={i18n._({
|
|
1320
|
+
id: 'editor.tools.distribution.distributionRunTool.typeALength',
|
|
1321
|
+
message: 'type a length',
|
|
1322
|
+
})}
|
|
1323
|
+
ref={lengthFieldRef}
|
|
1324
|
+
style={{ pointerEvents: 'auto' }}
|
|
1325
|
+
type="text"
|
|
1326
|
+
value={lengthInput ?? ''}
|
|
1327
|
+
/>,
|
|
1328
|
+
)}
|
|
1329
|
+
<span className="ml-2">
|
|
1330
|
+
{i18n._({
|
|
1331
|
+
id: 'editor.tools.distribution.distributionRunTool.tabDirection',
|
|
1332
|
+
message: 'Tab: direction',
|
|
1333
|
+
})}
|
|
1334
|
+
</span>
|
|
1311
1335
|
</label>
|
|
1312
1336
|
) : null}
|
|
1313
1337
|
{validationMessage ? (
|
|
@@ -22,12 +22,12 @@ import {
|
|
|
22
22
|
WallNode,
|
|
23
23
|
} from '@pascal-app/core'
|
|
24
24
|
import { useViewer } from '@pascal-app/viewer'
|
|
25
|
-
import { Html } from '@react-three/drei'
|
|
26
25
|
import { act, create } from '@react-three/test-renderer'
|
|
27
26
|
import { Children, Component, cloneElement, isValidElement, type ReactNode } from 'react'
|
|
28
27
|
import { Group } from 'three'
|
|
29
28
|
import useEditor from '../../../store/use-editor'
|
|
30
29
|
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
30
|
+
import { Html } from '../../editor/localized-html'
|
|
31
31
|
import { useDraftNode } from './use-draft-node'
|
|
32
32
|
import { usePlacementCoordinator } from './use-placement-coordinator'
|
|
33
33
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
BlockNode,
|
|
4
|
+
BuildingNode,
|
|
4
5
|
type CeilingEvent,
|
|
5
6
|
type CeilingNode,
|
|
6
7
|
type GridEvent,
|
|
@@ -11,7 +12,11 @@ import {
|
|
|
11
12
|
type WallEvent,
|
|
12
13
|
type WallNode,
|
|
13
14
|
} from '@pascal-app/core'
|
|
15
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
14
16
|
import { BufferGeometry, Mesh, MeshBasicMaterial, type Object3D, Vector3 } from 'three'
|
|
17
|
+
import { GROUP_MOVE_DRAG_LABEL } from '../../../lib/contextual-help'
|
|
18
|
+
import useEditor from '../../../store/use-editor'
|
|
19
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
15
20
|
import {
|
|
16
21
|
ceilingStrategy,
|
|
17
22
|
faceHostStrategy,
|
|
@@ -498,6 +503,64 @@ describe('floorStrategy.move', () => {
|
|
|
498
503
|
expect(result.cursorPosition[1]).toBe(0)
|
|
499
504
|
expect(result.nodeUpdate?.position?.[1]).toBe(0)
|
|
500
505
|
})
|
|
506
|
+
|
|
507
|
+
describe('in grid mode', () => {
|
|
508
|
+
const originalSelection = useViewer.getState().selection
|
|
509
|
+
|
|
510
|
+
beforeEach(() => {
|
|
511
|
+
// The group-move scope resolves to the item snap context without the
|
|
512
|
+
// node registry; grid mode at a 0.5 m step.
|
|
513
|
+
useInteractionScope
|
|
514
|
+
.getState()
|
|
515
|
+
.begin({ kind: 'handle-drag', nodeId: 'item_test', handle: GROUP_MOVE_DRAG_LABEL })
|
|
516
|
+
useEditor.getState().setSnappingMode('item', 'grid')
|
|
517
|
+
useEditor.getState().setGridSnapStep(0.5)
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
afterEach(() => {
|
|
521
|
+
useInteractionScope.getState().end()
|
|
522
|
+
useEditor.getState().setSnappingMode('item', 'lines')
|
|
523
|
+
useViewer.setState({ selection: originalSelection } as never)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
// A 0.5 m plant centres half a cell off the lattice, its edges on it.
|
|
527
|
+
test("inside a turned building it lands on the building's lattice, square to the house", () => {
|
|
528
|
+
const yaw = (17 * Math.PI) / 180
|
|
529
|
+
const building = BuildingNode.parse({ position: [12.37, 0, -8.91], rotation: [0, yaw, 0] })
|
|
530
|
+
useScene.setState((state) => ({
|
|
531
|
+
...state,
|
|
532
|
+
nodes: { ...state.nodes, [building.id]: building },
|
|
533
|
+
}))
|
|
534
|
+
useViewer.setState({
|
|
535
|
+
selection: { buildingId: building.id, levelId: null, zoneId: null, selectedIds: [] },
|
|
536
|
+
} as never)
|
|
537
|
+
const local: [number, number] = [1.37, 2.61]
|
|
538
|
+
const cos = Math.cos(yaw)
|
|
539
|
+
const sin = Math.sin(yaw)
|
|
540
|
+
const event: GridEvent = {
|
|
541
|
+
position: [
|
|
542
|
+
12.37 + local[0] * cos + local[1] * sin,
|
|
543
|
+
0,
|
|
544
|
+
-8.91 - local[0] * sin + local[1] * cos,
|
|
545
|
+
],
|
|
546
|
+
localPosition: [local[0], 0, local[1]],
|
|
547
|
+
nativeEvent: {} as GridEvent['nativeEvent'],
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const result = floorStrategy.move(floorItemContext(), event)
|
|
551
|
+
expect(result?.nodeUpdate?.position?.[0]).toBeCloseTo(1.25, 9)
|
|
552
|
+
expect(result?.nodeUpdate?.position?.[2]).toBeCloseTo(2.75, 9)
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
test('outside any building it keeps the world grid', () => {
|
|
556
|
+
useViewer.setState({
|
|
557
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
558
|
+
} as never)
|
|
559
|
+
const result = floorStrategy.move(floorItemContext(), makeGridEvent(5.37, 0, -3.12))
|
|
560
|
+
expect(result?.nodeUpdate?.position?.[0]).toBeCloseTo(5.25, 9)
|
|
561
|
+
expect(result?.nodeUpdate?.position?.[2]).toBeCloseTo(-3.25, 9)
|
|
562
|
+
})
|
|
563
|
+
})
|
|
501
564
|
})
|
|
502
565
|
|
|
503
566
|
describe('ceilingStrategy.move', () => {
|
|
@@ -27,13 +27,12 @@ import {
|
|
|
27
27
|
resolveSurfacePlacement,
|
|
28
28
|
roofFacePointToSegment,
|
|
29
29
|
sceneRegistry,
|
|
30
|
-
snapLocalXZInWorld,
|
|
31
30
|
useScene,
|
|
32
31
|
wouldCreateHostingCycle,
|
|
33
32
|
} from '@pascal-app/core'
|
|
34
33
|
import { Euler, Quaternion, Vector3 } from 'three'
|
|
35
34
|
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
36
|
-
import {
|
|
35
|
+
import { snapPlanPointToActiveGrid } from '../../../lib/world-grid-snap'
|
|
37
36
|
import { itemEventToSurfaceHit, surfaceWorldNormalY } from '../shared/surface-hit'
|
|
38
37
|
import {
|
|
39
38
|
calculateItemRotation,
|
|
@@ -73,17 +72,14 @@ export const floorStrategy = {
|
|
|
73
72
|
const [dimX, , dimZ] = dims
|
|
74
73
|
const rotY = ctx.draftItem?.rotation?.[1] ?? 0
|
|
75
74
|
const swapDims = Math.abs(Math.sin(rotY)) > 0.9
|
|
76
|
-
// Snap on the
|
|
77
|
-
// item edges
|
|
78
|
-
//
|
|
79
|
-
// for storage. Without this, a rotated building drags placement off
|
|
80
|
-
// the world grid.
|
|
75
|
+
// Snap on the lattice the grid is drawn on: a turned building's own
|
|
76
|
+
// lattice inside it (item edges square to the house), the world grid
|
|
77
|
+
// outside any building.
|
|
81
78
|
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
82
79
|
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
83
80
|
const dimensions = [swapDims ? dimZ : dimX, swapDims ? dimX : dimZ] as const
|
|
84
|
-
const [x, z] =
|
|
81
|
+
const [x, z] = snapPlanPointToActiveGrid(
|
|
85
82
|
[event.localPosition[0], event.localPosition[2]],
|
|
86
|
-
getActiveBuildingPose(),
|
|
87
83
|
(value, axis) => snapToGrid(value, dimensions[axis]),
|
|
88
84
|
)
|
|
89
85
|
const y = ctx.gridPosition.y
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
type WallEvent,
|
|
31
31
|
} from '@pascal-app/core'
|
|
32
32
|
import { useViewer } from '@pascal-app/viewer'
|
|
33
|
-
import { Html } from '@react-three/drei'
|
|
34
33
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
35
34
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
36
35
|
import {
|
|
@@ -74,6 +73,7 @@ import useEditor, {
|
|
|
74
73
|
import useFacingPose from '../../../store/use-facing-pose'
|
|
75
74
|
import { getMovingNode } from '../../../store/use-interaction-scope'
|
|
76
75
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
76
|
+
import { Html } from '../../editor/localized-html'
|
|
77
77
|
import {
|
|
78
78
|
createItemSurfaceGridDispatch,
|
|
79
79
|
createItemSurfacePointerArbitration,
|
|
@@ -1008,9 +1008,9 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1008
1008
|
|
|
1009
1009
|
// Floor grab-offset: the item tracks the grabbed point instead of snapping
|
|
1010
1010
|
// its origin under the cursor. The offset is computed in building-local space
|
|
1011
|
-
// (`event.localPosition
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1011
|
+
// (`event.localPosition`, which `floorStrategy.move` snaps), and the corrected
|
|
1012
|
+
// local point is re-projected to a corrected world point so both frames carry
|
|
1013
|
+
// the offset and stay consistent.
|
|
1014
1014
|
const applyFloorGrabOffset = (event: GridEvent): GridEvent => {
|
|
1015
1015
|
if (relativeFloorStart === null) return event
|
|
1016
1016
|
const rawX = event.localPosition[0]
|
|
@@ -1871,6 +1871,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
1871
1871
|
position: floorPos,
|
|
1872
1872
|
rotation,
|
|
1873
1873
|
})
|
|
1874
|
+
// The surface moves published a building-space cursor (the host's top, level height
|
|
1875
|
+
// included) as the live transform. Back on the floor the item renders level-local, so
|
|
1876
|
+
// that stale height lifted it by its level's elevation (a bedside left on a 4th-floor bed
|
|
1877
|
+
// floated ~10 m up) until the next floor move: publish the floor pose now.
|
|
1878
|
+
useLiveTransforms.getState().set(draft.id, { position: floorPos, rotation: rotation[1] })
|
|
1874
1879
|
}
|
|
1875
1880
|
|
|
1876
1881
|
revalidate()
|
|
@@ -2717,13 +2722,31 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2717
2722
|
|
|
2718
2723
|
// ---- Subscribe ----
|
|
2719
2724
|
|
|
2725
|
+
// With a level selected, surfaces on other floors don't capture the draft: past the building
|
|
2726
|
+
// edge, through a window or over a stairwell the ray finds a lower floor's furniture, slab or
|
|
2727
|
+
// wall, and adopting it re-parented the item to that floor mid-gesture (and paid a level
|
|
2728
|
+
// change each time). The floor cursor keeps following the selected level instead.
|
|
2729
|
+
const onActiveLevel = (node: AnyNode | undefined) => {
|
|
2730
|
+
const levelId = useViewer.getState().selection.levelId
|
|
2731
|
+
if (!levelId || !node) return true
|
|
2732
|
+
return resolveLevelId(node, useScene.getState().nodes) === levelId
|
|
2733
|
+
}
|
|
2720
2734
|
const specializedKinds = new Set<string>()
|
|
2721
|
-
const
|
|
2735
|
+
const unsubscribeSpecialized: (() => void)[] = []
|
|
2736
|
+
const subscribeSpecialized = <E extends { node?: AnyNode }>(
|
|
2737
|
+
key: string,
|
|
2738
|
+
handler: (event: E) => void,
|
|
2739
|
+
) => {
|
|
2722
2740
|
specializedKinds.add(key.slice(0, key.lastIndexOf(':')))
|
|
2723
|
-
|
|
2741
|
+
const gated = (event: E) => {
|
|
2742
|
+
if (onActiveLevel(event.node)) handler(event)
|
|
2743
|
+
}
|
|
2744
|
+
emitter.on(key as never, gated as never)
|
|
2745
|
+
unsubscribeSpecialized.push(() => emitter.off(key as never, gated as never))
|
|
2724
2746
|
}
|
|
2725
2747
|
const genericRoute = (phase: 'enter' | 'move' | 'click' | 'leave') => (event: NodeEvent) => {
|
|
2726
2748
|
if (specializedKinds.has(event.node.type)) return
|
|
2749
|
+
if (!onActiveLevel(event.node)) return
|
|
2727
2750
|
if (
|
|
2728
2751
|
!surfaceOwnership.allows(event.node.id, event.nativeEvent.nativeEvent ?? event.nativeEvent)
|
|
2729
2752
|
)
|
|
@@ -2820,38 +2843,11 @@ export function usePlacementCoordinator(config: PlacementCoordinatorConfig): Rea
|
|
|
2820
2843
|
gridDispatch.cancel()
|
|
2821
2844
|
emitter.off('grid:move', gridDispatch.schedule)
|
|
2822
2845
|
emitter.off('grid:click', onGridClick)
|
|
2823
|
-
|
|
2824
|
-
emitter.off('item:move', onItemMove)
|
|
2825
|
-
emitter.off('item:leave', onItemLeave)
|
|
2826
|
-
emitter.off('item:click', onItemClick)
|
|
2827
|
-
emitter.off('wall:enter', onWallEnter)
|
|
2828
|
-
emitter.off('wall:move', onWallMove)
|
|
2829
|
-
emitter.off('wall:click', onWallClick)
|
|
2830
|
-
emitter.off('wall:leave', onWallLeave)
|
|
2831
|
-
emitter.off('roof:enter', onRoofWallEnter)
|
|
2832
|
-
emitter.off('roof:move', onRoofWallMove)
|
|
2833
|
-
emitter.off('roof:click', onRoofWallClick)
|
|
2834
|
-
emitter.off('roof:leave', onRoofWallLeave)
|
|
2846
|
+
for (const unsubscribe of unsubscribeSpecialized) unsubscribe()
|
|
2835
2847
|
emitter.off('node:enter', onNodeEnter)
|
|
2836
2848
|
emitter.off('node:move', onNodeMove)
|
|
2837
2849
|
emitter.off('node:click', onNodeClick)
|
|
2838
2850
|
emitter.off('node:leave', onNodeLeave)
|
|
2839
|
-
emitter.off('ceiling:enter', onCeilingEnter)
|
|
2840
|
-
emitter.off('ceiling:move', onCeilingMove)
|
|
2841
|
-
emitter.off('ceiling:click', onCeilingClick)
|
|
2842
|
-
emitter.off('ceiling:leave', onCeilingLeave)
|
|
2843
|
-
emitter.off('cabinet:enter', onItemEnter)
|
|
2844
|
-
emitter.off('procedural-item:enter' as never, onItemEnter)
|
|
2845
|
-
emitter.off('cabinet:move', onItemMove)
|
|
2846
|
-
emitter.off('procedural-item:move' as never, onItemMove)
|
|
2847
|
-
emitter.off('cabinet:click', onItemClick)
|
|
2848
|
-
emitter.off('procedural-item:click' as never, onItemClick)
|
|
2849
|
-
emitter.off('cabinet:leave', onItemLeave)
|
|
2850
|
-
emitter.off('procedural-item:leave' as never, onItemLeave)
|
|
2851
|
-
emitter.off('shelf:enter', onShelfEnter)
|
|
2852
|
-
emitter.off('shelf:move', onShelfMove)
|
|
2853
|
-
emitter.off('shelf:click', onShelfClick)
|
|
2854
|
-
emitter.off('shelf:leave', onShelfLeave)
|
|
2855
2851
|
emitter.off('node:click', commitFloorOnSurfaceClick as never)
|
|
2856
2852
|
emitter.off('tool:cancel', onCancel)
|
|
2857
2853
|
window.removeEventListener('keydown', onKeyDown)
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from '@pascal-app/core/procedural-items'
|
|
23
23
|
import { Group, Vector3 } from 'three'
|
|
24
24
|
import { commitFreshPlacementSubtree } from '../../../lib/fresh-planar-placement'
|
|
25
|
+
import useEditor from '../../../store/use-editor'
|
|
25
26
|
import { registerHostingTestNode } from '../__fixtures__/hosting'
|
|
26
27
|
import {
|
|
27
28
|
createItemSurfaceGridDispatch,
|
|
@@ -62,6 +63,7 @@ const host = ItemNode.parse({
|
|
|
62
63
|
let restoreRegistry: () => void
|
|
63
64
|
let oldNodes: ReturnType<typeof useScene.getState>['nodes']
|
|
64
65
|
let oldRoots: AnyNodeId[]
|
|
66
|
+
let oldSnapping: ReturnType<typeof useEditor.getState>['snappingModeByContext']
|
|
65
67
|
|
|
66
68
|
beforeEach(() => {
|
|
67
69
|
globalThis.requestAnimationFrame = (callback) => {
|
|
@@ -69,6 +71,14 @@ beforeEach(() => {
|
|
|
69
71
|
return 0
|
|
70
72
|
}
|
|
71
73
|
globalThis.cancelAnimationFrame = () => {}
|
|
74
|
+
// Poses are asserted raw: whatever context a previous suite left active
|
|
75
|
+
// (a tool, a scope, its snapping mode) must not snap them to the grid.
|
|
76
|
+
oldSnapping = useEditor.getState().snappingModeByContext
|
|
77
|
+
useEditor.setState({
|
|
78
|
+
snappingModeByContext: Object.fromEntries(
|
|
79
|
+
Object.keys(oldSnapping).map((context) => [context, 'off']),
|
|
80
|
+
) as typeof oldSnapping,
|
|
81
|
+
})
|
|
72
82
|
restoreRegistry = nodeRegistry._snapshot()
|
|
73
83
|
oldNodes = useScene.getState().nodes
|
|
74
84
|
oldRoots = useScene.getState().rootNodeIds
|
|
@@ -119,6 +129,7 @@ afterEach(() => {
|
|
|
119
129
|
globalThis.requestAnimationFrame = originalRaf
|
|
120
130
|
globalThis.cancelAnimationFrame = originalCancelRaf
|
|
121
131
|
restoreRegistry()
|
|
132
|
+
useEditor.setState({ snappingModeByContext: oldSnapping })
|
|
122
133
|
useLiveTransforms.getState().clear(original.id)
|
|
123
134
|
for (const id of ['level_surface-move', host.id, 'item_second-host'])
|
|
124
135
|
sceneRegistry.nodes.delete(id)
|