@ifc-lite/viewer 1.1.7 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
- package/dist/assets/arrow2-bb-jcVEo.js +2 -0
- package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
- package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
- package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
- package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
- package/dist/assets/event-DIOks52T.js +1 -0
- package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-Dgd6vzw_.js +65252 -0
- package/dist/assets/index-v3mcCUPN.css +1 -0
- package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
- package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
- package/dist/favicon-16x16-cropped.png +0 -0
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-192x192-cropped.png +0 -0
- package/dist/favicon-192x192.png +0 -0
- package/dist/favicon-32x32-cropped.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon-48x48-cropped.png +0 -0
- package/dist/favicon-48x48.png +0 -0
- package/dist/favicon-512x512-cropped.png +0 -0
- package/dist/favicon-512x512.png +0 -0
- package/dist/favicon-64x64-cropped.png +0 -0
- package/dist/favicon-64x64.png +0 -0
- package/dist/favicon-96x96-cropped.png +0 -0
- package/dist/favicon-96x96.png +0 -0
- package/dist/favicon-square-512.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +3 -0
- package/dist/index.html +44 -0
- package/dist/logo.png +0 -0
- package/dist/manifest.json +48 -0
- package/index.html +33 -2
- package/package.json +34 -17
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16-cropped.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-192x192-cropped.png +0 -0
- package/public/favicon-192x192.png +0 -0
- package/public/favicon-32x32-cropped.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon-48x48-cropped.png +0 -0
- package/public/favicon-48x48.png +0 -0
- package/public/favicon-512x512-cropped.png +0 -0
- package/public/favicon-512x512.png +0 -0
- package/public/favicon-64x64-cropped.png +0 -0
- package/public/favicon-64x64.png +0 -0
- package/public/favicon-96x96-cropped.png +0 -0
- package/public/favicon-96x96.png +0 -0
- package/public/favicon-square-512.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +3 -0
- package/public/logo.png +0 -0
- package/public/manifest.json +48 -0
- package/src/App.tsx +2 -0
- package/src/components/ui/alert.tsx +62 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/dialog.tsx +120 -0
- package/src/components/ui/label.tsx +27 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +120 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/BCFPanel.tsx +1164 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
- package/src/components/viewer/DataConnector.tsx +840 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
- package/src/components/viewer/EntityContextMenu.tsx +45 -17
- package/src/components/viewer/ExportChangesButton.tsx +195 -0
- package/src/components/viewer/ExportDialog.tsx +402 -0
- package/src/components/viewer/HierarchyPanel.tsx +1132 -218
- package/src/components/viewer/IDSPanel.tsx +661 -0
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
- package/src/components/viewer/MainToolbar.tsx +418 -94
- package/src/components/viewer/PropertiesPanel.tsx +1355 -91
- package/src/components/viewer/PropertyEditor.tsx +611 -0
- package/src/components/viewer/Section2DPanel.tsx +3313 -0
- package/src/components/viewer/SheetSetupPanel.tsx +502 -0
- package/src/components/viewer/StatusBar.tsx +27 -16
- package/src/components/viewer/TitleBlockEditor.tsx +437 -0
- package/src/components/viewer/ToolOverlays.tsx +935 -127
- package/src/components/viewer/ViewerLayout.tsx +40 -11
- package/src/components/viewer/Viewport.tsx +1276 -336
- package/src/components/viewer/ViewportContainer.tsx +554 -18
- package/src/components/viewer/ViewportOverlays.tsx +24 -7
- package/src/hooks/useBCF.ts +504 -0
- package/src/hooks/useIDS.ts +1065 -0
- package/src/hooks/useIfc.ts +1534 -205
- package/src/hooks/useIfcCache.ts +279 -0
- package/src/hooks/useKeyboardShortcuts.ts +50 -8
- package/src/hooks/useModelSelection.ts +61 -0
- package/src/hooks/useViewerSelectors.ts +218 -0
- package/src/hooks/useWebGPU.ts +80 -0
- package/src/index.css +265 -27
- package/src/lib/platform.ts +23 -0
- package/src/services/cacheService.ts +142 -0
- package/src/services/desktop-cache.ts +143 -0
- package/src/services/fs-cache.ts +212 -0
- package/src/services/ifc-cache.ts +14 -6
- package/src/store/constants.ts +85 -0
- package/src/store/index.ts +214 -0
- package/src/store/slices/bcfSlice.ts +372 -0
- package/src/store/slices/cameraSlice.ts +63 -0
- package/src/store/slices/dataSlice.test.ts +226 -0
- package/src/store/slices/dataSlice.ts +112 -0
- package/src/store/slices/drawing2DSlice.ts +340 -0
- package/src/store/slices/hoverSlice.ts +40 -0
- package/src/store/slices/idsSlice.ts +310 -0
- package/src/store/slices/loadingSlice.ts +33 -0
- package/src/store/slices/measurementSlice.test.ts +217 -0
- package/src/store/slices/measurementSlice.ts +293 -0
- package/src/store/slices/modelSlice.test.ts +271 -0
- package/src/store/slices/modelSlice.ts +211 -0
- package/src/store/slices/mutationSlice.ts +502 -0
- package/src/store/slices/sectionSlice.test.ts +125 -0
- package/src/store/slices/sectionSlice.ts +58 -0
- package/src/store/slices/selectionSlice.test.ts +286 -0
- package/src/store/slices/selectionSlice.ts +263 -0
- package/src/store/slices/sheetSlice.ts +565 -0
- package/src/store/slices/uiSlice.ts +58 -0
- package/src/store/slices/visibilitySlice.test.ts +304 -0
- package/src/store/slices/visibilitySlice.ts +277 -0
- package/src/store/types.test.ts +135 -0
- package/src/store/types.ts +248 -0
- package/src/store.ts +40 -515
- package/src/utils/ifcConfig.ts +82 -0
- package/src/utils/localParsingUtils.ts +287 -0
- package/src/utils/serverDataModel.ts +783 -0
- package/src/utils/spatialHierarchy.ts +283 -0
- package/src/utils/viewportUtils.ts +334 -0
- package/src/vite-env.d.ts +23 -0
- package/src/webgpu-types.d.ts +128 -0
- package/src-tauri/Cargo.toml +29 -0
- package/src-tauri/build.rs +7 -0
- package/src-tauri/capabilities/default.json +18 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/Square107x107Logo.png +0 -0
- package/src-tauri/icons/Square142x142Logo.png +0 -0
- package/src-tauri/icons/Square150x150Logo.png +0 -0
- package/src-tauri/icons/Square284x284Logo.png +0 -0
- package/src-tauri/icons/Square30x30Logo.png +0 -0
- package/src-tauri/icons/Square310x310Logo.png +0 -0
- package/src-tauri/icons/Square44x44Logo.png +0 -0
- package/src-tauri/icons/Square71x71Logo.png +0 -0
- package/src-tauri/icons/Square89x89Logo.png +0 -0
- package/src-tauri/icons/StoreLogo.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/icons/icon.png +0 -0
- package/src-tauri/src/lib.rs +21 -0
- package/src-tauri/src/main.rs +10 -0
- package/src-tauri/tauri.conf.json +39 -0
- package/vite.config.ts +174 -26
- package/public/ifc-lite_bg.wasm +0 -0
- package/public/web-ifc.wasm +0 -0
- package/src/components/Viewport.tsx +0 -723
- package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DrawingSettingsPanel - Full control over 2D drawing graphic styles
|
|
7
|
+
*
|
|
8
|
+
* Provides:
|
|
9
|
+
* - Preset selection dropdown
|
|
10
|
+
* - Custom rule editor
|
|
11
|
+
* - Color, line weight, hatch controls
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useCallback, useState, useMemo } from 'react';
|
|
15
|
+
import { X, Palette, Plus, Trash2, ChevronDown, ChevronRight, GripVertical, Eye, EyeOff, Check, Copy, PenTool, Flame, Building2, Wrench, Printer, type LucideIcon } from 'lucide-react';
|
|
16
|
+
import { Button } from '@/components/ui/button';
|
|
17
|
+
import { Input } from '@/components/ui/input';
|
|
18
|
+
import { Label } from '@/components/ui/label';
|
|
19
|
+
import {
|
|
20
|
+
Select,
|
|
21
|
+
SelectContent,
|
|
22
|
+
SelectItem,
|
|
23
|
+
SelectTrigger,
|
|
24
|
+
SelectValue,
|
|
25
|
+
} from '@/components/ui/select';
|
|
26
|
+
import {
|
|
27
|
+
Collapsible,
|
|
28
|
+
CollapsibleContent,
|
|
29
|
+
CollapsibleTrigger,
|
|
30
|
+
} from '@/components/ui/collapsible';
|
|
31
|
+
import { useViewerStore } from '@/store';
|
|
32
|
+
import type { GraphicOverrideRule, GraphicStyle } from '@ifc-lite/drawing-2d';
|
|
33
|
+
|
|
34
|
+
// Common IFC types for the dropdown
|
|
35
|
+
const COMMON_IFC_TYPES = [
|
|
36
|
+
'IfcWall',
|
|
37
|
+
'IfcWallStandardCase',
|
|
38
|
+
'IfcSlab',
|
|
39
|
+
'IfcColumn',
|
|
40
|
+
'IfcBeam',
|
|
41
|
+
'IfcDoor',
|
|
42
|
+
'IfcWindow',
|
|
43
|
+
'IfcStair',
|
|
44
|
+
'IfcRoof',
|
|
45
|
+
'IfcRailing',
|
|
46
|
+
'IfcCovering',
|
|
47
|
+
'IfcFurnishingElement',
|
|
48
|
+
'IfcSpace',
|
|
49
|
+
'IfcBuildingElementProxy',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Line weight presets
|
|
53
|
+
const LINE_WEIGHTS = [
|
|
54
|
+
{ value: 'heavy', label: 'Heavy (0.5mm)' },
|
|
55
|
+
{ value: 'medium', label: 'Medium (0.35mm)' },
|
|
56
|
+
{ value: 'light', label: 'Light (0.25mm)' },
|
|
57
|
+
{ value: 'hairline', label: 'Hairline (0.18mm)' },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Icon mapping for presets
|
|
61
|
+
const PRESET_ICONS: Record<string, LucideIcon> = {
|
|
62
|
+
Palette,
|
|
63
|
+
PenTool,
|
|
64
|
+
Flame,
|
|
65
|
+
Building2,
|
|
66
|
+
Wrench,
|
|
67
|
+
Printer,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function PresetIcon({ iconName, className }: { iconName?: string; className?: string }) {
|
|
71
|
+
const Icon = iconName ? PRESET_ICONS[iconName] : Palette;
|
|
72
|
+
if (!Icon) return <Palette className={className} />;
|
|
73
|
+
return <Icon className={className} />;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface DrawingSettingsPanelProps {
|
|
77
|
+
onClose: () => void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function DrawingSettingsPanel({ onClose }: DrawingSettingsPanelProps) {
|
|
81
|
+
const graphicOverridePresets = useViewerStore((s) => s.graphicOverridePresets);
|
|
82
|
+
const activePresetId = useViewerStore((s) => s.activePresetId);
|
|
83
|
+
const setActivePreset = useViewerStore((s) => s.setActivePreset);
|
|
84
|
+
const customOverrideRules = useViewerStore((s) => s.customOverrideRules);
|
|
85
|
+
const addCustomRule = useViewerStore((s) => s.addCustomRule);
|
|
86
|
+
const updateCustomRule = useViewerStore((s) => s.updateCustomRule);
|
|
87
|
+
const removeCustomRule = useViewerStore((s) => s.removeCustomRule);
|
|
88
|
+
const overridesEnabled = useViewerStore((s) => s.overridesEnabled);
|
|
89
|
+
const toggleOverridesEnabled = useViewerStore((s) => s.toggleOverridesEnabled);
|
|
90
|
+
|
|
91
|
+
// Expanded sections
|
|
92
|
+
const [presetsOpen, setPresetsOpen] = useState(true);
|
|
93
|
+
const [customRulesOpen, setCustomRulesOpen] = useState(true);
|
|
94
|
+
const [editingRuleId, setEditingRuleId] = useState<string | null>(null);
|
|
95
|
+
|
|
96
|
+
// Get the active preset's rules for display
|
|
97
|
+
const activePreset = useMemo(() => {
|
|
98
|
+
if (!activePresetId) return null;
|
|
99
|
+
return graphicOverridePresets.find((p) => p.id === activePresetId) ?? null;
|
|
100
|
+
}, [activePresetId, graphicOverridePresets]);
|
|
101
|
+
|
|
102
|
+
// Add new custom rule
|
|
103
|
+
const handleAddRule = useCallback(() => {
|
|
104
|
+
const newRule: GraphicOverrideRule = {
|
|
105
|
+
id: `custom-${Date.now()}`,
|
|
106
|
+
name: 'New Rule',
|
|
107
|
+
enabled: true,
|
|
108
|
+
priority: customOverrideRules.length + 100, // Start after presets
|
|
109
|
+
criteria: {
|
|
110
|
+
type: 'ifcType',
|
|
111
|
+
ifcTypes: ['IfcWall'],
|
|
112
|
+
includeSubtypes: true,
|
|
113
|
+
},
|
|
114
|
+
style: {
|
|
115
|
+
fillColor: '#808080',
|
|
116
|
+
strokeColor: '#000000',
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
addCustomRule(newRule);
|
|
120
|
+
setEditingRuleId(newRule.id);
|
|
121
|
+
}, [customOverrideRules.length, addCustomRule]);
|
|
122
|
+
|
|
123
|
+
// Copy preset rules to custom rules for editing
|
|
124
|
+
const handleCopyPresetToCustom = useCallback(() => {
|
|
125
|
+
if (!activePreset) return;
|
|
126
|
+
|
|
127
|
+
// Copy each rule from preset to custom with new IDs
|
|
128
|
+
for (const rule of activePreset.rules) {
|
|
129
|
+
const newRule: GraphicOverrideRule = {
|
|
130
|
+
...rule,
|
|
131
|
+
id: `custom-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
132
|
+
priority: customOverrideRules.length + 100,
|
|
133
|
+
};
|
|
134
|
+
addCustomRule(newRule);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Clear preset selection and expand custom rules
|
|
138
|
+
setActivePreset(null);
|
|
139
|
+
setCustomRulesOpen(true);
|
|
140
|
+
}, [activePreset, customOverrideRules.length, addCustomRule, setActivePreset]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex flex-col h-full bg-background border-l">
|
|
144
|
+
{/* Header */}
|
|
145
|
+
<div className="flex items-center justify-between px-4 py-3 border-b bg-muted/50">
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<Palette className="h-5 w-5 text-primary" />
|
|
148
|
+
<h2 className="font-semibold text-sm">Drawing Settings</h2>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<Button
|
|
152
|
+
variant={overridesEnabled ? 'default' : 'outline'}
|
|
153
|
+
size="sm"
|
|
154
|
+
onClick={toggleOverridesEnabled}
|
|
155
|
+
className="h-7 text-xs"
|
|
156
|
+
>
|
|
157
|
+
{overridesEnabled ? 'Enabled' : 'Disabled'}
|
|
158
|
+
</Button>
|
|
159
|
+
<Button variant="ghost" size="icon-sm" onClick={onClose}>
|
|
160
|
+
<X className="h-4 w-4" />
|
|
161
|
+
</Button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Content */}
|
|
166
|
+
<div className="flex-1 overflow-y-auto">
|
|
167
|
+
{/* Presets Section */}
|
|
168
|
+
<Collapsible open={presetsOpen} onOpenChange={setPresetsOpen}>
|
|
169
|
+
<CollapsibleTrigger asChild>
|
|
170
|
+
<button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors">
|
|
171
|
+
<span className="text-sm font-medium">Style Presets</span>
|
|
172
|
+
{presetsOpen ? (
|
|
173
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
174
|
+
) : (
|
|
175
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
176
|
+
)}
|
|
177
|
+
</button>
|
|
178
|
+
</CollapsibleTrigger>
|
|
179
|
+
<CollapsibleContent>
|
|
180
|
+
<div className="px-4 pb-3 space-y-1">
|
|
181
|
+
{/* Built-in presets */}
|
|
182
|
+
{graphicOverridePresets.map((preset) => (
|
|
183
|
+
<button
|
|
184
|
+
key={preset.id}
|
|
185
|
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded transition-colors text-sm ${
|
|
186
|
+
activePresetId === preset.id
|
|
187
|
+
? 'bg-primary/10 text-primary'
|
|
188
|
+
: 'hover:bg-muted text-foreground'
|
|
189
|
+
}`}
|
|
190
|
+
onClick={() => setActivePreset(preset.id)}
|
|
191
|
+
>
|
|
192
|
+
<div className={`w-6 h-6 rounded flex items-center justify-center ${activePresetId === preset.id ? 'bg-primary/20' : 'bg-muted'}`}>
|
|
193
|
+
<PresetIcon iconName={preset.icon} className="h-3.5 w-3.5" />
|
|
194
|
+
</div>
|
|
195
|
+
<span className="flex-1 text-left font-medium">{preset.name}</span>
|
|
196
|
+
{activePresetId === preset.id && <Check className="h-3.5 w-3.5" />}
|
|
197
|
+
</button>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
</CollapsibleContent>
|
|
201
|
+
</Collapsible>
|
|
202
|
+
|
|
203
|
+
{/* Active Preset Rules (read-only with edit option) */}
|
|
204
|
+
{activePreset && activePreset.rules.length > 0 && (
|
|
205
|
+
<div className="border-t">
|
|
206
|
+
<div className="px-4 py-2 flex items-center justify-between">
|
|
207
|
+
<h3 className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
208
|
+
{activePreset.name} Rules
|
|
209
|
+
</h3>
|
|
210
|
+
<Button
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="sm"
|
|
213
|
+
className="h-6 text-xs"
|
|
214
|
+
onClick={handleCopyPresetToCustom}
|
|
215
|
+
title="Copy rules to custom for editing"
|
|
216
|
+
>
|
|
217
|
+
<Copy className="h-3 w-3 mr-1" />
|
|
218
|
+
Edit as Custom
|
|
219
|
+
</Button>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="px-4 pb-4 space-y-1">
|
|
222
|
+
{activePreset.rules.map((rule) => (
|
|
223
|
+
<PresetRuleItem key={rule.id} rule={rule} />
|
|
224
|
+
))}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Custom Rules Section */}
|
|
230
|
+
<Collapsible open={customRulesOpen} onOpenChange={setCustomRulesOpen}>
|
|
231
|
+
<div className="border-t">
|
|
232
|
+
<CollapsibleTrigger asChild>
|
|
233
|
+
<button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors">
|
|
234
|
+
<span className="text-sm font-medium">Custom Rules</span>
|
|
235
|
+
<div className="flex items-center gap-2">
|
|
236
|
+
<span className="text-xs text-muted-foreground">
|
|
237
|
+
{customOverrideRules.length} rules
|
|
238
|
+
</span>
|
|
239
|
+
{customRulesOpen ? (
|
|
240
|
+
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
241
|
+
) : (
|
|
242
|
+
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
</button>
|
|
246
|
+
</CollapsibleTrigger>
|
|
247
|
+
<CollapsibleContent>
|
|
248
|
+
<div className="px-4 pb-4 space-y-2">
|
|
249
|
+
{customOverrideRules.length === 0 ? (
|
|
250
|
+
<div className="text-center py-4 text-muted-foreground text-sm">
|
|
251
|
+
No custom rules yet
|
|
252
|
+
</div>
|
|
253
|
+
) : (
|
|
254
|
+
customOverrideRules.map((rule) => (
|
|
255
|
+
<CustomRuleItem
|
|
256
|
+
key={rule.id}
|
|
257
|
+
rule={rule}
|
|
258
|
+
isEditing={editingRuleId === rule.id}
|
|
259
|
+
onEdit={() => setEditingRuleId(rule.id)}
|
|
260
|
+
onSave={() => setEditingRuleId(null)}
|
|
261
|
+
onUpdate={(updates) => updateCustomRule(rule.id, updates)}
|
|
262
|
+
onRemove={() => removeCustomRule(rule.id)}
|
|
263
|
+
/>
|
|
264
|
+
))
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
<Button
|
|
268
|
+
variant="outline"
|
|
269
|
+
size="sm"
|
|
270
|
+
className="w-full"
|
|
271
|
+
onClick={handleAddRule}
|
|
272
|
+
>
|
|
273
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
274
|
+
Add Custom Rule
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
</CollapsibleContent>
|
|
278
|
+
</div>
|
|
279
|
+
</Collapsible>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Read-only preset rule display
|
|
286
|
+
function PresetRuleItem({ rule }: { rule: GraphicOverrideRule }) {
|
|
287
|
+
// Extract IFC types from criteria
|
|
288
|
+
const ifcTypes = useMemo(() => {
|
|
289
|
+
if ('ifcTypes' in rule.criteria && rule.criteria.ifcTypes) {
|
|
290
|
+
return rule.criteria.ifcTypes.join(', ');
|
|
291
|
+
}
|
|
292
|
+
if ('conditions' in rule.criteria) {
|
|
293
|
+
// Find ifcType criteria in conditions
|
|
294
|
+
for (const condition of rule.criteria.conditions) {
|
|
295
|
+
if ('ifcTypes' in condition && condition.ifcTypes) {
|
|
296
|
+
return condition.ifcTypes.join(', ');
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return 'All';
|
|
301
|
+
}, [rule.criteria]);
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className="flex items-center gap-2 px-2 py-1.5 bg-muted/30 rounded text-xs">
|
|
305
|
+
{rule.style.fillColor && (
|
|
306
|
+
<div
|
|
307
|
+
className="w-4 h-4 rounded border border-black/20"
|
|
308
|
+
style={{ backgroundColor: rule.style.fillColor }}
|
|
309
|
+
/>
|
|
310
|
+
)}
|
|
311
|
+
<div className="flex-1 min-w-0">
|
|
312
|
+
<div className="font-medium truncate">{rule.name}</div>
|
|
313
|
+
<div className="text-muted-foreground truncate">{ifcTypes}</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Editable custom rule
|
|
320
|
+
interface CustomRuleItemProps {
|
|
321
|
+
rule: GraphicOverrideRule;
|
|
322
|
+
isEditing: boolean;
|
|
323
|
+
onEdit: () => void;
|
|
324
|
+
onSave: () => void;
|
|
325
|
+
onUpdate: (updates: Partial<GraphicOverrideRule>) => void;
|
|
326
|
+
onRemove: () => void;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function CustomRuleItem({
|
|
330
|
+
rule,
|
|
331
|
+
isEditing,
|
|
332
|
+
onEdit,
|
|
333
|
+
onSave,
|
|
334
|
+
onUpdate,
|
|
335
|
+
onRemove,
|
|
336
|
+
}: CustomRuleItemProps) {
|
|
337
|
+
// Extract IFC types
|
|
338
|
+
const ifcTypes = useMemo(() => {
|
|
339
|
+
if ('ifcTypes' in rule.criteria && rule.criteria.ifcTypes) {
|
|
340
|
+
return rule.criteria.ifcTypes;
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
}, [rule.criteria]);
|
|
344
|
+
|
|
345
|
+
const handleIfcTypeChange = useCallback(
|
|
346
|
+
(type: string) => {
|
|
347
|
+
onUpdate({
|
|
348
|
+
criteria: {
|
|
349
|
+
type: 'ifcType',
|
|
350
|
+
ifcTypes: [type],
|
|
351
|
+
includeSubtypes: true,
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
[onUpdate]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const handleStyleChange = useCallback(
|
|
359
|
+
(key: keyof GraphicStyle, value: string | number | undefined) => {
|
|
360
|
+
onUpdate({
|
|
361
|
+
style: {
|
|
362
|
+
...rule.style,
|
|
363
|
+
[key]: value,
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
[rule.style, onUpdate]
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
if (!isEditing) {
|
|
371
|
+
return (
|
|
372
|
+
<div
|
|
373
|
+
className="flex items-center gap-2 px-2 py-1.5 bg-muted/30 rounded text-xs cursor-pointer hover:bg-muted/50"
|
|
374
|
+
onClick={onEdit}
|
|
375
|
+
>
|
|
376
|
+
<GripVertical className="h-3 w-3 text-muted-foreground" />
|
|
377
|
+
{rule.style.fillColor && (
|
|
378
|
+
<div
|
|
379
|
+
className="w-4 h-4 rounded border border-black/20"
|
|
380
|
+
style={{ backgroundColor: rule.style.fillColor }}
|
|
381
|
+
/>
|
|
382
|
+
)}
|
|
383
|
+
<div className="flex-1 min-w-0">
|
|
384
|
+
<div className="font-medium truncate">{rule.name}</div>
|
|
385
|
+
<div className="text-muted-foreground truncate">
|
|
386
|
+
{ifcTypes.join(', ') || 'Click to edit'}
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
<Button
|
|
390
|
+
variant="ghost"
|
|
391
|
+
size="icon-sm"
|
|
392
|
+
className="h-6 w-6 opacity-0 group-hover:opacity-100"
|
|
393
|
+
onClick={(e) => {
|
|
394
|
+
e.stopPropagation();
|
|
395
|
+
onUpdate({ enabled: !rule.enabled });
|
|
396
|
+
}}
|
|
397
|
+
>
|
|
398
|
+
{rule.enabled ? (
|
|
399
|
+
<Eye className="h-3 w-3" />
|
|
400
|
+
) : (
|
|
401
|
+
<EyeOff className="h-3 w-3 text-muted-foreground" />
|
|
402
|
+
)}
|
|
403
|
+
</Button>
|
|
404
|
+
</div>
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
<div className="p-3 bg-muted/30 rounded-lg border space-y-3">
|
|
410
|
+
{/* Name */}
|
|
411
|
+
<div>
|
|
412
|
+
<Label className="text-xs">Rule Name</Label>
|
|
413
|
+
<Input
|
|
414
|
+
value={rule.name}
|
|
415
|
+
onChange={(e) => onUpdate({ name: e.target.value })}
|
|
416
|
+
className="h-8 text-sm mt-1"
|
|
417
|
+
/>
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
{/* IFC Type */}
|
|
421
|
+
<div>
|
|
422
|
+
<Label className="text-xs">IFC Type</Label>
|
|
423
|
+
<Select
|
|
424
|
+
value={ifcTypes[0] || ''}
|
|
425
|
+
onValueChange={handleIfcTypeChange}
|
|
426
|
+
>
|
|
427
|
+
<SelectTrigger className="h-8 text-sm mt-1">
|
|
428
|
+
<SelectValue placeholder="Select type..." />
|
|
429
|
+
</SelectTrigger>
|
|
430
|
+
<SelectContent>
|
|
431
|
+
{COMMON_IFC_TYPES.map((type) => (
|
|
432
|
+
<SelectItem key={type} value={type}>
|
|
433
|
+
{type}
|
|
434
|
+
</SelectItem>
|
|
435
|
+
))}
|
|
436
|
+
</SelectContent>
|
|
437
|
+
</Select>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
{/* Colors */}
|
|
441
|
+
<div className="grid grid-cols-2 gap-2">
|
|
442
|
+
<div>
|
|
443
|
+
<Label className="text-xs">Fill Color</Label>
|
|
444
|
+
<div className="flex gap-1 mt-1">
|
|
445
|
+
<input
|
|
446
|
+
type="color"
|
|
447
|
+
value={rule.style.fillColor || '#808080'}
|
|
448
|
+
onChange={(e) => handleStyleChange('fillColor', e.target.value)}
|
|
449
|
+
className="w-8 h-8 rounded border cursor-pointer"
|
|
450
|
+
/>
|
|
451
|
+
<Input
|
|
452
|
+
value={rule.style.fillColor || '#808080'}
|
|
453
|
+
onChange={(e) => handleStyleChange('fillColor', e.target.value)}
|
|
454
|
+
className="h-8 text-xs font-mono flex-1"
|
|
455
|
+
/>
|
|
456
|
+
</div>
|
|
457
|
+
</div>
|
|
458
|
+
<div>
|
|
459
|
+
<Label className="text-xs">Stroke Color</Label>
|
|
460
|
+
<div className="flex gap-1 mt-1">
|
|
461
|
+
<input
|
|
462
|
+
type="color"
|
|
463
|
+
value={rule.style.strokeColor || '#000000'}
|
|
464
|
+
onChange={(e) => handleStyleChange('strokeColor', e.target.value)}
|
|
465
|
+
className="w-8 h-8 rounded border cursor-pointer"
|
|
466
|
+
/>
|
|
467
|
+
<Input
|
|
468
|
+
value={rule.style.strokeColor || '#000000'}
|
|
469
|
+
onChange={(e) => handleStyleChange('strokeColor', e.target.value)}
|
|
470
|
+
className="h-8 text-xs font-mono flex-1"
|
|
471
|
+
/>
|
|
472
|
+
</div>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
|
|
476
|
+
{/* Line Weight - preset or custom mm value */}
|
|
477
|
+
<div>
|
|
478
|
+
<Label className="text-xs">Line Weight</Label>
|
|
479
|
+
<div className="flex gap-2 mt-1">
|
|
480
|
+
<Select
|
|
481
|
+
value={typeof rule.style.lineWeight === 'string' ? rule.style.lineWeight : 'custom'}
|
|
482
|
+
onValueChange={(v) => {
|
|
483
|
+
if (v === 'custom') {
|
|
484
|
+
handleStyleChange('lineWeight', 0.35); // Default to 0.35mm
|
|
485
|
+
} else {
|
|
486
|
+
handleStyleChange('lineWeight', v);
|
|
487
|
+
}
|
|
488
|
+
}}
|
|
489
|
+
>
|
|
490
|
+
<SelectTrigger className="h-8 text-sm flex-1">
|
|
491
|
+
<SelectValue />
|
|
492
|
+
</SelectTrigger>
|
|
493
|
+
<SelectContent>
|
|
494
|
+
{LINE_WEIGHTS.map((w) => (
|
|
495
|
+
<SelectItem key={w.value} value={w.value}>
|
|
496
|
+
{w.label}
|
|
497
|
+
</SelectItem>
|
|
498
|
+
))}
|
|
499
|
+
<SelectItem value="custom">Custom...</SelectItem>
|
|
500
|
+
</SelectContent>
|
|
501
|
+
</Select>
|
|
502
|
+
{typeof rule.style.lineWeight === 'number' && (
|
|
503
|
+
<div className="flex items-center gap-1">
|
|
504
|
+
<Input
|
|
505
|
+
type="number"
|
|
506
|
+
min={0.05}
|
|
507
|
+
max={2}
|
|
508
|
+
step={0.05}
|
|
509
|
+
value={rule.style.lineWeight}
|
|
510
|
+
onChange={(e) => handleStyleChange('lineWeight', parseFloat(e.target.value) || 0.35)}
|
|
511
|
+
className="h-8 w-16 text-xs"
|
|
512
|
+
/>
|
|
513
|
+
<span className="text-xs text-muted-foreground">mm</span>
|
|
514
|
+
</div>
|
|
515
|
+
)}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
{/* Actions */}
|
|
520
|
+
<div className="flex items-center justify-between pt-2 border-t">
|
|
521
|
+
<Button
|
|
522
|
+
variant="ghost"
|
|
523
|
+
size="sm"
|
|
524
|
+
className="text-destructive hover:text-destructive"
|
|
525
|
+
onClick={onRemove}
|
|
526
|
+
>
|
|
527
|
+
<Trash2 className="h-4 w-4 mr-1" />
|
|
528
|
+
Delete
|
|
529
|
+
</Button>
|
|
530
|
+
<Button size="sm" onClick={onSave}>
|
|
531
|
+
Done
|
|
532
|
+
</Button>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
);
|
|
536
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Context menu for entity interactions
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useCallback, useEffect, useRef } from 'react';
|
|
9
|
+
import { useCallback, useEffect, useRef, useMemo } from 'react';
|
|
10
10
|
import {
|
|
11
11
|
Focus,
|
|
12
12
|
EyeOff,
|
|
@@ -28,8 +28,30 @@ export function EntityContextMenu() {
|
|
|
28
28
|
const setSelectedEntityId = useViewerStore((s) => s.setSelectedEntityId);
|
|
29
29
|
const setSelectedEntityIds = useViewerStore((s) => s.setSelectedEntityIds);
|
|
30
30
|
const cameraCallbacks = useViewerStore((s) => s.cameraCallbacks);
|
|
31
|
+
const resolveGlobalIdFromModels = useViewerStore((s) => s.resolveGlobalIdFromModels);
|
|
31
32
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
32
|
-
const { ifcDataStore } = useIfc();
|
|
33
|
+
const { ifcDataStore, models } = useIfc();
|
|
34
|
+
|
|
35
|
+
// Resolve contextMenu.entityId (globalId) to original expressId and model
|
|
36
|
+
// This is needed because IfcDataStore uses original expressIds, not globalIds
|
|
37
|
+
const { resolvedExpressId, activeDataStore } = useMemo(() => {
|
|
38
|
+
if (!contextMenu.entityId) {
|
|
39
|
+
return { resolvedExpressId: null, activeDataStore: ifcDataStore };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Use store-based resolver (more reliable than singleton)
|
|
43
|
+
const resolved = resolveGlobalIdFromModels(contextMenu.entityId);
|
|
44
|
+
if (resolved) {
|
|
45
|
+
const model = models.get(resolved.modelId);
|
|
46
|
+
return {
|
|
47
|
+
resolvedExpressId: resolved.expressId,
|
|
48
|
+
activeDataStore: model?.ifcDataStore ?? ifcDataStore,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fallback for single-model mode (offset = 0)
|
|
53
|
+
return { resolvedExpressId: contextMenu.entityId, activeDataStore: ifcDataStore };
|
|
54
|
+
}, [contextMenu.entityId, models, ifcDataStore, resolveGlobalIdFromModels]);
|
|
33
55
|
|
|
34
56
|
// Close menu when clicking outside
|
|
35
57
|
useEffect(() => {
|
|
@@ -87,24 +109,26 @@ export function EntityContextMenu() {
|
|
|
87
109
|
}, [showAll, closeContextMenu]);
|
|
88
110
|
|
|
89
111
|
const handleSelectSimilar = useCallback(() => {
|
|
90
|
-
|
|
112
|
+
// Use resolvedExpressId (original ID) for IfcDataStore lookups
|
|
113
|
+
if (!resolvedExpressId || !activeDataStore) {
|
|
91
114
|
closeContextMenu();
|
|
92
115
|
return;
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
// Get the type of the selected entity
|
|
96
|
-
const entity =
|
|
119
|
+
const entity = activeDataStore.entities;
|
|
97
120
|
let entityType: string | null = null;
|
|
98
121
|
|
|
99
122
|
for (let i = 0; i < entity.count; i++) {
|
|
100
|
-
if (entity.expressId[i] ===
|
|
101
|
-
entityType = entity.getTypeName(
|
|
123
|
+
if (entity.expressId[i] === resolvedExpressId) {
|
|
124
|
+
entityType = entity.getTypeName(resolvedExpressId);
|
|
102
125
|
break;
|
|
103
126
|
}
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
if (entityType) {
|
|
107
130
|
// Select all entities of the same type
|
|
131
|
+
// NOTE: These are original expressIds - for multi-model, should transform to globalIds
|
|
108
132
|
const sameTypeIds: number[] = [];
|
|
109
133
|
for (let i = 0; i < entity.count; i++) {
|
|
110
134
|
if (entity.getTypeName(entity.expressId[i]) === entityType) {
|
|
@@ -115,45 +139,49 @@ export function EntityContextMenu() {
|
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
closeContextMenu();
|
|
118
|
-
}, [
|
|
142
|
+
}, [resolvedExpressId, activeDataStore, setSelectedEntityIds, closeContextMenu]);
|
|
119
143
|
|
|
120
144
|
const handleSelectSameStorey = useCallback(() => {
|
|
121
|
-
|
|
145
|
+
// Use resolvedExpressId (original ID) for IfcDataStore lookups
|
|
146
|
+
if (!resolvedExpressId || !activeDataStore?.spatialHierarchy) {
|
|
122
147
|
closeContextMenu();
|
|
123
148
|
return;
|
|
124
149
|
}
|
|
125
150
|
|
|
126
|
-
const storeyId =
|
|
151
|
+
const storeyId = activeDataStore.spatialHierarchy.elementToStorey.get(resolvedExpressId);
|
|
127
152
|
if (storeyId) {
|
|
128
|
-
const storeyElements =
|
|
153
|
+
const storeyElements = activeDataStore.spatialHierarchy.byStorey.get(storeyId);
|
|
129
154
|
if (storeyElements) {
|
|
155
|
+
// NOTE: These are original expressIds - for multi-model, should transform to globalIds
|
|
130
156
|
setSelectedEntityIds(Array.from(storeyElements));
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
closeContextMenu();
|
|
135
|
-
}, [
|
|
161
|
+
}, [resolvedExpressId, activeDataStore, setSelectedEntityIds, closeContextMenu]);
|
|
136
162
|
|
|
137
163
|
const handleCopyId = useCallback(() => {
|
|
138
|
-
|
|
139
|
-
|
|
164
|
+
// Use resolvedExpressId (original ID) for IfcDataStore lookups
|
|
165
|
+
if (resolvedExpressId && activeDataStore) {
|
|
166
|
+
const globalId = activeDataStore.entities.getGlobalId(resolvedExpressId);
|
|
140
167
|
if (globalId) {
|
|
141
168
|
navigator.clipboard.writeText(globalId);
|
|
142
169
|
}
|
|
143
170
|
}
|
|
144
171
|
closeContextMenu();
|
|
145
|
-
}, [
|
|
172
|
+
}, [resolvedExpressId, activeDataStore, closeContextMenu]);
|
|
146
173
|
|
|
147
174
|
if (!contextMenu.isOpen) {
|
|
148
175
|
return null;
|
|
149
176
|
}
|
|
150
177
|
|
|
151
178
|
// Get entity info for display
|
|
179
|
+
// Use resolvedExpressId (original ID) for IfcDataStore lookups
|
|
152
180
|
let entityName = '';
|
|
153
181
|
let entityType = '';
|
|
154
|
-
if (
|
|
155
|
-
entityName =
|
|
156
|
-
entityType =
|
|
182
|
+
if (resolvedExpressId && activeDataStore) {
|
|
183
|
+
entityName = activeDataStore.entities.getName(resolvedExpressId) || '';
|
|
184
|
+
entityType = activeDataStore.entities.getTypeName(resolvedExpressId) || '';
|
|
157
185
|
}
|
|
158
186
|
|
|
159
187
|
return (
|