/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * DrawingSettingsPanel - Full control over 2D drawing graphic styles * * Provides: * - Preset selection dropdown * - Custom rule editor * - Color, line weight, hatch controls */ import React, { useCallback, useState, useMemo } from 'react'; import { X, Palette, Plus, Trash2, ChevronDown, ChevronRight, GripVertical, Eye, EyeOff, Check, Copy, PenTool, Flame, Building2, Wrench, Printer, type LucideIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; import { useViewerStore } from '@/store'; import type { GraphicOverrideRule, GraphicStyle } from '@ifc-lite/drawing-2d'; // Common IFC types for the dropdown const COMMON_IFC_TYPES = [ 'IfcWall', 'IfcWallStandardCase', 'IfcSlab', 'IfcColumn', 'IfcBeam', 'IfcDoor', 'IfcWindow', 'IfcStair', 'IfcRoof', 'IfcRailing', 'IfcCovering', 'IfcFurnishingElement', 'IfcSpace', 'IfcBuildingElementProxy', ]; // Line weight presets const LINE_WEIGHTS = [ { value: 'heavy', label: 'Heavy (0.5mm)' }, { value: 'medium', label: 'Medium (0.35mm)' }, { value: 'light', label: 'Light (0.25mm)' }, { value: 'hairline', label: 'Hairline (0.18mm)' }, ]; // Icon mapping for presets const PRESET_ICONS: Record = { Palette, PenTool, Flame, Building2, Wrench, Printer, }; function PresetIcon({ iconName, className }: { iconName?: string; className?: string }) { const Icon = iconName ? PRESET_ICONS[iconName] : Palette; if (!Icon) return ; return ; } interface DrawingSettingsPanelProps { onClose: () => void; } export function DrawingSettingsPanel({ onClose }: DrawingSettingsPanelProps) { const graphicOverridePresets = useViewerStore((s) => s.graphicOverridePresets); const activePresetId = useViewerStore((s) => s.activePresetId); const setActivePreset = useViewerStore((s) => s.setActivePreset); const customOverrideRules = useViewerStore((s) => s.customOverrideRules); const addCustomRule = useViewerStore((s) => s.addCustomRule); const updateCustomRule = useViewerStore((s) => s.updateCustomRule); const removeCustomRule = useViewerStore((s) => s.removeCustomRule); const overridesEnabled = useViewerStore((s) => s.overridesEnabled); const toggleOverridesEnabled = useViewerStore((s) => s.toggleOverridesEnabled); // Expanded sections const [presetsOpen, setPresetsOpen] = useState(true); const [customRulesOpen, setCustomRulesOpen] = useState(true); const [editingRuleId, setEditingRuleId] = useState(null); // Get the active preset's rules for display const activePreset = useMemo(() => { if (!activePresetId) return null; return graphicOverridePresets.find((p) => p.id === activePresetId) ?? null; }, [activePresetId, graphicOverridePresets]); // Add new custom rule const handleAddRule = useCallback(() => { const newRule: GraphicOverrideRule = { id: `custom-${Date.now()}`, name: 'New Rule', enabled: true, priority: customOverrideRules.length + 100, // Start after presets criteria: { type: 'ifcType', ifcTypes: ['IfcWall'], includeSubtypes: true, }, style: { fillColor: '#808080', strokeColor: '#000000', }, }; addCustomRule(newRule); setEditingRuleId(newRule.id); }, [customOverrideRules.length, addCustomRule]); // Copy preset rules to custom rules for editing const handleCopyPresetToCustom = useCallback(() => { if (!activePreset) return; // Copy each rule from preset to custom with new IDs for (const rule of activePreset.rules) { const newRule: GraphicOverrideRule = { ...rule, id: `custom-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, priority: customOverrideRules.length + 100, }; addCustomRule(newRule); } // Clear preset selection and expand custom rules setActivePreset(null); setCustomRulesOpen(true); }, [activePreset, customOverrideRules.length, addCustomRule, setActivePreset]); return (
{/* Header */}

Drawing Settings

{/* Content */}
{/* Presets Section */}
{/* Built-in presets */} {graphicOverridePresets.map((preset) => ( ))}
{/* Active Preset Rules (read-only with edit option) */} {activePreset && activePreset.rules.length > 0 && (

{activePreset.name} Rules

{activePreset.rules.map((rule) => ( ))}
)} {/* Custom Rules Section */}
{customOverrideRules.length === 0 ? (
No custom rules yet
) : ( customOverrideRules.map((rule) => ( setEditingRuleId(rule.id)} onSave={() => setEditingRuleId(null)} onUpdate={(updates) => updateCustomRule(rule.id, updates)} onRemove={() => removeCustomRule(rule.id)} /> )) )}
); } // Read-only preset rule display function PresetRuleItem({ rule }: { rule: GraphicOverrideRule }) { // Extract IFC types from criteria const ifcTypes = useMemo(() => { if ('ifcTypes' in rule.criteria && rule.criteria.ifcTypes) { return rule.criteria.ifcTypes.join(', '); } if ('conditions' in rule.criteria) { // Find ifcType criteria in conditions for (const condition of rule.criteria.conditions) { if ('ifcTypes' in condition && condition.ifcTypes) { return condition.ifcTypes.join(', '); } } } return 'All'; }, [rule.criteria]); return (
{rule.style.fillColor && (
)}
{rule.name}
{ifcTypes}
); } // Editable custom rule interface CustomRuleItemProps { rule: GraphicOverrideRule; isEditing: boolean; onEdit: () => void; onSave: () => void; onUpdate: (updates: Partial) => void; onRemove: () => void; } function CustomRuleItem({ rule, isEditing, onEdit, onSave, onUpdate, onRemove, }: CustomRuleItemProps) { // Extract IFC types const ifcTypes = useMemo(() => { if ('ifcTypes' in rule.criteria && rule.criteria.ifcTypes) { return rule.criteria.ifcTypes; } return []; }, [rule.criteria]); const handleIfcTypeChange = useCallback( (type: string) => { onUpdate({ criteria: { type: 'ifcType', ifcTypes: [type], includeSubtypes: true, }, }); }, [onUpdate] ); const handleStyleChange = useCallback( (key: keyof GraphicStyle, value: string | number | undefined) => { onUpdate({ style: { ...rule.style, [key]: value, }, }); }, [rule.style, onUpdate] ); if (!isEditing) { return (
{rule.style.fillColor && (
)}
{rule.name}
{ifcTypes.join(', ') || 'Click to edit'}
); } return (
{/* Name */}
onUpdate({ name: e.target.value })} className="h-8 text-sm mt-1" />
{/* IFC Class */}
{/* Colors */}
handleStyleChange('fillColor', e.target.value)} className="w-8 h-8 rounded border cursor-pointer" /> handleStyleChange('fillColor', e.target.value)} className="h-8 text-xs font-mono flex-1" />
handleStyleChange('strokeColor', e.target.value)} className="w-8 h-8 rounded border cursor-pointer" /> handleStyleChange('strokeColor', e.target.value)} className="h-8 text-xs font-mono flex-1" />
{/* Line Weight - preset or custom mm value */}
{typeof rule.style.lineWeight === 'number' && (
handleStyleChange('lineWeight', parseFloat(e.target.value) || 0.35)} className="h-8 w-16 text-xs" /> mm
)}
{/* Actions */}
); }