@pascal-app/editor 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,140 @@
1
+ import { useScene, type WallNode, WallNode as WallSchema } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import { sfxEmitter } from '../../../lib/sfx-bus'
4
+
5
+ export type WallPlanPoint = [number, number]
6
+
7
+ export const WALL_GRID_STEP = 0.5
8
+ export const WALL_JOIN_SNAP_RADIUS = 0.35
9
+ export const WALL_MIN_LENGTH = 0.01
10
+
11
+ function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
12
+ const dx = a[0] - b[0]
13
+ const dz = a[1] - b[1]
14
+ return dx * dx + dz * dz
15
+ }
16
+
17
+ function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
18
+ return Math.round(value / step) * step
19
+ }
20
+
21
+ export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): WallPlanPoint {
22
+ return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
23
+ }
24
+
25
+ export function snapPointTo45Degrees(start: WallPlanPoint, cursor: WallPlanPoint): WallPlanPoint {
26
+ const dx = cursor[0] - start[0]
27
+ const dz = cursor[1] - start[1]
28
+ const angle = Math.atan2(dz, dx)
29
+ const snappedAngle = Math.round(angle / (Math.PI / 4)) * (Math.PI / 4)
30
+ const distance = Math.sqrt(dx * dx + dz * dz)
31
+
32
+ return snapPointToGrid([
33
+ start[0] + Math.cos(snappedAngle) * distance,
34
+ start[1] + Math.sin(snappedAngle) * distance,
35
+ ])
36
+ }
37
+
38
+ function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
39
+ const [x1, z1] = wall.start
40
+ const [x2, z2] = wall.end
41
+ const dx = x2 - x1
42
+ const dz = z2 - z1
43
+ const lengthSquared = dx * dx + dz * dz
44
+ if (lengthSquared < 1e-9) {
45
+ return null
46
+ }
47
+
48
+ const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
49
+ if (t <= 0 || t >= 1) {
50
+ return null
51
+ }
52
+
53
+ return [x1 + dx * t, z1 + dz * t]
54
+ }
55
+
56
+ export function findWallSnapTarget(
57
+ point: WallPlanPoint,
58
+ walls: WallNode[],
59
+ options?: { ignoreWallIds?: string[]; radius?: number },
60
+ ): WallPlanPoint | null {
61
+ const ignoreWallIds = new Set(options?.ignoreWallIds ?? [])
62
+ const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
63
+ let bestTarget: WallPlanPoint | null = null
64
+ let bestDistanceSquared = Number.POSITIVE_INFINITY
65
+
66
+ for (const wall of walls) {
67
+ if (ignoreWallIds.has(wall.id)) {
68
+ continue
69
+ }
70
+
71
+ const candidates: Array<WallPlanPoint | null> = [
72
+ wall.start,
73
+ wall.end,
74
+ projectPointOntoWall(point, wall),
75
+ ]
76
+ for (const candidate of candidates) {
77
+ if (!candidate) {
78
+ continue
79
+ }
80
+
81
+ const candidateDistanceSquared = distanceSquared(point, candidate)
82
+ if (
83
+ candidateDistanceSquared > radiusSquared ||
84
+ candidateDistanceSquared >= bestDistanceSquared
85
+ ) {
86
+ continue
87
+ }
88
+
89
+ bestTarget = candidate
90
+ bestDistanceSquared = candidateDistanceSquared
91
+ }
92
+ }
93
+
94
+ return bestTarget
95
+ }
96
+
97
+ export function snapWallDraftPoint(args: {
98
+ point: WallPlanPoint
99
+ walls: WallNode[]
100
+ start?: WallPlanPoint
101
+ angleSnap?: boolean
102
+ ignoreWallIds?: string[]
103
+ }): WallPlanPoint {
104
+ const { point, walls, start, angleSnap = false, ignoreWallIds } = args
105
+ const basePoint = start && angleSnap ? snapPointTo45Degrees(start, point) : snapPointToGrid(point)
106
+
107
+ return (
108
+ findWallSnapTarget(basePoint, walls, {
109
+ ignoreWallIds,
110
+ }) ?? basePoint
111
+ )
112
+ }
113
+
114
+ export function isWallLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean {
115
+ return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
116
+ }
117
+
118
+ export function createWallOnCurrentLevel(
119
+ start: WallPlanPoint,
120
+ end: WallPlanPoint,
121
+ ): WallNode | null {
122
+ const currentLevelId = useViewer.getState().selection.levelId
123
+ const { createNode, nodes } = useScene.getState()
124
+
125
+ if (!(currentLevelId && isWallLongEnough(start, end))) {
126
+ return null
127
+ }
128
+
129
+ const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
130
+ const wall = WallSchema.parse({
131
+ name: `Wall ${wallCount + 1}`,
132
+ start,
133
+ end,
134
+ })
135
+
136
+ createNode(wall, currentLevelId)
137
+ sfxEmitter.emit('sfx:structure-build')
138
+
139
+ return wall
140
+ }
@@ -0,0 +1,210 @@
1
+ import { emitter, type GridEvent, type LevelNode, useScene, type WallNode } from '@pascal-app/core'
2
+ import { useViewer } from '@pascal-app/viewer'
3
+ import { useEffect, useRef } from 'react'
4
+ import { DoubleSide, type Group, type Mesh, Shape, ShapeGeometry, Vector3 } from 'three'
5
+ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
6
+ import { EDITOR_LAYER } from '../../../lib/constants'
7
+ import { sfxEmitter } from '../../../lib/sfx-bus'
8
+ import { CursorSphere } from '../shared/cursor-sphere'
9
+ import { createWallOnCurrentLevel, snapWallDraftPoint, type WallPlanPoint } from './wall-drafting'
10
+
11
+ const WALL_HEIGHT = 2.5
12
+
13
+ /**
14
+ * Update wall preview mesh geometry to create a vertical plane between two points
15
+ */
16
+ const updateWallPreview = (mesh: Mesh, start: Vector3, end: Vector3) => {
17
+ // Calculate direction and perpendicular for wall thickness
18
+ const direction = new Vector3(end.x - start.x, 0, end.z - start.z)
19
+ const length = direction.length()
20
+
21
+ if (length < 0.01) {
22
+ mesh.visible = false
23
+ return
24
+ }
25
+
26
+ mesh.visible = true
27
+ direction.normalize()
28
+
29
+ // Create wall shape (vertical rectangle in XY plane)
30
+ const shape = new Shape()
31
+ shape.moveTo(0, 0)
32
+ shape.lineTo(length, 0)
33
+ shape.lineTo(length, WALL_HEIGHT)
34
+ shape.lineTo(0, WALL_HEIGHT)
35
+ shape.closePath()
36
+
37
+ // Create geometry
38
+ const geometry = new ShapeGeometry(shape)
39
+
40
+ // Calculate rotation angle
41
+ // Negate the angle to fix the opposite direction issue
42
+ const angle = -Math.atan2(direction.z, direction.x)
43
+
44
+ // Position at start point and rotate
45
+ mesh.position.set(start.x, start.y, start.z)
46
+ mesh.rotation.y = angle
47
+
48
+ // Dispose old geometry and assign new one
49
+ if (mesh.geometry) {
50
+ mesh.geometry.dispose()
51
+ }
52
+ mesh.geometry = geometry
53
+ }
54
+
55
+ const getCurrentLevelWalls = (): WallNode[] => {
56
+ const currentLevelId = useViewer.getState().selection.levelId
57
+ const { nodes } = useScene.getState()
58
+
59
+ if (!currentLevelId) return []
60
+
61
+ const levelNode = nodes[currentLevelId]
62
+ if (!levelNode || levelNode.type !== 'level') return []
63
+
64
+ return (levelNode as LevelNode).children
65
+ .map((childId) => nodes[childId])
66
+ .filter((node): node is WallNode => node?.type === 'wall')
67
+ }
68
+
69
+ export const WallTool: React.FC = () => {
70
+ const cursorRef = useRef<Group>(null)
71
+ const wallPreviewRef = useRef<Mesh>(null!)
72
+ const startingPoint = useRef(new Vector3(0, 0, 0))
73
+ const endingPoint = useRef(new Vector3(0, 0, 0))
74
+ const buildingState = useRef(0)
75
+ const shiftPressed = useRef(false)
76
+
77
+ useEffect(() => {
78
+ let gridPosition: WallPlanPoint = [0, 0]
79
+ let previousWallEnd: [number, number] | null = null
80
+
81
+ const onGridMove = (event: GridEvent) => {
82
+ if (!(cursorRef.current && wallPreviewRef.current)) return
83
+
84
+ const walls = getCurrentLevelWalls()
85
+ const cursorPoint: WallPlanPoint = [event.position[0], event.position[2]]
86
+ gridPosition = snapWallDraftPoint({
87
+ point: cursorPoint,
88
+ walls,
89
+ })
90
+
91
+ if (buildingState.current === 1) {
92
+ const snappedPoint = snapWallDraftPoint({
93
+ point: cursorPoint,
94
+ walls,
95
+ start: [startingPoint.current.x, startingPoint.current.z],
96
+ angleSnap: !shiftPressed.current,
97
+ })
98
+ const snapped = new Vector3(snappedPoint[0], event.position[1], snappedPoint[1])
99
+ endingPoint.current.copy(snapped)
100
+
101
+ // Position the cursor at the end of the wall being drawn
102
+ cursorRef.current.position.set(snapped.x, snapped.y, snapped.z)
103
+
104
+ // Play snap sound only when the actual wall end position changes
105
+ const currentWallEnd: [number, number] = [endingPoint.current.x, endingPoint.current.z]
106
+ if (
107
+ previousWallEnd &&
108
+ (currentWallEnd[0] !== previousWallEnd[0] || currentWallEnd[1] !== previousWallEnd[1])
109
+ ) {
110
+ sfxEmitter.emit('sfx:grid-snap')
111
+ }
112
+ previousWallEnd = currentWallEnd
113
+
114
+ // Update wall preview geometry
115
+ updateWallPreview(wallPreviewRef.current, startingPoint.current, endingPoint.current)
116
+ } else {
117
+ // Not drawing a wall yet, show the snapped anchor point.
118
+ cursorRef.current.position.set(gridPosition[0], event.position[1], gridPosition[1])
119
+ }
120
+ }
121
+
122
+ const onGridClick = (event: GridEvent) => {
123
+ const walls = getCurrentLevelWalls()
124
+ const clickPoint: WallPlanPoint = [event.position[0], event.position[2]]
125
+
126
+ if (buildingState.current === 0) {
127
+ const snappedStart = snapWallDraftPoint({
128
+ point: clickPoint,
129
+ walls,
130
+ })
131
+ gridPosition = snappedStart
132
+ startingPoint.current.set(snappedStart[0], event.position[1], snappedStart[1])
133
+ endingPoint.current.copy(startingPoint.current)
134
+ buildingState.current = 1
135
+ wallPreviewRef.current.visible = true
136
+ } else if (buildingState.current === 1) {
137
+ const snappedEnd = snapWallDraftPoint({
138
+ point: clickPoint,
139
+ walls,
140
+ start: [startingPoint.current.x, startingPoint.current.z],
141
+ angleSnap: !shiftPressed.current,
142
+ })
143
+ endingPoint.current.set(snappedEnd[0], event.position[1], snappedEnd[1])
144
+ const dx = endingPoint.current.x - startingPoint.current.x
145
+ const dz = endingPoint.current.z - startingPoint.current.z
146
+ if (dx * dx + dz * dz < 0.01 * 0.01) return
147
+ createWallOnCurrentLevel(
148
+ [startingPoint.current.x, startingPoint.current.z],
149
+ [endingPoint.current.x, endingPoint.current.z],
150
+ )
151
+ wallPreviewRef.current.visible = false
152
+ buildingState.current = 0
153
+ }
154
+ }
155
+
156
+ const onKeyDown = (e: KeyboardEvent) => {
157
+ if (e.key === 'Shift') {
158
+ shiftPressed.current = true
159
+ }
160
+ }
161
+
162
+ const onKeyUp = (e: KeyboardEvent) => {
163
+ if (e.key === 'Shift') {
164
+ shiftPressed.current = false
165
+ }
166
+ }
167
+
168
+ const onCancel = () => {
169
+ if (buildingState.current === 1) {
170
+ markToolCancelConsumed()
171
+ buildingState.current = 0
172
+ wallPreviewRef.current.visible = false
173
+ }
174
+ }
175
+
176
+ emitter.on('grid:move', onGridMove)
177
+ emitter.on('grid:click', onGridClick)
178
+ emitter.on('tool:cancel', onCancel)
179
+ window.addEventListener('keydown', onKeyDown)
180
+ window.addEventListener('keyup', onKeyUp)
181
+
182
+ return () => {
183
+ emitter.off('grid:move', onGridMove)
184
+ emitter.off('grid:click', onGridClick)
185
+ emitter.off('tool:cancel', onCancel)
186
+ window.removeEventListener('keydown', onKeyDown)
187
+ window.removeEventListener('keyup', onKeyUp)
188
+ }
189
+ }, [])
190
+
191
+ return (
192
+ <group>
193
+ {/* Cursor indicator */}
194
+ <CursorSphere ref={cursorRef} />
195
+
196
+ {/* Wall preview */}
197
+ <mesh layers={EDITOR_LAYER} ref={wallPreviewRef} renderOrder={1} visible={false}>
198
+ <shapeGeometry />
199
+ <meshBasicMaterial
200
+ color="#818cf8"
201
+ depthTest={false}
202
+ depthWrite={false}
203
+ opacity={0.5}
204
+ side={DoubleSide}
205
+ transparent
206
+ />
207
+ </mesh>
208
+ </group>
209
+ )
210
+ }