Add roof material targets and UV mapping
This commit is contained in:
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
Reference in New Issue
Block a user