@thatopen/services 0.1.3 → 0.3.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 (120) hide show
  1. package/README.md +59 -46
  2. package/dist/built-in/index.d.ts +486 -563
  3. package/dist/cli/lib/npmrc.d.ts +28 -0
  4. package/dist/cli/lib/npmrc.test.d.ts +1 -0
  5. package/dist/cli.js +91 -7
  6. package/dist/core/client.d.ts +10 -0
  7. package/dist/index.cjs.js +2 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.es.js +42 -32
  10. package/dist/types/npm.d.ts +14 -0
  11. package/dist/vite-env.d.ts +1 -1
  12. package/docs/builtin/paths.json +74 -70
  13. package/docs/cli/create.md +28 -28
  14. package/docs/cli/local-server.md +38 -38
  15. package/docs/cli/login.md +32 -32
  16. package/docs/cli/publish.md +31 -31
  17. package/docs/cli/run.md +26 -26
  18. package/docs/cli/serve.md +43 -43
  19. package/docs/cli/swap.md +30 -30
  20. package/package.json +100 -100
  21. package/resources/AGENTS.md +40 -40
  22. package/src/built-in/CDEManager/example.ts +214 -214
  23. package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
  24. package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
  25. package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
  26. package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
  27. package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
  28. package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
  29. package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
  30. package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
  31. package/src/built-in/UIManager/src/app/example.ts +99 -99
  32. package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
  33. package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
  34. package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
  35. package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
  36. package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
  37. package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
  38. package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
  39. package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
  40. package/src/built-in/index.ts +649 -755
  41. package/src/cli/templates/app/README.md +241 -241
  42. package/src/cli/templates/app/index.html +35 -27
  43. package/src/cli/templates/app/package.json +31 -27
  44. package/src/cli/templates/app/src/app.ts +37 -0
  45. package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
  46. package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
  47. package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
  48. package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
  49. package/src/cli/templates/app/src/bim-components/index.ts +1 -1
  50. package/src/cli/templates/app/src/globals.ts +1 -0
  51. package/src/cli/templates/app/src/main.ts +340 -56
  52. package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
  53. package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
  54. package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
  55. package/src/cli/templates/app/src/setups/card-header.ts +35 -0
  56. package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
  57. package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
  58. package/src/cli/templates/app/src/setups/clipper.ts +75 -0
  59. package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
  60. package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
  61. package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
  62. package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
  63. package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
  64. package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
  65. package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
  66. package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
  67. package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
  68. package/src/cli/templates/app/src/setups/hider.ts +98 -0
  69. package/src/cli/templates/app/src/setups/index.ts +33 -3
  70. package/src/cli/templates/app/src/setups/inspection.ts +136 -0
  71. package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
  72. package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
  73. package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
  74. package/src/cli/templates/app/src/setups/measurements.ts +133 -0
  75. package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
  76. package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
  77. package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
  78. package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
  79. package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
  80. package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
  81. package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
  82. package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
  83. package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
  84. package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
  85. package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
  86. package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
  87. package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
  88. package/src/cli/templates/app/src/setups/styles.ts +375 -0
  89. package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
  90. package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
  91. package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
  92. package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
  93. package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
  94. package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
  95. package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
  96. package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
  97. package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
  98. package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
  99. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
  100. package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
  101. package/src/cli/templates/app/src/ui-components/index.ts +2 -1
  102. package/src/cli/templates/app/tsconfig.json +16 -17
  103. package/src/cli/templates/app/vite.config.js +45 -42
  104. package/src/cli/templates/cloud-component/README.md +205 -205
  105. package/src/cli/templates/cloud-component/_thatopen +5 -5
  106. package/src/cli/templates/cloud-component/declarations.json +4 -4
  107. package/src/cli/templates/cloud-component/package.json +21 -21
  108. package/src/cli/templates/cloud-component/src/main.ts +70 -70
  109. package/src/cli/templates/cloud-component/tsconfig.json +16 -16
  110. package/src/cli/templates/cloud-component/vite.config.js +46 -46
  111. package/src/cli/templates/shared/AGENTS.md +8 -8
  112. package/src/cli/templates/shared/CLAUDE.md +1 -1
  113. package/src/cli/templates/shared/_gitignore +5 -4
  114. package/src/built-in/AppManager/example.ts +0 -86
  115. package/src/built-in/FileList/example.ts +0 -107
  116. package/src/built-in/TabbedNavigation/example.ts +0 -115
  117. package/src/built-in/ViewportsManager/example.ts +0 -106
  118. package/src/cli/templates/app/src/setups/fragments.ts +0 -18
  119. package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
  120. package/src/cli/templates/app/src/ui-components/AppPanel/index.ts +0 -160
@@ -0,0 +1,246 @@
1
+ import * as THREE from "three";
2
+ import * as OBC from "@thatopen/components";
3
+ import * as FRAGS from "@thatopen/fragments";
4
+
5
+ /**
6
+ * EXPLODED VIEW — a HEADLESS controller (no UI). The toolbar wires a single
7
+ * `mdi:arrow-expand-vertical` button to `toggle()` / `isActive()` / `onChange()`.
8
+ *
9
+ * ── Technique ──────────────────────────────────────────────────────────────
10
+ * Fragments has no runtime "translate these items" call; the supported path is
11
+ * the Edit API's GLOBAL TRANSFORMS (each item placement is a global transform
12
+ * with a `position`). So per model we:
13
+ * 1. Group element localIds by IFCBUILDINGSTOREY (from the spatial structure)
14
+ * and order storeys by elevation (min-Y of each group's bbox).
15
+ * 2. Map items → their global-transform ids; a transform shared across MORE
16
+ * than one storey is skipped (it can't be moved per-storey cleanly).
17
+ * 3. SNAPSHOT each transform's original position ONCE.
18
+ * 4. To explode at magnitude m: set each transform's Y to
19
+ * `originalY + storeyIndex × step × m` — ALWAYS absolute from the snapshot,
20
+ * never accumulated, so repeated explode/reset never drifts. m = 0 restores
21
+ * exactly.
22
+ *
23
+ * Edits are applied in the background (the Edit API can't do 60/s). Visibility &
24
+ * selection are untouched (only transforms change).
25
+ *
26
+ * const explode = explodedView(components);
27
+ * // toolbar: button @click=explode.toggle(); active styling = explode.isActive()
28
+ *
29
+ * @param components engine components
30
+ */
31
+
32
+ const STOREY = "IFCBUILDINGSTOREY";
33
+ const DEFAULT_MAGNITUDE = 1.0;
34
+
35
+ export interface ExplodedViewController {
36
+ /** Off→on (applies the default magnitude) / on→off (restores exactly). */
37
+ toggle(): void;
38
+ /** Whether the model is currently exploded (magnitude > 0). */
39
+ isActive(): boolean;
40
+ /** Subscribe to active changes (toggle / auto-reset). Returns an unsubscribe. */
41
+ onChange(cb: (active: boolean) => void): () => void;
42
+ /** Graded control (0→1). Kept for a future popover slider; 0 restores exactly. */
43
+ setMagnitude(v: number): void;
44
+ }
45
+
46
+ interface ModelExplode {
47
+ // global-transform localId → its storey index (0 = lowest). Only single-storey
48
+ // transforms are included.
49
+ gtToStorey: Map<number, number>;
50
+ // global-transform localId → original position [x,y,z] (the snapshot).
51
+ snapshot: Map<number, [number, number, number]>;
52
+ // current global-transform objects (other fields preserved on UPDATE).
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ gts: Map<number, any>;
55
+ step: number; // vertical units per storey index at magnitude 1
56
+ }
57
+
58
+ export const explodedView = (
59
+ components: OBC.Components,
60
+ ): ExplodedViewController => {
61
+ const fragments = components.get(OBC.FragmentsManager);
62
+ // The Edit API; opt into the in-place incremental edit path (cheaper than a
63
+ // per-edit delta-model rebuild). Harmless if already enabled.
64
+ try {
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ (fragments.core as any).settings.incrementalEdit = true;
67
+ } catch {
68
+ /* setting may not exist on older cores — edits still work, just slower */
69
+ }
70
+
71
+ let magnitude = 0;
72
+ let active = false;
73
+ let busy = false;
74
+ const prepared = new Map<string, ModelExplode>();
75
+ const listeners = new Set<(active: boolean) => void>();
76
+
77
+ const setActive = (a: boolean) => {
78
+ if (a === active) return;
79
+ active = a;
80
+ for (const cb of listeners) cb(a);
81
+ };
82
+
83
+ // Collect, per storey node, all descendant element localIds.
84
+ const collectStoreys = (root: FRAGS.SpatialTreeItem) => {
85
+ const storeys: number[][] = [];
86
+ const descendants = (node: FRAGS.SpatialTreeItem, acc: number[]) => {
87
+ if (node.localId !== null && node.localId !== undefined && node.localId >= 0) {
88
+ acc.push(node.localId);
89
+ }
90
+ node.children?.forEach((c) => descendants(c, acc));
91
+ };
92
+ const walk = (node: FRAGS.SpatialTreeItem) => {
93
+ if ((node.category ?? "").toUpperCase() === STOREY) {
94
+ const ids: number[] = [];
95
+ descendants(node, ids);
96
+ if (ids.length) storeys.push(ids);
97
+ } else {
98
+ node.children?.forEach(walk);
99
+ }
100
+ };
101
+ walk(root);
102
+ return storeys;
103
+ };
104
+
105
+ // Build the explode plan for one model (snapshot + storey→transform map).
106
+ const prepareModel = async (
107
+ model: FRAGS.FragmentsModel,
108
+ ): Promise<ModelExplode | null> => {
109
+ const structure = await model.getSpatialStructure();
110
+ if (!structure) return null;
111
+ let storeys = collectStoreys(structure);
112
+ if (storeys.length < 2) return null; // nothing to separate
113
+
114
+ // Order storeys by elevation (min-Y of the group's merged bbox).
115
+ const withY = await Promise.all(
116
+ storeys.map(async (ids) => {
117
+ let minY = Number.POSITIVE_INFINITY;
118
+ try {
119
+ const boxes = (await fragments.getBBoxes({
120
+ [model.modelId]: new Set(ids),
121
+ })) as THREE.Box3[];
122
+ for (const b of boxes) if (!b.isEmpty()) minY = Math.min(minY, b.min.y);
123
+ } catch {
124
+ /* leave at +Inf → sorts last; harmless */
125
+ }
126
+ return { ids, minY };
127
+ }),
128
+ );
129
+ withY.sort((a, b) => a.minY - b.minY);
130
+ storeys = withY.map((s) => s.ids);
131
+
132
+ // Map global-transform ids → storey index; mark cross-storey ones for skip.
133
+ const gtToStorey = new Map<number, number>();
134
+ const skip = new Set<number>();
135
+ for (let i = 0; i < storeys.length; i += 1) {
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ const gtIds: Iterable<number> = await (model as any).getGlobalTranformsIdsOfItems(
138
+ storeys[i],
139
+ );
140
+ for (const gt of gtIds) {
141
+ if (skip.has(gt)) continue;
142
+ if (gtToStorey.has(gt) && gtToStorey.get(gt) !== i) {
143
+ gtToStorey.delete(gt); // shared across storeys → can't move cleanly
144
+ skip.add(gt);
145
+ } else {
146
+ gtToStorey.set(gt, i);
147
+ }
148
+ }
149
+ }
150
+ if (gtToStorey.size === 0) return null;
151
+
152
+ // Snapshot original positions.
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ const gts: Map<number, any> = await model.getGlobalTransforms();
155
+ const snapshot = new Map<number, [number, number, number]>();
156
+ for (const [id, gt] of gts) {
157
+ if (!gtToStorey.has(id)) continue;
158
+ const p = gt.position as number[];
159
+ snapshot.set(id, [p[0], p[1], p[2]]);
160
+ }
161
+
162
+ // Vertical step per storey index: a fraction of the model's height so the
163
+ // explode reads clearly regardless of model scale.
164
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
165
+ const box = (model as any).box as THREE.Box3 | undefined;
166
+ const height = box && !box.isEmpty() ? box.max.y - box.min.y : 10;
167
+ const step = (height * 0.45) / Math.max(1, storeys.length - 1);
168
+
169
+ return { gtToStorey, snapshot, gts, step };
170
+ };
171
+
172
+ const rebuild = async () => {
173
+ prepared.clear();
174
+ for (const model of fragments.list.values()) {
175
+ try {
176
+ const plan = await prepareModel(model);
177
+ if (plan) prepared.set(model.modelId, plan);
178
+ } catch (error) {
179
+ console.warn("[exploded-view] prepare failed", model.modelId, error);
180
+ }
181
+ }
182
+ // Re-apply the current magnitude to freshly-loaded models.
183
+ if (magnitude > 0) void apply();
184
+ };
185
+
186
+ // Apply the current magnitude across all prepared models. Always sets the
187
+ // absolute position from the snapshot (no accumulation → no drift).
188
+ const apply = async () => {
189
+ if (busy) return;
190
+ busy = true;
191
+ try {
192
+ for (const [modelId, plan] of prepared) {
193
+ const model = fragments.list.get(modelId);
194
+ if (!model) continue;
195
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
+ const requests: any[] = [];
197
+ for (const [id, orig] of plan.snapshot) {
198
+ const i = plan.gtToStorey.get(id) ?? 0;
199
+ const gt = plan.gts.get(id);
200
+ if (!gt) continue;
201
+ requests.push({
202
+ type: FRAGS.EditRequestType.UPDATE_GLOBAL_TRANSFORM,
203
+ localId: id,
204
+ data: {
205
+ ...gt,
206
+ position: [orig[0], orig[1] + i * plan.step * magnitude, orig[2]],
207
+ },
208
+ });
209
+ }
210
+ if (requests.length === 0) continue;
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ await (fragments.core as any).editor.edit(modelId, requests);
213
+ }
214
+ await fragments.core.update(true);
215
+ } catch (error) {
216
+ console.warn("[exploded-view] apply failed", error);
217
+ } finally {
218
+ busy = false;
219
+ }
220
+ };
221
+
222
+ const setMagnitude = (v: number) => {
223
+ magnitude = Math.max(0, Math.min(1, v));
224
+ void apply();
225
+ setActive(magnitude > 0);
226
+ };
227
+
228
+ // Recompute the plan whenever the loaded models change.
229
+ fragments.core.onModelLoaded.add(() => void rebuild());
230
+ fragments.list.onItemDeleted.add(() => void rebuild());
231
+ void rebuild();
232
+
233
+ return {
234
+ toggle() {
235
+ setMagnitude(active ? 0 : DEFAULT_MAGNITUDE);
236
+ },
237
+ isActive() {
238
+ return active;
239
+ },
240
+ onChange(cb) {
241
+ listeners.add(cb);
242
+ return () => listeners.delete(cb);
243
+ },
244
+ setMagnitude,
245
+ };
246
+ };
@@ -0,0 +1,95 @@
1
+ import * as BUI from "@thatopen/ui";
2
+
3
+ /**
4
+ * Platform-matching file-format glyphs, ported from the platform's own file
5
+ * browser (platform_backend-api .../util/fileIcons). The platform's icons are
6
+ * NOT iconify/mdi, so they can't go through bim-icon:
7
+ * - ifc / gltf are raster PNG assets — inlined here as data URIs and drawn the
8
+ * same way the platform does (`<image>` in a 24×24 SvgIcon, x/y 2, 20×20).
9
+ * - json replicates the platform's `FileExtensionLabel` badge (rounded rect +
10
+ * centered text), same geometry/colors.
11
+ * - anything the platform doesn't map (e.g. .frag) falls back to the platform's
12
+ * generic file icon (MUI InsertDriveFileOutlined path), muted gray.
13
+ *
14
+ * Covered: ifc, gltf/glb, json, generic fallback. More platform formats
15
+ * (xml/yaml/csv/doc/xls badges, png/jpg/pdf…) are easy one-liners to add — each
16
+ * just needs its label+color or PNG ported the same way.
17
+ */
18
+
19
+ // Inlined from platform_backend-api/src/client/src/assets/icons/ifc.png
20
+ const IFC_PNG =
21
+ "data:image/png;base64,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";
22
+
23
+ // Inlined from platform_backend-api/src/client/src/assets/icons/gltf.png
24
+ const GLTF_PNG =
25
+ "data:image/png;base64,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";
26
+
27
+ const SIZE = "1rem";
28
+ const SVG_STYLE =
29
+ "flex-shrink: 0; display: inline-block; vertical-align: middle;";
30
+
31
+ const pngGlyph = (dataUri: string, title: string) => BUI.html`
32
+ <svg viewBox="0 0 24 24" width=${SIZE} height=${SIZE} style=${SVG_STYLE}>
33
+ <title>${title}</title>
34
+ <image href=${dataUri} x="2" y="2" width="20" height="20"></image>
35
+ </svg>`;
36
+
37
+ const labelGlyph = (label: string, bg: string) => BUI.html`
38
+ <svg viewBox="0 0 24 24" width=${SIZE} height=${SIZE} style=${SVG_STYLE}>
39
+ <title>${label}</title>
40
+ <rect x="1" y="4" width="22" height="16" rx="3" fill=${bg}></rect>
41
+ <text
42
+ x="12" y="14.5" text-anchor="middle" fill="#FFFFFF"
43
+ font-size=${label.length > 3 ? "6" : "7"} font-weight="700"
44
+ font-family="Arial, sans-serif"
45
+ >${label}</text>
46
+ </svg>`;
47
+
48
+ // Fragments (.frag) — the official ThatOpen isotype (the green brand mark,
49
+ // extracted from the platform's HeaderLogoIcon — just the two mark paths, no
50
+ // wordmark), in the brand colour #BCF124.
51
+ const fragGlyph = () => BUI.html`
52
+ <svg viewBox="0 0 32 40" width=${SIZE} height=${SIZE} style=${SVG_STYLE}>
53
+ <title>Fragments</title>
54
+ <path
55
+ fill="#BCF124"
56
+ d="M7.00861 16.1097H0C0.00583172 11.8399 1.68502 7.74676 4.66939 4.7276C7.65377 1.70845 11.6998 0.0096915 15.9203 0.00379181V7.09409C13.558 7.09802 11.2935 8.04914 9.62308 9.73903C7.95266 11.4289 7.0125 13.7198 7.00861 16.1097Z"
57
+ ></path>
58
+ <path
59
+ fill="#BCF124"
60
+ d="M15.9243 39.3095C11.7037 39.3036 7.65771 37.6048 4.67333 34.5856C1.68896 31.5665 0.00976849 27.4733 0.00393677 23.2036H7.01255C7.01255 24.9874 7.53543 26.7312 8.51506 28.2144C9.4947 29.6976 10.8871 30.8537 12.5162 31.5363C14.1452 32.219 15.9378 32.3976 17.6672 32.0496C19.3966 31.7016 20.9852 30.8426 22.2321 29.5812C23.4789 28.3198 24.328 26.7127 24.672 24.9632C25.016 23.2136 24.8394 21.4001 24.1647 19.7521C23.4899 18.104 22.3472 16.6954 20.881 15.7043C19.4149 14.7133 17.6912 14.1843 15.9279 14.1843V7.09029C20.1512 7.09029 24.2016 8.78754 27.1879 11.8087C30.1742 14.8298 31.8519 18.9273 31.8519 23.1999C31.8519 27.4724 30.1742 31.5699 27.1879 34.5911C24.2016 37.6122 20.1512 39.3095 15.9279 39.3095H15.9243Z"
61
+ ></path>
62
+ </svg>`;
63
+
64
+ // 3D Tiles archive (.3tz) — a tiled-format badge, drawn the same way as the
65
+ // JSON/label glyphs (rounded rect + centered text), in the platform's tiles/
66
+ // reality-capture accent (teal). Mirrors fragGlyph()/labelGlyph() style; swap
67
+ // for a brand isotype later if the platform ships one.
68
+ const threetzGlyph = () => labelGlyph("3TZ", "#26A69A");
69
+
70
+ const genericGlyph = () => BUI.html`
71
+ <svg viewBox="0 0 24 24" width=${SIZE} height=${SIZE} style=${SVG_STYLE}>
72
+ <path
73
+ fill="var(--bim-ui_bg-contrast-60, #9a9a9a)"
74
+ d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"
75
+ ></path>
76
+ </svg>`;
77
+
78
+ /** Leading file-format glyph for a row, matching the platform's file browser. */
79
+ export const formatGlyph = (ext: string) => {
80
+ switch (ext) {
81
+ case "ifc":
82
+ return pngGlyph(IFC_PNG, "IFC");
83
+ case "gltf":
84
+ case "glb":
85
+ return pngGlyph(GLTF_PNG, "glTF");
86
+ case "frag":
87
+ return fragGlyph();
88
+ case "3tz":
89
+ return threetzGlyph();
90
+ case "json":
91
+ return labelGlyph("JSON", "#FF7043");
92
+ default:
93
+ return genericGlyph();
94
+ }
95
+ };