- Add MaterialSchema with 10 presets (white, brick, concrete, wood, glass, metal, plaster, tile, marble, custom) - Add material field to Wall, Slab, Door, Window, Ceiling, Roof, RoofSegment nodes - Create MaterialPicker UI component with preset selection and custom properties - Update all renderers to support material rendering with caching - Add Material section to all node panels (WallPanel, SlabPanel, DoorPanel, WindowPanel, CeilingPanel, RoofPanel, RoofSegmentPanel) - Update AGENTS.md with Material System documentation
179 lines
6.2 KiB
TypeScript
179 lines
6.2 KiB
TypeScript
'use client'
|
|
|
|
import { DEFAULT_MATERIALS, type MaterialPreset, type MaterialSchema } from '@pascal-app/core'
|
|
import { useState } from 'react'
|
|
|
|
const PRESET_COLORS: Record<MaterialPreset, string> = {
|
|
white: '#ffffff',
|
|
brick: '#8b4513',
|
|
concrete: '#808080',
|
|
wood: '#deb887',
|
|
glass: '#87ceeb',
|
|
metal: '#c0c0c0',
|
|
plaster: '#f5f5dc',
|
|
tile: '#d3d3d3',
|
|
marble: '#fafafa',
|
|
custom: '#ffffff',
|
|
}
|
|
|
|
const PRESET_LABELS: Record<MaterialPreset, string> = {
|
|
white: 'White',
|
|
brick: 'Brick',
|
|
concrete: 'Concrete',
|
|
wood: 'Wood',
|
|
glass: 'Glass',
|
|
metal: 'Metal',
|
|
plaster: 'Plaster',
|
|
tile: 'Tile',
|
|
marble: 'Marble',
|
|
custom: 'Custom',
|
|
}
|
|
|
|
type MaterialPickerProps = {
|
|
value?: MaterialSchema
|
|
onChange: (material: MaterialSchema) => void
|
|
}
|
|
|
|
export function MaterialPicker({ value, onChange }: MaterialPickerProps) {
|
|
const [showCustom, setShowCustom] = useState<boolean>(value?.preset === 'custom' || !!value?.properties)
|
|
|
|
const currentPreset = value?.preset || 'white'
|
|
const currentProps = value?.properties || DEFAULT_MATERIALS[currentPreset]
|
|
|
|
const handlePresetChange = (preset: MaterialPreset) => {
|
|
if (preset === 'custom') {
|
|
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',
|
|
},
|
|
})
|
|
} else {
|
|
setShowCustom(false)
|
|
onChange({ preset })
|
|
}
|
|
}
|
|
|
|
const handlePropertyChange = (prop: keyof typeof currentProps, val: typeof currentProps[keyof typeof currentProps]) => {
|
|
onChange({
|
|
preset: showCustom ? 'custom' : currentPreset,
|
|
properties: {
|
|
...currentProps,
|
|
[prop]: val,
|
|
},
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="grid grid-cols-5 gap-1.5">
|
|
{(Object.keys(PRESET_COLORS) as MaterialPreset[]).map((preset) => (
|
|
<button
|
|
className={`h-8 w-8 rounded border-2 transition-all ${
|
|
currentPreset === preset
|
|
? 'border-blue-500 ring-2 ring-blue-500/30'
|
|
: 'border-gray-300 hover:border-gray-400'
|
|
}`}
|
|
key={preset}
|
|
onClick={() => handlePresetChange(preset)}
|
|
style={{
|
|
backgroundColor: PRESET_COLORS[preset],
|
|
backgroundImage: preset === 'glass' ? 'linear-gradient(135deg, rgba(255,255,255,0.3) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.3) 50%, rgba(255,255,255,0.3) 75%, transparent 75%, transparent)' : undefined,
|
|
backgroundSize: preset === 'glass' ? '8px 8px' : undefined,
|
|
}}
|
|
title={PRESET_LABELS[preset]}
|
|
type="button"
|
|
/>
|
|
))}
|
|
</div>
|
|
|
|
{showCustom && (
|
|
<div className="space-y-2 pt-2">
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-xs text-gray-500 w-16">Color</label>
|
|
<input
|
|
className="h-7 w-12 rounded border border-gray-300 cursor-pointer"
|
|
onChange={(e) => handlePropertyChange('color', e.target.value)}
|
|
type="color"
|
|
value={currentProps.color}
|
|
/>
|
|
<input
|
|
className="flex-1 h-7 px-2 text-xs border border-gray-300 rounded"
|
|
onChange={(e) => handlePropertyChange('color', e.target.value)}
|
|
type="text"
|
|
value={currentProps.color}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-xs text-gray-500 w-16">Roughness</label>
|
|
<input
|
|
className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
max={1}
|
|
min={0}
|
|
onChange={(e) => handlePropertyChange('roughness', parseFloat(e.target.value))}
|
|
step={0.01}
|
|
type="range"
|
|
value={currentProps.roughness}
|
|
/>
|
|
<span className="text-xs text-gray-400 w-8 text-right">{currentProps.roughness.toFixed(2)}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-xs text-gray-500 w-16">Metalness</label>
|
|
<input
|
|
className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
max={1}
|
|
min={0}
|
|
onChange={(e) => handlePropertyChange('metalness', parseFloat(e.target.value))}
|
|
step={0.01}
|
|
type="range"
|
|
value={currentProps.metalness}
|
|
/>
|
|
<span className="text-xs text-gray-400 w-8 text-right">{currentProps.metalness.toFixed(2)}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-xs text-gray-500 w-16">Opacity</label>
|
|
<input
|
|
className="flex-1 h-1.5 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
|
max={1}
|
|
min={0}
|
|
onChange={(e) => {
|
|
const opacity = 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="text-xs text-gray-400 w-8 text-right">{currentProps.opacity.toFixed(2)}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<label className="text-xs text-gray-500 w-16">Side</label>
|
|
<select
|
|
className="flex-1 h-7 px-2 text-xs border border-gray-300 rounded"
|
|
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>
|
|
)
|
|
}
|