editor: improve floorplan navigation performance (#535)
* 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(editor): defer floorplan annotation collision layout * fix(viewer): suspend outlines during camera movement * fix(editor): keep floorplan zoom off the render hot path * fix(editor): keep floorplan pan off render hot path * fix(editor): keep floorplan rotation off render hot path * fix(editor): keep rotation backdrop white * fix(editor): finalize floorplan navigation safely * fix(editor): keep navigation interaction state current * fix(editor): stop floorplan animation before pan --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
26c9e17fed
commit
fcac55ca30
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type ConstructionDimensionDatumPolicy,
|
||||
type ConstructionDimensionDrawingPresentation,
|
||||
@@ -86,16 +87,6 @@ export default function ConstructionDimensionPanel() {
|
||||
const node = selectedId ? state.nodes[selectedId as AnyNodeId] : undefined
|
||||
return node?.type === 'construction-dimension' ? node : null
|
||||
})
|
||||
const foundationControllers = useScene(
|
||||
useShallow((state) =>
|
||||
Object.values(state.nodes).filter(
|
||||
(candidate): candidate is ConstructionDimensionNode =>
|
||||
candidate.type === 'construction-dimension' &&
|
||||
candidate.id !== dimension?.id &&
|
||||
candidate.drawingType === 'foundation-plan',
|
||||
),
|
||||
),
|
||||
)
|
||||
const updateNode = useScene((state) => state.updateNode)
|
||||
const deleteNode = useScene((state) => state.deleteNode)
|
||||
const activeDrawingType = useDrawingView((state) => state.drawingType)
|
||||
@@ -127,10 +118,14 @@ export default function ConstructionDimensionPanel() {
|
||||
drawingType,
|
||||
presentation,
|
||||
)
|
||||
const firstFoundationController =
|
||||
presentation === 'controlled' && !dimension.controllingDimensionId
|
||||
? selectFoundationControllers(useScene.getState().nodes, dimension.id)[0]
|
||||
: undefined
|
||||
update({
|
||||
drawingOverrides,
|
||||
...(presentation === 'controlled' && !dimension.controllingDimensionId
|
||||
? { controllingDimensionId: foundationControllers[0]?.id ?? null }
|
||||
? { controllingDimensionId: firstFoundationController?.id ?? null }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
@@ -207,21 +202,13 @@ export default function ConstructionDimensionPanel() {
|
||||
value={activePresentation}
|
||||
/>
|
||||
{activeDrawingType === 'floor-plan' && activePresentation === 'controlled' ? (
|
||||
<SelectField
|
||||
disabled={foundationControllers.length === 0}
|
||||
label="Foundation controller"
|
||||
<FoundationControllerField
|
||||
dimensionId={dimension.id}
|
||||
onChange={(controllingDimensionId) =>
|
||||
update({
|
||||
controllingDimensionId: controllingDimensionId as NonNullable<
|
||||
ConstructionDimensionNode['controllingDimensionId']
|
||||
>,
|
||||
controllingDimensionId,
|
||||
})
|
||||
}
|
||||
options={foundationControllers.map((controller) => ({
|
||||
label: controller.name || 'Foundation dimension',
|
||||
value: controller.id,
|
||||
}))}
|
||||
placeholder="No foundation dimensions"
|
||||
value={dimension.controllingDimensionId ?? ''}
|
||||
/>
|
||||
) : null}
|
||||
@@ -339,6 +326,51 @@ export default function ConstructionDimensionPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function selectFoundationControllers(
|
||||
nodes: Record<string, AnyNode>,
|
||||
excludedId: AnyNodeId,
|
||||
): ConstructionDimensionNode[] {
|
||||
return Object.values(nodes).filter(
|
||||
(candidate): candidate is ConstructionDimensionNode =>
|
||||
candidate.type === 'construction-dimension' &&
|
||||
candidate.id !== excludedId &&
|
||||
candidate.drawingType === 'foundation-plan',
|
||||
)
|
||||
}
|
||||
|
||||
function FoundationControllerField({
|
||||
dimensionId,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
dimensionId: AnyNodeId
|
||||
value: string
|
||||
onChange: (value: NonNullable<ConstructionDimensionNode['controllingDimensionId']>) => void
|
||||
}) {
|
||||
const foundationControllers = useScene(
|
||||
useShallow((state) => selectFoundationControllers(state.nodes, dimensionId)),
|
||||
)
|
||||
return (
|
||||
<SelectField
|
||||
disabled={foundationControllers.length === 0}
|
||||
label="Foundation controller"
|
||||
onChange={(controllingDimensionId) =>
|
||||
onChange(
|
||||
controllingDimensionId as NonNullable<
|
||||
ConstructionDimensionNode['controllingDimensionId']
|
||||
>,
|
||||
)
|
||||
}
|
||||
options={foundationControllers.map((controller) => ({
|
||||
label: controller.name || 'Foundation dimension',
|
||||
value: controller.id,
|
||||
}))}
|
||||
placeholder="No foundation dimensions"
|
||||
value={value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function parseSuppressedSegments(value: string): number[] {
|
||||
return [
|
||||
...new Set(
|
||||
|
||||
Reference in New Issue
Block a user