Align roof and slab dragging to snap targets
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
type AnyNodeId,
|
type AnyNodeId,
|
||||||
emitter,
|
emitter,
|
||||||
|
type FenceNode,
|
||||||
type GridEvent,
|
type GridEvent,
|
||||||
|
type LevelNode,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
type RoofSegmentNode,
|
type RoofSegmentNode,
|
||||||
type StairNode,
|
type StairNode,
|
||||||
@@ -9,13 +11,16 @@ import {
|
|||||||
sceneRegistry,
|
sceneRegistry,
|
||||||
useLiveTransforms,
|
useLiveTransforms,
|
||||||
useScene,
|
useScene,
|
||||||
|
type WallNode,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import * as THREE from 'three'
|
import * as THREE from 'three'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
|
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
import type { WallPlanPoint } from '../wall/wall-drafting'
|
||||||
|
|
||||||
export const MoveRoofTool: React.FC<{
|
export const MoveRoofTool: React.FC<{
|
||||||
node: RoofNode | RoofSegmentNode | StairNode | StairSegmentNode
|
node: RoofNode | RoofSegmentNode | StairNode | StairSegmentNode
|
||||||
@@ -118,6 +123,46 @@ export const MoveRoofTool: React.FC<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolveLevelId = () => {
|
||||||
|
if (movingNode.type === 'roof' || movingNode.type === 'stair') {
|
||||||
|
return movingNode.parentId ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(movingNode.type === 'roof-segment' || movingNode.type === 'stair-segment') &&
|
||||||
|
movingNode.parentId
|
||||||
|
) {
|
||||||
|
const parentNode = useScene.getState().nodes[movingNode.parentId as AnyNodeId]
|
||||||
|
return parentNode && 'parentId' in parentNode ? (parentNode.parentId ?? null) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelId = resolveLevelId()
|
||||||
|
const levelNode =
|
||||||
|
levelId && useScene.getState().nodes[levelId as AnyNodeId]?.type === 'level'
|
||||||
|
? (useScene.getState().nodes[levelId as AnyNodeId] as LevelNode)
|
||||||
|
: null
|
||||||
|
const levelChildren = levelNode?.children ?? []
|
||||||
|
const levelWalls = levelChildren
|
||||||
|
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
||||||
|
.filter((node): node is WallNode => node?.type === 'wall')
|
||||||
|
const levelFences = levelChildren
|
||||||
|
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
||||||
|
.filter((node): node is FenceNode => node?.type === 'fence')
|
||||||
|
const buildingId = useViewer.getState().selection.buildingId
|
||||||
|
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
||||||
|
|
||||||
|
const localToWorldPoint = (localPoint: WallPlanPoint, y: number): [number, number, number] => {
|
||||||
|
if (buildingObj) {
|
||||||
|
const worldPoint = buildingObj.localToWorld(new THREE.Vector3(localPoint[0], y, localPoint[1]))
|
||||||
|
return [worldPoint.x, worldPoint.y, worldPoint.z]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [localPoint[0], y, localPoint[1]]
|
||||||
|
}
|
||||||
|
|
||||||
const computeLocal = (
|
const computeLocal = (
|
||||||
gridX: number,
|
gridX: number,
|
||||||
gridZ: number,
|
gridZ: number,
|
||||||
@@ -155,21 +200,21 @@ export const MoveRoofTool: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2
|
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
||||||
const y = event.position[1]
|
const y = event.position[1]
|
||||||
|
|
||||||
if (
|
const snappedLocal = snapFenceDraftPoint({
|
||||||
previousGridPosRef.current &&
|
point: [event.localPosition[0], event.localPosition[2]],
|
||||||
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
walls: levelWalls,
|
||||||
) {
|
fences: levelFences,
|
||||||
|
})
|
||||||
|
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
||||||
|
|
||||||
|
if (previousGridPosRef.current && (gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])) {
|
||||||
sfxEmitter.emit('sfx:grid-snap')
|
sfxEmitter.emit('sfx:grid-snap')
|
||||||
}
|
}
|
||||||
|
|
||||||
previousGridPosRef.current = [gridX, gridZ]
|
previousGridPosRef.current = [gridX, gridZ]
|
||||||
// Cursor is inside the building-local ToolManager group — use local position
|
const [lx, lz] = snappedLocal
|
||||||
const lx = Math.round(event.localPosition[0] * 2) / 2
|
|
||||||
const lz = Math.round(event.localPosition[2] * 2) / 2
|
|
||||||
setCursorWorldPos([lx, event.localPosition[1], lz])
|
setCursorWorldPos([lx, event.localPosition[1], lz])
|
||||||
|
|
||||||
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||||
@@ -189,11 +234,14 @@ export const MoveRoofTool: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGridClick = (event: GridEvent) => {
|
const onGridClick = (event: GridEvent) => {
|
||||||
const gridX = Math.round(event.position[0] * 2) / 2 // world, for computeLocal
|
|
||||||
const gridZ = Math.round(event.position[2] * 2) / 2
|
|
||||||
const y = event.position[1]
|
const y = event.position[1]
|
||||||
const lx = Math.round(event.localPosition[0] * 2) / 2
|
const snappedLocal = snapFenceDraftPoint({
|
||||||
const lz = Math.round(event.localPosition[2] * 2) / 2
|
point: [event.localPosition[0], event.localPosition[2]],
|
||||||
|
walls: levelWalls,
|
||||||
|
fences: levelFences,
|
||||||
|
})
|
||||||
|
const [gridX, , gridZ] = localToWorldPoint(snappedLocal, y)
|
||||||
|
const [lx, lz] = snappedLocal
|
||||||
|
|
||||||
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
const [localX, localZ] = computeLocal(gridX, gridZ, y, lx, lz)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { type AnyNodeId, emitter, type GridEvent, useScene, type SlabNode } from '@pascal-app/core'
|
import {
|
||||||
|
type AnyNodeId,
|
||||||
|
emitter,
|
||||||
|
type FenceNode,
|
||||||
|
type GridEvent,
|
||||||
|
type LevelNode,
|
||||||
|
type SlabNode,
|
||||||
|
useScene,
|
||||||
|
type WallNode,
|
||||||
|
} from '@pascal-app/core'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||||
import useEditor from '../../../store/use-editor'
|
import useEditor from '../../../store/use-editor'
|
||||||
|
import { snapFenceDraftPoint } from '../fence/fence-drafting'
|
||||||
import { CursorSphere } from '../shared/cursor-sphere'
|
import { CursorSphere } from '../shared/cursor-sphere'
|
||||||
|
|
||||||
function snap(value: number) {
|
|
||||||
return Math.round(value * 2) / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
function translatePolygon(
|
function translatePolygon(
|
||||||
polygon: Array<[number, number]>,
|
polygon: Array<[number, number]>,
|
||||||
deltaX: number,
|
deltaX: number,
|
||||||
@@ -56,6 +62,17 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const originalPolygon = originalPolygonRef.current
|
const originalPolygon = originalPolygonRef.current
|
||||||
const originalHoles = originalHolesRef.current
|
const originalHoles = originalHolesRef.current
|
||||||
|
const levelNode =
|
||||||
|
node.parentId && useScene.getState().nodes[node.parentId as AnyNodeId]?.type === 'level'
|
||||||
|
? (useScene.getState().nodes[node.parentId as AnyNodeId] as LevelNode)
|
||||||
|
: null
|
||||||
|
const levelChildren = levelNode?.children ?? []
|
||||||
|
const levelWalls = levelChildren
|
||||||
|
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
||||||
|
.filter((child): child is WallNode => child?.type === 'wall')
|
||||||
|
const levelFences = levelChildren
|
||||||
|
.map((childId) => useScene.getState().nodes[childId as AnyNodeId])
|
||||||
|
.filter((child): child is FenceNode => child?.type === 'fence')
|
||||||
|
|
||||||
useScene.temporal.getState().pause()
|
useScene.temporal.getState().pause()
|
||||||
let wasCommitted = false
|
let wasCommitted = false
|
||||||
@@ -80,8 +97,11 @@ export const MoveSlabTool: React.FC<{ node: SlabNode }> = ({ node }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onGridMove = (event: GridEvent) => {
|
const onGridMove = (event: GridEvent) => {
|
||||||
const localX = snap(event.localPosition[0])
|
const [localX, localZ] = snapFenceDraftPoint({
|
||||||
const localZ = snap(event.localPosition[2])
|
point: [event.localPosition[0], event.localPosition[2]],
|
||||||
|
walls: levelWalls,
|
||||||
|
fences: levelFences,
|
||||||
|
})
|
||||||
|
|
||||||
if (
|
if (
|
||||||
previousGridPosRef.current &&
|
previousGridPosRef.current &&
|
||||||
|
|||||||
Reference in New Issue
Block a user