@pascal-app/editor 1.0.0-beta.4 → 1.0.0
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/alignment-3d-guide-layer.tsx +12 -3
- package/src/components/editor/bake-exporter.tsx +1 -0
- 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 +17 -5
- package/src/components/editor/elevation-3d-guide-layer.tsx +3 -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 +182 -57
- package/src/components/editor/floating-action-menu.tsx +55 -55
- package/src/components/editor/floorplan-background-selection.test.ts +31 -4
- package/src/components/editor/floorplan-background-selection.ts +27 -7
- package/src/components/editor/floorplan-panel.tsx +81 -57
- package/src/components/editor/grid.tsx +12 -7
- package/src/components/editor/group-actions.ts +34 -50
- package/src/components/editor/group-floating-action-menu.tsx +30 -15
- 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/linear-resize-drag.ts +67 -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 +18 -1
- package/src/components/editor/index.tsx +252 -46
- package/src/components/editor/measurement-pill.tsx +12 -12
- package/src/components/editor/node-action-menu.tsx +42 -1
- package/src/components/editor/node-arrow-handles.tsx +47 -58
- package/src/components/editor/opening-guides-3d-layer.tsx +13 -4
- package/src/components/editor/selection-manager.tsx +132 -47
- package/src/components/editor/site-edge-labels.tsx +2 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +309 -41
- 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 -232
- package/src/components/editor/use-floorplan-background-placement.ts +53 -5
- package/src/components/editor/wall-measurement-label.tsx +3 -2
- package/src/components/editor/wall-move-side-handles.tsx +299 -7
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +23 -16
- 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-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 +204 -114
- 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/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/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 +494 -0
- package/src/components/tools/item/placement-strategies.ts +192 -24
- 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 +6 -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 +26 -4
- package/src/components/tools/item/use-placement-coordinator.tsx +447 -180
- package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
- package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
- package/src/components/tools/registry/move-registry-node-tool.tsx +306 -110
- 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 +63 -8
- 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 +45 -8
- package/src/components/tools/site/terrain-brush-cursor.tsx +2 -1
- 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 +527 -4
- package/src/components/tools/wall/wall-drafting.ts +55 -462
- 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 +12 -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/scene-material-list.tsx +6 -5
- package/src/components/ui/controls/segmented-control.tsx +8 -1
- package/src/components/ui/controls/slider-control.tsx +19 -3
- 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 +23 -6
- 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 +263 -0
- package/src/components/ui/panels/multi-selection-panel.tsx +78 -26
- 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/panel-wrapper.tsx +228 -12
- 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 +29 -162
- package/src/components/ui/panels/reference-panel.tsx +31 -1
- 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/items-panel/function-tree-panel.tsx +7 -2
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +390 -36
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +31 -23
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +140 -0
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +109 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +128 -46
- 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/tree-node.test.ts +8 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +22 -7
- package/src/components/ui/sidebar/tab-bar.tsx +9 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
- package/src/components/viewer/floorplan-compass-button.tsx +50 -0
- package/src/components/viewer/floorplan-preview-geometry.test.ts +107 -0
- package/src/components/viewer/floorplan-preview-geometry.ts +281 -0
- package/src/components/viewer/floorplan-preview-navigation.test.ts +43 -0
- package/src/components/viewer/floorplan-preview-navigation.ts +62 -0
- package/src/components/viewer/floorplan-preview.tsx +1069 -0
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +140 -0
- package/src/components/viewer/viewer-scene-header.tsx +2 -1
- package/src/components/viewer/viewer-stage-modes.test.ts +38 -0
- package/src/components/viewer/viewer-stage-modes.ts +49 -0
- package/src/components/viewer/viewer-stage-switcher.tsx +95 -0
- package/src/components/viewer/viewer-stage.test.tsx +47 -0
- package/src/components/viewer/viewer-stage.tsx +221 -0
- package/src/components/viewer-overlay.tsx +13 -30
- 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 +122 -1
- package/src/hooks/use-auto-save.ts +119 -13
- package/src/hooks/use-ceiling-events.test.ts +122 -0
- 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 +164 -63
- package/src/hooks/use-registered-tool-enabled.ts +20 -0
- package/src/hooks/use-save-shortcut.ts +33 -0
- package/src/index.tsx +109 -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/contextual-help.test.ts +8 -0
- package/src/lib/contextual-help.ts +8 -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/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 +397 -1
- package/src/lib/floorplan/floorplan-export.tsx +111 -16
- 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.test.ts +47 -0
- package/src/lib/floorplan/floorplan-readonly.ts +85 -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/glb-export.test.ts +1169 -21
- package/src/lib/glb-export.ts +629 -101
- package/src/lib/grid-event-presentation.ts +16 -0
- package/src/lib/history.test.ts +523 -77
- package/src/lib/history.ts +133 -14
- package/src/lib/host-tree-children.test.ts +27 -0
- package/src/lib/host-tree-children.ts +79 -0
- package/src/lib/inspector-card-mode.test.ts +92 -0
- package/src/lib/inspector-card-mode.ts +60 -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-print-export.test.ts +484 -0
- package/src/lib/level-print-export.ts +526 -0
- 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/measurement-parser.ts +3 -0
- package/src/lib/measurements.test.ts +4 -0
- package/src/lib/model-export.ts +29 -0
- package/src/lib/paint-preview-owner.ts +71 -0
- package/src/lib/paint-scope.test.ts +58 -2
- package/src/lib/paint-scope.ts +8 -2
- 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 +366 -0
- package/src/lib/portable-export.ts +969 -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 +330 -0
- package/src/lib/print-shell-compiler-manifold-worker.ts +187 -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 +30 -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.ts +22 -8
- package/src/lib/selection-routing.test.ts +125 -6
- package/src/lib/selection-routing.ts +34 -1
- package/src/lib/session-groups.test.ts +88 -0
- package/src/lib/session-groups.ts +201 -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/structured-clone-fallback.test.ts +45 -0
- package/src/lib/structured-clone-fallback.ts +24 -0
- package/src/lib/usdz-export.ts +38 -0
- 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-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-session-groups.ts +103 -0
- package/src/components/tools/roof/roof-tool.tsx +0 -656
- package/src/components/ui/helpers/roof-helper.tsx +0 -14
|
@@ -7,28 +7,27 @@ import { useFrame } from '@react-three/fiber'
|
|
|
7
7
|
import { useCallback, useMemo, useRef } from 'react'
|
|
8
8
|
import * as THREE from 'three'
|
|
9
9
|
import { resolveOverlayPolicy } from '../../lib/interaction/overlay-policy'
|
|
10
|
+
import { canCreateSessionGroup, selectionIntersectsSessionGroup } from '../../lib/session-groups'
|
|
10
11
|
import useEditor from '../../store/use-editor'
|
|
11
12
|
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
13
|
+
import useSessionGroups, {
|
|
14
|
+
groupCurrentSelection,
|
|
15
|
+
ungroupCurrentSelection,
|
|
16
|
+
} from '../../store/use-session-groups'
|
|
12
17
|
import { deleteSelection, duplicateSelectionAndPickUp, startGroupPickUp } from './group-actions'
|
|
13
18
|
import { classifyParticipant, computeGroupBox, expandToComponent } from './group-transform-shared'
|
|
14
19
|
import { NodeActionMenu } from './node-action-menu'
|
|
15
20
|
import { useMeshSettleEpoch } from './use-mesh-settle-epoch'
|
|
16
21
|
|
|
17
|
-
// Matches the single-node FloatingActionMenu's zoom-compensation constants.
|
|
18
22
|
const REF_ORTHO_ZOOM = 50
|
|
19
23
|
const REF_CAMERA_DISTANCE = 12
|
|
20
24
|
const MIN_MENU_SCALE = 0.6
|
|
21
25
|
const MAX_MENU_SCALE = 1.4
|
|
22
|
-
// Clearance above the group's bbox top so the pill doesn't sit on the meshes.
|
|
23
26
|
const MENU_Y_OFFSET = 0.42
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
|
-
* Floating
|
|
27
|
-
*
|
|
28
|
-
* sole-selection only). Anchored above the selection's bounding-box center;
|
|
29
|
-
* every action targets the whole selection: Move picks the group up (it rides
|
|
30
|
-
* the cursor until a click places it), Duplicate clones the selection and
|
|
31
|
-
* picks the clones up, Delete removes everything selected.
|
|
29
|
+
* Floating pill for MULTI-selection in 3D: Move, Group, Ungroup, Duplicate, Delete.
|
|
30
|
+
* Group/Ungroup are session-only (Ctrl/Cmd+G / Ctrl/Cmd+Shift+G).
|
|
32
31
|
*/
|
|
33
32
|
export function GroupFloatingActionMenu() {
|
|
34
33
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
@@ -37,8 +36,7 @@ export function GroupFloatingActionMenu() {
|
|
|
37
36
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
38
37
|
const movingNode = useMovingNode()
|
|
39
38
|
const nodes = useScene((s) => s.nodes)
|
|
40
|
-
|
|
41
|
-
// pill never competes with the live action — same policy as the 1-node menu.
|
|
39
|
+
const sessionGroups = useSessionGroups((s) => s.groups)
|
|
42
40
|
const scope = useInteractionScope((s) => s.scope)
|
|
43
41
|
const menuStepBack = resolveOverlayPolicy(scope).conflictingControls === 'hidden'
|
|
44
42
|
|
|
@@ -55,9 +53,18 @@ export function GroupFloatingActionMenu() {
|
|
|
55
53
|
[selectedIds, levelId, nodes],
|
|
56
54
|
)
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
56
|
+
const liveIds = useMemo(() => new Set(Object.keys(nodes)), [nodes])
|
|
57
|
+
// Always offer Group when multi-select is not already exactly a session group.
|
|
58
|
+
const showGroup = useMemo(
|
|
59
|
+
() => canCreateSessionGroup(sessionGroups, selectedIds, liveIds),
|
|
60
|
+
[sessionGroups, selectedIds, liveIds],
|
|
61
|
+
)
|
|
62
|
+
// Ungroup when any selected member is in a session group.
|
|
63
|
+
const showUngroup = useMemo(
|
|
64
|
+
() => selectionIntersectsSessionGroup(sessionGroups, selectedIds, liveIds),
|
|
65
|
+
[sessionGroups, selectedIds, liveIds],
|
|
66
|
+
)
|
|
67
|
+
|
|
61
68
|
const meshEpoch = useMeshSettleEpoch(nodes)
|
|
62
69
|
const anchor = useMemo(() => {
|
|
63
70
|
void meshEpoch
|
|
@@ -73,8 +80,6 @@ export function GroupFloatingActionMenu() {
|
|
|
73
80
|
}, [participantIds, nodes, levelId, meshEpoch])
|
|
74
81
|
|
|
75
82
|
useFrame((state) => {
|
|
76
|
-
// Scale the HTML pill with camera zoom / distance so it feels anchored to
|
|
77
|
-
// the world — mirrors the single-node menu.
|
|
78
83
|
if (!(menuScaleRef.current && groupRef.current)) return
|
|
79
84
|
const raw =
|
|
80
85
|
state.camera instanceof THREE.OrthographicCamera
|
|
@@ -92,6 +97,14 @@ export function GroupFloatingActionMenu() {
|
|
|
92
97
|
event.stopPropagation()
|
|
93
98
|
startGroupPickUp()
|
|
94
99
|
}, [])
|
|
100
|
+
const handleGroup = useCallback((event: React.MouseEvent) => {
|
|
101
|
+
event.stopPropagation()
|
|
102
|
+
groupCurrentSelection()
|
|
103
|
+
}, [])
|
|
104
|
+
const handleUngroup = useCallback((event: React.MouseEvent) => {
|
|
105
|
+
event.stopPropagation()
|
|
106
|
+
ungroupCurrentSelection()
|
|
107
|
+
}, [])
|
|
95
108
|
const handleDuplicate = useCallback((event: React.MouseEvent) => {
|
|
96
109
|
event.stopPropagation()
|
|
97
110
|
duplicateSelectionAndPickUp()
|
|
@@ -119,9 +132,11 @@ export function GroupFloatingActionMenu() {
|
|
|
119
132
|
<NodeActionMenu
|
|
120
133
|
onDelete={handleDelete}
|
|
121
134
|
onDuplicate={handleDuplicate}
|
|
135
|
+
onGroup={showGroup ? handleGroup : undefined}
|
|
122
136
|
onMove={handleMove}
|
|
123
137
|
onPointerDown={stopPointer}
|
|
124
138
|
onPointerUp={stopPointer}
|
|
139
|
+
onUngroup={showUngroup ? handleUngroup : undefined}
|
|
125
140
|
/>
|
|
126
141
|
</div>
|
|
127
142
|
</Html>
|
|
@@ -32,9 +32,12 @@ import {
|
|
|
32
32
|
collectParticipants,
|
|
33
33
|
computeGroupBox,
|
|
34
34
|
expandToComponent,
|
|
35
|
+
type GroupPlanBounds,
|
|
36
|
+
groupPlanBounds,
|
|
35
37
|
levelFrame,
|
|
36
|
-
|
|
38
|
+
planBoundsCenter,
|
|
37
39
|
rotateGroupSnapshots,
|
|
40
|
+
rotatePlanBounds,
|
|
38
41
|
translateGroupPatches,
|
|
39
42
|
type Vec2,
|
|
40
43
|
} from './group-transform-shared'
|
|
@@ -88,6 +91,9 @@ export function armGroupMove3d(args: {
|
|
|
88
91
|
affectedIds: AnyNodeId[]
|
|
89
92
|
candidates: ReturnType<typeof collectAlignmentAnchors>
|
|
90
93
|
restAnchors: ReturnType<typeof bboxCornerAnchors>
|
|
94
|
+
startBounds: GroupPlanBounds
|
|
95
|
+
restBounds: GroupPlanBounds
|
|
96
|
+
rotation: number
|
|
91
97
|
restCenter: Vec2
|
|
92
98
|
plane: Plane
|
|
93
99
|
startLocal: Vector3
|
|
@@ -130,14 +136,14 @@ export function armGroupMove3d(args: {
|
|
|
130
136
|
if (n && !movingIdSet.has(nid)) staticNodes[nid] = n
|
|
131
137
|
}
|
|
132
138
|
const candidates = collectAlignmentAnchors(staticNodes, '', levelId)
|
|
133
|
-
const
|
|
134
|
-
|
|
139
|
+
const restBounds = groupPlanBounds(restBox, starts, frameInv)
|
|
140
|
+
if (!restBounds) return null
|
|
135
141
|
const restAnchors = bboxCornerAnchors(
|
|
136
142
|
'group-move',
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
restBounds.minX,
|
|
144
|
+
restBounds.minZ,
|
|
145
|
+
restBounds.maxX,
|
|
146
|
+
restBounds.maxZ,
|
|
141
147
|
)
|
|
142
148
|
|
|
143
149
|
for (const id of affectedIds) {
|
|
@@ -154,9 +160,11 @@ export function armGroupMove3d(args: {
|
|
|
154
160
|
nodeId,
|
|
155
161
|
handle: GROUP_MOVE_DRAG_LABEL,
|
|
156
162
|
})
|
|
157
|
-
// Rotation pivot for mid-drag R/T — the
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
// Rotation pivot for mid-drag R/T — the START footprint's center, the same
|
|
164
|
+
// point the idle keyboard rotate and the rotate gizmos orbit. Fixed for the
|
|
165
|
+
// whole session: the snapshots are start placements, and `applyDelta` adds
|
|
166
|
+
// the live drag delta on top of them.
|
|
167
|
+
const restCenter = planBoundsCenter(restBounds)
|
|
160
168
|
|
|
161
169
|
return {
|
|
162
170
|
starts,
|
|
@@ -164,6 +172,9 @@ export function armGroupMove3d(args: {
|
|
|
164
172
|
affectedIds,
|
|
165
173
|
candidates,
|
|
166
174
|
restAnchors,
|
|
175
|
+
startBounds: restBounds,
|
|
176
|
+
restBounds,
|
|
177
|
+
rotation: 0,
|
|
167
178
|
restCenter,
|
|
168
179
|
plane,
|
|
169
180
|
startLocal,
|
|
@@ -245,18 +256,22 @@ export function armGroupMove3d(args: {
|
|
|
245
256
|
// current delta — the carried group turns exactly like the idle keyboard
|
|
246
257
|
// rotate, and the commit stays a single updateNodes.
|
|
247
258
|
const rotateSession = (s: Session, direction: 1 | -1) => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
{ x: s.restCenter[0], z: s.restCenter[1] },
|
|
252
|
-
-direction * (Math.PI / 4),
|
|
253
|
-
)
|
|
259
|
+
const pivot = { x: s.restCenter[0], z: s.restCenter[1] }
|
|
260
|
+
const delta = -direction * (Math.PI / 4)
|
|
261
|
+
const rotated = rotateGroupSnapshots(s.starts, s.links, pivot, delta)
|
|
254
262
|
s.starts = rotated.starts
|
|
255
263
|
s.links = rotated.links
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
264
|
+
// Re-fit from the start footprint at the accumulated angle: rotating the
|
|
265
|
+
// previous axis-aligned fit would inflate the box every step.
|
|
266
|
+
s.rotation += delta
|
|
267
|
+
s.restBounds = rotatePlanBounds(s.startBounds, pivot, s.rotation)
|
|
268
|
+
s.restAnchors = bboxCornerAnchors(
|
|
269
|
+
'group-move',
|
|
270
|
+
s.restBounds.minX,
|
|
271
|
+
s.restBounds.minZ,
|
|
272
|
+
s.restBounds.maxX,
|
|
273
|
+
s.restBounds.maxZ,
|
|
274
|
+
)
|
|
260
275
|
sfxEmitter.emit('sfx:item-rotate')
|
|
261
276
|
applyDelta(s, s.lastDelta?.[0] ?? 0, s.lastDelta?.[1] ?? 0)
|
|
262
277
|
}
|
|
@@ -369,7 +384,22 @@ export function armGroupMove3d(args: {
|
|
|
369
384
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
370
385
|
const key = e.key.toLowerCase()
|
|
371
386
|
if ((key === 'r' || key === 't') && !e.metaKey && !e.ctrlKey && !e.altKey && !e.shiftKey) {
|
|
372
|
-
|
|
387
|
+
// Armed but still under the drag threshold: engage first (exactly what
|
|
388
|
+
// the next pointer-move would do) so the rotation lands inside this
|
|
389
|
+
// session. Falling through to the global idle arm instead would write
|
|
390
|
+
// the scene behind snapshots already captured here, and the first
|
|
391
|
+
// `applyDelta` would republish them — undoing the rotation.
|
|
392
|
+
if (!session) {
|
|
393
|
+
session = engage()
|
|
394
|
+
if (!session) {
|
|
395
|
+
// No plane hit yet: swallow the chord and keep the gesture armed so
|
|
396
|
+
// the next pointer-move can still engage; the idle arm must not run
|
|
397
|
+
// behind the snapshots captured here.
|
|
398
|
+
e.preventDefault()
|
|
399
|
+
e.stopPropagation()
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
}
|
|
373
403
|
e.preventDefault()
|
|
374
404
|
e.stopPropagation()
|
|
375
405
|
rotateSession(session, key === 'r' ? 1 : -1)
|
|
@@ -4,7 +4,10 @@ import { z } from 'zod'
|
|
|
4
4
|
import {
|
|
5
5
|
classifyParticipant,
|
|
6
6
|
collectParticipants,
|
|
7
|
+
planBoundsCenter,
|
|
7
8
|
rotateGroupPatches,
|
|
9
|
+
rotateGroupSnapshots,
|
|
10
|
+
rotatePlanBounds,
|
|
8
11
|
translateGroupPatches,
|
|
9
12
|
} from './group-transform-shared'
|
|
10
13
|
|
|
@@ -40,6 +43,34 @@ function registerElevatorTestKind() {
|
|
|
40
43
|
} as AnyNodeDefinition)
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
// A level holding one of each rigid placement shape: an item ([x,y,z] rotation)
|
|
47
|
+
// and a column (numeric rotation).
|
|
48
|
+
function placedNodes() {
|
|
49
|
+
return {
|
|
50
|
+
building_test: { id: 'building_test', type: 'building', children: ['level_test'] },
|
|
51
|
+
level_test: {
|
|
52
|
+
id: 'level_test',
|
|
53
|
+
type: 'level',
|
|
54
|
+
parentId: 'building_test',
|
|
55
|
+
children: ['item_chair', 'column_post'],
|
|
56
|
+
},
|
|
57
|
+
item_chair: {
|
|
58
|
+
id: 'item_chair',
|
|
59
|
+
type: 'item',
|
|
60
|
+
parentId: 'level_test',
|
|
61
|
+
position: [1, 0, 3],
|
|
62
|
+
rotation: [0, 0.5, 0],
|
|
63
|
+
},
|
|
64
|
+
column_post: {
|
|
65
|
+
id: 'column_post',
|
|
66
|
+
type: 'column',
|
|
67
|
+
parentId: 'level_test',
|
|
68
|
+
position: [4, 0, -1],
|
|
69
|
+
rotation: 1.25,
|
|
70
|
+
},
|
|
71
|
+
} as unknown as Record<string, AnyNode>
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
describe('group transform participants', () => {
|
|
44
75
|
beforeAll(() => {
|
|
45
76
|
registerBuildingScopedTestKind()
|
|
@@ -362,6 +393,53 @@ describe('group transform participants', () => {
|
|
|
362
393
|
expect(lampPatch.position).toEqual([3, 2.4, 2])
|
|
363
394
|
})
|
|
364
395
|
|
|
396
|
+
test('translate patches carry the snapshot rotation for vec3 and scalar kinds', () => {
|
|
397
|
+
const { starts } = collectParticipants(
|
|
398
|
+
['item_chair', 'column_post'],
|
|
399
|
+
placedNodes(),
|
|
400
|
+
'level_test',
|
|
401
|
+
)
|
|
402
|
+
const patches = Object.fromEntries(translateGroupPatches(starts, [], 1, 2))
|
|
403
|
+
|
|
404
|
+
expect(patches.item_chair).toEqual({ position: [2, 0, 5], rotation: [0, 0.5, 0] })
|
|
405
|
+
expect(patches.column_post).toEqual({ position: [5, 0, 1], rotation: 1.25 })
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
test('a mid-drag rotation survives the next translate re-publish', () => {
|
|
409
|
+
const { starts } = collectParticipants(
|
|
410
|
+
['item_chair', 'column_post'],
|
|
411
|
+
placedNodes(),
|
|
412
|
+
'level_test',
|
|
413
|
+
)
|
|
414
|
+
// What a mid-drag R does: turn the snapshots, then re-apply the live delta.
|
|
415
|
+
const rotated = rotateGroupSnapshots(starts, [], { x: 0, z: 0 }, Math.PI / 2)
|
|
416
|
+
const patches = Object.fromEntries(
|
|
417
|
+
translateGroupPatches(rotated.starts, rotated.links, 1, 2),
|
|
418
|
+
) as Record<string, { position: number[]; rotation: number[] | number }>
|
|
419
|
+
|
|
420
|
+
// Orbited 90° in the atan2 x→z sense ((x, z) → (-z, x)), then slid.
|
|
421
|
+
expect(patches.item_chair!.position[0]).toBeCloseTo(-2)
|
|
422
|
+
expect(patches.item_chair!.position[2]).toBeCloseTo(3)
|
|
423
|
+
expect(patches.column_post!.position[0]).toBeCloseTo(2)
|
|
424
|
+
expect(patches.column_post!.position[2]).toBeCloseTo(6)
|
|
425
|
+
// …and the facings turned with it instead of reverting to the pre-drag yaw.
|
|
426
|
+
expect((patches.item_chair!.rotation as number[])[1]).toBeCloseTo(0.5 - Math.PI / 2)
|
|
427
|
+
expect(patches.column_post!.rotation as number).toBeCloseTo(1.25 - Math.PI / 2)
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('rotating the plan bounds keeps the footprint centred on the pivot', () => {
|
|
431
|
+
const bounds = { minX: 0, minZ: 0, maxX: 4, maxZ: 2 }
|
|
432
|
+
const [pivotX, pivotZ] = planBoundsCenter(bounds)
|
|
433
|
+
const rotated = rotatePlanBounds(bounds, { x: pivotX, z: pivotZ }, Math.PI / 2)
|
|
434
|
+
|
|
435
|
+
expect(rotated.minX).toBeCloseTo(1)
|
|
436
|
+
expect(rotated.maxX).toBeCloseTo(3)
|
|
437
|
+
expect(rotated.minZ).toBeCloseTo(-1)
|
|
438
|
+
expect(rotated.maxZ).toBeCloseTo(3)
|
|
439
|
+
expect(planBoundsCenter(rotated)[0]).toBeCloseTo(pivotX)
|
|
440
|
+
expect(planBoundsCenter(rotated)[1]).toBeCloseTo(pivotZ)
|
|
441
|
+
})
|
|
442
|
+
|
|
365
443
|
test('supports legacy level-parented elevators already loaded in the editor', () => {
|
|
366
444
|
const nodes = {
|
|
367
445
|
building_test: {
|
|
@@ -387,12 +387,11 @@ export function rotateGroupSnapshots(
|
|
|
387
387
|
return { starts: rotatedStarts, links: rotatedLinks }
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
export type GroupPlanBounds = { minX: number; minZ: number; maxX: number; maxZ: number }
|
|
391
|
+
|
|
390
392
|
// Level-frame XZ extents of the participant DATA — the mesh-free sibling of
|
|
391
|
-
// `computeGroupBox`, used when meshes aren't mounted yet
|
|
392
|
-
|
|
393
|
-
export function participantExtents(
|
|
394
|
-
starts: ParticipantStart[],
|
|
395
|
-
): { minX: number; minZ: number; maxX: number; maxZ: number } | null {
|
|
393
|
+
// `computeGroupBox`, used when meshes aren't mounted yet.
|
|
394
|
+
function participantExtents(starts: ParticipantStart[]): GroupPlanBounds | null {
|
|
396
395
|
let minX = Number.POSITIVE_INFINITY
|
|
397
396
|
let minZ = Number.POSITIVE_INFINITY
|
|
398
397
|
let maxX = Number.NEGATIVE_INFINITY
|
|
@@ -419,8 +418,73 @@ export function participantExtents(
|
|
|
419
418
|
return { minX, minZ, maxX, maxZ }
|
|
420
419
|
}
|
|
421
420
|
|
|
421
|
+
// The one footprint every group transform measures itself against: the
|
|
422
|
+
// selection's mounted meshes (world box, converted into the level frame) with
|
|
423
|
+
// the participant DATA extents as the fallback when the meshes aren't up yet
|
|
424
|
+
// (Duplicate picks up its clones a frame before their renderers mount). Anchor
|
|
425
|
+
// points alone sit metres inside a wide selection's real footprint, so a
|
|
426
|
+
// gesture that pivots on the data extents orbits a different point than the
|
|
427
|
+
// idle keyboard rotate and the rotate gizmos, which both use the mesh box.
|
|
428
|
+
export function groupPlanBounds(
|
|
429
|
+
box: Box3 | null,
|
|
430
|
+
starts: ParticipantStart[],
|
|
431
|
+
frameInv: Matrix4,
|
|
432
|
+
): GroupPlanBounds | null {
|
|
433
|
+
if (!box) return participantExtents(starts)
|
|
434
|
+
const min = box.min.clone().applyMatrix4(frameInv)
|
|
435
|
+
const max = box.max.clone().applyMatrix4(frameInv)
|
|
436
|
+
return {
|
|
437
|
+
minX: Math.min(min.x, max.x),
|
|
438
|
+
minZ: Math.min(min.z, max.z),
|
|
439
|
+
maxX: Math.max(min.x, max.x),
|
|
440
|
+
maxZ: Math.max(min.z, max.z),
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export const planBoundsCenter = (b: GroupPlanBounds): Vec2 => [
|
|
445
|
+
(b.minX + b.maxX) / 2,
|
|
446
|
+
(b.minZ + b.maxZ) / 2,
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
// Re-seed the footprint after a mid-gesture rotation by orbiting the box
|
|
450
|
+
// corners and re-fitting an axis-aligned box. Re-measuring the rotated DATA
|
|
451
|
+
// extents instead would slide the centre off the pivot the snapshots turned
|
|
452
|
+
// around, dragging the alignment anchors away from the group under the cursor.
|
|
453
|
+
export function rotatePlanBounds(
|
|
454
|
+
b: GroupPlanBounds,
|
|
455
|
+
center: { x: number; z: number },
|
|
456
|
+
delta: number,
|
|
457
|
+
): GroupPlanBounds {
|
|
458
|
+
const cos = Math.cos(delta)
|
|
459
|
+
const sin = Math.sin(delta)
|
|
460
|
+
let minX = Number.POSITIVE_INFINITY
|
|
461
|
+
let minZ = Number.POSITIVE_INFINITY
|
|
462
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
463
|
+
let maxZ = Number.NEGATIVE_INFINITY
|
|
464
|
+
const corners: Vec2[] = [
|
|
465
|
+
[b.minX, b.minZ],
|
|
466
|
+
[b.maxX, b.minZ],
|
|
467
|
+
[b.maxX, b.maxZ],
|
|
468
|
+
[b.minX, b.maxZ],
|
|
469
|
+
]
|
|
470
|
+
for (const [x, z] of corners) {
|
|
471
|
+
const dx = x - center.x
|
|
472
|
+
const dz = z - center.z
|
|
473
|
+
const rx = center.x + dx * cos - dz * sin
|
|
474
|
+
const rz = center.z + dx * sin + dz * cos
|
|
475
|
+
minX = Math.min(minX, rx)
|
|
476
|
+
minZ = Math.min(minZ, rz)
|
|
477
|
+
maxX = Math.max(maxX, rx)
|
|
478
|
+
maxZ = Math.max(maxZ, rz)
|
|
479
|
+
}
|
|
480
|
+
return { minX, minZ, maxX, maxZ }
|
|
481
|
+
}
|
|
482
|
+
|
|
422
483
|
// Rigid group slide: shift every participant (and each linked neighbour's
|
|
423
|
-
// shared endpoint) by the same level-frame XZ delta. Y
|
|
484
|
+
// shared endpoint) by the same level-frame XZ delta. Y is untouched; the
|
|
485
|
+
// snapshot's rotation rides along because a mid-gesture R/T turns the
|
|
486
|
+
// SNAPSHOTS — dropping it here would republish (and commit) the pre-rotation
|
|
487
|
+
// facing, orbiting the layout while every member keeps its old bearing.
|
|
424
488
|
export function translateGroupPatches(
|
|
425
489
|
starts: ParticipantStart[],
|
|
426
490
|
links: LinkedNeighbor[],
|
|
@@ -437,7 +501,10 @@ export function translateGroupPatches(
|
|
|
437
501
|
if (s.holes) patch.holes = s.holes.map((hole) => hole.map(shift))
|
|
438
502
|
patches.push([s.id, patch])
|
|
439
503
|
} else {
|
|
440
|
-
|
|
504
|
+
const position: Vec3 = [s.position[0] + dx, s.position[1], s.position[2] + dz]
|
|
505
|
+
const rotation =
|
|
506
|
+
s.kind === 'vec3' ? ([s.rotation[0], s.rotation[1], s.rotation[2]] as Vec3) : s.rotation
|
|
507
|
+
patches.push([s.id, { position, rotation }])
|
|
441
508
|
}
|
|
442
509
|
}
|
|
443
510
|
for (const l of links) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { _roots, act, createRoot, extend } from '@react-three/fiber'
|
|
3
|
+
import { createElement } from 'react'
|
|
4
|
+
import { BoxGeometry, Mesh, MeshBasicMaterial, Raycaster, Vector3, type WebGLRenderer } from 'three'
|
|
5
|
+
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
6
|
+
import useEditor from '../../../store/use-editor'
|
|
7
|
+
import { hitAreaRaycast, InvisibleHandleHitArea } from './handle-arrow'
|
|
8
|
+
|
|
9
|
+
extend({ Mesh })
|
|
10
|
+
|
|
11
|
+
test('an occluded handle does not sort ahead of a nearer scene body', () => {
|
|
12
|
+
const geometry = new BoxGeometry(0.5, 0.5, 0.5)
|
|
13
|
+
const material = new MeshBasicMaterial()
|
|
14
|
+
const body = new Mesh(geometry, material)
|
|
15
|
+
body.position.z = 1
|
|
16
|
+
body.updateMatrixWorld()
|
|
17
|
+
const handle = new Mesh(geometry, material)
|
|
18
|
+
handle.position.z = 2
|
|
19
|
+
handle.raycast = hitAreaRaycast
|
|
20
|
+
handle.updateMatrixWorld()
|
|
21
|
+
|
|
22
|
+
const raycaster = new Raycaster(new Vector3(0, 0, 0), new Vector3(0, 0, 1))
|
|
23
|
+
const hits = raycaster.intersectObjects([body, handle], false)
|
|
24
|
+
|
|
25
|
+
expect(hits[0]?.object).toBe(body)
|
|
26
|
+
expect(hits.find((hit) => hit.object === handle)?.distance).toBeGreaterThan(
|
|
27
|
+
hits.find((hit) => hit.object === body)?.distance ?? Number.POSITIVE_INFINITY,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
geometry.dispose()
|
|
31
|
+
material.dispose()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('mounted handles disable synchronously during placement drags and restore after release', async () => {
|
|
35
|
+
const previousDragMode = useEditor.getState().placementDragMode
|
|
36
|
+
const actGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
37
|
+
const previousAct = actGlobal.IS_REACT_ACT_ENVIRONMENT
|
|
38
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = true
|
|
39
|
+
const canvas = new EventTarget() as HTMLCanvasElement
|
|
40
|
+
const root = createRoot(canvas)
|
|
41
|
+
const geometry = new BoxGeometry(1, 1, 1)
|
|
42
|
+
const material = new MeshBasicNodeMaterial()
|
|
43
|
+
try {
|
|
44
|
+
useEditor.setState({ placementDragMode: true })
|
|
45
|
+
await root.configure({
|
|
46
|
+
gl: { render() {}, setSize() {}, setPixelRatio() {} } as unknown as WebGLRenderer,
|
|
47
|
+
frameloop: 'never',
|
|
48
|
+
dpr: 1,
|
|
49
|
+
size: { width: 100, height: 100, top: 0, left: 0 },
|
|
50
|
+
})
|
|
51
|
+
const render = (scale: number) =>
|
|
52
|
+
createElement(InvisibleHandleHitArea, {
|
|
53
|
+
geometry,
|
|
54
|
+
material,
|
|
55
|
+
scale,
|
|
56
|
+
onPointerDown() {},
|
|
57
|
+
onPointerEnter() {},
|
|
58
|
+
onPointerLeave() {},
|
|
59
|
+
})
|
|
60
|
+
await act(async () => root.render(render(1)))
|
|
61
|
+
const scene = _roots.get(canvas)!.store.getState().scene
|
|
62
|
+
const handle = scene.children[0] as Mesh
|
|
63
|
+
scene.updateMatrixWorld(true)
|
|
64
|
+
const raycaster = new Raycaster(new Vector3(0, 0, 2), new Vector3(0, 0, -1))
|
|
65
|
+
raycaster.layers.enableAll()
|
|
66
|
+
expect(raycaster.intersectObject(handle)).toHaveLength(0)
|
|
67
|
+
useEditor.setState({ placementDragMode: false })
|
|
68
|
+
expect(handle.raycast).toBe(hitAreaRaycast)
|
|
69
|
+
expect(raycaster.intersectObject(handle).length).toBeGreaterThan(0)
|
|
70
|
+
useEditor.setState({ placementDragMode: true })
|
|
71
|
+
expect(raycaster.intersectObject(handle)).toHaveLength(0)
|
|
72
|
+
await act(async () => root.render(render(2)))
|
|
73
|
+
expect(raycaster.intersectObject(handle)).toHaveLength(0)
|
|
74
|
+
await act(async () => root.render(null))
|
|
75
|
+
const detachedRaycast = handle.raycast
|
|
76
|
+
useEditor.setState({ placementDragMode: false })
|
|
77
|
+
expect(handle.raycast).toBe(detachedRaycast)
|
|
78
|
+
} finally {
|
|
79
|
+
await act(async () => root.render(null))
|
|
80
|
+
_roots.delete(canvas)
|
|
81
|
+
geometry.dispose()
|
|
82
|
+
material.dispose()
|
|
83
|
+
useEditor.setState({ placementDragMode: previousDragMode })
|
|
84
|
+
actGlobal.IS_REACT_ACT_ENVIRONMENT = previousAct
|
|
85
|
+
}
|
|
86
|
+
})
|