@pascal-app/editor 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type AnyNode,
|
|
5
|
-
type AnyNodeId,
|
|
6
|
-
useLiveNodeOverrides,
|
|
7
|
-
useLiveTransforms,
|
|
8
|
-
useScene,
|
|
9
|
-
} from '@pascal-app/core'
|
|
10
|
-
import { useViewer } from '@pascal-app/viewer'
|
|
11
|
-
import { createPortal, type ThreeEvent, useThree } from '@react-three/fiber'
|
|
12
|
-
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
13
|
-
import { OrthographicCamera, Plane, Vector2, Vector3 } from 'three'
|
|
14
|
-
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
15
|
-
import useEditor from '../../store/use-editor'
|
|
16
|
-
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
17
|
-
import {
|
|
18
|
-
CORNER_OFFSET,
|
|
19
|
-
classifyParticipant,
|
|
20
|
-
collectParticipants,
|
|
21
|
-
computeGroupBox,
|
|
22
|
-
expandToComponent,
|
|
23
|
-
levelFrame,
|
|
24
|
-
type Vec2,
|
|
25
|
-
} from './group-transform-shared'
|
|
26
|
-
import {
|
|
27
|
-
ARROW_COLOR,
|
|
28
|
-
ARROW_HOVER_COLOR,
|
|
29
|
-
ARROW_SCALE,
|
|
30
|
-
createMoveCrossHandleGeometry,
|
|
31
|
-
swallowNextClick,
|
|
32
|
-
useArrowMaterial,
|
|
33
|
-
} from './node-arrow-handles'
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Group-move gizmo — the 4-way cross sibling of `GroupRotateHandle`. When 2+
|
|
37
|
-
* transformable nodes are selected, a single move cross appears at the
|
|
38
|
-
* selection's front-left bounding-box corner (the rotate gizmo sits on the
|
|
39
|
-
* right). Dragging it slides every selected node by the same ground-plane
|
|
40
|
-
* delta; connected (unselected) wall/fence endpoints follow so junctions stay
|
|
41
|
-
* welded. Commits the whole slide in one batched `updateNodes` (one undo).
|
|
42
|
-
*/
|
|
43
|
-
export function GroupMoveHandle() {
|
|
44
|
-
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
45
|
-
const levelId = useViewer((s) => s.selection.levelId)
|
|
46
|
-
const mode = useEditor((s) => s.mode)
|
|
47
|
-
const movingNode = useEditor((s) => s.movingNode)
|
|
48
|
-
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
49
|
-
const nodes = useScene((s) => s.nodes)
|
|
50
|
-
|
|
51
|
-
const participantIds = useMemo(
|
|
52
|
-
() =>
|
|
53
|
-
selectedIds.filter(
|
|
54
|
-
(id) => classifyParticipant(nodes[id as AnyNodeId], levelId, nodes) !== null,
|
|
55
|
-
),
|
|
56
|
-
[selectedIds, levelId, nodes],
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
// Gate on the explicit selection, but move the full connected wall/fence
|
|
60
|
-
// component so attached structure slides rigidly as one piece.
|
|
61
|
-
const fullIds = useMemo(
|
|
62
|
-
() => expandToComponent(participantIds, nodes, levelId),
|
|
63
|
-
[participantIds, levelId, nodes],
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
const shouldRender =
|
|
67
|
-
participantIds.length >= 2 && mode !== 'delete' && !movingNode && !isFloorplanHovered
|
|
68
|
-
|
|
69
|
-
if (!shouldRender) return null
|
|
70
|
-
return <GroupMoveHandleInner ids={fullIds} key={fullIds.join(',')} />
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function GroupMoveHandleInner({ ids }: { ids: string[] }) {
|
|
74
|
-
const { camera, raycaster, gl, scene } = useThree()
|
|
75
|
-
const arrowGeometry = useMemo(() => createMoveCrossHandleGeometry(), [])
|
|
76
|
-
const arrowMaterial = useArrowMaterial()
|
|
77
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
78
|
-
const [isDragging, setIsDragging] = useState(false)
|
|
79
|
-
// Live ground-plane delta so the gizmo rides along with the group it moves.
|
|
80
|
-
const [liveDelta, setLiveDelta] = useState<Vec2>([0, 0])
|
|
81
|
-
const dragCleanupRef = useRef<(() => void) | null>(null)
|
|
82
|
-
const frozenCorner = useRef<Vector3 | null>(null)
|
|
83
|
-
|
|
84
|
-
useEffect(() => {
|
|
85
|
-
arrowMaterial.color.set(isHovered ? ARROW_HOVER_COLOR : ARROW_COLOR)
|
|
86
|
-
}, [arrowMaterial, isHovered])
|
|
87
|
-
useEffect(() => () => arrowGeometry.dispose(), [arrowGeometry])
|
|
88
|
-
useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
|
|
89
|
-
useEffect(() => () => dragCleanupRef.current?.(), [])
|
|
90
|
-
|
|
91
|
-
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
92
|
-
const scale = (isHovered ? 1.12 : 1) * zoom * ARROW_SCALE
|
|
93
|
-
|
|
94
|
-
// Front-left bbox corner at mid-height (mirrors the rotate gizmo on the
|
|
95
|
-
// right), plus the group's base Y for the ground drag plane.
|
|
96
|
-
const rest = useMemo(() => {
|
|
97
|
-
const box = computeGroupBox(ids)
|
|
98
|
-
if (!box) return null
|
|
99
|
-
const corner = new Vector3(
|
|
100
|
-
box.min.x - CORNER_OFFSET,
|
|
101
|
-
(box.min.y + box.max.y) / 2,
|
|
102
|
-
box.max.z + CORNER_OFFSET,
|
|
103
|
-
)
|
|
104
|
-
return { corner, baseY: box.min.y }
|
|
105
|
-
}, [ids])
|
|
106
|
-
|
|
107
|
-
if (!rest) return null
|
|
108
|
-
const baseCorner = isDragging && frozenCorner.current ? frozenCorner.current : rest.corner
|
|
109
|
-
const corner: [number, number, number] = [
|
|
110
|
-
baseCorner.x + liveDelta[0],
|
|
111
|
-
baseCorner.y,
|
|
112
|
-
baseCorner.z + liveDelta[1],
|
|
113
|
-
]
|
|
114
|
-
|
|
115
|
-
const activate = (event: ThreeEvent<PointerEvent>) => {
|
|
116
|
-
event.stopPropagation()
|
|
117
|
-
suppressBoxSelectForPointer(event)
|
|
118
|
-
frozenCorner.current = rest.corner.clone()
|
|
119
|
-
const planeY = rest.baseY
|
|
120
|
-
|
|
121
|
-
// Snapshot selected participants + connected wall/fence neighbours.
|
|
122
|
-
const levelId = useViewer.getState().selection.levelId
|
|
123
|
-
const { starts, links } = collectParticipants(ids, useScene.getState().nodes, levelId)
|
|
124
|
-
if (starts.length === 0) return
|
|
125
|
-
|
|
126
|
-
// Placements are stored in the level frame, so convert each world-space
|
|
127
|
-
// ground-plane hit into that frame before measuring the delta. Frozen at
|
|
128
|
-
// drag-start; `frameOrigin` lets us map the local delta back to world for
|
|
129
|
-
// the gizmo's own travel (it's portalled to the scene root).
|
|
130
|
-
const { matrix: frame, inverse: frameInv } = levelFrame(levelId)
|
|
131
|
-
const frameOrigin = new Vector3().applyMatrix4(frame)
|
|
132
|
-
|
|
133
|
-
// Horizontal drag plane at the group's base.
|
|
134
|
-
const plane = new Plane(new Vector3(0, 1, 0), -planeY)
|
|
135
|
-
const ndc = new Vector2()
|
|
136
|
-
const setNDC = (clientX: number, clientY: number) => {
|
|
137
|
-
const rect = gl.domElement.getBoundingClientRect()
|
|
138
|
-
ndc.set(
|
|
139
|
-
((clientX - rect.left) / rect.width) * 2 - 1,
|
|
140
|
-
-((clientY - rect.top) / rect.height) * 2 + 1,
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
setNDC(event.nativeEvent.clientX, event.nativeEvent.clientY)
|
|
145
|
-
raycaster.setFromCamera(ndc, camera)
|
|
146
|
-
const hit = new Vector3()
|
|
147
|
-
if (!raycaster.ray.intersectPlane(plane, hit)) return
|
|
148
|
-
const startLocal = hit.clone().applyMatrix4(frameInv)
|
|
149
|
-
|
|
150
|
-
document.body.style.cursor = 'grabbing'
|
|
151
|
-
sfxEmitter.emit('sfx:item-pick')
|
|
152
|
-
useViewer.getState().setInputDragging(true)
|
|
153
|
-
useScene.temporal.getState().pause()
|
|
154
|
-
setIsDragging(true)
|
|
155
|
-
|
|
156
|
-
// Snap the slide to the active grid step so the group lands on the grid
|
|
157
|
-
// (Shift bypasses for free movement). Snapping the delta keeps the
|
|
158
|
-
// selection's internal layout intact — grid-aligned items stay aligned.
|
|
159
|
-
const step = useEditor.getState().gridSnapStep
|
|
160
|
-
let lastSnap: Vec2 | null = null
|
|
161
|
-
|
|
162
|
-
const onMove = (e: PointerEvent) => {
|
|
163
|
-
setNDC(e.clientX, e.clientY)
|
|
164
|
-
raycaster.setFromCamera(ndc, camera)
|
|
165
|
-
const moveHit = new Vector3()
|
|
166
|
-
if (!raycaster.ray.intersectPlane(plane, moveHit)) return
|
|
167
|
-
const moveLocal = moveHit.applyMatrix4(frameInv)
|
|
168
|
-
const snap = !e.shiftKey && step > 0
|
|
169
|
-
const dx = snap
|
|
170
|
-
? Math.round((moveLocal.x - startLocal.x) / step) * step
|
|
171
|
-
: moveLocal.x - startLocal.x
|
|
172
|
-
const dz = snap
|
|
173
|
-
? Math.round((moveLocal.z - startLocal.z) / step) * step
|
|
174
|
-
: moveLocal.z - startLocal.z
|
|
175
|
-
|
|
176
|
-
// Ticker on each grid-cell crossing, like single-item placement.
|
|
177
|
-
if (snap && (!lastSnap || lastSnap[0] !== dx || lastSnap[1] !== dz)) {
|
|
178
|
-
sfxEmitter.emit('sfx:grid-snap')
|
|
179
|
-
lastSnap = [dx, dz]
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const overrideEntries: Array<readonly [string, Record<string, unknown>]> = []
|
|
183
|
-
const liveTransforms = useLiveTransforms.getState()
|
|
184
|
-
for (const s of starts) {
|
|
185
|
-
if (s.kind === 'endpoint') {
|
|
186
|
-
overrideEntries.push([
|
|
187
|
-
s.id,
|
|
188
|
-
{
|
|
189
|
-
start: [s.start[0] + dx, s.start[1] + dz],
|
|
190
|
-
end: [s.end[0] + dx, s.end[1] + dz],
|
|
191
|
-
},
|
|
192
|
-
])
|
|
193
|
-
} else {
|
|
194
|
-
// Slide on the floor: XZ shift, Y and rotation untouched.
|
|
195
|
-
const position: [number, number, number] = [
|
|
196
|
-
s.position[0] + dx,
|
|
197
|
-
s.position[1],
|
|
198
|
-
s.position[2] + dz,
|
|
199
|
-
]
|
|
200
|
-
overrideEntries.push([s.id, { position }])
|
|
201
|
-
if (s.kind === 'scalar') {
|
|
202
|
-
liveTransforms.set(s.id, { position, rotation: s.rotation })
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
useScene.getState().markDirty(s.id)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Shared endpoints of connected neighbours follow by the same delta so
|
|
209
|
-
// the junction stays welded; the far end stays put.
|
|
210
|
-
for (const l of links) {
|
|
211
|
-
overrideEntries.push([
|
|
212
|
-
l.id,
|
|
213
|
-
{
|
|
214
|
-
start: l.startLinked ? [l.start[0] + dx, l.start[1] + dz] : l.start,
|
|
215
|
-
end: l.endLinked ? [l.end[0] + dx, l.end[1] + dz] : l.end,
|
|
216
|
-
},
|
|
217
|
-
])
|
|
218
|
-
useScene.getState().markDirty(l.id)
|
|
219
|
-
}
|
|
220
|
-
useLiveNodeOverrides.getState().setMany(overrideEntries)
|
|
221
|
-
|
|
222
|
-
// Gizmo rides the group in world space; map the level-frame delta back out.
|
|
223
|
-
const worldDelta = new Vector3(dx, 0, dz).applyMatrix4(frame).sub(frameOrigin)
|
|
224
|
-
setLiveDelta([worldDelta.x, worldDelta.z])
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const affectedIds: AnyNodeId[] = [...starts.map((s) => s.id), ...links.map((l) => l.id)]
|
|
228
|
-
const clearLivePreviews = () => {
|
|
229
|
-
const overrides = useLiveNodeOverrides.getState()
|
|
230
|
-
const liveTransforms = useLiveTransforms.getState()
|
|
231
|
-
for (const id of affectedIds) {
|
|
232
|
-
overrides.clear(id)
|
|
233
|
-
liveTransforms.clear(id)
|
|
234
|
-
useScene.getState().markDirty(id)
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const cleanup = () => {
|
|
239
|
-
window.removeEventListener('pointermove', onMove)
|
|
240
|
-
window.removeEventListener('pointerup', onUp)
|
|
241
|
-
window.removeEventListener('pointercancel', onCancel)
|
|
242
|
-
if (document.body.style.cursor === 'grabbing') document.body.style.cursor = ''
|
|
243
|
-
useScene.temporal.getState().resume()
|
|
244
|
-
useViewer.getState().setInputDragging(false)
|
|
245
|
-
setIsDragging(false)
|
|
246
|
-
setLiveDelta([0, 0])
|
|
247
|
-
frozenCorner.current = null
|
|
248
|
-
dragCleanupRef.current = null
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const commitFromOverrides = () => {
|
|
252
|
-
const overrides = useLiveNodeOverrides.getState()
|
|
253
|
-
const updates: { id: AnyNodeId; data: Partial<AnyNode> }[] = []
|
|
254
|
-
for (const id of affectedIds) {
|
|
255
|
-
const patch = overrides.get(id)
|
|
256
|
-
if (patch) updates.push({ id, data: patch as Partial<AnyNode> })
|
|
257
|
-
}
|
|
258
|
-
return updates
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const onUp = () => {
|
|
262
|
-
// Eat the click that follows pointer-up so the selection manager doesn't
|
|
263
|
-
// treat it as a canvas click and clear the multi-selection.
|
|
264
|
-
swallowNextClick()
|
|
265
|
-
sfxEmitter.emit('sfx:item-place')
|
|
266
|
-
const updates = commitFromOverrides()
|
|
267
|
-
// Resume before the commit so the single batched `updateNodes` is the one
|
|
268
|
-
// tracked set — collapsing the whole group move into one undo.
|
|
269
|
-
useScene.temporal.getState().resume()
|
|
270
|
-
if (updates.length > 0) useScene.getState().updateNodes(updates)
|
|
271
|
-
clearLivePreviews()
|
|
272
|
-
cleanup()
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const onCancel = () => {
|
|
276
|
-
clearLivePreviews()
|
|
277
|
-
cleanup()
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
dragCleanupRef.current = () => {
|
|
281
|
-
clearLivePreviews()
|
|
282
|
-
cleanup()
|
|
283
|
-
}
|
|
284
|
-
for (const id of affectedIds) {
|
|
285
|
-
useLiveTransforms.getState().clear(id)
|
|
286
|
-
}
|
|
287
|
-
window.addEventListener('pointermove', onMove)
|
|
288
|
-
window.addEventListener('pointerup', onUp)
|
|
289
|
-
window.addEventListener('pointercancel', onCancel)
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return createPortal(
|
|
293
|
-
<group position={corner} scale={scale}>
|
|
294
|
-
<mesh
|
|
295
|
-
frustumCulled={false}
|
|
296
|
-
geometry={arrowGeometry}
|
|
297
|
-
material={arrowMaterial}
|
|
298
|
-
onPointerDown={activate}
|
|
299
|
-
onPointerEnter={(event) => {
|
|
300
|
-
event.stopPropagation()
|
|
301
|
-
setIsHovered(true)
|
|
302
|
-
if (document.body.style.cursor !== 'grabbing') document.body.style.cursor = 'move'
|
|
303
|
-
}}
|
|
304
|
-
onPointerLeave={(event) => {
|
|
305
|
-
event.stopPropagation()
|
|
306
|
-
setIsHovered(false)
|
|
307
|
-
if (document.body.style.cursor === 'move') document.body.style.cursor = ''
|
|
308
|
-
}}
|
|
309
|
-
renderOrder={1010}
|
|
310
|
-
/>
|
|
311
|
-
</group>,
|
|
312
|
-
scene,
|
|
313
|
-
)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export default GroupMoveHandle
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import useEditor from '../../../store/use-editor'
|
|
4
|
-
import { PanelSection } from '../controls/panel-section'
|
|
5
|
-
import { Input } from '../primitives/input'
|
|
6
|
-
import { PanelWrapper } from './panel-wrapper'
|
|
7
|
-
|
|
8
|
-
function buildDefaultCustomMaterial() {
|
|
9
|
-
return {
|
|
10
|
-
preset: 'custom' as const,
|
|
11
|
-
properties: {
|
|
12
|
-
color: '#ffffff',
|
|
13
|
-
roughness: 0.5,
|
|
14
|
-
metalness: 0,
|
|
15
|
-
opacity: 1,
|
|
16
|
-
transparent: false,
|
|
17
|
-
side: 'front' as const,
|
|
18
|
-
},
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function PaintPanel() {
|
|
23
|
-
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
|
|
24
|
-
const activePaintTarget = useEditor((state) => state.activePaintTarget)
|
|
25
|
-
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
|
|
26
|
-
const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen)
|
|
27
|
-
|
|
28
|
-
const customMaterial =
|
|
29
|
-
activePaintMaterial?.material?.properties && !activePaintMaterial.materialPreset
|
|
30
|
-
? activePaintMaterial.material
|
|
31
|
-
: null
|
|
32
|
-
|
|
33
|
-
if (!customMaterial) return null
|
|
34
|
-
|
|
35
|
-
const currentProps = customMaterial.properties ?? buildDefaultCustomMaterial().properties
|
|
36
|
-
|
|
37
|
-
const updateCustomMaterial = (
|
|
38
|
-
updates: Partial<typeof currentProps>,
|
|
39
|
-
nextTransparent = currentProps.transparent,
|
|
40
|
-
) => {
|
|
41
|
-
setActivePaintMaterial({
|
|
42
|
-
material: {
|
|
43
|
-
preset: 'custom',
|
|
44
|
-
properties: {
|
|
45
|
-
...currentProps,
|
|
46
|
-
...updates,
|
|
47
|
-
transparent: nextTransparent,
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
sourceTarget: activePaintMaterial?.sourceTarget ?? activePaintTarget,
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return (
|
|
55
|
-
<PanelWrapper onClose={() => setPaintPanelOpen(false)} title="Material" width={320}>
|
|
56
|
-
<PanelSection title="Custom Material">
|
|
57
|
-
<div className="space-y-3">
|
|
58
|
-
<div className="space-y-2">
|
|
59
|
-
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
60
|
-
Color
|
|
61
|
-
</label>
|
|
62
|
-
<div className="flex items-center gap-2">
|
|
63
|
-
<input
|
|
64
|
-
className="h-10 w-14 cursor-pointer rounded-md border border-input bg-transparent"
|
|
65
|
-
onChange={(e) => updateCustomMaterial({ color: e.target.value })}
|
|
66
|
-
type="color"
|
|
67
|
-
value={currentProps.color}
|
|
68
|
-
/>
|
|
69
|
-
<Input
|
|
70
|
-
onChange={(e) => updateCustomMaterial({ color: e.target.value })}
|
|
71
|
-
value={currentProps.color}
|
|
72
|
-
/>
|
|
73
|
-
</div>
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
<div className="space-y-2">
|
|
77
|
-
<div className="flex items-center justify-between">
|
|
78
|
-
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
79
|
-
Roughness
|
|
80
|
-
</label>
|
|
81
|
-
<span className="font-mono text-muted-foreground text-xs">
|
|
82
|
-
{currentProps.roughness.toFixed(2)}
|
|
83
|
-
</span>
|
|
84
|
-
</div>
|
|
85
|
-
<input
|
|
86
|
-
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
87
|
-
max={1}
|
|
88
|
-
min={0}
|
|
89
|
-
onChange={(e) =>
|
|
90
|
-
updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })
|
|
91
|
-
}
|
|
92
|
-
step={0.01}
|
|
93
|
-
type="range"
|
|
94
|
-
value={currentProps.roughness}
|
|
95
|
-
/>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
<div className="space-y-2">
|
|
99
|
-
<div className="flex items-center justify-between">
|
|
100
|
-
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
101
|
-
Metalness
|
|
102
|
-
</label>
|
|
103
|
-
<span className="font-mono text-muted-foreground text-xs">
|
|
104
|
-
{currentProps.metalness.toFixed(2)}
|
|
105
|
-
</span>
|
|
106
|
-
</div>
|
|
107
|
-
<input
|
|
108
|
-
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
109
|
-
max={1}
|
|
110
|
-
min={0}
|
|
111
|
-
onChange={(e) =>
|
|
112
|
-
updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })
|
|
113
|
-
}
|
|
114
|
-
step={0.01}
|
|
115
|
-
type="range"
|
|
116
|
-
value={currentProps.metalness}
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
<div className="space-y-2">
|
|
121
|
-
<div className="flex items-center justify-between">
|
|
122
|
-
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
123
|
-
Opacity
|
|
124
|
-
</label>
|
|
125
|
-
<span className="font-mono text-muted-foreground text-xs">
|
|
126
|
-
{currentProps.opacity.toFixed(2)}
|
|
127
|
-
</span>
|
|
128
|
-
</div>
|
|
129
|
-
<input
|
|
130
|
-
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
|
131
|
-
max={1}
|
|
132
|
-
min={0}
|
|
133
|
-
onChange={(e) => {
|
|
134
|
-
const opacity = Number.parseFloat(e.target.value)
|
|
135
|
-
updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
|
136
|
-
}}
|
|
137
|
-
step={0.01}
|
|
138
|
-
type="range"
|
|
139
|
-
value={currentProps.opacity}
|
|
140
|
-
/>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
<div className="space-y-2">
|
|
144
|
-
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
|
145
|
-
Side
|
|
146
|
-
</label>
|
|
147
|
-
<select
|
|
148
|
-
className="h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30"
|
|
149
|
-
onChange={(e) =>
|
|
150
|
-
updateCustomMaterial({ side: e.target.value as 'front' | 'back' | 'double' })
|
|
151
|
-
}
|
|
152
|
-
value={currentProps.side}
|
|
153
|
-
>
|
|
154
|
-
<option value="front">Front</option>
|
|
155
|
-
<option value="back">Back</option>
|
|
156
|
-
<option value="double">Double</option>
|
|
157
|
-
</select>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</PanelSection>
|
|
161
|
-
</PanelWrapper>
|
|
162
|
-
)
|
|
163
|
-
}
|
package/src/lib/level-name.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { LevelNode } from '@pascal-app/core'
|
|
2
|
-
|
|
3
|
-
export function getDefaultLevelName(level: number): string {
|
|
4
|
-
if (level === 0) return 'Ground Floor'
|
|
5
|
-
if (level > 0) return `Floor ${level}`
|
|
6
|
-
return `Basement ${-level}`
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function getLevelDisplayName(level: Pick<LevelNode, 'name' | 'level'>): string {
|
|
10
|
-
return level.name || getDefaultLevelName(level.level)
|
|
11
|
-
}
|