editor: stabilize floorplan and camera interactions (#545)
* Add roof surface placement support for items Items (e.g. solar panels) can now be placed on sloped roof surfaces. The placement system computes euler rotation from the roof surface normal so items sit flush on the slope instead of going inside. - Add roofStrategy to placement-strategies with enter/move/click/leave - Wire roof:enter/move/click/leave events in the placement coordinator - Add calculateRoofRotation in placement-math using surface normals - Support full 3D cursor rotation for sloped surfaces - Items on roofs are parented to the level with world-space rotation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fixed conflict * fix(viewer): keep outlines during camera movement * fix(editor): prevent floorplan clipping during rotation * fix(editor): keep compass rotation in sync Stream live headings in 2D and 3D, and defer restoring the compositor rotation preview until the committed floor-plan state is ready to paint so pointer release cannot snap back. * fix(editor): stabilize floorplan and camera sync * fix floorplan registry scale subscriptions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f0985df887
commit
7309fc8f1d
@@ -0,0 +1,50 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { type AnyNodeId, useLiveNodeOverrides, useScene, WallNode } from '@pascal-app/core'
|
||||
import { wallCurveAffordance } from './floorplan-affordances'
|
||||
|
||||
globalThis.requestAnimationFrame ??= (callback) => {
|
||||
callback(0)
|
||||
return 0
|
||||
}
|
||||
globalThis.cancelAnimationFrame ??= () => {}
|
||||
|
||||
const modifiers = {
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false,
|
||||
}
|
||||
|
||||
describe('wall center curve handle release', () => {
|
||||
afterEach(() => {
|
||||
useLiveNodeOverrides.getState().clearAll()
|
||||
})
|
||||
|
||||
test('persists the previewed curve offset after commit clears the override', () => {
|
||||
const wall = WallNode.parse({
|
||||
id: 'wall_curve-release',
|
||||
parentId: null,
|
||||
start: [0, 0],
|
||||
end: [4, 0],
|
||||
})
|
||||
useScene.setState({ nodes: { [wall.id]: wall } as never })
|
||||
|
||||
const session = wallCurveAffordance.start({
|
||||
node: wall,
|
||||
payload: { wallId: wall.id },
|
||||
nodes: useScene.getState().nodes,
|
||||
initialPlanPoint: [2, 0],
|
||||
gridSnapStep: 0.1,
|
||||
})
|
||||
session.apply({ planPoint: [2, 1], modifiers })
|
||||
|
||||
expect((useScene.getState().nodes[wall.id] as typeof wall).curveOffset ?? 0).toBe(0)
|
||||
expect(useLiveNodeOverrides.getState().get(wall.id as AnyNodeId)?.curveOffset).not.toBe(0)
|
||||
|
||||
expect(session.canCommit()).toBe(true)
|
||||
session.commit?.()
|
||||
|
||||
expect((useScene.getState().nodes[wall.id] as typeof wall).curveOffset).not.toBe(0)
|
||||
expect(useLiveNodeOverrides.getState().get(wall.id as AnyNodeId)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user