@ifc-lite/viewer 1.14.3 → 1.14.4
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/CHANGELOG.md +11 -0
- package/dist/assets/{Arrow.dom-BgkZDIQm.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/{basketViewActivator-h_M3YbMW.js → basketViewActivator-BZcoCL3V.js} +1 -1
- package/dist/assets/{browser-CRQ0bPh1.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/{index-C4VVJRL-.js → index-D7nEDctQ.js} +4 -4
- package/dist/assets/{index-Be6XjVeM.js → index-DX-Qf5fA.js} +17153 -16920
- package/dist/assets/{native-bridge-DtcJqlOi.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-BJJVu9P2.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/components/viewer/CommandPalette.tsx +1 -0
- package/src/components/viewer/HierarchyPanel.tsx +28 -13
- package/src/components/viewer/MainToolbar.tsx +113 -95
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +10 -3
- package/src/components/viewer/hierarchy/treeDataBuilder.test.ts +126 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +139 -38
- package/src/components/viewer/hierarchy/types.ts +6 -1
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +27 -12
- package/dist/assets/index-DdwD4c-E.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{I as f,a as m}from"./index-
|
|
1
|
+
import{I as f,a as m}from"./index-DX-Qf5fA.js";class u{bridge;initialized=!1;constructor(){this.bridge=new f}async init(){this.initialized||(await this.bridge.init(),this.initialized=!0)}isInitialized(){return this.initialized}async processGeometry(s){this.initialized||await this.init(),performance.now();const i=new m(this.bridge.getApi(),s),n=i.collectMeshes(),r=i.getBuildingRotation();performance.now();let e=0,o=0;for(const c of n)e+=c.positions.length/3,o+=c.indices.length/3;return{meshes:n,totalVertices:e,totalTriangles:o,coordinateInfo:{originShift:{x:0,y:0,z:0},originalBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},shiftedBounds:{min:{x:0,y:0,z:0},max:{x:0,y:0,z:0}},hasLargeCoordinates:!1,buildingRotation:r}}}async processGeometryStreaming(s,i){this.initialized||await this.init();const n=performance.now(),r=new m(this.bridge.getApi(),s);let e=0,o=0,a=0;try{for await(const t of r.collectMeshesStreaming(50)){if(t&&typeof t=="object"&&"type"in t&&t.type==="colorUpdate")continue;const l=t;e+=l.length;for(const d of l)o+=d.positions.length/3,a+=d.indices.length/3;i.onBatch?.({meshes:l,progress:{processed:e,total:e,currentType:"processing"}})}}catch(t){throw i.onError?.(t instanceof Error?t:new Error(String(t))),t}const h=performance.now()-n,g={totalMeshes:e,totalVertices:o,totalTriangles:a,parseTimeMs:h*.3,geometryTimeMs:h*.7};return i.onComplete?.(g),g}getApi(){return this.bridge.getApi()}}export{u as WasmBridge};
|
package/dist/index.html
CHANGED
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
<meta name="theme-color" content="#7aa2f7">
|
|
45
45
|
<meta name="msapplication-TileColor" content="#1a1b26">
|
|
46
46
|
<meta name="msapplication-TileImage" content="/favicon-192x192-cropped.png">
|
|
47
|
-
<script type="module" crossorigin src="/assets/index-
|
|
48
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
47
|
+
<script type="module" crossorigin src="/assets/index-DX-Qf5fA.js"></script>
|
|
48
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CMQ_Dgkr.css">
|
|
49
49
|
</head>
|
|
50
50
|
<body>
|
|
51
51
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ifc-lite/viewer",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.4",
|
|
4
4
|
"description": "IFC-Lite viewer application",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"dependencies": {
|
|
@@ -47,18 +47,18 @@
|
|
|
47
47
|
"@ifc-lite/data": "^1.14.3",
|
|
48
48
|
"@ifc-lite/drawing-2d": "^1.14.3",
|
|
49
49
|
"@ifc-lite/encoding": "^1.14.3",
|
|
50
|
-
"@ifc-lite/export": "^1.14.
|
|
50
|
+
"@ifc-lite/export": "^1.14.4",
|
|
51
51
|
"@ifc-lite/geometry": "^1.14.3",
|
|
52
52
|
"@ifc-lite/ids": "^1.14.3",
|
|
53
|
-
"@ifc-lite/lens": "^1.14.3",
|
|
54
53
|
"@ifc-lite/lists": "^1.14.3",
|
|
54
|
+
"@ifc-lite/lens": "^1.14.3",
|
|
55
55
|
"@ifc-lite/mutations": "^1.14.3",
|
|
56
|
-
"@ifc-lite/parser": "^
|
|
57
|
-
"@ifc-lite/query": "^1.14.
|
|
56
|
+
"@ifc-lite/parser": "^2.0.0",
|
|
57
|
+
"@ifc-lite/query": "^1.14.4",
|
|
58
58
|
"@ifc-lite/renderer": "^1.14.3",
|
|
59
|
-
"@ifc-lite/sandbox": "^1.14.3",
|
|
60
59
|
"@ifc-lite/server-client": "^1.14.3",
|
|
61
60
|
"@ifc-lite/spatial": "^1.14.3",
|
|
61
|
+
"@ifc-lite/sandbox": "^1.14.3",
|
|
62
62
|
"@ifc-lite/wasm": "^1.14.3"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
@@ -218,6 +218,7 @@ function activateBottomPanel(panel: 'script' | 'list') {
|
|
|
218
218
|
s.setListPanelVisible(false);
|
|
219
219
|
|
|
220
220
|
if (!isActive) {
|
|
221
|
+
s.setRightPanelCollapsed(false);
|
|
221
222
|
if (panel === 'script') s.setScriptPanelVisible(true);
|
|
222
223
|
else s.setListPanelVisible(true);
|
|
223
224
|
}
|
|
@@ -319,27 +319,42 @@ export function HierarchyPanel() {
|
|
|
319
319
|
setStoreysSelection(storeyIds);
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
|
+
} else if (node.type === 'IfcSpace') {
|
|
323
|
+
const spaceId = node.expressIds[0];
|
|
324
|
+
const modelId = node.modelIds[0];
|
|
325
|
+
const globalId = node.globalIds[0] ?? spaceId;
|
|
326
|
+
|
|
327
|
+
setSelectedEntityIds([]);
|
|
328
|
+
|
|
329
|
+
if (modelId && modelId !== 'legacy') {
|
|
330
|
+
setSelectedEntityId(globalId);
|
|
331
|
+
setSelectedEntity({ modelId, expressId: spaceId });
|
|
332
|
+
setActiveModel(modelId);
|
|
333
|
+
} else {
|
|
334
|
+
setSelectedEntityId(globalId);
|
|
335
|
+
setSelectedEntity({ modelId: 'legacy', expressId: spaceId });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (node.hasChildren) {
|
|
339
|
+
toggleExpand(node.id);
|
|
340
|
+
}
|
|
322
341
|
} else if (node.type === 'element') {
|
|
323
342
|
// Element click - select it
|
|
324
|
-
const elementId = node.expressIds[0];
|
|
343
|
+
const elementId = node.expressIds[0];
|
|
325
344
|
const modelId = node.modelIds[0];
|
|
345
|
+
const globalId = node.globalIds[0] ?? elementId;
|
|
326
346
|
|
|
327
347
|
// Clear multi-selection (e.g. from a prior type-group click) so only
|
|
328
348
|
// this single element is highlighted, matching Viewport pick behavior
|
|
329
349
|
setSelectedEntityIds([]);
|
|
330
350
|
|
|
331
351
|
if (modelId !== 'legacy') {
|
|
332
|
-
// Multi-model: need to convert to globalId for renderer
|
|
333
|
-
const model = models.get(modelId);
|
|
334
|
-
const globalId = elementId + (model?.idOffset ?? 0);
|
|
335
352
|
setSelectedEntityId(globalId);
|
|
336
353
|
setSelectedEntity({ modelId, expressId: elementId });
|
|
337
354
|
setActiveModel(modelId);
|
|
338
355
|
} else {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// Also set selectedEntity for property panel (was missing, causing blank panel)
|
|
342
|
-
setSelectedEntity(resolveEntityRef(elementId));
|
|
356
|
+
setSelectedEntityId(globalId);
|
|
357
|
+
setSelectedEntity(resolveEntityRef(globalId));
|
|
343
358
|
}
|
|
344
359
|
}
|
|
345
360
|
}, [selectedStoreys, setStoreysSelection, clearStoreySelection, setSelectedEntityId, setSelectedEntityIds, setSelectedEntity, setSelectedEntities, setActiveModel, toggleExpand, unifiedStoreys, models, isolateEntities, getNodeElements, setHierarchyBasketSelection, toGlobalId]);
|
|
@@ -352,8 +367,8 @@ export function HierarchyPanel() {
|
|
|
352
367
|
? node.expressIds.some(id => selectedStoreys.has(id))
|
|
353
368
|
: node.type === 'IfcBuildingStorey'
|
|
354
369
|
? selectedStoreys.has(node.expressIds[0])
|
|
355
|
-
: node.type === 'element'
|
|
356
|
-
? selectedEntityId === node.expressIds[0]
|
|
370
|
+
: node.type === 'IfcSpace' || node.type === 'element'
|
|
371
|
+
? selectedEntityId === (node.globalIds[0] ?? node.expressIds[0])
|
|
357
372
|
: node.type === 'ifc-type'
|
|
358
373
|
? (() => {
|
|
359
374
|
const typeExpressId = node.entityExpressId;
|
|
@@ -369,8 +384,8 @@ export function HierarchyPanel() {
|
|
|
369
384
|
// Compute visibility inline - for elements check directly, for storeys use getNodeElements
|
|
370
385
|
let nodeHidden = false;
|
|
371
386
|
if (node.type === 'element') {
|
|
372
|
-
nodeHidden = hiddenEntities.has(node.expressIds[0]);
|
|
373
|
-
} else if (node.type === 'IfcBuildingStorey' || node.type === 'unified-storey' ||
|
|
387
|
+
nodeHidden = hiddenEntities.has(node.globalIds[0] ?? node.expressIds[0]);
|
|
388
|
+
} else if (node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' ||
|
|
374
389
|
node.type === 'type-group' || node.type === 'ifc-type' ||
|
|
375
390
|
(node.type === 'model-header' && node.id.startsWith('contrib-'))) {
|
|
376
391
|
const elements = getNodeElements(node);
|
|
@@ -385,7 +400,7 @@ export function HierarchyPanel() {
|
|
|
385
400
|
}
|
|
386
401
|
|
|
387
402
|
return { isSelected, nodeHidden, modelVisible };
|
|
388
|
-
}, [selectedStoreys, selectedEntityId, hiddenEntities, getNodeElements, models]);
|
|
403
|
+
}, [selectedStoreys, selectedEntityId, hiddenEntities, getNodeElements, models, toGlobalId]);
|
|
389
404
|
|
|
390
405
|
if (!ifcDataStore && models.size === 0) {
|
|
391
406
|
return (
|
|
@@ -36,7 +36,9 @@ import {
|
|
|
36
36
|
ClipboardCheck,
|
|
37
37
|
Palette,
|
|
38
38
|
Orbit,
|
|
39
|
+
Layout,
|
|
39
40
|
LayoutTemplate,
|
|
41
|
+
FileCode2,
|
|
40
42
|
} from 'lucide-react';
|
|
41
43
|
import { Button } from '@/components/ui/button';
|
|
42
44
|
import { Separator } from '@/components/ui/separator';
|
|
@@ -70,6 +72,7 @@ import { ThemeSwitch } from './ThemeSwitch';
|
|
|
70
72
|
import { toast } from '@/components/ui/toast';
|
|
71
73
|
|
|
72
74
|
type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
|
|
75
|
+
type WorkspacePanel = 'script' | 'list' | 'bcf' | 'ids' | 'lens';
|
|
73
76
|
|
|
74
77
|
// #region FIX: Move ToolButton OUTSIDE MainToolbar to prevent recreation on every render
|
|
75
78
|
// This fixes Radix UI Tooltip's asChild prop becoming stale during re-renders
|
|
@@ -176,13 +179,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
176
179
|
const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
|
|
177
180
|
const resetViewerState = useViewerStore((state) => state.resetViewerState);
|
|
178
181
|
const bcfPanelVisible = useViewerStore((state) => state.bcfPanelVisible);
|
|
179
|
-
const toggleBcfPanel = useViewerStore((state) => state.toggleBcfPanel);
|
|
180
182
|
const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
|
|
181
183
|
const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
|
|
182
|
-
const toggleIdsPanel = useViewerStore((state) => state.toggleIdsPanel);
|
|
183
184
|
const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
|
|
184
185
|
const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
|
|
185
|
-
const
|
|
186
|
+
const setListPanelVisible = useViewerStore((state) => state.setListPanelVisible);
|
|
186
187
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
187
188
|
const projectionMode = useViewerStore((state) => state.projectionMode);
|
|
188
189
|
const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
|
|
@@ -193,8 +194,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
193
194
|
const toggleBasketPresentationVisible = useViewerStore((state) => state.toggleBasketPresentationVisible);
|
|
194
195
|
// Lens state
|
|
195
196
|
const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
|
|
196
|
-
const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
|
|
197
197
|
const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
|
|
198
|
+
const scriptPanelVisible = useViewerStore((state) => state.scriptPanelVisible);
|
|
199
|
+
const setScriptPanelVisible = useViewerStore((state) => state.setScriptPanelVisible);
|
|
198
200
|
|
|
199
201
|
// Check which type geometries exist across ALL loaded models (federation-aware).
|
|
200
202
|
// PERF: Use meshes.length as dep proxy instead of full geometryResult, and
|
|
@@ -361,6 +363,61 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
361
363
|
goHomeFromStore();
|
|
362
364
|
}, []);
|
|
363
365
|
|
|
366
|
+
const handleToggleBottomPanel = useCallback((panel: 'script' | 'list') => {
|
|
367
|
+
const isScriptPanel = panel === 'script';
|
|
368
|
+
const nextScriptVisible = isScriptPanel ? !scriptPanelVisible : false;
|
|
369
|
+
const nextListVisible = isScriptPanel ? false : !listPanelVisible;
|
|
370
|
+
|
|
371
|
+
setScriptPanelVisible(nextScriptVisible);
|
|
372
|
+
setListPanelVisible(nextListVisible);
|
|
373
|
+
|
|
374
|
+
if (nextScriptVisible || nextListVisible) {
|
|
375
|
+
setRightPanelCollapsed(false);
|
|
376
|
+
}
|
|
377
|
+
}, [listPanelVisible, scriptPanelVisible, setListPanelVisible, setRightPanelCollapsed, setScriptPanelVisible]);
|
|
378
|
+
|
|
379
|
+
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens') => {
|
|
380
|
+
const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
|
|
381
|
+
const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
|
|
382
|
+
const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
|
|
383
|
+
|
|
384
|
+
setBcfPanelVisible(nextBcfVisible);
|
|
385
|
+
setIdsPanelVisible(nextIdsVisible);
|
|
386
|
+
setLensPanelVisible(nextLensVisible);
|
|
387
|
+
|
|
388
|
+
if (nextBcfVisible || nextIdsVisible || nextLensVisible) {
|
|
389
|
+
setRightPanelCollapsed(false);
|
|
390
|
+
}
|
|
391
|
+
}, [
|
|
392
|
+
bcfPanelVisible,
|
|
393
|
+
idsPanelVisible,
|
|
394
|
+
lensPanelVisible,
|
|
395
|
+
setBcfPanelVisible,
|
|
396
|
+
setIdsPanelVisible,
|
|
397
|
+
setLensPanelVisible,
|
|
398
|
+
setRightPanelCollapsed,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
const activeWorkspacePanels = useMemo(() => {
|
|
402
|
+
const panels = new Set<WorkspacePanel>();
|
|
403
|
+
if (scriptPanelVisible) panels.add('script');
|
|
404
|
+
if (listPanelVisible) panels.add('list');
|
|
405
|
+
if (bcfPanelVisible) panels.add('bcf');
|
|
406
|
+
if (idsPanelVisible) panels.add('ids');
|
|
407
|
+
if (lensPanelVisible) panels.add('lens');
|
|
408
|
+
return panels;
|
|
409
|
+
}, [bcfPanelVisible, idsPanelVisible, lensPanelVisible, listPanelVisible, scriptPanelVisible]);
|
|
410
|
+
|
|
411
|
+
const workspacePanelLabel = useMemo(() => {
|
|
412
|
+
if (activeWorkspacePanels.size === 0) return null;
|
|
413
|
+
if (activeWorkspacePanels.size > 1) return 'Multiple Panels';
|
|
414
|
+
if (activeWorkspacePanels.has('script')) return 'Script Editor';
|
|
415
|
+
if (activeWorkspacePanels.has('list')) return 'Lists';
|
|
416
|
+
if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
|
|
417
|
+
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
418
|
+
return 'Lens Rules';
|
|
419
|
+
}, [activeWorkspacePanels]);
|
|
420
|
+
|
|
364
421
|
const handleExportGLB = useCallback(() => {
|
|
365
422
|
if (!geometryResult) return;
|
|
366
423
|
try {
|
|
@@ -624,76 +681,61 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
624
681
|
<ExportChangesButton />
|
|
625
682
|
|
|
626
683
|
{/* ── Panels ── */}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
684
|
+
<DropdownMenu>
|
|
685
|
+
<Tooltip>
|
|
686
|
+
<TooltipTrigger asChild>
|
|
687
|
+
<DropdownMenuTrigger asChild>
|
|
688
|
+
<Button
|
|
689
|
+
variant={activeWorkspacePanels.size > 0 ? 'default' : 'ghost'}
|
|
690
|
+
size="icon-sm"
|
|
691
|
+
aria-label={workspacePanelLabel ? `Panels: ${workspacePanelLabel}` : 'Panels'}
|
|
692
|
+
className={cn(activeWorkspacePanels.size > 0 && 'bg-primary text-primary-foreground')}
|
|
693
|
+
>
|
|
694
|
+
<Layout className="h-4 w-4" />
|
|
695
|
+
</Button>
|
|
696
|
+
</DropdownMenuTrigger>
|
|
697
|
+
</TooltipTrigger>
|
|
698
|
+
<TooltipContent>{workspacePanelLabel ? `Panels: ${workspacePanelLabel}` : 'Panels'}</TooltipContent>
|
|
699
|
+
</Tooltip>
|
|
700
|
+
<DropdownMenuContent align="start" className="w-56">
|
|
701
|
+
<DropdownMenuCheckboxItem
|
|
702
|
+
checked={activeWorkspacePanels.has('script')}
|
|
703
|
+
onCheckedChange={() => handleToggleBottomPanel('script')}
|
|
644
704
|
>
|
|
645
|
-
<
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
{/* IDS Validation Button */}
|
|
652
|
-
<Tooltip>
|
|
653
|
-
<TooltipTrigger asChild>
|
|
654
|
-
<Button
|
|
655
|
-
variant={idsPanelVisible ? 'default' : 'ghost'}
|
|
656
|
-
size="icon-sm"
|
|
657
|
-
onClick={(e) => {
|
|
658
|
-
(e.currentTarget as HTMLButtonElement).blur();
|
|
659
|
-
if (!idsPanelVisible) {
|
|
660
|
-
// Close other right-panel content first, then expand
|
|
661
|
-
setBcfPanelVisible(false);
|
|
662
|
-
setLensPanelVisible(false);
|
|
663
|
-
setRightPanelCollapsed(false);
|
|
664
|
-
}
|
|
665
|
-
toggleIdsPanel();
|
|
666
|
-
}}
|
|
667
|
-
className={cn(idsPanelVisible && 'bg-primary text-primary-foreground')}
|
|
705
|
+
<FileCode2 className="h-4 w-4 mr-2" />
|
|
706
|
+
Script Editor
|
|
707
|
+
</DropdownMenuCheckboxItem>
|
|
708
|
+
<DropdownMenuCheckboxItem
|
|
709
|
+
checked={activeWorkspacePanels.has('list')}
|
|
710
|
+
onCheckedChange={() => handleToggleBottomPanel('list')}
|
|
668
711
|
>
|
|
669
|
-
<
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
<Tooltip>
|
|
677
|
-
<TooltipTrigger asChild>
|
|
678
|
-
<Button
|
|
679
|
-
variant={listPanelVisible ? 'default' : 'ghost'}
|
|
680
|
-
size="icon-sm"
|
|
681
|
-
onClick={(e) => {
|
|
682
|
-
(e.currentTarget as HTMLButtonElement).blur();
|
|
683
|
-
// Close other bottom panels (bottom-panel exclusivity)
|
|
684
|
-
useViewerStore.getState().setScriptPanelVisible(false);
|
|
685
|
-
if (!listPanelVisible) {
|
|
686
|
-
setRightPanelCollapsed(false);
|
|
687
|
-
}
|
|
688
|
-
toggleListPanel();
|
|
689
|
-
}}
|
|
690
|
-
className={cn(listPanelVisible && 'bg-primary text-primary-foreground')}
|
|
712
|
+
<FileSpreadsheet className="h-4 w-4 mr-2" />
|
|
713
|
+
Lists
|
|
714
|
+
</DropdownMenuCheckboxItem>
|
|
715
|
+
<DropdownMenuSeparator />
|
|
716
|
+
<DropdownMenuCheckboxItem
|
|
717
|
+
checked={activeWorkspacePanels.has('bcf')}
|
|
718
|
+
onCheckedChange={() => handleToggleRightPanel('bcf')}
|
|
691
719
|
>
|
|
692
|
-
<
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
720
|
+
<MessageSquare className="h-4 w-4 mr-2" />
|
|
721
|
+
BCF Issues
|
|
722
|
+
</DropdownMenuCheckboxItem>
|
|
723
|
+
<DropdownMenuCheckboxItem
|
|
724
|
+
checked={activeWorkspacePanels.has('ids')}
|
|
725
|
+
onCheckedChange={() => handleToggleRightPanel('ids')}
|
|
726
|
+
>
|
|
727
|
+
<ClipboardCheck className="h-4 w-4 mr-2" />
|
|
728
|
+
IDS Validation
|
|
729
|
+
</DropdownMenuCheckboxItem>
|
|
730
|
+
<DropdownMenuCheckboxItem
|
|
731
|
+
checked={activeWorkspacePanels.has('lens')}
|
|
732
|
+
onCheckedChange={() => handleToggleRightPanel('lens')}
|
|
733
|
+
>
|
|
734
|
+
<Palette className="h-4 w-4 mr-2" />
|
|
735
|
+
Lens Rules
|
|
736
|
+
</DropdownMenuCheckboxItem>
|
|
737
|
+
</DropdownMenuContent>
|
|
738
|
+
</DropdownMenu>
|
|
697
739
|
|
|
698
740
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
699
741
|
|
|
@@ -821,30 +863,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
821
863
|
</DropdownMenuContent>
|
|
822
864
|
</DropdownMenu>
|
|
823
865
|
|
|
824
|
-
{/* Lens (rule-based filtering) */}
|
|
825
|
-
<Tooltip>
|
|
826
|
-
<TooltipTrigger asChild>
|
|
827
|
-
<Button
|
|
828
|
-
variant={lensPanelVisible ? 'default' : 'ghost'}
|
|
829
|
-
size="icon-sm"
|
|
830
|
-
onClick={(e) => {
|
|
831
|
-
(e.currentTarget as HTMLButtonElement).blur();
|
|
832
|
-
if (!lensPanelVisible) {
|
|
833
|
-
// Close other right-panel content first, then expand
|
|
834
|
-
setBcfPanelVisible(false);
|
|
835
|
-
setIdsPanelVisible(false);
|
|
836
|
-
setRightPanelCollapsed(false);
|
|
837
|
-
}
|
|
838
|
-
toggleLensPanel();
|
|
839
|
-
}}
|
|
840
|
-
className={cn(lensPanelVisible && 'bg-primary text-primary-foreground')}
|
|
841
|
-
>
|
|
842
|
-
<Palette className="h-4 w-4" />
|
|
843
|
-
</Button>
|
|
844
|
-
</TooltipTrigger>
|
|
845
|
-
<TooltipContent>Lens (Color Rules)</TooltipContent>
|
|
846
|
-
</Tooltip>
|
|
847
|
-
|
|
848
866
|
<Separator orientation="vertical" className="h-6 mx-1" />
|
|
849
867
|
|
|
850
868
|
{/* ── Camera & View ── */}
|
|
@@ -68,7 +68,8 @@ export function HierarchyNode({
|
|
|
68
68
|
onRemoveModel,
|
|
69
69
|
onModelHeaderClick,
|
|
70
70
|
}: HierarchyNodeProps) {
|
|
71
|
-
const
|
|
71
|
+
const resolvedType = node.ifcType || node.type;
|
|
72
|
+
const Icon = TYPE_ICONS[resolvedType] || TYPE_ICONS[node.type] || TYPE_ICONS.default;
|
|
72
73
|
|
|
73
74
|
// Model header nodes (for visibility control and expansion)
|
|
74
75
|
if (node.type === 'model-header' && node.id.startsWith('model-')) {
|
|
@@ -261,19 +262,25 @@ export function HierarchyNode({
|
|
|
261
262
|
<Icon className="h-3.5 w-3.5 shrink-0 text-zinc-500 dark:text-zinc-400" />
|
|
262
263
|
</TooltipTrigger>
|
|
263
264
|
<TooltipContent>
|
|
264
|
-
<p className="text-xs">{
|
|
265
|
+
<p className="text-xs">{resolvedType}</p>
|
|
265
266
|
</TooltipContent>
|
|
266
267
|
</Tooltip>
|
|
267
268
|
|
|
268
269
|
{/* Name */}
|
|
269
270
|
<span className={cn(
|
|
270
271
|
'flex-1 text-sm truncate ml-1.5',
|
|
271
|
-
isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'unified-storey' || node.type === 'type-group'
|
|
272
|
+
isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group'
|
|
272
273
|
? 'font-medium text-zinc-900 dark:text-zinc-100'
|
|
273
274
|
: 'text-zinc-700 dark:text-zinc-300',
|
|
274
275
|
nodeHidden && 'line-through decoration-zinc-400 dark:decoration-zinc-600'
|
|
275
276
|
)}>{node.name}</span>
|
|
276
277
|
|
|
278
|
+
{node.ifcType && node.type === 'element' && (
|
|
279
|
+
<span className="text-[10px] font-mono text-zinc-400 dark:text-zinc-500 truncate max-w-[90px]">
|
|
280
|
+
{node.ifcType}
|
|
281
|
+
</span>
|
|
282
|
+
)}
|
|
283
|
+
|
|
277
284
|
{/* Storey Elevation */}
|
|
278
285
|
{node.storeyElevation !== undefined && (
|
|
279
286
|
<Tooltip>
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import assert from 'node:assert';
|
|
7
|
+
import { IfcTypeEnum, type SpatialHierarchy, type SpatialNode } from '@ifc-lite/data';
|
|
8
|
+
import type { IfcDataStore } from '@ifc-lite/parser';
|
|
9
|
+
import { useViewerStore, type FederatedModel } from '@/store';
|
|
10
|
+
import { buildTreeData } from './treeDataBuilder';
|
|
11
|
+
|
|
12
|
+
function createSpatialNode(
|
|
13
|
+
expressId: number,
|
|
14
|
+
type: IfcTypeEnum,
|
|
15
|
+
name: string,
|
|
16
|
+
children: SpatialNode[] = [],
|
|
17
|
+
): SpatialNode {
|
|
18
|
+
return {
|
|
19
|
+
expressId,
|
|
20
|
+
type,
|
|
21
|
+
name,
|
|
22
|
+
children,
|
|
23
|
+
elements: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function createDataStore(): IfcDataStore {
|
|
28
|
+
const spaceNode = createSpatialNode(5, IfcTypeEnum.IfcSpace, 'e3035b71');
|
|
29
|
+
const storeyNode = createSpatialNode(4, IfcTypeEnum.IfcBuildingStorey, 'MY_STOREY', [spaceNode]);
|
|
30
|
+
const buildingNode = createSpatialNode(3, IfcTypeEnum.IfcBuilding, 'MY_BUILDING', [storeyNode]);
|
|
31
|
+
const siteNode = createSpatialNode(2, IfcTypeEnum.IfcSite, 'MY_SITE', [buildingNode]);
|
|
32
|
+
const projectNode = createSpatialNode(1, IfcTypeEnum.IfcProject, 'MY_PROJECT', [siteNode]);
|
|
33
|
+
|
|
34
|
+
const spatialHierarchy: SpatialHierarchy = {
|
|
35
|
+
project: projectNode,
|
|
36
|
+
byStorey: new Map([[4, [6, 7]]]),
|
|
37
|
+
byBuilding: new Map(),
|
|
38
|
+
bySite: new Map(),
|
|
39
|
+
bySpace: new Map([[5, [7]]]),
|
|
40
|
+
storeyElevations: new Map(),
|
|
41
|
+
storeyHeights: new Map(),
|
|
42
|
+
elementToStorey: new Map([[6, 4], [7, 4]]),
|
|
43
|
+
getStoreyElements: () => [],
|
|
44
|
+
getStoreyByElevation: () => null,
|
|
45
|
+
getContainingSpace: (elementId: number) => (elementId === 7 ? 5 : null),
|
|
46
|
+
getPath: () => [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
spatialHierarchy,
|
|
51
|
+
entities: {
|
|
52
|
+
count: 0,
|
|
53
|
+
getName: (id: number) => {
|
|
54
|
+
if (id === 6) return 'Wall';
|
|
55
|
+
if (id === 7) return '';
|
|
56
|
+
return '';
|
|
57
|
+
},
|
|
58
|
+
getTypeName: (id: number) => {
|
|
59
|
+
if (id === 6) return 'IfcWall';
|
|
60
|
+
if (id === 7) return 'IfcWindow';
|
|
61
|
+
if (id === 5) return 'IfcSpace';
|
|
62
|
+
return 'Unknown';
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
} as unknown as IfcDataStore;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createModel(idOffset: number): FederatedModel {
|
|
69
|
+
return {
|
|
70
|
+
id: 'model-1',
|
|
71
|
+
name: 'Model 1',
|
|
72
|
+
ifcDataStore: createDataStore(),
|
|
73
|
+
geometryResult: { meshes: [], totalVertices: 0, totalTriangles: 0, coordinateInfo: null as never },
|
|
74
|
+
visible: true,
|
|
75
|
+
collapsed: false,
|
|
76
|
+
schemaVersion: 'IFC4',
|
|
77
|
+
loadedAt: 1,
|
|
78
|
+
fileSize: 1,
|
|
79
|
+
idOffset,
|
|
80
|
+
maxExpressId: 7,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('buildTreeData', () => {
|
|
85
|
+
it('keeps IfcSpace as a spatial node, expands bySpace children, and avoids storey duplicates', () => {
|
|
86
|
+
useViewerStore.setState({ models: new Map() });
|
|
87
|
+
useViewerStore.getState().registerModelOffset('tree-test-padding', 99);
|
|
88
|
+
const idOffset = useViewerStore.getState().registerModelOffset('model-1', 7);
|
|
89
|
+
const model = createModel(idOffset);
|
|
90
|
+
useViewerStore.setState({ models: new Map([['model-1', model]]) });
|
|
91
|
+
|
|
92
|
+
const models = new Map<string, FederatedModel>([['model-1', model]]);
|
|
93
|
+
const expandedNodes = new Set([
|
|
94
|
+
'root-1',
|
|
95
|
+
'root-1-2',
|
|
96
|
+
'root-1-2-3',
|
|
97
|
+
'root-1-2-3-4',
|
|
98
|
+
'root-1-2-3-4-5',
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
const nodes = buildTreeData(models, null, expandedNodes, false, []);
|
|
102
|
+
|
|
103
|
+
const storeyNode = nodes.find((node) => node.id === 'root-1-2-3-4');
|
|
104
|
+
assert.ok(storeyNode);
|
|
105
|
+
assert.strictEqual(storeyNode.elementCount, 1);
|
|
106
|
+
|
|
107
|
+
const spaceNode = nodes.find((node) => node.id === 'root-1-2-3-4-5');
|
|
108
|
+
assert.ok(spaceNode);
|
|
109
|
+
assert.strictEqual(spaceNode.type, 'IfcSpace');
|
|
110
|
+
assert.deepStrictEqual(spaceNode.expressIds, [5]);
|
|
111
|
+
assert.deepStrictEqual(spaceNode.globalIds, [105]);
|
|
112
|
+
assert.strictEqual(spaceNode.elementCount, 1);
|
|
113
|
+
assert.strictEqual(spaceNode.hasChildren, true);
|
|
114
|
+
|
|
115
|
+
const windowNode = nodes.find((node) => node.id === 'element-model-1-7');
|
|
116
|
+
assert.ok(windowNode);
|
|
117
|
+
assert.strictEqual(windowNode.type, 'element');
|
|
118
|
+
assert.strictEqual(windowNode.ifcType, 'IfcWindow');
|
|
119
|
+
assert.deepStrictEqual(windowNode.expressIds, [7]);
|
|
120
|
+
assert.deepStrictEqual(windowNode.globalIds, [107]);
|
|
121
|
+
assert.strictEqual(windowNode.name, 'IfcWindow #7');
|
|
122
|
+
|
|
123
|
+
assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-6').length, 1);
|
|
124
|
+
assert.strictEqual(nodes.filter((node) => node.id === 'element-model-1-7').length, 1);
|
|
125
|
+
});
|
|
126
|
+
});
|