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