@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
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
DIMENSION_FLOOR_LIFT,
|
|
12
12
|
mezzanineEdgeDimensions,
|
|
13
13
|
type RoomDimension,
|
|
14
|
+
type WallDimensionFaces,
|
|
14
15
|
wallPushDimensionRooms,
|
|
15
16
|
wallPushDimensions,
|
|
17
|
+
wallSelectionDimensions,
|
|
16
18
|
} from './room-push-dimensions'
|
|
17
19
|
|
|
18
20
|
// Rooms are real: the walls go through core's room topology, so the faces the
|
|
@@ -66,6 +68,20 @@ function measure(
|
|
|
66
68
|
})
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
function selection({ nodes, walls }: Scene, wall: WallNode, faces: WallDimensionFaces) {
|
|
72
|
+
const index = createRoomTopologyIndex()
|
|
73
|
+
index.rebuildLevel(LEVEL, nodes)
|
|
74
|
+
const rooms = index.getLevelTopology(LEVEL)?.rooms ?? []
|
|
75
|
+
return wallSelectionDimensions({
|
|
76
|
+
rooms: wallPushDimensionRooms(rooms, { wallId: wall.id, t0: 0, t1: 1 }, () => 0),
|
|
77
|
+
wall,
|
|
78
|
+
walls,
|
|
79
|
+
faces,
|
|
80
|
+
resolve: (id) => nodes[id],
|
|
81
|
+
baseElevation: 0,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
69
85
|
const distances = (dimensions: RoomDimension[]) =>
|
|
70
86
|
dimensions.map((dimension) => Number(dimension.distance.toFixed(4)))
|
|
71
87
|
|
|
@@ -206,6 +222,27 @@ describe('two rooms sharing the moving wall', () => {
|
|
|
206
222
|
expect(west.key).not.toBe(east.key)
|
|
207
223
|
})
|
|
208
224
|
|
|
225
|
+
test('selected, a shared wall shows both room widths and no outside size, whatever the faces', () => {
|
|
226
|
+
for (const faces of ['both', 'interior', 'exterior'] as const) {
|
|
227
|
+
const dimensions = selection(rooms, middle, faces)
|
|
228
|
+
expect(dimensions.map((dimension) => dimension.faces)).toEqual(['interior', 'interior'])
|
|
229
|
+
expect(distances(dimensions).sort()).toEqual([3.8, 3.8])
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('selected, an exterior wall adds the outside size across the building', () => {
|
|
234
|
+
const east = wallAt(rooms, [8, 0], [8, 4])
|
|
235
|
+
const dimensions = selection(rooms, east, 'both')
|
|
236
|
+
const outside = dimensions.find((dimension) => dimension.faces === 'exterior')!
|
|
237
|
+
// Outer face to outer face, past the middle wall, clear of the room width's line.
|
|
238
|
+
expect(outside.distance).toBeCloseTo(8 + THICKNESS)
|
|
239
|
+
expect(outside.from[0]).toBeCloseTo(8 + THICKNESS / 2)
|
|
240
|
+
expect(outside.to[0]).toBeCloseTo(-THICKNESS / 2)
|
|
241
|
+
expect(distances(dimensions.filter((dimension) => dimension.faces === 'interior'))).toEqual([
|
|
242
|
+
3.8,
|
|
243
|
+
])
|
|
244
|
+
})
|
|
245
|
+
|
|
209
246
|
test('a push over part of the wall measures only the rooms along that part', () => {
|
|
210
247
|
const dimensions = measure(rooms, middle, [1, 0], 0, { t0: 0, t1: 0.5 })
|
|
211
248
|
expect(dimensions).toHaveLength(2)
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
getWallFaceOffsets,
|
|
5
5
|
isCurvedWall,
|
|
6
6
|
type Point,
|
|
7
|
+
type SeparatorNode,
|
|
7
8
|
type WallNode,
|
|
8
9
|
} from '@pascal-app/core'
|
|
9
10
|
|
|
@@ -153,12 +154,62 @@ export function wallPushDimensionRooms(
|
|
|
153
154
|
)
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The rooms along a pushed edge of invisible walls, as `wallPushDimensionRooms`
|
|
159
|
+
* reads them along a wall: each room's stretch of the edge's separators, put
|
|
160
|
+
* on the edge's one `line` (`[0, 1]` its length), faced as that line runs.
|
|
161
|
+
*/
|
|
162
|
+
export function separatorEdgeDimensionRooms(
|
|
163
|
+
rooms: ReadonlyArray<{ id: string; spans: readonly BoundarySpan[] }>,
|
|
164
|
+
edge: {
|
|
165
|
+
line: Pick<SeparatorNode, 'start' | 'end'>
|
|
166
|
+
separators: ReadonlyArray<{ separatorId: string; t0: number; t1: number }>
|
|
167
|
+
},
|
|
168
|
+
resolve: (id: string) => AnyNode | undefined,
|
|
169
|
+
elevationOf: (roomId: string) => number,
|
|
170
|
+
): PushDimensionRoom[] {
|
|
171
|
+
const { start, end } = edge.line
|
|
172
|
+
const dx = end[0] - start[0]
|
|
173
|
+
const dz = end[1] - start[1]
|
|
174
|
+
const lengthSq = dx * dx + dz * dz
|
|
175
|
+
if (lengthSq < 1e-12) return []
|
|
176
|
+
const members = new Set(edge.separators.map((member) => member.separatorId))
|
|
177
|
+
const along = ([x, z]: Point) => ((x - start[0]) * dx + (z - start[1]) * dz) / lengthSq
|
|
178
|
+
return rooms.flatMap((room) =>
|
|
179
|
+
room.spans.flatMap((own): PushDimensionRoom[] => {
|
|
180
|
+
const member = edge.separators.find((entry) => entry.separatorId === own.boundaryId)
|
|
181
|
+
const separator = resolve(own.boundaryId)
|
|
182
|
+
if (!member || separator?.type !== 'separator') return []
|
|
183
|
+
const t0 = Math.max(member.t0, own.t0)
|
|
184
|
+
const t1 = Math.min(member.t1, own.t1)
|
|
185
|
+
if (t1 - t0 < 1e-4) return []
|
|
186
|
+
const sx = separator.end[0] - separator.start[0]
|
|
187
|
+
const sz = separator.end[1] - separator.start[1]
|
|
188
|
+
const point = (t: number): Point => [separator.start[0] + sx * t, separator.start[1] + sz * t]
|
|
189
|
+
const [u0, u1] = [along(point(t0)), along(point(t1))]
|
|
190
|
+
// A separator drawn against the edge's line has its faces the other way round.
|
|
191
|
+
const same = sx * dx + sz * dz > 0
|
|
192
|
+
return [
|
|
193
|
+
{
|
|
194
|
+
id: room.id,
|
|
195
|
+
face: same ? own.face : own.face === 'a' ? 'b' : 'a',
|
|
196
|
+
t0: Math.min(u0, u1),
|
|
197
|
+
t1: Math.max(u0, u1),
|
|
198
|
+
others: room.spans.filter((other) => !members.has(other.boundaryId)),
|
|
199
|
+
elevation: elevationOf(room.id),
|
|
200
|
+
},
|
|
201
|
+
]
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
156
206
|
/**
|
|
157
207
|
* The live dimensions of a wall push: for every room along the pushed stretch,
|
|
158
208
|
* the moving face (the resting face moved `distance` along `outward` — a push
|
|
159
209
|
* is a pure translation) to each facing parallel boundary of that room, read
|
|
160
210
|
* through `resolve` so the preview's geometry counts. Per side of the wall:
|
|
161
|
-
* nearest first, one per distinct distance, at most three.
|
|
211
|
+
* nearest first, one per distinct distance, at most three. An edge of
|
|
212
|
+
* invisible walls is measured as its one line.
|
|
162
213
|
*/
|
|
163
214
|
export function wallPushDimensions({
|
|
164
215
|
rooms,
|
|
@@ -168,7 +219,7 @@ export function wallPushDimensions({
|
|
|
168
219
|
resolve,
|
|
169
220
|
}: {
|
|
170
221
|
rooms: readonly PushDimensionRoom[]
|
|
171
|
-
wall: WallNode
|
|
222
|
+
wall: WallNode | SeparatorNode
|
|
172
223
|
outward: Point
|
|
173
224
|
distance: number
|
|
174
225
|
resolve: (id: string) => AnyNode | undefined
|
|
@@ -195,6 +246,179 @@ export function wallPushDimensions({
|
|
|
195
246
|
)
|
|
196
247
|
}
|
|
197
248
|
|
|
249
|
+
/** Whether two dimensions measure the same span, either way round. */
|
|
250
|
+
export function sameRoomDimension(a: RoomDimension, b: RoomDimension): boolean {
|
|
251
|
+
const near = (p: Point, q: Point) => Math.hypot(p[0] - q[0], p[1] - q[1]) < 1e-3
|
|
252
|
+
return (near(a.from, b.from) && near(a.to, b.to)) || (near(a.from, b.to) && near(a.to, b.from))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** A selected wall's temporary dimension: a room width (ID) or the outside size (OD). */
|
|
256
|
+
export type WallSelectionDimension = RoomDimension & { faces: 'interior' | 'exterior' }
|
|
257
|
+
|
|
258
|
+
/** A selection's temporary dimension with the node it runs from: typing it moves that node. */
|
|
259
|
+
export type OwnedSelectionDimension = WallSelectionDimension & { ownerId: string }
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Several selected nodes' temporary dimensions as one list, keys unique across
|
|
263
|
+
* owners. Two selected walls across one room both measure it: the width shows
|
|
264
|
+
* once, owned by the first.
|
|
265
|
+
*/
|
|
266
|
+
export function mergeSelectionDimensions(
|
|
267
|
+
owners: ReadonlyArray<{ ownerId: string; dimensions: readonly WallSelectionDimension[] }>,
|
|
268
|
+
): OwnedSelectionDimension[] {
|
|
269
|
+
const kept: OwnedSelectionDimension[] = []
|
|
270
|
+
for (const { ownerId, dimensions } of owners)
|
|
271
|
+
for (const dimension of dimensions)
|
|
272
|
+
if (!kept.some((other) => sameRoomDimension(other, dimension)))
|
|
273
|
+
kept.push({ ...dimension, key: `${ownerId}:${dimension.key}`, ownerId })
|
|
274
|
+
return kept
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Which faces a selected wall's temporary dimensions measure to. */
|
|
278
|
+
export type WallDimensionFaces = 'interior' | 'exterior' | 'both'
|
|
279
|
+
|
|
280
|
+
const CONNECTION_TOLERANCE = 0.03
|
|
281
|
+
/** Where along the shared stretch the outside size is drawn, clear of the room widths at its middle. */
|
|
282
|
+
const EXTERIOR_DIMENSION_AT = 0.25
|
|
283
|
+
|
|
284
|
+
function pointSegmentDistance(p: Point, a: Point, b: Point): number {
|
|
285
|
+
const dx = b[0] - a[0]
|
|
286
|
+
const dz = b[1] - a[1]
|
|
287
|
+
const lengthSq = dx * dx + dz * dz
|
|
288
|
+
const t =
|
|
289
|
+
lengthSq < 1e-12
|
|
290
|
+
? 0
|
|
291
|
+
: Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dz) / lengthSq))
|
|
292
|
+
return Math.hypot(p[0] - (a[0] + dx * t), p[1] - (a[1] + dz * t))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function wallsTouch(left: WallNode, right: WallNode): boolean {
|
|
296
|
+
return (
|
|
297
|
+
[left.start, left.end].some(
|
|
298
|
+
(p) => pointSegmentDistance(p, right.start, right.end) <= CONNECTION_TOLERANCE,
|
|
299
|
+
) ||
|
|
300
|
+
[right.start, right.end].some(
|
|
301
|
+
(p) => pointSegmentDistance(p, left.start, left.end) <= CONNECTION_TOLERANCE,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The walls joined to `wall` through shared corners and T junctions, itself included. */
|
|
307
|
+
function connectedWalls(wall: WallNode, walls: readonly WallNode[]): WallNode[] {
|
|
308
|
+
const found = new Map<string, WallNode>([[wall.id, wall]])
|
|
309
|
+
const queue = [wall]
|
|
310
|
+
for (let current = queue.pop(); current; current = queue.pop()) {
|
|
311
|
+
for (const candidate of walls) {
|
|
312
|
+
if (found.has(candidate.id) || !wallsTouch(current, candidate)) continue
|
|
313
|
+
found.set(candidate.id, candidate)
|
|
314
|
+
queue.push(candidate)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return [...found.values()]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* The outside size across the building from a wall's exterior face: to the
|
|
322
|
+
* far face of the farthest parallel wall of the same connected building that
|
|
323
|
+
* shares some of its length — the opposite exterior wall. Null when nothing
|
|
324
|
+
* is across (a lone wall, a curved one).
|
|
325
|
+
*/
|
|
326
|
+
function overallExteriorDimension(
|
|
327
|
+
wall: WallNode,
|
|
328
|
+
exteriorFace: 'a' | 'b',
|
|
329
|
+
walls: readonly WallNode[],
|
|
330
|
+
): Measured | null {
|
|
331
|
+
const own = boundaryFace(wall, 0, 1, exteriorFace)
|
|
332
|
+
if (!own) return null
|
|
333
|
+
const dx = own.end[0] - own.start[0]
|
|
334
|
+
const dz = own.end[1] - own.start[1]
|
|
335
|
+
const length = Math.hypot(dx, dz)
|
|
336
|
+
const dir: Point = [dx / length, dz / length]
|
|
337
|
+
// The face's room-ward normal points out of the building here: across is its opposite.
|
|
338
|
+
const across: Point = [-own.inward[0], -own.inward[1]]
|
|
339
|
+
const along = (p: Point) => (p[0] - own.start[0]) * dir[0] + (p[1] - own.start[1]) * dir[1]
|
|
340
|
+
let best: { distance: number; lo: number; hi: number } | null = null
|
|
341
|
+
for (const other of connectedWalls(wall, walls)) {
|
|
342
|
+
if (other.id === wall.id) continue
|
|
343
|
+
for (const face of ['a', 'b'] as const) {
|
|
344
|
+
const candidate = boundaryFace(other, 0, 1, face)
|
|
345
|
+
if (!candidate) continue
|
|
346
|
+
const fx = candidate.end[0] - candidate.start[0]
|
|
347
|
+
const fz = candidate.end[1] - candidate.start[1]
|
|
348
|
+
const faceLength = Math.hypot(fx, fz)
|
|
349
|
+
if (Math.abs(dir[0] * fz - dir[1] * fx) / faceLength > PARALLEL_SIN) continue
|
|
350
|
+
const distance =
|
|
351
|
+
(candidate.start[0] - own.start[0]) * across[0] +
|
|
352
|
+
(candidate.start[1] - own.start[1]) * across[1]
|
|
353
|
+
if (distance <= MIN_GAP) continue
|
|
354
|
+
const a = along(candidate.start)
|
|
355
|
+
const b = along(candidate.end)
|
|
356
|
+
const lo = Math.max(0, Math.min(a, b))
|
|
357
|
+
const hi = Math.min(length, Math.max(a, b))
|
|
358
|
+
if (hi - lo < MIN_OVERLAP) continue
|
|
359
|
+
if (!best || distance > best.distance + SAME_DISTANCE) best = { distance, lo, hi }
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (!best) return null
|
|
363
|
+
const at = (u: number): Point => [own.start[0] + dir[0] * u, own.start[1] + dir[1] * u]
|
|
364
|
+
const [lo, hi] = [at(best.lo), at(best.hi)]
|
|
365
|
+
// Measured from the same end of the stretch whichever way the wall was drawn,
|
|
366
|
+
// so the walls on either side of the building put their outside size in one place.
|
|
367
|
+
const fromLo = Math.abs(lo[0] - hi[0]) > 1e-6 ? lo[0] < hi[0] : lo[1] <= hi[1]
|
|
368
|
+
const offset = (best.hi - best.lo) * EXTERIOR_DIMENSION_AT
|
|
369
|
+
const from = at(fromLo ? best.lo + offset : best.hi - offset)
|
|
370
|
+
return {
|
|
371
|
+
from,
|
|
372
|
+
to: [from[0] + across[0] * best.distance, from[1] + across[1] * best.distance],
|
|
373
|
+
distance: best.distance,
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* The temporary dimensions of a selected wall at rest, in 2D and 3D: the room
|
|
379
|
+
* widths a push of it shows (its face to each facing wall, both sides — ID),
|
|
380
|
+
* and for an exterior wall the outside size across the building (OD). `faces`
|
|
381
|
+
* picks which; a wall with no outside (rooms on both faces) shows its room
|
|
382
|
+
* widths whatever the choice. Each dimension runs `from` the selected wall,
|
|
383
|
+
* so typing a value moves that wall (`commitDimensionValue`).
|
|
384
|
+
*/
|
|
385
|
+
export function wallSelectionDimensions({
|
|
386
|
+
rooms,
|
|
387
|
+
wall,
|
|
388
|
+
walls,
|
|
389
|
+
faces,
|
|
390
|
+
resolve,
|
|
391
|
+
baseElevation,
|
|
392
|
+
}: {
|
|
393
|
+
rooms: readonly PushDimensionRoom[]
|
|
394
|
+
wall: WallNode
|
|
395
|
+
/** The level's walls, for the opposite exterior wall. */
|
|
396
|
+
walls: readonly WallNode[]
|
|
397
|
+
faces: WallDimensionFaces
|
|
398
|
+
resolve: (id: string) => AnyNode | undefined
|
|
399
|
+
/** The wall's base, for a dimension with no room floor to stand on. */
|
|
400
|
+
baseElevation: number
|
|
401
|
+
}): WallSelectionDimension[] {
|
|
402
|
+
const interior = wallPushDimensions({ rooms, wall, outward: [0, 0], distance: 0, resolve }).map(
|
|
403
|
+
(dimension): WallSelectionDimension => ({ ...dimension, faces: 'interior' }),
|
|
404
|
+
)
|
|
405
|
+
const roomFaces = new Set(rooms.map((room) => room.face))
|
|
406
|
+
const exteriorFace = roomFaces.size === 1 ? (roomFaces.has('a') ? 'b' : 'a') : null
|
|
407
|
+
if (!exteriorFace || faces === 'interior') return interior
|
|
408
|
+
const overall = overallExteriorDimension(wall, exteriorFace, walls)
|
|
409
|
+
const exterior: WallSelectionDimension[] = overall
|
|
410
|
+
? [
|
|
411
|
+
{
|
|
412
|
+
...overall,
|
|
413
|
+
key: 'exterior',
|
|
414
|
+
faces: 'exterior',
|
|
415
|
+
elevation: (rooms[0]?.elevation ?? baseElevation) + DIMENSION_FLOOR_LIFT,
|
|
416
|
+
},
|
|
417
|
+
]
|
|
418
|
+
: []
|
|
419
|
+
return faces === 'exterior' ? exterior : [...interior, ...exterior]
|
|
420
|
+
}
|
|
421
|
+
|
|
198
422
|
/**
|
|
199
423
|
* The live dimensions of a mezzanine edge push: the resting edge moved
|
|
200
424
|
* `distance` along `outward`, to each facing parallel edge of the outline the
|
|
@@ -868,17 +868,34 @@ describe('room drill-down state through the mounted selection manager', () => {
|
|
|
868
868
|
expect(useViewer.getState().selection.selectedIds).toEqual(['wall_free'])
|
|
869
869
|
})
|
|
870
870
|
})
|
|
871
|
-
test('the 2D plan
|
|
871
|
+
test('the 2D plan gives a wall hit to the wall and a room interior to the room', async () => {
|
|
872
872
|
await withRooms(async ({ left, right }) => {
|
|
873
|
-
|
|
873
|
+
// Without the kind's `planHitSelectsNode`, a band hit stays room-first.
|
|
874
874
|
expect(resolvePlanRoomHit('wall_shared', [3.95, 2])).toEqual(left)
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
const
|
|
875
|
+
// The wall definition declares it (its floor-plan extension).
|
|
876
|
+
const get = nodeRegistry.get.bind(nodeRegistry)
|
|
877
|
+
const spy = spyOn(nodeRegistry, 'get').mockImplementation(((kind: string) =>
|
|
878
|
+
kind === 'wall'
|
|
879
|
+
? ({
|
|
880
|
+
...get(kind),
|
|
881
|
+
extensions: { 'pascal:editor/floorplan': { planHitSelectsNode: true } },
|
|
882
|
+
} as AnyNodeDefinition)
|
|
883
|
+
: get(kind)) as typeof nodeRegistry.get)
|
|
884
|
+
try {
|
|
885
|
+
// Either half of a shared wall's band is the wall, never the room beside it.
|
|
886
|
+
expect(resolvePlanRoomHit('wall_west', [-0.05, 2])).toBeNull()
|
|
887
|
+
expect(resolvePlanRoomHit('wall_shared', [3.95, 2])).toBeNull()
|
|
888
|
+
expect(resolvePlanRoomHit('wall_shared', [4.05, 2])).toBeNull()
|
|
889
|
+
expect(resolvePlanRoomHit('wall_free', [9.95, 1])).toBeNull()
|
|
890
|
+
} finally {
|
|
891
|
+
spy.mockRestore()
|
|
892
|
+
}
|
|
893
|
+
expect(resolvePlanRoomHit(null, [2, 2])).toEqual(left)
|
|
894
|
+
expect(resolvePlanRoomHit(null, [6, 2])).toEqual(right)
|
|
878
895
|
const select = async (modifiers = plain) => {
|
|
879
896
|
let selected = false
|
|
880
897
|
await act(async () => {
|
|
881
|
-
selected = selectRoomFromHit(
|
|
898
|
+
selected = selectRoomFromHit(resolvePlanRoomHit(null, [2, 2]), modifiers, null)
|
|
882
899
|
})
|
|
883
900
|
return selected
|
|
884
901
|
}
|
|
@@ -744,58 +744,79 @@ describe('scene clipboard', () => {
|
|
|
744
744
|
}
|
|
745
745
|
})
|
|
746
746
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
747
|
+
const floorScript = {
|
|
748
|
+
kind: 'script',
|
|
749
|
+
script: 'e'.repeat(64),
|
|
750
|
+
artifact: 'f'.repeat(64),
|
|
751
|
+
inputs: 'key',
|
|
752
|
+
manifest: { bounds: { min: [0, 0, 0], max: [3, 0.01, 3] }, triangles: 2 },
|
|
753
|
+
} as const
|
|
754
|
+
const square: [number, number][] = [
|
|
755
|
+
[0, 0],
|
|
756
|
+
[3, 0],
|
|
757
|
+
[3, 3],
|
|
758
|
+
[0, 3],
|
|
759
|
+
]
|
|
760
|
+
for (const [what, host] of [
|
|
761
|
+
[
|
|
762
|
+
"a room copied to another project brings its floor script's artifacts",
|
|
763
|
+
() =>
|
|
764
|
+
ZoneNode.parse({
|
|
765
|
+
id: 'zone_clipboard-floor-script',
|
|
766
|
+
parentId: sourceLevelId,
|
|
767
|
+
name: 'Hall',
|
|
768
|
+
polygon: square,
|
|
769
|
+
floor: { script: floorScript },
|
|
770
|
+
}),
|
|
771
|
+
],
|
|
772
|
+
[
|
|
773
|
+
"a slab copied to another project brings its surface script's artifacts",
|
|
774
|
+
() =>
|
|
775
|
+
SlabNode.parse({
|
|
776
|
+
id: 'slab_clipboard-surface-script',
|
|
777
|
+
parentId: sourceLevelId,
|
|
778
|
+
name: 'Terrace',
|
|
779
|
+
polygon: square,
|
|
780
|
+
surfaceScript: floorScript,
|
|
781
|
+
}),
|
|
782
|
+
],
|
|
783
|
+
] as const) {
|
|
784
|
+
test(what, async () => {
|
|
785
|
+
let systemClipboardText = ''
|
|
786
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
787
|
+
configurable: true,
|
|
788
|
+
value: {
|
|
789
|
+
readText: async () => systemClipboardText,
|
|
790
|
+
writeText: async (text: string) => {
|
|
791
|
+
systemClipboardText = text
|
|
792
|
+
},
|
|
755
793
|
},
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
[0, 0],
|
|
764
|
-
[3, 0],
|
|
765
|
-
[3, 3],
|
|
766
|
-
[0, 3],
|
|
767
|
-
],
|
|
768
|
-
floor: {
|
|
769
|
-
script: {
|
|
770
|
-
kind: 'script',
|
|
771
|
-
script: 'e'.repeat(64),
|
|
772
|
-
artifact: 'f'.repeat(64),
|
|
773
|
-
inputs: 'key',
|
|
774
|
-
manifest: { bounds: { min: [0, 0, 0], max: [3, 0.01, 3] }, triangles: 2 },
|
|
794
|
+
})
|
|
795
|
+
const node = host()
|
|
796
|
+
useScene.setState((state) => ({
|
|
797
|
+
nodes: {
|
|
798
|
+
...state.nodes,
|
|
799
|
+
[sourceLevelId]: makeLevel(sourceLevelId, [node.id]),
|
|
800
|
+
[node.id]: node,
|
|
775
801
|
},
|
|
776
|
-
}
|
|
802
|
+
}))
|
|
803
|
+
useViewer.getState().setProjectId('project_clipboard-source')
|
|
804
|
+
expect(copySelectedNodesToEditorClipboard([node.id])).toBe(true)
|
|
805
|
+
useViewer.getState().setProjectId('project_clipboard-target')
|
|
806
|
+
const copyFrom = mock(async (_projectId: string, _sha256s: string[]) => [] as string[])
|
|
807
|
+
const previousStore = getArtifactStore()
|
|
808
|
+
configureArtifactStore({ ...previousStore, copyFrom })
|
|
809
|
+
try {
|
|
810
|
+
await pasteSystemEditorClipboardToLevel(targetLevelId)
|
|
811
|
+
expect(new Set(copyFrom.mock.calls[0]![1])).toEqual(
|
|
812
|
+
new Set(['e'.repeat(64), 'f'.repeat(64)]),
|
|
813
|
+
)
|
|
814
|
+
} finally {
|
|
815
|
+
configureArtifactStore(previousStore)
|
|
816
|
+
useViewer.getState().setProjectId(null)
|
|
817
|
+
}
|
|
777
818
|
})
|
|
778
|
-
|
|
779
|
-
nodes: {
|
|
780
|
-
...state.nodes,
|
|
781
|
-
[sourceLevelId]: makeLevel(sourceLevelId, [room.id]),
|
|
782
|
-
[room.id]: room,
|
|
783
|
-
},
|
|
784
|
-
}))
|
|
785
|
-
useViewer.getState().setProjectId('project_clipboard-source')
|
|
786
|
-
expect(copySelectedNodesToEditorClipboard([room.id])).toBe(true)
|
|
787
|
-
useViewer.getState().setProjectId('project_clipboard-target')
|
|
788
|
-
const copyFrom = mock(async (_projectId: string, _sha256s: string[]) => [] as string[])
|
|
789
|
-
const previousStore = getArtifactStore()
|
|
790
|
-
configureArtifactStore({ ...previousStore, copyFrom })
|
|
791
|
-
try {
|
|
792
|
-
await pasteSystemEditorClipboardToLevel(targetLevelId)
|
|
793
|
-
expect(new Set(copyFrom.mock.calls[0]![1])).toEqual(new Set(['e'.repeat(64), 'f'.repeat(64)]))
|
|
794
|
-
} finally {
|
|
795
|
-
configureArtifactStore(previousStore)
|
|
796
|
-
useViewer.getState().setProjectId(null)
|
|
797
|
-
}
|
|
798
|
-
})
|
|
819
|
+
}
|
|
799
820
|
|
|
800
821
|
test('navigation during artifact copy does not paste into the next project', async () => {
|
|
801
822
|
copyScriptedWindow()
|
|
@@ -488,14 +488,19 @@ export async function pasteSystemEditorClipboardToLevel(
|
|
|
488
488
|
return applyClipboardPayloadToLevel(payload, targetLevel.id, refused)
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
/**
|
|
491
|
+
/**
|
|
492
|
+
* The script and GLB a scripted node, a room's floor script, a slab's surface script or a wall's
|
|
493
|
+
* face scripts reference.
|
|
494
|
+
*/
|
|
492
495
|
function artifactHashes(node: AnyNode): string[] {
|
|
493
496
|
const sources =
|
|
494
497
|
node.type === 'zone'
|
|
495
498
|
? [node.floor?.script]
|
|
496
|
-
: node.type === '
|
|
497
|
-
? [node.
|
|
498
|
-
:
|
|
499
|
+
: node.type === 'slab'
|
|
500
|
+
? [node.surfaceScript]
|
|
501
|
+
: node.type === 'wall'
|
|
502
|
+
? [node.finishScripts?.a, node.finishScripts?.b]
|
|
503
|
+
: ['source' in node ? node.source : undefined]
|
|
499
504
|
return sources.flatMap((source) =>
|
|
500
505
|
typeof source === 'object' && source?.kind === 'script' ? [source.script, source.artifact] : [],
|
|
501
506
|
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tab steps the selection through everything stacked under the pointer in the
|
|
3
|
+
* floor plan, top first: where walls
|
|
4
|
+
* overlap, or a short wall lies inside a longer one or under a marker, a click
|
|
5
|
+
* takes the top one and Tab the ones below it in turn. Shift+Tab steps back.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AnyNodeId } from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
|
|
11
|
+
let pointer: { x: number; y: number } | null = null
|
|
12
|
+
let tracking = false
|
|
13
|
+
|
|
14
|
+
/** Start following the pointer, so a Tab knows where it is. Idempotent. */
|
|
15
|
+
export function trackSelectionCyclePointer(): void {
|
|
16
|
+
if (tracking || typeof window === 'undefined') return
|
|
17
|
+
tracking = true
|
|
18
|
+
const follow = (event: PointerEvent) => {
|
|
19
|
+
pointer = { x: event.clientX, y: event.clientY }
|
|
20
|
+
}
|
|
21
|
+
window.addEventListener('pointermove', follow, { capture: true, passive: true })
|
|
22
|
+
window.addEventListener('pointerdown', follow, { capture: true, passive: true })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The plan nodes under a screen point, topmost first, each once. */
|
|
26
|
+
export function floorplanNodesAt(x: number, y: number): string[] {
|
|
27
|
+
if (typeof document === 'undefined') return []
|
|
28
|
+
const ids: string[] = []
|
|
29
|
+
for (const element of document.elementsFromPoint(x, y)) {
|
|
30
|
+
if (!element.closest('[data-floorplan-scene]')) continue
|
|
31
|
+
const entry = element.closest<SVGGElement>('.floorplan-registry-entry[data-node-id]')
|
|
32
|
+
const id = entry?.dataset.nodeId
|
|
33
|
+
if (id && !ids.includes(id)) ids.push(id)
|
|
34
|
+
}
|
|
35
|
+
return ids
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The id a Tab selects among `stack` (topmost first): the one after the
|
|
40
|
+
* current selection, or before it for Shift+Tab, wrapping round; the top one
|
|
41
|
+
* when the selection is not in the stack.
|
|
42
|
+
*/
|
|
43
|
+
export function nextInStack(
|
|
44
|
+
stack: readonly string[],
|
|
45
|
+
selected: readonly string[],
|
|
46
|
+
back: boolean,
|
|
47
|
+
): string | null {
|
|
48
|
+
if (stack.length === 0) return null
|
|
49
|
+
const index = selected.length === 1 ? stack.indexOf(selected[0]!) : -1
|
|
50
|
+
if (index < 0) return stack[0]!
|
|
51
|
+
return stack[(index + (back ? stack.length - 1 : 1)) % stack.length]!
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Select the next object under the pointer in the floor plan. True when Tab was used. */
|
|
55
|
+
export function cycleSelectionAtPointer(back: boolean): boolean {
|
|
56
|
+
if (!pointer) return false
|
|
57
|
+
const stack = floorplanNodesAt(pointer.x, pointer.y)
|
|
58
|
+
const viewer = useViewer.getState()
|
|
59
|
+
const next = nextInStack(stack, viewer.selection.selectedIds, back)
|
|
60
|
+
if (!next) return false
|
|
61
|
+
viewer.setSelection({ selectedIds: [next as AnyNodeId] })
|
|
62
|
+
return true
|
|
63
|
+
}
|