From 2876f39876abbdc77e04cb0cc9c83d2fb790ff9f Mon Sep 17 00:00:00 2001 From: sudhir Date: Thu, 23 Apr 2026 12:48:50 +0530 Subject: [PATCH] Refactor paint materials to use category-based compatibility --- packages/core/src/index.ts | 5 +- packages/core/src/material-library.ts | 120 +++++----- .../src/components/ui/action-menu/index.tsx | 47 +--- .../ui/controls/material-picker.tsx | 212 ++++++------------ .../src/components/ui/panels/paint-panel.tsx | 164 ++++++++++++++ .../components/ui/panels/panel-manager.tsx | 13 ++ packages/editor/src/lib/material-paint.ts | 32 ++- packages/editor/src/store/use-editor.tsx | 4 + 8 files changed, 358 insertions(+), 239 deletions(-) create mode 100644 packages/editor/src/components/ui/panels/paint-panel.tsx diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8c7778ba..d8d80978 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,11 +40,14 @@ export { export { baseMaterial, glassMaterial } from './materials' export { getCatalogMaterialById, + getMaterialCategoryTargets, getLibraryMaterialIdFromRef, getMaterialPresetByRef, - getMaterialsForTarget, + getMaterialsForCategory, LIBRARY_MATERIAL_REF_PREFIX, MATERIAL_CATALOG, + MATERIAL_CATEGORIES, + type MaterialCategory, type MaterialCatalogItem, toLibraryMaterialRef, } from './material-library' diff --git a/packages/core/src/material-library.ts b/packages/core/src/material-library.ts index f5455c96..a52ac07f 100644 --- a/packages/core/src/material-library.ts +++ b/packages/core/src/material-library.ts @@ -7,51 +7,63 @@ import { export type MaterialCatalogItem = { id: string label: string + category: MaterialCategory description?: string - targets: MaterialTarget[] previewThumbnailUrl?: string previewColor?: string preset: MaterialPresetPayload } -const WALL_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.wall, -] +export const MATERIAL_CATEGORIES = [ + 'wood', + 'wallpaper', + 'parquet', + 'granite', + 'marble', + 'other', +] as const +export type MaterialCategory = (typeof MATERIAL_CATEGORIES)[number] -const SLAB_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.slab, -] - -const WALL_AND_SLAB_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.wall, - MaterialTargetSchema.enum.slab, -] - -const STAIR_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.stair, - MaterialTargetSchema.enum['stair-segment'], -] - -const STAIR_AND_FENCE_TARGETS: MaterialTarget[] = [ - ...STAIR_TARGETS, - MaterialTargetSchema.enum.fence, -] - -const ROOF_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.roof, - MaterialTargetSchema.enum['roof-segment'], -] - -const CEILING_TARGETS: MaterialTarget[] = [ - MaterialTargetSchema.enum.ceiling, -] +export const MATERIAL_CATEGORY_TARGETS: Record = { + wood: [ + MaterialTargetSchema.enum.wall, + MaterialTargetSchema.enum.slab, + MaterialTargetSchema.enum.stair, + MaterialTargetSchema.enum['stair-segment'], + MaterialTargetSchema.enum.fence, + MaterialTargetSchema.enum.roof, + MaterialTargetSchema.enum['roof-segment'], + ], + wallpaper: [MaterialTargetSchema.enum.wall], + parquet: [ + MaterialTargetSchema.enum.slab, + MaterialTargetSchema.enum.stair, + MaterialTargetSchema.enum['stair-segment'], + ], + granite: [MaterialTargetSchema.enum.slab], + marble: [ + MaterialTargetSchema.enum.slab, + MaterialTargetSchema.enum.stair, + MaterialTargetSchema.enum['stair-segment'], + ], + other: [ + MaterialTargetSchema.enum.wall, + MaterialTargetSchema.enum.roof, + MaterialTargetSchema.enum['roof-segment'], + MaterialTargetSchema.enum.stair, + MaterialTargetSchema.enum['stair-segment'], + MaterialTargetSchema.enum.fence, + MaterialTargetSchema.enum.slab, + MaterialTargetSchema.enum.ceiling, + ], +} export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wood1', label: 'Wood', + category: 'wood', description: 'Warm wood finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS], previewThumbnailUrl: '/material/wood1/wood1_thumbnail.webp', preset: { maps: { @@ -85,8 +97,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wood2', label: 'Wood', + category: 'wood', description: 'Textured wood finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS], previewThumbnailUrl: '/material/wood2/wood2_thumbnail.webp', preset: { maps: { @@ -121,8 +133,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wood3', label: 'Wood', + category: 'wood', description: 'Knotted timber finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS], previewThumbnailUrl: '/material/wood3/wood3_thumbnail.webp', preset: { maps: { @@ -155,8 +167,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wood4', label: 'Wood', + category: 'wood', description: 'Oak stretcher finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS], previewThumbnailUrl: '/material/wood4/wood4_thumbnail.webp', preset: { maps: { @@ -189,8 +201,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wood5', label: 'Wood', + category: 'wood', description: 'Rich grain wood finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS], previewThumbnailUrl: '/material/wood5/wood5_thumnail.webp', preset: { maps: { @@ -225,8 +237,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-granite1', label: 'Granite', + category: 'granite', description: 'Polished granite finish', - targets: SLAB_TARGETS, previewThumbnailUrl: '/material/granite1/granite_thumbnail.webp', preset: { maps: { @@ -259,8 +271,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-marble1', label: 'Marble', + category: 'marble', description: 'Smooth marble finish', - targets: [...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS], previewThumbnailUrl: '/material/marble1/marble1_thumbnail.webp', preset: { maps: { @@ -293,8 +305,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-marble2', label: 'Marble', + category: 'marble', description: 'Soft marble finish', - targets: [...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS], previewThumbnailUrl: '/material/marble2/marble2_thumbnail.webp', preset: { maps: { @@ -327,8 +339,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-parquet1', label: 'Parquet', + category: 'parquet', description: 'Parquet wood finish', - targets: SLAB_TARGETS, previewThumbnailUrl: '/material/parquet1/parquet_thumnail.webp', preset: { maps: { @@ -361,8 +373,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-parquet2', label: 'Parquet', + category: 'parquet', description: 'Soft parquet finish', - targets: SLAB_TARGETS, previewThumbnailUrl: '/material/parquet2/parquet2_thumbnail.webp', preset: { maps: { @@ -395,8 +407,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wallpaper1', label: 'Wallpaper', + category: 'wallpaper', description: 'Soft wallpaper finish', - targets: WALL_TARGETS, previewThumbnailUrl: '/material/wallpaper1/wallpaper1_thumbnail.webp', preset: { maps: { @@ -430,8 +442,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wallpaper2', label: 'Wallpaper', + category: 'wallpaper', description: 'Decorative wallpaper finish', - targets: WALL_TARGETS, previewThumbnailUrl: '/material/wallpaper2/wallpaper2_thumnail.webp', preset: { maps: { @@ -464,8 +476,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'wall-wallpaper3', label: 'Wallpaper', + category: 'wallpaper', description: 'Patterned wallpaper finish', - targets: WALL_TARGETS, previewThumbnailUrl: '/material/wallpaper3/wallpaper3_thumbnail.webp', preset: { maps: { @@ -498,14 +510,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'preset-white', label: 'White', + category: 'other', description: 'Clean painted finish', - targets: [ - ...WALL_TARGETS, - ...SLAB_TARGETS, - ...ROOF_TARGETS, - ...STAIR_AND_FENCE_TARGETS, - ...CEILING_TARGETS, - ], previewColor: '#ffffff', preset: { maps: {}, @@ -536,8 +542,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'preset-metal', label: 'Metal', + category: 'other', description: 'Brushed metal finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS], previewColor: '#c0c0c0', preset: { maps: {}, @@ -568,8 +574,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ { id: 'preset-glass', label: 'Glass', + category: 'other', description: 'Light glass finish', - targets: [...WALL_TARGETS, ...SLAB_TARGETS], previewColor: '#87ceeb', preset: { maps: {}, @@ -599,8 +605,12 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [ }, ] -export function getMaterialsForTarget(target: MaterialTarget): MaterialCatalogItem[] { - return MATERIAL_CATALOG.filter((item) => item.targets.includes(target)) +export function getMaterialsForCategory(category: MaterialCategory): MaterialCatalogItem[] { + return MATERIAL_CATALOG.filter((item) => item.category === category) +} + +export function getMaterialCategoryTargets(category: MaterialCategory): MaterialTarget[] { + return MATERIAL_CATEGORY_TARGETS[category] } export function getCatalogMaterialById(id?: string): MaterialCatalogItem | undefined { diff --git a/packages/editor/src/components/ui/action-menu/index.tsx b/packages/editor/src/components/ui/action-menu/index.tsx index 5eb7bf49..e1c59cb4 100644 --- a/packages/editor/src/components/ui/action-menu/index.tsx +++ b/packages/editor/src/components/ui/action-menu/index.tsx @@ -1,16 +1,13 @@ 'use client' -import { getMaterialsForTarget, toLibraryMaterialRef, useScene } from '@pascal-app/core' +import { useScene } from '@pascal-app/core' import { AnimatePresence, motion } from 'motion/react' import { useEffect, useMemo } from 'react' import { useViewer } from '@pascal-app/viewer' import { TooltipProvider } from './../../../components/ui/primitives/tooltip' import { MaterialPicker } from './../../../components/ui/controls/material-picker' import { useReducedMotion } from './../../../hooks/use-reduced-motion' -import { - isActivePaintMaterialCompatible, - resolvePaintTargetFromSelection, -} from './../../../lib/material-paint' +import { resolvePaintTargetFromSelection } from './../../../lib/material-paint' import { cn } from './../../../lib/utils' import useEditor from './../../../store/use-editor' import { ItemCatalog } from '../item-catalog/item-catalog' @@ -40,45 +37,17 @@ function PaintMaterialTray() { } }, [nodes, selectedId, setActivePaintTarget]) - const catalogItems = useMemo(() => getMaterialsForTarget(activePaintTarget), [activePaintTarget]) - - useEffect(() => { - const firstMaterial = catalogItems[0] - if (!firstMaterial) return - - const hasCompatibleMaterial = isActivePaintMaterialCompatible( - activePaintMaterial, - activePaintTarget, - ) - if (hasCompatibleMaterial) return - - setActivePaintMaterial({ - materialPreset: toLibraryMaterialRef(firstMaterial.id), - sourceTarget: activePaintTarget, - }) - }, [activePaintMaterial, activePaintTarget, catalogItems, setActivePaintMaterial]) - return (
{ - setActivePaintMaterial({ material, sourceTarget: activePaintTarget }) + onChange={(material, category) => { + setActivePaintMaterial({ material, category, sourceTarget: activePaintTarget }) }} - onSelectMaterialPreset={(materialPreset) => { - setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget }) + onSelectMaterialPreset={(materialPreset, category) => { + setActivePaintMaterial({ materialPreset, category, sourceTarget: activePaintTarget }) }} - selectedMaterialPreset={ - activePaintMaterial?.sourceTarget === activePaintTarget - ? activePaintMaterial.materialPreset - : undefined - } - value={ - activePaintMaterial?.sourceTarget === activePaintTarget - ? activePaintMaterial.material - : undefined - } + selectedMaterialPreset={activePaintMaterial?.materialPreset} + value={activePaintMaterial?.material} />
) diff --git a/packages/editor/src/components/ui/controls/material-picker.tsx b/packages/editor/src/components/ui/controls/material-picker.tsx index e189276e..dbe0a6e3 100755 --- a/packages/editor/src/components/ui/controls/material-picker.tsx +++ b/packages/editor/src/components/ui/controls/material-picker.tsx @@ -1,55 +1,69 @@ 'use client' import { - getMaterialsForTarget, + getCatalogMaterialById, + getLibraryMaterialIdFromRef, + getMaterialsForCategory, + MATERIAL_CATEGORIES, toLibraryMaterialRef, + type MaterialCategory, type MaterialSchema, - type MaterialTarget, } from '@pascal-app/core' import { useEffect, useRef, useState } from 'react' +import useEditor from '../../../store/use-editor' type MaterialPickerProps = { - nodeType?: MaterialTarget value?: MaterialSchema selectedMaterialPreset?: string - onChange?: (material: MaterialSchema) => void - onSelectMaterialPreset?: (materialPreset: string) => void - hideSideControl?: boolean + onChange?: (material: MaterialSchema, category: MaterialCategory) => void + onSelectMaterialPreset?: (materialPreset: string, category: MaterialCategory) => void disabled?: boolean } export function MaterialPicker({ - nodeType, value, selectedMaterialPreset, onChange, onSelectMaterialPreset, - hideSideControl = false, disabled = false, }: MaterialPickerProps) { + const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen) const [showCustom, setShowCustom] = useState(!!value?.properties) + const [selectedCategory, setSelectedCategory] = useState(MATERIAL_CATEGORIES[0]) const catalogScrollRef = useRef(null) - const catalogItems = nodeType ? getMaterialsForTarget(nodeType) : [] + const catalogItems = + selectedCategory === 'other' + ? getMaterialsForCategory('other') + : getMaterialsForCategory(selectedCategory) useEffect(() => { setShowCustom(!!value?.properties && !selectedMaterialPreset) }, [selectedMaterialPreset, value?.properties]) - const currentProps = value?.properties || { - color: '#ffffff', - roughness: 0.5, - metalness: 0, - opacity: 1, - transparent: false, - side: 'front' as const, - } + useEffect(() => { + if (!selectedMaterialPreset && value?.properties) { + setSelectedCategory('other') + return + } + + const catalogId = + getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined + const selectedCatalogEntry = getCatalogMaterialById(catalogId) + if (selectedCatalogEntry?.category) { + setSelectedCategory(selectedCatalogEntry.category) + } + }, [selectedMaterialPreset, value?.id]) + const selectedCatalogId = selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined) const handleCatalogSelect = (materialId: string) => { if (disabled) return setShowCustom(false) - onSelectMaterialPreset?.(toLibraryMaterialRef(materialId)) + setPaintPanelOpen(false) + const category = getCatalogMaterialById(materialId)?.category + if (!category) return + onSelectMaterialPreset?.(toLibraryMaterialRef(materialId), category) } useEffect(() => { @@ -76,40 +90,51 @@ export function MaterialPicker({ const handleCustomOpen = () => { if (disabled) return setShowCustom(true) - onChange?.({ - preset: 'custom', - properties: { - color: value?.properties?.color || '#ffffff', - roughness: value?.properties?.roughness ?? 0.5, - metalness: value?.properties?.metalness ?? 0, - opacity: value?.properties?.opacity ?? 1, - transparent: value?.properties?.transparent ?? false, - side: value?.properties?.side ?? 'front', + setPaintPanelOpen(true) + onChange?.( + { + preset: 'custom', + properties: { + color: value?.properties?.color || '#ffffff', + roughness: value?.properties?.roughness ?? 0.5, + metalness: value?.properties?.metalness ?? 0, + opacity: value?.properties?.opacity ?? 1, + transparent: value?.properties?.transparent ?? false, + side: value?.properties?.side ?? 'front', + }, }, - }) - } - - const handlePropertyChange = ( - prop: keyof typeof currentProps, - val: (typeof currentProps)[keyof typeof currentProps], - ) => { - if (disabled) return - onChange?.({ - preset: 'custom', - properties: { - ...currentProps, - [prop]: val, - }, - }) + 'other', + ) } return (
{(catalogItems.length > 0 || onChange) && (
- {catalogItems.length > 0 ? ( -
Library
- ) : null} +
+ {MATERIAL_CATEGORIES.map((category) => ( + + ))} +
))} - {onChange ? ( + {selectedCategory === 'other' && onChange ? (
)} - - {showCustom && onChange && ( -
-
- - handlePropertyChange('color', e.target.value)} - type="color" - value={currentProps.color} - /> - handlePropertyChange('color', e.target.value)} - type="text" - value={currentProps.color} - /> -
- -
- - handlePropertyChange('roughness', Number.parseFloat(e.target.value))} - step={0.01} - type="range" - value={currentProps.roughness} - /> - - {currentProps.roughness.toFixed(2)} - -
- -
- - handlePropertyChange('metalness', Number.parseFloat(e.target.value))} - step={0.01} - type="range" - value={currentProps.metalness} - /> - - {currentProps.metalness.toFixed(2)} - -
- -
- - { - const opacity = Number.parseFloat(e.target.value) - handlePropertyChange('opacity', opacity) - if (opacity < 1 && !currentProps.transparent) { - handlePropertyChange('transparent', true) - } - }} - step={0.01} - type="range" - value={currentProps.opacity} - /> - - {currentProps.opacity.toFixed(2)} - -
- - {!hideSideControl && ( -
- - -
- )} -
- )}
) } diff --git a/packages/editor/src/components/ui/panels/paint-panel.tsx b/packages/editor/src/components/ui/panels/paint-panel.tsx new file mode 100644 index 00000000..0df8c677 --- /dev/null +++ b/packages/editor/src/components/ui/panels/paint-panel.tsx @@ -0,0 +1,164 @@ +'use client' + +import useEditor from '../../../store/use-editor' +import { Input } from '../primitives/input' +import { PanelSection } from '../controls/panel-section' +import { PanelWrapper } from './panel-wrapper' + +function buildDefaultCustomMaterial() { + return { + preset: 'custom' as const, + properties: { + color: '#ffffff', + roughness: 0.5, + metalness: 0, + opacity: 1, + transparent: false, + side: 'front' as const, + }, + } +} + +export function PaintPanel() { + const activePaintMaterial = useEditor((state) => state.activePaintMaterial) + const activePaintTarget = useEditor((state) => state.activePaintTarget) + const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial) + const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen) + + const customMaterial = + activePaintMaterial?.material?.properties && !activePaintMaterial.materialPreset + ? activePaintMaterial.material + : null + + if (!customMaterial) return null + + const currentProps = customMaterial.properties ?? buildDefaultCustomMaterial().properties + + const updateCustomMaterial = ( + updates: Partial, + nextTransparent = currentProps.transparent, + ) => { + setActivePaintMaterial({ + material: { + preset: 'custom', + properties: { + ...currentProps, + ...updates, + transparent: nextTransparent, + }, + }, + category: 'other', + sourceTarget: activePaintMaterial?.sourceTarget ?? activePaintTarget, + }) + } + + return ( + setPaintPanelOpen(false)} + title="Material" + width={320} + > + +
+
+ +
+ updateCustomMaterial({ color: e.target.value })} + type="color" + value={currentProps.color} + /> + updateCustomMaterial({ color: e.target.value })} + value={currentProps.color} + /> +
+
+ +
+
+ + + {currentProps.roughness.toFixed(2)} + +
+ updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })} + step={0.01} + type="range" + value={currentProps.roughness} + /> +
+ +
+
+ + + {currentProps.metalness.toFixed(2)} + +
+ updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })} + step={0.01} + type="range" + value={currentProps.metalness} + /> +
+ +
+
+ + + {currentProps.opacity.toFixed(2)} + +
+ { + const opacity = Number.parseFloat(e.target.value) + updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent) + }} + step={0.01} + type="range" + value={currentProps.opacity} + /> +
+ +
+ + +
+
+
+
+ ) +} diff --git a/packages/editor/src/components/ui/panels/panel-manager.tsx b/packages/editor/src/components/ui/panels/panel-manager.tsx index 1750c6fc..cc20f364 100755 --- a/packages/editor/src/components/ui/panels/panel-manager.tsx +++ b/packages/editor/src/components/ui/panels/panel-manager.tsx @@ -7,6 +7,7 @@ import { CeilingPanel } from './ceiling-panel' import { DoorPanel } from './door-panel' import { FencePanel } from './fence-panel' import { ItemPanel } from './item-panel' +import { PaintPanel } from './paint-panel' import { ReferencePanel } from './reference-panel' import { RoofPanel } from './roof-panel' import { RoofSegmentPanel } from './roof-segment-panel' @@ -19,6 +20,9 @@ import { WindowPanel } from './window-panel' export function PanelManager() { const selectedIds = useViewer((s) => s.selection.selectedIds) const selectedReferenceId = useEditor((s) => s.selectedReferenceId) + const isPaintPanelOpen = useEditor((s) => s.isPaintPanelOpen) + const mode = useEditor((s) => s.mode) + const activePaintMaterial = useEditor((s) => s.activePaintMaterial) // Only subscribe to the *type* of the single-selected node — string primitive // so we don't re-render on unrelated scene mutations. const selectedNodeType = useScene((s) => { @@ -32,6 +36,15 @@ export function PanelManager() { return } + if ( + isPaintPanelOpen && + mode === 'material-paint' && + activePaintMaterial?.material?.properties && + !activePaintMaterial.materialPreset + ) { + return + } + // Show appropriate panel based on selected node type if (selectedNodeType) { switch (selectedNodeType) { diff --git a/packages/editor/src/lib/material-paint.ts b/packages/editor/src/lib/material-paint.ts index 88bce034..e1f52156 100644 --- a/packages/editor/src/lib/material-paint.ts +++ b/packages/editor/src/lib/material-paint.ts @@ -8,6 +8,8 @@ import { getEffectiveStairSurfaceMaterial, getEffectiveWallSurfaceMaterial, getLibraryMaterialIdFromRef, + getMaterialCategoryTargets, + type MaterialCategory, type MaterialSchema, type MaterialTarget, type RoofNode, @@ -29,6 +31,7 @@ export type SingleSurfaceMaterialRole = 'surface' export type ActivePaintMaterial = { material?: MaterialSchema materialPreset?: string + category?: MaterialCategory sourceTarget: PaintableMaterialTarget } @@ -47,6 +50,17 @@ function getCatalogEntryForActivePaintMaterial(material: ActivePaintMaterial | n return getCatalogMaterialById(catalogId) } +function resolvePaintMaterialCategory( + material: MaterialSchema | undefined, + materialPreset: string | undefined, +): MaterialCategory | undefined { + const catalogId = getLibraryMaterialIdFromRef(materialPreset) ?? material?.id ?? undefined + const catalogEntry = getCatalogMaterialById(catalogId) + if (catalogEntry?.category) return catalogEntry.category + if (material || materialPreset === undefined) return 'other' + return undefined +} + export function getActivePaintMaterialLabel(material: ActivePaintMaterial | null | undefined) { return getCatalogEntryForActivePaintMaterial(material)?.label ?? 'Custom' } @@ -58,9 +72,10 @@ export function isActivePaintMaterialCompatible( if (!hasActivePaintMaterial(material)) return false const catalogEntry = getCatalogEntryForActivePaintMaterial(material) - if (!catalogEntry) return true + const category = material?.category ?? catalogEntry?.category + if (!category) return false - return catalogEntry.targets.includes(target) + return getMaterialCategoryTargets(category).includes(target) } export function buildWallSurfaceMaterialPatch( @@ -182,9 +197,10 @@ export function resolveActivePaintMaterialFromSelection(params: { materialPreset: surface.materialPreset, sourceTarget: 'wall', }) - ? { + ? { material: surface.material, materialPreset: surface.materialPreset, + category: resolvePaintMaterialCategory(surface.material, surface.materialPreset), sourceTarget: 'wall', } : null @@ -202,9 +218,10 @@ export function resolveActivePaintMaterialFromSelection(params: { materialPreset: surface.materialPreset, sourceTarget: 'roof', }) - ? { + ? { material: surface.material, materialPreset: surface.materialPreset, + category: resolvePaintMaterialCategory(surface.material, surface.materialPreset), sourceTarget: 'roof', } : null @@ -222,9 +239,10 @@ export function resolveActivePaintMaterialFromSelection(params: { materialPreset: surface.materialPreset, sourceTarget: 'stair', }) - ? { + ? { material: surface.material, materialPreset: surface.materialPreset, + category: resolvePaintMaterialCategory(surface.material, surface.materialPreset), sourceTarget: 'stair', } : null @@ -245,6 +263,10 @@ export function resolveActivePaintMaterialFromSelection(params: { ? { material: selectedNode.material, materialPreset: selectedNode.materialPreset, + category: resolvePaintMaterialCategory( + selectedNode.material, + selectedNode.materialPreset, + ), sourceTarget: target, } : null diff --git a/packages/editor/src/store/use-editor.tsx b/packages/editor/src/store/use-editor.tsx index 96043be3..f6d0488b 100644 --- a/packages/editor/src/store/use-editor.tsx +++ b/packages/editor/src/store/use-editor.tsx @@ -169,6 +169,8 @@ type EditorState = { primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot hoveredPaintTarget: PaintableMaterialTarget | null setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void + isPaintPanelOpen: boolean + setPaintPanelOpen: (open: boolean) => void selectedReferenceId: string | null setSelectedReferenceId: (id: string | null) => void // Space detection for cutaway mode @@ -591,6 +593,8 @@ const useEditor = create()( set((state) => state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target }, ), + isPaintPanelOpen: false, + setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }), selectedReferenceId: null, setSelectedReferenceId: (id) => set({ selectedReferenceId: id }), spaces: {},