Add 3D wall endpoint move controls
This commit is contained in:
@@ -14,11 +14,13 @@ import {
|
|||||||
StairSegmentNode,
|
StairSegmentNode,
|
||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useScene,
|
useScene,
|
||||||
|
type WallNode,
|
||||||
WindowNode,
|
WindowNode,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { Html } from '@react-three/drei'
|
import { Html } from '@react-three/drei'
|
||||||
import { useFrame } from '@react-three/fiber'
|
import { useFrame } from '@react-three/fiber'
|
||||||
|
import { Move } from 'lucide-react'
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback, useRef } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { sfxEmitter } from '../../lib/sfx-bus'
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
||||||
@@ -47,12 +49,16 @@ export function FloatingActionMenu() {
|
|||||||
const updateNode = useScene((s) => s.updateNode)
|
const updateNode = useScene((s) => s.updateNode)
|
||||||
const mode = useEditor((s) => s.mode)
|
const mode = useEditor((s) => s.mode)
|
||||||
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
||||||
|
const movingWallEndpoint = useEditor((s) => s.movingWallEndpoint)
|
||||||
const setMovingNode = useEditor((s) => s.setMovingNode)
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
||||||
|
const setMovingWallEndpoint = useEditor((s) => s.setMovingWallEndpoint)
|
||||||
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
||||||
const setSelection = useViewer((s) => s.setSelection)
|
const setSelection = useViewer((s) => s.setSelection)
|
||||||
const setEditingHole = useEditor((s) => s.setEditingHole)
|
const setEditingHole = useEditor((s) => s.setEditingHole)
|
||||||
|
|
||||||
const groupRef = useRef<THREE.Group>(null)
|
const groupRef = useRef<THREE.Group>(null)
|
||||||
|
const startEndpointGroupRef = useRef<THREE.Group>(null)
|
||||||
|
const endEndpointGroupRef = useRef<THREE.Group>(null)
|
||||||
|
|
||||||
// Only show for single selection of specific types
|
// Only show for single selection of specific types
|
||||||
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
||||||
@@ -85,6 +91,29 @@ export function FloatingActionMenu() {
|
|||||||
const yOffset = isStructural ? 0.8 : 0.3
|
const yOffset = isStructural ? 0.8 : 0.3
|
||||||
groupRef.current.position.set(center.x, box.max.y + yOffset, center.z)
|
groupRef.current.position.set(center.x, box.max.y + yOffset, center.z)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (node?.type === 'wall') {
|
||||||
|
const wall = node as WallNode
|
||||||
|
const wallLength = Math.hypot(wall.end[0] - wall.start[0], wall.end[1] - wall.start[1])
|
||||||
|
const endpointYOffset = 0.35
|
||||||
|
const startWorld = obj.localToWorld(new THREE.Vector3(0, 0, 0))
|
||||||
|
const endWorld = obj.localToWorld(new THREE.Vector3(wallLength, 0, 0))
|
||||||
|
|
||||||
|
if (startEndpointGroupRef.current) {
|
||||||
|
startEndpointGroupRef.current.position.set(
|
||||||
|
startWorld.x,
|
||||||
|
startWorld.y + endpointYOffset,
|
||||||
|
startWorld.z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (endEndpointGroupRef.current) {
|
||||||
|
endEndpointGroupRef.current.position.set(
|
||||||
|
endWorld.x,
|
||||||
|
endWorld.y + endpointYOffset,
|
||||||
|
endWorld.z,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,6 +151,16 @@ export function FloatingActionMenu() {
|
|||||||
},
|
},
|
||||||
[canCurveSelectedWall, node, setCurvingWall, setSelection],
|
[canCurveSelectedWall, node, setCurvingWall, setSelection],
|
||||||
)
|
)
|
||||||
|
const handleEndpointMove = useCallback(
|
||||||
|
(endpoint: 'start' | 'end', e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (!(node && node.type === 'wall')) return
|
||||||
|
sfxEmitter.emit('sfx:item-pick')
|
||||||
|
setMovingWallEndpoint({ wall: node, endpoint })
|
||||||
|
setSelection({ selectedIds: [] })
|
||||||
|
},
|
||||||
|
[node, setMovingWallEndpoint, setSelection],
|
||||||
|
)
|
||||||
|
|
||||||
const handleDuplicate = useCallback(
|
const handleDuplicate = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
@@ -307,32 +346,68 @@ export function FloatingActionMenu() {
|
|||||||
[node?.type, selectedId, setSelection],
|
[node?.type, selectedId, setSelection],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete')) return null
|
if (
|
||||||
|
!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete') ||
|
||||||
|
movingWallEndpoint
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group ref={groupRef}>
|
<group>
|
||||||
<Html
|
<group ref={groupRef}>
|
||||||
center
|
<Html
|
||||||
style={{
|
center
|
||||||
pointerEvents: 'auto',
|
style={{
|
||||||
touchAction: 'none',
|
pointerEvents: 'auto',
|
||||||
}}
|
touchAction: 'none',
|
||||||
zIndexRange={[100, 0]}
|
}}
|
||||||
>
|
zIndexRange={[100, 0]}
|
||||||
<NodeActionMenu
|
>
|
||||||
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
<NodeActionMenu
|
||||||
onCurve={canCurveSelectedWall ? handleCurve : undefined}
|
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
||||||
onDelete={handleDelete}
|
onCurve={canCurveSelectedWall ? handleCurve : undefined}
|
||||||
onDuplicate={
|
onDelete={handleDelete}
|
||||||
node && !DELETE_ONLY_TYPES.includes(node.type) && !HOLE_TYPES.includes(node.type)
|
onDuplicate={
|
||||||
? handleDuplicate
|
node && !DELETE_ONLY_TYPES.includes(node.type) && !HOLE_TYPES.includes(node.type)
|
||||||
: undefined
|
? handleDuplicate
|
||||||
}
|
: undefined
|
||||||
onMove={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleMove : undefined}
|
}
|
||||||
onPointerDown={(e) => e.stopPropagation()}
|
onMove={node && !DELETE_ONLY_TYPES.includes(node.type) ? handleMove : undefined}
|
||||||
onPointerUp={(e) => e.stopPropagation()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
/>
|
onPointerUp={(e) => e.stopPropagation()}
|
||||||
</Html>
|
/>
|
||||||
|
</Html>
|
||||||
|
</group>
|
||||||
|
{node?.type === 'wall' && (
|
||||||
|
<>
|
||||||
|
<group ref={startEndpointGroupRef}>
|
||||||
|
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
||||||
|
<button
|
||||||
|
aria-label="Move wall start"
|
||||||
|
className="pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-muted-foreground shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-foreground"
|
||||||
|
onClick={(e) => handleEndpointMove('start', e)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Move className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</Html>
|
||||||
|
</group>
|
||||||
|
<group ref={endEndpointGroupRef}>
|
||||||
|
<Html center style={{ pointerEvents: 'auto', touchAction: 'none' }} zIndexRange={[100, 0]}>
|
||||||
|
<button
|
||||||
|
aria-label="Move wall end"
|
||||||
|
className="pointer-events-auto flex h-8 w-8 items-center justify-center rounded-full border border-border bg-background/95 text-muted-foreground shadow-lg backdrop-blur-md transition-colors hover:bg-accent hover:text-foreground"
|
||||||
|
onClick={(e) => handleEndpointMove('end', e)}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Move className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</Html>
|
||||||
|
</group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { SlabHoleEditor } from './slab/slab-hole-editor'
|
|||||||
import { SlabTool } from './slab/slab-tool'
|
import { SlabTool } from './slab/slab-tool'
|
||||||
import { StairTool } from './stair/stair-tool'
|
import { StairTool } from './stair/stair-tool'
|
||||||
import { CurveWallTool } from './wall/curve-wall-tool'
|
import { CurveWallTool } from './wall/curve-wall-tool'
|
||||||
|
import { MoveWallEndpointTool } from './wall/move-wall-endpoint-tool'
|
||||||
import { WallTool } from './wall/wall-tool'
|
import { WallTool } from './wall/wall-tool'
|
||||||
import { WindowTool } from './window/window-tool'
|
import { WindowTool } from './window/window-tool'
|
||||||
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
|
import { ZoneBoundaryEditor } from './zone/zone-boundary-editor'
|
||||||
@@ -52,6 +53,7 @@ export const ToolManager: React.FC = () => {
|
|||||||
const mode = useEditor((state) => state.mode)
|
const mode = useEditor((state) => state.mode)
|
||||||
const tool = useEditor((state) => state.tool)
|
const tool = useEditor((state) => state.tool)
|
||||||
const movingNode = useEditor((state) => state.movingNode)
|
const movingNode = useEditor((state) => state.movingNode)
|
||||||
|
const movingWallEndpoint = useEditor((state) => state.movingWallEndpoint)
|
||||||
const curvingWall = useEditor((state) => state.curvingWall)
|
const curvingWall = useEditor((state) => state.curvingWall)
|
||||||
const editingHole = useEditor((state) => state.editingHole)
|
const editingHole = useEditor((state) => state.editingHole)
|
||||||
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
const selectedZoneId = useViewer((state) => state.selection.zoneId)
|
||||||
@@ -142,6 +144,7 @@ export const ToolManager: React.FC = () => {
|
|||||||
{showCeilingHoleEditor && selectedCeilingId && editingHole && (
|
{showCeilingHoleEditor && selectedCeilingId && editingHole && (
|
||||||
<CeilingHoleEditor ceilingId={selectedCeilingId} holeIndex={editingHole.holeIndex} />
|
<CeilingHoleEditor ceilingId={selectedCeilingId} holeIndex={editingHole.holeIndex} />
|
||||||
)}
|
)}
|
||||||
|
{movingWallEndpoint && <MoveWallEndpointTool target={movingWallEndpoint} />}
|
||||||
{curvingWall && <CurveWallTool node={curvingWall} />}
|
{curvingWall && <CurveWallTool node={curvingWall} />}
|
||||||
{movingNode && movingNode.type !== 'building' && <MoveTool />}
|
{movingNode && movingNode.type !== 'building' && <MoveTool />}
|
||||||
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
{!movingNode && BuildToolComponent && <BuildToolComponent />}
|
||||||
|
|||||||
@@ -0,0 +1,266 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { type AnyNodeId, emitter, type GridEvent, useScene, type WallNode } from '@pascal-app/core'
|
||||||
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
|
import useEditor, { type MovingWallEndpoint } from '../../../store/use-editor'
|
||||||
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
import {
|
||||||
|
isWallLongEnough,
|
||||||
|
snapWallDraftPoint,
|
||||||
|
type WallPlanPoint,
|
||||||
|
} from './wall-drafting'
|
||||||
|
|
||||||
|
function samePoint(a: WallPlanPoint, b: WallPlanPoint) {
|
||||||
|
return a[0] === b[0] && a[1] === b[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkedWallSnapshot = {
|
||||||
|
id: WallNode['id']
|
||||||
|
start: WallPlanPoint
|
||||||
|
end: WallPlanPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinkedWallSnapshots(args: {
|
||||||
|
wallId: WallNode['id']
|
||||||
|
wallParentId: string | null
|
||||||
|
originalStart: WallPlanPoint
|
||||||
|
originalEnd: WallPlanPoint
|
||||||
|
}) {
|
||||||
|
const { wallId, wallParentId, originalStart, originalEnd } = args
|
||||||
|
const { nodes } = useScene.getState()
|
||||||
|
const snapshots: LinkedWallSnapshot[] = []
|
||||||
|
|
||||||
|
for (const node of Object.values(nodes)) {
|
||||||
|
if (!(node?.type === 'wall' && node.id !== wallId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((node.parentId ?? null) !== wallParentId) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!samePoint(node.start, originalStart) &&
|
||||||
|
!samePoint(node.start, originalEnd) &&
|
||||||
|
!samePoint(node.end, originalStart) &&
|
||||||
|
!samePoint(node.end, originalEnd)
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshots.push({
|
||||||
|
id: node.id,
|
||||||
|
start: [...node.start] as WallPlanPoint,
|
||||||
|
end: [...node.end] as WallPlanPoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshots
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLinkedWallUpdates(
|
||||||
|
linkedWalls: LinkedWallSnapshot[],
|
||||||
|
originalStart: WallPlanPoint,
|
||||||
|
originalEnd: WallPlanPoint,
|
||||||
|
nextStart: WallPlanPoint,
|
||||||
|
nextEnd: WallPlanPoint,
|
||||||
|
) {
|
||||||
|
return linkedWalls.map((wall) => ({
|
||||||
|
id: wall.id,
|
||||||
|
start: samePoint(wall.start, originalStart)
|
||||||
|
? nextStart
|
||||||
|
: samePoint(wall.start, originalEnd)
|
||||||
|
? nextEnd
|
||||||
|
: wall.start,
|
||||||
|
end: samePoint(wall.end, originalStart)
|
||||||
|
? nextStart
|
||||||
|
: samePoint(wall.end, originalEnd)
|
||||||
|
? nextEnd
|
||||||
|
: wall.end,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MoveWallEndpointTool: React.FC<{ target: MovingWallEndpoint }> = ({ target }) => {
|
||||||
|
const activatedAtRef = useRef<number>(Date.now())
|
||||||
|
const previousGridPosRef = useRef<WallPlanPoint | null>(null)
|
||||||
|
const shiftPressedRef = useRef(false)
|
||||||
|
const nodeIdRef = useRef(target.wall.id)
|
||||||
|
const originalStartRef = useRef<WallPlanPoint>([...target.wall.start] as WallPlanPoint)
|
||||||
|
const originalEndRef = useRef<WallPlanPoint>([...target.wall.end] as WallPlanPoint)
|
||||||
|
const fixedPointRef = useRef<WallPlanPoint>(
|
||||||
|
target.endpoint === 'start'
|
||||||
|
? ([...target.wall.end] as WallPlanPoint)
|
||||||
|
: ([...target.wall.start] as WallPlanPoint),
|
||||||
|
)
|
||||||
|
const linkedOriginalsRef = useRef(
|
||||||
|
getLinkedWallSnapshots({
|
||||||
|
wallId: target.wall.id,
|
||||||
|
wallParentId: target.wall.parentId ?? null,
|
||||||
|
originalStart: target.wall.start,
|
||||||
|
originalEnd: target.wall.end,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const previewRef = useRef<{ start: WallPlanPoint; end: WallPlanPoint } | null>(null)
|
||||||
|
|
||||||
|
const [cursorLocalPos, setCursorLocalPos] = useState<[number, number, number]>(() => {
|
||||||
|
const point = target.endpoint === 'start' ? target.wall.start : target.wall.end
|
||||||
|
return [point[0], 0, point[1]]
|
||||||
|
})
|
||||||
|
|
||||||
|
const exitMoveMode = useCallback(() => {
|
||||||
|
useEditor.getState().setMovingWallEndpoint(null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nodeId = nodeIdRef.current
|
||||||
|
const originalStart = originalStartRef.current
|
||||||
|
const originalEnd = originalEndRef.current
|
||||||
|
const fixedPoint = fixedPointRef.current
|
||||||
|
const levelWalls = Object.values(useScene.getState().nodes).filter(
|
||||||
|
(node): node is WallNode =>
|
||||||
|
node?.type === 'wall' && (node.parentId ?? null) === (target.wall.parentId ?? null),
|
||||||
|
)
|
||||||
|
|
||||||
|
useScene.temporal.getState().pause()
|
||||||
|
let wasCommitted = false
|
||||||
|
|
||||||
|
const applyNodePreview = (
|
||||||
|
updates: Array<{ id: WallNode['id']; start: WallPlanPoint; end: WallPlanPoint }>,
|
||||||
|
) => {
|
||||||
|
useScene.getState().updateNodes(
|
||||||
|
updates.map((entry) => ({
|
||||||
|
id: entry.id as AnyNodeId,
|
||||||
|
data: { start: entry.start, end: entry.end },
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
for (const entry of updates) {
|
||||||
|
useScene.getState().markDirty(entry.id as AnyNodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyPreview = (movingPoint: WallPlanPoint) => {
|
||||||
|
const nextStart = target.endpoint === 'start' ? movingPoint : fixedPoint
|
||||||
|
const nextEnd = target.endpoint === 'end' ? movingPoint : fixedPoint
|
||||||
|
previewRef.current = { start: nextStart, end: nextEnd }
|
||||||
|
setCursorLocalPos([movingPoint[0], 0, movingPoint[1]])
|
||||||
|
applyNodePreview([
|
||||||
|
{ id: nodeId, start: nextStart, end: nextEnd },
|
||||||
|
...getLinkedWallUpdates(
|
||||||
|
linkedOriginalsRef.current,
|
||||||
|
originalStart,
|
||||||
|
originalEnd,
|
||||||
|
nextStart,
|
||||||
|
nextEnd,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const restoreOriginal = () => {
|
||||||
|
applyNodePreview([{ id: nodeId, start: originalStart, end: originalEnd }, ...linkedOriginalsRef.current])
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGridMove = (event: GridEvent) => {
|
||||||
|
const planPoint: WallPlanPoint = [event.localPosition[0], event.localPosition[2]]
|
||||||
|
const snappedPoint = snapWallDraftPoint({
|
||||||
|
point: planPoint,
|
||||||
|
walls: levelWalls,
|
||||||
|
start: fixedPoint,
|
||||||
|
angleSnap: !shiftPressedRef.current,
|
||||||
|
ignoreWallIds: [nodeId],
|
||||||
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousGridPosRef.current &&
|
||||||
|
(snappedPoint[0] !== previousGridPosRef.current[0] ||
|
||||||
|
snappedPoint[1] !== previousGridPosRef.current[1])
|
||||||
|
) {
|
||||||
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
|
}
|
||||||
|
previousGridPosRef.current = snappedPoint
|
||||||
|
|
||||||
|
applyPreview(snappedPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGridClick = (event: GridEvent) => {
|
||||||
|
if (Date.now() - activatedAtRef.current < 150) {
|
||||||
|
event.nativeEvent?.stopPropagation?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = previewRef.current ?? { start: originalStart, end: originalEnd }
|
||||||
|
const hasChanged =
|
||||||
|
!samePoint(preview.start, originalStart) || !samePoint(preview.end, originalEnd)
|
||||||
|
|
||||||
|
if (hasChanged && isWallLongEnough(preview.start, preview.end)) {
|
||||||
|
wasCommitted = true
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
applyNodePreview([
|
||||||
|
{ id: nodeId, start: preview.start, end: preview.end },
|
||||||
|
...getLinkedWallUpdates(
|
||||||
|
linkedOriginalsRef.current,
|
||||||
|
originalStart,
|
||||||
|
originalEnd,
|
||||||
|
preview.start,
|
||||||
|
preview.end,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
useScene.temporal.getState().pause()
|
||||||
|
sfxEmitter.emit('sfx:item-place')
|
||||||
|
}
|
||||||
|
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
|
exitMoveMode()
|
||||||
|
event.nativeEvent?.stopPropagation?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
restoreOriginal()
|
||||||
|
useViewer.getState().setSelection({ selectedIds: [nodeId] })
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
markToolCancelConsumed()
|
||||||
|
exitMoveMode()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
shiftPressedRef.current = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
shiftPressedRef.current = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emitter.on('grid:move', onGridMove)
|
||||||
|
emitter.on('grid:click', onGridClick)
|
||||||
|
emitter.on('tool:cancel', onCancel)
|
||||||
|
window.addEventListener('keydown', onKeyDown)
|
||||||
|
window.addEventListener('keyup', onKeyUp)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!wasCommitted) {
|
||||||
|
restoreOriginal()
|
||||||
|
}
|
||||||
|
useScene.temporal.getState().resume()
|
||||||
|
emitter.off('grid:move', onGridMove)
|
||||||
|
emitter.off('grid:click', onGridClick)
|
||||||
|
emitter.off('tool:cancel', onCancel)
|
||||||
|
window.removeEventListener('keydown', onKeyDown)
|
||||||
|
window.removeEventListener('keyup', onKeyUp)
|
||||||
|
}
|
||||||
|
}, [exitMoveMode, target])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
<CursorSphere position={cursorLocalPos} showTooltip={false} />
|
||||||
|
</group>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -74,6 +74,11 @@ export type FloorplanSelectionTool = 'click' | 'marquee'
|
|||||||
// Combined tool type
|
// Combined tool type
|
||||||
export type Tool = SiteTool | StructureTool | FurnishTool
|
export type Tool = SiteTool | StructureTool | FurnishTool
|
||||||
|
|
||||||
|
export type MovingWallEndpoint = {
|
||||||
|
wall: WallNode
|
||||||
|
endpoint: 'start' | 'end'
|
||||||
|
}
|
||||||
|
|
||||||
type EditorState = {
|
type EditorState = {
|
||||||
phase: Phase
|
phase: Phase
|
||||||
setPhase: (phase: Phase) => void
|
setPhase: (phase: Phase) => void
|
||||||
@@ -117,6 +122,8 @@ type EditorState = {
|
|||||||
| BuildingNode
|
| BuildingNode
|
||||||
| null,
|
| null,
|
||||||
) => void
|
) => void
|
||||||
|
movingWallEndpoint: MovingWallEndpoint | null
|
||||||
|
setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
|
||||||
curvingWall: WallNode | null
|
curvingWall: WallNode | null
|
||||||
setCurvingWall: (wall: WallNode | null) => void
|
setCurvingWall: (wall: WallNode | null) => void
|
||||||
selectedReferenceId: string | null
|
selectedReferenceId: string | null
|
||||||
@@ -467,6 +474,10 @@ const useEditor = create<EditorState>()(
|
|||||||
| ItemNode
|
| ItemNode
|
||||||
| WindowNode
|
| WindowNode
|
||||||
| DoorNode
|
| DoorNode
|
||||||
|
| FenceNode
|
||||||
|
| CeilingNode
|
||||||
|
| SlabNode
|
||||||
|
| WallNode
|
||||||
| RoofNode
|
| RoofNode
|
||||||
| RoofSegmentNode
|
| RoofSegmentNode
|
||||||
| StairNode
|
| StairNode
|
||||||
@@ -474,6 +485,8 @@ const useEditor = create<EditorState>()(
|
|||||||
| BuildingNode
|
| BuildingNode
|
||||||
| null,
|
| null,
|
||||||
setMovingNode: (node) => set({ movingNode: node }),
|
setMovingNode: (node) => set({ movingNode: node }),
|
||||||
|
movingWallEndpoint: null,
|
||||||
|
setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
|
||||||
curvingWall: null,
|
curvingWall: null,
|
||||||
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
||||||
selectedReferenceId: null,
|
selectedReferenceId: null,
|
||||||
|
|||||||
Reference in New Issue
Block a user