feat(paint-slots): authored item materials + unified slot painting
Phase 1 + paint unification of the paint-slots plan. - core: scene-material data layer (materials map mirroring collections, undo/partialize/setScene full-graph support), SceneMaterial schema, scene:/library: MaterialRef helpers + parseMaterialRef, slot id helpers (deriveSlotId/slotLabelFromId), slots map on ItemNode, hitObject on PaintResolveArgs, optional PaintCapability.commit. - viewer: resolveMaterialRef (library:/scene: -> three material, null on dangling). - nodes(item): renderer keeps authored GLB materials for slot-authored assets and applies per-slot overrides per-instance (never mutates the shared cached GLB); textures-off still collapses to furnishing role; non-authored items unchanged. Item paint capability + registration. - editor: item joins the unified (nodeId, slotId) paint dispatch; item paint target + slot reset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cf24b62c44
commit
7afb286e47
@@ -906,6 +906,7 @@ export const SelectionManager = () => {
|
||||
normal: event.normal,
|
||||
localPosition: event.localPosition as readonly [number, number, number] | undefined,
|
||||
hitObjectName: event.nativeEvent.object?.name,
|
||||
hitObject: getEventObject(event),
|
||||
})
|
||||
const compatible = role !== null && paintEnabled
|
||||
return {
|
||||
@@ -915,15 +916,22 @@ export const SelectionManager = () => {
|
||||
apply:
|
||||
compatible && role
|
||||
? () => {
|
||||
useScene.getState().updateNode(
|
||||
node.id as AnyNodeId,
|
||||
paintCap.buildPatch({
|
||||
node,
|
||||
role,
|
||||
material: paintSpec.material,
|
||||
materialPreset: paintSpec.materialPreset,
|
||||
}) as Partial<AnyNode>,
|
||||
)
|
||||
const args = {
|
||||
node,
|
||||
role,
|
||||
material: paintSpec.material,
|
||||
materialPreset: paintSpec.materialPreset,
|
||||
}
|
||||
if (paintCap.commit) {
|
||||
paintCap.commit(args)
|
||||
} else {
|
||||
useScene
|
||||
.getState()
|
||||
.updateNode(
|
||||
node.id as AnyNodeId,
|
||||
paintCap.buildPatch(args) as Partial<AnyNode>,
|
||||
)
|
||||
}
|
||||
}
|
||||
: null,
|
||||
preview:
|
||||
@@ -1086,7 +1094,7 @@ export const SelectionManager = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const disabledNodeTypes = ['item', 'window', 'door', 'zone']
|
||||
const disabledNodeTypes = ['window', 'door', 'zone']
|
||||
if (disabledNodeTypes.includes(node.type)) {
|
||||
return {
|
||||
key: `${node.type}:${node.id}:unsupported`,
|
||||
@@ -1549,6 +1557,7 @@ export const SelectionManager = () => {
|
||||
normal: event.normal,
|
||||
localPosition: event.localPosition as readonly [number, number, number] | undefined,
|
||||
hitObjectName: event.nativeEvent.object?.name,
|
||||
hitObject: getEventObject(event),
|
||||
})
|
||||
if (role) {
|
||||
setSelectedMaterialTargetForNode(nodeToSelect, role as MaterialTargetRole)
|
||||
@@ -1930,7 +1939,8 @@ const SelectionStateSync = () => {
|
||||
const selectedNode = useScene.getState().nodes[singleSelectedId as AnyNodeId]
|
||||
if (
|
||||
!selectedNode ||
|
||||
(selectedNode.type !== 'wall' &&
|
||||
(!nodeRegistry.get(selectedNode.type)?.capabilities?.paint &&
|
||||
selectedNode.type !== 'wall' &&
|
||||
selectedNode.type !== 'fence' &&
|
||||
selectedNode.type !== 'slab' &&
|
||||
selectedNode.type !== 'ceiling' &&
|
||||
|
||||
@@ -30,24 +30,26 @@ import {
|
||||
type WallSurfaceSide,
|
||||
} from '@pascal-app/core'
|
||||
|
||||
export type PaintableMaterialTarget = Extract<
|
||||
MaterialTarget,
|
||||
| 'wall'
|
||||
| 'roof'
|
||||
| 'stair'
|
||||
| 'fence'
|
||||
| 'column'
|
||||
| 'slab'
|
||||
| 'ceiling'
|
||||
| 'shelf'
|
||||
| 'chimney'
|
||||
| 'dormer'
|
||||
| 'box-vent'
|
||||
| 'ridge-vent'
|
||||
| 'turbine-vent'
|
||||
| 'cupola'
|
||||
| 'eyebrow-vent'
|
||||
>
|
||||
export type PaintableMaterialTarget =
|
||||
| Extract<
|
||||
MaterialTarget,
|
||||
| 'wall'
|
||||
| 'roof'
|
||||
| 'stair'
|
||||
| 'fence'
|
||||
| 'column'
|
||||
| 'slab'
|
||||
| 'ceiling'
|
||||
| 'shelf'
|
||||
| 'chimney'
|
||||
| 'dormer'
|
||||
| 'box-vent'
|
||||
| 'ridge-vent'
|
||||
| 'turbine-vent'
|
||||
| 'cupola'
|
||||
| 'eyebrow-vent'
|
||||
>
|
||||
| 'item'
|
||||
|
||||
export type SingleSurfaceMaterialRole = 'surface'
|
||||
|
||||
@@ -179,6 +181,7 @@ export function buildResetSurfaceMaterialUpdates(
|
||||
if (
|
||||
key === 'material' ||
|
||||
key === 'materialPreset' ||
|
||||
key === 'slots' ||
|
||||
key.endsWith('Material') ||
|
||||
key.endsWith('MaterialPreset')
|
||||
) {
|
||||
@@ -276,6 +279,7 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
||||
| ChimneyMaterialRole
|
||||
| DormerSurfaceMaterialRole
|
||||
| SingleSurfaceMaterialRole
|
||||
| string
|
||||
} | null
|
||||
}): ActivePaintMaterial | null {
|
||||
const { nodes, selectedId, selectedMaterialTarget } = params
|
||||
@@ -444,6 +448,10 @@ export function resolvePaintTargetFromSelection(params: {
|
||||
return 'shelf'
|
||||
}
|
||||
|
||||
if (selectedNode.type === 'item') {
|
||||
return 'item'
|
||||
}
|
||||
|
||||
if (selectedNode.type === 'chimney') {
|
||||
return 'chimney'
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ export type MaterialTargetRole =
|
||||
| ChimneyMaterialRole
|
||||
| DormerSurfaceMaterialRole
|
||||
| SingleSurfaceMaterialRole
|
||||
| string
|
||||
|
||||
export type SelectedMaterialTarget = {
|
||||
nodeId: AnyNodeId
|
||||
|
||||
Reference in New Issue
Block a user