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 { 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,
|
if (!nextMaterialTargetHandled && useEditor.getState().selectedMaterialTarget) {
|
||||||
})
|
useEditor.getState().setSelectedMaterialTarget(null)
|
||||||
} 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
|
// 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,16 +316,15 @@ 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}
|
||||||
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
||||||
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,16 +616,15 @@ 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}
|
||||||
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
onSelectMaterialPreset={handleTargetedMaterialPresetChange}
|
||||||
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,16 +243,15 @@ 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}
|
||||||
onSelectMaterialPreset={handleMaterialPresetChange}
|
onSelectMaterialPreset={handleMaterialPresetChange}
|
||||||
selectedMaterialPreset={materialPickerValue.materialPreset}
|
selectedMaterialPreset={materialPickerValue.materialPreset}
|
||||||
value={materialPickerValue.material}
|
value={materialPickerValue.material}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</PanelSection>
|
</PanelSection>
|
||||||
|
|
||||||
<PanelSection title="Actions">
|
<PanelSection title="Actions">
|
||||||
|
|||||||
@@ -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: {},
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
Reference in New Issue
Block a user