Merge branch 'feat/paint-slots-fence' into feat/paint-slots
This commit is contained in:
@@ -3,8 +3,10 @@ import { buildFenceFloorplan } from './floorplan'
|
|||||||
import { fenceCurveAffordance, fenceMoveEndpointAffordance } from './floorplan-affordances'
|
import { fenceCurveAffordance, fenceMoveEndpointAffordance } from './floorplan-affordances'
|
||||||
import { fenceFloorplanMoveTarget } from './floorplan-move'
|
import { fenceFloorplanMoveTarget } from './floorplan-move'
|
||||||
import { buildFenceGeometry } from './geometry'
|
import { buildFenceGeometry } from './geometry'
|
||||||
|
import { fencePaint } from './paint'
|
||||||
import { fenceParametrics } from './parametrics'
|
import { fenceParametrics } from './parametrics'
|
||||||
import { FenceNode } from './schema'
|
import { FenceNode } from './schema'
|
||||||
|
import { fenceSlots } from './slots'
|
||||||
|
|
||||||
const SIDE_HANDLE_OFFSET = 0.27
|
const SIDE_HANDLE_OFFSET = 0.27
|
||||||
const SIDE_HANDLE_MIN_OFFSET = 0.33
|
const SIDE_HANDLE_MIN_OFFSET = 0.33
|
||||||
@@ -163,6 +165,8 @@ export const fenceDefinition: NodeDefinition<typeof FenceNode> = {
|
|||||||
surfaces: { sides: { faces: 'all' } },
|
surfaces: { sides: { faces: 'all' } },
|
||||||
duplicable: true,
|
duplicable: true,
|
||||||
deletable: true,
|
deletable: true,
|
||||||
|
slots: () => fenceSlots(),
|
||||||
|
paint: fencePaint,
|
||||||
// Placed by drawing the span with the two-click tool; a saved preset
|
// Placed by drawing the span with the two-click tool; a saved preset
|
||||||
// seeds its build parameters via `toolDefaults.fence` (see `tool.tsx`
|
// seeds its build parameters via `toolDefaults.fence` (see `tool.tsx`
|
||||||
// and `createFenceOnCurrentLevel`).
|
// and `createFenceOnCurrentLevel`).
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { type GeometryContext, getMaterialPresetByRef } from '@pascal-app/core'
|
||||||
import {
|
import {
|
||||||
DEFAULT_STAIR_MATERIAL,
|
applyMaterialPresetToMaterials,
|
||||||
|
type ColorPreset,
|
||||||
|
createDefaultMaterial,
|
||||||
|
createMaterial,
|
||||||
|
createSurfaceRoleMaterial,
|
||||||
generateFenceGeometry,
|
generateFenceGeometry,
|
||||||
type RenderShading,
|
type RenderShading,
|
||||||
|
resolveMaterialRef,
|
||||||
|
resolveSlotDefaultMaterial,
|
||||||
} from '@pascal-app/viewer'
|
} from '@pascal-app/viewer'
|
||||||
import { Group, Mesh } from 'three'
|
import { FrontSide, Group, type Material, Mesh, type Texture } from 'three'
|
||||||
import type { FenceNode } from './schema'
|
import type { FenceNode } from './schema'
|
||||||
|
import { FENCE_SLOT_DEFAULT } from './slots'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
* Stage B builder for fence. Reuses the legacy `generateFenceGeometry`
|
||||||
@@ -12,26 +20,98 @@ import type { FenceNode } from './schema'
|
|||||||
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
* posts + base + top rail + curve spans) and wraps it in a Mesh-in-Group
|
||||||
* shape the generic `<GeometrySystem>` expects.
|
* shape the generic `<GeometrySystem>` expects.
|
||||||
*
|
*
|
||||||
* Material is a single shared reference — fences look the same regardless
|
* Materials follow the unified slot model: the single `surface` slot resolves
|
||||||
* of instance, so we don't clone per node. If per-fence material
|
* `node.slots.surface` (a shared scene material or `library:` finish) → the
|
||||||
* customization lands later (color picker on the panel maps to a real
|
* legacy inline `node.material` / `materialPreset` (pre-slot-model scenes) →
|
||||||
* material), this becomes a per-node lookup.
|
* the declared slot default. Textures-off collapses to themed joinery.
|
||||||
*
|
*
|
||||||
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
* Phase 6 cleanup moves the 280 lines of geometry math out of the
|
||||||
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
* legacy `viewer/src/systems/fence/fence-system.tsx` into this folder
|
||||||
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
* once the legacy system file is deleted. Until then `generateFenceGeometry`
|
||||||
* is publicly re-exported from viewer.
|
* is publicly re-exported from viewer.
|
||||||
*/
|
*/
|
||||||
|
type FenceMaterial = Material & {
|
||||||
|
alphaMap?: Texture | null
|
||||||
|
depthWrite: boolean
|
||||||
|
opacity: number
|
||||||
|
transparent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const fenceMaterialCache = new Map<string, Material>()
|
||||||
|
|
||||||
|
function getFenceMaterial(
|
||||||
|
node: FenceNode,
|
||||||
|
shading: RenderShading,
|
||||||
|
textures: boolean,
|
||||||
|
colorPreset: ColorPreset,
|
||||||
|
sceneTheme: string | undefined,
|
||||||
|
sceneMaterials: GeometryContext['materials'],
|
||||||
|
): Material {
|
||||||
|
if (!textures) {
|
||||||
|
return createSurfaceRoleMaterial('joinery', colorPreset, FrontSide, sceneTheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotRef = node.slots?.surface
|
||||||
|
if (slotRef) {
|
||||||
|
const resolved = resolveMaterialRef(slotRef, sceneMaterials, shading)
|
||||||
|
if (resolved) return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.materialPreset || node.material) {
|
||||||
|
return getLegacyFenceMaterial(node, shading)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveSlotDefaultMaterial(FENCE_SLOT_DEFAULT, shading, 0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLegacyFenceMaterial(node: FenceNode, shading: RenderShading): Material {
|
||||||
|
const cacheKey = JSON.stringify({
|
||||||
|
shading,
|
||||||
|
material: node.material ?? null,
|
||||||
|
materialPreset: node.materialPreset ?? null,
|
||||||
|
})
|
||||||
|
const cached = fenceMaterialCache.get(cacheKey)
|
||||||
|
if (cached) return cached
|
||||||
|
|
||||||
|
const preset = getMaterialPresetByRef(node.materialPreset)
|
||||||
|
const material = preset
|
||||||
|
? createDefaultMaterial('#ffffff', 0.5, shading)
|
||||||
|
: node.material
|
||||||
|
? createMaterial(node.material, shading).clone()
|
||||||
|
: createDefaultMaterial('#ffffff', 0.9, shading)
|
||||||
|
|
||||||
|
if (preset) {
|
||||||
|
applyMaterialPresetToMaterials(material, preset)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fenceMaterial = material as FenceMaterial
|
||||||
|
fenceMaterial.transparent = false
|
||||||
|
fenceMaterial.opacity = 1
|
||||||
|
fenceMaterial.alphaMap = null
|
||||||
|
fenceMaterial.side = FrontSide
|
||||||
|
fenceMaterial.depthWrite = true
|
||||||
|
fenceMaterial.needsUpdate = true
|
||||||
|
|
||||||
|
fenceMaterialCache.set(cacheKey, material)
|
||||||
|
return material
|
||||||
|
}
|
||||||
|
|
||||||
export function buildFenceGeometry(
|
export function buildFenceGeometry(
|
||||||
node: FenceNode,
|
node: FenceNode,
|
||||||
_ctx?: unknown,
|
ctx?: GeometryContext,
|
||||||
shading: RenderShading = 'rendered',
|
shading: RenderShading = 'rendered',
|
||||||
|
textures = true,
|
||||||
|
colorPreset: ColorPreset = 'clay',
|
||||||
|
sceneTheme?: string,
|
||||||
): Group {
|
): Group {
|
||||||
const group = new Group()
|
const group = new Group()
|
||||||
const geometry = generateFenceGeometry(node)
|
const geometry = generateFenceGeometry(node)
|
||||||
const mesh = new Mesh(geometry, DEFAULT_STAIR_MATERIAL(shading))
|
const material = getFenceMaterial(node, shading, textures, colorPreset, sceneTheme, ctx?.materials)
|
||||||
|
const mesh = new Mesh(geometry, material)
|
||||||
mesh.castShadow = true
|
mesh.castShadow = true
|
||||||
mesh.receiveShadow = true
|
mesh.receiveShadow = true
|
||||||
|
mesh.userData.slotId = 'surface'
|
||||||
|
mesh.userData.surfaceRole = 'joinery'
|
||||||
group.add(mesh)
|
group.add(mesh)
|
||||||
return group
|
return group
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { AnyNode, FenceNode } from '@pascal-app/core'
|
||||||
|
import { createSlotPaintCapability, previewGeometrySlot } from '../shared/slot-paint'
|
||||||
|
|
||||||
|
export const fencePaint = createSlotPaintCapability({
|
||||||
|
resolveRole: () => 'surface',
|
||||||
|
applyPreview: previewGeometrySlot,
|
||||||
|
legacyEffective: (node: AnyNode) => {
|
||||||
|
const fence = node as FenceNode
|
||||||
|
if (fence.materialPreset || fence.material) {
|
||||||
|
return { material: fence.material, materialPreset: fence.materialPreset }
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { SlotDeclaration } from '@pascal-app/core'
|
||||||
|
|
||||||
|
export type FenceSlotId = 'surface'
|
||||||
|
|
||||||
|
export const FENCE_SLOT_DEFAULT = 'library:wood-finewood27'
|
||||||
|
|
||||||
|
export function fenceSlots(): SlotDeclaration[] {
|
||||||
|
return [{ slotId: 'surface', label: 'Surface', default: FENCE_SLOT_DEFAULT }]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user