Fix relative move drag offsets

This commit is contained in:
Aymeric Rabot
2026-06-07 18:18:46 -04:00
parent b2e7a090d6
commit be0f491bbd
21 changed files with 712 additions and 623 deletions
+26 -36
View File
@@ -4,17 +4,19 @@ import {
type AnyNodeId,
emitter,
type RoofEvent,
type RoofNode,
type RoofSegmentNode,
type SolarPanelNode,
sceneRegistry,
useScene,
} from '@pascal-app/core'
import { EDITOR_LAYER, markToolCancelConsumed, triggerSFX, useEditor } from '@pascal-app/editor'
import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import * as THREE from 'three'
import { resolveRoofSegmentHit } from '../shared/roof-segment-hit'
import {
createRelativeRoofDrag,
type RelativeRoofDragTarget,
roofSegmentLocalToBuildingLocal,
} from '../shared/relative-roof-drag'
import { getAnalyticalNormal, surfaceQuatFromNormal } from '../shared/roof-surface'
// MeshBasicMaterial: avoids the WebGPU "Color target has no corresponding
@@ -86,27 +88,18 @@ export default function MoveSolarPanelTool({ node }: { node: SolarPanelNode }) {
const panelObj = sceneRegistry.nodes.get(node.id)
if (panelObj) panelObj.visible = false
const worldToBuildingLocal = (wx: number, wy: number, wz: number): [number, number, number] => {
const buildingId = useViewer.getState().selection.buildingId
const buildingObj = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
if (buildingObj) {
const v = new THREE.Vector3(wx, wy, wz)
buildingObj.worldToLocal(v)
return [v.x, v.y, v.z]
}
return [wx, wy, wz]
}
let lastSnapX = 0
let lastSnapZ = 0
let lastTarget: RelativeRoofDragTarget | null = null
const roofDrag = createRelativeRoofDrag(original)
const updateGhost = (event: RoofEvent) => {
const wx = event.position[0]
const wy = event.position[1]
const wz = event.position[2]
const target = roofDrag.resolve(event)
if (!target) return
lastTarget = target
const sx = Math.round(wx * 20) / 20
const sz = Math.round(wz * 20) / 20
const sx = Math.round(target.localX * 20) / 20
const sz = Math.round(target.localZ * 20) / 20
if (sx !== lastSnapX || sz !== lastSnapZ) {
triggerSFX('sfx:grid-snap')
lastSnapX = sx
@@ -119,35 +112,32 @@ export default function MoveSolarPanelTool({ node }: { node: SolarPanelNode }) {
// because analytical normals are computed in segment-local space
// and the yaw is applied explicitly, avoiding any world-vs-local
// normal mismatch.
const hit = resolveRoofSegmentHit(event.node as RoofNode, wx, wy, wz)
if (!hit) return
const segLocalNormal = getAnalyticalNormal(hit.localX, hit.localZ, hit.segment)
const segLocalNormal = getAnalyticalNormal(target.localX, target.localZ, target.segment)
setPreviewSurfaceQuat(surfaceQuatFromNormal(segLocalNormal, new THREE.Quaternion()))
setPreviewYaw((event.node.rotation ?? 0) + (hit.segment.rotation ?? 0))
setPreviewPos(worldToBuildingLocal(wx, wy, wz))
setPreviewYaw((event.node.rotation ?? 0) + (target.segment.rotation ?? 0))
setPreviewPos(
roofSegmentLocalToBuildingLocal(target.segment.id, [
target.localX,
target.localY,
target.localZ,
]),
)
setHasHit(true)
event.stopPropagation()
}
const onRoofClick = (event: RoofEvent) => {
const roof = event.node as RoofNode
const st = useScene.getState()
const hit = resolveRoofSegmentHit(
roof,
event.position[0],
event.position[1],
event.position[2],
)
if (!hit) return
const target = lastTarget ?? roofDrag.resolve(event)
if (!target) return
const targetSegmentId = hit.segment.id as AnyNodeId
const targetSegmentId = target.segment.id as AnyNodeId
// Compute segment-local normal for the committed node so the
// renderer's surfaceQuat + outer segment.rotation compose to
// the same world orientation the ghost showed.
const segLocalNormal = getAnalyticalNormal(hit.localX, hit.localZ, hit.segment)
const segLocalNormal = getAnalyticalNormal(target.localX, target.localZ, target.segment)
st.updateNode(node.id as AnyNodeId, {
position: original.position,
@@ -161,7 +151,7 @@ export default function MoveSolarPanelTool({ node }: { node: SolarPanelNode }) {
st.updateNode(node.id as AnyNodeId, {
roofSegmentId: targetSegmentId,
parentId: targetSegmentId,
position: [hit.localX, hit.localY, hit.localZ],
position: [target.localX, target.localY, target.localZ],
rotation: original.rotation,
// Segment-local normal — must stay consistent with getAnalyticalNormal
// semantics so the renderer's surfaceQuat is in the correct frame.