Add roof material targets and UV mapping

This commit is contained in:
sudhir
2026-04-20 12:57:06 +05:30
parent 05002a7865
commit bbc351d3a4
11 changed files with 428 additions and 47 deletions
@@ -5,6 +5,8 @@ import {
emitter,
type ItemNode,
type NodeEvent,
type RoofEvent,
type RoofSegmentEvent,
resolveLevelId,
sceneRegistry,
type StairEvent,
@@ -132,6 +134,21 @@ function resolveStairMaterialTarget(
return null
}
function resolveRoofMaterialTarget(
event: RoofEvent | RoofSegmentEvent,
): 'top' | 'edge' | 'wall' | null {
if (event.materialIndex === 3) return 'top'
if (event.materialIndex === 0) return 'edge'
if (event.materialIndex === 1 || event.materialIndex === 2) return 'wall'
const normalY = event.normal?.[1]
if (normalY !== undefined && normalY > 0.35) return 'top'
if (normalY !== undefined && Math.abs(normalY) <= 0.35) return 'edge'
if (normalY !== undefined && normalY < -0.35) return 'wall'
return null
}
const HIGHLIGHT_PROFILES = {
delete: {
color: new Color('#dc2626'),
@@ -542,6 +559,28 @@ export const SelectionManager = () => {
useEditor.getState().setSelectedStairMaterialTarget(null)
}
if (
(node.type === 'roof' || node.type === 'roof-segment') &&
nodeToSelect.type === 'roof'
) {
const nextRoofMaterialTarget = resolveRoofMaterialTarget(
event as RoofEvent | RoofSegmentEvent,
)
if (nextRoofMaterialTarget) {
useEditor.getState().setSelectedRoofMaterialTarget({
roofId: nodeToSelect.id,
role: nextRoofMaterialTarget,
})
} else {
const currentRoofMaterialTarget = useEditor.getState().selectedRoofMaterialTarget
if (currentRoofMaterialTarget?.roofId !== nodeToSelect.id) {
useEditor.getState().setSelectedRoofMaterialTarget(null)
}
}
} else if (useEditor.getState().selectedRoofMaterialTarget) {
useEditor.getState().setSelectedRoofMaterialTarget(null)
}
// Reset the handled flag after a short delay to allow grid:click to be ignored
setTimeout(() => {
clickHandledRef.current = false
@@ -576,6 +615,7 @@ export const SelectionManager = () => {
if (activeStrategy) activeStrategy.handleDeselect()
useEditor.getState().setSelectedWallMaterialTarget(null)
useEditor.getState().setSelectedStairMaterialTarget(null)
useEditor.getState().setSelectedRoofMaterialTarget(null)
// When deselecting from zone mode, return to structure select
if (phase === 'structure' && structureLayer === 'zones') {
@@ -813,6 +853,8 @@ const SelectionStateSync = () => {
const setSelectedWallMaterialTarget = useEditor((s) => s.setSelectedWallMaterialTarget)
const selectedStairMaterialTarget = useEditor((s) => s.selectedStairMaterialTarget)
const setSelectedStairMaterialTarget = useEditor((s) => s.setSelectedStairMaterialTarget)
const selectedRoofMaterialTarget = useEditor((s) => s.selectedRoofMaterialTarget)
const setSelectedRoofMaterialTarget = useEditor((s) => s.setSelectedRoofMaterialTarget)
const singleSelectedId = useViewer((s) =>
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
)
@@ -883,6 +925,25 @@ const SelectionStateSync = () => {
}
}, [selectedStairMaterialTarget, setSelectedStairMaterialTarget, singleSelectedId])
useEffect(() => {
if (!selectedRoofMaterialTarget) return
if (!singleSelectedId) {
setSelectedRoofMaterialTarget(null)
return
}
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
if (!(selectedNode?.type === 'roof')) {
setSelectedRoofMaterialTarget(null)
return
}
if (selectedRoofMaterialTarget.roofId !== selectedNode.id) {
setSelectedRoofMaterialTarget(null)
}
}, [selectedRoofMaterialTarget, setSelectedRoofMaterialTarget, singleSelectedId])
return null
}
@@ -3,8 +3,10 @@
import {
type AnyNode,
type AnyNodeId,
getEffectiveRoofSurfaceMaterial,
type MaterialSchema,
type RoofNode,
type RoofSurfaceMaterialRole,
RoofNode as RoofNodeSchema,
type RoofSegmentNode,
RoofSegmentNode as RoofSegmentNodeSchema,
@@ -22,12 +24,39 @@ import { PanelSection } from '../controls/panel-section'
import { SliderControl } from '../controls/slider-control'
import { PanelWrapper } from './panel-wrapper'
function buildRoofSurfaceMaterialPatch(
node: RoofNode,
targetRole: RoofSurfaceMaterialRole,
material: MaterialSchema | undefined,
materialPreset: string | undefined,
): Partial<RoofNode> {
const nextSurfaceMaterial = { material, materialPreset }
const nextTop =
targetRole === 'top' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'top')
const nextEdge =
targetRole === 'edge' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'edge')
const nextWall =
targetRole === 'wall' ? nextSurfaceMaterial : getEffectiveRoofSurfaceMaterial(node, 'wall')
return {
topMaterial: nextTop.material,
topMaterialPreset: nextTop.materialPreset,
edgeMaterial: nextEdge.material,
edgeMaterialPreset: nextEdge.materialPreset,
wallMaterial: nextWall.material,
wallMaterialPreset: nextWall.materialPreset,
material: undefined,
materialPreset: undefined,
}
}
export function RoofPanel() {
const selectedId = useViewer((s) => s.selection.selectedIds[0])
const setSelection = useViewer((s) => s.setSelection)
const updateNode = useScene((s) => s.updateNode)
const createNode = useScene((s) => s.createNode)
const setMovingNode = useEditor((s) => s.setMovingNode)
const selectedRoofMaterialTarget = useEditor((s) => s.selectedRoofMaterialTarget)
const node = useScene((s) =>
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
@@ -50,18 +79,27 @@ export function RoofPanel() {
[selectedId, updateNode],
)
const handleMaterialChange = useCallback(
const materialTargetRole =
selectedRoofMaterialTarget && selectedRoofMaterialTarget.roofId === node?.id
? selectedRoofMaterialTarget.role
: null
const materialPickerValue =
node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
const handleTargetedMaterialChange = useCallback(
(material: MaterialSchema) => {
handleUpdate({ material, materialPreset: undefined })
if (!node || !materialTargetRole) return
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, material, undefined))
},
[handleUpdate],
[handleUpdate, materialTargetRole, node],
)
const handleMaterialPresetChange = useCallback(
const handleTargetedMaterialPresetChange = useCallback(
(materialPreset: string) => {
handleUpdate({ materialPreset, material: undefined })
if (!node || !materialTargetRole) return
handleUpdate(buildRoofSurfaceMaterialPatch(node, materialTargetRole, undefined, materialPreset))
},
[handleUpdate],
[handleUpdate, materialTargetRole, node],
)
const handleClose = useCallback(() => {
@@ -267,13 +305,21 @@ export function RoofPanel() {
</ActionGroup>
</PanelSection>
<PanelSection title="Material">
<MaterialPicker
nodeType="roof"
onChange={handleMaterialChange}
onSelectMaterialPreset={handleMaterialPresetChange}
selectedMaterialPreset={node.materialPreset}
value={node.material}
/>
{!materialTargetRole ? (
<div className="mb-3 rounded-lg border border-border/50 bg-[#2C2C2E] px-3 py-2 text-[11px] text-muted-foreground">
Click the roof surface you want to edit. Materials apply to one target at a time.
</div>
) : null}
{materialTargetRole ? (
<MaterialPicker
hideSideControl
nodeType="roof"
onChange={handleTargetedMaterialChange}
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
selectedMaterialPreset={materialPickerValue.materialPreset}
value={materialPickerValue.material}
/>
) : null}
</PanelSection>
</PanelWrapper>
)
+10
View File
@@ -8,6 +8,7 @@ import {
type FenceNode,
type ItemNode,
type LevelNode,
type RoofSurfaceMaterialRole,
type RoofNode,
type RoofSegmentNode,
type SlabNode,
@@ -93,6 +94,11 @@ export type SelectedStairMaterialTarget = {
role: StairMaterialTargetRole
}
export type SelectedRoofMaterialTarget = {
roofId: RoofNode['id']
role: RoofSurfaceMaterialRole
}
type EditorState = {
phase: Phase
setPhase: (phase: Phase) => void
@@ -144,6 +150,8 @@ type EditorState = {
setSelectedWallMaterialTarget: (target: SelectedWallMaterialTarget | null) => void
selectedStairMaterialTarget: SelectedStairMaterialTarget | null
setSelectedStairMaterialTarget: (target: SelectedStairMaterialTarget | null) => void
selectedRoofMaterialTarget: SelectedRoofMaterialTarget | null
setSelectedRoofMaterialTarget: (target: SelectedRoofMaterialTarget | null) => void
selectedReferenceId: string | null
setSelectedReferenceId: (id: string | null) => void
// Space detection for cutaway mode
@@ -523,6 +531,8 @@ const useEditor = create<EditorState>()(
setSelectedWallMaterialTarget: (target) => set({ selectedWallMaterialTarget: target }),
selectedStairMaterialTarget: null,
setSelectedStairMaterialTarget: (target) => set({ selectedStairMaterialTarget: target }),
selectedRoofMaterialTarget: null,
setSelectedRoofMaterialTarget: (target) => set({ selectedRoofMaterialTarget: target }),
selectedReferenceId: null,
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
spaces: {},