@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.
Files changed (262) hide show
  1. package/package.json +12 -9
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
  3. package/src/components/editor/bake-exporter.tsx +47 -0
  4. package/src/components/editor/custom-camera-controls.tsx +63 -25
  5. package/src/components/editor/editor-layout-mobile.tsx +5 -0
  6. package/src/components/editor/editor-layout-v2.tsx +18 -0
  7. package/src/components/editor/export-manager.tsx +71 -63
  8. package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
  9. package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
  10. package/src/components/editor/first-person/build-collider-world.ts +32 -5
  11. package/src/components/editor/first-person-controls.tsx +118 -7
  12. package/src/components/editor/floating-action-menu.tsx +410 -33
  13. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  14. package/src/components/editor/floorplan-background-selection.test.ts +58 -0
  15. package/src/components/editor/floorplan-background-selection.ts +3 -2
  16. package/src/components/editor/floorplan-panel.tsx +1804 -849
  17. package/src/components/editor/grid.tsx +166 -39
  18. package/src/components/editor/group-actions.ts +457 -0
  19. package/src/components/editor/group-floating-action-menu.tsx +130 -0
  20. package/src/components/editor/group-move-3d.ts +397 -0
  21. package/src/components/editor/group-rotate-handle.tsx +102 -64
  22. package/src/components/editor/group-selection-box-3d.tsx +173 -0
  23. package/src/components/editor/group-transform-shared.test.ts +192 -1
  24. package/src/components/editor/group-transform-shared.ts +240 -7
  25. package/src/components/editor/handles/handle-arrow.tsx +81 -23
  26. package/src/components/editor/handles/use-handle-drag.ts +37 -4
  27. package/src/components/editor/index.tsx +248 -27
  28. package/src/components/editor/measurement-pill.tsx +62 -22
  29. package/src/components/editor/node-action-menu.tsx +14 -1
  30. package/src/components/editor/node-arrow-handles.tsx +366 -173
  31. package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
  32. package/src/components/editor/quick-measurement-card.tsx +70 -0
  33. package/src/components/editor/quick-measurement-hud.tsx +28 -0
  34. package/src/components/editor/riser-diagram-panel.tsx +137 -0
  35. package/src/components/editor/selection-manager.tsx +812 -466
  36. package/src/components/editor/site-edge-labels.tsx +4 -4
  37. package/src/components/editor/slab-hole-highlights.tsx +13 -5
  38. package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
  39. package/src/components/editor/three-context-bridge.ts +22 -0
  40. package/src/components/editor/thumbnail-generator.tsx +136 -43
  41. package/src/components/editor/use-floorplan-background-placement.ts +113 -45
  42. package/src/components/editor/use-floorplan-scene-data.ts +5 -5
  43. package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
  44. package/src/components/editor/wall-measurement-label.tsx +3 -13
  45. package/src/components/editor/wall-move-side-handles.tsx +36 -21
  46. package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
  47. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
  48. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
  49. package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
  50. package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
  51. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
  52. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
  53. package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
  54. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
  55. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
  56. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
  57. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
  58. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
  59. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
  60. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
  61. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
  62. package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
  63. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
  64. package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
  65. package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
  66. package/src/components/systems/selection-affordance-manager.tsx +38 -0
  67. package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
  68. package/src/components/systems/zone/zone-system.tsx +42 -13
  69. package/src/components/tools/elevator/elevator-tool.tsx +34 -8
  70. package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
  71. package/src/components/tools/fence/fence-drafting.ts +80 -8
  72. package/src/components/tools/item/move-tool.tsx +17 -13
  73. package/src/components/tools/item/placement-math.test.ts +13 -1
  74. package/src/components/tools/item/placement-math.ts +28 -2
  75. package/src/components/tools/item/placement-strategies.ts +246 -5
  76. package/src/components/tools/item/placement-types.ts +13 -1
  77. package/src/components/tools/item/use-draft-node.ts +31 -1
  78. package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
  79. package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
  80. package/src/components/tools/roof/roof-tool.tsx +320 -94
  81. package/src/components/tools/select/box-select-state.ts +18 -7
  82. package/src/components/tools/select/box-select-tool.tsx +176 -40
  83. package/src/components/tools/select/marquee-geometry.test.ts +161 -0
  84. package/src/components/tools/select/marquee-geometry.ts +155 -0
  85. package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
  86. package/src/components/tools/select/select-candidates.ts +1 -1
  87. package/src/components/tools/shared/cursor-sphere.tsx +62 -26
  88. package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
  89. package/src/components/tools/shared/facing-indicator.tsx +87 -0
  90. package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
  91. package/src/components/tools/shared/placement-box.tsx +183 -1
  92. package/src/components/tools/shared/polygon-editor.tsx +276 -35
  93. package/src/components/tools/site/site-boundary-editor.tsx +88 -36
  94. package/src/components/tools/stair/stair-defaults.ts +1 -0
  95. package/src/components/tools/stair/stair-tool.tsx +82 -11
  96. package/src/components/tools/tool-manager.tsx +86 -25
  97. package/src/components/tools/wall/wall-drafting.test.ts +330 -0
  98. package/src/components/tools/wall/wall-drafting.ts +180 -103
  99. package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
  100. package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
  101. package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
  102. package/src/components/tools/zone/zone-tool.tsx +82 -88
  103. package/src/components/ui/action-menu/camera-actions.tsx +24 -17
  104. package/src/components/ui/action-menu/control-modes.tsx +40 -93
  105. package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
  106. package/src/components/ui/action-menu/index.tsx +2 -4
  107. package/src/components/ui/action-menu/measurement-control.tsx +188 -0
  108. package/src/components/ui/action-menu/structure-tools.tsx +22 -13
  109. package/src/components/ui/action-menu/view-toggles.tsx +42 -90
  110. package/src/components/ui/command-palette/editor-commands.tsx +2 -2
  111. package/src/components/ui/command-palette/index.tsx +4 -3
  112. package/src/components/ui/controls/material-paint-panel.tsx +86 -17
  113. package/src/components/ui/controls/material-picker.tsx +83 -152
  114. package/src/components/ui/controls/material-properties-editor.tsx +108 -0
  115. package/src/components/ui/controls/metric-control.tsx +118 -44
  116. package/src/components/ui/controls/scene-material-list.tsx +247 -0
  117. package/src/components/ui/controls/slider-control.tsx +88 -38
  118. package/src/components/ui/floating-level-selector.tsx +1 -1
  119. package/src/components/ui/helpers/building-helper.tsx +10 -23
  120. package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
  121. package/src/components/ui/helpers/helper-manager.tsx +227 -14
  122. package/src/components/ui/helpers/item-helper.tsx +28 -32
  123. package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
  124. package/src/components/ui/helpers/roof-helper.tsx +10 -12
  125. package/src/components/ui/icon-ref.tsx +47 -0
  126. package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
  127. package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
  128. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  129. package/src/components/ui/panels/node-display.ts +17 -17
  130. package/src/components/ui/panels/panel-manager.tsx +22 -13
  131. package/src/components/ui/panels/panel-wrapper.tsx +24 -3
  132. package/src/components/ui/panels/parametric-inspector.tsx +15 -2
  133. package/src/components/ui/panels/reference-panel.tsx +54 -19
  134. package/src/components/ui/primitives/shortcut-token.tsx +39 -3
  135. package/src/components/ui/primitives/sidebar.tsx +1 -0
  136. package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
  137. package/src/components/ui/sidebar/icon-rail.tsx +50 -31
  138. package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
  139. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
  140. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
  141. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
  142. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
  144. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
  145. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
  146. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
  147. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
  148. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
  149. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
  150. package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
  151. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
  152. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  153. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
  154. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
  155. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
  156. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
  157. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
  158. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  159. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
  160. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
  161. package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
  162. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
  163. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
  164. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
  165. package/src/components/ui/sidebar/tab-bar.tsx +42 -28
  166. package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
  167. package/src/components/ui/snap-target-badge.test.tsx +40 -0
  168. package/src/components/ui/snap-target-badge.tsx +88 -0
  169. package/src/components/viewer-overlay.tsx +85 -10
  170. package/src/components/viewer-zone-system.tsx +6 -1
  171. package/src/hooks/use-auto-save.test.ts +14 -0
  172. package/src/hooks/use-auto-save.ts +54 -7
  173. package/src/hooks/use-ceiling-events.ts +3 -2
  174. package/src/hooks/use-drag-action.ts +4 -1
  175. package/src/hooks/use-keyboard.ts +385 -40
  176. package/src/index.tsx +219 -6
  177. package/src/lib/active-placement-surface.ts +35 -0
  178. package/src/lib/ceiling-plan-snap.ts +24 -0
  179. package/src/lib/contextual-help.test.ts +112 -0
  180. package/src/lib/contextual-help.ts +144 -0
  181. package/src/lib/continuation.ts +64 -0
  182. package/src/lib/direct-manipulation.test.ts +192 -0
  183. package/src/lib/direct-manipulation.ts +109 -0
  184. package/src/lib/editor-api.ts +23 -35
  185. package/src/lib/floorplan/apply-alignment.test.ts +25 -0
  186. package/src/lib/floorplan/apply-alignment.ts +18 -12
  187. package/src/lib/floorplan/floorplan-export.tsx +364 -0
  188. package/src/lib/floorplan/geometry.ts +53 -0
  189. package/src/lib/floorplan/index.ts +3 -0
  190. package/src/lib/floorplan/items.ts +5 -2
  191. package/src/lib/floorplan/plan-coords.ts +21 -0
  192. package/src/lib/fresh-planar-placement.test.ts +240 -3
  193. package/src/lib/fresh-planar-placement.ts +68 -1
  194. package/src/lib/glb-export.test.ts +441 -0
  195. package/src/lib/glb-export.ts +874 -0
  196. package/src/lib/history.ts +9 -0
  197. package/src/lib/interaction/hot-set.test.ts +133 -0
  198. package/src/lib/interaction/hot-set.ts +67 -0
  199. package/src/lib/interaction/overlay-policy.test.ts +51 -0
  200. package/src/lib/interaction/overlay-policy.ts +59 -0
  201. package/src/lib/interaction/scope.ts +211 -0
  202. package/src/lib/level-selection.ts +2 -2
  203. package/src/lib/material-paint.ts +36 -49
  204. package/src/lib/measurement-kind.test.ts +30 -0
  205. package/src/lib/measurement-kind.ts +19 -0
  206. package/src/lib/measurement-label.test.ts +47 -0
  207. package/src/lib/measurement-label.ts +68 -0
  208. package/src/lib/measurement-parser.ts +116 -0
  209. package/src/lib/measurements.test.ts +183 -0
  210. package/src/lib/measurements.ts +199 -0
  211. package/src/lib/paint-scope.test.ts +454 -0
  212. package/src/lib/paint-scope.ts +461 -0
  213. package/src/lib/placement-drag-release.ts +23 -0
  214. package/src/lib/planar-cursor-placement.test.ts +57 -0
  215. package/src/lib/plugin-panels.test.ts +19 -0
  216. package/src/lib/plugin-panels.ts +91 -0
  217. package/src/lib/quick-measurement.test.ts +77 -0
  218. package/src/lib/quick-measurement.ts +109 -0
  219. package/src/lib/roof-duplication.ts +6 -6
  220. package/src/lib/roof-hover-outline-proxy.ts +22 -0
  221. package/src/lib/roof-wall-hit.ts +164 -0
  222. package/src/lib/scene-clipboard.test.ts +196 -0
  223. package/src/lib/scene-clipboard.ts +73 -8
  224. package/src/lib/scene.ts +28 -5
  225. package/src/lib/selection-routing.test.ts +298 -0
  226. package/src/lib/selection-routing.ts +175 -0
  227. package/src/lib/sfx/index.ts +1 -0
  228. package/src/lib/sfx/movement-tick.test.ts +65 -0
  229. package/src/lib/sfx/movement-tick.ts +18 -0
  230. package/src/lib/sfx-bus.ts +50 -13
  231. package/src/lib/sfx-player.test.ts +124 -0
  232. package/src/lib/sfx-player.ts +111 -66
  233. package/src/lib/slab-plan-snap.test.ts +93 -0
  234. package/src/lib/slab-plan-snap.ts +108 -0
  235. package/src/lib/snapping-mode.test.ts +138 -0
  236. package/src/lib/snapping-mode.ts +179 -0
  237. package/src/lib/stair-duplication.ts +4 -4
  238. package/src/lib/surface-plan-snap.test.ts +71 -0
  239. package/src/lib/surface-plan-snap.ts +256 -0
  240. package/src/lib/use-linear-display.ts +40 -0
  241. package/src/lib/world-grid-snap.ts +37 -5
  242. package/src/store/use-direct-manipulation-feedback.ts +20 -0
  243. package/src/store/use-editor.tsx +484 -114
  244. package/src/store/use-facing-pose.ts +44 -0
  245. package/src/store/use-fence-curve-draft.ts +21 -0
  246. package/src/store/use-floorplan-draft-preview.ts +101 -0
  247. package/src/store/use-floorplan-marquee.ts +50 -0
  248. package/src/store/use-interaction-scope.test.ts +186 -0
  249. package/src/store/use-interaction-scope.ts +132 -0
  250. package/src/store/use-measurement-draft.test.ts +530 -0
  251. package/src/store/use-measurement-draft.ts +543 -0
  252. package/src/store/use-opening-guides.ts +41 -0
  253. package/src/store/use-placement-preview.ts +11 -3
  254. package/src/store/use-quick-measurement-hud.test.ts +53 -0
  255. package/src/store/use-quick-measurement-hud.ts +77 -0
  256. package/src/store/use-segment-draft-chain.ts +28 -0
  257. package/src/store/use-stair-build-preview.ts +43 -0
  258. package/src/store/use-wall-snap-indicator.ts +2 -0
  259. package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
  260. package/src/components/editor/group-move-handle.tsx +0 -316
  261. package/src/components/ui/panels/paint-panel.tsx +0 -163
  262. 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
- }
@@ -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
- }