@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,88 @@
1
+ // 3DTilesRendererJS plugin: serve a STANDARD loose 3D Tiles dataset whose tiles
2
+ // are stored as platform HIDDEN files, streamed one-file-per-tile.
3
+ //
4
+ // Seam: the renderer's `fetchData(url, options)` hook (intercepts BOTH the root
5
+ // tileset and every tile content fetch). Point the TilesRenderer at
6
+ // `<baseUrl>/tileset.json`; relative content URIs then resolve to
7
+ // `<baseUrl>/<relUri>` (e.g. "mem://t/tiles/n12_d3.spz"), which we strip back to
8
+ // the dataset-relative path (`relUri`) and look up in the `hiddenFiles` map to
9
+ // find the platform hidden-file id to download.
10
+ //
11
+ // SHARED CONTRACT (matches the converters): the main visible file is a standard
12
+ // `tileset.json`; each tile `content.uri` is a relative path under "tiles/".
13
+ // tileset.json carries a top-level `hiddenFiles: { "<content.uri>": "<id>" }`
14
+ // map. We fetch a tile blob with `client.downloadHiddenFile(id)` -> Response.
15
+ //
16
+ // Per-tile streaming = one `downloadHiddenFile` per visible tile; the controller's
17
+ // Q1 LRU/count-budget governor bounds how many are resident/loading at once.
18
+
19
+ // Structural type for the platform client: download a normal file by id (the
20
+ // tileset main file) and a hidden file by id (each tile). PlatformClient /
21
+ // EngineServicesClient satisfy this.
22
+ export interface HiddenTilesClient {
23
+ downloadFile(fileId: string, params?: any): Promise<Response>;
24
+ downloadHiddenFile(hiddenId: string): Promise<Response>;
25
+ }
26
+
27
+ export interface HiddenTilesOptions {
28
+ baseUrl: string; // synthetic base the TilesRenderer is pointed at, e.g. "mem://t"
29
+ tilesetBytes: Uint8Array; // the already-downloaded tileset.json bytes
30
+ hiddenFiles: Record<string, string>; // content.uri -> hidden file id
31
+ client: HiddenTilesClient;
32
+ }
33
+
34
+ export class HiddenTilesPlugin {
35
+ name = "HIDDEN_TILES_PLUGIN";
36
+ private baseUrl: string;
37
+ private tilesetBytes: Uint8Array;
38
+ private hiddenFiles: Record<string, string>;
39
+ private client: HiddenTilesClient;
40
+
41
+ constructor({ baseUrl, tilesetBytes, hiddenFiles, client }: HiddenTilesOptions) {
42
+ this.baseUrl = baseUrl.replace(/\/$/, "");
43
+ this.tilesetBytes = tilesetBytes;
44
+ this.hiddenFiles = hiddenFiles;
45
+ this.client = client;
46
+ }
47
+
48
+ // dataset-relative path for URLs under our synthetic base, else null.
49
+ private rel(url: string): string | null {
50
+ const prefix = this.baseUrl + "/";
51
+ return url.startsWith(prefix) ? url.slice(prefix.length) : null;
52
+ }
53
+
54
+ // The renderer fetch hook. Return a Response, or null to let the default fetch
55
+ // run (URLs that aren't under our base).
56
+ async fetchData(url: string, _options: any): Promise<Response | null> {
57
+ const rel = this.rel(url);
58
+ if (rel === null) return null;
59
+
60
+ // Root tileset request: serve the bytes we already downloaded.
61
+ if (rel === "tileset.json") {
62
+ // copy out into a standalone ArrayBuffer for the Response
63
+ const b = this.tilesetBytes;
64
+ const ab = (b.byteOffset === 0 && b.byteLength === b.buffer.byteLength
65
+ ? b.buffer
66
+ : b.slice().buffer) as ArrayBuffer;
67
+ return new Response(ab, {
68
+ status: 200,
69
+ headers: { "Content-Type": "application/json" },
70
+ });
71
+ }
72
+
73
+ // Content (tile) request: strip query/fragment, look up the hidden id.
74
+ const relUri = decodeURIComponent(rel.split("?")[0].split("#")[0]);
75
+ const hiddenId = this.hiddenFiles[relUri];
76
+ if (!hiddenId) {
77
+ // not a known tile -> 404 so the renderer treats it as a failed fetch.
78
+ return new Response(null, { status: 404, statusText: "Not Found" });
79
+ }
80
+
81
+ const res = await this.client.downloadHiddenFile(hiddenId);
82
+ const buf = await res.arrayBuffer();
83
+ return new Response(buf, {
84
+ status: 200,
85
+ headers: { "Content-Type": "application/octet-stream" },
86
+ });
87
+ }
88
+ }
@@ -0,0 +1,166 @@
1
+ import * as THREE from "three";
2
+ import { frame, placeAtAnchor } from "./render-frame";
3
+
4
+ // ---- decode worker pool -----------------------------------------------------
5
+ // Dequantizes node blobs off the main thread (grimoire: worker-zero-copy decode).
6
+ //
7
+ // INLINE worker: the decode logic is bundled as a source STRING -> Blob -> object
8
+ // URL classic worker. NO `new URL("./worker.ts", import.meta.url)` — that breaks
9
+ // under esbuild bundling + the platform's sandboxed iframe (Invalid URL). This
10
+ // blob-URL form works in every bundler/sandbox (same approach Spark uses).
11
+ // Blob: f64 anchor[3] | f32 scale[3] | u32 count | (u16 x,y,z + u8 r,g,b)*count.
12
+ const WORKER_SRC = `
13
+ self.onmessage = function (e) {
14
+ var id = e.data.id, buffer = e.data.buffer;
15
+ var dv = new DataView(buffer);
16
+ var ax = dv.getFloat64(0, true), ay = dv.getFloat64(8, true), az = dv.getFloat64(16, true);
17
+ var sx = dv.getFloat32(24, true), sy = dv.getFloat32(28, true), sz = dv.getFloat32(32, true);
18
+ var count = dv.getUint32(36, true);
19
+ var positions = new Float32Array(count * 3);
20
+ var colors = new Uint8Array(count * 3);
21
+ var o = 40;
22
+ for (var k = 0; k < count; k++) {
23
+ positions[k * 3] = dv.getUint16(o, true) * sx;
24
+ positions[k * 3 + 1] = dv.getUint16(o + 2, true) * sy;
25
+ positions[k * 3 + 2] = dv.getUint16(o + 4, true) * sz;
26
+ colors[k * 3] = dv.getUint8(o + 6);
27
+ colors[k * 3 + 1] = dv.getUint8(o + 7);
28
+ colors[k * 3 + 2] = dv.getUint8(o + 8);
29
+ o += 9;
30
+ }
31
+ self.postMessage({ id: id, positions: positions, colors: colors, count: count, anchor: [ax, ay, az] }, [positions.buffer, colors.buffer]);
32
+ };
33
+ `;
34
+ let _blobURL: string | null = null;
35
+ function workerURL(): string {
36
+ return (_blobURL ??= URL.createObjectURL(new Blob([WORKER_SRC], { type: "text/javascript" })));
37
+ }
38
+
39
+ type Decoded = { positions: Float32Array; colors: Uint8Array; count: number; anchor: [number, number, number] };
40
+ class DecodePool {
41
+ private workers: Worker[] = [];
42
+ private idle: Worker[] = [];
43
+ private queue: { id: number; buffer: ArrayBuffer; resolve: (d: Decoded) => void }[] = [];
44
+ private pending = new Map<number, (d: Decoded) => void>();
45
+ private nextId = 0;
46
+ constructor(n: number) {
47
+ for (let i = 0; i < n; i++) {
48
+ const w = new Worker(workerURL()); // classic blob worker (no import.meta.url, no module type)
49
+ w.onmessage = (e: MessageEvent<Decoded & { id: number }>) => {
50
+ const r = this.pending.get(e.data.id);
51
+ this.pending.delete(e.data.id);
52
+ this.idle.push(w);
53
+ r?.(e.data);
54
+ this.drain();
55
+ };
56
+ this.workers.push(w); this.idle.push(w);
57
+ }
58
+ }
59
+ decode(buffer: ArrayBuffer): Promise<Decoded> {
60
+ return new Promise((resolve) => { this.queue.push({ id: this.nextId++, buffer, resolve }); this.drain(); });
61
+ }
62
+ private drain() {
63
+ while (this.idle.length && this.queue.length) {
64
+ const w = this.idle.pop()!; const job = this.queue.shift()!;
65
+ this.pending.set(job.id, job.resolve);
66
+ w.postMessage({ id: job.id, buffer: job.buffer }, [job.buffer]); // transfer in (zero-copy)
67
+ }
68
+ }
69
+ }
70
+ // LAZY: workers are created on the FIRST decode (a points tile), never at module
71
+ // load — so merely importing this module (e.g. for the splat path) spins up nothing.
72
+ let _pool: DecodePool | null = null;
73
+ function getPool(): DecodePool {
74
+ return (_pool ??= new DecodePool(Math.min(16, Math.max(3, (navigator.hardwareConcurrency || 4) - 1))));
75
+ }
76
+ export function decodeStats() {
77
+ const p = _pool as any;
78
+ return p ? { workers: p.workers.length, idle: p.idle.length, queued: p.queue.length, pending: p.pending.size }
79
+ : { workers: 0, idle: 0, queued: 0, pending: 0 };
80
+ }
81
+
82
+ // 3DTilesRendererJS plugin: decodes our quantized point-node format into a
83
+ // node-local THREE.Points, tagged with its float64 world anchor for RTC.
84
+ //
85
+ // Node blob (sliced out of cloud.bin by byte range):
86
+ // header: f64 anchor[3] | f32 scale[3] | u32 count (40 B)
87
+ // body: per point u16 x,y,z + u8 r,g,b (9 B)
88
+ // Decoded position = stored * scale (node-local, small, float32-safe).
89
+ //
90
+ // Same `parseToMesh` seam the experiment proved with Spark: return a
91
+ // THREE.Object3D and the renderer owns transform/cull/cache/eviction. We must
92
+ // NOT be async at the top level (invokeOnePlugin treats a Promise as truthy and
93
+ // stops iterating), so non-matching content returns null synchronously.
94
+
95
+ // Shared material for all point nodes. Writes rgb + view-space log-depth (alpha)
96
+ // so the EDL post pass gets its depth field for free (no second target).
97
+ const pointUniforms = { uPointSize: { value: frame.pointSize } };
98
+
99
+ const pointMaterial = new THREE.ShaderMaterial({
100
+ uniforms: pointUniforms,
101
+ vertexShader: /* glsl */ `
102
+ attribute vec3 color;
103
+ uniform float uPointSize;
104
+ varying vec3 vColor;
105
+ varying float vLogZ;
106
+ void main() {
107
+ vColor = color;
108
+ vec4 mv = modelViewMatrix * vec4(position, 1.0);
109
+ vLogZ = log2(max(1e-6, -mv.z));
110
+ gl_Position = projectionMatrix * mv;
111
+ gl_PointSize = uPointSize;
112
+ }
113
+ `,
114
+ fragmentShader: /* glsl */ `
115
+ varying vec3 vColor;
116
+ varying float vLogZ;
117
+ void main() {
118
+ gl_FragColor = vec4(vColor, vLogZ);
119
+ }
120
+ `,
121
+ });
122
+
123
+ export function setPointSize(px: number) {
124
+ pointUniforms.uPointSize.value = px;
125
+ }
126
+
127
+ export class PointTilePlugin {
128
+ name = "POINT_TILE_PLUGIN";
129
+ tiles: any = null;
130
+
131
+ init(tiles: any) {
132
+ this.tiles = tiles;
133
+ }
134
+
135
+ // parseToMesh: hand the blob to the decode WORKER POOL (off-thread dequant +
136
+ // zero-copy transfer back) so tile loads never hitch the main thread. Must
137
+ // return null synchronously for non-matching content (a Promise is truthy and
138
+ // would stop plugin iteration); matching content returns a Promise<Points>.
139
+ parseToMesh(
140
+ buffer: ArrayBuffer,
141
+ _tile: any,
142
+ _extension: string,
143
+ uri: string,
144
+ signal: AbortSignal
145
+ ) {
146
+ if (!uri.includes(".pnt")) return null;
147
+ if (signal.aborted) return null;
148
+ return getPool().decode(buffer).then((d) => {
149
+ if (signal.aborted) return null;
150
+ const geom = new THREE.BufferGeometry();
151
+ geom.setAttribute("position", new THREE.BufferAttribute(d.positions, 3));
152
+ geom.setAttribute("color", new THREE.BufferAttribute(d.colors, 3, true));
153
+ const points = new THREE.Points(geom, pointMaterial);
154
+ points.matrixAutoUpdate = false;
155
+ points.frustumCulled = false; // renderer culls per tile already
156
+ points.userData.worldAnchor = d.anchor; // float64 world anchor for RTC
157
+ placeAtAnchor(points, d.anchor);
158
+ return points;
159
+ });
160
+ }
161
+
162
+ disposeTile(tile: any) {
163
+ const scene = tile.engineData?.scene;
164
+ if (scene && scene.geometry) scene.geometry.dispose();
165
+ }
166
+ }
@@ -0,0 +1,31 @@
1
+ import * as THREE from "three";
2
+
3
+ // Shared floating-origin (RTC) state for the whole viewer.
4
+ //
5
+ // The precision stack: authoritative coordinates live in float64 (plain JS
6
+ // numbers). `origin` is a float64 world point near the camera. Every renderable
7
+ // (point node, splat) is placed at worldAnchor - origin computed in float64,
8
+ // then assigned to a float32 Object3D.position — which is small and safe. When
9
+ // the camera travels far, we rebase `origin` and shift everything, so GPU
10
+ // coordinates never grow large no matter the absolute world position.
11
+
12
+ export const POINT_LAYER = 1;
13
+ export const SPLAT_LAYER = 2;
14
+
15
+ export const frame = {
16
+ // float64 render origin in world space
17
+ origin: [0, 0, 0] as [number, number, number],
18
+ pointSize: 3.0,
19
+ edlStrength: 0.35,
20
+ edlRadius: 1.4,
21
+ edlOn: true,
22
+ };
23
+
24
+ // Place a point node at its FULL float64 world anchor. The recentering (origin
25
+ // subtraction) is done ONCE by pointContainer.position = -origin each frame — so
26
+ // world = -origin + anchor + localOffset = (worldPoint - origin), small + precise.
27
+ // (Do NOT subtract origin here too, or it gets subtracted twice.)
28
+ export function placeAtAnchor(obj: THREE.Object3D, anchor: [number, number, number]) {
29
+ obj.position.set(anchor[0], anchor[1], anchor[2]);
30
+ obj.updateMatrix();
31
+ }
@@ -0,0 +1,51 @@
1
+ import * as THREE from "three";
2
+ import { SplatMesh } from "@sparkjsdev/spark";
3
+
4
+ // 3DTilesRendererJS plugin that renders .ply Gaussian-splat tiles via Spark —
5
+ // the proven seam from Antonio's 3d-tiles experiment, kept intact. The point of
6
+ // the slice: the SAME streaming foundation (3DTilesRendererJS) drives this splat
7
+ // branch and the point-cloud branch as peers, differing only at parseToMesh.
8
+ //
9
+ // parseToMesh must not be async at the top level (a returned Promise is treated
10
+ // as truthy and stops plugin iteration) — return null synchronously for non-ply.
11
+ export class SplatTilePlugin {
12
+ name = "SPLAT_TILE_PLUGIN";
13
+ tiles: any = null;
14
+
15
+ init(tiles: any) {
16
+ this.tiles = tiles;
17
+ }
18
+
19
+ parseToMesh(
20
+ buffer: ArrayBuffer,
21
+ _tile: any,
22
+ extension: string,
23
+ _uri: string,
24
+ signal: AbortSignal
25
+ ) {
26
+ if (extension !== "ply" && extension !== "spz") return null;
27
+ // Spark's SplatMesh sniffs the actual file type (PLY/SPZ/SPLAT/…) from the
28
+ // bytes, so both load the same way.
29
+ return (async () => {
30
+ if (signal.aborted) return null;
31
+ const splatMesh = new SplatMesh({ fileBytes: new Uint8Array(buffer) });
32
+ await splatMesh.initialized;
33
+ if (signal.aborted) {
34
+ splatMesh.dispose();
35
+ return null;
36
+ }
37
+ // Real splat count from the LOADED data — format-agnostic. (A byte probe of
38
+ // the header is fragile: SPZ is gzipped, so reading bytes off the top gives
39
+ // garbage ~2.6e9, which poisons the motion-budget governor's count and makes
40
+ // the Motion-detail slider a no-op.) packedSplats.numSplats is correct for
41
+ // every format.
42
+ splatMesh.userData.splatCount = (splatMesh as any).packedSplats?.numSplats ?? 0;
43
+ return splatMesh as unknown as THREE.Object3D;
44
+ })();
45
+ }
46
+
47
+ disposeTile(tile: any) {
48
+ const scene = tile.engineData?.scene;
49
+ if (scene && typeof scene.dispose === "function") scene.dispose();
50
+ }
51
+ }