@ifc-lite/viewer 1.1.6 → 1.5.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 (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-Dgd6vzw_.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
  15. package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
@@ -0,0 +1,502 @@
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
+ * SheetSetupPanel - Configure drawing sheet for architectural output
7
+ *
8
+ * Provides controls for:
9
+ * - Paper size selection (ISO, ANSI, ARCH)
10
+ * - Drawing frame style
11
+ * - Scale selection
12
+ * - Title block configuration
13
+ * - Scale bar and north arrow
14
+ */
15
+
16
+ import React, { useCallback, useState, useMemo } from 'react';
17
+ import {
18
+ X,
19
+ FileText,
20
+ ChevronDown,
21
+ ChevronRight,
22
+ Ruler,
23
+ Compass,
24
+ Edit3,
25
+ Save,
26
+ Trash2,
27
+ Plus,
28
+ } from 'lucide-react';
29
+ import { Button } from '@/components/ui/button';
30
+ import { Label } from '@/components/ui/label';
31
+ import { Switch } from '@/components/ui/switch';
32
+ import { Input } from '@/components/ui/input';
33
+ import {
34
+ Select,
35
+ SelectContent,
36
+ SelectItem,
37
+ SelectTrigger,
38
+ SelectValue,
39
+ } from '@/components/ui/select';
40
+ import {
41
+ Collapsible,
42
+ CollapsibleContent,
43
+ CollapsibleTrigger,
44
+ } from '@/components/ui/collapsible';
45
+ import { useViewerStore } from '@/store';
46
+ import {
47
+ PAPER_SIZE_REGISTRY,
48
+ FRAME_PRESETS,
49
+ TITLE_BLOCK_PRESETS,
50
+ COMMON_SCALES,
51
+ type FrameStyle,
52
+ type TitleBlockLayout,
53
+ type DrawingScale,
54
+ } from '@ifc-lite/drawing-2d';
55
+
56
+ interface SheetSetupPanelProps {
57
+ onClose: () => void;
58
+ onOpenTitleBlockEditor?: () => void;
59
+ }
60
+
61
+ // Group paper sizes by category
62
+ const PAPER_SIZE_GROUPS = {
63
+ ISO: ['A0_PORTRAIT', 'A0_LANDSCAPE', 'A1_PORTRAIT', 'A1_LANDSCAPE', 'A2_PORTRAIT', 'A2_LANDSCAPE', 'A3_PORTRAIT', 'A3_LANDSCAPE', 'A4_PORTRAIT', 'A4_LANDSCAPE'],
64
+ ANSI: ['LETTER_PORTRAIT', 'LETTER_LANDSCAPE', 'LEGAL_PORTRAIT', 'LEGAL_LANDSCAPE', 'TABLOID_PORTRAIT', 'TABLOID_LANDSCAPE', 'ANSI_C', 'ANSI_D', 'ANSI_E'],
65
+ ARCH: ['ARCH_A', 'ARCH_B', 'ARCH_C', 'ARCH_D', 'ARCH_E', 'ARCH_E1'],
66
+ };
67
+
68
+ const FRAME_STYLE_OPTIONS: { value: FrameStyle; label: string }[] = [
69
+ { value: 'simple', label: 'Simple' },
70
+ { value: 'professional', label: 'Professional' },
71
+ { value: 'minimal', label: 'Minimal' },
72
+ { value: 'iso', label: 'ISO Standard' },
73
+ ];
74
+
75
+ const TITLE_BLOCK_LAYOUT_OPTIONS: { value: TitleBlockLayout; label: string }[] = [
76
+ { value: 'standard', label: 'Standard (Bottom Right)' },
77
+ { value: 'extended', label: 'Extended (Full Width)' },
78
+ { value: 'compact', label: 'Compact (Smaller)' },
79
+ ];
80
+
81
+ export function SheetSetupPanel({ onClose, onOpenTitleBlockEditor }: SheetSetupPanelProps): React.ReactElement {
82
+ const activeSheet = useViewerStore((s) => s.activeSheet);
83
+ const sheetEnabled = useViewerStore((s) => s.sheetEnabled);
84
+ const setSheetEnabled = useViewerStore((s) => s.setSheetEnabled);
85
+ const createSheet = useViewerStore((s) => s.createSheet);
86
+ const setPaperSize = useViewerStore((s) => s.setPaperSize);
87
+ const setFrameStyle = useViewerStore((s) => s.setFrameStyle);
88
+ const setDrawingScale = useViewerStore((s) => s.setDrawingScale);
89
+ const setTitleBlockLayout = useViewerStore((s) => s.setTitleBlockLayout);
90
+ const toggleScaleBar = useViewerStore((s) => s.toggleScaleBar);
91
+ const toggleNorthArrow = useViewerStore((s) => s.toggleNorthArrow);
92
+ const savedSheetTemplates = useViewerStore((s) => s.savedSheetTemplates);
93
+ const saveAsTemplate = useViewerStore((s) => s.saveAsTemplate);
94
+ const loadTemplate = useViewerStore((s) => s.loadTemplate);
95
+ const deleteTemplate = useViewerStore((s) => s.deleteTemplate);
96
+
97
+ // Section state
98
+ const [paperSizeOpen, setPaperSizeOpen] = useState(true);
99
+ const [frameOpen, setFrameOpen] = useState(true);
100
+ const [scaleOpen, setScaleOpen] = useState(true);
101
+ const [titleBlockOpen, setTitleBlockOpen] = useState(true);
102
+ const [scaleBarOpen, setScaleBarOpen] = useState(true);
103
+ const [templatesOpen, setTemplatesOpen] = useState(false);
104
+ const [newTemplateName, setNewTemplateName] = useState('');
105
+
106
+ // Get current paper size ID
107
+ const currentPaperId = useMemo(() => {
108
+ if (!activeSheet) return 'A3_LANDSCAPE';
109
+ const paper = activeSheet.paper;
110
+ // Find matching paper in registry
111
+ for (const [id, def] of Object.entries(PAPER_SIZE_REGISTRY)) {
112
+ if (def.widthMm === paper.widthMm && def.heightMm === paper.heightMm) {
113
+ return id;
114
+ }
115
+ }
116
+ return 'A3_LANDSCAPE';
117
+ }, [activeSheet]);
118
+
119
+ // Initialize sheet if needed
120
+ const handleEnableSheet = useCallback((enabled: boolean) => {
121
+ if (enabled && !activeSheet) {
122
+ createSheet();
123
+ }
124
+ setSheetEnabled(enabled);
125
+ }, [activeSheet, createSheet, setSheetEnabled]);
126
+
127
+ // Paper size change
128
+ const handlePaperSizeChange = useCallback((paperId: string) => {
129
+ setPaperSize(paperId);
130
+ }, [setPaperSize]);
131
+
132
+ // Frame style change
133
+ const handleFrameStyleChange = useCallback((style: string) => {
134
+ setFrameStyle(style as FrameStyle);
135
+ }, [setFrameStyle]);
136
+
137
+ // Scale change
138
+ const handleScaleChange = useCallback((scaleName: string) => {
139
+ const scale = COMMON_SCALES.find((s) => s.name === scaleName);
140
+ if (scale) {
141
+ setDrawingScale(scale);
142
+ }
143
+ }, [setDrawingScale]);
144
+
145
+ // Title block layout change
146
+ const handleTitleBlockLayoutChange = useCallback((layout: string) => {
147
+ setTitleBlockLayout(layout as TitleBlockLayout);
148
+ }, [setTitleBlockLayout]);
149
+
150
+ // Save template
151
+ const handleSaveTemplate = useCallback(() => {
152
+ if (newTemplateName.trim()) {
153
+ saveAsTemplate(newTemplateName.trim());
154
+ setNewTemplateName('');
155
+ }
156
+ }, [newTemplateName, saveAsTemplate]);
157
+
158
+ return (
159
+ <div className="flex flex-col h-full bg-background border-l">
160
+ {/* Header */}
161
+ <div className="flex items-center justify-between px-4 py-3 border-b bg-muted/50">
162
+ <div className="flex items-center gap-2">
163
+ <FileText className="h-5 w-5 text-primary" />
164
+ <h2 className="font-semibold text-sm">Drawing Sheet</h2>
165
+ </div>
166
+ <div className="flex items-center gap-2">
167
+ <Switch
168
+ checked={sheetEnabled}
169
+ onCheckedChange={handleEnableSheet}
170
+ />
171
+ <Button variant="ghost" size="icon-sm" onClick={onClose}>
172
+ <X className="h-4 w-4" />
173
+ </Button>
174
+ </div>
175
+ </div>
176
+
177
+ {/* Content */}
178
+ <div className="flex-1 overflow-y-auto">
179
+ {!activeSheet && !sheetEnabled ? (
180
+ <div className="p-4 text-center text-muted-foreground">
181
+ <p className="text-sm">Enable drawing sheet to configure paper size, frame, and title block.</p>
182
+ <Button
183
+ variant="outline"
184
+ size="sm"
185
+ className="mt-4"
186
+ onClick={() => handleEnableSheet(true)}
187
+ >
188
+ Enable Sheet
189
+ </Button>
190
+ </div>
191
+ ) : (
192
+ <>
193
+ {/* Paper Size Section */}
194
+ <Collapsible open={paperSizeOpen} onOpenChange={setPaperSizeOpen}>
195
+ <CollapsibleTrigger asChild>
196
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
197
+ <span className="text-sm font-medium">Paper Size</span>
198
+ {paperSizeOpen ? (
199
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
200
+ ) : (
201
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
202
+ )}
203
+ </button>
204
+ </CollapsibleTrigger>
205
+ <CollapsibleContent>
206
+ <div className="px-4 py-3 space-y-3">
207
+ <Select value={currentPaperId} onValueChange={handlePaperSizeChange}>
208
+ <SelectTrigger className="h-8 text-sm">
209
+ <SelectValue />
210
+ </SelectTrigger>
211
+ <SelectContent>
212
+ {Object.entries(PAPER_SIZE_GROUPS).map(([group, ids]) => (
213
+ <React.Fragment key={group}>
214
+ <div className="px-2 py-1 text-xs font-semibold text-muted-foreground">
215
+ {group}
216
+ </div>
217
+ {ids.filter((id) => id in PAPER_SIZE_REGISTRY).map((id) => {
218
+ const paper = PAPER_SIZE_REGISTRY[id];
219
+ return (
220
+ <SelectItem key={id} value={id}>
221
+ {paper.name} ({paper.widthMm}×{paper.heightMm}mm)
222
+ </SelectItem>
223
+ );
224
+ })}
225
+ </React.Fragment>
226
+ ))}
227
+ </SelectContent>
228
+ </Select>
229
+
230
+ {activeSheet && (
231
+ <div className="text-xs text-muted-foreground">
232
+ {activeSheet.paper.widthMm} × {activeSheet.paper.heightMm} mm
233
+ </div>
234
+ )}
235
+ </div>
236
+ </CollapsibleContent>
237
+ </Collapsible>
238
+
239
+ {/* Frame Section */}
240
+ <Collapsible open={frameOpen} onOpenChange={setFrameOpen}>
241
+ <CollapsibleTrigger asChild>
242
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
243
+ <span className="text-sm font-medium">Frame Style</span>
244
+ {frameOpen ? (
245
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
246
+ ) : (
247
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
248
+ )}
249
+ </button>
250
+ </CollapsibleTrigger>
251
+ <CollapsibleContent>
252
+ <div className="px-4 py-3 space-y-3">
253
+ <Select
254
+ value={activeSheet?.frame.style || 'professional'}
255
+ onValueChange={handleFrameStyleChange}
256
+ >
257
+ <SelectTrigger className="h-8 text-sm">
258
+ <SelectValue />
259
+ </SelectTrigger>
260
+ <SelectContent>
261
+ {FRAME_STYLE_OPTIONS.map((opt) => (
262
+ <SelectItem key={opt.value} value={opt.value}>
263
+ {opt.label}
264
+ </SelectItem>
265
+ ))}
266
+ </SelectContent>
267
+ </Select>
268
+
269
+ {activeSheet && (
270
+ <div className="text-xs text-muted-foreground">
271
+ Margins: {activeSheet.frame.margins.top}/{activeSheet.frame.margins.right}/{activeSheet.frame.margins.bottom}/{activeSheet.frame.margins.left}mm
272
+ </div>
273
+ )}
274
+ </div>
275
+ </CollapsibleContent>
276
+ </Collapsible>
277
+
278
+ {/* Scale Section */}
279
+ <Collapsible open={scaleOpen} onOpenChange={setScaleOpen}>
280
+ <CollapsibleTrigger asChild>
281
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
282
+ <div className="flex items-center gap-2">
283
+ <Ruler className="h-4 w-4 text-muted-foreground" />
284
+ <span className="text-sm font-medium">Drawing Scale</span>
285
+ </div>
286
+ {scaleOpen ? (
287
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
288
+ ) : (
289
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
290
+ )}
291
+ </button>
292
+ </CollapsibleTrigger>
293
+ <CollapsibleContent>
294
+ <div className="px-4 py-3 space-y-3">
295
+ <Select
296
+ value={activeSheet?.scale.name || '1:100'}
297
+ onValueChange={handleScaleChange}
298
+ >
299
+ <SelectTrigger className="h-8 text-sm">
300
+ <SelectValue />
301
+ </SelectTrigger>
302
+ <SelectContent>
303
+ {COMMON_SCALES.map((scale) => (
304
+ <SelectItem key={scale.name} value={scale.name}>
305
+ {scale.name} - {scale.useCase}
306
+ </SelectItem>
307
+ ))}
308
+ </SelectContent>
309
+ </Select>
310
+ </div>
311
+ </CollapsibleContent>
312
+ </Collapsible>
313
+
314
+ {/* Title Block Section */}
315
+ <Collapsible open={titleBlockOpen} onOpenChange={setTitleBlockOpen}>
316
+ <CollapsibleTrigger asChild>
317
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
318
+ <span className="text-sm font-medium">Title Block</span>
319
+ {titleBlockOpen ? (
320
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
321
+ ) : (
322
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
323
+ )}
324
+ </button>
325
+ </CollapsibleTrigger>
326
+ <CollapsibleContent>
327
+ <div className="px-4 py-3 space-y-3">
328
+ <div>
329
+ <Label className="text-xs">Layout</Label>
330
+ <Select
331
+ value={activeSheet?.titleBlock.layout || 'standard'}
332
+ onValueChange={handleTitleBlockLayoutChange}
333
+ >
334
+ <SelectTrigger className="h-8 text-sm mt-1">
335
+ <SelectValue />
336
+ </SelectTrigger>
337
+ <SelectContent>
338
+ {TITLE_BLOCK_LAYOUT_OPTIONS.map((opt) => (
339
+ <SelectItem key={opt.value} value={opt.value}>
340
+ {opt.label}
341
+ </SelectItem>
342
+ ))}
343
+ </SelectContent>
344
+ </Select>
345
+ </div>
346
+
347
+ {activeSheet && (
348
+ <div className="text-xs text-muted-foreground">
349
+ {activeSheet.titleBlock.widthMm} × {activeSheet.titleBlock.heightMm}mm
350
+ <br />
351
+ {activeSheet.titleBlock.fields.length} fields configured
352
+ </div>
353
+ )}
354
+
355
+ <Button
356
+ variant="outline"
357
+ size="sm"
358
+ className="w-full"
359
+ onClick={onOpenTitleBlockEditor}
360
+ >
361
+ <Edit3 className="h-4 w-4 mr-2" />
362
+ Edit Title Block Fields
363
+ </Button>
364
+ </div>
365
+ </CollapsibleContent>
366
+ </Collapsible>
367
+
368
+ {/* Scale Bar & North Arrow Section */}
369
+ <Collapsible open={scaleBarOpen} onOpenChange={setScaleBarOpen}>
370
+ <CollapsibleTrigger asChild>
371
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
372
+ <div className="flex items-center gap-2">
373
+ <Compass className="h-4 w-4 text-muted-foreground" />
374
+ <span className="text-sm font-medium">Scale Bar & North Arrow</span>
375
+ </div>
376
+ {scaleBarOpen ? (
377
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
378
+ ) : (
379
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
380
+ )}
381
+ </button>
382
+ </CollapsibleTrigger>
383
+ <CollapsibleContent>
384
+ <div className="px-4 py-3 space-y-3">
385
+ {/* Scale Bar Toggle */}
386
+ <div className="flex items-center justify-between">
387
+ <Label className="text-xs">Scale Bar</Label>
388
+ <Switch
389
+ checked={activeSheet?.scaleBar.visible ?? true}
390
+ onCheckedChange={toggleScaleBar}
391
+ />
392
+ </div>
393
+
394
+ {/* North Arrow Toggle */}
395
+ <div className="flex items-center justify-between">
396
+ <Label className="text-xs">North Arrow</Label>
397
+ <Switch
398
+ checked={(activeSheet?.northArrow.style ?? 'simple') !== 'none'}
399
+ onCheckedChange={toggleNorthArrow}
400
+ />
401
+ </div>
402
+ </div>
403
+ </CollapsibleContent>
404
+ </Collapsible>
405
+
406
+ {/* Templates Section */}
407
+ <Collapsible open={templatesOpen} onOpenChange={setTemplatesOpen}>
408
+ <CollapsibleTrigger asChild>
409
+ <button className="w-full flex items-center justify-between px-4 py-2 hover:bg-muted/50 transition-colors border-b">
410
+ <span className="text-sm font-medium">Saved Templates</span>
411
+ <div className="flex items-center gap-2">
412
+ <span className="text-xs text-muted-foreground">
413
+ {savedSheetTemplates.length}
414
+ </span>
415
+ {templatesOpen ? (
416
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
417
+ ) : (
418
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
419
+ )}
420
+ </div>
421
+ </button>
422
+ </CollapsibleTrigger>
423
+ <CollapsibleContent>
424
+ <div className="px-4 py-3 space-y-3">
425
+ {/* Save current as template */}
426
+ <div className="flex gap-2">
427
+ <Input
428
+ placeholder="Template name..."
429
+ value={newTemplateName}
430
+ onChange={(e) => setNewTemplateName(e.target.value)}
431
+ className="h-8 text-sm flex-1"
432
+ />
433
+ <Button
434
+ variant="outline"
435
+ size="sm"
436
+ onClick={handleSaveTemplate}
437
+ disabled={!newTemplateName.trim() || !activeSheet}
438
+ >
439
+ <Save className="h-4 w-4" />
440
+ </Button>
441
+ </div>
442
+
443
+ {/* Template list */}
444
+ {savedSheetTemplates.length === 0 ? (
445
+ <div className="text-xs text-muted-foreground text-center py-2">
446
+ No saved templates
447
+ </div>
448
+ ) : (
449
+ <div className="space-y-1">
450
+ {savedSheetTemplates.map((template) => (
451
+ <div
452
+ key={template.id}
453
+ className="flex items-center justify-between px-2 py-1.5 bg-muted/30 rounded text-xs"
454
+ >
455
+ <span className="truncate flex-1">{template.name}</span>
456
+ <div className="flex gap-1">
457
+ <Button
458
+ variant="ghost"
459
+ size="icon-sm"
460
+ className="h-6 w-6"
461
+ onClick={() => loadTemplate(template.id)}
462
+ >
463
+ <Plus className="h-3 w-3" />
464
+ </Button>
465
+ <Button
466
+ variant="ghost"
467
+ size="icon-sm"
468
+ className="h-6 w-6 text-destructive hover:text-destructive"
469
+ onClick={() => deleteTemplate(template.id)}
470
+ >
471
+ <Trash2 className="h-3 w-3" />
472
+ </Button>
473
+ </div>
474
+ </div>
475
+ ))}
476
+ </div>
477
+ )}
478
+ </div>
479
+ </CollapsibleContent>
480
+ </Collapsible>
481
+
482
+ {/* Viewport Info */}
483
+ {activeSheet && (
484
+ <div className="px-4 py-3 border-t">
485
+ <div className="text-xs text-muted-foreground space-y-1">
486
+ <div>
487
+ <strong>Drawing Area:</strong>{' '}
488
+ {activeSheet.viewportBounds.width.toFixed(1)} ×{' '}
489
+ {activeSheet.viewportBounds.height.toFixed(1)} mm
490
+ </div>
491
+ <div>
492
+ <strong>Scale:</strong> {activeSheet.scale.name}
493
+ </div>
494
+ </div>
495
+ </div>
496
+ )}
497
+ </>
498
+ )}
499
+ </div>
500
+ </div>
501
+ );
502
+ }
@@ -3,26 +3,22 @@
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
5
  import { useMemo, useState, useEffect } from 'react';
6
- import { Boxes, Triangle, CheckCircle2, AlertCircle } from 'lucide-react';
6
+ import { Boxes, Triangle, CheckCircle2, AlertCircle, Loader2 } from 'lucide-react';
7
7
  import { Separator } from '@/components/ui/separator';
8
8
  import { formatNumber, formatBytes } from '@/lib/utils';
9
9
  import { useViewerStore } from '@/store';
10
10
  import { useIfc } from '@/hooks/useIfc';
11
+ import { useWebGPU } from '@/hooks/useWebGPU';
11
12
 
12
13
  export function StatusBar() {
13
14
  const { loading, geometryResult, ifcDataStore } = useIfc();
14
15
  const progress = useViewerStore((s) => s.progress);
15
16
  const error = useViewerStore((s) => s.error);
16
- const selectedStorey = useViewerStore((s) => s.selectedStorey);
17
+ const selectedStoreys = useViewerStore((s) => s.selectedStoreys);
18
+ const webgpu = useWebGPU();
17
19
 
18
20
  const [fps, setFps] = useState(60);
19
21
  const [memory, setMemory] = useState(0);
20
- const [webgpuSupported, setWebgpuSupported] = useState<boolean | null>(null);
21
-
22
- // Check WebGPU support
23
- useEffect(() => {
24
- setWebgpuSupported('gpu' in navigator);
25
- }, []);
26
22
 
27
23
  // FPS counter (simplified)
28
24
  useEffect(() => {
@@ -71,12 +67,19 @@ export function StatusBar() {
71
67
  }, [geometryResult]);
72
68
 
73
69
  const visibleElements = useMemo(() => {
74
- if (!selectedStorey || !ifcDataStore?.spatialHierarchy) {
70
+ if (selectedStoreys.size === 0 || !ifcDataStore?.spatialHierarchy) {
75
71
  return stats.elements;
76
72
  }
77
- const storeyElements = ifcDataStore.spatialHierarchy.byStorey.get(selectedStorey);
78
- return (storeyElements as number[] | undefined)?.length ?? stats.elements;
79
- }, [selectedStorey, ifcDataStore, stats.elements]);
73
+ // Count elements from all selected storeys
74
+ let count = 0;
75
+ for (const storeyId of selectedStoreys) {
76
+ const storeyElements = ifcDataStore.spatialHierarchy.byStorey.get(storeyId);
77
+ if (storeyElements) {
78
+ count += storeyElements.length;
79
+ }
80
+ }
81
+ return count || stats.elements;
82
+ }, [selectedStoreys, ifcDataStore, stats.elements]);
80
83
 
81
84
  return (
82
85
  <div className="h-7 px-3 border-t bg-muted/30 flex items-center justify-between text-xs text-muted-foreground">
@@ -97,7 +100,7 @@ export function StatusBar() {
97
100
  <Boxes className="h-3.5 w-3.5" />
98
101
  <span>
99
102
  {formatNumber(visibleElements)}
100
- {selectedStorey && stats.elements !== visibleElements && (
103
+ {selectedStoreys.size > 0 && stats.elements !== visibleElements && (
101
104
  <span className="opacity-60"> / {formatNumber(stats.elements)}</span>
102
105
  )}
103
106
  {' '}elements
@@ -128,13 +131,21 @@ export function StatusBar() {
128
131
  <Separator orientation="vertical" className="h-3.5" />
129
132
 
130
133
  <div className="flex items-center gap-1">
131
- {webgpuSupported ? (
134
+ {webgpu.checking ? (
135
+ <Loader2 className="h-3.5 w-3.5 text-zinc-400 animate-spin" />
136
+ ) : webgpu.supported ? (
132
137
  <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
133
138
  ) : (
134
- <AlertCircle className="h-3.5 w-3.5 text-yellow-500" />
139
+ <AlertCircle className="h-3.5 w-3.5 text-[#f7768e]" />
135
140
  )}
136
- <span>{webgpuSupported ? 'WebGPU' : 'WebGL'}</span>
141
+ <span className={!webgpu.supported && !webgpu.checking ? 'text-[#f7768e]' : ''}>
142
+ {webgpu.checking ? 'Checking...' : webgpu.supported ? 'WebGPU' : 'No WebGPU'}
143
+ </span>
137
144
  </div>
145
+
146
+ <Separator orientation="vertical" className="h-3.5" />
147
+
148
+ <span className="opacity-60">v{__APP_VERSION__}</span>
138
149
  </div>
139
150
  </div>
140
151
  );