@ifc-lite/viewer 1.7.0 → 1.9.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.
Files changed (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -0,0 +1,416 @@
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
+ * ScriptPanel — Code editor + output console for BIM scripting.
7
+ *
8
+ * Uses CodeMirror 6 for the code editor with bim.* autocomplete.
9
+ * Connects to the QuickJS sandbox via useSandbox() and displays results
10
+ * in a log console.
11
+ */
12
+
13
+ import { useCallback, useMemo, useState, memo } from 'react';
14
+ import {
15
+ Play,
16
+ Save,
17
+ Plus,
18
+ Trash2,
19
+ X,
20
+ ChevronDown,
21
+ FileCode2,
22
+ RotateCcw,
23
+ AlertCircle,
24
+ CheckCircle2,
25
+ Info,
26
+ AlertTriangle,
27
+ } from 'lucide-react';
28
+ import { Button } from '@/components/ui/button';
29
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
30
+ import {
31
+ DropdownMenu,
32
+ DropdownMenuContent,
33
+ DropdownMenuItem,
34
+ DropdownMenuSeparator,
35
+ DropdownMenuTrigger,
36
+ } from '@/components/ui/dropdown-menu';
37
+ import {
38
+ Dialog,
39
+ DialogContent,
40
+ DialogHeader,
41
+ DialogFooter,
42
+ DialogTitle,
43
+ DialogDescription,
44
+ } from '@/components/ui/dialog';
45
+ import { ScrollArea } from '@/components/ui/scroll-area';
46
+ import { cn, formatDuration } from '@/lib/utils';
47
+ import { useViewerStore } from '@/store';
48
+ import { useSandbox } from '@/hooks/useSandbox';
49
+ import { SCRIPT_TEMPLATES } from '@/lib/scripts/templates';
50
+ import { CodeEditor } from './CodeEditor';
51
+ import type { LogEntry } from '@/store/slices/scriptSlice';
52
+
53
+ interface ScriptPanelProps {
54
+ onClose?: () => void;
55
+ }
56
+
57
+ /** Consolidated script state selector — single subscription instead of 14 */
58
+ function useScriptState() {
59
+ const editorContent = useViewerStore((s) => s.scriptEditorContent);
60
+ const setEditorContent = useViewerStore((s) => s.setScriptEditorContent);
61
+ const executionState = useViewerStore((s) => s.scriptExecutionState);
62
+ const lastResult = useViewerStore((s) => s.scriptLastResult);
63
+ const lastError = useViewerStore((s) => s.scriptLastError);
64
+ const savedScripts = useViewerStore((s) => s.savedScripts);
65
+ const activeScriptId = useViewerStore((s) => s.activeScriptId);
66
+ const editorDirty = useViewerStore((s) => s.scriptEditorDirty);
67
+ const createScript = useViewerStore((s) => s.createScript);
68
+ const saveActiveScript = useViewerStore((s) => s.saveActiveScript);
69
+ const deleteScript = useViewerStore((s) => s.deleteScript);
70
+ const setActiveScriptId = useViewerStore((s) => s.setActiveScriptId);
71
+ const deleteConfirmId = useViewerStore((s) => s.scriptDeleteConfirmId);
72
+ const setDeleteConfirmId = useViewerStore((s) => s.setScriptDeleteConfirmId);
73
+
74
+ return {
75
+ editorContent,
76
+ setEditorContent,
77
+ executionState,
78
+ lastResult,
79
+ lastError,
80
+ savedScripts,
81
+ activeScriptId,
82
+ editorDirty,
83
+ createScript,
84
+ saveActiveScript,
85
+ deleteScript,
86
+ setActiveScriptId,
87
+ deleteConfirmId,
88
+ setDeleteConfirmId,
89
+ };
90
+ }
91
+
92
+ export function ScriptPanel({ onClose }: ScriptPanelProps) {
93
+ const {
94
+ editorContent,
95
+ setEditorContent,
96
+ executionState,
97
+ lastResult,
98
+ lastError,
99
+ savedScripts,
100
+ activeScriptId,
101
+ editorDirty,
102
+ createScript,
103
+ saveActiveScript,
104
+ deleteScript,
105
+ setActiveScriptId,
106
+ deleteConfirmId,
107
+ setDeleteConfirmId,
108
+ } = useScriptState();
109
+
110
+ const { execute, reset } = useSandbox();
111
+ const [outputCollapsed, setOutputCollapsed] = useState(false);
112
+
113
+ const activeScript = useMemo(
114
+ () => savedScripts.find((s) => s.id === activeScriptId),
115
+ [savedScripts, activeScriptId],
116
+ );
117
+
118
+ const deleteConfirmScript = useMemo(
119
+ () => (deleteConfirmId ? savedScripts.find((s) => s.id === deleteConfirmId) : null),
120
+ [savedScripts, deleteConfirmId],
121
+ );
122
+
123
+ const handleRun = useCallback(async () => {
124
+ if (executionState === 'running') return;
125
+ await execute(editorContent);
126
+ }, [execute, editorContent, executionState]);
127
+
128
+ const handleSave = useCallback(() => {
129
+ if (activeScriptId) {
130
+ saveActiveScript();
131
+ } else {
132
+ createScript('Untitled Script');
133
+ }
134
+ }, [activeScriptId, saveActiveScript, createScript]);
135
+
136
+ const handleNew = useCallback((name: string, code?: string) => {
137
+ createScript(name, code);
138
+ }, [createScript]);
139
+
140
+ const handleDeleteConfirm = useCallback(() => {
141
+ if (deleteConfirmId) {
142
+ deleteScript(deleteConfirmId);
143
+ }
144
+ }, [deleteConfirmId, deleteScript]);
145
+
146
+ return (
147
+ <div className="h-full flex flex-col bg-background">
148
+ {/* Header */}
149
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b shrink-0">
150
+ <FileCode2 className="h-4 w-4 text-muted-foreground shrink-0" />
151
+ <span className="text-sm font-medium truncate">
152
+ {activeScript ? activeScript.name : 'Script Editor'}
153
+ {editorDirty && <span className="text-muted-foreground ml-1">*</span>}
154
+ </span>
155
+ <div className="flex-1" />
156
+
157
+ {/* Script selector dropdown */}
158
+ {savedScripts.length > 0 && (
159
+ <DropdownMenu>
160
+ <DropdownMenuTrigger asChild>
161
+ <Button variant="ghost" size="icon-xs">
162
+ <ChevronDown className="h-3.5 w-3.5" />
163
+ </Button>
164
+ </DropdownMenuTrigger>
165
+ <DropdownMenuContent align="end">
166
+ {savedScripts.map((s) => (
167
+ <DropdownMenuItem
168
+ key={s.id}
169
+ onClick={() => setActiveScriptId(s.id)}
170
+ className={cn(s.id === activeScriptId && 'bg-accent')}
171
+ >
172
+ <FileCode2 className="h-3.5 w-3.5 mr-2" />
173
+ {s.name}
174
+ </DropdownMenuItem>
175
+ ))}
176
+ <DropdownMenuSeparator />
177
+ {activeScriptId && (
178
+ <DropdownMenuItem
179
+ onClick={() => setDeleteConfirmId(activeScriptId)}
180
+ className="text-destructive"
181
+ >
182
+ <Trash2 className="h-3.5 w-3.5 mr-2" />
183
+ Delete
184
+ </DropdownMenuItem>
185
+ )}
186
+ </DropdownMenuContent>
187
+ </DropdownMenu>
188
+ )}
189
+
190
+ {onClose && (
191
+ <Button variant="ghost" size="icon-xs" onClick={onClose}>
192
+ <X className="h-3.5 w-3.5" />
193
+ </Button>
194
+ )}
195
+ </div>
196
+
197
+ {/* Toolbar */}
198
+ <div className="flex items-center gap-1 px-2 py-1 border-b shrink-0">
199
+ <Tooltip>
200
+ <TooltipTrigger asChild>
201
+ <Button
202
+ variant="default"
203
+ size="sm"
204
+ onClick={handleRun}
205
+ disabled={executionState === 'running'}
206
+ className="gap-1"
207
+ >
208
+ <Play className="h-3.5 w-3.5" />
209
+ Run
210
+ </Button>
211
+ </TooltipTrigger>
212
+ <TooltipContent>Run script (Ctrl+Enter)</TooltipContent>
213
+ </Tooltip>
214
+
215
+ <Tooltip>
216
+ <TooltipTrigger asChild>
217
+ <Button variant="ghost" size="icon-xs" onClick={handleSave}>
218
+ <Save className="h-3.5 w-3.5" />
219
+ </Button>
220
+ </TooltipTrigger>
221
+ <TooltipContent>Save (Ctrl+S)</TooltipContent>
222
+ </Tooltip>
223
+
224
+ {/* New script dropdown with templates */}
225
+ <DropdownMenu>
226
+ <Tooltip>
227
+ <TooltipTrigger asChild>
228
+ <DropdownMenuTrigger asChild>
229
+ <Button variant="ghost" size="icon-xs">
230
+ <Plus className="h-3.5 w-3.5" />
231
+ </Button>
232
+ </DropdownMenuTrigger>
233
+ </TooltipTrigger>
234
+ <TooltipContent>New script</TooltipContent>
235
+ </Tooltip>
236
+ <DropdownMenuContent align="start">
237
+ <DropdownMenuItem onClick={() => handleNew('Untitled Script')}>
238
+ <FileCode2 className="h-3.5 w-3.5 mr-2" />
239
+ Blank Script
240
+ </DropdownMenuItem>
241
+ <DropdownMenuSeparator />
242
+ {SCRIPT_TEMPLATES.map((t) => (
243
+ <DropdownMenuItem key={t.name} onClick={() => handleNew(t.name, t.code)}>
244
+ <FileCode2 className="h-3.5 w-3.5 mr-2" />
245
+ {t.name}
246
+ </DropdownMenuItem>
247
+ ))}
248
+ </DropdownMenuContent>
249
+ </DropdownMenu>
250
+
251
+ <Tooltip>
252
+ <TooltipTrigger asChild>
253
+ <Button variant="ghost" size="icon-xs" onClick={reset}>
254
+ <RotateCcw className="h-3.5 w-3.5" />
255
+ </Button>
256
+ </TooltipTrigger>
257
+ <TooltipContent>Reset sandbox</TooltipContent>
258
+ </Tooltip>
259
+
260
+ {/* Status indicator */}
261
+ <div className="flex-1" />
262
+ {executionState === 'running' && (
263
+ <span className="text-xs text-muted-foreground animate-pulse">Running...</span>
264
+ )}
265
+ {executionState === 'success' && lastResult && (
266
+ <span className="text-xs text-green-600 dark:text-green-400 flex items-center gap-1">
267
+ <CheckCircle2 className="h-3 w-3" />
268
+ {formatDuration(lastResult.durationMs)}
269
+ </span>
270
+ )}
271
+ {executionState === 'error' && (
272
+ <span className="text-xs text-destructive flex items-center gap-1">
273
+ <AlertCircle className="h-3 w-3" />
274
+ Error
275
+ </span>
276
+ )}
277
+ </div>
278
+
279
+ {/* Code Editor */}
280
+ <div className="flex-1 min-h-0 overflow-hidden">
281
+ <CodeEditor
282
+ value={editorContent}
283
+ onChange={setEditorContent}
284
+ onRun={handleRun}
285
+ onSave={handleSave}
286
+ className="h-full"
287
+ />
288
+ </div>
289
+
290
+ {/* Output Console */}
291
+ <div className="shrink-0 border-t">
292
+ {/* Output header */}
293
+ <button
294
+ className="flex items-center gap-1.5 px-2 py-1 w-full hover:bg-muted/50 transition-colors text-left"
295
+ onClick={() => setOutputCollapsed(!outputCollapsed)}
296
+ >
297
+ <ChevronDown
298
+ className={cn('h-3 w-3 transition-transform', outputCollapsed && '-rotate-90')}
299
+ />
300
+ <span className="text-xs font-medium text-muted-foreground">Output</span>
301
+ {lastResult && lastResult.logs.length > 0 && (
302
+ <span className="text-xs text-muted-foreground">({lastResult.logs.length})</span>
303
+ )}
304
+ </button>
305
+
306
+ {!outputCollapsed && (
307
+ <ScrollArea className="h-[140px]">
308
+ <div className="px-2 pb-2 font-mono text-xs space-y-0.5">
309
+ {/* Error message */}
310
+ {lastError && (
311
+ <div className="flex items-start gap-1.5 text-destructive">
312
+ <AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
313
+ <span className="whitespace-pre-wrap break-all">{lastError}</span>
314
+ </div>
315
+ )}
316
+
317
+ {/* Log entries */}
318
+ {lastResult?.logs.map((log, i) => (
319
+ <MemoizedLogLine key={i} log={log} />
320
+ ))}
321
+
322
+ {/* Return value */}
323
+ {lastResult && lastResult.value !== undefined && lastResult.value !== null && (
324
+ <div className="text-muted-foreground mt-1 pt-1 border-t border-border/50">
325
+ <span className="opacity-60">Return: </span>
326
+ <span className="text-foreground">
327
+ {typeof lastResult.value === 'object'
328
+ ? JSON.stringify(lastResult.value, null, 2)
329
+ : String(lastResult.value)}
330
+ </span>
331
+ </div>
332
+ )}
333
+
334
+ {/* Empty state */}
335
+ {!lastError && !lastResult && (
336
+ <div className="text-muted-foreground py-2 text-center">
337
+ Press Run or Ctrl+Enter to execute
338
+ </div>
339
+ )}
340
+ </div>
341
+ </ScrollArea>
342
+ )}
343
+ </div>
344
+
345
+ {/* Delete confirmation dialog */}
346
+ <Dialog open={deleteConfirmId !== null} onOpenChange={(open) => { if (!open) setDeleteConfirmId(null); }}>
347
+ <DialogContent className="sm:max-w-[400px]">
348
+ <DialogHeader>
349
+ <DialogTitle>Delete Script</DialogTitle>
350
+ <DialogDescription>
351
+ Are you sure you want to delete &ldquo;{deleteConfirmScript?.name ?? 'this script'}&rdquo;?
352
+ This action cannot be undone.
353
+ </DialogDescription>
354
+ </DialogHeader>
355
+ <DialogFooter>
356
+ <Button variant="ghost" onClick={() => setDeleteConfirmId(null)}>
357
+ Cancel
358
+ </Button>
359
+ <Button variant="destructive" onClick={handleDeleteConfirm}>
360
+ Delete
361
+ </Button>
362
+ </DialogFooter>
363
+ </DialogContent>
364
+ </Dialog>
365
+ </div>
366
+ );
367
+ }
368
+
369
+ /** Format a log entry's args into a display string */
370
+ function formatLogArgs(args: unknown[]): string {
371
+ return args.map((a) => {
372
+ if (typeof a === 'object' && a !== null) {
373
+ try {
374
+ return JSON.stringify(a, null, 2);
375
+ } catch {
376
+ return String(a);
377
+ }
378
+ }
379
+ return String(a);
380
+ }).join(' ');
381
+ }
382
+
383
+ /** Render a single log entry with appropriate icon and color — memoized */
384
+ const MemoizedLogLine = memo(function LogLine({ log }: { log: LogEntry }) {
385
+ const formatted = useMemo(() => formatLogArgs(log.args), [log.args]);
386
+
387
+ switch (log.level) {
388
+ case 'error':
389
+ return (
390
+ <div className="flex items-start gap-1.5 text-destructive">
391
+ <AlertCircle className="h-3 w-3 mt-0.5 shrink-0" />
392
+ <span className="whitespace-pre-wrap break-all">{formatted}</span>
393
+ </div>
394
+ );
395
+ case 'warn':
396
+ return (
397
+ <div className="flex items-start gap-1.5 text-yellow-600 dark:text-yellow-400">
398
+ <AlertTriangle className="h-3 w-3 mt-0.5 shrink-0" />
399
+ <span className="whitespace-pre-wrap break-all">{formatted}</span>
400
+ </div>
401
+ );
402
+ case 'info':
403
+ return (
404
+ <div className="flex items-start gap-1.5 text-blue-600 dark:text-blue-400">
405
+ <Info className="h-3 w-3 mt-0.5 shrink-0" />
406
+ <span className="whitespace-pre-wrap break-all">{formatted}</span>
407
+ </div>
408
+ );
409
+ default:
410
+ return (
411
+ <div className="flex items-start gap-1.5">
412
+ <span className="whitespace-pre-wrap break-all">{formatted}</span>
413
+ </div>
414
+ );
415
+ }
416
+ });