@pascal-app/editor 1.0.0-beta.5 → 1.0.1
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/LICENSE +21 -0
- package/bunfig.toml +4 -0
- package/package.json +15 -8
- package/scripts/generate-print-golden-house.ts +89 -0
- package/src/components/editor/bake-exporter.tsx +5 -0
- package/src/components/editor/bake-thumbnail.tsx +4 -1
- package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
- package/src/components/editor/capture-camera-rig.tsx +59 -0
- package/src/components/editor/custom-camera-controls.tsx +20 -11
- package/src/components/editor/editor-layout-mobile.tsx +6 -6
- package/src/components/editor/editor-layout-v2.tsx +9 -2
- package/src/components/editor/export-manager.tsx +175 -33
- package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
- package/src/components/editor/first-person-controls.tsx +180 -12
- package/src/components/editor/floating-action-menu.tsx +55 -51
- package/src/components/editor/floorplan-panel.tsx +99 -15
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-move-3d.ts +51 -21
- package/src/components/editor/group-transform-shared.test.ts +78 -0
- package/src/components/editor/group-transform-shared.ts +74 -7
- package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
- package/src/components/editor/handles/handle-arrow.tsx +109 -48
- package/src/components/editor/handles/handle-placement.test.ts +111 -0
- package/src/components/editor/handles/handle-placement.ts +69 -0
- package/src/components/editor/handles/linear-resize-drag.ts +78 -0
- package/src/components/editor/handles/resize-snap.test.ts +46 -0
- package/src/components/editor/handles/resize-snap.ts +10 -1
- package/src/components/editor/handles/use-handle-drag.ts +23 -3
- package/src/components/editor/index.tsx +151 -24
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +67 -119
- package/src/components/editor/selection-manager.tsx +153 -42
- package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
- package/src/components/editor/snapshot-capture.test.ts +422 -0
- package/src/components/editor/snapshot-capture.ts +145 -0
- package/src/components/editor/thumbnail-generator.tsx +295 -239
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
- package/src/components/procedural-items/inspector.tsx +223 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
- package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
- package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
- package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
- package/src/components/systems/roof/roof-edit-system.tsx +10 -19
- package/src/components/systems/selection-affordance-manager.tsx +59 -9
- package/src/components/systems/selection-affordance-services.ts +19 -0
- package/src/components/systems/stair/stair-edit-system.tsx +5 -2
- package/src/components/tools/fence/fence-drafting.test.ts +100 -0
- package/src/components/tools/fence/fence-drafting.ts +6 -25
- package/src/components/tools/item/draft-creation.test.ts +12 -0
- package/src/components/tools/item/draft-creation.ts +10 -0
- package/src/components/tools/item/face-host-commit.test.ts +65 -0
- package/src/components/tools/item/face-host-commit.ts +56 -0
- package/src/components/tools/item/face-host-preview.test.ts +77 -0
- package/src/components/tools/item/face-host-preview.ts +82 -0
- package/src/components/tools/item/item-surface-placement.test.ts +175 -0
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
- package/src/components/tools/item/move-tool.tsx +4 -3
- package/src/components/tools/item/placement-math.test.ts +11 -1
- package/src/components/tools/item/placement-math.ts +0 -20
- package/src/components/tools/item/placement-strategies.test.ts +331 -4
- package/src/components/tools/item/placement-strategies.ts +304 -171
- package/src/components/tools/item/placement-surface.test.ts +61 -0
- package/src/components/tools/item/placement-surface.ts +24 -0
- package/src/components/tools/item/placement-types.ts +10 -0
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
- package/src/components/tools/item/test-face-host.ts +121 -0
- package/src/components/tools/item/use-draft-node.test.tsx +336 -0
- package/src/components/tools/item/use-draft-node.ts +116 -44
- package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
- package/src/components/tools/registry/item-surface-move.test.ts +501 -0
- package/src/components/tools/registry/item-surface-move.ts +441 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
- package/src/components/tools/registry-tool-context.tsx +30 -0
- package/src/components/tools/select/box-select-tool.tsx +3 -1
- package/src/components/tools/select/marquee-footprint.test.ts +53 -0
- package/src/components/tools/select/marquee-footprint.ts +29 -0
- package/src/components/tools/shared/placement-box.tsx +57 -7
- package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
- package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
- package/src/components/tools/shared/pointer-support-cap.ts +56 -17
- package/src/components/tools/shared/shelf-stickiness.ts +54 -0
- package/src/components/tools/shared/surface-hit.test.ts +56 -0
- package/src/components/tools/shared/surface-hit.ts +23 -0
- package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
- package/src/components/tools/shared/surface-rejection.tsx +29 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
- package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
- package/src/components/tools/stair/stair-tool.tsx +127 -74
- package/src/components/tools/tool-manager.tsx +25 -8
- package/src/components/tools/wall/wall-drafting.test.ts +320 -2
- package/src/components/tools/wall/wall-drafting.ts +23 -114
- package/src/components/tools/zone/zone-tool.tsx +13 -4
- package/src/components/ui/action-menu/control-modes.tsx +9 -6
- package/src/components/ui/action-menu/index.tsx +11 -1
- package/src/components/ui/command-palette/editor-commands.tsx +63 -13
- package/src/components/ui/command-palette/index.tsx +7 -2
- package/src/components/ui/controls/material-paint-panel.tsx +6 -4
- package/src/components/ui/controls/material-picker.tsx +4 -4
- package/src/components/ui/controls/metric-control.tsx +29 -13
- package/src/components/ui/controls/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +44 -21
- package/src/components/ui/controls/toggle-control.tsx +18 -5
- package/src/components/ui/controls/tool-options-panel.tsx +126 -0
- package/src/components/ui/floating-level-selector.tsx +103 -10
- package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
- package/src/components/ui/helpers/helper-manager.tsx +47 -11
- package/src/components/ui/helpers/item-helper.tsx +1 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
- package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
- package/src/components/ui/panels/homogeneous-selection.ts +47 -0
- package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
- package/src/components/ui/panels/multi-field-value.test.ts +209 -0
- package/src/components/ui/panels/multi-field-value.ts +163 -0
- package/src/components/ui/panels/multi-height-mode.tsx +185 -0
- package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
- package/src/components/ui/panels/node-display.ts +4 -0
- package/src/components/ui/panels/panel-manager.tsx +77 -3
- package/src/components/ui/panels/parametric-field-control.tsx +173 -0
- package/src/components/ui/panels/parametric-field-utils.ts +18 -0
- package/src/components/ui/panels/parametric-inspector.tsx +35 -164
- package/src/components/ui/panels/reference-panel.tsx +34 -4
- package/src/components/ui/primitives/checkbox.tsx +34 -0
- package/src/components/ui/primitives/shortcut-token.tsx +23 -1
- package/src/components/ui/scene-loader.tsx +35 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
- package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
- package/src/components/ui/sidebar/tab-bar.tsx +2 -0
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/ui/snap-target-badge.tsx +2 -0
- package/src/components/viewer/viewer-scene-header.tsx +5 -1
- package/src/components/viewer/viewer-stage.tsx +4 -1
- package/src/components/viewer/viewer-units-panel.tsx +116 -0
- package/src/components/viewer-overlay.tsx +1 -22
- package/src/components/viewer-zone-system.tsx +6 -2
- package/src/components/walkthrough-hud.tsx +19 -10
- package/src/hooks/use-auto-save.test.ts +73 -1
- package/src/hooks/use-auto-save.ts +76 -5
- package/src/hooks/use-ceiling-events.test.ts +144 -0
- package/src/hooks/use-ceiling-events.ts +5 -1
- package/src/hooks/use-drag-action.ts +17 -3
- package/src/hooks/use-grid-events.test.ts +240 -0
- package/src/hooks/use-grid-events.ts +190 -11
- package/src/hooks/use-keyboard.test.ts +244 -0
- package/src/hooks/use-keyboard.ts +132 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +87 -4
- package/src/lib/active-placement-surface.test.ts +16 -0
- package/src/lib/active-placement-surface.ts +14 -0
- package/src/lib/ceiling-surface-raycast.test.ts +64 -0
- package/src/lib/ceiling-surface-raycast.ts +12 -0
- package/src/lib/contextual-help-extension.ts +17 -0
- package/src/lib/continuation.test.ts +11 -0
- package/src/lib/continuation.ts +8 -1
- package/src/lib/direct-manipulation.test.ts +132 -0
- package/src/lib/direct-manipulation.ts +38 -1
- package/src/lib/editor-api.ts +6 -3
- package/src/lib/elevation-guides.test.ts +28 -1
- package/src/lib/elevation-guides.ts +22 -7
- package/src/lib/export-texture-utils.test.ts +167 -0
- package/src/lib/export-texture-utils.ts +121 -0
- package/src/lib/export-user-data.test.ts +48 -0
- package/src/lib/export-user-data.ts +69 -0
- package/src/lib/floating-menu-scale.test.ts +29 -0
- package/src/lib/floating-menu-scale.ts +14 -0
- package/src/lib/floorplan/apply-alignment.test.ts +47 -2
- package/src/lib/floorplan/apply-alignment.ts +10 -1
- package/src/lib/floorplan/floorplan-export.test.ts +536 -2
- package/src/lib/floorplan/floorplan-export.tsx +120 -19
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
- package/src/lib/floorplan/floorplan-readonly.ts +5 -0
- package/src/lib/floorplan-grid-event-point.test.ts +29 -0
- package/src/lib/floorplan-grid-event-point.ts +13 -0
- package/src/lib/fresh-planar-placement.ts +13 -5
- package/src/lib/glb-export-serialize.test.ts +280 -0
- package/src/lib/glb-export.test.ts +1375 -21
- package/src/lib/glb-export.ts +781 -106
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +528 -147
- package/src/lib/history.ts +81 -7
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/interaction/hot-set.test.ts +10 -1
- package/src/lib/interaction/hot-set.ts +3 -1
- package/src/lib/interaction/overlay-policy.test.ts +33 -1
- package/src/lib/interaction/overlay-policy.ts +16 -0
- package/src/lib/interaction/registered-drafting.ts +36 -0
- package/src/lib/interaction/scope.ts +31 -0
- package/src/lib/level-duplication.test.ts +73 -0
- package/src/lib/level-duplication.ts +32 -2
- package/src/lib/level-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- package/src/lib/linear-display.test.ts +59 -0
- package/src/lib/linear-display.ts +39 -0
- package/src/lib/local-guide-image.test.ts +24 -0
- package/src/lib/local-guide-image.ts +34 -1
- package/src/lib/local-project-presentation-persistence.test.ts +302 -0
- package/src/lib/local-project-presentation-persistence.ts +335 -0
- package/src/lib/material-paint.ts +2 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/node-action-movement.ts +21 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/planar-cursor-placement.test.ts +178 -1
- package/src/lib/planar-cursor-placement.ts +61 -5
- package/src/lib/plugin-panels.test.ts +92 -1
- package/src/lib/plugin-panels.ts +48 -0
- package/src/lib/portable-export.test.ts +455 -0
- package/src/lib/portable-export.ts +1042 -0
- package/src/lib/print-3mf.test.ts +86 -0
- package/src/lib/print-3mf.ts +161 -0
- package/src/lib/print-content-scope.test.ts +107 -0
- package/src/lib/print-content-scope.ts +90 -0
- package/src/lib/print-export.test.ts +294 -0
- package/src/lib/print-export.ts +735 -0
- package/src/lib/print-feature-thickness.test.ts +147 -0
- package/src/lib/print-feature-thickness.ts +180 -0
- package/src/lib/print-golden-house.test-fixture.ts +352 -0
- package/src/lib/print-golden-house.test.ts +249 -0
- package/src/lib/print-roof-solids.test.ts +133 -0
- package/src/lib/print-roof-solids.ts +630 -0
- package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
- package/src/lib/print-shell-compiler-baseline.ts +225 -0
- package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
- package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
- package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
- package/src/lib/print-shell-compiler-protocol.ts +38 -0
- package/src/lib/print-shell-compiler.ts +273 -0
- package/src/lib/print-wall-solids.test.ts +194 -0
- package/src/lib/print-wall-solids.ts +328 -0
- package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
- package/src/lib/rigid-plan-svg-transform.ts +24 -0
- package/src/lib/scene-clipboard.ts +1 -1
- package/src/lib/scene.ts +22 -8
- package/src/lib/selection-routing.test.ts +105 -1
- package/src/lib/selection-routing.ts +19 -0
- package/src/lib/send-to-app.test.ts +108 -0
- package/src/lib/send-to-app.ts +121 -0
- package/src/lib/sfx/movement-tick.ts +37 -0
- package/src/lib/sfx-bus.ts +4 -0
- package/src/lib/sfx-player.ts +8 -0
- package/src/lib/snapping-mode.test.ts +32 -4
- package/src/lib/snapping-mode.ts +11 -7
- package/src/lib/surface-attachment.ts +77 -0
- package/src/lib/units.ts +254 -0
- package/src/lib/usdz-export.ts +65 -0
- package/src/lib/use-linear-display.ts +8 -32
- package/src/lib/walkthrough-pointer-lock.ts +47 -0
- package/src/store/live-draft-preview-stores.test.ts +3 -0
- package/src/store/terrain-sculpt-mode.test.ts +3 -6
- package/src/store/tool-mode.test.ts +87 -0
- package/src/store/use-camera-hint-focus.ts +48 -0
- package/src/store/use-editor.tsx +299 -188
- package/src/store/use-floorplan-draft-preview.ts +9 -0
- package/src/store/use-handle-group.ts +4 -0
- package/src/store/use-interaction-scope.test.ts +23 -1
- package/src/store/use-interaction-scope.ts +63 -2
- package/src/store/use-placement-preview.ts +56 -3
- package/src/store/use-send-to-app.ts +21 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -3,14 +3,26 @@ import {
|
|
|
3
3
|
type EventSuffix,
|
|
4
4
|
emitter,
|
|
5
5
|
type GridEvent,
|
|
6
|
+
nodeRegistry,
|
|
6
7
|
sceneRegistry,
|
|
8
|
+
useScene,
|
|
7
9
|
} from '@pascal-app/core'
|
|
8
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { setSurfaceRaycastLayers, timeSpan, useViewer } from '@pascal-app/viewer'
|
|
9
11
|
import { useThree } from '@react-three/fiber'
|
|
10
12
|
import { useEffect, useRef } from 'react'
|
|
11
|
-
import { Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
13
|
+
import { Matrix3, type Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three'
|
|
12
14
|
import { getPlacementSurface } from '../lib/active-placement-surface'
|
|
15
|
+
import { raycastCeilingUnderside } from '../lib/ceiling-surface-raycast'
|
|
13
16
|
import { resolveTerrainGroundHit } from '../lib/ground-surface'
|
|
17
|
+
import {
|
|
18
|
+
type DraftingSurfaceExtension,
|
|
19
|
+
registeredDraftingConfig,
|
|
20
|
+
registeredDraftingSurface,
|
|
21
|
+
} from '../lib/interaction/registered-drafting'
|
|
22
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
23
|
+
|
|
24
|
+
// Keep tool previews tracking camera navigation at 10 Hz without querying every move.
|
|
25
|
+
const CAMERA_DRAG_MOVE_INTERVAL_MS = 100
|
|
14
26
|
|
|
15
27
|
/**
|
|
16
28
|
* Custom grid events hook that uses manual raycasting instead of mesh events.
|
|
@@ -18,11 +30,28 @@ import { resolveTerrainGroundHit } from '../lib/ground-surface'
|
|
|
18
30
|
*/
|
|
19
31
|
export function useGridEvents(gridY: number) {
|
|
20
32
|
const { camera, gl } = useThree()
|
|
33
|
+
const interactionScope = useInteractionScope((state) => state.scope)
|
|
34
|
+
const semanticSurfaceQueryRef = useRef(false)
|
|
35
|
+
semanticSurfaceQueryRef.current =
|
|
36
|
+
interactionScope.kind === 'placing' ||
|
|
37
|
+
interactionScope.kind === 'moving' ||
|
|
38
|
+
registeredDraftingConfig(interactionScope)?.surfaceQuery === true
|
|
21
39
|
const raycaster = useRef(new Raycaster())
|
|
22
40
|
const pointer = useRef(new Vector2())
|
|
23
41
|
const groundPlane = useRef(new Plane(new Vector3(0, 1, 0), 0))
|
|
24
42
|
const intersectionPoint = useRef(new Vector3())
|
|
25
43
|
|
|
44
|
+
type GridIntersection = {
|
|
45
|
+
point: Vector3
|
|
46
|
+
surface?: {
|
|
47
|
+
point: Vector3
|
|
48
|
+
object: Object3D
|
|
49
|
+
hostId: AnyNodeId
|
|
50
|
+
descriptor: DraftingSurfaceExtension
|
|
51
|
+
worldNormal?: Vector3
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
// Update ground plane when grid Y changes
|
|
27
56
|
useEffect(() => {
|
|
28
57
|
groundPlane.current.constant = -gridY
|
|
@@ -30,8 +59,64 @@ export function useGridEvents(gridY: number) {
|
|
|
30
59
|
|
|
31
60
|
useEffect(() => {
|
|
32
61
|
const canvas = gl.domElement
|
|
62
|
+
setSurfaceRaycastLayers(raycaster.current.layers)
|
|
63
|
+
|
|
64
|
+
const getSurfaceIntersection = (): GridIntersection | null => {
|
|
65
|
+
let closest: GridIntersection | null = null
|
|
66
|
+
let closestDistance = Number.POSITIVE_INFINITY
|
|
67
|
+
|
|
68
|
+
for (const [type, definition] of nodeRegistry.entries()) {
|
|
69
|
+
const descriptor = registeredDraftingSurface(definition)
|
|
70
|
+
if (!descriptor) continue
|
|
71
|
+
for (const id of sceneRegistry.byType[type] ?? []) {
|
|
72
|
+
const root = sceneRegistry.nodes.get(id)
|
|
73
|
+
if (!root) continue
|
|
74
|
+
const scope = useInteractionScope.getState().scope
|
|
75
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
76
|
+
if (surfaceDraft && useScene.getState().nodes[id as AnyNodeId]?.visible === false)
|
|
77
|
+
continue
|
|
78
|
+
if (surfaceDraft && !root.visible) continue
|
|
79
|
+
const intersections =
|
|
80
|
+
surfaceDraft && descriptor.raycast === 'underside'
|
|
81
|
+
? raycastCeilingUnderside(raycaster.current, root)
|
|
82
|
+
: raycaster.current.intersectObject(root, true)
|
|
83
|
+
const hit = intersections.find((candidate) => {
|
|
84
|
+
if (!surfaceDraft) return true
|
|
85
|
+
if (useScene.getState().nodes[id as AnyNodeId]?.visible === false) return false
|
|
86
|
+
let object: Object3D | null = candidate.object
|
|
87
|
+
while (object) {
|
|
88
|
+
if (!object.visible || object.userData.wallHidden === true) return false
|
|
89
|
+
object = object.parent
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
92
|
+
})
|
|
93
|
+
if (!hit || hit.distance >= closestDistance) continue
|
|
94
|
+
closestDistance = hit.distance
|
|
95
|
+
const worldNormal = hit.face
|
|
96
|
+
? hit.face.normal
|
|
97
|
+
.clone()
|
|
98
|
+
.applyNormalMatrix(new Matrix3().getNormalMatrix(hit.object.matrixWorld))
|
|
99
|
+
.normalize()
|
|
100
|
+
: undefined
|
|
101
|
+
if (surfaceDraft && worldNormal && worldNormal.dot(raycaster.current.ray.direction) > 0)
|
|
102
|
+
worldNormal.negate()
|
|
103
|
+
closest = {
|
|
104
|
+
point: hit.point.clone(),
|
|
105
|
+
surface: {
|
|
106
|
+
point: hit.point.clone(),
|
|
107
|
+
object: hit.object,
|
|
108
|
+
hostId: id as AnyNodeId,
|
|
109
|
+
descriptor,
|
|
110
|
+
worldNormal,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return closest
|
|
117
|
+
}
|
|
33
118
|
|
|
34
|
-
const getIntersection = (nativeEvent: MouseEvent | PointerEvent):
|
|
119
|
+
const getIntersection = (nativeEvent: MouseEvent | PointerEvent): GridIntersection | null => {
|
|
35
120
|
// Convert mouse position to normalized device coordinates (-1 to +1)
|
|
36
121
|
const rect = canvas.getBoundingClientRect()
|
|
37
122
|
pointer.current.x = ((nativeEvent.clientX - rect.left) / rect.width) * 2 - 1
|
|
@@ -40,6 +125,33 @@ export function useGridEvents(gridY: number) {
|
|
|
40
125
|
// Update raycaster
|
|
41
126
|
raycaster.current.setFromCamera(pointer.current, camera)
|
|
42
127
|
|
|
128
|
+
// R3F node events can be stopped by another mesh, so this canvas-level
|
|
129
|
+
// raycast is the reliable architectural-surface source for placement and
|
|
130
|
+
// drawing tools. Keep it separate from the ordinary grid point so tools
|
|
131
|
+
// that intentionally place on the floor retain their existing behavior.
|
|
132
|
+
// Architectural meshes are expensive to raycast and are meaningful only
|
|
133
|
+
// to an active placement/drafting interaction. Floor tools retain the
|
|
134
|
+
// ordinary terrain/grid intersection without scanning every wall.
|
|
135
|
+
const surfaceHit = semanticSurfaceQueryRef.current ? getSurfaceIntersection() : null
|
|
136
|
+
|
|
137
|
+
// A semantic architectural hit is the authoritative cursor position.
|
|
138
|
+
// Do not replace it with the terrain/grid intersection below: that would
|
|
139
|
+
// make a wall hit carry wall metadata while still placing at the ground
|
|
140
|
+
// floor, especially in perspective views.
|
|
141
|
+
if (surfaceHit) return surfaceHit
|
|
142
|
+
|
|
143
|
+
const scope = useInteractionScope.getState().scope
|
|
144
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
145
|
+
const workingSurface = getPlacementSurface()
|
|
146
|
+
if (surfaceDraft && workingSurface) {
|
|
147
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(
|
|
148
|
+
workingSurface.normal,
|
|
149
|
+
workingSurface.point,
|
|
150
|
+
)
|
|
151
|
+
const projected = raycaster.current.ray.intersectPlane(plane, intersectionPoint.current)
|
|
152
|
+
return { point: (projected ?? workingSurface.point).clone() }
|
|
153
|
+
}
|
|
154
|
+
|
|
43
155
|
// Sculpted ground wins over the plane, but only while the plane IS the
|
|
44
156
|
// ground (see `isSiteGroundPlane`): a plane riding a storey base or a slab
|
|
45
157
|
// top is a real flat surface and must stay planar. The march is what removes
|
|
@@ -60,14 +172,18 @@ export function useGridEvents(gridY: number) {
|
|
|
60
172
|
[direction.x, direction.y, direction.z],
|
|
61
173
|
-groundPlane.current.constant,
|
|
62
174
|
)
|
|
63
|
-
if (hit)
|
|
175
|
+
if (hit) {
|
|
176
|
+
return {
|
|
177
|
+
point: intersectionPoint.current.set(hit.x, hit.y, hit.z).clone(),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
64
180
|
|
|
65
181
|
// Intersect with ground plane
|
|
66
182
|
if (raycaster.current.ray.intersectPlane(groundPlane.current, intersectionPoint.current)) {
|
|
67
|
-
return intersectionPoint.current.clone()
|
|
183
|
+
return { point: intersectionPoint.current.clone() }
|
|
68
184
|
}
|
|
69
185
|
|
|
70
|
-
return
|
|
186
|
+
return surfaceHit
|
|
71
187
|
}
|
|
72
188
|
|
|
73
189
|
const emit = (suffix: EventSuffix, nativeEvent: MouseEvent | PointerEvent) => {
|
|
@@ -75,14 +191,69 @@ export function useGridEvents(gridY: number) {
|
|
|
75
191
|
if (!point) return
|
|
76
192
|
|
|
77
193
|
// Convert world-space point to building-local for tools that live inside a building.
|
|
78
|
-
const
|
|
194
|
+
const scope = useInteractionScope.getState().scope
|
|
195
|
+
const surfaceDraft = registeredDraftingConfig(scope)?.surfaceQuery === true
|
|
196
|
+
const buildingId = surfaceDraft
|
|
197
|
+
? useViewer.getState().selection.levelId
|
|
198
|
+
: useViewer.getState().selection.buildingId
|
|
79
199
|
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
80
|
-
const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.clone()) : point
|
|
200
|
+
const localPoint = buildingMesh ? buildingMesh.worldToLocal(point.point.clone()) : point.point
|
|
201
|
+
const surfaceLocalPoint = point.surface
|
|
202
|
+
? buildingMesh
|
|
203
|
+
? buildingMesh.worldToLocal(point.surface.point.clone())
|
|
204
|
+
: point.surface.point.clone()
|
|
205
|
+
: undefined
|
|
206
|
+
const localNormal = point.surface?.worldNormal
|
|
207
|
+
? buildingMesh
|
|
208
|
+
? buildingMesh
|
|
209
|
+
.worldToLocal(point.surface.point.clone().add(point.surface.worldNormal))
|
|
210
|
+
.sub(surfaceLocalPoint ?? localPoint)
|
|
211
|
+
.normalize()
|
|
212
|
+
: point.surface.worldNormal
|
|
213
|
+
: undefined
|
|
214
|
+
const surfaceNode = point.surface
|
|
215
|
+
? useScene.getState().nodes[point.surface.hostId]
|
|
216
|
+
: undefined
|
|
217
|
+
const classifiedFace =
|
|
218
|
+
point.surface && localNormal
|
|
219
|
+
? point.surface.descriptor.classifyFace?.(surfaceNode, [
|
|
220
|
+
localNormal.x,
|
|
221
|
+
localNormal.y,
|
|
222
|
+
localNormal.z,
|
|
223
|
+
])
|
|
224
|
+
: null
|
|
225
|
+
const { origin, direction } = raycaster.current.ray
|
|
226
|
+
const localRayOrigin = buildingMesh
|
|
227
|
+
? buildingMesh.worldToLocal(origin.clone())
|
|
228
|
+
: origin.clone()
|
|
229
|
+
const localRayDirection = buildingMesh
|
|
230
|
+
? buildingMesh.worldToLocal(origin.clone().add(direction)).sub(localRayOrigin).normalize()
|
|
231
|
+
: direction.clone()
|
|
81
232
|
|
|
82
233
|
const eventKey = `grid:${suffix}` as `grid:${EventSuffix}`
|
|
83
234
|
const payload: GridEvent = {
|
|
84
|
-
|
|
235
|
+
localFrameId: buildingId ?? undefined,
|
|
236
|
+
position: [point.point.x, point.point.y, point.point.z],
|
|
85
237
|
localPosition: [localPoint.x, localPoint.y, localPoint.z],
|
|
238
|
+
localRay: {
|
|
239
|
+
origin: [localRayOrigin.x, localRayOrigin.y, localRayOrigin.z],
|
|
240
|
+
direction: [localRayDirection.x, localRayDirection.y, localRayDirection.z],
|
|
241
|
+
},
|
|
242
|
+
surfaceLocalPosition: surfaceLocalPoint
|
|
243
|
+
? [surfaceLocalPoint.x, surfaceLocalPoint.y, surfaceLocalPoint.z]
|
|
244
|
+
: undefined,
|
|
245
|
+
surfaceNormal: localNormal ? [localNormal.x, localNormal.y, localNormal.z] : undefined,
|
|
246
|
+
surfaceObject: point.surface?.object,
|
|
247
|
+
surfaceHit:
|
|
248
|
+
semanticSurfaceQueryRef.current && point.surface
|
|
249
|
+
? {
|
|
250
|
+
kind: point.surface.descriptor.kind,
|
|
251
|
+
hostId: point.surface.hostId,
|
|
252
|
+
face: classifiedFace?.face ?? 'unknown',
|
|
253
|
+
levelId: useViewer.getState().selection.levelId ?? undefined,
|
|
254
|
+
side: classifiedFace?.side,
|
|
255
|
+
}
|
|
256
|
+
: undefined,
|
|
86
257
|
nativeEvent: nativeEvent as any, // Type compatibility with ThreeEvent
|
|
87
258
|
}
|
|
88
259
|
|
|
@@ -107,9 +278,17 @@ export function useGridEvents(gridY: number) {
|
|
|
107
278
|
emit('click', e)
|
|
108
279
|
}
|
|
109
280
|
|
|
281
|
+
let lastCameraDragMove = Number.NEGATIVE_INFINITY
|
|
110
282
|
const handlePointerMove = (e: PointerEvent) => {
|
|
111
|
-
//
|
|
112
|
-
|
|
283
|
+
// Moves keep tool cursor snapshots current, including wheel zoom during a tool gesture.
|
|
284
|
+
if (useViewer.getState().cameraDragging) {
|
|
285
|
+
const now = performance.now()
|
|
286
|
+
if (now - lastCameraDragMove < CAMERA_DRAG_MOVE_INTERVAL_MS) return
|
|
287
|
+
lastCameraDragMove = now
|
|
288
|
+
} else {
|
|
289
|
+
lastCameraDragMove = Number.NEGATIVE_INFINITY
|
|
290
|
+
}
|
|
291
|
+
timeSpan('pointer', () => emit('move', e))
|
|
113
292
|
}
|
|
114
293
|
|
|
115
294
|
const handleDoubleClick = (e: MouseEvent) => {
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
BlockNode,
|
|
6
|
+
clearSceneHistory,
|
|
7
|
+
DuctFittingNode,
|
|
8
|
+
DuctSegmentNode,
|
|
9
|
+
emitter,
|
|
10
|
+
nodeRegistry,
|
|
11
|
+
PipeFittingNode,
|
|
12
|
+
PipeSegmentNode,
|
|
13
|
+
useScene,
|
|
14
|
+
} from '@pascal-app/core'
|
|
15
|
+
import { runRedo, runUndo } from '../lib/history'
|
|
16
|
+
import { meshEditScope } from '../lib/interaction/scope'
|
|
17
|
+
import useEditor from '../store/use-editor'
|
|
18
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
19
|
+
import {
|
|
20
|
+
blocksSnappingShortcut,
|
|
21
|
+
canCycleSnappingModeShortcut,
|
|
22
|
+
canRunGlobalRotationShortcut,
|
|
23
|
+
isToolOwnedCanopyForm,
|
|
24
|
+
isToolOwnedRotation,
|
|
25
|
+
markToolCancelConsumed,
|
|
26
|
+
runHistoryShortcut,
|
|
27
|
+
} from './use-keyboard'
|
|
28
|
+
|
|
29
|
+
type RafFn = (callback: (time: number) => void) => number
|
|
30
|
+
;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (
|
|
31
|
+
callback,
|
|
32
|
+
) => {
|
|
33
|
+
callback(0)
|
|
34
|
+
return 0
|
|
35
|
+
}
|
|
36
|
+
;(globalThis as unknown as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??=
|
|
37
|
+
() => {}
|
|
38
|
+
|
|
39
|
+
const NODE_ID = 'block_history' as AnyNodeId
|
|
40
|
+
|
|
41
|
+
describe('snapping shortcuts while entering a run length', () => {
|
|
42
|
+
test('allows snap mode and spacing shortcuts in the duct and pipe length field', () => {
|
|
43
|
+
expect(
|
|
44
|
+
blocksSnappingShortcut({
|
|
45
|
+
tagName: 'INPUT',
|
|
46
|
+
isContentEditable: false,
|
|
47
|
+
hasAttribute: (name) => name === 'data-run-length-input',
|
|
48
|
+
}),
|
|
49
|
+
).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('continues to protect ordinary text fields and editable content', () => {
|
|
53
|
+
for (const tagName of ['INPUT', 'TEXTAREA', 'DIV']) {
|
|
54
|
+
expect(
|
|
55
|
+
blocksSnappingShortcut({
|
|
56
|
+
tagName,
|
|
57
|
+
isContentEditable: tagName === 'DIV',
|
|
58
|
+
hasAttribute: () => false,
|
|
59
|
+
}),
|
|
60
|
+
).toBe(true)
|
|
61
|
+
}
|
|
62
|
+
expect(blocksSnappingShortcut(null)).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
const node = BlockNode.parse({ id: NODE_ID, position: [0, 0, 0] })
|
|
68
|
+
useScene.setState({
|
|
69
|
+
nodes: { [NODE_ID]: node },
|
|
70
|
+
rootNodeIds: [NODE_ID],
|
|
71
|
+
dirtyNodes: new Set<AnyNodeId>(),
|
|
72
|
+
collections: {},
|
|
73
|
+
materials: {},
|
|
74
|
+
readOnly: false,
|
|
75
|
+
} as never)
|
|
76
|
+
clearSceneHistory()
|
|
77
|
+
useScene.getState().updateNode(NODE_ID, { position: [1, 2, 3] } as Partial<AnyNode>)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
useInteractionScope.getState().end()
|
|
82
|
+
useEditor.getState().armToolMode({ mode: 'select' })
|
|
83
|
+
useEditor.setState({ selectedItem: null })
|
|
84
|
+
clearSceneHistory()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('rotation shortcut ownership', () => {
|
|
88
|
+
test('does not reserve R and T for an armed item tool without a placement item', () => {
|
|
89
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
|
|
90
|
+
|
|
91
|
+
expect(isToolOwnedRotation()).toBe(false)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('leaves R and T to the active item placement tool', () => {
|
|
95
|
+
useEditor.getState().setSelectedItem({
|
|
96
|
+
asset: { id: 'asset:test' },
|
|
97
|
+
} as never)
|
|
98
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'item' })
|
|
99
|
+
|
|
100
|
+
expect(isToolOwnedRotation()).toBe(true)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('leaves R and T to the active lean-to placement tool', () => {
|
|
104
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'lean-to-extension' })
|
|
105
|
+
|
|
106
|
+
expect(isToolOwnedRotation()).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('leaves R and T to a moving lean-to extension', () => {
|
|
110
|
+
const leanTo = { id: 'lean_to_moving', type: 'lean-to-extension' } as unknown as AnyNode
|
|
111
|
+
useInteractionScope.getState().begin({
|
|
112
|
+
kind: 'moving',
|
|
113
|
+
node: leanTo,
|
|
114
|
+
nodeId: leanTo.id,
|
|
115
|
+
nodeType: leanTo.type,
|
|
116
|
+
view: '3d',
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
expect(isToolOwnedRotation()).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('leaves F to the active lean-to placement tool', () => {
|
|
123
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'lean-to-extension' })
|
|
124
|
+
|
|
125
|
+
expect(isToolOwnedCanopyForm()).toBe(true)
|
|
126
|
+
useEditor.getState().armToolMode({ mode: 'build', tool: 'wall' })
|
|
127
|
+
expect(isToolOwnedCanopyForm()).toBe(false)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('history shortcuts during block editing', () => {
|
|
132
|
+
test('reserves global rotation shortcuts for the active mesh editor', () => {
|
|
133
|
+
expect(canRunGlobalRotationShortcut()).toBe(true)
|
|
134
|
+
useInteractionScope.getState().begin(meshEditScope(NODE_ID))
|
|
135
|
+
expect(canRunGlobalRotationShortcut()).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('keeps Shift available to cycle snapping while a mesh operation is active', () => {
|
|
139
|
+
useInteractionScope.getState().begin(meshEditScope(NODE_ID))
|
|
140
|
+
expect(canCycleSnappingModeShortcut(true)).toBe(true)
|
|
141
|
+
|
|
142
|
+
useInteractionScope.getState().begin(meshEditScope(NODE_ID, 'operating', 'translate'))
|
|
143
|
+
expect(canCycleSnappingModeShortcut(true)).toBe(true)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('undoes and redoes mesh changes without leaving component selection mode', () => {
|
|
147
|
+
useInteractionScope.getState().begin(meshEditScope(NODE_ID))
|
|
148
|
+
|
|
149
|
+
expect(runHistoryShortcut('undo')).toBe(true)
|
|
150
|
+
expect((useScene.getState().nodes[NODE_ID] as BlockNode).position).toEqual([0, 0, 0])
|
|
151
|
+
expect(useInteractionScope.getState().scope).toEqual({
|
|
152
|
+
kind: 'mesh-editing',
|
|
153
|
+
nodeId: NODE_ID,
|
|
154
|
+
phase: 'selecting',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(runHistoryShortcut('redo')).toBe(true)
|
|
158
|
+
expect((useScene.getState().nodes[NODE_ID] as BlockNode).position).toEqual([1, 2, 3])
|
|
159
|
+
expect(useInteractionScope.getState().scope).toEqual({
|
|
160
|
+
kind: 'mesh-editing',
|
|
161
|
+
nodeId: NODE_ID,
|
|
162
|
+
phase: 'selecting',
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('history while drawing distribution runs', () => {
|
|
168
|
+
let restoreRegistry = () => {}
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
restoreRegistry = nodeRegistry._snapshot()
|
|
172
|
+
nodeRegistry._reset()
|
|
173
|
+
for (const kind of ['duct-segment', 'pipe-segment']) {
|
|
174
|
+
nodeRegistry._register({
|
|
175
|
+
kind,
|
|
176
|
+
schemaVersion: 1,
|
|
177
|
+
drafting: { cancelOnHistoryJump: true },
|
|
178
|
+
} as never)
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
afterEach(() => {
|
|
183
|
+
restoreRegistry()
|
|
184
|
+
restoreRegistry = () => {}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
for (const [kind, schema, fittingSchema] of [
|
|
188
|
+
['duct', DuctSegmentNode, DuctFittingNode],
|
|
189
|
+
['pipe', PipeSegmentNode, PipeFittingNode],
|
|
190
|
+
] as const) {
|
|
191
|
+
test(`undoes and redoes individual ${kind} commits while drafting`, () => {
|
|
192
|
+
clearSceneHistory()
|
|
193
|
+
const first = schema.parse({
|
|
194
|
+
path: [
|
|
195
|
+
[0, 1, 0],
|
|
196
|
+
[1, 1, 0],
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
const second = schema.parse({
|
|
200
|
+
path: [
|
|
201
|
+
[1, 1, 0],
|
|
202
|
+
[2, 1, 0],
|
|
203
|
+
],
|
|
204
|
+
})
|
|
205
|
+
useScene.getState().applyNodeChanges({ create: [{ node: first }] })
|
|
206
|
+
const fitting = fittingSchema.parse({ position: [1, 1, 0] })
|
|
207
|
+
const trimmedPath: [number, number, number][] = [
|
|
208
|
+
[0, 1, 0],
|
|
209
|
+
[0.8, 1, 0],
|
|
210
|
+
]
|
|
211
|
+
useScene.getState().applyNodeChanges({
|
|
212
|
+
create: [{ node: second }, { node: fitting }],
|
|
213
|
+
update: [{ id: first.id, data: { path: trimmedPath } }],
|
|
214
|
+
})
|
|
215
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(2)
|
|
216
|
+
useInteractionScope.getState().begin({ kind: 'drafting', tool: first.type })
|
|
217
|
+
let cancellations = 0
|
|
218
|
+
const cancel = () => {
|
|
219
|
+
cancellations += 1
|
|
220
|
+
markToolCancelConsumed()
|
|
221
|
+
}
|
|
222
|
+
emitter.on('tool:cancel', cancel)
|
|
223
|
+
try {
|
|
224
|
+
expect(runHistoryShortcut('undo')).toBe(true)
|
|
225
|
+
expect(useScene.getState().nodes[second.id]).toBeUndefined()
|
|
226
|
+
expect(useScene.getState().nodes[fitting.id]).toBeUndefined()
|
|
227
|
+
expect(useScene.getState().nodes[first.id]).toMatchObject({ path: first.path })
|
|
228
|
+
expect(runUndo().kind).toBe('applied')
|
|
229
|
+
expect(useScene.getState().nodes[first.id]).toBeUndefined()
|
|
230
|
+
expect(runRedo().kind).toBe('applied')
|
|
231
|
+
expect(useScene.getState().nodes[first.id]).toBeDefined()
|
|
232
|
+
expect(useScene.getState().nodes[second.id]).toBeUndefined()
|
|
233
|
+
expect(runHistoryShortcut('redo')).toBe(true)
|
|
234
|
+
expect(useScene.getState().nodes[second.id]).toBeDefined()
|
|
235
|
+
expect(useScene.getState().nodes[fitting.id]).toBeDefined()
|
|
236
|
+
expect(useScene.getState().nodes[first.id]).toMatchObject({ path: trimmedPath })
|
|
237
|
+
expect(cancellations).toBe(4)
|
|
238
|
+
expect(useInteractionScope.getState().scope).toEqual({ kind: 'drafting', tool: first.type })
|
|
239
|
+
} finally {
|
|
240
|
+
emitter.off('tool:cancel', cancel)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
})
|