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 { buildColumnFloorplan } from './floorplan'
|
||||||
import { columnResizeAffordance, columnRotateAffordance } from './floorplan-affordances'
|
import { columnResizeAffordance, columnRotateAffordance } from './floorplan-affordances'
|
||||||
import { columnFloorplanMoveTarget } from './floorplan-move'
|
import { columnFloorplanMoveTarget } from './floorplan-move'
|
||||||
|
import { columnPaint } from './paint'
|
||||||
import { columnParametrics } from './parametrics'
|
import { columnParametrics } from './parametrics'
|
||||||
import { ColumnNode } from './schema'
|
import { ColumnNode } from './schema'
|
||||||
|
import { columnSlots } from './slots'
|
||||||
|
|
||||||
// Limits + offsets shared with the in-world arrows. Mirrors the floors
|
// 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
|
// 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' },
|
selectable: { hitVolume: 'bbox' },
|
||||||
duplicable: true,
|
duplicable: true,
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
slots: (node) => columnSlots(node as ColumnNodeType),
|
||||||
|
paint: columnPaint,
|
||||||
// Slab elevation lift via the generic `<FloorElevationSystem>`.
|
// Slab elevation lift via the generic `<FloorElevationSystem>`.
|
||||||
floorPlaced: {
|
floorPlaced: {
|
||||||
footprint: (node) => {
|
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,
|
useLiveNodeOverrides,
|
||||||
useLiveTransforms,
|
useLiveTransforms,
|
||||||
useRegistry,
|
useRegistry,
|
||||||
|
useScene,
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import {
|
import {
|
||||||
baseMaterial,
|
baseMaterial,
|
||||||
@@ -17,21 +18,52 @@ import {
|
|||||||
createMaterialFromPresetRef,
|
createMaterialFromPresetRef,
|
||||||
createSurfaceRoleMaterial,
|
createSurfaceRoleMaterial,
|
||||||
type RenderShading,
|
type RenderShading,
|
||||||
|
resolveMaterialRef,
|
||||||
|
resolveSlotDefaultMaterial,
|
||||||
useNodeEvents,
|
useNodeEvents,
|
||||||
useViewer,
|
useViewer,
|
||||||
} from '@pascal-app/viewer'
|
} 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 { 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)
|
const ColumnEdgeSoftnessContext = createContext(0.025)
|
||||||
|
|
||||||
function ColumnMaterial() {
|
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} />
|
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,
|
material,
|
||||||
materialPreset,
|
materialPreset,
|
||||||
shading,
|
shading,
|
||||||
@@ -50,6 +82,99 @@ function createColumnMaterial({
|
|||||||
return baseMaterial(shading)
|
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) {
|
function getSegments(node: ColumnNode) {
|
||||||
if (node.crossSection === 'octagonal') return 8
|
if (node.crossSection === 'octagonal') return 8
|
||||||
if (node.crossSection === 'sixteen-sided') return 16
|
if (node.crossSection === 'sixteen-sided') return 16
|
||||||
@@ -126,6 +251,7 @@ function MappedBox({
|
|||||||
width: number
|
width: number
|
||||||
}) {
|
}) {
|
||||||
const edgeSoftness = useContext(ColumnEdgeSoftnessContext)
|
const edgeSoftness = useContext(ColumnEdgeSoftnessContext)
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const minDimension = Math.max(0, Math.min(width, height, depth))
|
const minDimension = Math.max(0, Math.min(width, height, depth))
|
||||||
const bevelRadius = softenEdges ? Math.min(Math.max(0, edgeSoftness), minDimension * 0.35) : 0
|
const bevelRadius = softenEdges ? Math.min(Math.max(0, edgeSoftness), minDimension * 0.35) : 0
|
||||||
const geometry = useMemo(() => {
|
const geometry = useMemo(() => {
|
||||||
@@ -136,7 +262,14 @@ function MappedBox({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
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} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -154,6 +287,7 @@ function FlatEndedBeam({
|
|||||||
start: VectorTuple
|
start: VectorTuple
|
||||||
width: number
|
width: number
|
||||||
}) {
|
}) {
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const dx = end[0] - start[0]
|
const dx = end[0] - start[0]
|
||||||
const dy = end[1] - start[1]
|
const dy = end[1] - start[1]
|
||||||
const dz = end[2] - start[2]
|
const dz = end[2] - start[2]
|
||||||
@@ -244,7 +378,7 @@ function FlatEndedBeam({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow dispose={null} receiveShadow>
|
<mesh castShadow dispose={null} receiveShadow userData={{ slotId }}>
|
||||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -763,6 +897,7 @@ function MappedCylinder({
|
|||||||
rotation?: VectorTuple
|
rotation?: VectorTuple
|
||||||
segments?: number
|
segments?: number
|
||||||
}) {
|
}) {
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const geometry = useMemo(() => {
|
const geometry = useMemo(() => {
|
||||||
if (height <= 0 || radius <= 0 || radiusBottom < 0 || radiusTop < 0) return null
|
if (height <= 0 || radius <= 0 || radiusBottom < 0 || radiusTop < 0) return null
|
||||||
return createColumnCylinderGeometry({
|
return createColumnCylinderGeometry({
|
||||||
@@ -778,7 +913,14 @@ function MappedCylinder({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
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} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -800,6 +942,7 @@ function MappedCone({
|
|||||||
rotation?: VectorTuple
|
rotation?: VectorTuple
|
||||||
segments?: number
|
segments?: number
|
||||||
}) {
|
}) {
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const geometry = useMemo(() => {
|
const geometry = useMemo(() => {
|
||||||
if (height <= 0 || radiusX <= 0 || radiusZ <= 0) return null
|
if (height <= 0 || radiusX <= 0 || radiusZ <= 0) return null
|
||||||
return createColumnCylinderGeometry({
|
return createColumnCylinderGeometry({
|
||||||
@@ -815,7 +958,14 @@ function MappedCone({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
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} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -833,6 +983,7 @@ function MappedSphere({
|
|||||||
segments?: number
|
segments?: number
|
||||||
verticalSegments?: number
|
verticalSegments?: number
|
||||||
}) {
|
}) {
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const geometry = useMemo(() => {
|
const geometry = useMemo(() => {
|
||||||
if (radius <= 0) return null
|
if (radius <= 0) return null
|
||||||
return createColumnSphereGeometry(radius, segments, verticalSegments)
|
return createColumnSphereGeometry(radius, segments, verticalSegments)
|
||||||
@@ -841,7 +992,7 @@ function MappedSphere({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh castShadow dispose={null} position={position} receiveShadow>
|
<mesh castShadow dispose={null} position={position} receiveShadow userData={{ slotId }}>
|
||||||
<primitive attach="geometry" dispose={null} object={geometry} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -867,6 +1018,7 @@ function MappedTorus({
|
|||||||
scaleZ?: number
|
scaleZ?: number
|
||||||
tubeRadius: number
|
tubeRadius: number
|
||||||
}) {
|
}) {
|
||||||
|
const slotId = useContext(ColumnSlotContext)
|
||||||
const geometry = useMemo(() => {
|
const geometry = useMemo(() => {
|
||||||
if (ringRadius <= 0 || tubeRadius <= 0) return null
|
if (ringRadius <= 0 || tubeRadius <= 0) return null
|
||||||
return createColumnTorusGeometry({
|
return createColumnTorusGeometry({
|
||||||
@@ -882,7 +1034,14 @@ function MappedTorus({
|
|||||||
if (!geometry) return null
|
if (!geometry) return null
|
||||||
|
|
||||||
return (
|
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} />
|
<primitive attach="geometry" dispose={null} object={geometry} />
|
||||||
<ColumnMaterial />
|
<ColumnMaterial />
|
||||||
</mesh>
|
</mesh>
|
||||||
@@ -2094,7 +2253,9 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
return { baseHeight, capitalHeight, shaftY: baseHeight, shaftHeight }
|
return { baseHeight, capitalHeight, shaftY: baseHeight, shaftHeight }
|
||||||
}, [node.baseHeight, node.baseStyle, node.capitalHeight, node.capitalStyle, node.height])
|
}, [node.baseHeight, node.baseStyle, node.capitalHeight, node.capitalStyle, node.height])
|
||||||
|
|
||||||
return node.supportStyle === 'a-frame' ? (
|
if (node.supportStyle !== 'vertical') {
|
||||||
|
const support =
|
||||||
|
node.supportStyle === 'a-frame' ? (
|
||||||
<AFrameSupport node={node} />
|
<AFrameSupport node={node} />
|
||||||
) : node.supportStyle === 'y-frame' ? (
|
) : node.supportStyle === 'y-frame' ? (
|
||||||
<YFrameSupport node={node} />
|
<YFrameSupport node={node} />
|
||||||
@@ -2112,15 +2273,26 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
<TrestleSupport node={node} />
|
<TrestleSupport node={node} />
|
||||||
) : node.supportStyle === 'portal-frame' ? (
|
) : node.supportStyle === 'portal-frame' ? (
|
||||||
<PortalFrameSupport node={node} />
|
<PortalFrameSupport node={node} />
|
||||||
) : node.supportStyle === 'box-frame' ? (
|
|
||||||
<BoxFrameSupport node={node} />
|
|
||||||
) : (
|
) : (
|
||||||
|
<BoxFrameSupport node={node} />
|
||||||
|
)
|
||||||
|
return <ColumnSlot slotId="frame">{support}</ColumnSlot>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ColumnSlot slotId="base">
|
||||||
<Base height={shaftLayout.baseHeight} node={node} />
|
<Base height={shaftLayout.baseHeight} node={node} />
|
||||||
<BaseCarvings height={shaftLayout.baseHeight} node={node} />
|
<BaseCarvings height={shaftLayout.baseHeight} node={node} />
|
||||||
|
</ColumnSlot>
|
||||||
|
<ColumnSlot slotId="shaft">
|
||||||
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
<Shaft height={shaftLayout.shaftHeight} node={node} y={shaftLayout.shaftY} />
|
||||||
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
<Rings node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
<LatheBands 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} />
|
<Flutes node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
||||||
<LowerCarvedBand
|
<LowerCarvedBand
|
||||||
node={node}
|
node={node}
|
||||||
@@ -2132,7 +2304,13 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
shaftHeight={shaftLayout.shaftHeight}
|
shaftHeight={shaftLayout.shaftHeight}
|
||||||
shaftY={shaftLayout.shaftY}
|
shaftY={shaftLayout.shaftY}
|
||||||
/>
|
/>
|
||||||
<SpiralRibs node={node} shaftHeight={shaftLayout.shaftHeight} shaftY={shaftLayout.shaftY} />
|
<SpiralRibs
|
||||||
|
node={node}
|
||||||
|
shaftHeight={shaftLayout.shaftHeight}
|
||||||
|
shaftY={shaftLayout.shaftY}
|
||||||
|
/>
|
||||||
|
</ColumnSlot>
|
||||||
|
<ColumnSlot slotId="capital">
|
||||||
<Capital
|
<Capital
|
||||||
height={shaftLayout.capitalHeight}
|
height={shaftLayout.capitalHeight}
|
||||||
node={node}
|
node={node}
|
||||||
@@ -2143,6 +2321,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
capitalY={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
capitalY={shaftLayout.baseHeight + shaftLayout.shaftHeight}
|
||||||
node={node}
|
node={node}
|
||||||
/>
|
/>
|
||||||
|
</ColumnSlot>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2152,7 +2331,7 @@ function ColumnBody({ node }: { node: ColumnNode }) {
|
|||||||
* cursor preview, mirroring `ShelfPreview`. Builds the same geometry tree
|
* cursor preview, mirroring `ShelfPreview`. Builds the same geometry tree
|
||||||
* as the real renderer via `<ColumnBody>` but:
|
* as the real renderer via `<ColumnBody>` but:
|
||||||
* - clones the material and makes it transparent (cloning is required:
|
* - 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);
|
* mutating it would turn every committed column see-through);
|
||||||
* - disables raycast on every mesh so the ghost doesn't intercept the
|
* - disables raycast on every mesh so the ghost doesn't intercept the
|
||||||
* placement cursor ray (which would stall `grid:move`);
|
* 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 colorPreset = useViewer((state) => state.colorPreset)
|
||||||
const groupRef = useRef<Group>(null)
|
const groupRef = useRef<Group>(null)
|
||||||
|
|
||||||
const material = useMemo(() => {
|
const materials = useMemo(() => {
|
||||||
const ghost = createColumnMaterial({
|
const ghost = createLegacyColumnMaterial({
|
||||||
material: node.material,
|
material: node.material,
|
||||||
materialPreset: node.materialPreset,
|
materialPreset: node.materialPreset,
|
||||||
shading,
|
shading,
|
||||||
@@ -2175,10 +2354,15 @@ export const ColumnPreview = ({ node }: { node: ColumnNode }) => {
|
|||||||
ghost.transparent = true
|
ghost.transparent = true
|
||||||
ghost.opacity = 0.5
|
ghost.opacity = 0.5
|
||||||
ghost.depthWrite = false
|
ghost.depthWrite = false
|
||||||
return ghost
|
return createSingleColumnMaterialMap(ghost)
|
||||||
}, [shading, textures, colorPreset, node.material, node.materialPreset])
|
}, [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
|
// Strip pointer events off the freshly-built meshes every render — the
|
||||||
// geometry tree rebuilds when the ghost's dimensions change, so a one-shot
|
// geometry tree rebuilds when the ghost's dimensions change, so a one-shot
|
||||||
@@ -2190,7 +2374,7 @@ export const ColumnPreview = ({ node }: { node: ColumnNode }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnMaterialContext.Provider value={material}>
|
<ColumnMaterialContext.Provider value={materials}>
|
||||||
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
||||||
<group ref={groupRef}>
|
<group ref={groupRef}>
|
||||||
<ColumnBody node={node} />
|
<ColumnBody node={node} />
|
||||||
@@ -2216,11 +2400,14 @@ export const ColumnRenderer = ({ node: rawNode }: { node: ColumnNode }) => {
|
|||||||
const shading = useViewer((state) => state.shading)
|
const shading = useViewer((state) => state.shading)
|
||||||
const textures = useViewer((state) => state.textures)
|
const textures = useViewer((state) => state.textures)
|
||||||
const colorPreset = useViewer((state) => state.colorPreset)
|
const colorPreset = useViewer((state) => state.colorPreset)
|
||||||
const material = useMemo(
|
const sceneMaterials = useScene((state) => state.materials)
|
||||||
|
const materials = useMemo(
|
||||||
() =>
|
() =>
|
||||||
createColumnMaterial({
|
createColumnSlotMaterials({
|
||||||
material: node.material,
|
material: node.material,
|
||||||
materialPreset: node.materialPreset,
|
materialPreset: node.materialPreset,
|
||||||
|
node,
|
||||||
|
sceneMaterials,
|
||||||
shading,
|
shading,
|
||||||
textures,
|
textures,
|
||||||
colorPreset,
|
colorPreset,
|
||||||
@@ -2234,13 +2421,15 @@ export const ColumnRenderer = ({ node: rawNode }: { node: ColumnNode }) => {
|
|||||||
node.material?.properties,
|
node.material?.properties,
|
||||||
node.material?.texture,
|
node.material?.texture,
|
||||||
node.materialPreset,
|
node.materialPreset,
|
||||||
|
node.slots,
|
||||||
|
sceneMaterials,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
useRegistry(node.id, node.type, ref)
|
useRegistry(node.id, node.type, ref)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnMaterialContext.Provider value={material}>
|
<ColumnMaterialContext.Provider value={materials}>
|
||||||
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
<ColumnEdgeSoftnessContext.Provider value={node.edgeSoftness ?? 0.025}>
|
||||||
<group
|
<group
|
||||||
position={liveTransform?.position ?? node.position}
|
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