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:
Sudhir Yadav
2026-07-24 12:33:58 -04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent f0985df887
commit 7309fc8f1d
23 changed files with 482 additions and 120 deletions
@@ -526,7 +526,6 @@ const PostProcessingPasses = ({
let visualAlpha = contentAlpha
if (outlineEnabled) {
const outlineNode = mergedOutline(scene, camera, {
enabled: () => !useViewer.getState().cameraDragging,
primaryObjects: outliner.selectedObjects,
secondaryObjects: outliner.hoveredObjects,
primaryEdgeThickness: uniform(1),
@@ -5,18 +5,20 @@ import { Object3D, PerspectiveCamera, Scene } from 'three'
import { mergedOutline } from './merged-outline-node'
describe('merged outline rendering', () => {
test('skips outline work while the pass is disabled', () => {
const outline = mergedOutline(new Scene(), new PerspectiveCamera(), {
enabled: () => false,
test('keeps selected outlines active during camera interaction', () => {
const cameraInteractionActive = true
const params = {
enabled: () => !cameraInteractionActive,
primaryObjects: [new Object3D()],
})
}
const outline = mergedOutline(new Scene(), new PerspectiveCamera(), params)
const frame = {
get renderer(): never {
throw new Error('outline renderer should not be touched')
throw new Error('outline renderer was reached')
},
}
expect(() => outline.updateBefore(frame)).not.toThrow()
expect(() => outline.updateBefore(frame)).toThrow('outline renderer was reached')
outline.dispose()
})
})
@@ -125,7 +125,6 @@ export class MergedOutlineNode extends TempNode {
primaryEdgeGlowNode: any
secondaryEdgeGlowNode: any
downSampleRatio: number
enabled: () => boolean
updateBeforeType: string
private readonly _depthRT: RenderTarget
@@ -190,7 +189,6 @@ export class MergedOutlineNode extends TempNode {
primaryEdgeGlow?: any
secondaryEdgeGlow?: any
downSampleRatio?: number
enabled?: () => boolean
} = {},
) {
super('vec4')
@@ -203,7 +201,6 @@ export class MergedOutlineNode extends TempNode {
primaryEdgeGlow = float(0),
secondaryEdgeGlow = float(0),
downSampleRatio = 2,
enabled = () => true,
} = params
this.scene = scene
@@ -215,7 +212,6 @@ export class MergedOutlineNode extends TempNode {
this.primaryEdgeGlowNode = nodeObject(primaryEdgeGlow)
this.secondaryEdgeGlowNode = nodeObject(secondaryEdgeGlow)
this.downSampleRatio = downSampleRatio
this.enabled = enabled
this.updateBeforeType = NodeUpdateType.FRAME
this._depthRT = new RenderTarget()
@@ -305,9 +301,8 @@ export class MergedOutlineNode extends TempNode {
}
updateBefore(frame: any) {
const enabled = this.enabled()
const hasPrimary = enabled && this.primaryObjects.length > 0
const hasSecondary = enabled && this.secondaryObjects.length > 0
const hasPrimary = this.primaryObjects.length > 0
const hasSecondary = this.secondaryObjects.length > 0
const hasAny = hasPrimary || hasSecondary
// Fast-path: nothing to render and nothing was rendered last frame either,