@ifc-lite/viewer 1.26.0 → 1.28.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/.turbo/turbo-build.log +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -0,0 +1,420 @@
|
|
|
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
|
+
* Model comparison panel (issue #924). Pick two loaded models as A (base) and
|
|
7
|
+
* B (head), choose a data/geometry/both scope, run the `@ifc-lite/diff` engine,
|
|
8
|
+
* and review added / modified / deleted elements — colour-coded in 3D (via
|
|
9
|
+
* `useCompareOverlay`) and listed here. Row click selects + frames the element.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useEffect, useMemo } from 'react';
|
|
13
|
+
import { GitCompareArrows, Plus, Minus, PencilLine, Loader2, Play, X, Trash2 } from 'lucide-react';
|
|
14
|
+
import { Button } from '@/components/ui/button';
|
|
15
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
16
|
+
import { cn } from '@/lib/utils';
|
|
17
|
+
import { useViewerStore } from '@/store';
|
|
18
|
+
import { useCompare } from '@/hooks/useCompare';
|
|
19
|
+
import { useCompareOverlay } from '@/hooks/useCompareOverlay';
|
|
20
|
+
import { COMPARE_COLORS, type RGBA } from '@/lib/compare/overlay';
|
|
21
|
+
import type { CompareRef } from '@/lib/compare/buildFingerprints';
|
|
22
|
+
import { describeChange, type ChangeDetail, type FieldDelta, type GeometrySummary } from '@/lib/compare/describeChange';
|
|
23
|
+
import type { DiffScope, DiffState, DiffEntry } from '@ifc-lite/diff';
|
|
24
|
+
|
|
25
|
+
interface ComparePanelProps {
|
|
26
|
+
onClose?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SCOPES: { id: DiffScope; label: string }[] = [
|
|
30
|
+
{ id: 'both', label: 'Both' },
|
|
31
|
+
{ id: 'data', label: 'Data' },
|
|
32
|
+
{ id: 'geometry', label: 'Geometry' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** States listed in the panel (unchanged only affects 3D ghosting). */
|
|
36
|
+
const LISTED_STATES: { state: Exclude<DiffState, 'unchanged'>; label: string; color: RGBA; Icon: typeof Plus }[] = [
|
|
37
|
+
{ state: 'modified', label: 'Changed', color: COMPARE_COLORS.modified, Icon: PencilLine },
|
|
38
|
+
{ state: 'added', label: 'Added', color: COMPARE_COLORS.added, Icon: Plus },
|
|
39
|
+
{ state: 'deleted', label: 'Deleted', color: COMPARE_COLORS.deleted, Icon: Minus },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Cap rows rendered per group so a huge diff can't stall the DOM. */
|
|
43
|
+
const MAX_ROWS_PER_GROUP = 1000;
|
|
44
|
+
|
|
45
|
+
function rgbaCss([r, g, b, a]: RGBA): string {
|
|
46
|
+
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface CompareRow {
|
|
50
|
+
key: string;
|
|
51
|
+
ifcType: string;
|
|
52
|
+
name: string;
|
|
53
|
+
changeKinds: string[];
|
|
54
|
+
ref: CompareRef;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The side actually drawn for an entry: base for deletions, head otherwise. */
|
|
58
|
+
function renderRef(entry: DiffEntry<CompareRef>): CompareRef | undefined {
|
|
59
|
+
return (entry.state === 'deleted' ? entry.base?.ref : entry.head?.ref) ?? entry.base?.ref;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ComparePanel({ onClose }: ComparePanelProps) {
|
|
63
|
+
useCompareOverlay();
|
|
64
|
+
|
|
65
|
+
const models = useViewerStore((s) => s.models);
|
|
66
|
+
const baseModelId = useViewerStore((s) => s.compareBaseModelId);
|
|
67
|
+
const headModelId = useViewerStore((s) => s.compareHeadModelId);
|
|
68
|
+
const scope = useViewerStore((s) => s.compareScope);
|
|
69
|
+
const showUnchanged = useViewerStore((s) => s.compareShowUnchanged);
|
|
70
|
+
const selectedKey = useViewerStore((s) => s.compareSelectedKey);
|
|
71
|
+
const setBaseModelId = useViewerStore((s) => s.setCompareBaseModelId);
|
|
72
|
+
const setHeadModelId = useViewerStore((s) => s.setCompareHeadModelId);
|
|
73
|
+
const setScope = useViewerStore((s) => s.setCompareScope);
|
|
74
|
+
const setShowUnchanged = useViewerStore((s) => s.setCompareShowUnchanged);
|
|
75
|
+
const clearCompare = useViewerStore((s) => s.clearCompare);
|
|
76
|
+
|
|
77
|
+
const { running, result, error, runComparison } = useCompare();
|
|
78
|
+
|
|
79
|
+
const modelList = useMemo(() => Array.from(models.values()), [models]);
|
|
80
|
+
|
|
81
|
+
// Default the A/B selection to the first two loaded models, and repair the
|
|
82
|
+
// selection if a chosen model was removed.
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
const ids = modelList.map((m) => m.id);
|
|
85
|
+
// A comparison computed against a model that's since been removed leaves a
|
|
86
|
+
// stale overlay on the survivor (the overlay hook keys off the result, not
|
|
87
|
+
// the model list) — drop it so the scene is restored.
|
|
88
|
+
const ran = useViewerStore.getState().compareResult;
|
|
89
|
+
if (ran && (!ids.includes(ran.baseModelId) || !ids.includes(ran.headModelId))) {
|
|
90
|
+
clearCompare();
|
|
91
|
+
}
|
|
92
|
+
if (ids.length === 0) return;
|
|
93
|
+
if (!baseModelId || !ids.includes(baseModelId)) {
|
|
94
|
+
setBaseModelId(ids[0]);
|
|
95
|
+
}
|
|
96
|
+
if (ids.length > 1 && (!headModelId || !ids.includes(headModelId) || headModelId === ids[0])) {
|
|
97
|
+
const other = ids.find((id) => id !== ids[0]);
|
|
98
|
+
if (other) setHeadModelId(other);
|
|
99
|
+
}
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [modelList]);
|
|
102
|
+
|
|
103
|
+
// Resolve a display name + grouped rows from the diff result. Names live in
|
|
104
|
+
// the per-model store (the engine result carries only type + key), so we
|
|
105
|
+
// look them up here via each entry's ref.
|
|
106
|
+
const groups = useMemo(() => {
|
|
107
|
+
const empty = new Map<DiffState, { rows: CompareRow[]; truncated: number }>();
|
|
108
|
+
if (!result) return empty;
|
|
109
|
+
const out = new Map<DiffState, { rows: CompareRow[]; truncated: number }>();
|
|
110
|
+
for (const { state } of LISTED_STATES) out.set(state, { rows: [], truncated: 0 });
|
|
111
|
+
|
|
112
|
+
for (const entry of result.diff.entries) {
|
|
113
|
+
const bucket = out.get(entry.state);
|
|
114
|
+
if (!bucket) continue; // skip unchanged
|
|
115
|
+
const ref = renderRef(entry);
|
|
116
|
+
if (!ref) continue;
|
|
117
|
+
if (bucket.rows.length >= MAX_ROWS_PER_GROUP) {
|
|
118
|
+
bucket.truncated++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const store = models.get(ref.modelId)?.ifcDataStore;
|
|
122
|
+
const name = store?.entities.getName(ref.localId) || '';
|
|
123
|
+
const ifcType = (entry.head ?? entry.base)?.ifcType ?? 'IfcProduct';
|
|
124
|
+
bucket.rows.push({ key: entry.key, ifcType, name, changeKinds: entry.changeKinds, ref });
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}, [result, models]);
|
|
128
|
+
|
|
129
|
+
const counts = result?.diff.counts;
|
|
130
|
+
const canRun = !!baseModelId && !!headModelId && baseModelId !== headModelId && !running;
|
|
131
|
+
|
|
132
|
+
// "What changed" detail for the selected entry — computed lazily from both
|
|
133
|
+
// stores so a huge diff stays cheap (only the selection is described).
|
|
134
|
+
const detail = useMemo<ChangeDetail | null>(() => {
|
|
135
|
+
if (!result || !selectedKey) return null;
|
|
136
|
+
const entry = result.diff.byKey.get(selectedKey);
|
|
137
|
+
return entry ? describeChange(entry, models) : null;
|
|
138
|
+
}, [result, selectedKey, models]);
|
|
139
|
+
|
|
140
|
+
const selectedRow = useMemo<CompareRow | null>(() => {
|
|
141
|
+
if (!selectedKey) return null;
|
|
142
|
+
for (const bucket of groups.values()) {
|
|
143
|
+
const row = bucket.rows.find((r) => r.key === selectedKey);
|
|
144
|
+
if (row) return row;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}, [groups, selectedKey]);
|
|
148
|
+
|
|
149
|
+
const focusEntry = (row: CompareRow) => {
|
|
150
|
+
const state = useViewerStore.getState();
|
|
151
|
+
state.clearEntitySelection();
|
|
152
|
+
state.setSelectedEntityIds([row.ref.globalId]);
|
|
153
|
+
state.addEntitiesToSelection([{ modelId: row.ref.modelId, expressId: row.ref.localId }]);
|
|
154
|
+
state.setCompareSelectedKey(row.key);
|
|
155
|
+
requestAnimationFrame(() => state.cameraCallbacks.frameSelection?.());
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<div className="h-full flex flex-col bg-background text-foreground overflow-hidden min-w-0">
|
|
160
|
+
{/* Header */}
|
|
161
|
+
<div className="flex items-center gap-2 p-3 border-b border-border">
|
|
162
|
+
<GitCompareArrows className="h-4 w-4 text-primary shrink-0" />
|
|
163
|
+
<span className="text-sm font-semibold tracking-tight min-w-0">Compare models</span>
|
|
164
|
+
<div className="ml-auto flex items-center gap-1 shrink-0">
|
|
165
|
+
{result && (
|
|
166
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" title="Clear results" onClick={clearCompare}>
|
|
167
|
+
<Trash2 className="h-4 w-4" />
|
|
168
|
+
</Button>
|
|
169
|
+
)}
|
|
170
|
+
{onClose && (
|
|
171
|
+
<Button variant="ghost" size="icon" className="h-7 w-7" title="Close" onClick={onClose}>
|
|
172
|
+
<X className="h-4 w-4" />
|
|
173
|
+
</Button>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{modelList.length < 2 ? (
|
|
179
|
+
<div className="p-4 text-sm text-muted-foreground">
|
|
180
|
+
Load a second model to compare. Open two IFC files (federation), then pick
|
|
181
|
+
version A and version B here.
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
<>
|
|
185
|
+
{/* Run controls */}
|
|
186
|
+
<div className="p-3 space-y-3 border-b border-border">
|
|
187
|
+
<div className="grid grid-cols-[1.25rem_1fr] items-center gap-x-2 gap-y-2 text-xs">
|
|
188
|
+
<span className="text-muted-foreground">A</span>
|
|
189
|
+
<select
|
|
190
|
+
value={baseModelId ?? ''}
|
|
191
|
+
onChange={(e) => setBaseModelId(e.target.value)}
|
|
192
|
+
className="w-full rounded border border-border bg-transparent px-2 py-1 text-foreground min-w-0"
|
|
193
|
+
>
|
|
194
|
+
{modelList.map((m) => (
|
|
195
|
+
<option key={m.id} value={m.id}>{m.name}</option>
|
|
196
|
+
))}
|
|
197
|
+
</select>
|
|
198
|
+
<span className="text-muted-foreground">B</span>
|
|
199
|
+
<select
|
|
200
|
+
value={headModelId ?? ''}
|
|
201
|
+
onChange={(e) => setHeadModelId(e.target.value)}
|
|
202
|
+
className="w-full rounded border border-border bg-transparent px-2 py-1 text-foreground min-w-0"
|
|
203
|
+
>
|
|
204
|
+
{modelList.map((m) => (
|
|
205
|
+
<option key={m.id} value={m.id}>{m.name}</option>
|
|
206
|
+
))}
|
|
207
|
+
</select>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{baseModelId === headModelId && (
|
|
211
|
+
<p className="text-xs text-[#e0af68]">Pick two different models.</p>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
215
|
+
<div className="inline-flex rounded-md border border-border overflow-hidden text-xs shrink-0">
|
|
216
|
+
{SCOPES.map((s) => (
|
|
217
|
+
<button
|
|
218
|
+
key={s.id}
|
|
219
|
+
onClick={() => setScope(s.id)}
|
|
220
|
+
className={cn(
|
|
221
|
+
'px-2.5 py-1 transition-colors',
|
|
222
|
+
scope === s.id ? 'bg-primary text-primary-foreground' : 'hover:bg-muted',
|
|
223
|
+
)}
|
|
224
|
+
>
|
|
225
|
+
{s.label}
|
|
226
|
+
</button>
|
|
227
|
+
))}
|
|
228
|
+
</div>
|
|
229
|
+
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
|
|
230
|
+
<input
|
|
231
|
+
type="checkbox"
|
|
232
|
+
checked={showUnchanged}
|
|
233
|
+
onChange={(e) => setShowUnchanged(e.target.checked)}
|
|
234
|
+
/>
|
|
235
|
+
Show unchanged
|
|
236
|
+
</label>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<Button size="sm" className="w-full gap-1.5" disabled={!canRun} onClick={() => void runComparison()}>
|
|
240
|
+
{running ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
|
|
241
|
+
{running ? 'Comparing…' : 'Run comparison'}
|
|
242
|
+
</Button>
|
|
243
|
+
|
|
244
|
+
{error && <p className="text-xs text-[#f7768e]">{error}</p>}
|
|
245
|
+
|
|
246
|
+
{result?.geometryUnavailable && scope !== 'data' && (
|
|
247
|
+
<p className="text-xs text-[#e0af68]">
|
|
248
|
+
One model has no geometry fingerprints (loaded outside the WASM
|
|
249
|
+
mesh path), so geometry changes can’t be detected. Data changes
|
|
250
|
+
are still accurate — switch to the Data scope for reliable results.
|
|
251
|
+
</p>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
{/* Counts */}
|
|
256
|
+
{counts && (
|
|
257
|
+
<div className="grid grid-cols-4 gap-1 p-3 border-b border-border text-center">
|
|
258
|
+
<CountBadge label="Changed" value={counts.modified} color={COMPARE_COLORS.modified} />
|
|
259
|
+
<CountBadge label="Added" value={counts.added} color={COMPARE_COLORS.added} />
|
|
260
|
+
<CountBadge label="Deleted" value={counts.deleted} color={COMPARE_COLORS.deleted} />
|
|
261
|
+
<CountBadge label="Unchanged" value={counts.unchanged} color={COMPARE_COLORS.unchanged} />
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
|
|
265
|
+
{/* Results list */}
|
|
266
|
+
<ScrollArea className="flex-1 min-h-0">
|
|
267
|
+
{!result ? (
|
|
268
|
+
<div className="p-4 text-sm text-muted-foreground">
|
|
269
|
+
Run a comparison to see added, changed, and deleted elements.
|
|
270
|
+
</div>
|
|
271
|
+
) : (
|
|
272
|
+
<div className="p-2 space-y-3">
|
|
273
|
+
{LISTED_STATES.map(({ state, label, color, Icon }) => {
|
|
274
|
+
const bucket = groups.get(state);
|
|
275
|
+
if (!bucket || bucket.rows.length === 0) return null;
|
|
276
|
+
return (
|
|
277
|
+
<div key={state}>
|
|
278
|
+
<div className="flex items-center gap-1.5 px-1 py-1 text-xs font-medium">
|
|
279
|
+
<Icon className="h-3.5 w-3.5" style={{ color: rgbaCss(color) }} />
|
|
280
|
+
<span>{label}</span>
|
|
281
|
+
<span className="text-muted-foreground">({bucket.rows.length + bucket.truncated})</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div className="space-y-0.5">
|
|
284
|
+
{bucket.rows.map((row) => (
|
|
285
|
+
<button
|
|
286
|
+
key={row.key}
|
|
287
|
+
onClick={() => focusEntry(row)}
|
|
288
|
+
className={cn(
|
|
289
|
+
'w-full text-left rounded px-2 py-1 flex items-center gap-2 hover:bg-muted transition-colors min-w-0',
|
|
290
|
+
selectedKey === row.key && 'bg-muted',
|
|
291
|
+
)}
|
|
292
|
+
>
|
|
293
|
+
<span
|
|
294
|
+
className="h-2.5 w-2.5 rounded-sm shrink-0"
|
|
295
|
+
style={{ backgroundColor: rgbaCss(color) }}
|
|
296
|
+
/>
|
|
297
|
+
<span className="min-w-0 flex-1 truncate text-xs">
|
|
298
|
+
{row.name || row.ifcType}
|
|
299
|
+
</span>
|
|
300
|
+
<span className="shrink-0 text-[10px] text-muted-foreground">
|
|
301
|
+
{state === 'modified' && row.changeKinds.length > 0
|
|
302
|
+
? row.changeKinds.join(' · ')
|
|
303
|
+
: row.ifcType.replace(/^Ifc/, '')}
|
|
304
|
+
</span>
|
|
305
|
+
</button>
|
|
306
|
+
))}
|
|
307
|
+
{bucket.truncated > 0 && (
|
|
308
|
+
<p className="px-2 py-1 text-[10px] text-muted-foreground">
|
|
309
|
+
+{bucket.truncated} more not shown
|
|
310
|
+
</p>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
})}
|
|
316
|
+
{counts && counts.added + counts.modified + counts.deleted === 0 && (
|
|
317
|
+
<div className="p-3 text-sm text-muted-foreground">
|
|
318
|
+
No differences in scope “{result.scope}”. The models match.
|
|
319
|
+
</div>
|
|
320
|
+
)}
|
|
321
|
+
</div>
|
|
322
|
+
)}
|
|
323
|
+
</ScrollArea>
|
|
324
|
+
|
|
325
|
+
{/* What-changed detail for the selected element */}
|
|
326
|
+
{detail && selectedRow && <ChangeDetailView row={selectedRow} detail={detail} />}
|
|
327
|
+
</>
|
|
328
|
+
)}
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Per-element "what changed" — geometry move/reshape + data field deltas. */
|
|
334
|
+
function ChangeDetailView({ row, detail }: { row: CompareRow; detail: ChangeDetail }) {
|
|
335
|
+
return (
|
|
336
|
+
<div className="border-t border-border shrink-0 max-h-[42%] overflow-auto">
|
|
337
|
+
<div className="px-3 pt-2.5 pb-1.5 flex items-center gap-1.5 sticky top-0 bg-background">
|
|
338
|
+
<PencilLine className="h-3.5 w-3.5 text-primary shrink-0" />
|
|
339
|
+
<span className="text-xs font-semibold truncate">{row.name || row.ifcType}</span>
|
|
340
|
+
<span className="ml-auto text-[10px] text-muted-foreground shrink-0">{row.ifcType.replace(/^Ifc/, '')}</span>
|
|
341
|
+
</div>
|
|
342
|
+
<div className="px-3 pb-3 space-y-2.5 text-xs">
|
|
343
|
+
{detail.geometry && (
|
|
344
|
+
<div className="space-y-1">
|
|
345
|
+
<div className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">Geometry</div>
|
|
346
|
+
<GeometryDetail summary={detail.geometry} />
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
{detail.data.length > 0 ? (
|
|
350
|
+
<div className="space-y-1">
|
|
351
|
+
<div className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
|
352
|
+
Data <span className="text-muted-foreground/70">({detail.data.length})</span>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="space-y-1">
|
|
355
|
+
{detail.data.map((d, i) => <FieldDeltaRow key={i} delta={d} />)}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
) : detail.dataOnlyGeometric ? (
|
|
359
|
+
<div className="text-[11px] text-muted-foreground italic">
|
|
360
|
+
Data fingerprint differs but no field-level change could be pinpointed.
|
|
361
|
+
</div>
|
|
362
|
+
) : !detail.geometry ? (
|
|
363
|
+
<div className="text-[11px] text-muted-foreground italic">No field-level detail available.</div>
|
|
364
|
+
) : null}
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function GeometryDetail({ summary }: { summary: GeometrySummary }) {
|
|
371
|
+
const moved = summary.movedDistance >= 1e-3;
|
|
372
|
+
const fmt = (n: number) => (Math.abs(n) < 1e-3 ? '0' : n.toFixed(n >= 1 ? 2 : 3));
|
|
373
|
+
const headline = summary.reshaped ? (moved ? 'Reshaped + moved' : 'Reshaped') : moved ? 'Moved' : 'Geometry changed';
|
|
374
|
+
return (
|
|
375
|
+
<div className="rounded border border-border/60 px-2 py-1.5 space-y-0.5">
|
|
376
|
+
<div className="font-medium">{headline}</div>
|
|
377
|
+
{moved && (
|
|
378
|
+
<div className="text-muted-foreground tabular-nums">
|
|
379
|
+
{fmt(summary.movedDistance)} m
|
|
380
|
+
<span className="text-muted-foreground/70">
|
|
381
|
+
{' '}(Δx {fmt(summary.delta.x)}, Δy {fmt(summary.delta.y)}, Δz {fmt(summary.delta.z)})
|
|
382
|
+
</span>
|
|
383
|
+
</div>
|
|
384
|
+
)}
|
|
385
|
+
</div>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function FieldDeltaRow({ delta }: { delta: FieldDelta }) {
|
|
390
|
+
const kindColor: Record<FieldDelta['kind'], string> = {
|
|
391
|
+
changed: 'text-[#e0af68]',
|
|
392
|
+
added: 'text-[#9ece6a]',
|
|
393
|
+
removed: 'text-[#f7768e]',
|
|
394
|
+
};
|
|
395
|
+
return (
|
|
396
|
+
<div className="rounded border border-border/40 px-2 py-1">
|
|
397
|
+
<div className="flex items-baseline gap-1.5 min-w-0">
|
|
398
|
+
{delta.group && <span className="text-[10px] text-muted-foreground shrink-0 truncate max-w-[40%]">{delta.group}</span>}
|
|
399
|
+
<span className="text-[11px] font-medium truncate">{delta.name}</span>
|
|
400
|
+
<span className={cn('ml-auto text-[10px] shrink-0', kindColor[delta.kind])}>{delta.kind}</span>
|
|
401
|
+
</div>
|
|
402
|
+
<div className="flex items-center gap-1.5 text-[11px] tabular-nums mt-0.5 min-w-0">
|
|
403
|
+
<span className="text-muted-foreground line-through truncate max-w-[45%]">{delta.before ?? '—'}</span>
|
|
404
|
+
<span className="text-muted-foreground/60 shrink-0">→</span>
|
|
405
|
+
<span className="truncate max-w-[45%]">{delta.after ?? '—'}</span>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function CountBadge({ label, value, color }: { label: string; value: number; color: RGBA }) {
|
|
412
|
+
return (
|
|
413
|
+
<div className="flex flex-col items-center gap-0.5">
|
|
414
|
+
<span className="text-sm font-semibold tabular-nums" style={{ color: rgbaCss([color[0], color[1], color[2], 1]) }}>
|
|
415
|
+
{value.toLocaleString()}
|
|
416
|
+
</span>
|
|
417
|
+
<span className="text-[10px] text-muted-foreground">{label}</span>
|
|
418
|
+
</div>
|
|
419
|
+
);
|
|
420
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
LayoutTemplate,
|
|
12
12
|
FileBox,
|
|
13
13
|
GripHorizontal,
|
|
14
|
+
Palette,
|
|
14
15
|
} from 'lucide-react';
|
|
15
16
|
import { Input } from '@/components/ui/input';
|
|
16
17
|
import { Button } from '@/components/ui/button';
|
|
@@ -322,6 +323,34 @@ export function HierarchyPanel() {
|
|
|
322
323
|
return;
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
// Material group nodes (Materials tab) - select the material entity for the
|
|
327
|
+
// totals panel + isolate the elements that use it.
|
|
328
|
+
if (node.type === 'material-group') {
|
|
329
|
+
const modelId = node.modelIds[0];
|
|
330
|
+
const materialExpressId = node.entityExpressId;
|
|
331
|
+
|
|
332
|
+
// Clear multi-selection first (setSelectedEntityIds([]) resets selectedEntityId)
|
|
333
|
+
setSelectedEntityIds([]);
|
|
334
|
+
|
|
335
|
+
if (materialExpressId !== undefined) {
|
|
336
|
+
if (modelId && modelId !== 'legacy') {
|
|
337
|
+
setSelectedEntityId(toGlobalId(modelId, materialExpressId));
|
|
338
|
+
setSelectedEntity({ modelId, expressId: materialExpressId });
|
|
339
|
+
setActiveModel(modelId);
|
|
340
|
+
} else {
|
|
341
|
+
setSelectedEntityId(materialExpressId);
|
|
342
|
+
setSelectedEntity({ modelId: 'legacy', expressId: materialExpressId });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Isolate the elements using this material
|
|
347
|
+
const elements = getNodeElements(node);
|
|
348
|
+
if (elements.length > 0) {
|
|
349
|
+
isolateEntities(elements);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
325
354
|
// IFC type entity nodes (e.g. IfcWallType/W01) - select type entity for property panel + isolate instances
|
|
326
355
|
if (node.type === 'ifc-type') {
|
|
327
356
|
const modelId = node.modelIds[0];
|
|
@@ -478,14 +507,14 @@ export function HierarchyPanel() {
|
|
|
478
507
|
? selectedStoreys.has(node.expressIds[0])
|
|
479
508
|
: node.type === 'IfcSpace' || node.type === 'element'
|
|
480
509
|
? selectedEntityId === (node.globalIds[0] ?? node.expressIds[0])
|
|
481
|
-
: node.type === 'ifc-type'
|
|
510
|
+
: node.type === 'ifc-type' || node.type === 'material-group'
|
|
482
511
|
? (() => {
|
|
483
|
-
const
|
|
484
|
-
if (!
|
|
512
|
+
const entityExpressId = node.entityExpressId;
|
|
513
|
+
if (!entityExpressId) return false;
|
|
485
514
|
const mId = node.modelIds[0];
|
|
486
515
|
const gId = mId && mId !== 'legacy'
|
|
487
|
-
? toGlobalId(mId,
|
|
488
|
-
:
|
|
516
|
+
? toGlobalId(mId, entityExpressId)
|
|
517
|
+
: entityExpressId;
|
|
489
518
|
return selectedEntityId === gId;
|
|
490
519
|
})()
|
|
491
520
|
: false;
|
|
@@ -495,7 +524,7 @@ export function HierarchyPanel() {
|
|
|
495
524
|
if (node.type === 'element') {
|
|
496
525
|
nodeHidden = hiddenEntities.has(node.globalIds[0] ?? node.expressIds[0]);
|
|
497
526
|
} else if (node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' ||
|
|
498
|
-
node.type === 'type-group' || node.type === 'ifc-type' ||
|
|
527
|
+
node.type === 'type-group' || node.type === 'ifc-type' || node.type === 'material-group' ||
|
|
499
528
|
(node.type === 'model-header' && node.id.startsWith('contrib-'))) {
|
|
500
529
|
const elements = getNodeElements(node);
|
|
501
530
|
nodeHidden = elements.length > 0 && elements.every(id => hiddenEntities.has(id));
|
|
@@ -732,6 +761,16 @@ export function HierarchyPanel() {
|
|
|
732
761
|
<FileBox className="h-3 w-3 shrink-0 panel-compact-icon" />
|
|
733
762
|
<span className="panel-compact-text">Type</span>
|
|
734
763
|
</Button>
|
|
764
|
+
<Button
|
|
765
|
+
variant={groupingMode === 'material' ? 'default' : 'outline'}
|
|
766
|
+
size="sm"
|
|
767
|
+
className="h-6 text-[10px] flex-1 min-w-0 rounded-none uppercase tracking-wider"
|
|
768
|
+
onClick={() => setGroupingMode('material')}
|
|
769
|
+
title="Materials"
|
|
770
|
+
>
|
|
771
|
+
<Palette className="h-3 w-3 shrink-0 panel-compact-icon" />
|
|
772
|
+
<span className="panel-compact-text">Material</span>
|
|
773
|
+
</Button>
|
|
735
774
|
</div>
|
|
736
775
|
);
|
|
737
776
|
|
|
@@ -871,7 +910,7 @@ export function HierarchyPanel() {
|
|
|
871
910
|
</div>
|
|
872
911
|
|
|
873
912
|
{/* Section Header */}
|
|
874
|
-
<SectionHeader icon={groupingMode === 'spatial' ? Building2 : groupingMode === 'type' ? Layers : FileBox} title={groupingMode === 'spatial' ? 'Hierarchy' : groupingMode === 'type' ? 'By Class' : 'By Type'} count={filteredNodes.length} />
|
|
913
|
+
<SectionHeader icon={groupingMode === 'spatial' ? Building2 : groupingMode === 'type' ? Layers : groupingMode === 'material' ? Palette : FileBox} title={groupingMode === 'spatial' ? 'Hierarchy' : groupingMode === 'type' ? 'By Class' : groupingMode === 'material' ? 'By Material' : 'By Type'} count={filteredNodes.length} />
|
|
875
914
|
|
|
876
915
|
{/* Tree */}
|
|
877
916
|
<div ref={parentRef} className="flex-1 overflow-auto scrollbar-thin bg-white dark:bg-black">
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
EyeOff,
|
|
16
16
|
Equal,
|
|
17
17
|
Crosshair,
|
|
18
|
+
GitCompareArrows,
|
|
18
19
|
Home,
|
|
19
20
|
Maximize2,
|
|
20
21
|
Grid3x3,
|
|
@@ -562,6 +563,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
562
563
|
const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
|
|
563
564
|
const clashPanelVisible = useViewerStore((state) => state.clashPanelVisible);
|
|
564
565
|
const setClashPanelVisible = useViewerStore((state) => state.setClashPanelVisible);
|
|
566
|
+
const comparePanelVisible = useViewerStore((state) => state.comparePanelVisible);
|
|
567
|
+
const setComparePanelVisible = useViewerStore((state) => state.setComparePanelVisible);
|
|
565
568
|
const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
|
|
566
569
|
const setListPanelVisible = useViewerStore((state) => state.setListPanelVisible);
|
|
567
570
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
@@ -761,7 +764,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
761
764
|
setScriptPanelVisible,
|
|
762
765
|
]);
|
|
763
766
|
|
|
764
|
-
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'addElement' | 'extensions') => {
|
|
767
|
+
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'compare' | 'addElement' | 'extensions') => {
|
|
765
768
|
if (activeAnalysisExtension?.placement !== 'bottom') {
|
|
766
769
|
closeActiveAnalysisExtension();
|
|
767
770
|
}
|
|
@@ -779,6 +782,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
779
782
|
const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
|
|
780
783
|
const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
|
|
781
784
|
const nextClashVisible = panel === 'clash' ? !clashPanelVisible : false;
|
|
785
|
+
const nextCompareVisible = panel === 'compare' ? !comparePanelVisible : false;
|
|
782
786
|
const nextExtensionsVisible = panel === 'extensions' ? !extensionsPanelVisible : false;
|
|
783
787
|
const isAddElementActive = activeTool === 'addElement';
|
|
784
788
|
const nextAddElementActive = panel === 'addElement' ? !isAddElementActive : false;
|
|
@@ -787,6 +791,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
787
791
|
setIdsPanelVisible(nextIdsVisible);
|
|
788
792
|
setLensPanelVisible(nextLensVisible);
|
|
789
793
|
setClashPanelVisible(nextClashVisible);
|
|
794
|
+
setComparePanelVisible(nextCompareVisible);
|
|
790
795
|
setExtensionsPanelVisible(nextExtensionsVisible);
|
|
791
796
|
|
|
792
797
|
if (panel === 'addElement') {
|
|
@@ -795,7 +800,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
795
800
|
setActiveTool('select');
|
|
796
801
|
}
|
|
797
802
|
|
|
798
|
-
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
803
|
+
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextCompareVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
799
804
|
setRightPanelCollapsed(false);
|
|
800
805
|
}
|
|
801
806
|
}, [
|
|
@@ -803,6 +808,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
803
808
|
activeTool,
|
|
804
809
|
bcfPanelVisible,
|
|
805
810
|
clashPanelVisible,
|
|
811
|
+
comparePanelVisible,
|
|
806
812
|
extensionsPanelVisible,
|
|
807
813
|
idsPanelVisible,
|
|
808
814
|
lensPanelVisible,
|
|
@@ -810,6 +816,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
810
816
|
setActiveTool,
|
|
811
817
|
setBcfPanelVisible,
|
|
812
818
|
setClashPanelVisible,
|
|
819
|
+
setComparePanelVisible,
|
|
813
820
|
setExtensionsPanelVisible,
|
|
814
821
|
setIdsPanelVisible,
|
|
815
822
|
setLensPanelVisible,
|
|
@@ -877,6 +884,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
877
884
|
if (idsPanelVisible) panels.add('ids');
|
|
878
885
|
if (lensPanelVisible) panels.add('lens');
|
|
879
886
|
if (clashPanelVisible) panels.add('clash');
|
|
887
|
+
if (comparePanelVisible) panels.add('compare');
|
|
880
888
|
if (extensionsPanelVisible) panels.add('extensions');
|
|
881
889
|
if (activeTool === 'addElement') panels.add('addElement');
|
|
882
890
|
if (analysisExtensionState.activeId) panels.add(analysisExtensionState.activeId);
|
|
@@ -886,6 +894,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
886
894
|
analysisExtensionState.activeId,
|
|
887
895
|
bcfPanelVisible,
|
|
888
896
|
clashPanelVisible,
|
|
897
|
+
comparePanelVisible,
|
|
889
898
|
extensionsPanelVisible,
|
|
890
899
|
ganttPanelVisible,
|
|
891
900
|
idsPanelVisible,
|
|
@@ -904,6 +913,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
904
913
|
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
905
914
|
if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
|
|
906
915
|
if (activeWorkspacePanels.has('clash')) return 'Clash Detection';
|
|
916
|
+
if (activeWorkspacePanels.has('compare')) return 'Compare Models';
|
|
907
917
|
if (activeWorkspacePanels.has('extensions')) return 'Extensions';
|
|
908
918
|
if (activeWorkspacePanels.has('addElement')) return 'Add Element';
|
|
909
919
|
return activeAnalysisExtension?.label ?? 'Analysis';
|
|
@@ -1265,6 +1275,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1265
1275
|
<Crosshair className="h-4 w-4 mr-2" />
|
|
1266
1276
|
Clash Detection
|
|
1267
1277
|
</DropdownMenuCheckboxItem>
|
|
1278
|
+
<DropdownMenuCheckboxItem
|
|
1279
|
+
checked={activeWorkspacePanels.has('compare')}
|
|
1280
|
+
onCheckedChange={() => handleToggleRightPanel('compare')}
|
|
1281
|
+
>
|
|
1282
|
+
<GitCompareArrows className="h-4 w-4 mr-2" />
|
|
1283
|
+
Compare Models
|
|
1284
|
+
</DropdownMenuCheckboxItem>
|
|
1268
1285
|
<DropdownMenuSeparator />
|
|
1269
1286
|
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1270
1287
|
Author
|