diff --git a/packages/nodes/src/elevator/definition.ts b/packages/nodes/src/elevator/definition.ts index f72bd8a3..e65a9a3e 100644 --- a/packages/nodes/src/elevator/definition.ts +++ b/packages/nodes/src/elevator/definition.ts @@ -13,7 +13,9 @@ import { import { buildElevatorFloorplan } from './floorplan' import { elevatorResizeAffordance, elevatorRotateAffordance } from './floorplan-affordances' import { elevatorParametrics } from './parametrics' +import { elevatorPaint } from './paint' import { ElevatorNode } from './schema' +import { elevatorSlots } from './slots' const SIDE_HANDLE_OFFSET = 0.22 const HEIGHT_HANDLE_OFFSET = 0.3 @@ -223,6 +225,8 @@ export const elevatorDefinition: NodeDefinition = { }, duplicable: true, deletable: true, + slots: (node) => elevatorSlots(node as ElevatorNodeType), + paint: elevatorPaint, }, parametrics: elevatorParametrics, diff --git a/packages/nodes/src/elevator/paint.ts b/packages/nodes/src/elevator/paint.ts new file mode 100644 index 00000000..ac38e484 --- /dev/null +++ b/packages/nodes/src/elevator/paint.ts @@ -0,0 +1,7 @@ +import { createSlotPaintCapability, previewSlotByUserData } from '../shared/slot-paint' + +export const elevatorPaint = createSlotPaintCapability({ + resolveRole: (args) => (args.hitObject?.userData?.slotId as string) ?? null, + applyPreview: previewSlotByUserData, + legacyEffective: () => null, +}) diff --git a/packages/nodes/src/elevator/renderer.tsx b/packages/nodes/src/elevator/renderer.tsx index 31410b4e..47669bed 100644 --- a/packages/nodes/src/elevator/renderer.tsx +++ b/packages/nodes/src/elevator/renderer.tsx @@ -28,11 +28,13 @@ import { createDefaultMaterial, createSurfaceRoleMaterial, type RenderShading, + resolveMaterialRef, + resolveSlotDefaultMaterial, useNodeEvents, useViewer, } from '@pascal-app/viewer' import { useFrame } from '@react-three/fiber' -import { useCallback, useLayoutEffect, useMemo, useRef } from 'react' +import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react' import { BoxGeometry, CylinderGeometry, @@ -43,6 +45,13 @@ import { TorusGeometry, } from 'three' import { useShallow } from 'zustand/react/shallow' +import { + ELEVATOR_CAB_SLOT_DEFAULT, + ELEVATOR_DOORS_SLOT_DEFAULT, + ELEVATOR_GLASS_SLOT_DEFAULT, + ELEVATOR_SHAFT_SLOT_DEFAULT, + type ElevatorSlotId, +} from './slots' const DEFAULT_STRUCTURE_WHITE = '#f2f0ed' const SHAFT_WALL_COLOR = DEFAULT_STRUCTURE_WHITE @@ -360,48 +369,102 @@ function getElevatorMaterials( return materials } -let { - SHAFT_WALL_MATERIAL, - SHAFT_SIDE_MATERIAL, - SHAFT_TRIM_MATERIAL, - CAB_MATERIAL, - DOOR_MATERIAL, - DOOR_GROOVE_MATERIAL, - GLASS_MATERIAL, - PANEL_MATERIAL, - LANDING_PANEL_MATERIAL, - INDICATOR_SCREEN_MATERIALS, - INDICATOR_GLYPH_MATERIALS, - BUTTON_FACE_MATERIALS, - BUTTON_RING_MATERIALS, - BUTTON_GLOW_MATERIALS, - BUTTON_LABEL_MATERIALS, - QUEUE_STRIP_MATERIALS, -} = getElevatorMaterials('rendered') +type ElevatorSceneMaterials = ReturnType['materials'] -function setElevatorMaterials( +function resolveElevatorFinishMaterial( + node: ElevatorNode, + slotId: ElevatorSlotId, + slotDefault: string, + sceneMaterials: ElevatorSceneMaterials, shading: RenderShading, - textures = true, - colorPreset: ColorPreset = 'clay', -) { - ;({ - SHAFT_WALL_MATERIAL, - SHAFT_SIDE_MATERIAL, - SHAFT_TRIM_MATERIAL, - CAB_MATERIAL, - DOOR_MATERIAL, - DOOR_GROOVE_MATERIAL, - GLASS_MATERIAL, - PANEL_MATERIAL, - LANDING_PANEL_MATERIAL, - INDICATOR_SCREEN_MATERIALS, - INDICATOR_GLYPH_MATERIALS, - BUTTON_FACE_MATERIALS, - BUTTON_RING_MATERIALS, - BUTTON_GLOW_MATERIALS, - BUTTON_LABEL_MATERIALS, - QUEUE_STRIP_MATERIALS, - } = getElevatorMaterials(shading, textures, colorPreset)) + roughness: number, +): Material { + const ref = node.slots?.[slotId] + if (ref) { + const resolved = resolveMaterialRef(ref, sceneMaterials, shading) + if (resolved) return resolved + } + return resolveSlotDefaultMaterial(slotDefault, shading, roughness) +} + +function withElevatorGlassTransparency(material: Material): Material { + const glass = material.clone() + glass.depthWrite = false + glass.opacity = 0.2 + glass.transparent = true + glass.needsUpdate = true + return glass +} + +function getResolvedElevatorMaterials( + node: ElevatorNode, + shading: RenderShading, + textures: boolean, + colorPreset: ColorPreset, + sceneMaterials: ElevatorSceneMaterials, +): ElevatorMaterialSet { + const materials = getElevatorMaterials(shading, textures, colorPreset) + if (!textures) return materials + + const cab = resolveElevatorFinishMaterial( + node, + 'cab', + ELEVATOR_CAB_SLOT_DEFAULT, + sceneMaterials, + shading, + 0.48, + ) + const doors = resolveElevatorFinishMaterial( + node, + 'doors', + ELEVATOR_DOORS_SLOT_DEFAULT, + sceneMaterials, + shading, + 0.34, + ) + const shaft = resolveElevatorFinishMaterial( + node, + 'shaft', + ELEVATOR_SHAFT_SLOT_DEFAULT, + sceneMaterials, + shading, + 0.56, + ) + const glass = withElevatorGlassTransparency( + resolveElevatorFinishMaterial( + node, + 'glass', + ELEVATOR_GLASS_SLOT_DEFAULT, + sceneMaterials, + shading, + 0.08, + ), + ) + + return { + ...materials, + SHAFT_WALL_MATERIAL: shaft, + SHAFT_SIDE_MATERIAL: shaft, + SHAFT_TRIM_MATERIAL: shaft, + CAB_MATERIAL: cab, + DOOR_MATERIAL: doors, + DOOR_GROOVE_MATERIAL: doors, + GLASS_MATERIAL: glass, + } +} + +const DEFAULT_ELEVATOR_MATERIALS = getElevatorMaterials('rendered') +const ElevatorMaterialsContext = createContext(DEFAULT_ELEVATOR_MATERIALS) + +function useElevatorMaterialSet(): ElevatorMaterialSet { + return useContext(ElevatorMaterialsContext) +} + +const ELEVATOR_SLOT_USER_DATA: Record = { + cab: { slotId: 'cab' }, + doors: { slotId: 'doors' }, + shaft: { slotId: 'shaft' }, + glass: { slotId: 'glass' }, } type ElevatorButtonAction = 'open-door' | 'request-level' @@ -449,6 +512,7 @@ function BoxPrimitive({ receiveShadow = false, rotation, scale, + slotId, }: { castShadow?: boolean material: Material @@ -456,6 +520,7 @@ function BoxPrimitive({ receiveShadow?: boolean rotation?: Vector3Tuple scale: Vector3Tuple + slotId?: ElevatorSlotId }) { return ( ) } @@ -597,6 +663,8 @@ function ElevatorFloorIndicator({ scale?: number showReadout?: boolean }) { + const { INDICATOR_GLYPH_MATERIALS, INDICATOR_SCREEN_MATERIALS, PANEL_MATERIAL } = + useElevatorMaterialSet() const glyphMaterial = active ? INDICATOR_GLYPH_MATERIALS.active : INDICATOR_GLYPH_MATERIALS.idle const screenMaterial = active ? INDICATOR_SCREEN_MATERIALS.active @@ -721,6 +789,12 @@ function ElevatorMeshButton({ queued: boolean radius?: number }) { + const { + BUTTON_FACE_MATERIALS, + BUTTON_GLOW_MATERIALS, + BUTTON_LABEL_MATERIALS, + BUTTON_RING_MATERIALS, + } = useElevatorMaterialSet() const state = disabled ? 'disabled' : active ? 'active' : queued ? 'queued' : 'idle' const depth = active ? 0.028 : 0.04 const faceZ = faceSign * (depth / 2 + 0.004) @@ -845,6 +919,7 @@ function DoorLeaf({ y: number z: number }) { + const { DOOR_GROOVE_MATERIAL, DOOR_MATERIAL, GLASS_MATERIAL } = useElevatorMaterialSet() const ref = useRef(null) const getLeafX = (openAmount: number) => getElevatorDoorLeafX(side, width, openAmount, doorStyle) const leafWidth = getElevatorDoorLeafWidth(width, doorStyle) @@ -880,6 +955,7 @@ function DoorLeaf({ position={[0, height / 2 - railHeight / 2, 0]} receiveShadow scale={[leafWidth, railHeight, 0.05]} + slotId="doors" /> ) : ( @@ -916,11 +996,13 @@ function DoorLeaf({ position={[0, 0, 0]} receiveShadow scale={[leafWidth, height, 0.05]} + slotId="doors" /> {resolvedPanelStyle === 'segmented-panel' ? Array.from({ length: segmentCount - 1 }).map((_, index) => ( @@ -929,6 +1011,7 @@ function DoorLeaf({ material={DOOR_GROOVE_MATERIAL} position={[0, -panelInsetHeight / 2 + segmentSpacing * (index + 1), -0.03]} scale={[panelInsetWidth, 0.018, 0.012]} + slotId="doors" /> )) : null} @@ -936,11 +1019,13 @@ function DoorLeaf({ material={DOOR_GROOVE_MATERIAL} position={[0, panelInsetHeight / 2, -0.029]} scale={[panelInsetWidth, 0.012, 0.01]} + slotId="doors" /> )} @@ -1011,6 +1096,7 @@ function LandingDoorFrame({ shaftWidth: number z: number }) { + const { SHAFT_TRIM_MATERIAL, SHAFT_WALL_MATERIAL } = useElevatorMaterialSet() const wallDepth = 0.09 const levelHeight = Math.max(levelTopY - levelY, 0.01) const jambWidth = Math.max((shaftWidth - doorWidth) / 2, 0.08) @@ -1026,6 +1112,7 @@ function LandingDoorFrame({ position={[-jambCenterOffset, levelY + levelHeight / 2, z]} receiveShadow scale={[jambWidth, levelHeight, wallDepth]} + slotId="shaft" /> {headerHeight > 0.01 && ( )} ) @@ -1119,6 +1212,7 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => { const shading = useViewer((state) => state.shading) const textures = useViewer((state) => state.textures) const colorPreset = useViewer((state) => state.colorPreset) + const sceneMaterials = useScene((state) => state.materials) const liveOverrides = useLiveNodeOverrides((state) => state.get(node.id)) const liveTransform = useLiveTransforms((state) => state.get(node.id)) const renderNode = useMemo( @@ -1128,8 +1222,19 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => { const levelContextNodes = useScene( useShallow((state) => getElevatorLevelContextNodes(renderNode, state.nodes)), ) - - setElevatorMaterials(shading, textures, colorPreset) + const materials = useMemo( + () => getResolvedElevatorMaterials(renderNode, shading, textures, colorPreset, sceneMaterials), + [colorPreset, renderNode, sceneMaterials, shading, textures], + ) + const { + CAB_MATERIAL, + GLASS_MATERIAL, + LANDING_PANEL_MATERIAL, + PANEL_MATERIAL, + QUEUE_STRIP_MATERIALS, + SHAFT_SIDE_MATERIAL, + SHAFT_TRIM_MATERIAL, + } = materials useRegistry(node.id, 'elevator', ref) @@ -1174,6 +1279,7 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => { const doorStyle = getResolvedDoorStyle(renderNode.doorStyle) const shaftStyle = getResolvedShaftStyle(renderNode.shaftStyle) const shaftShellMaterial = shaftStyle === 'glass' ? GLASS_MATERIAL : SHAFT_SIDE_MATERIAL + const shaftShellSlotId: ElevatorSlotId = shaftStyle === 'glass' ? 'glass' : 'shaft' const shaftTopMaterial = shaftStyle === 'glass' ? SHAFT_TRIM_MATERIAL : SHAFT_SIDE_MATERIAL const shaftHeight = Math.max(totalHeight, cabHeight + 0.3) const shaftBodyHeight = Math.max(shaftHeight - shaftWallThickness, 0.01) @@ -1269,19 +1375,21 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => { ) return ( - + + { position={[-shaftWidth / 2 - shaftWallThickness / 2, shaftBodyCenterY, 0]} receiveShadow scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]} + slotId={shaftShellSlotId} /> { position={[shaftWidth / 2 + shaftWallThickness / 2, shaftBodyCenterY, 0]} receiveShadow scale={[shaftWallThickness, shaftBodyHeight, shaftDepth + shaftWallThickness * 2]} + slotId={shaftShellSlotId} /> { shaftWallThickness, shaftDepth + shaftWallThickness * 2, ]} + slotId="shaft" /> @@ -1316,6 +1427,7 @@ export const ElevatorRenderer = ({ node }: { node: ElevatorNode }) => { position={[0, 0.04, cabCenterZ]} receiveShadow scale={[cabWidth, 0.08, cabDepth]} + slotId="cab" /> { position={[0, cabHeight - 0.04, cabCenterZ]} receiveShadow scale={[cabWidth, 0.08, cabDepth]} + slotId="cab" /> { position={[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - 0.04]} receiveShadow scale={[cabWidth, cabHeight, 0.08]} + slotId="cab" /> { position={[-cabWidth / 2 + 0.04, cabHeight / 2, cabCenterZ]} receiveShadow scale={[0.08, cabHeight, cabDepth]} + slotId="cab" /> { position={[cabWidth / 2 - 0.04, cabHeight / 2, cabCenterZ]} receiveShadow scale={[0.08, cabHeight, cabDepth]} + slotId="cab" /> { ) })} - + + ) } diff --git a/packages/nodes/src/elevator/slots.ts b/packages/nodes/src/elevator/slots.ts new file mode 100644 index 00000000..0bc88ce3 --- /dev/null +++ b/packages/nodes/src/elevator/slots.ts @@ -0,0 +1,29 @@ +import { + type ElevatorNode, + getResolvedElevatorDoorPanelStyle, + getResolvedElevatorShaftStyle, + type SlotDeclaration, +} from '@pascal-app/core' + +export type ElevatorSlotId = 'cab' | 'doors' | 'shaft' | 'glass' + +export const ELEVATOR_CAB_SLOT_DEFAULT = 'library:preset-softwhite' +export const ELEVATOR_DOORS_SLOT_DEFAULT = 'library:metal-steel' +export const ELEVATOR_SHAFT_SLOT_DEFAULT = 'library:preset-lightgrey' +export const ELEVATOR_GLASS_SLOT_DEFAULT = 'library:preset-glass' + +export function elevatorSlots(node: ElevatorNode): SlotDeclaration[] { + const slots: SlotDeclaration[] = [ + { slotId: 'cab', label: 'Cab', default: ELEVATOR_CAB_SLOT_DEFAULT }, + { slotId: 'doors', label: 'Doors', default: ELEVATOR_DOORS_SLOT_DEFAULT }, + { slotId: 'shaft', label: 'Shaft', default: ELEVATOR_SHAFT_SLOT_DEFAULT }, + ] + + const hasGlass = + getResolvedElevatorShaftStyle(node.shaftStyle) === 'glass' || + getResolvedElevatorDoorPanelStyle(node.doorPanelStyle) === 'glass-frame' + + if (hasGlass) slots.push({ slotId: 'glass', label: 'Glass', default: ELEVATOR_GLASS_SLOT_DEFAULT }) + + return slots +}