@pascal-app/editor 1.0.4 → 1.0.6
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/README.md +106 -0
- package/package.json +40 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/bake-thumbnail.tsx +6 -15
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +81 -8
- package/src/components/editor/editor-layout-mobile.tsx +1 -1
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/export-manager.tsx +14 -19
- package/src/components/editor/first-person/build-collider-world.test.ts +269 -0
- package/src/components/editor/first-person/build-collider-world.ts +22 -5
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +89 -2
- package/src/components/editor/first-person/mechanism-targets.ts +52 -0
- package/src/components/editor/first-person-controls.tsx +19 -11
- package/src/components/editor/floor-aware-camera-controls.test.ts +24 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +72 -23
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/group-actions.ts +2 -0
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/index.tsx +166 -79
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-highlight.tsx +13 -5
- package/src/components/editor/room-labels.test.ts +621 -128
- package/src/components/editor/room-labels.tsx +1090 -190
- package/src/components/editor/selection-manager.tsx +38 -117
- package/src/components/editor/slab-hole-highlights.tsx +2 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +3 -2
- package/src/components/editor/snapshot-capture.test.ts +69 -58
- package/src/components/editor/snapshot-capture.ts +7 -1
- package/src/components/editor/thumbnail-generator.test.ts +350 -4
- package/src/components/editor/thumbnail-generator.tsx +128 -54
- package/src/components/editor/use-floorplan-background-placement.ts +3 -1
- package/src/components/editor/views/plugins-view.tsx +1 -8
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +140 -17
- package/src/components/editor/wall-snap-beacon-layer.tsx +5 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +3 -4
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +1 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +29 -7
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +38 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +3 -6
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +94 -94
- package/src/components/tools/item/use-placement-coordinator.tsx +55 -49
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +63 -29
- package/src/components/tools/tool-manager.tsx +14 -5
- package/src/components/tools/wall/wall-drafting.test.ts +170 -57
- package/src/components/ui/action-menu/action-button.tsx +6 -1
- package/src/components/ui/action-menu/camera-actions.tsx +7 -3
- package/src/components/ui/action-menu/control-modes.tsx +13 -7
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +11 -9
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +10 -3
- package/src/components/ui/command-palette/editor-commands.tsx +3 -12
- package/src/components/ui/command-palette/index.tsx +45 -15
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +150 -45
- package/src/components/ui/helpers/contextual-helper-panel.tsx +32 -7
- package/src/components/ui/helpers/helper-manager.test.tsx +54 -2
- package/src/components/ui/helpers/helper-manager.tsx +7 -1
- package/src/components/ui/helpers/item-helper.tsx +9 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +8 -8
- package/src/components/ui/panels/mobile-panel-sheet.tsx +2 -2
- package/src/components/ui/panels/mobile-selection-bar.tsx +6 -3
- package/src/components/ui/panels/multi-selection-panel.tsx +120 -3
- package/src/components/ui/panels/panel-wrapper.tsx +53 -27
- package/src/components/ui/panels/parametric-field-control.tsx +2 -2
- package/src/components/ui/panels/parametric-inspector.tsx +3 -3
- package/src/components/ui/panels/room-panel.tsx +65 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/sidebar.tsx +1 -1
- package/src/components/ui/right-stack.tsx +21 -2
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +3 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +107 -23
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +5 -12
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -10
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/tab-bar.tsx +4 -4
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-rooms.tsx +16 -0
- package/src/hooks/use-auto-save.integration.test.tsx +96 -38
- package/src/hooks/use-auto-save.ts +26 -1
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +65 -16
- package/src/hooks/use-keyboard.ts +29 -12
- package/src/hooks/use-placement-notice.test.ts +24 -1
- package/src/hooks/use-placement-notice.ts +37 -6
- package/src/index.tsx +71 -24
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.ts +9 -2
- package/src/lib/editor-views.ts +35 -1
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-foundation-support.test.ts +1 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +31 -16
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +39 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +4 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/sheet-export.test.ts +24 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +34 -45
- package/src/lib/floorplan/site-plan/coverage.ts +2 -4
- package/src/lib/floorplan/site-plan/front-edge.ts +2 -1
- package/src/lib/floorplan/site-plan/geometry.test.ts +9 -15
- package/src/lib/floorplan/site-plan/geometry.ts +1 -416
- package/src/lib/floorplan/site-plan/index.ts +0 -16
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +117 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +3 -2
- package/src/lib/floorplan/site-plan/site-parts.ts +14 -106
- package/src/lib/floorplan/site-plan/yards.test.ts +2 -1
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +112 -0
- package/src/lib/fresh-planar-placement.ts +57 -3
- package/src/lib/geometry-script/author.ts +6 -1
- package/src/lib/geometry-script/client.ts +8 -1
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +1 -0
- package/src/lib/glb-export.test.ts +187 -162
- package/src/lib/glb-export.ts +4 -1
- package/src/lib/history.test.ts +12 -7
- package/src/lib/interaction/scope.ts +4 -0
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/lot/clean-ring.test.ts +2 -1
- package/src/lib/lot/drop-in.ts +9 -24
- package/src/lib/lot/lot-patch.test.ts +1 -1
- package/src/lib/lot/lot-patch.ts +2 -2
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/orbit-floor-clearance.test.ts +36 -0
- package/src/lib/orbit-floor-clearance.ts +48 -0
- package/src/lib/paint-pass-through.test.ts +11 -0
- package/src/lib/paint-pass-through.ts +4 -2
- package/src/lib/paint-scope.ts +1 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plugin-chat.ts +15 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/room-assembly-overlay.ts +2 -3
- package/src/lib/room-selection.test.ts +45 -1
- package/src/lib/scene-clipboard.test.ts +246 -1
- package/src/lib/scene-clipboard.ts +44 -14
- package/src/lib/selection-routing.ts +1 -1
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.ts +2 -1
- package/src/lib/viewer-selection.ts +22 -11
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-stair-build-preview.ts +25 -5
- package/src/store/view-layout.test.ts +27 -1
- package/src/store/view-layout.ts +4 -1
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -15
- package/src/components/tools/stair/stair-tool.tsx +0 -682
|
@@ -129,114 +129,114 @@ test('grid moves throttle camera drags at 100 ms and resume immediately during t
|
|
|
129
129
|
}
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
test.each([
|
|
133
|
-
'
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
new MeshBasicMaterial({ side: DoubleSide }),
|
|
153
|
-
)
|
|
154
|
-
surface.position.y = 5
|
|
155
|
-
surface.updateMatrixWorld(true)
|
|
156
|
-
const surfaceId = `${kind}_grid_batch_test` as const
|
|
157
|
-
const wall = WallNode.parse({ start: [0, 0], end: [0, 2] })
|
|
158
|
-
const delivered: GridEvent[] = []
|
|
159
|
-
const onMove = (event: GridEvent) => delivered.push(event)
|
|
160
|
-
emitter.on('grid:move', onMove)
|
|
161
|
-
|
|
162
|
-
function Grid() {
|
|
163
|
-
useRegistry(surfaceId, kind, { current: surface })
|
|
164
|
-
useGridEvents(0)
|
|
165
|
-
return null
|
|
166
|
-
}
|
|
167
|
-
const send = () => {
|
|
168
|
-
const before = delivered.length
|
|
169
|
-
canvas.dispatchEvent(
|
|
170
|
-
Object.assign(new Event('pointermove'), { clientX: 55, clientY: 50, button: 0 }),
|
|
132
|
+
test.each(['slab', 'ceiling'] as const)(
|
|
133
|
+
'grid moves keep the same %s hit when its source joins or leaves a batch',
|
|
134
|
+
async (kind) => {
|
|
135
|
+
const previousViewer = useViewer.getState()
|
|
136
|
+
const previousScope = useInteractionScope.getState().scope
|
|
137
|
+
const restoreRegistry = nodeRegistry._snapshot()
|
|
138
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
139
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
140
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
141
|
+
const canvas = Object.assign(new EventTarget(), {
|
|
142
|
+
getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 100 }),
|
|
143
|
+
}) as unknown as HTMLCanvasElement
|
|
144
|
+
const root = createRoot(canvas)
|
|
145
|
+
const camera = new PerspectiveCamera(60, 1, 0.1, 100)
|
|
146
|
+
camera.position.set(0, kind === 'ceiling' ? 1 : 10, 10)
|
|
147
|
+
camera.lookAt(0, 5, 0)
|
|
148
|
+
camera.updateMatrixWorld()
|
|
149
|
+
const surface = new Mesh(
|
|
150
|
+
new PlaneGeometry(20, 20).rotateX(-Math.PI / 2),
|
|
151
|
+
new MeshBasicMaterial({ side: DoubleSide }),
|
|
171
152
|
)
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
153
|
+
surface.position.y = 5
|
|
154
|
+
surface.updateMatrixWorld(true)
|
|
155
|
+
const surfaceId = `${kind}_grid_batch_test` as const
|
|
156
|
+
const wall = WallNode.parse({ start: [0, 0], end: [0, 2] })
|
|
157
|
+
const delivered: GridEvent[] = []
|
|
158
|
+
const onMove = (event: GridEvent) => delivered.push(event)
|
|
159
|
+
emitter.on('grid:move', onMove)
|
|
175
160
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
161
|
+
function Grid() {
|
|
162
|
+
useRegistry(surfaceId, kind, { current: surface })
|
|
163
|
+
useGridEvents(0)
|
|
164
|
+
return null
|
|
165
|
+
}
|
|
166
|
+
const send = () => {
|
|
167
|
+
const before = delivered.length
|
|
168
|
+
canvas.dispatchEvent(
|
|
169
|
+
Object.assign(new Event('pointermove'), { clientX: 55, clientY: 50, button: 0 }),
|
|
170
|
+
)
|
|
171
|
+
expect(delivered).toHaveLength(before + 1)
|
|
172
|
+
return delivered.at(-1)!
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
nodeRegistry._reset()
|
|
177
|
+
registerNode({
|
|
178
|
+
kind,
|
|
179
|
+
schemaVersion: 1,
|
|
180
|
+
category: 'structure',
|
|
181
|
+
capabilities: {},
|
|
182
|
+
schema: kind === 'ceiling' ? CeilingNode : SlabNode,
|
|
183
|
+
defaults: () => ({}),
|
|
184
|
+
drafting: { surfaceQuery: true },
|
|
185
|
+
extensions: {
|
|
186
|
+
[DRAFTING_SURFACE_EXTENSION_KEY]: {
|
|
187
|
+
kind,
|
|
188
|
+
...(kind === 'ceiling' ? { raycast: 'underside' } : {}),
|
|
189
|
+
},
|
|
190
190
|
},
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
})
|
|
192
|
+
useViewer.setState({
|
|
193
|
+
cameraDragging: false,
|
|
194
|
+
selection: { buildingId: null, levelId: null, zoneId: null, selectedIds: [] },
|
|
195
|
+
})
|
|
196
|
+
await root.configure({
|
|
197
|
+
gl: {
|
|
198
|
+
domElement: canvas,
|
|
199
|
+
render() {},
|
|
200
|
+
setSize() {},
|
|
201
|
+
setPixelRatio() {},
|
|
202
|
+
xr: { isPresenting: false },
|
|
203
|
+
} as unknown as WebGLRenderer,
|
|
204
|
+
camera,
|
|
205
|
+
frameloop: 'never',
|
|
206
|
+
dpr: 1,
|
|
207
|
+
size: { width: 100, height: 100, top: 0, left: 0 },
|
|
208
|
+
})
|
|
209
|
+
for (const scope of [
|
|
210
|
+
{ kind: 'moving', node: wall, nodeId: wall.id, nodeType: 'wall', view: '3d' },
|
|
211
|
+
{ kind: 'drafting', tool: kind },
|
|
212
|
+
] as const) {
|
|
213
|
+
await act(async () => {
|
|
214
|
+
useInteractionScope.setState({ scope })
|
|
215
|
+
root.render(createElement(Grid))
|
|
216
|
+
})
|
|
217
|
+
const before = send()
|
|
218
|
+
expect(before.surfaceHit?.hostId).toBe(surfaceId)
|
|
219
|
+
expect(before.position[1]).toBeCloseTo(5)
|
|
220
|
+
hideFromScene(surface, 'batched')
|
|
221
|
+
const batched = send()
|
|
222
|
+
expect(batched.surfaceHit).toEqual(before.surfaceHit)
|
|
223
|
+
expect(batched.position).toEqual(before.position)
|
|
224
|
+
expect(batched.localPosition).toEqual(before.localPosition)
|
|
225
|
+
showInScene(surface, 'batched')
|
|
226
|
+
expect(send().position).toEqual(before.position)
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
214
229
|
await act(async () => {
|
|
215
|
-
|
|
216
|
-
root.render(createElement(Grid))
|
|
230
|
+
root.render(null)
|
|
217
231
|
})
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
showInScene(surface, 'batched')
|
|
227
|
-
expect(send().position).toEqual(before.position)
|
|
232
|
+
emitter.off('grid:move', onMove)
|
|
233
|
+
_roots.delete(canvas)
|
|
234
|
+
surface.geometry.dispose()
|
|
235
|
+
surface.material.dispose()
|
|
236
|
+
restoreRegistry()
|
|
237
|
+
useInteractionScope.setState({ scope: previousScope })
|
|
238
|
+
useViewer.setState(previousViewer)
|
|
239
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
228
240
|
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
root.render(null)
|
|
232
|
-
})
|
|
233
|
-
emitter.off('grid:move', onMove)
|
|
234
|
-
_roots.delete(canvas)
|
|
235
|
-
surface.geometry.dispose()
|
|
236
|
-
surface.material.dispose()
|
|
237
|
-
restoreRegistry()
|
|
238
|
-
useInteractionScope.setState({ scope: previousScope })
|
|
239
|
-
useViewer.setState(previousViewer)
|
|
240
|
-
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
241
|
-
}
|
|
242
|
-
})
|
|
241
|
+
},
|
|
242
|
+
)
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
blocksSnappingShortcut,
|
|
27
27
|
canCycleSnappingModeShortcut,
|
|
28
28
|
canRunGlobalRotationShortcut,
|
|
29
|
+
hasSceneKeyboardFocus,
|
|
29
30
|
isToolOwnedCanopyForm,
|
|
30
31
|
isToolOwnedRotation,
|
|
31
32
|
markToolCancelConsumed,
|
|
@@ -48,28 +49,76 @@ type RafFn = (callback: (time: number) => void) => number
|
|
|
48
49
|
const NODE_ID = 'block_history' as AnyNodeId
|
|
49
50
|
|
|
50
51
|
describe('snapping shortcuts while entering a run length', () => {
|
|
52
|
+
test('document panes keep copy, delete and editing shortcuts away from the scene until it is focused', () => {
|
|
53
|
+
useEditor.setState({ viewLayouts: {} })
|
|
54
|
+
expect(hasSceneKeyboardFocus()).toBe(true)
|
|
55
|
+
useEditor.getState().showView('document:plan', { beside: true })
|
|
56
|
+
expect(hasSceneKeyboardFocus()).toBe(false)
|
|
57
|
+
useEditor.getState().focusViewPane(0)
|
|
58
|
+
expect(hasSceneKeyboardFocus()).toBe(true)
|
|
59
|
+
useEditor
|
|
60
|
+
.getState()
|
|
61
|
+
.setViewLayout({ split: false, panes: ['document:plan', '3d'], ratio: 0.5, focus: 0 })
|
|
62
|
+
expect(hasSceneKeyboardFocus()).toBe(false)
|
|
63
|
+
useEditor.getState().closeView('document:plan')
|
|
64
|
+
})
|
|
51
65
|
test('allows snap mode and spacing shortcuts in the duct and pipe length field', () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
).toBe(false)
|
|
66
|
+
const field = {
|
|
67
|
+
tagName: 'INPUT',
|
|
68
|
+
isContentEditable: false,
|
|
69
|
+
hasAttribute: (name: string) => name === 'data-run-length-input',
|
|
70
|
+
closest: (): object => field,
|
|
71
|
+
}
|
|
72
|
+
expect(blocksSnappingShortcut(field)).toBe(false)
|
|
59
73
|
})
|
|
60
74
|
|
|
61
75
|
test('continues to protect ordinary text fields and editable content', () => {
|
|
62
|
-
for (const tagName of ['INPUT', 'TEXTAREA', 'DIV']) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
).toBe(true)
|
|
76
|
+
for (const tagName of ['INPUT', 'TEXTAREA', 'SELECT', 'DIV']) {
|
|
77
|
+
const field = {
|
|
78
|
+
tagName,
|
|
79
|
+
isContentEditable: tagName === 'DIV',
|
|
80
|
+
hasAttribute: () => false,
|
|
81
|
+
closest: (): object => field,
|
|
82
|
+
}
|
|
83
|
+
expect(blocksSnappingShortcut(field)).toBe(true)
|
|
70
84
|
}
|
|
71
85
|
expect(blocksSnappingShortcut(null)).toBe(false)
|
|
72
86
|
})
|
|
87
|
+
|
|
88
|
+
test('honors closest-owned fields and dialogs without claiming ordinary canvas keys', () => {
|
|
89
|
+
// Model the DOM closest result, not its selector spelling. Native descendant
|
|
90
|
+
// propagation is checked by the protected browser fixture.
|
|
91
|
+
for (const role of ['textbox', 'combobox', 'dialog', 'alertdialog']) {
|
|
92
|
+
const owner = { role }
|
|
93
|
+
const descendant = {
|
|
94
|
+
tagName: 'SPAN',
|
|
95
|
+
isContentEditable: false,
|
|
96
|
+
hasAttribute: () => false,
|
|
97
|
+
closest: () => owner,
|
|
98
|
+
}
|
|
99
|
+
expect(blocksSnappingShortcut(descendant)).toBe(true)
|
|
100
|
+
}
|
|
101
|
+
const canvas = {
|
|
102
|
+
tagName: 'CANVAS',
|
|
103
|
+
isContentEditable: false,
|
|
104
|
+
hasAttribute: () => false,
|
|
105
|
+
closest: () => null,
|
|
106
|
+
}
|
|
107
|
+
expect(blocksSnappingShortcut(canvas)).toBe(false)
|
|
108
|
+
expect(blocksSnappingShortcut({ ...canvas, tagName: 'SPAN', isContentEditable: true })).toBe(
|
|
109
|
+
true,
|
|
110
|
+
)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('reserves the run-length exception for its actual input, not another editable owner', () => {
|
|
114
|
+
const textarea = {
|
|
115
|
+
tagName: 'TEXTAREA',
|
|
116
|
+
isContentEditable: false,
|
|
117
|
+
hasAttribute: (name: string) => name === 'data-run-length-input',
|
|
118
|
+
closest: (): object => textarea,
|
|
119
|
+
}
|
|
120
|
+
expect(blocksSnappingShortcut(textarea)).toBe(true)
|
|
121
|
+
})
|
|
73
122
|
})
|
|
74
123
|
|
|
75
124
|
beforeEach(() => {
|
|
@@ -89,7 +138,7 @@ beforeEach(() => {
|
|
|
89
138
|
afterEach(() => {
|
|
90
139
|
useInteractionScope.getState().end()
|
|
91
140
|
useEditor.getState().armToolMode({ mode: 'select' })
|
|
92
|
-
useEditor.setState({ selectedItem: null })
|
|
141
|
+
useEditor.setState({ selectedItem: null, viewLayouts: {} })
|
|
93
142
|
clearSceneHistory()
|
|
94
143
|
})
|
|
95
144
|
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { steppedRotation } from '../components/tools/item/placement-math'
|
|
27
27
|
import { keyCyclableContinuationContext } from '../lib/continuation'
|
|
28
28
|
import { resolveDirectManipulationNode } from '../lib/direct-manipulation'
|
|
29
|
+
import { isSceneView } from '../lib/editor-views'
|
|
29
30
|
import { cancelGestures } from '../lib/gesture-lifecycle'
|
|
30
31
|
import { guideEmitter } from '../lib/guide-events'
|
|
31
32
|
import { isHistoryShortcut, runRedo, runUndo, shouldCancelDraftOnHistoryJump } from '../lib/history'
|
|
@@ -36,6 +37,7 @@ import { copySelectedNodesToEditorClipboard } from '../lib/scene-clipboard'
|
|
|
36
37
|
import { sfxEmitter } from '../lib/sfx-bus'
|
|
37
38
|
import { openSidebarPanel } from '../lib/sidebar-panel'
|
|
38
39
|
import { activeSiteNode, clampBrushRadius } from '../lib/terrain-sculpt'
|
|
40
|
+
import { isTypingTarget } from '../lib/typing-target'
|
|
39
41
|
import { leaveUnitFocus } from '../lib/units'
|
|
40
42
|
import { selectWallDrawVariant } from '../lib/wall-draw-variant'
|
|
41
43
|
import useDeleteConfirmation from '../store/use-delete-confirmation'
|
|
@@ -45,6 +47,12 @@ import useInteractionScope, {
|
|
|
45
47
|
isInteractionSubtreeDraft,
|
|
46
48
|
} from '../store/use-interaction-scope'
|
|
47
49
|
import { groupCurrentSelection, ungroupCurrentSelection } from '../store/use-session-groups'
|
|
50
|
+
import { activeViewLayout } from '../store/view-layout'
|
|
51
|
+
|
|
52
|
+
export function hasSceneKeyboardFocus(): boolean {
|
|
53
|
+
const layout = activeViewLayout(useEditor.getState())
|
|
54
|
+
return isSceneView(layout.panes[layout.focus])
|
|
55
|
+
}
|
|
48
56
|
|
|
49
57
|
// References (guide/scan) are selected via `useEditor.selectedReferenceId`, not
|
|
50
58
|
// the viewer selection, so selection-based key arms (R/T rotate) need this
|
|
@@ -274,7 +282,13 @@ export function blocksSnappingShortcut(
|
|
|
274
282
|
): boolean {
|
|
275
283
|
if (!target) return false
|
|
276
284
|
if (target.tagName === 'INPUT' && target.hasAttribute('data-run-length-input')) return false
|
|
277
|
-
return
|
|
285
|
+
return (
|
|
286
|
+
target.tagName === 'INPUT' ||
|
|
287
|
+
target.tagName === 'TEXTAREA' ||
|
|
288
|
+
target.tagName === 'SELECT' ||
|
|
289
|
+
target.isContentEditable ||
|
|
290
|
+
isTypingTarget(target as HTMLElement)
|
|
291
|
+
)
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
/** E on one selected node: the kind's own E action, else its mechanism. False when neither applies. */
|
|
@@ -317,6 +331,11 @@ export const useKeyboard = ({
|
|
|
317
331
|
let shiftTapClean = false
|
|
318
332
|
|
|
319
333
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
334
|
+
if (e.defaultPrevented || !hasSceneKeyboardFocus()) {
|
|
335
|
+
ctrlTapClean = false
|
|
336
|
+
shiftTapClean = false
|
|
337
|
+
return
|
|
338
|
+
}
|
|
320
339
|
if (e.key === 'Shift') {
|
|
321
340
|
shiftTapClean = !e.repeat && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
322
341
|
} else {
|
|
@@ -345,12 +364,8 @@ export const useKeyboard = ({
|
|
|
345
364
|
return
|
|
346
365
|
}
|
|
347
366
|
|
|
348
|
-
//
|
|
349
|
-
if (
|
|
350
|
-
e.target instanceof HTMLInputElement ||
|
|
351
|
-
e.target instanceof HTMLTextAreaElement ||
|
|
352
|
-
(e.target instanceof HTMLElement && e.target.isContentEditable)
|
|
353
|
-
) {
|
|
367
|
+
// Editable controls own their keyboard events.
|
|
368
|
+
if (isTypingTarget(e.target)) {
|
|
354
369
|
return
|
|
355
370
|
}
|
|
356
371
|
|
|
@@ -770,6 +785,11 @@ export const useKeyboard = ({
|
|
|
770
785
|
}
|
|
771
786
|
}
|
|
772
787
|
const handleKeyUp = (e: KeyboardEvent) => {
|
|
788
|
+
if (e.defaultPrevented || !hasSceneKeyboardFocus()) {
|
|
789
|
+
ctrlTapClean = false
|
|
790
|
+
shiftTapClean = false
|
|
791
|
+
return
|
|
792
|
+
}
|
|
773
793
|
if (e.key === 'Shift') {
|
|
774
794
|
const wasClean = shiftTapClean
|
|
775
795
|
shiftTapClean = false
|
|
@@ -804,13 +824,10 @@ export const useKeyboard = ({
|
|
|
804
824
|
// registry move overlay) — safe only because none of them claim Ctrl/Cmd+G.
|
|
805
825
|
// `e.code` keeps it on the physical G key across keyboard layouts.
|
|
806
826
|
const handleSessionGroupKeyDown = (e: KeyboardEvent) => {
|
|
827
|
+
if (e.defaultPrevented || !hasSceneKeyboardFocus()) return
|
|
807
828
|
if (e.key !== 'Shift') shiftTapClean = false
|
|
808
829
|
if (e.key !== 'Control' && e.key !== 'Meta') ctrlTapClean = false
|
|
809
|
-
if (
|
|
810
|
-
e.target instanceof HTMLInputElement ||
|
|
811
|
-
e.target instanceof HTMLTextAreaElement ||
|
|
812
|
-
(e.target instanceof HTMLElement && e.target.isContentEditable)
|
|
813
|
-
) {
|
|
830
|
+
if (isTypingTarget(e.target)) {
|
|
814
831
|
return
|
|
815
832
|
}
|
|
816
833
|
if (useDeleteConfirmation.getState().request) return
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import { type AnyNode, registerNode } from '@pascal-app/core'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
placementNoticeOf,
|
|
5
|
+
previewPlacementNoticeOf,
|
|
6
|
+
transientDraftId,
|
|
7
|
+
} from './use-placement-notice'
|
|
4
8
|
|
|
5
9
|
// The editor asks a node's definition for its placement notice; it names no kind.
|
|
6
10
|
function registerKind(placementNotice?: unknown): string {
|
|
@@ -46,3 +50,22 @@ describe('the placement notice of the node in hand', () => {
|
|
|
46
50
|
expect(transientDraftId({ kept: nodes.kept })).toBeNull()
|
|
47
51
|
})
|
|
48
52
|
})
|
|
53
|
+
|
|
54
|
+
test('ephemeral preview notices see their context without inserting a draft into the scene', () => {
|
|
55
|
+
const kind = registerKind((node: AnyNode, ctx: { nodes: Record<string, AnyNode> }) => ({
|
|
56
|
+
line: `${node.id}:${ctx.nodes.child?.name}`,
|
|
57
|
+
}))
|
|
58
|
+
const nodes = {}
|
|
59
|
+
const draft = nodeOf('preview', kind)
|
|
60
|
+
expect(
|
|
61
|
+
previewPlacementNoticeOf(nodes, {
|
|
62
|
+
node: draft,
|
|
63
|
+
parentNode: null,
|
|
64
|
+
contextNodes: [{ ...nodeOf('child', kind), name: 'draft child' }],
|
|
65
|
+
}),
|
|
66
|
+
).toEqual({ line: 'preview:draft child' })
|
|
67
|
+
expect(nodes).toEqual({})
|
|
68
|
+
expect(
|
|
69
|
+
previewPlacementNoticeOf(nodes, { node: null, parentNode: null, contextNodes: [] }),
|
|
70
|
+
).toBeNull()
|
|
71
|
+
})
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
useScene,
|
|
9
9
|
} from '@pascal-app/core'
|
|
10
10
|
import { useMemo } from 'react'
|
|
11
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
12
|
+
import usePlacementPreview from '../store/use-placement-preview'
|
|
11
13
|
|
|
12
14
|
/** The notice `nodeId`'s kind gives for where it stands (`placementNotice`), if it declares one. */
|
|
13
15
|
export function placementNoticeOf(
|
|
@@ -21,18 +23,47 @@ export function placementNoticeOf(
|
|
|
21
23
|
return notice(node as never, live ? { nodes, live } : { nodes })
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
export function previewPlacementNoticeOf(
|
|
27
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
28
|
+
draft: Pick<
|
|
29
|
+
ReturnType<typeof usePlacementPreview.getState>,
|
|
30
|
+
'node' | 'parentNode' | 'contextNodes'
|
|
31
|
+
>,
|
|
32
|
+
): PlacementNotice | null {
|
|
33
|
+
if (!draft.node) return null
|
|
34
|
+
const context = {
|
|
35
|
+
...nodes,
|
|
36
|
+
...Object.fromEntries(draft.contextNodes.map((node) => [node.id, node])),
|
|
37
|
+
...(draft.parentNode ? { [draft.parentNode.id]: draft.parentNode } : {}),
|
|
38
|
+
[draft.node.id]: draft.node,
|
|
39
|
+
}
|
|
40
|
+
return placementNoticeOf(context, draft.node.id)
|
|
41
|
+
}
|
|
42
|
+
|
|
24
43
|
/**
|
|
25
44
|
* What the node in hand, or the selected node, says about where it stands, read at its live pose
|
|
26
45
|
* while it is placed or moved. Subscribes to the scene only while a node is given, so an
|
|
27
46
|
* always-mounted caller doesn't re-render on every write.
|
|
28
47
|
*/
|
|
29
|
-
export function usePlacementNotice(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
48
|
+
export function usePlacementNotice(
|
|
49
|
+
nodeId: string | null | undefined,
|
|
50
|
+
preview = false,
|
|
51
|
+
): PlacementNotice | null {
|
|
52
|
+
const draft = usePlacementPreview(
|
|
53
|
+
useShallow((state) =>
|
|
54
|
+
preview
|
|
55
|
+
? { node: state.node, parentNode: state.parentNode, contextNodes: state.contextNodes }
|
|
56
|
+
: null,
|
|
57
|
+
),
|
|
35
58
|
)
|
|
59
|
+
const nodes = useScene((state) => (nodeId || draft?.node ? state.nodes : null))
|
|
60
|
+
const live = useLiveTransforms((state) => (nodeId ? state.get(nodeId) : undefined))
|
|
61
|
+
return useMemo(() => {
|
|
62
|
+
if (!nodes) return null
|
|
63
|
+
if (nodeId) return placementNoticeOf(nodes, nodeId, live)
|
|
64
|
+
if (!draft?.node) return null
|
|
65
|
+
return previewPlacementNoticeOf(nodes, draft)
|
|
66
|
+
}, [nodes, live, nodeId, draft])
|
|
36
67
|
}
|
|
37
68
|
|
|
38
69
|
/** The node a construction tool is placing: its transient draft. */
|