@pascal-app/editor 0.7.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/package.json +12 -13
  2. package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
  3. package/src/components/editor/custom-camera-controls.tsx +755 -25
  4. package/src/components/editor/editor-layout-mobile.tsx +3 -3
  5. package/src/components/editor/editor-layout-v2.tsx +62 -54
  6. package/src/components/editor/export-manager.tsx +32 -3
  7. package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
  8. package/src/components/editor/first-person/build-collider-world.ts +203 -87
  9. package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
  10. package/src/components/editor/first-person-controls.tsx +836 -31
  11. package/src/components/editor/floating-action-menu.tsx +287 -196
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floorplan-panel.tsx +5837 -12006
  14. package/src/components/editor/grid.tsx +51 -17
  15. package/src/components/editor/group-move-handle.tsx +316 -0
  16. package/src/components/editor/group-rotate-handle.tsx +382 -0
  17. package/src/components/editor/group-transform-shared.test.ts +205 -0
  18. package/src/components/editor/group-transform-shared.ts +256 -0
  19. package/src/components/editor/handles/handle-arrow.tsx +523 -0
  20. package/src/components/editor/handles/use-handle-drag.ts +213 -0
  21. package/src/components/editor/index.tsx +234 -190
  22. package/src/components/editor/measurement-pill.tsx +75 -0
  23. package/src/components/editor/node-arrow-handles.tsx +1453 -0
  24. package/src/components/editor/selection-manager.tsx +435 -158
  25. package/src/components/editor/site-edge-labels.tsx +44 -20
  26. package/src/components/editor/slab-hole-highlights.tsx +466 -0
  27. package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
  28. package/src/components/editor/thumbnail-generator.tsx +27 -64
  29. package/src/components/editor/use-floorplan-background-placement.ts +122 -35
  30. package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
  31. package/src/components/editor/use-floorplan-scene-data.ts +14 -2
  32. package/src/components/editor/wall-measurement-label.tsx +11 -6
  33. package/src/components/editor/wall-move-side-handles.tsx +855 -0
  34. package/src/components/editor/wall-opening-highlights.tsx +181 -0
  35. package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
  36. package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
  37. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
  38. package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
  39. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
  40. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
  41. package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
  42. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
  43. package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
  44. package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
  45. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
  46. package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
  47. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
  48. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
  49. package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
  50. package/src/components/editor-2d/svg-paths.ts +10 -2
  51. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
  52. package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
  53. package/src/components/systems/roof/roof-edit-system.tsx +86 -12
  54. package/src/components/systems/stair/stair-edit-system.tsx +2 -2
  55. package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
  56. package/src/components/systems/zone/zone-system.tsx +0 -0
  57. package/src/components/tools/elevator/elevator-defaults.ts +8 -0
  58. package/src/components/tools/elevator/elevator-tool.tsx +349 -0
  59. package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
  60. package/src/components/tools/fence/fence-drafting.ts +64 -22
  61. package/src/components/tools/item/move-tool.tsx +40 -107
  62. package/src/components/tools/item/placement-math.test.ts +10 -0
  63. package/src/components/tools/item/placement-math.ts +33 -9
  64. package/src/components/tools/item/placement-strategies.ts +201 -25
  65. package/src/components/tools/item/placement-types.ts +10 -1
  66. package/src/components/tools/item/use-draft-node.ts +20 -2
  67. package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
  68. package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
  69. package/src/components/tools/roof/roof-tool.tsx +108 -12
  70. package/src/components/tools/select/box-select-state.ts +77 -0
  71. package/src/components/tools/select/box-select-tool.tsx +324 -543
  72. package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
  73. package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
  74. package/src/components/tools/select/select-candidates.test.ts +109 -0
  75. package/src/components/tools/select/select-candidates.ts +85 -0
  76. package/src/components/tools/shared/affordance-dispatch.ts +30 -0
  77. package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
  78. package/src/components/tools/shared/floor-stack-preview.ts +25 -0
  79. package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
  80. package/src/components/tools/shared/placement-box-geometry.ts +123 -0
  81. package/src/components/tools/shared/placement-box.tsx +123 -0
  82. package/src/components/tools/shared/polygon-editor.tsx +617 -167
  83. package/src/components/tools/shared/segment-angle.ts +57 -1
  84. package/src/components/tools/site/site-boundary-editor.tsx +464 -12
  85. package/src/components/tools/site/site-flag-model.ts +18 -0
  86. package/src/components/tools/stair/stair-defaults.ts +1 -1
  87. package/src/components/tools/stair/stair-tool.tsx +269 -35
  88. package/src/components/tools/tool-manager.tsx +172 -56
  89. package/src/components/tools/wall/wall-drafting.ts +98 -98
  90. package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
  91. package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
  92. package/src/components/tools/zone/zone-tool.tsx +36 -11
  93. package/src/components/ui/action-menu/action-button.tsx +21 -1
  94. package/src/components/ui/action-menu/camera-actions.tsx +0 -0
  95. package/src/components/ui/action-menu/control-modes.tsx +5 -76
  96. package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
  97. package/src/components/ui/action-menu/index.tsx +33 -196
  98. package/src/components/ui/action-menu/structure-tools.tsx +13 -88
  99. package/src/components/ui/action-menu/view-toggles.tsx +262 -37
  100. package/src/components/ui/command-palette/editor-commands.tsx +24 -17
  101. package/src/components/ui/command-palette/index.tsx +6 -256
  102. package/src/components/ui/controls/material-paint-panel.tsx +83 -0
  103. package/src/components/ui/controls/material-picker.tsx +17 -35
  104. package/src/components/ui/controls/metric-control.tsx +33 -11
  105. package/src/components/ui/floating-level-selector.tsx +48 -11
  106. package/src/components/ui/helpers/helper-manager.tsx +21 -17
  107. package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
  108. package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
  109. package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
  110. package/src/components/ui/level-duplicate-dialog.tsx +5 -6
  111. package/src/components/ui/panels/node-display.ts +1 -0
  112. package/src/components/ui/panels/paint-panel.tsx +66 -41
  113. package/src/components/ui/panels/panel-manager.tsx +33 -74
  114. package/src/components/ui/panels/panel-wrapper.tsx +217 -12
  115. package/src/components/ui/panels/parametric-inspector.tsx +469 -0
  116. package/src/components/ui/panels/reference-panel.tsx +28 -13
  117. package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
  118. package/src/components/ui/primitives/number-input.tsx +1 -1
  119. package/src/components/ui/primitives/sidebar.tsx +0 -0
  120. package/src/components/ui/scene-loader.tsx +1 -3
  121. package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
  122. package/src/components/ui/sidebar/icon-rail.tsx +0 -0
  123. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
  124. package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
  125. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
  126. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
  127. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
  128. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
  129. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
  130. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
  131. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
  132. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
  133. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
  134. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
  135. package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
  136. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
  137. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
  138. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
  139. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
  140. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
  141. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
  142. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
  144. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
  145. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
  146. package/src/components/ui/sidebar/tab-bar.tsx +61 -1
  147. package/src/components/ui/slider.tsx +1 -1
  148. package/src/components/viewer-overlay.tsx +169 -52
  149. package/src/components/viewer-zone-system.tsx +1 -1
  150. package/src/hooks/use-auto-save.ts +24 -3
  151. package/src/hooks/use-ceiling-events.ts +175 -0
  152. package/src/hooks/use-drag-action.ts +124 -0
  153. package/src/hooks/use-keyboard.ts +125 -62
  154. package/src/hooks/use-selection.ts +64 -0
  155. package/src/index.tsx +243 -4
  156. package/src/lib/constants.ts +6 -2
  157. package/src/lib/editor-api.ts +62 -0
  158. package/src/lib/elevator-support.ts +96 -0
  159. package/src/lib/floorplan/apply-alignment.ts +111 -0
  160. package/src/lib/floorplan/index.ts +7 -0
  161. package/src/lib/floorplan/selection-tool.ts +40 -0
  162. package/src/lib/floorplan/stairs.ts +12 -1
  163. package/src/lib/fresh-planar-placement.test.ts +102 -0
  164. package/src/lib/fresh-planar-placement.ts +63 -0
  165. package/src/lib/history.ts +2 -1
  166. package/src/lib/level-duplication.test.ts +0 -2
  167. package/src/lib/level-duplication.ts +55 -43
  168. package/src/lib/level-name.ts +11 -0
  169. package/src/lib/material-paint.ts +212 -20
  170. package/src/lib/placement-metadata.ts +29 -0
  171. package/src/lib/planar-cursor-placement.test.ts +43 -0
  172. package/src/lib/planar-cursor-placement.ts +42 -0
  173. package/src/lib/roof-duplication.ts +2 -2
  174. package/src/lib/scene-bounds.ts +1 -1
  175. package/src/lib/scene-clipboard.ts +267 -0
  176. package/src/lib/scene.ts +13 -4
  177. package/src/lib/sfx-bus.ts +25 -1
  178. package/src/lib/sfx-player.ts +75 -27
  179. package/src/lib/site-boundary.ts +1 -0
  180. package/src/lib/stair-duplication.ts +2 -2
  181. package/src/lib/stair-levels.test.ts +146 -0
  182. package/src/lib/stair-levels.ts +177 -0
  183. package/src/lib/world-grid-snap.ts +199 -0
  184. package/src/lib/zone-content.ts +130 -0
  185. package/src/store/use-alignment-guides.ts +21 -0
  186. package/src/store/use-editor.tsx +342 -91
  187. package/src/store/use-placement-preview.ts +31 -0
  188. package/src/store/use-wall-move-ghosts.ts +36 -0
  189. package/src/store/use-wall-snap-indicator.ts +32 -0
  190. package/tsconfig.json +2 -1
  191. package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
  192. package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
  193. package/src/components/feedback-dialog.tsx +0 -265
  194. package/src/components/pascal-radio.tsx +0 -280
  195. package/src/components/preview-button.tsx +0 -16
  196. package/src/components/tools/building/move-building-tool.tsx +0 -157
  197. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
  198. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
  199. package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
  200. package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
  201. package/src/components/tools/column/column-tool.tsx +0 -97
  202. package/src/components/tools/column/move-column-tool.tsx +0 -105
  203. package/src/components/tools/door/door-math.ts +0 -110
  204. package/src/components/tools/door/door-tool.tsx +0 -324
  205. package/src/components/tools/door/move-door-tool.tsx +0 -412
  206. package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
  207. package/src/components/tools/fence/fence-tool.tsx +0 -346
  208. package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
  209. package/src/components/tools/fence/move-fence-tool.tsx +0 -298
  210. package/src/components/tools/item/item-tool.tsx +0 -26
  211. package/src/components/tools/roof/move-roof-tool.tsx +0 -359
  212. package/src/components/tools/slab/move-slab-tool.tsx +0 -182
  213. package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
  214. package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
  215. package/src/components/tools/slab/slab-tool.tsx +0 -322
  216. package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
  217. package/src/components/tools/spawn/spawn-tool.tsx +0 -130
  218. package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
  219. package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
  220. package/src/components/tools/wall/move-wall-tool.tsx +0 -356
  221. package/src/components/tools/wall/wall-tool.tsx +0 -332
  222. package/src/components/tools/window/move-window-tool.tsx +0 -447
  223. package/src/components/tools/window/window-math.ts +0 -117
  224. package/src/components/tools/window/window-tool.tsx +0 -332
  225. package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
  226. package/src/components/ui/helpers/slab-helper.tsx +0 -20
  227. package/src/components/ui/helpers/wall-helper.tsx +0 -20
  228. package/src/components/ui/panels/ceiling-panel.tsx +0 -250
  229. package/src/components/ui/panels/column-panel.tsx +0 -715
  230. package/src/components/ui/panels/door-panel.tsx +0 -1292
  231. package/src/components/ui/panels/fence-panel.tsx +0 -227
  232. package/src/components/ui/panels/item-panel.tsx +0 -306
  233. package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
  234. package/src/components/ui/panels/roof-panel.tsx +0 -230
  235. package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
  236. package/src/components/ui/panels/slab-panel.tsx +0 -250
  237. package/src/components/ui/panels/spawn-panel.tsx +0 -155
  238. package/src/components/ui/panels/stair-panel.tsx +0 -525
  239. package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
  240. package/src/components/ui/panels/wall-panel.tsx +0 -177
  241. package/src/components/ui/panels/window-panel.tsx +0 -960
  242. package/src/components/ui/viewer-toolbar.tsx +0 -436
  243. package/src/contexts/presets-context.tsx +0 -121
  244. package/src/hooks/use-contextual-tools.ts +0 -61
@@ -1,423 +0,0 @@
1
- 'use client'
2
-
3
- import {
4
- type AnyNodeId,
5
- emitter,
6
- type GridEvent,
7
- pauseSceneHistory,
8
- resumeSceneHistory,
9
- useScene,
10
- type WallNode,
11
- } from '@pascal-app/core'
12
- import { useViewer } from '@pascal-app/viewer'
13
- import { Html } from '@react-three/drei'
14
- import { useCallback, useEffect, useRef, useState } from 'react'
15
- import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
16
- import { sfxEmitter } from '../../../lib/sfx-bus'
17
- import useEditor, { type MovingWallEndpoint } from '../../../store/use-editor'
18
- import { CursorSphere } from '../shared/cursor-sphere'
19
- import {
20
- formatAngleRadians,
21
- getAngleToSegmentReference,
22
- getSegmentAngleReferenceAtPoint,
23
- } from '../shared/segment-angle'
24
- import { isWallLongEnough, snapWallDraftPoint, type WallPlanPoint } from './wall-drafting'
25
-
26
- function samePoint(a: WallPlanPoint, b: WallPlanPoint) {
27
- return a[0] === b[0] && a[1] === b[1]
28
- }
29
-
30
- type WallSegmentLike = {
31
- id: WallNode['id']
32
- start: WallPlanPoint
33
- end: WallPlanPoint
34
- curveOffset?: number
35
- }
36
-
37
- type AngleLabelState = {
38
- label: string
39
- position: [number, number, number]
40
- } | null
41
-
42
- function getEndpointAngleLabel(args: {
43
- preview: { start: WallPlanPoint; end: WallPlanPoint; curveOffset?: number }
44
- walls: WallSegmentLike[]
45
- nodeId: WallNode['id']
46
- }): AngleLabelState {
47
- const { preview, walls, nodeId } = args
48
- const endpoints = [
49
- {
50
- point: preview.start,
51
- },
52
- {
53
- point: preview.end,
54
- },
55
- ]
56
- const targetSegment: WallSegmentLike = {
57
- id: nodeId,
58
- start: preview.start,
59
- end: preview.end,
60
- curveOffset: preview.curveOffset,
61
- }
62
-
63
- for (const endpoint of endpoints) {
64
- const targetReference = getSegmentAngleReferenceAtPoint(endpoint.point, targetSegment)
65
- if (!targetReference) continue
66
-
67
- const connectedWall = walls.find(
68
- (wall) =>
69
- wall.id !== nodeId && Boolean(getSegmentAngleReferenceAtPoint(endpoint.point, wall)),
70
- )
71
- if (!connectedWall) continue
72
-
73
- const connectedReference = getSegmentAngleReferenceAtPoint(endpoint.point, connectedWall)
74
- if (!connectedReference) continue
75
-
76
- const angle = getAngleToSegmentReference(targetReference.vector, connectedReference)
77
- if (angle === null) continue
78
-
79
- return {
80
- label: formatAngleRadians(angle),
81
- position: [endpoint.point[0], 0.34, endpoint.point[1]],
82
- }
83
- }
84
-
85
- return null
86
- }
87
-
88
- type LinkedWallSnapshot = {
89
- id: WallNode['id']
90
- start: WallPlanPoint
91
- end: WallPlanPoint
92
- curveOffset?: number
93
- }
94
-
95
- function getLinkedWallSnapshots(args: {
96
- wallId: WallNode['id']
97
- wallParentId: string | null
98
- originalStart: WallPlanPoint
99
- originalEnd: WallPlanPoint
100
- }) {
101
- const { wallId, wallParentId, originalStart, originalEnd } = args
102
- const { nodes } = useScene.getState()
103
- const snapshots: LinkedWallSnapshot[] = []
104
-
105
- for (const node of Object.values(nodes)) {
106
- if (!(node?.type === 'wall' && node.id !== wallId)) {
107
- continue
108
- }
109
-
110
- if ((node.parentId ?? null) !== wallParentId) {
111
- continue
112
- }
113
-
114
- if (
115
- !samePoint(node.start, originalStart) &&
116
- !samePoint(node.start, originalEnd) &&
117
- !samePoint(node.end, originalStart) &&
118
- !samePoint(node.end, originalEnd)
119
- ) {
120
- continue
121
- }
122
-
123
- snapshots.push({
124
- id: node.id,
125
- start: [...node.start] as WallPlanPoint,
126
- end: [...node.end] as WallPlanPoint,
127
- curveOffset: node.curveOffset,
128
- })
129
- }
130
-
131
- return snapshots
132
- }
133
-
134
- function getLinkedWallUpdates(
135
- linkedWalls: LinkedWallSnapshot[],
136
- originalStart: WallPlanPoint,
137
- originalEnd: WallPlanPoint,
138
- nextStart: WallPlanPoint,
139
- nextEnd: WallPlanPoint,
140
- ) {
141
- return linkedWalls.map((wall) => ({
142
- id: wall.id,
143
- curveOffset: wall.curveOffset,
144
- start: samePoint(wall.start, originalStart)
145
- ? nextStart
146
- : samePoint(wall.start, originalEnd)
147
- ? nextEnd
148
- : wall.start,
149
- end: samePoint(wall.end, originalStart)
150
- ? nextStart
151
- : samePoint(wall.end, originalEnd)
152
- ? nextEnd
153
- : wall.end,
154
- }))
155
- }
156
-
157
- export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => {
158
- const activatedAtRef = useRef<number>(Date.now())
159
- const previousGridPosRef = useRef<WallPlanPoint | null>(null)
160
- const shiftPressedRef = useRef(false)
161
- const altPressedRef = useRef(false)
162
- const nodeIdRef = useRef(target.wall.id)
163
- const originalStartRef = useRef<WallPlanPoint>([...target.wall.start] as WallPlanPoint)
164
- const originalEndRef = useRef<WallPlanPoint>([...target.wall.end] as WallPlanPoint)
165
- const fixedPointRef = useRef<WallPlanPoint>(
166
- target.endpoint === 'start'
167
- ? ([...target.wall.end] as WallPlanPoint)
168
- : ([...target.wall.start] as WallPlanPoint),
169
- )
170
- const linkedOriginalsRef = useRef(
171
- getLinkedWallSnapshots({
172
- wallId: target.wall.id,
173
- wallParentId: target.wall.parentId ?? null,
174
- originalStart: target.wall.start,
175
- originalEnd: target.wall.end,
176
- }),
177
- )
178
- const previewRef = useRef<{ start: WallPlanPoint; end: WallPlanPoint } | null>(null)
179
- const [angleLabel, setAngleLabel] = useState<AngleLabelState>(null)
180
-
181
- const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
182
- const point = target.endpoint === 'start' ? target.wall.start : target.wall.end
183
- return [point[0], 0, point[1]]
184
- })
185
- const [altPressed, setAltPressed] = useState(false)
186
-
187
- const exitMoveMode = useCallback(() => {
188
- useEditor.getState().setMovingWallEndpoint(null)
189
- }, [])
190
-
191
- useEffect(() => {
192
- const nodeId = nodeIdRef.current
193
- const originalStart = originalStartRef.current
194
- const originalEnd = originalEndRef.current
195
- const fixedPoint = fixedPointRef.current
196
- const levelWalls = Object.values(useScene.getState().nodes).filter(
197
- (node): node is WallNode =>
198
- node?.type === 'wall' && (node.parentId ?? null) === (target.wall.parentId ?? null),
199
- )
200
-
201
- pauseSceneHistory(useScene)
202
- let wasCommitted = false
203
-
204
- const applyNodePreview = (
205
- updates: Array<{ id: WallNode['id']; start: WallPlanPoint; end: WallPlanPoint }>,
206
- ) => {
207
- useScene.getState().updateNodes(
208
- updates.map((entry) => ({
209
- id: entry.id as AnyNodeId,
210
- data: { start: entry.start, end: entry.end },
211
- })),
212
- )
213
- for (const entry of updates) {
214
- useScene.getState().markDirty(entry.id as AnyNodeId)
215
- }
216
- }
217
-
218
- const applyPreview = (movingPoint: WallPlanPoint, detachLinkedWalls = false) => {
219
- const nextStart = target.endpoint === 'start' ? movingPoint : fixedPoint
220
- const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint
221
- const linkedUpdates = detachLinkedWalls
222
- ? []
223
- : getLinkedWallUpdates(
224
- linkedOriginalsRef.current,
225
- originalStart,
226
- originalEnd,
227
- nextStart,
228
- nextEnd,
229
- )
230
- previewRef.current = { start: nextStart, end: nextEnd }
231
- setCursorLocalPos([movingPoint[0], 0, movingPoint[1]])
232
- setAngleLabel(
233
- getEndpointAngleLabel({
234
- preview: { start: nextStart, end: nextEnd, curveOffset: target.wall.curveOffset },
235
- walls: [
236
- ...levelWalls.map((wall) => ({
237
- id: wall.id,
238
- start: wall.start,
239
- end: wall.end,
240
- curveOffset: wall.curveOffset,
241
- })),
242
- ...linkedUpdates,
243
- ],
244
- nodeId,
245
- }),
246
- )
247
- applyNodePreview([{ id: nodeId, start: nextStart, end: nextEnd }, ...linkedUpdates])
248
- }
249
-
250
- const restoreOriginal = (clearAngleLabel = true) => {
251
- applyNodePreview([
252
- { id: nodeId, start: originalStart, end: originalEnd },
253
- ...linkedOriginalsRef.current,
254
- ])
255
- if (clearAngleLabel) {
256
- setAngleLabel(null)
257
- }
258
- }
259
-
260
- const onGridMove = (event: GridEvent) => {
261
- const planPoint: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
262
- const snappedPoint = snapWallDraftPoint({
263
- point: planPoint,
264
- walls: levelWalls,
265
- start: fixedPoint,
266
- angleSnap: !shiftPressedRef.current,
267
- ignoreWallIds: [nodeId],
268
- })
269
-
270
- if (
271
- previousGridPosRef.current &&
272
- (snappedPoint[0] !== previousGridPosRef.current[0] ||
273
- snappedPoint[1] !== previousGridPosRef.current[1])
274
- ) {
275
- sfxEmitter.emit('sfx:grid-snap')
276
- }
277
- previousGridPosRef.current = snappedPoint
278
-
279
- applyPreview(snappedPoint, event.nativeEvent.altKey)
280
- }
281
-
282
- const onGridClick = (event: GridEvent) => {
283
- if (Date.now() - activatedAtRef.current < 150) {
284
- event.nativeEvent?.stopPropagation?.()
285
- return
286
- }
287
-
288
- const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
289
- const hasChanged =
290
- !samePoint(preview.start, originalStart) || !samePoint(preview.end, originalEnd)
291
-
292
- if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
293
- wasCommitted = true
294
-
295
- // Restore original baseline while paused so the next resume+update
296
- // registers as a single tracked change (undo reverts to original).
297
- applyNodePreview([
298
- { id: nodeId, start: originalStart, end: originalEnd },
299
- ...linkedOriginalsRef.current,
300
- ])
301
-
302
- resumeSceneHistory(useScene)
303
- applyNodePreview([
304
- { id: nodeId, start: preview.start, end: preview.end },
305
- ...(altPressedRef.current
306
- ? []
307
- : getLinkedWallUpdates(
308
- linkedOriginalsRef.current,
309
- originalStart,
310
- originalEnd,
311
- preview.start,
312
- preview.end,
313
- )),
314
- ])
315
- pauseSceneHistory(useScene)
316
- sfxEmitter.emit('sfx:item-place')
317
- }
318
-
319
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
320
- setAngleLabel(null)
321
- exitMoveMode()
322
- event.nativeEvent?.stopPropagation?.()
323
- }
324
-
325
- const onCancel = () => {
326
- restoreOriginal()
327
- useViewer.getState().setSelection({ selectedIds: [nodeId] })
328
- resumeSceneHistory(useScene)
329
- setAngleLabel(null)
330
- markToolCancelConsumed()
331
- exitMoveMode()
332
- }
333
-
334
- const onKeyDown = (event: KeyboardEvent) => {
335
- if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
336
- return
337
- }
338
- if (event.key === 'Shift') {
339
- shiftPressedRef.current = true
340
- }
341
- if (event.key === 'Alt') {
342
- altPressedRef.current = true
343
- setAltPressed(true)
344
- }
345
- }
346
-
347
- const onKeyUp = (event: KeyboardEvent) => {
348
- if (event.key === 'Shift') {
349
- shiftPressedRef.current = false
350
- }
351
- if (event.key === 'Alt') {
352
- altPressedRef.current = false
353
- setAltPressed(false)
354
- }
355
- }
356
-
357
- const onWindowBlur = () => {
358
- shiftPressedRef.current = false
359
- altPressedRef.current = false
360
- setAltPressed(false)
361
- }
362
-
363
- emitter.on('grid:move', onGridMove)
364
- emitter.on('grid:click', onGridClick)
365
- emitter.on('tool:cancel', onCancel)
366
- window.addEventListener('keydown', onKeyDown)
367
- window.addEventListener('keyup', onKeyUp)
368
- window.addEventListener('blur', onWindowBlur)
369
-
370
- return () => {
371
- if (!wasCommitted) {
372
- restoreOriginal(false)
373
- }
374
- resumeSceneHistory(useScene)
375
- emitter.off('grid:move', onGridMove)
376
- emitter.off('grid:click', onGridClick)
377
- emitter.off('tool:cancel', onCancel)
378
- window.removeEventListener('keydown', onKeyDown)
379
- window.removeEventListener('keyup', onKeyUp)
380
- window.removeEventListener('blur', onWindowBlur)
381
- }
382
- }, [exitMoveMode, target])
383
-
384
- return (
385
- <group>
386
- <CursorSphere position={cursorLocalPos} showTooltip={false} />
387
- <Html
388
- position={[cursorLocalPos[0], 0, cursorLocalPos[2]]}
389
- style={{ pointerEvents: 'none', touchAction: 'none' }}
390
- zIndexRange={[100, 0]}
391
- >
392
- <div className="translate-y-10">
393
- <div
394
- className={`whitespace-nowrap rounded-full border px-2 py-1 text-[11px] font-medium shadow-lg backdrop-blur-md transition-colors ${
395
- altPressed
396
- ? 'border-amber-500/80 bg-amber-500/15 text-amber-100'
397
- : 'border-border bg-background/95 text-muted-foreground'
398
- }`}
399
- >
400
- {altPressed ? 'Detaching corner' : 'Alt to detach'}
401
- </div>
402
- </div>
403
- </Html>
404
- {angleLabel && <EndpointAngleLabel label={angleLabel.label} position={angleLabel.position} />}
405
- </group>
406
- )
407
- }
408
-
409
- function EndpointAngleLabel({
410
- label,
411
- position,
412
- }: {
413
- label: string
414
- position: [number, number, number]
415
- }) {
416
- return (
417
- <Html center position={position} style={{ pointerEvents: 'none' }} zIndexRange={[100, 0]}>
418
- <div className="whitespace-nowrap rounded-full border border-border bg-background/95 px-2 py-1 font-mono text-[11px] font-semibold text-foreground shadow-lg backdrop-blur-md">
419
- {label}
420
- </div>
421
- </Html>
422
- )
423
- }