feat(paint-slots): migrate column onto the unified slot model
Column exposes shaft / base / capital / frame slots on node.slots (base when baseStyle!=none, capital when capitalStyle!=none, frame only for non-vertical support styles). The single material context is widened to a per-slot map plus a ColumnSlotContext; each part subtree is wrapped in <ColumnSlot> so every mesh primitive resolves its material AND stamps userData.slotId from the same context. Decorative shaft parts inherit shaft, base/capital carvings inherit their parent slot. Resolution: textures-off wall role -> node.slots ref -> legacy material/preset -> declared default (shaft/base/capital concrete-plaster, frame metal-steel). Monochrome unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a8f32dd47a
commit
0cdaf8b1b9
@@ -7,8 +7,10 @@ import {
|
||||
import { buildColumnFloorplan } from './floorplan'
|
||||
import { columnResizeAffordance, columnRotateAffordance } from './floorplan-affordances'
|
||||
import { columnFloorplanMoveTarget } from './floorplan-move'
|
||||
import { columnPaint } from './paint'
|
||||
import { columnParametrics } from './parametrics'
|
||||
import { ColumnNode } from './schema'
|
||||
import { columnSlots } from './slots'
|
||||
|
||||
// Limits + offsets shared with the in-world arrows. Mirrors the floors
|
||||
// the renderer clamps to (`Math.max(0.2, node.height)` etc.) so a drag
|
||||
@@ -325,6 +327,8 @@ export const columnDefinition: NodeDefinition<typeof ColumnNode> = {
|
||||
selectable: { hitVolume: 'bbox' },
|
||||
duplicable: true,
|
||||
deletable: true,
|
||||
slots: (node) => columnSlots(node as ColumnNodeType),
|
||||
paint: columnPaint,
|
||||
// Slab elevation lift via the generic `<FloorElevationSystem>`.
|
||||
floorPlaced: {
|
||||
footprint: (node) => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { AnyNode } from '@pascal-app/core'
|
||||
import { createSlotPaintCapability, previewSlotByUserData } from '../shared/slot-paint'
|
||||
import type { ColumnNode } from './schema'
|
||||
|
||||
export const columnPaint = createSlotPaintCapability({
|
||||
resolveRole: (args) => {
|
||||
const slotId = args.hitObject?.userData?.slotId
|
||||
return typeof slotId === 'string' ? slotId : null
|
||||
},
|
||||
applyPreview: previewSlotByUserData,
|
||||
legacyEffective: (node: AnyNode) => {
|
||||
const column = node as ColumnNode
|
||||
if (column.materialPreset || column.material) {
|
||||
return { material: column.material, materialPreset: column.materialPreset }
|
||||
}
|
||||
return null
|
||||
},
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
useLiveNodeOverrides,
|
||||
useLiveTransforms,
|
||||
useRegistry,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import {
|
||||
baseMaterial,
|
||||
@@ -17,21 +18,52 @@ import {
|
||||
createMaterialFromPresetRef,
|
||||
createSurfaceRoleMaterial,
|
||||
type RenderShading,
|
||||
resolveMaterialRef,
|
||||
resolveSlotDefaultMaterial,
|
||||
useNodeEvents,
|
||||
useViewer,
|
||||
} from '@pascal-app/viewer'
|
||||
import { createContext, useContext, useEffect, useMemo, useRef } from 'react'
|
||||
import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef } from 'react'
|
||||
import { BufferGeometry, Float32BufferAttribute, type Group, type Material } from 'three'
|
||||
import {
|
||||
COLUMN_BASE_DEFAULT,
|
||||
COLUMN_CAPITAL_DEFAULT,
|
||||
COLUMN_FRAME_DEFAULT,
|
||||
COLUMN_SHAFT_DEFAULT,
|
||||
type ColumnSlotId,
|
||||
} from './slots'
|
||||
|
||||
const ColumnMaterialContext = createContext<Material>(baseMaterial())
|
||||
type ColumnSlotMaterials = Record<ColumnSlotId, Material>
|
||||
type SceneMaterials = ReturnType<typeof useScene.getState>['materials']
|
||||
|
||||
const DEFAULT_COLUMN_MATERIAL = baseMaterial()
|
||||
const DEFAULT_COLUMN_SLOT_MATERIALS = createSingleColumnMaterialMap(DEFAULT_COLUMN_MATERIAL)
|
||||
|
||||
const ColumnMaterialContext = createContext<ColumnSlotMaterials>(DEFAULT_COLUMN_SLOT_MATERIALS)
|
||||
const ColumnSlotContext = createContext<ColumnSlotId>('shaft')
|
||||
const ColumnEdgeSoftnessContext = createContext(0.025)
|
||||
|
||||
function ColumnMaterial() {
|
||||
const material = useContext(ColumnMaterialContext)
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const materials = useContext(ColumnMaterialContext)
|
||||
const material = materials[slotId] ?? materials.shaft
|
||||
return <primitive attach="material" object={material} />
|
||||
}
|
||||
|
||||
function createColumnMaterial({
|
||||
function ColumnSlot({ children, slotId }: { children: ReactNode; slotId: ColumnSlotId }) {
|
||||
return <ColumnSlotContext.Provider value={slotId}>{children}</ColumnSlotContext.Provider>
|
||||
}
|
||||
|
||||
function createSingleColumnMaterialMap(material: Material): ColumnSlotMaterials {
|
||||
return {
|
||||
shaft: material,
|
||||
base: material,
|
||||
capital: material,
|
||||
frame: material,
|
||||
}
|
||||
}
|
||||
|
||||
function createLegacyColumnMaterial({
|
||||
material,
|
||||
materialPreset,
|
||||
shading,
|
||||
@@ -50,6 +82,99 @@ function createColumnMaterial({
|
||||
return baseMaterial(shading)
|
||||
}
|
||||
|
||||
function resolveColumnSlotMaterial({
|
||||
colorPreset,
|
||||
legacyMaterial,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
slotId,
|
||||
textures,
|
||||
}: {
|
||||
colorPreset: ColorPreset
|
||||
legacyMaterial: Material | null
|
||||
node: ColumnNode
|
||||
sceneMaterials: SceneMaterials
|
||||
shading: RenderShading
|
||||
slotId: ColumnSlotId
|
||||
textures: boolean
|
||||
}): Material {
|
||||
if (!textures) return createSurfaceRoleMaterial('wall', colorPreset)
|
||||
|
||||
const slotRef = node.slots?.[slotId]
|
||||
if (slotRef) {
|
||||
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
||||
if (resolved) return resolved
|
||||
}
|
||||
|
||||
if (legacyMaterial) return legacyMaterial
|
||||
|
||||
if (slotId === 'frame') return resolveSlotDefaultMaterial(COLUMN_FRAME_DEFAULT, shading)
|
||||
if (slotId === 'base') return resolveSlotDefaultMaterial(COLUMN_BASE_DEFAULT, shading)
|
||||
if (slotId === 'capital') return resolveSlotDefaultMaterial(COLUMN_CAPITAL_DEFAULT, shading)
|
||||
return resolveSlotDefaultMaterial(COLUMN_SHAFT_DEFAULT, shading)
|
||||
}
|
||||
|
||||
function createColumnSlotMaterials({
|
||||
colorPreset,
|
||||
material,
|
||||
materialPreset,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
textures,
|
||||
}: Pick<ColumnNode, 'material' | 'materialPreset'> & {
|
||||
colorPreset: ColorPreset
|
||||
node: ColumnNode
|
||||
sceneMaterials: SceneMaterials
|
||||
shading: RenderShading
|
||||
textures: boolean
|
||||
}): ColumnSlotMaterials {
|
||||
const legacyMaterial =
|
||||
materialPreset || material
|
||||
? createLegacyColumnMaterial({ colorPreset, material, materialPreset, shading, textures })
|
||||
: null
|
||||
|
||||
return {
|
||||
shaft: resolveColumnSlotMaterial({
|
||||
colorPreset,
|
||||
legacyMaterial,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
slotId: 'shaft',
|
||||
textures,
|
||||
}),
|
||||
base: resolveColumnSlotMaterial({
|
||||
colorPreset,
|
||||
legacyMaterial,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
slotId: 'base',
|
||||
textures,
|
||||
}),
|
||||
capital: resolveColumnSlotMaterial({
|
||||
colorPreset,
|
||||
legacyMaterial,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
slotId: 'capital',
|
||||
textures,
|
||||
}),
|
||||
frame: resolveColumnSlotMaterial({
|
||||
colorPreset,
|
||||
legacyMaterial,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
slotId: 'frame',
|
||||
textures,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function getSegments(node: ColumnNode) {
|
||||
if (node.crossSection === 'octagonal') return 8
|
||||
if (node.crossSection === 'sixteen-sided') return 16
|
||||
@@ -126,6 +251,7 @@ function MappedBox({
|
||||
width: number
|
||||
}) {
|
||||
const edgeSoftness = useContext(ColumnEdgeSoftnessContext)
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const minDimension = Math.max(0, Math.min(width, height, depth))
|
||||
const bevelRadius = softenEdges ? Math.min(Math.max(0, edgeSoftness), minDimension * 0.35) : 0
|
||||
const geometry = useMemo(() => {
|
||||
@@ -136,7 +262,14 @@ function MappedBox({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow rotation={rotation}>
|
||||
<mesh
|
||||
castShadow
|
||||
dispose={null}
|
||||
position={position}
|
||||
receiveShadow
|
||||
rotation={rotation}
|
||||
userData={{ slotId }}
|
||||
>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -154,6 +287,7 @@ function FlatEndedBeam({
|
||||
start: VectorTuple
|
||||
width: number
|
||||
}) {
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const dx = end[0] - start[0]
|
||||
const dy = end[1] - start[1]
|
||||
const dz = end[2] - start[2]
|
||||
@@ -244,7 +378,7 @@ function FlatEndedBeam({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} receiveShadow>
|
||||
<mesh castShadow dispose={null} receiveShadow userData={{ slotId }}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -763,6 +897,7 @@ function MappedCylinder({
|
||||
rotation?: VectorTuple
|
||||
segments?: number
|
||||
}) {
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const geometry = useMemo(() => {
|
||||
if (height <= 0 || radius <= 0 || radiusBottom < 0 || radiusTop < 0) return null
|
||||
return createColumnCylinderGeometry({
|
||||
@@ -778,7 +913,14 @@ function MappedCylinder({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow rotation={rotation}>
|
||||
<mesh
|
||||
castShadow
|
||||
dispose={null}
|
||||
position={position}
|
||||
receiveShadow
|
||||
rotation={rotation}
|
||||
userData={{ slotId }}
|
||||
>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -800,6 +942,7 @@ function MappedCone({
|
||||
rotation?: VectorTuple
|
||||
segments?: number
|
||||
}) {
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const geometry = useMemo(() => {
|
||||
if (height <= 0 || radiusX <= 0 || radiusZ <= 0) return null
|
||||
return createColumnCylinderGeometry({
|
||||
@@ -815,7 +958,14 @@ function MappedCone({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow rotation={rotation}>
|
||||
<mesh
|
||||
castShadow
|
||||
dispose={null}
|
||||
position={position}
|
||||
receiveShadow
|
||||
rotation={rotation}
|
||||
userData={{ slotId }}
|
||||
>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -833,6 +983,7 @@ function MappedSphere({
|
||||
segments?: number
|
||||
verticalSegments?: number
|
||||
}) {
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const geometry = useMemo(() => {
|
||||
if (radius <= 0) return null
|
||||
return createColumnSphereGeometry(radius, segments, verticalSegments)
|
||||
@@ -841,7 +992,7 @@ function MappedSphere({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow>
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow userData={{ slotId }}>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -867,6 +1018,7 @@ function MappedTorus({
|
||||
scaleZ?: number
|
||||
tubeRadius: number
|
||||
}) {
|
||||
const slotId = useContext(ColumnSlotContext)
|
||||
const geometry = useMemo(() => {
|
||||
if (ringRadius <= 0 || tubeRadius <= 0) return null
|
||||
return createColumnTorusGeometry({
|
||||
@@ -882,7 +1034,14 @@ function MappedTorus({
|
||||
if (!geometry) return null
|
||||
|
||||
return (
|
||||
<mesh castShadow dispose={null} position={position} receiveShadow rotation={rotation}>
|
||||
<mesh
|
||||
castShadow
|
||||
dispose={null}
|
||||
position={position}
|
||||
receiveShadow
|
||||
rotation={rotation}
|
||||
userData={{ slotId }}
|
||||
>
|
||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||
<ColumnMaterial />
|
||||
</mesh>
|
||||
@@ -2094,55 +2253,75 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
||||
return { baseHeight, capitalHeight, shaftY: baseHeight, shaftHeight }
|
||||
}, [node.baseHeight, node.baseStyle, node.capitalHeight, node.capitalStyle, node.height])
|
||||
|
||||
return node.supportStyle === 'a-frame' ? (
|
||||
<AFrameSupport node={node} />
|
||||
) : node.supportStyle === 'y-frame' ? (
|
||||
<YFrameSupport node={node} />
|
||||
) : node.supportStyle === 'v-frame' ? (
|
||||
<VFrameSupport node={node} />
|
||||
) : node.supportStyle === 'x-brace' ? (
|
||||
<XBraceSupport node={node} />
|
||||
) : node.supportStyle === 'k-brace' ? (
|
||||
<KBraceSupport node={node} />
|
||||
) : node.supportStyle === 'single-strut' ? (
|
||||
<SingleStrutSupport node={node} />
|
||||
) : node.supportStyle === 'tripod' ? (
|
||||
<TripodSupport node={node} />
|
||||
) : node.supportStyle === 'trestle' ? (
|
||||
<TrestleSupport node={node} />
|
||||
) : node.supportStyle === 'portal-frame' ? (
|
||||
<PortalFrameSupport node={node} />
|
||||
) : node.supportStyle === 'box-frame' ? (
|
||||
<BoxFrameSupport node={node} />
|
||||
) : (
|
||||
if (node.supportStyle !== 'vertical') {
|
||||
const support =
|
||||
node.supportStyle === 'a-frame' ? (
|
||||
<AFrameSupport node={node} />
|
||||
) : node.supportStyle === 'y-frame' ? (
|
||||
<YFrameSupport node={node} />
|
||||
) : node.supportStyle === 'v-frame' ? (
|
||||
<VFrameSupport node={node} />
|
||||
) : node.supportStyle === 'x-brace' ? (
|
||||
<XBraceSupport node={node} />
|
||||
) : node.supportStyle === 'k-brace' ? (
|
||||
<KBraceSupport node={node} />
|
||||
) : node.supportStyle === 'single-strut' ? (
|
||||
<SingleStrutSupport node={node} />
|
||||
) : node.supportStyle === 'tripod' ? (
|
||||
<TripodSupport node={node} />
|
||||
) : node.supportStyle === 'trestle' ? (
|
||||
<TrestleSupport node={node} />
|
||||
) : node.supportStyle === 'portal-frame' ? (
|
||||
<PortalFrameSupport node={node} />
|
||||
) : (
|
||||
<BoxFrameSupport node={node} />
|
||||
)
|
||||
return <ColumnSlot slotId="frame">{support}</ColumnSlot>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Base height={shaftLayout.baseHeight} node={node} />
|
||||
<BaseCarvings height={shaftLayout.baseHeight} node={node} />
|
||||
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
||||
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<LatheBands node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<LowerCarvedBand
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
<DravidianShaftPanels
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
<SpiralRibs node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<Capital
|
||||
height={shaftLayout.capitalHeight}
|
||||
node={node}
|
||||
y={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
||||
/>
|
||||
<CapitalCarvings
|
||||
capitalHeight={shaftLayout.capitalHeight}
|
||||
capitalY={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
||||
node={node}
|
||||
/>
|
||||
<ColumnSlot slotId="base">
|
||||
<Base height={shaftLayout.baseHeight} node={node} />
|
||||
<BaseCarvings height={shaftLayout.baseHeight} node={node} />
|
||||
</ColumnSlot>
|
||||
<ColumnSlot slotId="shaft">
|
||||
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
||||
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<LatheBands
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||
<LowerCarvedBand
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
<DravidianShaftPanels
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
<SpiralRibs
|
||||
node={node}
|
||||
shaftHeight={shaftLayout.shaftHeight}
|
||||
shaftY={shaftLayout.shaftY}
|
||||
/>
|
||||
</ColumnSlot>
|
||||
<ColumnSlot slotId="capital">
|
||||
<Capital
|
||||
height={shaftLayout.capitalHeight}
|
||||
node={node}
|
||||
y={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
||||
/>
|
||||
<CapitalCarvings
|
||||
capitalHeight={shaftLayout.capitalHeight}
|
||||
capitalY={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
||||
node={node}
|
||||
/>
|
||||
</ColumnSlot>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -2152,7 +2331,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
||||
* cursor preview, mirroring `ShelfPreview`. Builds the same geometry tree
|
||||
* as the real renderer via `<ColumnBody>` but:
|
||||
* - clones the material and makes it transparent (cloning is required:
|
||||
* `createColumnMaterial` can hand back a shared/cached instance, and
|
||||
* `createLegacyColumnMaterial` can hand back a shared/cached instance, and
|
||||
* mutating it would turn every committed column see-through);
|
||||
* - disables raycast on every mesh so the ghost doesn't intercept the
|
||||
* placement cursor ray (which would stall `grid:move`);
|
||||
@@ -2164,8 +2343,8 @@ export const ColumnPreview = ({ node }: { node: ColumnNode }) => {
|
||||
const colorPreset = useViewer((state) => state.colorPreset)
|
||||
const groupRef = useRef<Group>(null)
|
||||
|
||||
const material = useMemo(() => {
|
||||
const ghost = createColumnMaterial({
|
||||
const materials = useMemo(() => {
|
||||
const ghost = createLegacyColumnMaterial({
|
||||
material: node.material,
|
||||
materialPreset: node.materialPreset,
|
||||
shading,
|
||||
@@ -2175,10 +2354,15 @@ export const ColumnPreview = ({ node }: { node: ColumnNode }) => {
|
||||
ghost.transparent = true
|
||||
ghost.opacity = 0.5
|
||||
ghost.depthWrite = false
|
||||
return ghost
|
||||
return createSingleColumnMaterialMap(ghost)
|
||||
}, [shading, textures, colorPreset, node.material, node.materialPreset])
|
||||
|
||||
useEffect(() => () => material.dispose(), [material])
|
||||
useEffect(
|
||||
() => () => {
|
||||
for (const material of new Set(Object.values(materials))) material.dispose()
|
||||
},
|
||||
[materials],
|
||||
)
|
||||
|
||||
// Strip pointer events off the freshly-built meshes every render — the
|
||||
// geometry tree rebuilds when the ghost's dimensions change, so a one-shot
|
||||
@@ -2190,7 +2374,7 @@ export const ColumnPreview = ({ node }: { node: ColumnNode }) => {
|
||||
})
|
||||
|
||||
return (
|
||||
<ColumnMaterialContext.Provider value={material}>
|
||||
<ColumnMaterialContext.Provider value={materials}>
|
||||
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
||||
<group ref={groupRef}>
|
||||
<ColumnBody node={node} />
|
||||
@@ -2216,11 +2400,14 @@ export const ColumnRenderer = ({ node: rawNode }: { node: ColumnNode }) => {
|
||||
const shading = useViewer((state) => state.shading)
|
||||
const textures = useViewer((state) => state.textures)
|
||||
const colorPreset = useViewer((state) => state.colorPreset)
|
||||
const material = useMemo(
|
||||
const sceneMaterials = useScene((state) => state.materials)
|
||||
const materials = useMemo(
|
||||
() =>
|
||||
createColumnMaterial({
|
||||
createColumnSlotMaterials({
|
||||
material: node.material,
|
||||
materialPreset: node.materialPreset,
|
||||
node,
|
||||
sceneMaterials,
|
||||
shading,
|
||||
textures,
|
||||
colorPreset,
|
||||
@@ -2234,13 +2421,15 @@ export const ColumnRenderer = ({ node: rawNode }: { node: ColumnNode }) => {
|
||||
node.material?.properties,
|
||||
node.material?.texture,
|
||||
node.materialPreset,
|
||||
node.slots,
|
||||
sceneMaterials,
|
||||
],
|
||||
)
|
||||
|
||||
useRegistry(node.id, node.type, ref)
|
||||
|
||||
return (
|
||||
<ColumnMaterialContext.Provider value={material}>
|
||||
<ColumnMaterialContext.Provider value={materials}>
|
||||
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
||||
<group
|
||||
position={liveTransform?.position ?? node.position}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { SlotDeclaration } from '@pascal-app/core'
|
||||
import type { ColumnNode } from './schema'
|
||||
|
||||
export type ColumnSlotId = 'shaft' | 'base' | 'capital' | 'frame'
|
||||
|
||||
export const COLUMN_SHAFT_DEFAULT = 'library:concrete-plaster'
|
||||
export const COLUMN_BASE_DEFAULT = 'library:concrete-plaster'
|
||||
export const COLUMN_CAPITAL_DEFAULT = 'library:concrete-plaster'
|
||||
export const COLUMN_FRAME_DEFAULT = 'library:metal-steel'
|
||||
|
||||
export function columnSlots(node: ColumnNode): SlotDeclaration[] {
|
||||
const slots: SlotDeclaration[] = [
|
||||
{ slotId: 'shaft', label: 'Shaft', default: COLUMN_SHAFT_DEFAULT },
|
||||
]
|
||||
|
||||
if (node.baseStyle !== 'none') {
|
||||
slots.push({ slotId: 'base', label: 'Base', default: COLUMN_BASE_DEFAULT })
|
||||
}
|
||||
|
||||
if (node.capitalStyle !== 'none') {
|
||||
slots.push({ slotId: 'capital', label: 'Capital', default: COLUMN_CAPITAL_DEFAULT })
|
||||
}
|
||||
|
||||
if (node.supportStyle !== 'vertical') {
|
||||
slots.push({ slotId: 'frame', label: 'Frame', default: COLUMN_FRAME_DEFAULT })
|
||||
}
|
||||
|
||||
return slots
|
||||
}
|
||||
Reference in New Issue
Block a user