@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.
- package/package.json +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- 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
|
+
}
|