@@ -4,8 +4,8 @@ import { useViewer } from '@pascal-app/viewer'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect } from 'react'
|
||||
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js'
|
||||
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter.js'
|
||||
|
||||
export function ExportManager() {
|
||||
const scene = useThree((state) => state.scene)
|
||||
|
||||
@@ -6,7 +6,12 @@ import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
||||
import { EDITOR_LAYER } from '../../../lib/constants'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import { CursorSphere } from '../shared/cursor-sphere'
|
||||
import { createWallOnCurrentLevel, snapWallDraftPoint, WALL_MIN_LENGTH, type WallPlanPoint } from './wall-drafting'
|
||||
import {
|
||||
createWallOnCurrentLevel,
|
||||
snapWallDraftPoint,
|
||||
WALL_MIN_LENGTH,
|
||||
type WallPlanPoint,
|
||||
} from './wall-drafting'
|
||||
|
||||
const WALL_HEIGHT = 2.5
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type CeilingNode, useScene } from '@pascal-app/core'
|
||||
import { type AnyNode, type CeilingNode, type MaterialSchema, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Edit, Plus, Trash2 } from 'lucide-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
import { PanelWrapper } from './panel-wrapper'
|
||||
@@ -94,6 +95,10 @@ export function CeilingPanel() {
|
||||
[selectedId, node?.holes, handleUpdate, editingHole, setEditingHole],
|
||||
)
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
if (!node || node.type !== 'ceiling' || selectedIds.length !== 1) return null
|
||||
|
||||
const calculateArea = (polygon: Array<[number, number]>): number => {
|
||||
@@ -217,6 +222,13 @@ export function CeilingPanel() {
|
||||
/>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
</PanelWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type AnyNodeId, DoorNode, emitter, useScene } from '@pascal-app/core'
|
||||
import { type AnyNode, type AnyNodeId, type MaterialSchema, DoorNode, emitter, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
@@ -8,6 +8,7 @@ import { usePresetsAdapter } from '../../../contexts/presets-context'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { MetricControl } from '../controls/metric-control'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SegmentedControl } from '../controls/segmented-control'
|
||||
@@ -562,6 +563,13 @@ export function DoorPanel() {
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={(material) => handleUpdate({ material })}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Actions">
|
||||
<ActionGroup>
|
||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type MaterialSchema,
|
||||
type RoofNode,
|
||||
RoofNode as RoofNodeSchema,
|
||||
type RoofSegmentNode,
|
||||
@@ -15,6 +16,7 @@ import { useCallback } from 'react'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { MetricControl } from '../controls/metric-control'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
@@ -122,6 +124,10 @@ export function RoofPanel() {
|
||||
setSelection({ selectedIds: [] })
|
||||
}, [selectedId, node, setSelection])
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
if (!node || node.type !== 'roof' || selectedIds.length !== 1) return null
|
||||
|
||||
const segments = (node.children ?? [])
|
||||
@@ -229,6 +235,13 @@ export function RoofPanel() {
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Actions">
|
||||
<ActionGroup>
|
||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type MaterialSchema,
|
||||
type RoofSegmentNode,
|
||||
RoofSegmentNode as RoofSegmentNodeSchema,
|
||||
type RoofType,
|
||||
@@ -14,6 +15,7 @@ import { useCallback } from 'react'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { MetricControl } from '../controls/metric-control'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SegmentedControl } from '../controls/segmented-control'
|
||||
@@ -108,6 +110,10 @@ export function RoofSegmentPanel() {
|
||||
}
|
||||
}, [selectedId, node, setSelection])
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
if (!node || node.type !== 'roof-segment' || selectedIds.length !== 1) return null
|
||||
|
||||
return (
|
||||
@@ -293,6 +299,13 @@ export function RoofSegmentPanel() {
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Actions">
|
||||
<ActionGroup>
|
||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type SlabNode, useScene } from '@pascal-app/core'
|
||||
import { type AnyNode, type MaterialSchema, type SlabNode, useScene } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { Edit, Plus, Trash2 } from 'lucide-react'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
import { PanelWrapper } from './panel-wrapper'
|
||||
@@ -29,6 +30,10 @@ export function SlabPanel() {
|
||||
[selectedId, updateNode],
|
||||
)
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSelection({ selectedIds: [] })
|
||||
setEditingHole(null)
|
||||
@@ -216,6 +221,13 @@ export function SlabPanel() {
|
||||
/>
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
</PanelWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type AnyNodeId, useScene, type WallNode } from '@pascal-app/core'
|
||||
import { type AnyNode, type AnyNodeId, type MaterialSchema, useScene, type WallNode } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { useCallback } from 'react'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
import { PanelWrapper } from './panel-wrapper'
|
||||
@@ -25,7 +26,6 @@ export function WallPanel() {
|
||||
[selectedId, updateNode],
|
||||
)
|
||||
|
||||
// Função mágica para a Issue #191: Atualiza o comprimento via cálculo vetorial
|
||||
const handleUpdateLength = useCallback((newLength: number) => {
|
||||
if (!node || newLength <= 0) return
|
||||
|
||||
@@ -35,11 +35,9 @@ export function WallPanel() {
|
||||
|
||||
if (currentLength === 0) return
|
||||
|
||||
// Calcula a direção (vetor unitário)
|
||||
const dirX = dx / currentLength
|
||||
const dirZ = dz / currentLength
|
||||
|
||||
// Define o novo ponto final baseado no novo comprimento
|
||||
const newEnd: [number, number] = [
|
||||
node.start[0] + dirX * newLength,
|
||||
node.start[1] + dirZ * newLength
|
||||
@@ -48,6 +46,10 @@ export function WallPanel() {
|
||||
handleUpdate({ end: newEnd })
|
||||
}, [node, handleUpdate])
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setSelection({ selectedIds: [] })
|
||||
}, [setSelection])
|
||||
@@ -69,7 +71,6 @@ export function WallPanel() {
|
||||
width={280}
|
||||
>
|
||||
<PanelSection title="Dimensions">
|
||||
{/* Adicionando o controle de Length solicitado na Issue #191 */}
|
||||
<SliderControl
|
||||
label="Length"
|
||||
max={20}
|
||||
@@ -101,6 +102,13 @@ export function WallPanel() {
|
||||
value={Math.round(thickness * 1000) / 1000}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
</PanelWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { type AnyNode, type AnyNodeId, emitter, useScene, WindowNode } from '@pascal-app/core'
|
||||
import { type AnyNode, type AnyNodeId, emitter, type MaterialSchema, useScene, WindowNode } from '@pascal-app/core'
|
||||
import { useViewer } from '@pascal-app/viewer'
|
||||
import { BookMarked, Copy, FlipHorizontal2, Move, Trash2 } from 'lucide-react'
|
||||
import { useCallback } from 'react'
|
||||
@@ -8,6 +8,7 @@ import { usePresetsAdapter } from '../../../contexts/presets-context'
|
||||
import { sfxEmitter } from '../../../lib/sfx-bus'
|
||||
import useEditor from '../../../store/use-editor'
|
||||
import { ActionButton, ActionGroup } from '../controls/action-button'
|
||||
import { MaterialPicker } from '../controls/material-picker'
|
||||
import { MetricControl } from '../controls/metric-control'
|
||||
import { PanelSection } from '../controls/panel-section'
|
||||
import { SliderControl } from '../controls/slider-control'
|
||||
@@ -138,6 +139,10 @@ export function WindowPanel() {
|
||||
[handleUpdate],
|
||||
)
|
||||
|
||||
const handleMaterialChange = useCallback((material: MaterialSchema) => {
|
||||
handleUpdate({ material })
|
||||
}, [handleUpdate])
|
||||
|
||||
if (!node || node.type !== 'window' || selectedIds.length !== 1) return null
|
||||
|
||||
const numCols = node.columnRatios.length
|
||||
@@ -402,6 +407,13 @@ export function WindowPanel() {
|
||||
)}
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Material">
|
||||
<MaterialPicker
|
||||
onChange={handleMaterialChange}
|
||||
value={node.material}
|
||||
/>
|
||||
</PanelSection>
|
||||
|
||||
<PanelSection title="Actions">
|
||||
<ActionGroup>
|
||||
<ActionButton icon={<Move className="h-3.5 w-3.5" />} label="Move" onClick={handleMove} />
|
||||
|
||||
Reference in New Issue
Block a user