@thatopen/services 0.2.0 → 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.
- package/README.md +59 -59
- package/dist/built-in/index.d.ts +486 -563
- package/dist/cli.js +10 -0
- package/dist/index.cjs.js +2 -2
- package/dist/index.es.js +3 -5
- package/dist/vite-env.d.ts +1 -1
- package/docs/builtin/paths.json +74 -70
- package/docs/cli/create.md +28 -28
- package/docs/cli/local-server.md +38 -38
- package/docs/cli/login.md +32 -32
- package/docs/cli/publish.md +31 -31
- package/docs/cli/run.md +26 -26
- package/docs/cli/serve.md +43 -43
- package/docs/cli/swap.md +30 -30
- package/package.json +100 -100
- package/resources/AGENTS.md +40 -40
- package/src/built-in/CDEManager/example.ts +214 -214
- package/src/built-in/CDEManager/examples/FileEditing/example.ts +226 -226
- package/src/built-in/CDEManager/examples/FileMoves/example.ts +186 -186
- package/src/built-in/CDEManager/examples/FileOperations/example.ts +295 -295
- package/src/built-in/CDEManager/examples/FileTransfers/example.ts +210 -210
- package/src/built-in/CDEManager/examples/FolderManagement/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataSchema/example.ts +230 -230
- package/src/built-in/CDEManager/examples/MetadataViews/example.ts +166 -166
- package/src/built-in/CDEManager/examples/ProjectConfig/example.ts +177 -177
- package/src/built-in/UIManager/src/app/example.ts +99 -99
- package/src/built-in/UIManager/src/app/examples/AppState/example.ts +142 -142
- package/src/built-in/UIManager/src/app/examples/Banner/example.ts +161 -0
- package/src/built-in/UIManager/src/app/examples/Contexts/example.ts +125 -125
- package/src/built-in/UIManager/src/app/examples/FileDrop/example.ts +126 -0
- package/src/built-in/UIManager/src/app/examples/FloatingLayout/example.ts +92 -92
- package/src/built-in/UIManager/src/app/examples/Modal/example.ts +165 -0
- package/src/built-in/UIManager/src/app/examples/Notifications/example.ts +135 -161
- package/src/built-in/UIManager/src/app/examples/Sidebar/example.ts +104 -104
- package/src/built-in/index.ts +649 -755
- package/src/cli/templates/app/README.md +241 -241
- package/src/cli/templates/app/index.html +35 -27
- package/src/cli/templates/app/package.json +31 -27
- package/src/cli/templates/app/src/app.ts +37 -0
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -0
- package/src/cli/templates/app/src/bim-components/CloudRunner/index.ts +95 -87
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/index.ts +1 -1
- package/src/cli/templates/app/src/bim-components/CloudRunner/src/types.ts +5 -5
- package/src/cli/templates/app/src/bim-components/index.ts +1 -1
- package/src/cli/templates/app/src/globals.ts +1 -0
- package/src/cli/templates/app/src/main.ts +340 -56
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +69 -0
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -0
- package/src/cli/templates/app/src/setups/camera-views.ts +201 -0
- package/src/cli/templates/app/src/setups/card-header.ts +35 -0
- package/src/cli/templates/app/src/setups/clipper-panel.ts +167 -0
- package/src/cli/templates/app/src/setups/clipper-tool.ts +754 -0
- package/src/cli/templates/app/src/setups/clipper.ts +75 -0
- package/src/cli/templates/app/src/setups/cloud-runner.ts +13 -9
- package/src/cli/templates/app/src/setups/commands-panel.ts +239 -0
- package/src/cli/templates/app/src/setups/data-table-panel.ts +2224 -0
- package/src/cli/templates/app/src/setups/exploded-view.ts +246 -0
- package/src/cli/templates/app/src/setups/file-format-icons.ts +95 -0
- package/src/cli/templates/app/src/setups/files-panel.ts +1503 -0
- package/src/cli/templates/app/src/setups/fps-indicator.ts +63 -0
- package/src/cli/templates/app/src/setups/graphics-panel.ts +490 -0
- package/src/cli/templates/app/src/setups/helper-panel.ts +88 -0
- package/src/cli/templates/app/src/setups/hider.ts +98 -0
- package/src/cli/templates/app/src/setups/index.ts +33 -3
- package/src/cli/templates/app/src/setups/inspection.ts +136 -0
- package/src/cli/templates/app/src/setups/measurement-panel.ts +188 -0
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +161 -0
- package/src/cli/templates/app/src/setups/measurement-tool.ts +570 -0
- package/src/cli/templates/app/src/setups/measurements.ts +133 -0
- package/src/cli/templates/app/src/setups/model-tree.ts +797 -0
- package/src/cli/templates/app/src/setups/navigation-gizmo.ts +316 -0
- package/src/cli/templates/app/src/setups/objects-panel.ts +127 -0
- package/src/cli/templates/app/src/setups/plans-panel.ts +619 -0
- package/src/cli/templates/app/src/setups/properties-panel.ts +1048 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -0
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -0
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -0
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -0
- package/src/cli/templates/app/src/setups/settings-panel.ts +94 -0
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -0
- package/src/cli/templates/app/src/setups/styles.ts +375 -0
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +150 -0
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -0
- package/src/cli/templates/app/src/setups/toolbar.ts +229 -0
- package/src/cli/templates/app/src/setups/ui-manager.ts +27 -0
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +498 -0
- package/src/cli/templates/app/src/setups/walkthrough.ts +528 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/index.ts +26 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/app-info-section/src/types.ts +15 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/index.ts +37 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/index.ts +1 -0
- package/src/cli/templates/app/src/ui-components/cloud-runner-section/src/types.ts +14 -0
- package/src/cli/templates/app/src/ui-components/index.ts +2 -1
- package/src/cli/templates/app/tsconfig.json +16 -17
- package/src/cli/templates/app/vite.config.js +45 -42
- package/src/cli/templates/cloud-component/README.md +205 -205
- package/src/cli/templates/cloud-component/_thatopen +5 -5
- package/src/cli/templates/cloud-component/declarations.json +4 -4
- package/src/cli/templates/cloud-component/package.json +21 -21
- package/src/cli/templates/cloud-component/src/main.ts +70 -70
- package/src/cli/templates/cloud-component/tsconfig.json +16 -16
- package/src/cli/templates/cloud-component/vite.config.js +46 -46
- package/src/cli/templates/shared/AGENTS.md +8 -8
- package/src/cli/templates/shared/CLAUDE.md +1 -1
- package/src/cli/templates/shared/_gitignore +5 -5
- package/src/built-in/AppManager/example.ts +0 -86
- package/src/built-in/FileList/example.ts +0 -107
- package/src/built-in/TabbedNavigation/example.ts +0 -115
- package/src/built-in/ViewportsManager/example.ts +0 -106
- package/src/cli/templates/app/src/setups/fragments.ts +0 -18
- package/src/cli/templates/app/src/setups/ifc-loader.ts +0 -12
- 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
|
+
};
|