@ifc-lite/viewer 1.0.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 (52) hide show
  1. package/LICENSE +373 -0
  2. package/components.json +22 -0
  3. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  4. package/dist/assets/geometry.worker-DpnHtNr3.ts +157 -0
  5. package/dist/assets/ifc_lite_wasm_bg-Cd3m3f2h.wasm +0 -0
  6. package/dist/assets/index-DKe9Oy-s.css +1 -0
  7. package/dist/assets/index-Dzz3WVwq.js +637 -0
  8. package/dist/ifc_lite_wasm_bg.wasm +0 -0
  9. package/dist/index.html +13 -0
  10. package/dist/web-ifc.wasm +0 -0
  11. package/index.html +12 -0
  12. package/package.json +52 -0
  13. package/postcss.config.js +6 -0
  14. package/public/ifc_lite_wasm_bg.wasm +0 -0
  15. package/public/web-ifc.wasm +0 -0
  16. package/src/App.tsx +13 -0
  17. package/src/components/Viewport.tsx +723 -0
  18. package/src/components/ui/button.tsx +58 -0
  19. package/src/components/ui/collapsible.tsx +11 -0
  20. package/src/components/ui/context-menu.tsx +174 -0
  21. package/src/components/ui/dropdown-menu.tsx +175 -0
  22. package/src/components/ui/input.tsx +49 -0
  23. package/src/components/ui/progress.tsx +26 -0
  24. package/src/components/ui/scroll-area.tsx +47 -0
  25. package/src/components/ui/separator.tsx +27 -0
  26. package/src/components/ui/tabs.tsx +56 -0
  27. package/src/components/ui/tooltip.tsx +31 -0
  28. package/src/components/viewer/AxisHelper.tsx +125 -0
  29. package/src/components/viewer/BoxSelectionOverlay.tsx +53 -0
  30. package/src/components/viewer/EntityContextMenu.tsx +220 -0
  31. package/src/components/viewer/HierarchyPanel.tsx +363 -0
  32. package/src/components/viewer/HoverTooltip.tsx +82 -0
  33. package/src/components/viewer/KeyboardShortcutsDialog.tsx +104 -0
  34. package/src/components/viewer/MainToolbar.tsx +441 -0
  35. package/src/components/viewer/PropertiesPanel.tsx +288 -0
  36. package/src/components/viewer/StatusBar.tsx +141 -0
  37. package/src/components/viewer/ToolOverlays.tsx +311 -0
  38. package/src/components/viewer/ViewCube.tsx +195 -0
  39. package/src/components/viewer/ViewerLayout.tsx +190 -0
  40. package/src/components/viewer/Viewport.tsx +1136 -0
  41. package/src/components/viewer/ViewportContainer.tsx +49 -0
  42. package/src/components/viewer/ViewportOverlays.tsx +185 -0
  43. package/src/hooks/useIfc.ts +168 -0
  44. package/src/hooks/useKeyboardShortcuts.ts +142 -0
  45. package/src/index.css +177 -0
  46. package/src/lib/utils.ts +45 -0
  47. package/src/main.tsx +18 -0
  48. package/src/store.ts +471 -0
  49. package/src/webgpu-types.d.ts +20 -0
  50. package/tailwind.config.js +72 -0
  51. package/tsconfig.json +16 -0
  52. package/vite.config.ts +45 -0
@@ -0,0 +1,441 @@
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 { useRef, useCallback } from 'react';
6
+ import {
7
+ FolderOpen,
8
+ Download,
9
+ MousePointer2,
10
+ Hand,
11
+ Rotate3d,
12
+ PersonStanding,
13
+ Ruler,
14
+ Scissors,
15
+ Eye,
16
+ EyeOff,
17
+ Focus,
18
+ Home,
19
+ Maximize2,
20
+ Grid3x3,
21
+ ArrowUp,
22
+ ArrowDown,
23
+ ArrowLeft,
24
+ ArrowRight,
25
+ Box,
26
+ Sun,
27
+ Moon,
28
+ HelpCircle,
29
+ Loader2,
30
+ Camera,
31
+ Info,
32
+ } from 'lucide-react';
33
+ import { Button } from '@/components/ui/button';
34
+ import { Separator } from '@/components/ui/separator';
35
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
36
+ import {
37
+ DropdownMenu,
38
+ DropdownMenuContent,
39
+ DropdownMenuItem,
40
+ DropdownMenuSeparator,
41
+ DropdownMenuTrigger,
42
+ } from '@/components/ui/dropdown-menu';
43
+ import { Progress } from '@/components/ui/progress';
44
+ import { useViewerStore } from '@/store';
45
+ import { useIfc } from '@/hooks/useIfc';
46
+ import { cn } from '@/lib/utils';
47
+ import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
48
+ import { FileSpreadsheet, FileJson, SquareDashedMousePointer } from 'lucide-react';
49
+
50
+ type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section' | 'boxselect';
51
+
52
+ interface MainToolbarProps {
53
+ onShowShortcuts?: () => void;
54
+ }
55
+
56
+ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainToolbarProps) {
57
+ const fileInputRef = useRef<HTMLInputElement>(null);
58
+ const { loadFile, loading, progress, geometryResult, ifcDataStore } = useIfc();
59
+ const activeTool = useViewerStore((state) => state.activeTool);
60
+ const setActiveTool = useViewerStore((state) => state.setActiveTool);
61
+ const theme = useViewerStore((state) => state.theme);
62
+ const toggleTheme = useViewerStore((state) => state.toggleTheme);
63
+ const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
64
+ const isolateEntity = useViewerStore((state) => state.isolateEntity);
65
+ const hideEntity = useViewerStore((state) => state.hideEntity);
66
+ const showAll = useViewerStore((state) => state.showAll);
67
+ const error = useViewerStore((state) => state.error);
68
+ const cameraCallbacks = useViewerStore((state) => state.cameraCallbacks);
69
+ const hoverTooltipsEnabled = useViewerStore((state) => state.hoverTooltipsEnabled);
70
+ const toggleHoverTooltips = useViewerStore((state) => state.toggleHoverTooltips);
71
+
72
+ const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
73
+ const file = e.target.files?.[0];
74
+ if (file) {
75
+ loadFile(file);
76
+ }
77
+ }, [loadFile]);
78
+
79
+ const handleIsolate = useCallback(() => {
80
+ if (selectedEntityId) {
81
+ isolateEntity(selectedEntityId);
82
+ }
83
+ }, [selectedEntityId, isolateEntity]);
84
+
85
+ const handleHide = useCallback(() => {
86
+ if (selectedEntityId) {
87
+ hideEntity(selectedEntityId);
88
+ }
89
+ }, [selectedEntityId, hideEntity]);
90
+
91
+ const handleExportGLB = useCallback(() => {
92
+ if (!geometryResult) return;
93
+ try {
94
+ const exporter = new GLTFExporter(geometryResult);
95
+ const glb = exporter.exportGLB({ includeMetadata: true });
96
+ // Create a new Uint8Array from the buffer to ensure correct typing
97
+ const blob = new Blob([new Uint8Array(glb)], { type: 'model/gltf-binary' });
98
+ const url = URL.createObjectURL(blob);
99
+ const a = document.createElement('a');
100
+ a.href = url;
101
+ a.download = 'model.glb';
102
+ a.click();
103
+ URL.revokeObjectURL(url);
104
+ } catch (err) {
105
+ console.error('Export failed:', err);
106
+ }
107
+ }, [geometryResult]);
108
+
109
+ const handleScreenshot = useCallback(() => {
110
+ const canvas = document.querySelector('canvas');
111
+ if (!canvas) return;
112
+ try {
113
+ const dataUrl = canvas.toDataURL('image/png');
114
+ const a = document.createElement('a');
115
+ a.href = dataUrl;
116
+ a.download = 'screenshot.png';
117
+ a.click();
118
+ } catch (err) {
119
+ console.error('Screenshot failed:', err);
120
+ }
121
+ }, []);
122
+
123
+ const handleExportCSV = useCallback((type: 'entities' | 'properties' | 'quantities') => {
124
+ if (!ifcDataStore) return;
125
+ try {
126
+ const exporter = new CSVExporter(ifcDataStore);
127
+ let csv: string;
128
+ let filename: string;
129
+
130
+ switch (type) {
131
+ case 'entities':
132
+ csv = exporter.exportEntities(undefined, { includeProperties: true, flattenProperties: true });
133
+ filename = 'entities.csv';
134
+ break;
135
+ case 'properties':
136
+ csv = exporter.exportProperties();
137
+ filename = 'properties.csv';
138
+ break;
139
+ case 'quantities':
140
+ csv = exporter.exportQuantities();
141
+ filename = 'quantities.csv';
142
+ break;
143
+ }
144
+
145
+ const blob = new Blob([csv], { type: 'text/csv' });
146
+ const url = URL.createObjectURL(blob);
147
+ const a = document.createElement('a');
148
+ a.href = url;
149
+ a.download = filename;
150
+ a.click();
151
+ URL.revokeObjectURL(url);
152
+ } catch (err) {
153
+ console.error('CSV export failed:', err);
154
+ }
155
+ }, [ifcDataStore]);
156
+
157
+ const handleExportJSON = useCallback(() => {
158
+ if (!ifcDataStore) return;
159
+ try {
160
+ // Export basic JSON structure of entities
161
+ const entities: Record<string, unknown>[] = [];
162
+ for (let i = 0; i < ifcDataStore.entities.count; i++) {
163
+ const id = ifcDataStore.entities.expressId[i];
164
+ entities.push({
165
+ expressId: id,
166
+ globalId: ifcDataStore.entities.getGlobalId(id),
167
+ name: ifcDataStore.entities.getName(id),
168
+ type: ifcDataStore.entities.getTypeName(id),
169
+ properties: ifcDataStore.properties.getForEntity(id),
170
+ });
171
+ }
172
+
173
+ const json = JSON.stringify({ entities }, null, 2);
174
+ const blob = new Blob([json], { type: 'application/json' });
175
+ const url = URL.createObjectURL(blob);
176
+ const a = document.createElement('a');
177
+ a.href = url;
178
+ a.download = 'model-data.json';
179
+ a.click();
180
+ URL.revokeObjectURL(url);
181
+ } catch (err) {
182
+ console.error('JSON export failed:', err);
183
+ }
184
+ }, [ifcDataStore]);
185
+
186
+ const ToolButton = ({
187
+ tool,
188
+ icon: Icon,
189
+ label,
190
+ shortcut
191
+ }: {
192
+ tool: Tool;
193
+ icon: React.ElementType;
194
+ label: string;
195
+ shortcut?: string;
196
+ }) => (
197
+ <Tooltip>
198
+ <TooltipTrigger asChild>
199
+ <Button
200
+ variant={activeTool === tool ? 'default' : 'ghost'}
201
+ size="icon-sm"
202
+ onClick={() => setActiveTool(tool)}
203
+ className={cn(activeTool === tool && 'bg-primary text-primary-foreground')}
204
+ >
205
+ <Icon className="h-4 w-4" />
206
+ </Button>
207
+ </TooltipTrigger>
208
+ <TooltipContent>
209
+ {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
210
+ </TooltipContent>
211
+ </Tooltip>
212
+ );
213
+
214
+ const ActionButton = ({
215
+ icon: Icon,
216
+ label,
217
+ onClick,
218
+ shortcut,
219
+ disabled
220
+ }: {
221
+ icon: React.ElementType;
222
+ label: string;
223
+ onClick: () => void;
224
+ shortcut?: string;
225
+ disabled?: boolean;
226
+ }) => (
227
+ <Tooltip>
228
+ <TooltipTrigger asChild>
229
+ <Button
230
+ variant="ghost"
231
+ size="icon-sm"
232
+ onClick={onClick}
233
+ disabled={disabled}
234
+ >
235
+ <Icon className="h-4 w-4" />
236
+ </Button>
237
+ </TooltipTrigger>
238
+ <TooltipContent>
239
+ {label} {shortcut && <span className="ml-2 text-xs opacity-60">({shortcut})</span>}
240
+ </TooltipContent>
241
+ </Tooltip>
242
+ );
243
+
244
+ return (
245
+ <div className="flex items-center gap-1 px-2 h-12 border-b bg-card">
246
+ {/* File Operations */}
247
+ <input
248
+ ref={fileInputRef}
249
+ type="file"
250
+ accept=".ifc"
251
+ onChange={handleFileSelect}
252
+ className="hidden"
253
+ />
254
+
255
+ <Tooltip>
256
+ <TooltipTrigger asChild>
257
+ <Button
258
+ variant="ghost"
259
+ size="icon-sm"
260
+ onClick={() => fileInputRef.current?.click()}
261
+ disabled={loading}
262
+ >
263
+ {loading ? (
264
+ <Loader2 className="h-4 w-4 animate-spin" />
265
+ ) : (
266
+ <FolderOpen className="h-4 w-4" />
267
+ )}
268
+ </Button>
269
+ </TooltipTrigger>
270
+ <TooltipContent>Open IFC File</TooltipContent>
271
+ </Tooltip>
272
+
273
+ <DropdownMenu>
274
+ <DropdownMenuTrigger asChild>
275
+ <Button variant="ghost" size="icon-sm" disabled={!geometryResult}>
276
+ <Download className="h-4 w-4" />
277
+ </Button>
278
+ </DropdownMenuTrigger>
279
+ <DropdownMenuContent>
280
+ <DropdownMenuItem onClick={handleExportGLB}>
281
+ <Download className="h-4 w-4 mr-2" />
282
+ Export GLB (3D Model)
283
+ </DropdownMenuItem>
284
+ <DropdownMenuSeparator />
285
+ <DropdownMenuItem onClick={() => handleExportCSV('entities')} disabled={!ifcDataStore}>
286
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
287
+ Export Entities (CSV)
288
+ </DropdownMenuItem>
289
+ <DropdownMenuItem onClick={() => handleExportCSV('properties')} disabled={!ifcDataStore}>
290
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
291
+ Export Properties (CSV)
292
+ </DropdownMenuItem>
293
+ <DropdownMenuItem onClick={() => handleExportCSV('quantities')} disabled={!ifcDataStore}>
294
+ <FileSpreadsheet className="h-4 w-4 mr-2" />
295
+ Export Quantities (CSV)
296
+ </DropdownMenuItem>
297
+ <DropdownMenuItem onClick={handleExportJSON} disabled={!ifcDataStore}>
298
+ <FileJson className="h-4 w-4 mr-2" />
299
+ Export JSON (All Data)
300
+ </DropdownMenuItem>
301
+ <DropdownMenuSeparator />
302
+ <DropdownMenuItem onClick={handleScreenshot}>
303
+ <Camera className="h-4 w-4 mr-2" />
304
+ Screenshot
305
+ </DropdownMenuItem>
306
+ </DropdownMenuContent>
307
+ </DropdownMenu>
308
+
309
+ <Separator orientation="vertical" className="h-6 mx-1" />
310
+
311
+ {/* Navigation Tools */}
312
+ <ToolButton tool="select" icon={MousePointer2} label="Select" shortcut="V" />
313
+ <ToolButton tool="boxselect" icon={SquareDashedMousePointer} label="Box Select" shortcut="B" />
314
+ <ToolButton tool="pan" icon={Hand} label="Pan" shortcut="P" />
315
+ <ToolButton tool="orbit" icon={Rotate3d} label="Orbit" shortcut="O" />
316
+ <ToolButton tool="walk" icon={PersonStanding} label="Walk Mode" shortcut="C" />
317
+
318
+ <Separator orientation="vertical" className="h-6 mx-1" />
319
+
320
+ {/* Measurement & Section */}
321
+ <ToolButton tool="measure" icon={Ruler} label="Measure" shortcut="M" />
322
+ <ToolButton tool="section" icon={Scissors} label="Section" shortcut="X" />
323
+
324
+ <Separator orientation="vertical" className="h-6 mx-1" />
325
+
326
+ {/* Visibility */}
327
+ <ActionButton icon={Focus} label="Isolate Selection" onClick={handleIsolate} shortcut="I" disabled={!selectedEntityId} />
328
+ <ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del" disabled={!selectedEntityId} />
329
+ <ActionButton icon={Eye} label="Show All" onClick={showAll} shortcut="A" />
330
+
331
+ <Separator orientation="vertical" className="h-6 mx-1" />
332
+
333
+ {/* Display Options */}
334
+ <Tooltip>
335
+ <TooltipTrigger asChild>
336
+ <Button
337
+ variant={hoverTooltipsEnabled ? 'default' : 'ghost'}
338
+ size="icon-sm"
339
+ onClick={toggleHoverTooltips}
340
+ className={cn(hoverTooltipsEnabled && 'bg-primary text-primary-foreground')}
341
+ >
342
+ <Info className="h-4 w-4" />
343
+ </Button>
344
+ </TooltipTrigger>
345
+ <TooltipContent>
346
+ {hoverTooltipsEnabled ? 'Disable' : 'Enable'} Hover Tooltips
347
+ </TooltipContent>
348
+ </Tooltip>
349
+
350
+ <Separator orientation="vertical" className="h-6 mx-1" />
351
+
352
+ {/* Camera */}
353
+ <ActionButton icon={Home} label="Home (Isometric)" onClick={() => cameraCallbacks.home?.()} shortcut="H" />
354
+ <ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
355
+ <ActionButton
356
+ icon={Focus}
357
+ label="Frame Selection"
358
+ onClick={() => cameraCallbacks.frameSelection?.()}
359
+ shortcut="F"
360
+ disabled={!selectedEntityId}
361
+ />
362
+
363
+ <DropdownMenu>
364
+ <Tooltip>
365
+ <TooltipTrigger asChild>
366
+ <DropdownMenuTrigger asChild>
367
+ <Button variant="ghost" size="icon-sm">
368
+ <Grid3x3 className="h-4 w-4" />
369
+ </Button>
370
+ </DropdownMenuTrigger>
371
+ </TooltipTrigger>
372
+ <TooltipContent>Preset Views (0-6)</TooltipContent>
373
+ </Tooltip>
374
+ <DropdownMenuContent>
375
+ <DropdownMenuItem onClick={() => cameraCallbacks.home?.()}>
376
+ <Box className="h-4 w-4 mr-2" /> Isometric <span className="ml-auto text-xs opacity-60">0</span>
377
+ </DropdownMenuItem>
378
+ <DropdownMenuSeparator />
379
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('top')}>
380
+ <ArrowUp className="h-4 w-4 mr-2" /> Top <span className="ml-auto text-xs opacity-60">1</span>
381
+ </DropdownMenuItem>
382
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('bottom')}>
383
+ <ArrowDown className="h-4 w-4 mr-2" /> Bottom <span className="ml-auto text-xs opacity-60">2</span>
384
+ </DropdownMenuItem>
385
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('front')}>
386
+ <ArrowRight className="h-4 w-4 mr-2" /> Front <span className="ml-auto text-xs opacity-60">3</span>
387
+ </DropdownMenuItem>
388
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('back')}>
389
+ <ArrowLeft className="h-4 w-4 mr-2" /> Back <span className="ml-auto text-xs opacity-60">4</span>
390
+ </DropdownMenuItem>
391
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('left')}>
392
+ <ArrowLeft className="h-4 w-4 mr-2" /> Left <span className="ml-auto text-xs opacity-60">5</span>
393
+ </DropdownMenuItem>
394
+ <DropdownMenuItem onClick={() => cameraCallbacks.setPresetView?.('right')}>
395
+ <ArrowRight className="h-4 w-4 mr-2" /> Right <span className="ml-auto text-xs opacity-60">6</span>
396
+ </DropdownMenuItem>
397
+ </DropdownMenuContent>
398
+ </DropdownMenu>
399
+
400
+ {/* Spacer */}
401
+ <div className="flex-1" />
402
+
403
+ {/* Loading Progress */}
404
+ {loading && progress && (
405
+ <div className="flex items-center gap-2 mr-4">
406
+ <span className="text-xs text-muted-foreground">{progress.phase}</span>
407
+ <Progress value={progress.percent} className="w-32 h-2" />
408
+ <span className="text-xs text-muted-foreground">{Math.round(progress.percent)}%</span>
409
+ </div>
410
+ )}
411
+
412
+ {/* Error Display */}
413
+ {error && (
414
+ <span className="text-xs text-destructive mr-4">{error}</span>
415
+ )}
416
+
417
+ {/* Right Side Actions */}
418
+ <Tooltip>
419
+ <TooltipTrigger asChild>
420
+ <Button variant="ghost" size="icon-sm" onClick={toggleTheme}>
421
+ {theme === 'dark' ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
422
+ </Button>
423
+ </TooltipTrigger>
424
+ <TooltipContent>Toggle Theme</TooltipContent>
425
+ </Tooltip>
426
+
427
+ <Tooltip>
428
+ <TooltipTrigger asChild>
429
+ <Button
430
+ variant="ghost"
431
+ size="icon-sm"
432
+ onClick={() => onShowShortcuts?.()}
433
+ >
434
+ <HelpCircle className="h-4 w-4" />
435
+ </Button>
436
+ </TooltipTrigger>
437
+ <TooltipContent>Keyboard Shortcuts (?)</TooltipContent>
438
+ </Tooltip>
439
+ </div>
440
+ );
441
+ }