Refactor paint materials to use category-based compatibility
This commit is contained in:
@@ -40,11 +40,14 @@ export {
|
|||||||
export { baseMaterial, glassMaterial } from './materials'
|
export { baseMaterial, glassMaterial } from './materials'
|
||||||
export {
|
export {
|
||||||
getCatalogMaterialById,
|
getCatalogMaterialById,
|
||||||
|
getMaterialCategoryTargets,
|
||||||
getLibraryMaterialIdFromRef,
|
getLibraryMaterialIdFromRef,
|
||||||
getMaterialPresetByRef,
|
getMaterialPresetByRef,
|
||||||
getMaterialsForTarget,
|
getMaterialsForCategory,
|
||||||
LIBRARY_MATERIAL_REF_PREFIX,
|
LIBRARY_MATERIAL_REF_PREFIX,
|
||||||
MATERIAL_CATALOG,
|
MATERIAL_CATALOG,
|
||||||
|
MATERIAL_CATEGORIES,
|
||||||
|
type MaterialCategory,
|
||||||
type MaterialCatalogItem,
|
type MaterialCatalogItem,
|
||||||
toLibraryMaterialRef,
|
toLibraryMaterialRef,
|
||||||
} from './material-library'
|
} from './material-library'
|
||||||
|
|||||||
@@ -7,51 +7,63 @@ import {
|
|||||||
export type MaterialCatalogItem = {
|
export type MaterialCatalogItem = {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
|
category: MaterialCategory
|
||||||
description?: string
|
description?: string
|
||||||
targets: MaterialTarget[]
|
|
||||||
previewThumbnailUrl?: string
|
previewThumbnailUrl?: string
|
||||||
previewColor?: string
|
previewColor?: string
|
||||||
preset: MaterialPresetPayload
|
preset: MaterialPresetPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
const WALL_TARGETS: MaterialTarget[] = [
|
export const MATERIAL_CATEGORIES = [
|
||||||
MaterialTargetSchema.enum.wall,
|
'wood',
|
||||||
]
|
'wallpaper',
|
||||||
|
'parquet',
|
||||||
|
'granite',
|
||||||
|
'marble',
|
||||||
|
'other',
|
||||||
|
] as const
|
||||||
|
export type MaterialCategory = (typeof MATERIAL_CATEGORIES)[number]
|
||||||
|
|
||||||
const SLAB_TARGETS: MaterialTarget[] = [
|
export const MATERIAL_CATEGORY_TARGETS: Record<MaterialCategory, MaterialTarget[]> = {
|
||||||
MaterialTargetSchema.enum.slab,
|
wood: [
|
||||||
]
|
MaterialTargetSchema.enum.wall,
|
||||||
|
MaterialTargetSchema.enum.slab,
|
||||||
const WALL_AND_SLAB_TARGETS: MaterialTarget[] = [
|
MaterialTargetSchema.enum.stair,
|
||||||
MaterialTargetSchema.enum.wall,
|
MaterialTargetSchema.enum['stair-segment'],
|
||||||
MaterialTargetSchema.enum.slab,
|
MaterialTargetSchema.enum.fence,
|
||||||
]
|
MaterialTargetSchema.enum.roof,
|
||||||
|
MaterialTargetSchema.enum['roof-segment'],
|
||||||
const STAIR_TARGETS: MaterialTarget[] = [
|
],
|
||||||
MaterialTargetSchema.enum.stair,
|
wallpaper: [MaterialTargetSchema.enum.wall],
|
||||||
MaterialTargetSchema.enum['stair-segment'],
|
parquet: [
|
||||||
]
|
MaterialTargetSchema.enum.slab,
|
||||||
|
MaterialTargetSchema.enum.stair,
|
||||||
const STAIR_AND_FENCE_TARGETS: MaterialTarget[] = [
|
MaterialTargetSchema.enum['stair-segment'],
|
||||||
...STAIR_TARGETS,
|
],
|
||||||
MaterialTargetSchema.enum.fence,
|
granite: [MaterialTargetSchema.enum.slab],
|
||||||
]
|
marble: [
|
||||||
|
MaterialTargetSchema.enum.slab,
|
||||||
const ROOF_TARGETS: MaterialTarget[] = [
|
MaterialTargetSchema.enum.stair,
|
||||||
MaterialTargetSchema.enum.roof,
|
MaterialTargetSchema.enum['stair-segment'],
|
||||||
MaterialTargetSchema.enum['roof-segment'],
|
],
|
||||||
]
|
other: [
|
||||||
|
MaterialTargetSchema.enum.wall,
|
||||||
const CEILING_TARGETS: MaterialTarget[] = [
|
MaterialTargetSchema.enum.roof,
|
||||||
MaterialTargetSchema.enum.ceiling,
|
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[] = [
|
export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
||||||
{
|
{
|
||||||
id: 'wall-wood1',
|
id: 'wall-wood1',
|
||||||
label: 'Wood',
|
label: 'Wood',
|
||||||
|
category: 'wood',
|
||||||
description: 'Warm wood finish',
|
description: 'Warm wood finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/wood1/wood1_thumbnail.webp',
|
previewThumbnailUrl: '/material/wood1/wood1_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -85,8 +97,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wood2',
|
id: 'wall-wood2',
|
||||||
label: 'Wood',
|
label: 'Wood',
|
||||||
|
category: 'wood',
|
||||||
description: 'Textured wood finish',
|
description: 'Textured wood finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/wood2/wood2_thumbnail.webp',
|
previewThumbnailUrl: '/material/wood2/wood2_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -121,8 +133,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wood3',
|
id: 'wall-wood3',
|
||||||
label: 'Wood',
|
label: 'Wood',
|
||||||
|
category: 'wood',
|
||||||
description: 'Knotted timber finish',
|
description: 'Knotted timber finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS, ...ROOF_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/wood3/wood3_thumbnail.webp',
|
previewThumbnailUrl: '/material/wood3/wood3_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -155,8 +167,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wood4',
|
id: 'wall-wood4',
|
||||||
label: 'Wood',
|
label: 'Wood',
|
||||||
|
category: 'wood',
|
||||||
description: 'Oak stretcher finish',
|
description: 'Oak stretcher finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/wood4/wood4_thumbnail.webp',
|
previewThumbnailUrl: '/material/wood4/wood4_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -189,8 +201,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wood5',
|
id: 'wall-wood5',
|
||||||
label: 'Wood',
|
label: 'Wood',
|
||||||
|
category: 'wood',
|
||||||
description: 'Rich grain wood finish',
|
description: 'Rich grain wood finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/wood5/wood5_thumnail.webp',
|
previewThumbnailUrl: '/material/wood5/wood5_thumnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -225,8 +237,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-granite1',
|
id: 'wall-granite1',
|
||||||
label: 'Granite',
|
label: 'Granite',
|
||||||
|
category: 'granite',
|
||||||
description: 'Polished granite finish',
|
description: 'Polished granite finish',
|
||||||
targets: SLAB_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/granite1/granite_thumbnail.webp',
|
previewThumbnailUrl: '/material/granite1/granite_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -259,8 +271,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-marble1',
|
id: 'wall-marble1',
|
||||||
label: 'Marble',
|
label: 'Marble',
|
||||||
|
category: 'marble',
|
||||||
description: 'Smooth marble finish',
|
description: 'Smooth marble finish',
|
||||||
targets: [...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/marble1/marble1_thumbnail.webp',
|
previewThumbnailUrl: '/material/marble1/marble1_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -293,8 +305,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-marble2',
|
id: 'wall-marble2',
|
||||||
label: 'Marble',
|
label: 'Marble',
|
||||||
|
category: 'marble',
|
||||||
description: 'Soft marble finish',
|
description: 'Soft marble finish',
|
||||||
targets: [...SLAB_TARGETS, ...STAIR_AND_FENCE_TARGETS],
|
|
||||||
previewThumbnailUrl: '/material/marble2/marble2_thumbnail.webp',
|
previewThumbnailUrl: '/material/marble2/marble2_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -327,8 +339,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-parquet1',
|
id: 'wall-parquet1',
|
||||||
label: 'Parquet',
|
label: 'Parquet',
|
||||||
|
category: 'parquet',
|
||||||
description: 'Parquet wood finish',
|
description: 'Parquet wood finish',
|
||||||
targets: SLAB_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/parquet1/parquet_thumnail.webp',
|
previewThumbnailUrl: '/material/parquet1/parquet_thumnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -361,8 +373,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-parquet2',
|
id: 'wall-parquet2',
|
||||||
label: 'Parquet',
|
label: 'Parquet',
|
||||||
|
category: 'parquet',
|
||||||
description: 'Soft parquet finish',
|
description: 'Soft parquet finish',
|
||||||
targets: SLAB_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/parquet2/parquet2_thumbnail.webp',
|
previewThumbnailUrl: '/material/parquet2/parquet2_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -395,8 +407,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wallpaper1',
|
id: 'wall-wallpaper1',
|
||||||
label: 'Wallpaper',
|
label: 'Wallpaper',
|
||||||
|
category: 'wallpaper',
|
||||||
description: 'Soft wallpaper finish',
|
description: 'Soft wallpaper finish',
|
||||||
targets: WALL_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/wallpaper1/wallpaper1_thumbnail.webp',
|
previewThumbnailUrl: '/material/wallpaper1/wallpaper1_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -430,8 +442,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wallpaper2',
|
id: 'wall-wallpaper2',
|
||||||
label: 'Wallpaper',
|
label: 'Wallpaper',
|
||||||
|
category: 'wallpaper',
|
||||||
description: 'Decorative wallpaper finish',
|
description: 'Decorative wallpaper finish',
|
||||||
targets: WALL_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/wallpaper2/wallpaper2_thumnail.webp',
|
previewThumbnailUrl: '/material/wallpaper2/wallpaper2_thumnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -464,8 +476,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'wall-wallpaper3',
|
id: 'wall-wallpaper3',
|
||||||
label: 'Wallpaper',
|
label: 'Wallpaper',
|
||||||
|
category: 'wallpaper',
|
||||||
description: 'Patterned wallpaper finish',
|
description: 'Patterned wallpaper finish',
|
||||||
targets: WALL_TARGETS,
|
|
||||||
previewThumbnailUrl: '/material/wallpaper3/wallpaper3_thumbnail.webp',
|
previewThumbnailUrl: '/material/wallpaper3/wallpaper3_thumbnail.webp',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {
|
maps: {
|
||||||
@@ -498,14 +510,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'preset-white',
|
id: 'preset-white',
|
||||||
label: 'White',
|
label: 'White',
|
||||||
|
category: 'other',
|
||||||
description: 'Clean painted finish',
|
description: 'Clean painted finish',
|
||||||
targets: [
|
|
||||||
...WALL_TARGETS,
|
|
||||||
...SLAB_TARGETS,
|
|
||||||
...ROOF_TARGETS,
|
|
||||||
...STAIR_AND_FENCE_TARGETS,
|
|
||||||
...CEILING_TARGETS,
|
|
||||||
],
|
|
||||||
previewColor: '#ffffff',
|
previewColor: '#ffffff',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {},
|
maps: {},
|
||||||
@@ -536,8 +542,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'preset-metal',
|
id: 'preset-metal',
|
||||||
label: 'Metal',
|
label: 'Metal',
|
||||||
|
category: 'other',
|
||||||
description: 'Brushed metal finish',
|
description: 'Brushed metal finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS],
|
|
||||||
previewColor: '#c0c0c0',
|
previewColor: '#c0c0c0',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {},
|
maps: {},
|
||||||
@@ -568,8 +574,8 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
{
|
{
|
||||||
id: 'preset-glass',
|
id: 'preset-glass',
|
||||||
label: 'Glass',
|
label: 'Glass',
|
||||||
|
category: 'other',
|
||||||
description: 'Light glass finish',
|
description: 'Light glass finish',
|
||||||
targets: [...WALL_TARGETS, ...SLAB_TARGETS],
|
|
||||||
previewColor: '#87ceeb',
|
previewColor: '#87ceeb',
|
||||||
preset: {
|
preset: {
|
||||||
maps: {},
|
maps: {},
|
||||||
@@ -599,8 +605,12 @@ export const MATERIAL_CATALOG: MaterialCatalogItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function getMaterialsForTarget(target: MaterialTarget): MaterialCatalogItem[] {
|
export function getMaterialsForCategory(category: MaterialCategory): MaterialCatalogItem[] {
|
||||||
return MATERIAL_CATALOG.filter((item) => item.targets.includes(target))
|
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 {
|
export function getCatalogMaterialById(id?: string): MaterialCatalogItem | undefined {
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { getMaterialsForTarget, toLibraryMaterialRef, useScene } from '@pascal-app/core'
|
import { useScene } from '@pascal-app/core'
|
||||||
import { AnimatePresence, motion } from 'motion/react'
|
import { AnimatePresence, motion } from 'motion/react'
|
||||||
import { useEffect, useMemo } from 'react'
|
import { useEffect, useMemo } from 'react'
|
||||||
import { useViewer } from '@pascal-app/viewer'
|
import { useViewer } from '@pascal-app/viewer'
|
||||||
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
|
||||||
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
|
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
|
||||||
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
|
||||||
import {
|
import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
|
||||||
isActivePaintMaterialCompatible,
|
|
||||||
resolvePaintTargetFromSelection,
|
|
||||||
} from './../../../lib/material-paint'
|
|
||||||
import { cn } from './../../../lib/utils'
|
import { cn } from './../../../lib/utils'
|
||||||
import useEditor from './../../../store/use-editor'
|
import useEditor from './../../../store/use-editor'
|
||||||
import { ItemCatalog } from '../item-catalog/item-catalog'
|
import { ItemCatalog } from '../item-catalog/item-catalog'
|
||||||
@@ -40,45 +37,17 @@ function PaintMaterialTray() {
|
|||||||
}
|
}
|
||||||
}, [nodes, selectedId, setActivePaintTarget])
|
}, [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 (
|
return (
|
||||||
<div className="w-[42rem] max-w-[calc(100vw-2rem)]">
|
<div className="w-[42rem] max-w-[calc(100vw-2rem)]">
|
||||||
<MaterialPicker
|
<MaterialPicker
|
||||||
hideSideControl
|
onChange={(material, category) => {
|
||||||
nodeType={activePaintTarget}
|
setActivePaintMaterial({ material, category, sourceTarget: activePaintTarget })
|
||||||
onChange={(material) => {
|
|
||||||
setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
|
|
||||||
}}
|
}}
|
||||||
onSelectMaterialPreset={(materialPreset) => {
|
onSelectMaterialPreset={(materialPreset, category) => {
|
||||||
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
|
setActivePaintMaterial({ materialPreset, category, sourceTarget: activePaintTarget })
|
||||||
}}
|
}}
|
||||||
selectedMaterialPreset={
|
selectedMaterialPreset={activePaintMaterial?.materialPreset}
|
||||||
activePaintMaterial?.sourceTarget === activePaintTarget
|
value={activePaintMaterial?.material}
|
||||||
? activePaintMaterial.materialPreset
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
activePaintMaterial?.sourceTarget === activePaintTarget
|
|
||||||
? activePaintMaterial.material
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,55 +1,69 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMaterialsForTarget,
|
getCatalogMaterialById,
|
||||||
|
getLibraryMaterialIdFromRef,
|
||||||
|
getMaterialsForCategory,
|
||||||
|
MATERIAL_CATEGORIES,
|
||||||
toLibraryMaterialRef,
|
toLibraryMaterialRef,
|
||||||
|
type MaterialCategory,
|
||||||
type MaterialSchema,
|
type MaterialSchema,
|
||||||
type MaterialTarget,
|
|
||||||
} from '@pascal-app/core'
|
} from '@pascal-app/core'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import useEditor from '../../../store/use-editor'
|
||||||
|
|
||||||
type MaterialPickerProps = {
|
type MaterialPickerProps = {
|
||||||
nodeType?: MaterialTarget
|
|
||||||
value?: MaterialSchema
|
value?: MaterialSchema
|
||||||
selectedMaterialPreset?: string
|
selectedMaterialPreset?: string
|
||||||
onChange?: (material: MaterialSchema) => void
|
onChange?: (material: MaterialSchema, category: MaterialCategory) => void
|
||||||
onSelectMaterialPreset?: (materialPreset: string) => void
|
onSelectMaterialPreset?: (materialPreset: string, category: MaterialCategory) => void
|
||||||
hideSideControl?: boolean
|
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MaterialPicker({
|
export function MaterialPicker({
|
||||||
nodeType,
|
|
||||||
value,
|
value,
|
||||||
selectedMaterialPreset,
|
selectedMaterialPreset,
|
||||||
onChange,
|
onChange,
|
||||||
onSelectMaterialPreset,
|
onSelectMaterialPreset,
|
||||||
hideSideControl = false,
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
}: MaterialPickerProps) {
|
}: MaterialPickerProps) {
|
||||||
|
const setPaintPanelOpen = useEditor((state) => state.setPaintPanelOpen)
|
||||||
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
|
const [showCustom, setShowCustom] = useState<boolean>(!!value?.properties)
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<MaterialCategory>(MATERIAL_CATEGORIES[0])
|
||||||
const catalogScrollRef = useRef<HTMLDivElement>(null)
|
const catalogScrollRef = useRef<HTMLDivElement>(null)
|
||||||
const catalogItems = nodeType ? getMaterialsForTarget(nodeType) : []
|
const catalogItems =
|
||||||
|
selectedCategory === 'other'
|
||||||
|
? getMaterialsForCategory('other')
|
||||||
|
: getMaterialsForCategory(selectedCategory)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setShowCustom(!!value?.properties && !selectedMaterialPreset)
|
setShowCustom(!!value?.properties && !selectedMaterialPreset)
|
||||||
}, [selectedMaterialPreset, value?.properties])
|
}, [selectedMaterialPreset, value?.properties])
|
||||||
|
|
||||||
const currentProps = value?.properties || {
|
useEffect(() => {
|
||||||
color: '#ffffff',
|
if (!selectedMaterialPreset && value?.properties) {
|
||||||
roughness: 0.5,
|
setSelectedCategory('other')
|
||||||
metalness: 0,
|
return
|
||||||
opacity: 1,
|
}
|
||||||
transparent: false,
|
|
||||||
side: 'front' as const,
|
const catalogId =
|
||||||
}
|
getLibraryMaterialIdFromRef(selectedMaterialPreset) ?? value?.id ?? undefined
|
||||||
|
const selectedCatalogEntry = getCatalogMaterialById(catalogId)
|
||||||
|
if (selectedCatalogEntry?.category) {
|
||||||
|
setSelectedCategory(selectedCatalogEntry.category)
|
||||||
|
}
|
||||||
|
}, [selectedMaterialPreset, value?.id])
|
||||||
|
|
||||||
const selectedCatalogId =
|
const selectedCatalogId =
|
||||||
selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined)
|
selectedMaterialPreset ?? (value?.id ? toLibraryMaterialRef(value.id) : undefined)
|
||||||
|
|
||||||
const handleCatalogSelect = (materialId: string) => {
|
const handleCatalogSelect = (materialId: string) => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
setShowCustom(false)
|
setShowCustom(false)
|
||||||
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId))
|
setPaintPanelOpen(false)
|
||||||
|
const category = getCatalogMaterialById(materialId)?.category
|
||||||
|
if (!category) return
|
||||||
|
onSelectMaterialPreset?.(toLibraryMaterialRef(materialId), category)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -76,40 +90,51 @@ export function MaterialPicker({
|
|||||||
const handleCustomOpen = () => {
|
const handleCustomOpen = () => {
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
setShowCustom(true)
|
setShowCustom(true)
|
||||||
onChange?.({
|
setPaintPanelOpen(true)
|
||||||
preset: 'custom',
|
onChange?.(
|
||||||
properties: {
|
{
|
||||||
color: value?.properties?.color || '#ffffff',
|
preset: 'custom',
|
||||||
roughness: value?.properties?.roughness ?? 0.5,
|
properties: {
|
||||||
metalness: value?.properties?.metalness ?? 0,
|
color: value?.properties?.color || '#ffffff',
|
||||||
opacity: value?.properties?.opacity ?? 1,
|
roughness: value?.properties?.roughness ?? 0.5,
|
||||||
transparent: value?.properties?.transparent ?? false,
|
metalness: value?.properties?.metalness ?? 0,
|
||||||
side: value?.properties?.side ?? 'front',
|
opacity: value?.properties?.opacity ?? 1,
|
||||||
|
transparent: value?.properties?.transparent ?? false,
|
||||||
|
side: value?.properties?.side ?? 'front',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
'other',
|
||||||
}
|
)
|
||||||
|
|
||||||
const handlePropertyChange = (
|
|
||||||
prop: keyof typeof currentProps,
|
|
||||||
val: (typeof currentProps)[keyof typeof currentProps],
|
|
||||||
) => {
|
|
||||||
if (disabled) return
|
|
||||||
onChange?.({
|
|
||||||
preset: 'custom',
|
|
||||||
properties: {
|
|
||||||
...currentProps,
|
|
||||||
[prop]: val,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-w-0 space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
<div className={`min-w-0 space-y-3 ${disabled ? 'pointer-events-none opacity-50' : ''}`}>
|
||||||
{(catalogItems.length > 0 || onChange) && (
|
{(catalogItems.length > 0 || onChange) && (
|
||||||
<div className="min-w-0 space-y-2">
|
<div className="min-w-0 space-y-2">
|
||||||
{catalogItems.length > 0 ? (
|
<div className="flex flex-wrap gap-1">
|
||||||
<div className="text-gray-500 text-xs uppercase tracking-[0.16em]">Library</div>
|
{MATERIAL_CATEGORIES.map((category) => (
|
||||||
) : null}
|
<button
|
||||||
|
className={`px-2 font-medium text-[11px] uppercase tracking-[0.12em] transition-all ${
|
||||||
|
selectedCategory === category
|
||||||
|
? 'bg-transparent text-foreground'
|
||||||
|
: 'bg-transparent text-muted-foreground opacity-70 hover:text-foreground hover:opacity-100'
|
||||||
|
}`}
|
||||||
|
key={category}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCategory(category)
|
||||||
|
if (showCustom) {
|
||||||
|
setShowCustom(false)
|
||||||
|
}
|
||||||
|
if (category !== 'other') {
|
||||||
|
setPaintPanelOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{category.charAt(0).toUpperCase() + category.slice(1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="w-full max-w-full overflow-x-auto overflow-y-hidden"
|
className="w-full max-w-full overflow-x-auto overflow-y-hidden"
|
||||||
ref={catalogScrollRef}
|
ref={catalogScrollRef}
|
||||||
@@ -142,12 +167,12 @@ export function MaterialPicker({
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{onChange ? (
|
{selectedCategory === 'other' && onChange ? (
|
||||||
<button
|
<button
|
||||||
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border text-[10px] font-medium transition-all ${
|
className={`flex h-14 w-14 shrink-0 items-center justify-center rounded-lg border text-[10px] font-medium transition-all ${
|
||||||
showCustom
|
showCustom
|
||||||
? 'border-blue-500 bg-blue-50 text-blue-700 ring-2 ring-blue-500/30'
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
||||||
: 'border-gray-300 bg-white text-gray-500 hover:border-gray-400'
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
}`}
|
}`}
|
||||||
onClick={handleCustomOpen}
|
onClick={handleCustomOpen}
|
||||||
title="Custom"
|
title="Custom"
|
||||||
@@ -160,97 +185,6 @@ export function MaterialPicker({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showCustom && onChange && (
|
|
||||||
<div className="space-y-2 pt-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="w-16 text-gray-500 text-xs">Color</label>
|
|
||||||
<input
|
|
||||||
className="h-7 w-12 cursor-pointer rounded border border-gray-300"
|
|
||||||
onChange={(e) => handlePropertyChange('color', e.target.value)}
|
|
||||||
type="color"
|
|
||||||
value={currentProps.color}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className="h-7 flex-1 rounded border border-gray-300 px-2 text-xs"
|
|
||||||
onChange={(e) => handlePropertyChange('color', e.target.value)}
|
|
||||||
type="text"
|
|
||||||
value={currentProps.color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="w-16 text-gray-500 text-xs">Roughness</label>
|
|
||||||
<input
|
|
||||||
className="h-1.5 flex-1 cursor-pointer appearance-none rounded-lg bg-gray-200"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => handlePropertyChange('roughness', Number.parseFloat(e.target.value))}
|
|
||||||
step={0.01}
|
|
||||||
type="range"
|
|
||||||
value={currentProps.roughness}
|
|
||||||
/>
|
|
||||||
<span className="w-8 text-right text-gray-400 text-xs">
|
|
||||||
{currentProps.roughness.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="w-16 text-gray-500 text-xs">Metalness</label>
|
|
||||||
<input
|
|
||||||
className="h-1.5 flex-1 cursor-pointer appearance-none rounded-lg bg-gray-200"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => handlePropertyChange('metalness', Number.parseFloat(e.target.value))}
|
|
||||||
step={0.01}
|
|
||||||
type="range"
|
|
||||||
value={currentProps.metalness}
|
|
||||||
/>
|
|
||||||
<span className="w-8 text-right text-gray-400 text-xs">
|
|
||||||
{currentProps.metalness.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="w-16 text-gray-500 text-xs">Opacity</label>
|
|
||||||
<input
|
|
||||||
className="h-1.5 flex-1 cursor-pointer appearance-none rounded-lg bg-gray-200"
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
onChange={(e) => {
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<span className="w-8 text-right text-gray-400 text-xs">
|
|
||||||
{currentProps.opacity.toFixed(2)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!hideSideControl && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<label className="w-16 text-gray-500 text-xs">Side</label>
|
|
||||||
<select
|
|
||||||
className="h-7 flex-1 rounded border border-gray-300 px-2 text-xs"
|
|
||||||
onChange={(e) =>
|
|
||||||
handlePropertyChange('side', e.target.value as 'front' | 'back' | 'double')
|
|
||||||
}
|
|
||||||
value={currentProps.side}
|
|
||||||
>
|
|
||||||
<option value="front">Front</option>
|
|
||||||
<option value="back">Back</option>
|
|
||||||
<option value="double">Double</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<typeof currentProps>,
|
||||||
|
nextTransparent = currentProps.transparent,
|
||||||
|
) => {
|
||||||
|
setActivePaintMaterial({
|
||||||
|
material: {
|
||||||
|
preset: 'custom',
|
||||||
|
properties: {
|
||||||
|
...currentProps,
|
||||||
|
...updates,
|
||||||
|
transparent: nextTransparent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
category: 'other',
|
||||||
|
sourceTarget: activePaintMaterial?.sourceTarget ?? activePaintTarget,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PanelWrapper
|
||||||
|
onClose={() => setPaintPanelOpen(false)}
|
||||||
|
title="Material"
|
||||||
|
width={320}
|
||||||
|
>
|
||||||
|
<PanelSection title="Custom Material">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Color
|
||||||
|
</label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
className="h-10 w-14 cursor-pointer rounded-md border border-input bg-transparent"
|
||||||
|
onChange={(e) => updateCustomMaterial({ color: e.target.value })}
|
||||||
|
type="color"
|
||||||
|
value={currentProps.color}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
onChange={(e) => updateCustomMaterial({ color: e.target.value })}
|
||||||
|
value={currentProps.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Roughness
|
||||||
|
</label>
|
||||||
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
|
{currentProps.roughness.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => updateCustomMaterial({ roughness: Number.parseFloat(e.target.value) })}
|
||||||
|
step={0.01}
|
||||||
|
type="range"
|
||||||
|
value={currentProps.roughness}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Metalness
|
||||||
|
</label>
|
||||||
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
|
{currentProps.metalness.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => updateCustomMaterial({ metalness: Number.parseFloat(e.target.value) })}
|
||||||
|
step={0.01}
|
||||||
|
type="range"
|
||||||
|
value={currentProps.metalness}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Opacity
|
||||||
|
</label>
|
||||||
|
<span className="font-mono text-muted-foreground text-xs">
|
||||||
|
{currentProps.opacity.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="h-2 w-full cursor-pointer appearance-none rounded-full bg-accent"
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
onChange={(e) => {
|
||||||
|
const opacity = Number.parseFloat(e.target.value)
|
||||||
|
updateCustomMaterial({ opacity }, opacity < 1 || currentProps.transparent)
|
||||||
|
}}
|
||||||
|
step={0.01}
|
||||||
|
type="range"
|
||||||
|
value={currentProps.opacity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="block font-medium text-muted-foreground text-xs uppercase tracking-[0.12em]">
|
||||||
|
Side
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="h-9 w-full rounded-md border border-input bg-transparent px-3 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:bg-input/30"
|
||||||
|
onChange={(e) =>
|
||||||
|
updateCustomMaterial({ side: e.target.value as 'front' | 'back' | 'double' })
|
||||||
|
}
|
||||||
|
value={currentProps.side}
|
||||||
|
>
|
||||||
|
<option value="front">Front</option>
|
||||||
|
<option value="back">Back</option>
|
||||||
|
<option value="double">Double</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PanelSection>
|
||||||
|
</PanelWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { CeilingPanel } from './ceiling-panel'
|
|||||||
import { DoorPanel } from './door-panel'
|
import { DoorPanel } from './door-panel'
|
||||||
import { FencePanel } from './fence-panel'
|
import { FencePanel } from './fence-panel'
|
||||||
import { ItemPanel } from './item-panel'
|
import { ItemPanel } from './item-panel'
|
||||||
|
import { PaintPanel } from './paint-panel'
|
||||||
import { ReferencePanel } from './reference-panel'
|
import { ReferencePanel } from './reference-panel'
|
||||||
import { RoofPanel } from './roof-panel'
|
import { RoofPanel } from './roof-panel'
|
||||||
import { RoofSegmentPanel } from './roof-segment-panel'
|
import { RoofSegmentPanel } from './roof-segment-panel'
|
||||||
@@ -19,6 +20,9 @@ import { WindowPanel } from './window-panel'
|
|||||||
export function PanelManager() {
|
export function PanelManager() {
|
||||||
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
||||||
const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
|
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
|
// Only subscribe to the *type* of the single-selected node — string primitive
|
||||||
// so we don't re-render on unrelated scene mutations.
|
// so we don't re-render on unrelated scene mutations.
|
||||||
const selectedNodeType = useScene((s) => {
|
const selectedNodeType = useScene((s) => {
|
||||||
@@ -32,6 +36,15 @@ export function PanelManager() {
|
|||||||
return <ReferencePanel />
|
return <ReferencePanel />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isPaintPanelOpen &&
|
||||||
|
mode === 'material-paint' &&
|
||||||
|
activePaintMaterial?.material?.properties &&
|
||||||
|
!activePaintMaterial.materialPreset
|
||||||
|
) {
|
||||||
|
return <PaintPanel />
|
||||||
|
}
|
||||||
|
|
||||||
// Show appropriate panel based on selected node type
|
// Show appropriate panel based on selected node type
|
||||||
if (selectedNodeType) {
|
if (selectedNodeType) {
|
||||||
switch (selectedNodeType) {
|
switch (selectedNodeType) {
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
getEffectiveStairSurfaceMaterial,
|
getEffectiveStairSurfaceMaterial,
|
||||||
getEffectiveWallSurfaceMaterial,
|
getEffectiveWallSurfaceMaterial,
|
||||||
getLibraryMaterialIdFromRef,
|
getLibraryMaterialIdFromRef,
|
||||||
|
getMaterialCategoryTargets,
|
||||||
|
type MaterialCategory,
|
||||||
type MaterialSchema,
|
type MaterialSchema,
|
||||||
type MaterialTarget,
|
type MaterialTarget,
|
||||||
type RoofNode,
|
type RoofNode,
|
||||||
@@ -29,6 +31,7 @@ export type SingleSurfaceMaterialRole = 'surface'
|
|||||||
export type ActivePaintMaterial = {
|
export type ActivePaintMaterial = {
|
||||||
material?: MaterialSchema
|
material?: MaterialSchema
|
||||||
materialPreset?: string
|
materialPreset?: string
|
||||||
|
category?: MaterialCategory
|
||||||
sourceTarget: PaintableMaterialTarget
|
sourceTarget: PaintableMaterialTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +50,17 @@ function getCatalogEntryForActivePaintMaterial(material: ActivePaintMaterial | n
|
|||||||
return getCatalogMaterialById(catalogId)
|
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) {
|
export function getActivePaintMaterialLabel(material: ActivePaintMaterial | null | undefined) {
|
||||||
return getCatalogEntryForActivePaintMaterial(material)?.label ?? 'Custom'
|
return getCatalogEntryForActivePaintMaterial(material)?.label ?? 'Custom'
|
||||||
}
|
}
|
||||||
@@ -58,9 +72,10 @@ export function isActivePaintMaterialCompatible(
|
|||||||
if (!hasActivePaintMaterial(material)) return false
|
if (!hasActivePaintMaterial(material)) return false
|
||||||
|
|
||||||
const catalogEntry = getCatalogEntryForActivePaintMaterial(material)
|
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(
|
export function buildWallSurfaceMaterialPatch(
|
||||||
@@ -182,9 +197,10 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
sourceTarget: 'wall',
|
sourceTarget: 'wall',
|
||||||
})
|
})
|
||||||
? {
|
? {
|
||||||
material: surface.material,
|
material: surface.material,
|
||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
|
category: resolvePaintMaterialCategory(surface.material, surface.materialPreset),
|
||||||
sourceTarget: 'wall',
|
sourceTarget: 'wall',
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -202,9 +218,10 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
sourceTarget: 'roof',
|
sourceTarget: 'roof',
|
||||||
})
|
})
|
||||||
? {
|
? {
|
||||||
material: surface.material,
|
material: surface.material,
|
||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
|
category: resolvePaintMaterialCategory(surface.material, surface.materialPreset),
|
||||||
sourceTarget: 'roof',
|
sourceTarget: 'roof',
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -222,9 +239,10 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
sourceTarget: 'stair',
|
sourceTarget: 'stair',
|
||||||
})
|
})
|
||||||
? {
|
? {
|
||||||
material: surface.material,
|
material: surface.material,
|
||||||
materialPreset: surface.materialPreset,
|
materialPreset: surface.materialPreset,
|
||||||
|
category: resolvePaintMaterialCategory(surface.material, surface.materialPreset),
|
||||||
sourceTarget: 'stair',
|
sourceTarget: 'stair',
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
@@ -245,6 +263,10 @@ export function resolveActivePaintMaterialFromSelection(params: {
|
|||||||
? {
|
? {
|
||||||
material: selectedNode.material,
|
material: selectedNode.material,
|
||||||
materialPreset: selectedNode.materialPreset,
|
materialPreset: selectedNode.materialPreset,
|
||||||
|
category: resolvePaintMaterialCategory(
|
||||||
|
selectedNode.material,
|
||||||
|
selectedNode.materialPreset,
|
||||||
|
),
|
||||||
sourceTarget: target,
|
sourceTarget: target,
|
||||||
}
|
}
|
||||||
: null
|
: null
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ type EditorState = {
|
|||||||
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
||||||
hoveredPaintTarget: PaintableMaterialTarget | null
|
hoveredPaintTarget: PaintableMaterialTarget | null
|
||||||
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
setHoveredPaintTarget: (target: PaintableMaterialTarget | null) => void
|
||||||
|
isPaintPanelOpen: boolean
|
||||||
|
setPaintPanelOpen: (open: boolean) => void
|
||||||
selectedReferenceId: string | null
|
selectedReferenceId: string | null
|
||||||
setSelectedReferenceId: (id: string | null) => void
|
setSelectedReferenceId: (id: string | null) => void
|
||||||
// Space detection for cutaway mode
|
// Space detection for cutaway mode
|
||||||
@@ -591,6 +593,8 @@ const useEditor = create<EditorState>()(
|
|||||||
set((state) =>
|
set((state) =>
|
||||||
state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
|
state.hoveredPaintTarget === target ? state : { hoveredPaintTarget: target },
|
||||||
),
|
),
|
||||||
|
isPaintPanelOpen: false,
|
||||||
|
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
|
||||||
selectedReferenceId: null,
|
selectedReferenceId: null,
|
||||||
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
||||||
spaces: {},
|
spaces: {},
|
||||||
|
|||||||
Reference in New Issue
Block a user