@thatopen/services 0.3.0 → 0.3.2
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 +3 -0
- package/dist/built-in/index.d.ts +22 -2
- package/dist/cli.js +3 -3
- package/docs/ai-quickstart.md +86 -0
- package/docs/cli/create.md +4 -3
- package/package.json +3 -3
- package/src/built-in/index.ts +669 -649
- package/src/cli/templates/app/package.json +31 -31
- package/src/cli/templates/app/src/app.ts +37 -37
- package/src/cli/templates/app/src/assets/tool-placeholder.ts +50 -50
- package/src/cli/templates/app/src/main.ts +223 -340
- package/src/cli/templates/app/src/setups/camera-tools.ts +63 -63
- package/src/cli/templates/app/src/setups/clipper-tool.ts +20 -1
- package/src/cli/templates/app/src/setups/clipper.ts +75 -75
- package/src/cli/templates/app/src/setups/index.ts +23 -33
- package/src/cli/templates/app/src/setups/inspection.ts +136 -136
- package/src/cli/templates/app/src/setups/measurement-tool.ts +19 -1
- package/src/cli/templates/app/src/setups/measurements.ts +133 -133
- package/src/cli/templates/app/src/setups/reality-capture/lib/hidden-tiles-plugin.ts +88 -88
- package/src/cli/templates/app/src/setups/reality-capture/lib/point-tile-plugin.ts +166 -166
- package/src/cli/templates/app/src/setups/reality-capture/lib/render-frame.ts +31 -31
- package/src/cli/templates/app/src/setups/reality-capture/lib/splat-tile-plugin.ts +51 -51
- package/src/cli/templates/app/src/setups/reality-capture-viewer.ts +1383 -1383
- package/src/cli/templates/app/src/setups/right-sidebar.ts +178 -178
- package/src/cli/templates/app/src/setups/styles-panel.ts +110 -110
- package/src/cli/templates/app/src/setups/tool-mode-manager.ts +15 -9
- package/src/cli/templates/app/src/setups/tool-mode.ts +23 -23
- package/src/cli/templates/app/src/setups/active-tool-hud.ts +0 -69
- package/src/cli/templates/app/src/setups/clipper-panel.ts +0 -167
- package/src/cli/templates/app/src/setups/commands-panel.ts +0 -239
- package/src/cli/templates/app/src/setups/data-table-panel.ts +0 -2224
- package/src/cli/templates/app/src/setups/file-format-icons.ts +0 -95
- package/src/cli/templates/app/src/setups/files-panel.ts +0 -1503
- package/src/cli/templates/app/src/setups/fps-indicator.ts +0 -63
- package/src/cli/templates/app/src/setups/graphics-panel.ts +0 -490
- package/src/cli/templates/app/src/setups/measurement-settings-panel.ts +0 -161
- package/src/cli/templates/app/src/setups/model-tree.ts +0 -797
- package/src/cli/templates/app/src/setups/objects-panel.ts +0 -127
- package/src/cli/templates/app/src/setups/properties-panel.ts +0 -1048
- package/src/cli/templates/app/src/setups/settings-panel.ts +0 -94
- package/src/cli/templates/app/src/setups/visibility-toolbar.ts +0 -498
- package/src/cli/templates/app/src/setups/walkthrough.ts +0 -528
|
@@ -88,7 +88,7 @@ type Measurer =
|
|
|
88
88
|
| OBF.AngleMeasurement
|
|
89
89
|
| OBF.VolumeMeasurement;
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
const measurementToolImpl = (
|
|
92
92
|
components: OBC.Components,
|
|
93
93
|
): MeasurementTool => {
|
|
94
94
|
const length = components.get(OBF.LengthMeasurement);
|
|
@@ -568,3 +568,21 @@ export const measurementTool = (
|
|
|
568
568
|
},
|
|
569
569
|
};
|
|
570
570
|
};
|
|
571
|
+
|
|
572
|
+
// Real OBC.Component wrapper → per-`components` singleton resolved by UUID. The
|
|
573
|
+
// toolbar built-in and the side panels (measurement-settings, objects) share the
|
|
574
|
+
// SAME MeasurementTool because `components` is a single cross-bundle instance.
|
|
575
|
+
export class MeasurementToolComponent extends OBC.Component {
|
|
576
|
+
static readonly uuid = "9b2c7e10-3a4f-4c21-8b6d-1f0a2c3d4e52" as const;
|
|
577
|
+
enabled = true;
|
|
578
|
+
readonly tool: MeasurementTool;
|
|
579
|
+
constructor(components: OBC.Components) {
|
|
580
|
+
super(components);
|
|
581
|
+
components.add(MeasurementToolComponent.uuid, this);
|
|
582
|
+
this.tool = measurementToolImpl(components);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Per-Components singleton accessor for the measurement tool. */
|
|
587
|
+
export const measurementTool = (components: OBC.Components): MeasurementTool =>
|
|
588
|
+
components.get(MeasurementToolComponent).tool;
|
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
import * as THREE from "three";
|
|
2
|
-
import * as OBC from "@thatopen/components";
|
|
3
|
-
import * as OBF from "@thatopen/components-front";
|
|
4
|
-
import type { ModeTool } from "./tool-mode";
|
|
5
|
-
|
|
6
|
-
// Brand purple, matching the selection outline so measurements read as "ours".
|
|
7
|
-
const MEASURE_COLOR = 0x6528d7;
|
|
8
|
-
|
|
9
|
-
const canvasOf = (world: OBC.World): HTMLCanvasElement => {
|
|
10
|
-
const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
|
|
11
|
-
.domElement;
|
|
12
|
-
if (!canvas) {
|
|
13
|
-
throw new Error("measurements setup: world has no renderer with a canvas");
|
|
14
|
-
}
|
|
15
|
-
return canvas;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Length-measurement tool. Wraps `OBF.LengthMeasurement` as a toolbar mode tool:
|
|
20
|
-
*
|
|
21
|
-
* - double-click places measurement points (snapping to vertices/edges); a
|
|
22
|
-
* dimension line + label is drawn between them,
|
|
23
|
-
* - Escape cancels the in-progress measurement (handled by the component),
|
|
24
|
-
* - Delete / Backspace → delete the measurement under the cursor,
|
|
25
|
-
* - Shift + Delete / Shift + Backspace → clear ALL measurements.
|
|
26
|
-
*
|
|
27
|
-
* Finished measurements persist when the mode is exited.
|
|
28
|
-
*/
|
|
29
|
-
export const lengthMeasurement = (
|
|
30
|
-
components: OBC.Components,
|
|
31
|
-
world: OBC.World,
|
|
32
|
-
): ModeTool => {
|
|
33
|
-
const measurer = components.get(OBF.LengthMeasurement);
|
|
34
|
-
measurer.world = world;
|
|
35
|
-
measurer.color = new THREE.Color(MEASURE_COLOR);
|
|
36
|
-
const canvas = canvasOf(world);
|
|
37
|
-
|
|
38
|
-
const onDblClick = () => measurer.create();
|
|
39
|
-
|
|
40
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
41
|
-
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
42
|
-
if (event.shiftKey) {
|
|
43
|
-
measurer.list.clear();
|
|
44
|
-
} else {
|
|
45
|
-
void measurer.delete();
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
let active = false;
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
label: "Measure length",
|
|
53
|
-
icon: "mdi:ruler",
|
|
54
|
-
activate() {
|
|
55
|
-
if (active) return;
|
|
56
|
-
active = true;
|
|
57
|
-
measurer.enabled = true;
|
|
58
|
-
canvas.addEventListener("dblclick", onDblClick);
|
|
59
|
-
window.addEventListener("keydown", onKeyDown);
|
|
60
|
-
canvas.style.cursor = "crosshair";
|
|
61
|
-
},
|
|
62
|
-
deactivate() {
|
|
63
|
-
if (!active) return;
|
|
64
|
-
active = false;
|
|
65
|
-
measurer.cancelCreation(); // drop any half-finished measurement
|
|
66
|
-
measurer.enabled = false;
|
|
67
|
-
canvas.removeEventListener("dblclick", onDblClick);
|
|
68
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
69
|
-
canvas.style.cursor = "";
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Area-measurement tool. Wraps `OBF.AreaMeasurement` as a toolbar mode tool:
|
|
76
|
-
*
|
|
77
|
-
* - double-click adds polygon points; Enter closes/finalizes the polygon and
|
|
78
|
-
* shows the filled area + value,
|
|
79
|
-
* - Escape cancels the in-progress polygon (handled by the component),
|
|
80
|
-
* - Delete / Backspace → delete the area under the cursor,
|
|
81
|
-
* - Shift + Delete / Shift + Backspace → clear ALL areas.
|
|
82
|
-
*
|
|
83
|
-
* NOTE: the click-to-add-point vs. double-click-to-create flow is worth a live
|
|
84
|
-
* check; tuned here to mirror the length tool's double-click entry.
|
|
85
|
-
*/
|
|
86
|
-
export const areaMeasurement = (
|
|
87
|
-
components: OBC.Components,
|
|
88
|
-
world: OBC.World,
|
|
89
|
-
): ModeTool => {
|
|
90
|
-
const measurer = components.get(OBF.AreaMeasurement);
|
|
91
|
-
measurer.world = world;
|
|
92
|
-
measurer.color = new THREE.Color(MEASURE_COLOR);
|
|
93
|
-
const canvas = canvasOf(world);
|
|
94
|
-
|
|
95
|
-
const onDblClick = () => measurer.create();
|
|
96
|
-
|
|
97
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
98
|
-
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
|
99
|
-
measurer.endCreation();
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
103
|
-
if (event.shiftKey) {
|
|
104
|
-
measurer.list.clear();
|
|
105
|
-
} else {
|
|
106
|
-
void measurer.delete();
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
let active = false;
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
label: "Measure area",
|
|
114
|
-
icon: "mdi:vector-square",
|
|
115
|
-
activate() {
|
|
116
|
-
if (active) return;
|
|
117
|
-
active = true;
|
|
118
|
-
measurer.enabled = true;
|
|
119
|
-
canvas.addEventListener("dblclick", onDblClick);
|
|
120
|
-
window.addEventListener("keydown", onKeyDown);
|
|
121
|
-
canvas.style.cursor = "crosshair";
|
|
122
|
-
},
|
|
123
|
-
deactivate() {
|
|
124
|
-
if (!active) return;
|
|
125
|
-
active = false;
|
|
126
|
-
measurer.cancelCreation();
|
|
127
|
-
measurer.enabled = false;
|
|
128
|
-
canvas.removeEventListener("dblclick", onDblClick);
|
|
129
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
130
|
-
canvas.style.cursor = "";
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
};
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import * as OBC from "@thatopen/components";
|
|
3
|
+
import * as OBF from "@thatopen/components-front";
|
|
4
|
+
import type { ModeTool } from "./tool-mode";
|
|
5
|
+
|
|
6
|
+
// Brand purple, matching the selection outline so measurements read as "ours".
|
|
7
|
+
const MEASURE_COLOR = 0x6528d7;
|
|
8
|
+
|
|
9
|
+
const canvasOf = (world: OBC.World): HTMLCanvasElement => {
|
|
10
|
+
const canvas = (world.renderer as OBF.PostproductionRenderer | null)?.three
|
|
11
|
+
.domElement;
|
|
12
|
+
if (!canvas) {
|
|
13
|
+
throw new Error("measurements setup: world has no renderer with a canvas");
|
|
14
|
+
}
|
|
15
|
+
return canvas;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Length-measurement tool. Wraps `OBF.LengthMeasurement` as a toolbar mode tool:
|
|
20
|
+
*
|
|
21
|
+
* - double-click places measurement points (snapping to vertices/edges); a
|
|
22
|
+
* dimension line + label is drawn between them,
|
|
23
|
+
* - Escape cancels the in-progress measurement (handled by the component),
|
|
24
|
+
* - Delete / Backspace → delete the measurement under the cursor,
|
|
25
|
+
* - Shift + Delete / Shift + Backspace → clear ALL measurements.
|
|
26
|
+
*
|
|
27
|
+
* Finished measurements persist when the mode is exited.
|
|
28
|
+
*/
|
|
29
|
+
export const lengthMeasurement = (
|
|
30
|
+
components: OBC.Components,
|
|
31
|
+
world: OBC.World,
|
|
32
|
+
): ModeTool => {
|
|
33
|
+
const measurer = components.get(OBF.LengthMeasurement);
|
|
34
|
+
measurer.world = world;
|
|
35
|
+
measurer.color = new THREE.Color(MEASURE_COLOR);
|
|
36
|
+
const canvas = canvasOf(world);
|
|
37
|
+
|
|
38
|
+
const onDblClick = () => measurer.create();
|
|
39
|
+
|
|
40
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
41
|
+
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
42
|
+
if (event.shiftKey) {
|
|
43
|
+
measurer.list.clear();
|
|
44
|
+
} else {
|
|
45
|
+
void measurer.delete();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let active = false;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
label: "Measure length",
|
|
53
|
+
icon: "mdi:ruler",
|
|
54
|
+
activate() {
|
|
55
|
+
if (active) return;
|
|
56
|
+
active = true;
|
|
57
|
+
measurer.enabled = true;
|
|
58
|
+
canvas.addEventListener("dblclick", onDblClick);
|
|
59
|
+
window.addEventListener("keydown", onKeyDown);
|
|
60
|
+
canvas.style.cursor = "crosshair";
|
|
61
|
+
},
|
|
62
|
+
deactivate() {
|
|
63
|
+
if (!active) return;
|
|
64
|
+
active = false;
|
|
65
|
+
measurer.cancelCreation(); // drop any half-finished measurement
|
|
66
|
+
measurer.enabled = false;
|
|
67
|
+
canvas.removeEventListener("dblclick", onDblClick);
|
|
68
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
69
|
+
canvas.style.cursor = "";
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Area-measurement tool. Wraps `OBF.AreaMeasurement` as a toolbar mode tool:
|
|
76
|
+
*
|
|
77
|
+
* - double-click adds polygon points; Enter closes/finalizes the polygon and
|
|
78
|
+
* shows the filled area + value,
|
|
79
|
+
* - Escape cancels the in-progress polygon (handled by the component),
|
|
80
|
+
* - Delete / Backspace → delete the area under the cursor,
|
|
81
|
+
* - Shift + Delete / Shift + Backspace → clear ALL areas.
|
|
82
|
+
*
|
|
83
|
+
* NOTE: the click-to-add-point vs. double-click-to-create flow is worth a live
|
|
84
|
+
* check; tuned here to mirror the length tool's double-click entry.
|
|
85
|
+
*/
|
|
86
|
+
export const areaMeasurement = (
|
|
87
|
+
components: OBC.Components,
|
|
88
|
+
world: OBC.World,
|
|
89
|
+
): ModeTool => {
|
|
90
|
+
const measurer = components.get(OBF.AreaMeasurement);
|
|
91
|
+
measurer.world = world;
|
|
92
|
+
measurer.color = new THREE.Color(MEASURE_COLOR);
|
|
93
|
+
const canvas = canvasOf(world);
|
|
94
|
+
|
|
95
|
+
const onDblClick = () => measurer.create();
|
|
96
|
+
|
|
97
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
98
|
+
if (event.code === "Enter" || event.code === "NumpadEnter") {
|
|
99
|
+
measurer.endCreation();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (event.code !== "Delete" && event.code !== "Backspace") return;
|
|
103
|
+
if (event.shiftKey) {
|
|
104
|
+
measurer.list.clear();
|
|
105
|
+
} else {
|
|
106
|
+
void measurer.delete();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let active = false;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
label: "Measure area",
|
|
114
|
+
icon: "mdi:vector-square",
|
|
115
|
+
activate() {
|
|
116
|
+
if (active) return;
|
|
117
|
+
active = true;
|
|
118
|
+
measurer.enabled = true;
|
|
119
|
+
canvas.addEventListener("dblclick", onDblClick);
|
|
120
|
+
window.addEventListener("keydown", onKeyDown);
|
|
121
|
+
canvas.style.cursor = "crosshair";
|
|
122
|
+
},
|
|
123
|
+
deactivate() {
|
|
124
|
+
if (!active) return;
|
|
125
|
+
active = false;
|
|
126
|
+
measurer.cancelCreation();
|
|
127
|
+
measurer.enabled = false;
|
|
128
|
+
canvas.removeEventListener("dblclick", onDblClick);
|
|
129
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
130
|
+
canvas.style.cursor = "";
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
};
|
|
@@ -1,88 +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
|
-
}
|
|
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
|
+
}
|