Unify material target selection across wall stair and roof

This commit is contained in:
sudhir
2026-04-20 22:45:56 +05:30
parent dd5b6e5e83
commit 8cf2df1483
8 changed files with 152 additions and 195 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
import type { ThreeEvent } from '@react-three/fiber' import type { ThreeEvent } from '@react-three/fiber'
import type { Object3D } from 'three'
import mitt from 'mitt' import mitt from 'mitt'
import type { import type {
BuildingNode, BuildingNode,
@@ -39,7 +40,7 @@ export interface NodeEvent<T extends AnyNode = AnyNode> {
localPosition: [number, number, number] localPosition: [number, number, number]
normal?: [number, number, number] normal?: [number, number, number]
faceIndex?: number faceIndex?: number
materialIndex?: number object: Object3D
stopPropagation: () => void stopPropagation: () => void
nativeEvent: ThreeEvent<PointerEvent> nativeEvent: ThreeEvent<PointerEvent>
} }
@@ -11,6 +11,7 @@ import {
sceneRegistry, sceneRegistry,
type StairEvent, type StairEvent,
type StairNode, type StairNode,
type StairSurfaceMaterialRole,
type StairSegmentEvent, type StairSegmentEvent,
useScene, useScene,
type WallEvent, type WallEvent,
@@ -19,9 +20,9 @@ import {
import { useViewer } from '@pascal-app/viewer' import { useViewer } from '@pascal-app/viewer'
import { useCallback, useEffect, useRef } from 'react' import { useCallback, useEffect, useRef } from 'react'
import { Color, type Material, type Mesh, type Object3D } from 'three' import { Color, type BufferGeometry, type Material, type Mesh, type Object3D } from 'three'
import { sfxEmitter } from '../../lib/sfx-bus' import { sfxEmitter } from '../../lib/sfx-bus'
import useEditor, { type Phase, type StairMaterialTargetRole, type StructureLayer } from './../../store/use-editor' import useEditor, { type MaterialTargetRole, type Phase, type StructureLayer } from './../../store/use-editor'
import { boxSelectHandled } from '../tools/select/box-select-tool' import { boxSelectHandled } from '../tools/select/box-select-tool'
const isNodeInCurrentLevel = (node: AnyNode): boolean => { const isNodeInCurrentLevel = (node: AnyNode): boolean => {
@@ -76,8 +77,9 @@ export const resolveBuildingId = (
} }
function resolveWallMaterialTarget(event: WallEvent): WallSurfaceSide | null { function resolveWallMaterialTarget(event: WallEvent): WallSurfaceSide | null {
if (event.materialIndex === 1) return 'interior' const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
if (event.materialIndex === 2) return 'exterior' if (materialIndex === 1) return 'interior'
if (materialIndex === 2) return 'exterior'
const normalZ = event.normal?.[2] const normalZ = event.normal?.[2]
const localZ = event.localPosition[2] const localZ = event.localPosition[2]
@@ -103,8 +105,9 @@ function resolveWallMaterialTarget(event: WallEvent): WallSurfaceSide | null {
function resolveStairMaterialTarget( function resolveStairMaterialTarget(
event: StairEvent | StairSegmentEvent, event: StairEvent | StairSegmentEvent,
): StairMaterialTargetRole | null { ): StairSurfaceMaterialRole | null {
const hitObjectName = event.nativeEvent.object?.name ?? '' const hitObjectName = event.nativeEvent.object?.name ?? ''
const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
if (hitObjectName.startsWith('stair-railing')) { if (hitObjectName.startsWith('stair-railing')) {
return 'railing' return 'railing'
@@ -114,11 +117,11 @@ function resolveStairMaterialTarget(
return 'side' return 'side'
} }
if (event.materialIndex === 0) { if (materialIndex === 0) {
return 'tread' return 'tread'
} }
if (event.materialIndex === 1) { if (materialIndex === 1) {
return 'side' return 'side'
} }
@@ -137,9 +140,10 @@ function resolveStairMaterialTarget(
function resolveRoofMaterialTarget( function resolveRoofMaterialTarget(
event: RoofEvent | RoofSegmentEvent, event: RoofEvent | RoofSegmentEvent,
): 'top' | 'edge' | 'wall' | null { ): 'top' | 'edge' | 'wall' | null {
if (event.materialIndex === 3) return 'top' const materialIndex = getIntersectionMaterialIndex(getEventObject(event), event.faceIndex)
if (event.materialIndex === 0) return 'edge' if (materialIndex === 3) return 'top'
if (event.materialIndex === 1 || event.materialIndex === 2) return 'wall' if (materialIndex === 0) return 'edge'
if (materialIndex === 1 || materialIndex === 2) return 'wall'
const normalY = event.normal?.[1] const normalY = event.normal?.[1]
if (normalY !== undefined && normalY > 0.35) return 'top' if (normalY !== undefined && normalY > 0.35) return 'top'
@@ -149,6 +153,46 @@ function resolveRoofMaterialTarget(
return null return null
} }
function getEventObject(event: NodeEvent): Object3D {
const eventWithObject = event as NodeEvent & { object?: Object3D }
return eventWithObject.object ?? event.nativeEvent.object
}
function getIntersectionMaterialIndex(
object: Object3D,
faceIndex: number | undefined,
): number | undefined {
if (faceIndex === undefined) return undefined
const geometry = (object as Mesh).geometry as BufferGeometry | undefined
if (!geometry || geometry.groups.length === 0) return undefined
const triangleStart = faceIndex * 3
const group = geometry.groups.find(
(entry) => triangleStart >= entry.start && triangleStart < entry.start + entry.count,
)
return group?.materialIndex
}
function setSelectedMaterialTargetForNode(
node: AnyNode,
role: MaterialTargetRole | null,
) {
if (!role) {
const currentTarget = useEditor.getState().selectedMaterialTarget
if (currentTarget?.nodeId !== node.id) {
useEditor.getState().setSelectedMaterialTarget(null)
}
return
}
useEditor.getState().setSelectedMaterialTarget({
nodeId: node.id as AnyNodeId,
role,
})
}
const HIGHLIGHT_PROFILES = { const HIGHLIGHT_PROFILES = {
delete: { delete: {
color: new Color('#dc2626'), color: new Color('#dc2626'),
@@ -520,65 +564,40 @@ export const SelectionManager = () => {
activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current) activeStrategy.handleSelect(nodeToSelect, event.nativeEvent, modifierKeysRef.current)
let nextMaterialTargetHandled = false
if (node.type === 'wall' && nodeToSelect.type === 'wall') { if (node.type === 'wall' && nodeToSelect.type === 'wall') {
const nextWallMaterialTarget = resolveWallMaterialTarget(event as WallEvent) setSelectedMaterialTargetForNode(
if (nextWallMaterialTarget) { nodeToSelect,
useEditor.getState().setSelectedWallMaterialTarget({ resolveWallMaterialTarget(event as WallEvent),
wallId: nodeToSelect.id, )
side: nextWallMaterialTarget, nextMaterialTargetHandled = true
})
} else {
const currentWallMaterialTarget = useEditor.getState().selectedWallMaterialTarget
if (currentWallMaterialTarget?.wallId !== nodeToSelect.id) {
useEditor.getState().setSelectedWallMaterialTarget(null)
}
}
} else if (useEditor.getState().selectedWallMaterialTarget) {
useEditor.getState().setSelectedWallMaterialTarget(null)
} }
if ( if (
(node.type === 'stair' || node.type === 'stair-segment') && (node.type === 'stair' || node.type === 'stair-segment') &&
nodeToSelect.type === 'stair' nodeToSelect.type === 'stair'
) { ) {
const nextStairMaterialTarget = resolveStairMaterialTarget( setSelectedMaterialTargetForNode(
event as StairEvent | StairSegmentEvent, nodeToSelect,
resolveStairMaterialTarget(event as StairEvent | StairSegmentEvent),
) )
if (nextStairMaterialTarget) { nextMaterialTargetHandled = true
useEditor.getState().setSelectedStairMaterialTarget({
stairId: nodeToSelect.id,
role: nextStairMaterialTarget,
})
} else {
const currentStairMaterialTarget = useEditor.getState().selectedStairMaterialTarget
if (currentStairMaterialTarget?.stairId !== nodeToSelect.id) {
useEditor.getState().setSelectedStairMaterialTarget(null)
}
}
} else if (useEditor.getState().selectedStairMaterialTarget) {
useEditor.getState().setSelectedStairMaterialTarget(null)
} }
if ( if (
(node.type === 'roof' || node.type === 'roof-segment') && (node.type === 'roof' || node.type === 'roof-segment') &&
nodeToSelect.type === 'roof' nodeToSelect.type === 'roof'
) { ) {
const nextRoofMaterialTarget = resolveRoofMaterialTarget( setSelectedMaterialTargetForNode(
event as RoofEvent | RoofSegmentEvent, nodeToSelect,
resolveRoofMaterialTarget(event as RoofEvent | RoofSegmentEvent),
) )
if (nextRoofMaterialTarget) { nextMaterialTargetHandled = true
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) { if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) {
useEditor.getState().setSelectedRoofMaterialTarget(null) useEditor.getState().setSelectedMaterialTarget(null)
} }
// Reset the handled flag after a short delay to allow grid:click to be ignored // Reset the handled flag after a short delay to allow grid:click to be ignored
@@ -613,9 +632,7 @@ export const SelectionManager = () => {
const { phase, structureLayer } = useEditor.getState() const { phase, structureLayer } = useEditor.getState()
const activeStrategy = SELECTION_STRATEGIES[phase] const activeStrategy = SELECTION_STRATEGIES[phase]
if (activeStrategy) activeStrategy.handleDeselect() if (activeStrategy) activeStrategy.handleDeselect()
useEditor.getState().setSelectedWallMaterialTarget(null) useEditor.getState().setSelectedMaterialTarget(null)
useEditor.getState().setSelectedStairMaterialTarget(null)
useEditor.getState().setSelectedRoofMaterialTarget(null)
// When deselecting from zone mode, return to structure select // When deselecting from zone mode, return to structure select
if (phase === 'structure' && structureLayer === 'zones') { if (phase === 'structure' && structureLayer === 'zones') {
@@ -849,12 +866,8 @@ export const SelectionManager = () => {
} }
const SelectionStateSync = () => { const SelectionStateSync = () => {
const selectedWallMaterialTarget = useEditor((s) => s.selectedWallMaterialTarget) const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
const setSelectedWallMaterialTarget = useEditor((s) => s.setSelectedWallMaterialTarget) const setSelectedMaterialTarget = useEditor((s) => s.setSelectedMaterialTarget)
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) => const singleSelectedId = useViewer((s) =>
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null, s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : null,
) )
@@ -888,61 +901,26 @@ const SelectionStateSync = () => {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (!selectedWallMaterialTarget) return if (!selectedMaterialTarget) return
if (!singleSelectedId) { if (!singleSelectedId) {
setSelectedWallMaterialTarget(null) setSelectedMaterialTarget(null)
return return
} }
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId] const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
if (!(selectedNode?.type === 'wall')) { if (
setSelectedWallMaterialTarget(null) !selectedNode ||
(selectedNode.type !== 'wall' && selectedNode.type !== 'stair' && selectedNode.type !== 'roof')
) {
setSelectedMaterialTarget(null)
return return
} }
if (selectedWallMaterialTarget.wallId !== selectedNode.id) { if (selectedMaterialTarget.nodeId !== selectedNode.id) {
setSelectedWallMaterialTarget(null) setSelectedMaterialTarget(null)
} }
}, [selectedWallMaterialTarget, setSelectedWallMaterialTarget, singleSelectedId]) }, [selectedMaterialTarget, setSelectedMaterialTarget, singleSelectedId])
useEffect(() => {
if (!selectedStairMaterialTarget) return
if (!singleSelectedId) {
setSelectedStairMaterialTarget(null)
return
}
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
if (!(selectedNode?.type === 'stair')) {
setSelectedStairMaterialTarget(null)
return
}
if (selectedStairMaterialTarget.stairId !== selectedNode.id) {
setSelectedStairMaterialTarget(null)
}
}, [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 return null
} }
@@ -15,6 +15,7 @@ type MaterialPickerProps = {
onChange?: (material: MaterialSchema) => void onChange?: (material: MaterialSchema) => void
onSelectMaterialPreset?: (materialPreset: string) => void onSelectMaterialPreset?: (materialPreset: string) => void
hideSideControl?: boolean hideSideControl?: boolean
disabled?: boolean
} }
export function MaterialPicker({ export function MaterialPicker({
@@ -24,6 +25,7 @@ export function MaterialPicker({
onChange, onChange,
onSelectMaterialPreset, onSelectMaterialPreset,
hideSideControl = false, hideSideControl = false,
disabled = false,
}: MaterialPickerProps) { }: MaterialPickerProps) {
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties) const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
const catalogItems = nodeType ? getMaterialsForTarget(nodeType) : [] const catalogItems = nodeType ? getMaterialsForTarget(nodeType) : []
@@ -44,11 +46,13 @@ export function MaterialPicker({
selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined) selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined)
const handleCatalogSelect = (materialId: string) => { const handleCatalogSelect = (materialId: string) => {
if (disabled) return
setShowCustom(false) setShowCustom(false)
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId)) onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
} }
const handleCustomOpen = () => { const handleCustomOpen = () => {
if (disabled) return
setShowCustom(true) setShowCustom(true)
onChange?.({ onChange?.({
preset: 'custom', preset: 'custom',
@@ -67,6 +71,7 @@ export function MaterialPicker({
prop: keyof typeof currentProps, prop: keyof typeof currentProps,
val: (typeof currentProps)[keyof typeof currentProps], val: (typeof currentProps)[keyof typeof currentProps],
) => { ) => {
if (disabled) return
onChange?.({ onChange?.({
preset: 'custom', preset: 'custom',
properties: { properties: {
@@ -77,7 +82,7 @@ export function MaterialPicker({
} }
return ( return (
<div className="space-y-3"> <div className={`space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
{(catalogItems.length > 0 || onChange) && ( {(catalogItems.length > 0 || onChange) && (
<div className="space-y-2"> <div className="space-y-2">
{catalogItems.length > 0 ? ( {catalogItems.length > 0 ? (
@@ -56,7 +56,7 @@ export function RoofPanel() {
const updateNode = useScene((s) => s.updateNode) const updateNode = useScene((s) => s.updateNode)
const createNode = useScene((s) => s.createNode) const createNode = useScene((s) => s.createNode)
const setMovingNode = useEditor((s) => s.setMovingNode) const setMovingNode = useEditor((s) => s.setMovingNode)
const selectedRoofMaterialTarget = useEditor((s) => s.selectedRoofMaterialTarget) const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
const node = useScene((s) => const node = useScene((s) =>
selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined, selectedId ? (s.nodes[selectedId as AnyNode['id']] as RoofNode | undefined) : undefined,
@@ -80,8 +80,12 @@ export function RoofPanel() {
) )
const materialTargetRole = const materialTargetRole =
selectedRoofMaterialTarget && selectedRoofMaterialTarget.roofId === node?.id selectedMaterialTarget &&
? selectedRoofMaterialTarget.role selectedMaterialTarget.nodeId === node?.id &&
(selectedMaterialTarget.role === 'top' ||
selectedMaterialTarget.role === 'edge' ||
selectedMaterialTarget.role === 'wall')
? selectedMaterialTarget.role
: null : null
const materialPickerValue = const materialPickerValue =
node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {} node && materialTargetRole ? getEffectiveRoofSurfaceMaterial(node, materialTargetRole) : {}
@@ -312,8 +316,8 @@ export function RoofPanel() {
Click the roof surface you want to edit. Materials apply to one target at a time. Click the roof surface you want to edit. Materials apply to one target at a time.
</div> </div>
) : null} ) : null}
{materialTargetRole ? (
<MaterialPicker <MaterialPicker
disabled={!materialTargetRole}
hideSideControl hideSideControl
nodeType="roof" nodeType="roof"
onChange={handleTargetedMaterialChange} onChange={handleTargetedMaterialChange}
@@ -321,7 +325,6 @@ export function RoofPanel() {
selectedMaterialPreset={materialPickerValue.materialPreset} selectedMaterialPreset={materialPickerValue.materialPreset}
value={materialPickerValue.material} value={materialPickerValue.material}
/> />
) : null}
</PanelSection> </PanelSection>
</PanelWrapper> </PanelWrapper>
) )
@@ -90,7 +90,7 @@ export function StairPanel() {
const createNode = useScene((s) => s.createNode) const createNode = useScene((s) => s.createNode)
const createNodes = useScene((s) => s.createNodes) const createNodes = useScene((s) => s.createNodes)
const setMovingNode = useEditor((s) => s.setMovingNode) const setMovingNode = useEditor((s) => s.setMovingNode)
const selectedStairMaterialTarget = useEditor((s) => s.selectedStairMaterialTarget) const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
const node = useScene((s) => const node = useScene((s) =>
selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined, selectedId ? (s.nodes[selectedId as AnyNode['id']] as StairNode | undefined) : undefined,
@@ -122,8 +122,12 @@ export function StairPanel() {
) )
const materialTargetRole = const materialTargetRole =
selectedStairMaterialTarget && selectedStairMaterialTarget.stairId === node?.id selectedMaterialTarget &&
? selectedStairMaterialTarget.role selectedMaterialTarget.nodeId === node?.id &&
(selectedMaterialTarget.role === 'railing' ||
selectedMaterialTarget.role === 'tread' ||
selectedMaterialTarget.role === 'side')
? selectedMaterialTarget.role
: null : null
const materialPickerValue = const materialPickerValue =
node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {} node && materialTargetRole ? getEffectiveStairSurfaceMaterial(node, materialTargetRole) : {}
@@ -612,8 +616,8 @@ export function StairPanel() {
Click the stair surface you want to edit. Materials apply to one target at a time. Click the stair surface you want to edit. Materials apply to one target at a time.
</div> </div>
) : null} ) : null}
{materialTargetRole ? (
<MaterialPicker <MaterialPicker
disabled={!materialTargetRole}
hideSideControl hideSideControl
nodeType="stair" nodeType="stair"
onChange={handleTargetedMaterialChange} onChange={handleTargetedMaterialChange}
@@ -621,7 +625,6 @@ export function StairPanel() {
selectedMaterialPreset={materialPickerValue.materialPreset} selectedMaterialPreset={materialPickerValue.materialPreset}
value={materialPickerValue.material} value={materialPickerValue.material}
/> />
) : null}
</PanelSection> </PanelSection>
</PanelWrapper> </PanelWrapper>
) )
@@ -57,7 +57,7 @@ export function WallPanel() {
const updateNode = useScene((s) => s.updateNode) const updateNode = useScene((s) => s.updateNode)
const setMovingNode = useEditor((s) => s.setMovingNode) const setMovingNode = useEditor((s) => s.setMovingNode)
const setCurvingWall = useEditor((s) => s.setCurvingWall) const setCurvingWall = useEditor((s) => s.setCurvingWall)
const selectedWallMaterialTarget = useEditor((s) => s.selectedWallMaterialTarget) const selectedMaterialTarget = useEditor((s) => s.selectedMaterialTarget)
const node = useScene((s) => const node = useScene((s) =>
selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined, selectedId ? (s.nodes[selectedId as AnyNode['id']] as WallNode | undefined) : undefined,
@@ -103,8 +103,10 @@ export function WallPanel() {
[effectiveExteriorMaterial, effectiveInteriorMaterial], [effectiveExteriorMaterial, effectiveInteriorMaterial],
) )
const materialTargetSide = const materialTargetSide =
selectedWallMaterialTarget && selectedWallMaterialTarget.wallId === node?.id selectedMaterialTarget &&
? selectedWallMaterialTarget.side selectedMaterialTarget.nodeId === node?.id &&
(selectedMaterialTarget.role === 'interior' || selectedMaterialTarget.role === 'exterior')
? selectedMaterialTarget.role
: null : null
const materialPickerValue = const materialPickerValue =
materialTargetSide === 'interior' materialTargetSide === 'interior'
@@ -241,8 +243,8 @@ export function WallPanel() {
Click the wall face you want to edit. Materials now apply to one side at a time. Click the wall face you want to edit. Materials now apply to one side at a time.
</div> </div>
) : null} ) : null}
{materialTargetSide ? (
<MaterialPicker <MaterialPicker
disabled={!materialTargetSide}
hideSideControl hideSideControl
nodeType="wall" nodeType="wall"
onChange={handleCustomMaterialChange} onChange={handleCustomMaterialChange}
@@ -250,7 +252,6 @@ export function WallPanel() {
selectedMaterialPreset={materialPickerValue.materialPreset} selectedMaterialPreset={materialPickerValue.materialPreset}
value={materialPickerValue.material} value={materialPickerValue.material}
/> />
) : null}
</PanelSection> </PanelSection>
<PanelSection title="Actions"> <PanelSection title="Actions">
+11 -27
View File
@@ -1,7 +1,8 @@
'use client' 'use client'
import type { AssetInput } from '@pascal-app/core'
import { import {
type AnyNodeId,
type AssetInput,
type BuildingNode, type BuildingNode,
type CeilingNode, type CeilingNode,
type DoorNode, type DoorNode,
@@ -13,6 +14,7 @@ import {
type RoofSegmentNode, type RoofSegmentNode,
type SlabNode, type SlabNode,
type Space, type Space,
type StairSurfaceMaterialRole,
type StairNode, type StairNode,
type StairSegmentNode, type StairSegmentNode,
useScene, useScene,
@@ -82,21 +84,11 @@ export type MovingWallEndpoint = {
endpoint: 'start' | 'end' endpoint: 'start' | 'end'
} }
export type SelectedWallMaterialTarget = { export type MaterialTargetRole = WallSurfaceSide | StairSurfaceMaterialRole | RoofSurfaceMaterialRole
wallId: WallNode['id']
side: WallSurfaceSide
}
export type StairMaterialTargetRole = 'railing' | 'tread' | 'side' export type SelectedMaterialTarget = {
nodeId: AnyNodeId
export type SelectedStairMaterialTarget = { role: MaterialTargetRole
stairId: StairNode['id']
role: StairMaterialTargetRole
}
export type SelectedRoofMaterialTarget = {
roofId: RoofNode['id']
role: RoofSurfaceMaterialRole
} }
type EditorState = { type EditorState = {
@@ -146,12 +138,8 @@ type EditorState = {
setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
curvingWall: WallNode | null curvingWall: WallNode | null
setCurvingWall: (wall: WallNode | null) => void setCurvingWall: (wall: WallNode | null) => void
selectedWallMaterialTarget: SelectedWallMaterialTarget | null selectedMaterialTarget: SelectedMaterialTarget | null
setSelectedWallMaterialTarget: (target: SelectedWallMaterialTarget | null) => void setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
selectedStairMaterialTarget: SelectedStairMaterialTarget | null
setSelectedStairMaterialTarget: (target: SelectedStairMaterialTarget | null) => void
selectedRoofMaterialTarget: SelectedRoofMaterialTarget | null
setSelectedRoofMaterialTarget: (target: SelectedRoofMaterialTarget | null) => void
selectedReferenceId: string | null selectedReferenceId: string | null
setSelectedReferenceId: (id: string | null) => void setSelectedReferenceId: (id: string | null) => void
// Space detection for cutaway mode // Space detection for cutaway mode
@@ -527,12 +515,8 @@ const useEditor = create<EditorState>()(
setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }), setMovingWallEndpoint: (value) => set({ movingWallEndpoint: value }),
curvingWall: null, curvingWall: null,
setCurvingWall: (wall) => set({ curvingWall: wall }), setCurvingWall: (wall) => set({ curvingWall: wall }),
selectedWallMaterialTarget: null, selectedMaterialTarget: null,
setSelectedWallMaterialTarget: (target) => set({ selectedWallMaterialTarget: target }), setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
selectedStairMaterialTarget: null,
setSelectedStairMaterialTarget: (target) => set({ selectedStairMaterialTarget: target }),
selectedRoofMaterialTarget: null,
setSelectedRoofMaterialTarget: (target) => set({ selectedRoofMaterialTarget: target }),
selectedReferenceId: null, selectedReferenceId: null,
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }), setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
spaces: {}, spaces: {},
+1 -19
View File
@@ -33,7 +33,6 @@ import {
type ZoneNode, type ZoneNode,
} from '@pascal-app/core' } from '@pascal-app/core'
import type { ThreeEvent } from '@react-three/fiber' import type { ThreeEvent } from '@react-three/fiber'
import type { BufferGeometry, Mesh } from 'three'
import useViewer from '../store/use-viewer' import useViewer from '../store/use-viewer'
type NodeConfig = { type NodeConfig = {
@@ -56,23 +55,6 @@ type NodeConfig = {
type NodeType = keyof NodeConfig type NodeType = keyof NodeConfig
function getIntersectionMaterialIndex(
object: ThreeEvent<PointerEvent>['object'],
faceIndex: number | undefined,
): number | undefined {
if (faceIndex === undefined) return undefined
const geometry = (object as Mesh).geometry as BufferGeometry | undefined
if (!geometry || geometry.groups.length === 0) return undefined
const triangleStart = faceIndex * 3
const group = geometry.groups.find(
(entry) => triangleStart >= entry.start && triangleStart < entry.start + entry.count,
)
return group?.materialIndex
}
export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) { export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], type: T) {
const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => { const emit = (suffix: EventSuffix, e: ThreeEvent<PointerEvent>) => {
const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}` const eventKey = `${type}:${suffix}` as `${T}:${EventSuffix}`
@@ -83,7 +65,7 @@ export function useNodeEvents<T extends NodeType>(node: NodeConfig[T]['node'], t
localPosition: [localPoint.x, localPoint.y, localPoint.z], localPosition: [localPoint.x, localPoint.y, localPoint.z],
normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined, normal: e.face ? [e.face.normal.x, e.face.normal.y, e.face.normal.z] : undefined,
faceIndex: e.faceIndex ?? undefined, faceIndex: e.faceIndex ?? undefined,
materialIndex: getIntersectionMaterialIndex(e.object, e.faceIndex ?? undefined), object: e.object,
stopPropagation: () => e.stopPropagation(), stopPropagation: () => e.stopPropagation(),
nativeEvent: e, nativeEvent: e,
} as NodeConfig[T]['event'] } as NodeConfig[T]['event']