@ifc-lite/viewer 1.14.2 → 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 +35 -0
- package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-_vGzMMKs.js} +1 -1
- package/dist/assets/basketViewActivator-BZcoCL3V.js +1 -0
- package/dist/assets/{browser-qSKWrKQW.js → browser-Czmf34bo.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
- package/dist/assets/index-CMQ_Dgkr.css +1 -0
- package/dist/assets/index-D7nEDctQ.js +229 -0
- package/dist/assets/{index-4Y4XaV8N.js → index-DX-Qf5fA.js} +72669 -61673
- package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DAOWftxE.js} +1 -1
- package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-D7jYpn8a.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +21 -20
- package/src/App.tsx +17 -1
- package/src/components/viewer/BasketPresentationDock.tsx +8 -4
- package/src/components/viewer/ChatPanel.tsx +1402 -0
- package/src/components/viewer/CodeEditor.tsx +70 -4
- 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/ScriptPanel.tsx +351 -184
- package/src/components/viewer/UpgradePage.tsx +69 -0
- package/src/components/viewer/Viewport.tsx +23 -0
- package/src/components/viewer/chat/ChatMessage.tsx +144 -0
- package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
- package/src/components/viewer/chat/ModelSelector.tsx +102 -0
- package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
- package/src/components/viewer/chat/renderTextContent.ts +19 -0
- 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/src/hooks/useIfcCache.ts +1 -2
- package/src/hooks/useSandbox.ts +122 -6
- package/src/index.css +10 -0
- package/src/lib/attachments.ts +46 -0
- package/src/lib/llm/ClerkChatSync.tsx +74 -0
- package/src/lib/llm/clerk-auth.ts +62 -0
- package/src/lib/llm/code-extractor.ts +50 -0
- package/src/lib/llm/context-builder.test.ts +18 -0
- package/src/lib/llm/context-builder.ts +305 -0
- package/src/lib/llm/free-models.test.ts +118 -0
- package/src/lib/llm/message-capabilities.test.ts +131 -0
- package/src/lib/llm/message-capabilities.ts +94 -0
- package/src/lib/llm/models.ts +197 -0
- package/src/lib/llm/repair-loop.test.ts +91 -0
- package/src/lib/llm/repair-loop.ts +76 -0
- package/src/lib/llm/script-diagnostics.ts +445 -0
- package/src/lib/llm/script-edit-ops.test.ts +399 -0
- package/src/lib/llm/script-edit-ops.ts +954 -0
- package/src/lib/llm/script-preflight.test.ts +513 -0
- package/src/lib/llm/script-preflight.ts +990 -0
- package/src/lib/llm/script-preservation.test.ts +128 -0
- package/src/lib/llm/script-preservation.ts +152 -0
- package/src/lib/llm/stream-client.test.ts +97 -0
- package/src/lib/llm/stream-client.ts +410 -0
- package/src/lib/llm/system-prompt.test.ts +181 -0
- package/src/lib/llm/system-prompt.ts +665 -0
- package/src/lib/llm/types.ts +150 -0
- package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
- package/src/lib/scripts/templates/create-building.ts +12 -12
- package/src/main.tsx +10 -1
- package/src/sdk/adapters/export-adapter.test.ts +24 -0
- package/src/sdk/adapters/export-adapter.ts +40 -16
- package/src/sdk/adapters/files-adapter.ts +39 -0
- package/src/sdk/adapters/model-compat.ts +1 -1
- package/src/sdk/adapters/mutate-adapter.ts +20 -6
- package/src/sdk/adapters/mutation-view.ts +112 -0
- package/src/sdk/adapters/query-adapter.ts +100 -4
- package/src/sdk/local-backend.ts +4 -0
- package/src/store/index.ts +15 -1
- package/src/store/slices/chatSlice.test.ts +325 -0
- package/src/store/slices/chatSlice.ts +468 -0
- package/src/store/slices/scriptSlice.test.ts +75 -0
- package/src/store/slices/scriptSlice.ts +256 -9
- package/src/vite-env.d.ts +10 -0
- package/vite.config.ts +21 -2
- package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
- package/dist/assets/index-ByrFvN5A.css +0 -1
- package/dist/assets/index-CN7qDq7G.js +0 -216
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
import { useRef, useEffect } from 'react';
|
|
14
14
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, highlightActiveLineGutter, drawSelection } from '@codemirror/view';
|
|
15
|
-
import { EditorState, Compartment } from '@codemirror/state';
|
|
15
|
+
import { EditorState, Compartment, Transaction } from '@codemirror/state';
|
|
16
16
|
import { javascript } from '@codemirror/lang-javascript';
|
|
17
17
|
import { autocompletion, type CompletionContext, type CompletionResult, type Completion } from '@codemirror/autocomplete';
|
|
18
|
-
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
|
18
|
+
import { defaultKeymap, history, historyKeymap, indentWithTab, undo, redo, undoDepth, redoDepth } from '@codemirror/commands';
|
|
19
19
|
import { syntaxHighlighting, defaultHighlightStyle, bracketMatching, indentOnInput } from '@codemirror/language';
|
|
20
20
|
import { highlightSelectionMatches } from '@codemirror/search';
|
|
21
21
|
import { NAMESPACE_SCHEMAS } from '@ifc-lite/sandbox/schema';
|
|
22
|
+
import type { ScriptEditorSelection, ScriptEditorTextChange } from '@/lib/llm/types';
|
|
22
23
|
|
|
23
24
|
/** Shared structural styles (mode-agnostic) */
|
|
24
25
|
const baseTheme = EditorView.theme({
|
|
@@ -201,20 +202,44 @@ function bimCompletions(context: CompletionContext): CompletionResult | null {
|
|
|
201
202
|
interface CodeEditorProps {
|
|
202
203
|
value: string;
|
|
203
204
|
onChange: (value: string) => void;
|
|
205
|
+
onSelectionChange?: (selection: ScriptEditorSelection) => void;
|
|
206
|
+
onHistoryChange?: (canUndo: boolean, canRedo: boolean) => void;
|
|
207
|
+
registerApplyAdapter?: ((adapter: {
|
|
208
|
+
apply: (
|
|
209
|
+
nextContent: string,
|
|
210
|
+
selection: ScriptEditorSelection,
|
|
211
|
+
options?: { userEvent?: string; changes?: ScriptEditorTextChange[] },
|
|
212
|
+
) => void;
|
|
213
|
+
undo: () => void;
|
|
214
|
+
redo: () => void;
|
|
215
|
+
} | null) => void);
|
|
204
216
|
onRun?: () => void;
|
|
205
217
|
onSave?: () => void;
|
|
206
218
|
className?: string;
|
|
207
219
|
}
|
|
208
220
|
|
|
209
|
-
export function CodeEditor({
|
|
221
|
+
export function CodeEditor({
|
|
222
|
+
value,
|
|
223
|
+
onChange,
|
|
224
|
+
onSelectionChange,
|
|
225
|
+
onHistoryChange,
|
|
226
|
+
registerApplyAdapter,
|
|
227
|
+
onRun,
|
|
228
|
+
onSave,
|
|
229
|
+
className,
|
|
230
|
+
}: CodeEditorProps) {
|
|
210
231
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
211
232
|
const viewRef = useRef<EditorView | null>(null);
|
|
212
233
|
const onChangeRef = useRef(onChange);
|
|
234
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
235
|
+
const onHistoryChangeRef = useRef(onHistoryChange);
|
|
213
236
|
const onRunRef = useRef(onRun);
|
|
214
237
|
const onSaveRef = useRef(onSave);
|
|
215
238
|
|
|
216
239
|
// Keep callback refs up to date without recreating the editor
|
|
217
240
|
onChangeRef.current = onChange;
|
|
241
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
242
|
+
onHistoryChangeRef.current = onHistoryChange;
|
|
218
243
|
onRunRef.current = onRun;
|
|
219
244
|
onSaveRef.current = onSave;
|
|
220
245
|
|
|
@@ -239,6 +264,16 @@ export function CodeEditor({ value, onChange, onRun, onSave, className }: CodeEd
|
|
|
239
264
|
if (update.docChanged) {
|
|
240
265
|
onChangeRef.current(update.state.doc.toString());
|
|
241
266
|
}
|
|
267
|
+
if (update.selectionSet || update.docChanged) {
|
|
268
|
+
const main = update.state.selection.main;
|
|
269
|
+
onSelectionChangeRef.current?.({ from: main.from, to: main.to });
|
|
270
|
+
}
|
|
271
|
+
if (update.docChanged) {
|
|
272
|
+
onHistoryChangeRef.current?.(
|
|
273
|
+
undoDepth(update.state) > 0,
|
|
274
|
+
redoDepth(update.state) > 0,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
242
277
|
});
|
|
243
278
|
|
|
244
279
|
const state = EditorState.create({
|
|
@@ -274,6 +309,35 @@ export function CodeEditor({ value, onChange, onRun, onSave, className }: CodeEd
|
|
|
274
309
|
});
|
|
275
310
|
|
|
276
311
|
viewRef.current = view;
|
|
312
|
+
const initialSelection = view.state.selection.main;
|
|
313
|
+
onSelectionChangeRef.current?.({ from: initialSelection.from, to: initialSelection.to });
|
|
314
|
+
onHistoryChangeRef.current?.(undoDepth(view.state) > 0, redoDepth(view.state) > 0);
|
|
315
|
+
registerApplyAdapter?.({
|
|
316
|
+
apply: (nextContent, selection, options) => {
|
|
317
|
+
const active = viewRef.current;
|
|
318
|
+
if (!active) return;
|
|
319
|
+
const safeFrom = Math.max(0, Math.min(selection.from, nextContent.length));
|
|
320
|
+
const safeTo = Math.max(safeFrom, Math.min(selection.to, nextContent.length));
|
|
321
|
+
const changes = options?.changes && options.changes.length > 0
|
|
322
|
+
? options.changes
|
|
323
|
+
: [{ from: 0, to: active.state.doc.length, insert: nextContent }];
|
|
324
|
+
active.dispatch({
|
|
325
|
+
changes,
|
|
326
|
+
selection: { anchor: safeFrom, head: safeTo },
|
|
327
|
+
annotations: options?.userEvent ? [Transaction.userEvent.of(options.userEvent)] : undefined,
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
undo: () => {
|
|
331
|
+
const active = viewRef.current;
|
|
332
|
+
if (!active) return;
|
|
333
|
+
undo(active);
|
|
334
|
+
},
|
|
335
|
+
redo: () => {
|
|
336
|
+
const active = viewRef.current;
|
|
337
|
+
if (!active) return;
|
|
338
|
+
redo(active);
|
|
339
|
+
},
|
|
340
|
+
});
|
|
277
341
|
|
|
278
342
|
// Watch for light/dark mode changes on <html> class
|
|
279
343
|
const observer = new MutationObserver(() => {
|
|
@@ -282,13 +346,15 @@ export function CodeEditor({ value, onChange, onRun, onSave, className }: CodeEd
|
|
|
282
346
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
283
347
|
|
|
284
348
|
return () => {
|
|
349
|
+
onHistoryChangeRef.current?.(false, false);
|
|
350
|
+
registerApplyAdapter?.(null);
|
|
285
351
|
observer.disconnect();
|
|
286
352
|
view.destroy();
|
|
287
353
|
viewRef.current = null;
|
|
288
354
|
};
|
|
289
355
|
// Only create once — value is set via initial doc
|
|
290
356
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
291
|
-
}, []);
|
|
357
|
+
}, [registerApplyAdapter]);
|
|
292
358
|
|
|
293
359
|
// Sync external value changes into the editor (e.g., loading a different script)
|
|
294
360
|
const lastExternalValue = useRef(value);
|
|
@@ -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 script panel (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 ── */}
|