u-space 0.0.28 → 0.0.30

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 (73) hide show
  1. package/README.md +9 -0
  2. package/dist/Viewer-BKNV67Jj.cjs +1 -0
  3. package/dist/Viewer-Cs_y7IiH.js +1256 -0
  4. package/dist/index.cjs +3 -3
  5. package/dist/index.js +1715 -1497
  6. package/dist/plugins/atmosphere/AgxToneMapping.d.ts +1 -1
  7. package/dist/plugins/atmosphere/index.cjs +1 -1
  8. package/dist/plugins/atmosphere/index.js +4 -4
  9. package/dist/plugins/curve-movement/index.cjs +1 -1
  10. package/dist/plugins/curve-movement/index.js +1 -1
  11. package/dist/plugins/fire/index.cjs +1 -1
  12. package/dist/plugins/fire/index.js +5 -5
  13. package/dist/plugins/object-controls/index.cjs +1 -1
  14. package/dist/plugins/object-controls/index.js +4 -4
  15. package/dist/plugins/tiles/index.cjs +1 -1
  16. package/dist/plugins/tiles/index.js +5 -5
  17. package/dist/plugins/u-manager/index.cjs +51 -51
  18. package/dist/plugins/u-manager/index.d.ts +1 -1
  19. package/dist/plugins/u-manager/index.js +8330 -8637
  20. package/dist/plugins/u-manager/loaders/SceneEditableBatchLayer.d.ts +17 -0
  21. package/dist/plugins/u-manager/loaders/SceneInstancedLayer.d.ts +3 -3
  22. package/dist/plugins/u-manager/loaders/SceneLoader.d.ts +20 -3
  23. package/dist/plugins/u-manager/loaders/UManagerLoader.d.ts +4 -4
  24. package/dist/plugins/u-manager/semantics/objects/BuildingGroup.d.ts +12 -5
  25. package/dist/plugins/u-manager/semantics/objects/FacilityInstancedLayer.d.ts +4 -4
  26. package/dist/plugins/u-manager/semantics/objects/FloorMesh.d.ts +28 -22
  27. package/dist/plugins/u-manager/semantics/objects/SemanticGroup.d.ts +6 -6
  28. package/dist/protocol-C-OMPz15.js +10 -0
  29. package/dist/protocol-IUAzx0lk.cjs +1 -0
  30. package/dist/src/batches/EditableGeometryBatchLayer.d.ts +50 -0
  31. package/dist/src/batches/ModelInstancedLayer.d.ts +42 -0
  32. package/dist/src/batches/index.d.ts +2 -0
  33. package/dist/src/effects/TSLEffects.d.ts +24 -1
  34. package/dist/src/index.d.ts +2 -0
  35. package/dist/src/instances/InstanceObject.d.ts +57 -0
  36. package/dist/src/instances/index.d.ts +1 -0
  37. package/dist/src/interactions/MeshBVHRaycast.d.ts +9 -0
  38. package/dist/src/interactions/index.d.ts +1 -0
  39. package/dist/src/viewers/RenderPipeline.d.ts +37 -0
  40. package/dist/src/viewers/ReversedDepthSSGICompat.d.ts +10 -0
  41. package/dist/src/viewers/ReversedDepthSSRCompat.d.ts +20 -0
  42. package/dist/src/viewers/Viewer.d.ts +3 -1
  43. package/dist/src/viewers/renderInvalidation.d.ts +2 -0
  44. package/dist/src/worker/FrameTimingWindow.d.ts +16 -0
  45. package/dist/src/worker/OffscreenViewerHost.d.ts +20 -0
  46. package/dist/src/worker/WorkerDomTarget.d.ts +44 -0
  47. package/dist/src/worker/createWorkerViewer.d.ts +18 -0
  48. package/dist/src/worker/index.d.ts +2 -0
  49. package/dist/src/worker/installWorkerImageLoader.d.ts +5 -0
  50. package/dist/src/worker/protocol.d.ts +102 -0
  51. package/dist/src/worker/runtime.d.ts +2 -0
  52. package/dist/worker/index.cjs +1 -0
  53. package/dist/worker/index.js +224 -0
  54. package/dist/worker/runtime.cjs +1 -0
  55. package/dist/worker/runtime.js +418 -0
  56. package/docs/api-batches.md +112 -0
  57. package/docs/api-effects.md +2 -2
  58. package/docs/api-interactions.md +8 -0
  59. package/docs/api-managers.md +1 -1
  60. package/docs/api-objects.md +207 -0
  61. package/docs/api-plugin-u-manager.md +245 -96
  62. package/docs/api-render-pipeline.md +103 -5
  63. package/docs/api-viewer.md +188 -1
  64. package/docs/changelog.md +69 -6
  65. package/docs/examples-guide.md +111 -17
  66. package/docs/getting-started.md +1 -1
  67. package/docs/index.md +11 -4
  68. package/docs/mcp.md +29 -14
  69. package/docs/release.md +95 -0
  70. package/package.json +27 -4
  71. package/dist/plugins/u-manager/instances/SemanticInstanceObject.d.ts +0 -37
  72. package/dist/plugins/u-manager/instances/SemanticModelInstancedLayer.d.ts +0 -33
  73. package/dist/plugins/u-manager/instances/index.d.ts +0 -2
@@ -0,0 +1,418 @@
1
+ import { s as W } from "../protocol-C-OMPz15.js";
2
+ import { V as D } from "../Viewer-Cs_y7IiH.js";
3
+ import { ImageLoader as A, Cache as z } from "three/webgpu";
4
+ const C = /* @__PURE__ */ new WeakMap();
5
+ class E {
6
+ type;
7
+ timeStamp = performance.now();
8
+ target;
9
+ currentTarget;
10
+ cancelable = !0;
11
+ bubbles = !0;
12
+ defaultPrevented = !1;
13
+ cancelBubble = !1;
14
+ immediatePropagationStopped = !1;
15
+ pointerId = 0;
16
+ pointerType = "mouse";
17
+ button = 0;
18
+ buttons = 0;
19
+ altKey = !1;
20
+ ctrlKey = !1;
21
+ metaKey = !1;
22
+ shiftKey = !1;
23
+ movementX = 0;
24
+ movementY = 0;
25
+ clientX = 0;
26
+ clientY = 0;
27
+ offsetX = 0;
28
+ offsetY = 0;
29
+ pageX = 0;
30
+ pageY = 0;
31
+ screenX = 0;
32
+ screenY = 0;
33
+ deltaX = 0;
34
+ deltaY = 0;
35
+ deltaZ = 0;
36
+ deltaMode = 0;
37
+ constructor(e, t) {
38
+ this.type = e.type, this.target = t, this.currentTarget = t;
39
+ for (const [r, a] of Object.entries(e))
40
+ r !== "type" && a !== void 0 && Reflect.set(this, r, a);
41
+ }
42
+ preventDefault() {
43
+ this.defaultPrevented = !0;
44
+ }
45
+ stopPropagation() {
46
+ this.cancelBubble = !0;
47
+ }
48
+ stopImmediatePropagation() {
49
+ this.cancelBubble = !0, this.immediatePropagationStopped = !0;
50
+ }
51
+ composedPath() {
52
+ return [this.target];
53
+ }
54
+ }
55
+ class y {
56
+ style = { cssText: "" };
57
+ innerText = "";
58
+ textContent = "";
59
+ className = "";
60
+ parentElement = null;
61
+ children = [];
62
+ appendChild(e) {
63
+ return this.children.push(e), e instanceof y && (e.parentElement = this), e;
64
+ }
65
+ removeChild(e) {
66
+ const t = this.children.indexOf(e);
67
+ return t >= 0 && this.children.splice(t, 1), e instanceof y && (e.parentElement = null), e;
68
+ }
69
+ remove() {
70
+ this.parentElement?.removeChild(this);
71
+ }
72
+ setAttribute() {
73
+ }
74
+ addEventListener() {
75
+ }
76
+ removeEventListener() {
77
+ }
78
+ getContext() {
79
+ return null;
80
+ }
81
+ }
82
+ function F(s) {
83
+ const e = (t) => t.toLowerCase() === "canvas" ? new OffscreenCanvas(1, 1) : new y();
84
+ return {
85
+ body: s,
86
+ documentElement: s,
87
+ pointerLockElement: null,
88
+ visibilityState: "visible",
89
+ createElement: e,
90
+ createElementNS: (t, r) => e(r),
91
+ addEventListener: s.addEventListener.bind(s),
92
+ removeEventListener: s.removeEventListener.bind(s),
93
+ exitPointerLock() {
94
+ }
95
+ };
96
+ }
97
+ function c(s, e, t) {
98
+ Object.defineProperty(s, e, {
99
+ configurable: !0,
100
+ value: t
101
+ });
102
+ }
103
+ function v(s, e, t) {
104
+ Object.defineProperty(s, e, {
105
+ configurable: !0,
106
+ get: t
107
+ });
108
+ }
109
+ function O(s, e) {
110
+ const t = {
111
+ size: { ...e },
112
+ listeners: /* @__PURE__ */ new Map(),
113
+ listenerWrappers: /* @__PURE__ */ new WeakMap()
114
+ };
115
+ C.set(s, t);
116
+ const r = s, a = {
117
+ cssText: "",
118
+ position: "relative",
119
+ touchAction: "none",
120
+ width: `${e.width}px`,
121
+ height: `${e.height}px`
122
+ }, l = (i, p) => {
123
+ if (!p) return;
124
+ let d;
125
+ typeof p == "function" ? d = p : (d = t.listenerWrappers.get(p) ?? ((b) => p.handleEvent(b)), t.listenerWrappers.set(p, d));
126
+ let h = t.listeners.get(i);
127
+ h || (h = /* @__PURE__ */ new Set(), t.listeners.set(i, h)), h.add(d);
128
+ }, u = (i, p) => {
129
+ if (!p) return;
130
+ const d = t.listeners.get(i);
131
+ if (d)
132
+ if (typeof p == "function")
133
+ d.delete(p);
134
+ else {
135
+ const h = t.listenerWrappers.get(p);
136
+ h && d.delete(h);
137
+ }
138
+ };
139
+ c(r, "style", a), c(r, "addEventListener", l), c(r, "removeEventListener", u), c(r, "appendChild", (i) => i), c(r, "removeChild", (i) => i), c(r, "setAttribute", () => {
140
+ }), c(r, "removeAttribute", () => {
141
+ }), c(r, "setPointerCapture", () => {
142
+ }), c(r, "releasePointerCapture", () => {
143
+ }), c(r, "hasPointerCapture", () => !1), c(r, "requestPointerLock", () => Promise.resolve()), c(r, "focus", () => {
144
+ }), c(r, "remove", () => {
145
+ }), v(r, "clientWidth", () => t.size.width), v(r, "clientHeight", () => t.size.height), v(r, "offsetWidth", () => t.size.width), v(r, "offsetHeight", () => t.size.height), v(r, "offsetTop", () => t.size.top), v(r, "offsetLeft", () => t.size.left), v(r, "clientTop", () => t.size.top), v(r, "clientLeft", () => t.size.left), v(r, "pageXOffset", () => t.size.left), v(r, "pageYOffset", () => t.size.top), v(r, "parentElement", () => r), c(r, "getBoundingClientRect", () => {
146
+ const { width: i, height: p, top: d, left: h } = t.size;
147
+ return {
148
+ width: i,
149
+ height: p,
150
+ top: d,
151
+ left: h,
152
+ right: h + i,
153
+ bottom: d + p,
154
+ x: h,
155
+ y: d,
156
+ toJSON: () => ({ width: i, height: p, top: d, left: h })
157
+ };
158
+ });
159
+ const f = F(r);
160
+ return c(r, "ownerDocument", f), c(r, "documentElement", r), c(r, "devicePixelRatio", e.pixelRatio), $(r, f), r;
161
+ }
162
+ function I(s, e) {
163
+ const t = C.get(s);
164
+ if (!t) throw new Error("The OffscreenCanvas has not been configured as a Worker DOM target.");
165
+ t.size = { ...e }, s.style.width = `${e.width}px`, s.style.height = `${e.height}px`, c(s, "devicePixelRatio", e.pixelRatio);
166
+ }
167
+ function V(s, e) {
168
+ const t = C.get(s);
169
+ if (!t) throw new Error("The OffscreenCanvas has not been configured as a Worker DOM target.");
170
+ const r = new E(e, s), a = [...t.listeners.get(e.type) ?? []];
171
+ for (const l of a)
172
+ if (l(r), r.immediatePropagationStopped) break;
173
+ return r;
174
+ }
175
+ function $(s, e) {
176
+ const t = globalThis;
177
+ t.window = s, t.document = e, t.getComputedStyle = () => ({ position: "relative" }), t.PointerEvent = E, t.MouseEvent = E, t.WheelEvent = E, t.Image = y;
178
+ }
179
+ const M = /* @__PURE__ */ new Map();
180
+ let P = !1;
181
+ function B() {
182
+ P || (P = !0, A.prototype.load = function(e, t, r, a) {
183
+ let l = e;
184
+ this.path !== void 0 && (l = this.path + l), l = this.manager.resolveURL(l);
185
+ const u = z.get(`image:${l}`);
186
+ if (this.manager.itemStart(l), u)
187
+ return queueMicrotask(() => {
188
+ t?.(u), this.manager.itemEnd(l);
189
+ }), u;
190
+ let f = M.get(l);
191
+ return f || (f = fetch(l).then((i) => {
192
+ if (!i.ok) throw new Error(`Failed to load image ${l}: ${i.status} ${i.statusText}`);
193
+ return i.blob();
194
+ }).then(
195
+ (i) => createImageBitmap(i, {
196
+ colorSpaceConversion: "none",
197
+ premultiplyAlpha: "none"
198
+ })
199
+ ).then((i) => (z.add(`image:${l}`, i), i)).finally(() => M.delete(l)), M.set(l, f)), f.then((i) => {
200
+ t?.(i), this.manager.itemEnd(l);
201
+ }).catch((i) => {
202
+ a?.(i), this.manager.itemError(l), this.manager.itemEnd(l);
203
+ }), {};
204
+ });
205
+ }
206
+ const _ = 1e3, j = 100, U = 25;
207
+ class X {
208
+ windowMs;
209
+ idleResetMs;
210
+ missedFrameThresholdMs;
211
+ #e = [];
212
+ #t = null;
213
+ constructor(e = {}) {
214
+ this.windowMs = e.windowMs ?? _, this.idleResetMs = e.idleResetMs ?? j, this.missedFrameThresholdMs = e.missedFrameThresholdMs ?? U;
215
+ }
216
+ add(e, t = performance.now()) {
217
+ return !Number.isFinite(e) || e <= 0 || !Number.isFinite(t) ? this.snapshot(t) : (this.#t !== null && t - this.#t >= this.idleResetMs && (this.#e.length = 0), this.#t = t, e < this.idleResetMs && this.#e.push({ at: t, duration: e }), this.snapshot(t));
218
+ }
219
+ snapshot(e = performance.now()) {
220
+ this.#r(e);
221
+ const t = this.#e.map(({ duration: f }) => f);
222
+ if (t.length === 0)
223
+ return {
224
+ samples: 0,
225
+ fps: 0,
226
+ average: 0,
227
+ p95: 0,
228
+ max: 0,
229
+ missedFramePercent: 0
230
+ };
231
+ const r = [...t].sort((f, i) => f - i), a = t.reduce((f, i) => f + i, 0) / t.length, l = Math.max(0, Math.ceil(r.length * 0.95) - 1), u = t.filter(
232
+ (f) => f > this.missedFrameThresholdMs
233
+ ).length;
234
+ return {
235
+ samples: t.length,
236
+ fps: 1e3 / a,
237
+ average: a,
238
+ p95: r[l],
239
+ max: r[r.length - 1],
240
+ missedFramePercent: u / t.length * 100
241
+ };
242
+ }
243
+ reset() {
244
+ this.#e.length = 0, this.#t = null;
245
+ }
246
+ #r(e) {
247
+ const t = e - this.windowMs;
248
+ for (; this.#e.length > 0 && this.#e[0].at < t; )
249
+ this.#e.shift();
250
+ }
251
+ }
252
+ function x(s, e) {
253
+ const { render: t } = s.renderer.info;
254
+ return {
255
+ drawCalls: t.drawCalls,
256
+ frameCalls: t.frameCalls,
257
+ triangles: t.triangles,
258
+ points: t.points,
259
+ lines: t.lines,
260
+ frameTime: e.average,
261
+ frameTiming: e
262
+ };
263
+ }
264
+ function H() {
265
+ const s = globalThis, e = {
266
+ viewer: null,
267
+ canvas: null,
268
+ domTarget: null,
269
+ initialized: !1,
270
+ disposed: !1,
271
+ ready: !1,
272
+ lastStatsAt: 0
273
+ }, t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Set(), a = /* @__PURE__ */ new WeakSet();
274
+ let l = () => {
275
+ }, u = () => {
276
+ };
277
+ const f = new Promise((n, o) => {
278
+ l = n, u = o;
279
+ }), i = (n, o) => {
280
+ e.disposed && n.type !== "disposed" || s.postMessage(n, o);
281
+ }, p = (n) => {
282
+ if (n && typeof n == "object") {
283
+ if (a.has(n)) return;
284
+ a.add(n);
285
+ }
286
+ i({ type: "error", error: W(n) });
287
+ }, d = (n, o) => {
288
+ if (t.has(n))
289
+ throw new Error(`Worker viewer command already exists: ${n}`);
290
+ return t.set(n, o), () => {
291
+ t.get(n) === o && t.delete(n);
292
+ };
293
+ }, h = async (n) => {
294
+ if (e.initialized || e.viewer)
295
+ throw new Error("The Worker viewer has already initialized.");
296
+ if (e.disposed) throw new Error("The Worker viewer has been disposed.");
297
+ if (!s.navigator.gpu)
298
+ throw new Error("WebGPU is not exposed in this Worker. Use a current Chromium browser or the main-thread Viewer.");
299
+ B(), e.canvas = n.canvas, e.domTarget = O(n.canvas, n.size);
300
+ const o = new D({
301
+ el: e.domTarget,
302
+ pixelRatio: n.size.pixelRatio,
303
+ rendererOptions: {
304
+ canvas: n.canvas,
305
+ forceWebGL: !1
306
+ }
307
+ });
308
+ e.viewer = o;
309
+ const m = new X();
310
+ o.addEventListener("afterRender", ({ delta: w }) => {
311
+ const g = performance.now(), R = m.add(w * 1e3, g);
312
+ g - e.lastStatsAt < 250 || (e.lastStatsAt = g, i({ type: "stats", stats: x(o, R) }));
313
+ }), d("getStats", () => x(o, m.snapshot())), d("getViewpoint", () => o.controls.getCameraViewpoint()), d("setViewpoint", async (w) => {
314
+ const g = w;
315
+ return await o.controls.setCameraViewpoint(g.viewpoint, g.enableTransition ?? !0), o.controls.getCameraViewpoint();
316
+ }), d("render", async () => (await o.render(), x(o, m.snapshot())));
317
+ const k = {
318
+ viewer: o,
319
+ get canvas() {
320
+ return n.canvas;
321
+ },
322
+ status(w) {
323
+ i({ type: "status", message: w });
324
+ },
325
+ emit(w, g) {
326
+ i({ type: "event", eventType: w, detail: g });
327
+ },
328
+ registerCommand: d,
329
+ onDispose(w) {
330
+ if (e.disposed) throw new Error("The Worker viewer has been disposed.");
331
+ return r.add(w), () => r.delete(w);
332
+ },
333
+ ready(w) {
334
+ if (e.disposed) throw new Error("The Worker viewer has been disposed.");
335
+ if (e.ready) throw new Error("The Worker viewer is already ready.");
336
+ i({ type: "ready", detail: w }), e.ready = !0;
337
+ }
338
+ };
339
+ if (await o.init(), e.disposed) throw new Error("The Worker viewer was disposed during initialization.");
340
+ e.initialized = !0, i({ type: "initialized" }), l(k);
341
+ }, b = (n) => {
342
+ !e.viewer || !e.domTarget || (I(e.domTarget, n.size), e.viewer.renderer.setPixelRatio(n.size.pixelRatio), e.viewer.onWindowResize());
343
+ }, S = async (n) => {
344
+ try {
345
+ if (!e.initialized) throw new Error("The Worker viewer has not initialized.");
346
+ const o = t.get(n.command);
347
+ if (!o) throw new Error(`Unknown Worker viewer command: ${n.command}`);
348
+ const m = await o(n.payload), k = m instanceof ArrayBuffer ? [m] : Y(m);
349
+ i({ type: "response", id: n.id, result: m }, k);
350
+ } catch (o) {
351
+ i({ type: "response", id: n.id, error: W(o) });
352
+ }
353
+ }, L = async () => {
354
+ if (e.disposed) return;
355
+ e.disposed = !0;
356
+ const n = [];
357
+ for (const o of [...r].reverse())
358
+ try {
359
+ await o();
360
+ } catch (m) {
361
+ n.push(m);
362
+ }
363
+ r.clear();
364
+ try {
365
+ e.viewer?.dispose();
366
+ } catch (o) {
367
+ n.push(o);
368
+ }
369
+ e.viewer = null, e.canvas = null, e.domTarget = null, e.initialized || u(new Error("The Worker viewer was disposed before initialization."));
370
+ for (const o of n)
371
+ s.postMessage({ type: "error", error: W(o) });
372
+ s.postMessage({ type: "disposed" }), s.close();
373
+ }, T = (n) => {
374
+ p(n), e.ready || L();
375
+ };
376
+ return s.addEventListener("error", (n) => {
377
+ T(n.error ?? new Error(n.message)), n.preventDefault();
378
+ }), s.addEventListener("unhandledrejection", (n) => {
379
+ T(n.reason), n.preventDefault();
380
+ }), s.addEventListener("message", (n) => {
381
+ const o = n.data;
382
+ o.type === "init" ? h(o).catch((m) => {
383
+ u(m), T(m);
384
+ }) : o.type === "resize" ? b(o) : o.type === "dom-event" ? e.domTarget && V(e.domTarget, o.event) : o.type === "request" ? S(o) : o.type === "dispose" && L();
385
+ }), f;
386
+ }
387
+ function Y(s) {
388
+ const e = /* @__PURE__ */ new Set(), t = /* @__PURE__ */ new WeakSet(), r = (a) => {
389
+ if (!(!a || typeof a != "object")) {
390
+ if (a instanceof ArrayBuffer) {
391
+ e.add(a);
392
+ return;
393
+ }
394
+ if (ArrayBuffer.isView(a)) {
395
+ a.buffer instanceof ArrayBuffer && e.add(a.buffer);
396
+ return;
397
+ }
398
+ if (!t.has(a)) {
399
+ if (t.add(a), a instanceof Map) {
400
+ a.forEach((l, u) => {
401
+ r(u), r(l);
402
+ });
403
+ return;
404
+ }
405
+ if (a instanceof Set) {
406
+ a.forEach(r);
407
+ return;
408
+ }
409
+ Object.values(a).forEach(r);
410
+ }
411
+ }
412
+ };
413
+ return r(s), [...e];
414
+ }
415
+ export {
416
+ H as createWorkerViewer,
417
+ W as serializeError
418
+ };
@@ -0,0 +1,112 @@
1
+ # Batches API
2
+
3
+ `u-space` 的 `src/batches` 模块提供两种共享渲染聚合层。`ModelInstancedLayer` 面向重复模板的 `InstancedMesh` 渲染;`EditableGeometryBatchLayer` 面向大量普通、主要静态但仍需按对象控制的 Geometry 烘焙合并。两者都从顶层 `u-space` 导出,并使用 `InstanceObject` 作为逻辑对象协议。
4
+
5
+ ```typescript
6
+ import {
7
+ EditableGeometryBatchLayer,
8
+ InstanceObject,
9
+ ModelInstancedLayer,
10
+ type EditableGeometryBatchOptions,
11
+ type EditableGeometryBatchStats,
12
+ } from 'u-space';
13
+ ```
14
+
15
+ ## 职责边界
16
+
17
+ | 模块 | 职责 |
18
+ | :--- | :--- |
19
+ | `src/instances` | `InstanceObject` 的身份、transform、样式、bounds、dirty 和 materialize 协议。 |
20
+ | `src/batches` | `InstanceObject` 对应的批量渲染、GPU 状态同步、raycast remap 和资源生命周期。 |
21
+ | 插件/业务 Loader | 模型加载、URL/业务 ID、unsupported fallback 和场景树装配策略。 |
22
+
23
+ ## `ModelInstancedLayer`
24
+
25
+ `ModelInstancedLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它按模板 key 创建 `InstancedMesh` batch,保留模板中的独立 Mesh 以维持多材质、透明排序和局部包围体,并在实例 dirty 时同步 matrix、颜色、透明度与可见实例集合。
26
+
27
+ 常用方法:
28
+
29
+ | 方法 | 说明 |
30
+ | :--- | :--- |
31
+ | `reserveBatch(key, template, capacity)` | 为模板预分配实例容量。 |
32
+ | `addInstance(key, template, instance)` / `addInstances(...)` | 添加一个或多个逻辑实例。 |
33
+ | `getInstances()` / `getInstanceById(id)` | 枚举实例或按 `instanceId` 查询。 |
34
+ | `removeInstance()` / `removeInstances()` | 按对象或 ID 删除实例。 |
35
+ | `removeBatch(key)` / `clearBatches()` | 删除一个 batch 或清空全部 batch。 |
36
+ | `setInstanceCulling(options)` | 配置可选的逐实例视锥和屏幕尺寸裁剪。 |
37
+
38
+ 复杂静态模板在射线通过实例包围体后会按需建立 `three-mesh-bvh`;命中会映射回对应 `InstanceObject`。修改实例 transform 时只需操作实例对象;根 Scene 冻结自动矩阵更新时,再调用 `instance.updateWorldMatrix(true, false)` 和 `viewer.invalidate()`。
39
+
40
+ ## `EditableGeometryBatchLayer`
41
+
42
+ `EditableGeometryBatchLayer<T extends InstanceObject>` 继承自 `BaseGroup`。它把兼容 Mesh 的 transform 烘焙进克隆 Geometry,按材质、Geometry attribute/index 布局、阴影和 `renderOrder` 分组,再通过 `mergeGeometries()` 合并为少量内部 `BaseMesh`。内部 Mesh 是实现细节,不从公共 API 导出。
43
+
44
+ ### 配置与统计
45
+
46
+ | `EditableGeometryBatchOptions` 字段 | 默认值 | 说明 |
47
+ | :--- | :--- | :--- |
48
+ | `maxVerticesPerBatch` | `1_500_000` | 单个 merged Geometry 的最大顶点数。 |
49
+ | `maxIndicesPerBatch` | `4_500_000` | 单个 merged Geometry 的最大索引数。 |
50
+ | `freezeAnimations` | `false` | 是否允许烘焙带 animation clip/mixer 模板的当前姿态;SkinnedMesh 和 morph target 仍 fallback。 |
51
+
52
+ `stats` / `build().stats` 包含 `instances`、`sourceMeshes`、`batches`、`drawCallsSaved`、`vertices`、`indices`、`unsupportedInstances` 和 `unsupportedByReason`。
53
+
54
+ ### 创建和构建
55
+
56
+ ```typescript
57
+ import {
58
+ EditableGeometryBatchLayer,
59
+ InstanceObject,
60
+ Model,
61
+ } from 'u-space';
62
+
63
+ const layer = new EditableGeometryBatchLayer<InstanceObject>({
64
+ maxVerticesPerBatch: 1_500_000,
65
+ maxIndicesPerBatch: 4_500_000,
66
+ });
67
+
68
+ const template = new Model();
69
+ const instances = [new InstanceObject(), new InstanceObject()];
70
+
71
+ layer.addSource({
72
+ key: '/models/building.glb',
73
+ template,
74
+ instances,
75
+ });
76
+
77
+ scene.add(layer, ...instances);
78
+ scene.updateMatrixWorld(true);
79
+
80
+ layer.addEventListener('materialize', ({ instance, object }) => {
81
+ console.log('materialized', instance.instanceId, object);
82
+ });
83
+
84
+ const result = layer.build();
85
+ console.table(result.stats);
86
+ ```
87
+
88
+ `addSource()` 接收 `key`、`Model` 模板和同一模板对应的实例数组。全部 source 添加完成后调用一次 `build()`;layer 是 one-shot builder,重复 `build()`、build 后继续 `addSource()` 或 dispose 后复用都会抛出错误。同一个 `InstanceObject` 在一个 layer 中只能出现一次,重复 source 会直接抛错,避免生成无法独立隐藏或拾取的重复烘焙几何。`build()` 返回统计信息,以及没有完全进入 merged Geometry 的 `unsupported` 数组。每个 unsupported 项包含 `key`、fallback `template`、`instances` 和 `reasons`,由调用方决定继续 instancing 还是挂载普通 `Model`。
89
+
90
+ 透明材质、SkinnedMesh、morph target、多材质数组、自定义 NodeMaterial / `onBeforeCompile`、不兼容 Geometry 布局、负行列式变换和未冻结动画会进入 unsupported 路径。负行列式会按实例拆分,正 determinant 的同模板实例仍可合并;`negative-scale` 实例必须使用普通 `Model` fallback,不能继续交给不支持负缩放的 `InstancedMesh`。混合模板仍会合并兼容的不透明子集,并返回只保留不支持子 Mesh 的 fallback 模板。
91
+
92
+ ### 对象状态与 materialize
93
+
94
+ 每个逻辑对象在 Float `DataTexture` 中占一个 RGBA texel。内部 NodeMaterial 通过 TSL 和顶点 `batchObjectIndex` attribute 读取显隐、颜色模式和颜色,因此普通显隐、颜色和不引入半透明的高亮只更新 dirty texel,不重新合并 Geometry。兼容 batch 只包含不透明源材质,并保留源材质原有的 opacity 路径;对象显隐使用独立布尔 `maskNode`,避免动态 opacity 同时参与 alpha 输出和 discard 判断而改变楼壳外观。dirty callback 会立即在 CPU 侧同步对应状态,GPU texture 在下一帧上传;transform materialize 不依赖内部 batch 先通过视锥裁剪,因此从屏幕外移动到屏幕内也不会丢失。
95
+
96
+ 实例 transform 偏离烘焙矩阵或有效 opacity 低于 `0.999` 时,layer 会 clone 完整模板、为本次 materialize 创建独立材质并调用 `InstanceObject.setInstanceRenderObject()`,同时 mask merged Geometry 中的旧副本。新挂载对象会立即提交世界矩阵,冻结根 Scene 时也不会错过当前帧;若另一个 layer materialize 了同一逻辑实例,旧 layer 会通过 dirty callback 隐藏自己的烘焙副本。不同 materialized 实例可以安全使用不同 opacity/highlight,不会反向修改模板或其他实例。业务也可以主动调用 `instance.materialize()`。materialize 成功后 layer 会派发类型化的 `materialize` 事件;如果需要观察 build 期间由初始 opacity 触发的 materialize,应像上例一样在 `build()` 前注册监听:
97
+
98
+ ```typescript
99
+ const model = instances[0].materialize();
100
+ ```
101
+
102
+ `setInstanceMaterializer()` / `clearInstanceMaterializer(materializer?)` 是 batching 实现连接 `InstanceObject` 的低层协议,普通业务不需要直接设置。带 delegate 参数的 clear 只会解除同一个 materializer,避免旧 layer dispose 时清除后来接管实例的新 layer。materialize 是单向操作;恢复原 transform 或 opacity 不会自动重新进入 batch。
103
+
104
+ ### Raycast 与释放
105
+
106
+ 内部 merged Mesh 首次拾取时按需建立 `three-mesh-bvh`,再通过 `faceIndex → vertexIndex → batchObjectIndex` 将命中映射回原始 `InstanceObject`。隐藏 layer 或内部 batch Mesh 会遵循 `ignoreInvisibleWhenRaycast`;已经 materialize 的 state 会从 merged BVH 命中过滤,旧烘焙位置不会残留 ghost picking。
107
+
108
+ 不再使用 layer 时必须调用 `dispose()`。它会取消 dirty 订阅、仅解除仍由当前 layer 持有的 instance materializer、释放状态纹理、Geometry 和克隆材质,并清空内部节点。materialized 普通对象及其独立材质已经交给对应 `InstanceObject`,不由 layer dispose。
109
+
110
+ ## 与 `THREE.BatchedMesh` 的区别
111
+
112
+ `EditableGeometryBatchLayer` 不是 `THREE.BatchedMesh`。它选择把主要静态 Geometry 真正烘焙合并,并只同步 dirty 对象状态,以减少 draw submission 和逐对象 render-list 工作;代价是展开重复 Geometry、增加 GPU 顶点内存,并在 transform 或半透明变化时 materialize 当前对象。高重复、较大且经常变换的同模板对象通常更适合 `ModelInstancedLayer`。
@@ -5,8 +5,8 @@
5
5
  ## `MaterialEffects`
6
6
 
7
7
  静态工具类,将基于 TSL 的视觉特效直接应用于对象的材质。适用于任何 `Object3D`(支持单个或数组),会自动遍历所有子网格。
8
- 当对象是 `SemanticInstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或楼层里的 `FloorSemanticInstanceObject`)时,`highlightColor()` / `removeHighlightColor()` 会通过 `setSemanticHighlight()` / `clearSemanticHighlight()` 改写该语义实例的颜色和透明度,而不是改动共享 batch 材质。
9
- `SemanticInstanceObject` 支持 `overwrite` 的替换/染色语义;`depthWrite` 属于共享 batch 材质状态,不能按单个实例设置,因此对该类对象不会生效。
8
+ 当对象是 `InstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或楼层里的 `FloorSemanticInstanceObject`)时,`highlightColor()` / `removeHighlightColor()` 会通过 `setInstanceHighlight()` / `clearInstanceHighlight()` 改写该实例的颜色和透明度,而不是改动共享 batch 材质。
9
+ `InstanceObject` 支持 `overwrite` 的替换/染色语义;`depthWrite` 属于共享 batch 材质状态,不能按单个实例设置,因此对该类对象不会生效。
10
10
 
11
11
  特性:
12
12
  - **效果可叠加**:高亮和呼吸效果可同时作用于同一对象,呼吸在高亮结果之上混合
@@ -4,6 +4,14 @@
4
4
 
5
5
  `InteractionManager` 在 `Viewer` 内部自动实例化,可通过 `viewer.interactionManager` 访问。
6
6
 
7
+ 对于三角形较多的静态模型,可以在模型加载后调用 `enableMeshBVHRaycast(root)`。该函数不会修改 Three.js 原型;它先保留每个 Mesh 的局部包围体粗筛,只有射线命中包围球后才为至少 10000 个三角形的 geometry 按需建立并复用 `three-mesh-bvh`。小几何、蒙皮、morph target 或自定义 raycast 对象会继续使用原始路径。
8
+
9
+ ```ts
10
+ import { enableMeshBVHRaycast } from 'u-space';
11
+
12
+ enableMeshBVHRaycast(loadedModel);
13
+ ```
14
+
7
15
  ## 启用交互事件
8
16
 
9
17
  出于性能考虑,指针移动事件默认处于禁用状态。如需悬停和拖拽效果,必须显式开启。
@@ -129,7 +129,7 @@ viewer.objectManager.showAll();
129
129
 
130
130
  #### `setOpacity(id: string, opacity: number)`
131
131
 
132
- 设置指定对象所有材质的透明度。对于 `SemanticInstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或 `FloorSemanticInstanceObject`),会调用 `setSemanticOpacity()` 写入该实例的透明度,而不会影响同一模型 path 或同一楼层内的其他实例。
132
+ 设置指定对象所有材质的透明度。对于 `InstanceObject`(例如 `SceneInstanceObject`、`FacilityInstanceObject` 或 `FloorSemanticInstanceObject`),会调用 `setInstanceOpacity()` 写入该实例的透明度,而不会影响同一模型 path 或同一楼层内的其他实例。
133
133
 
134
134
  ```typescript
135
135
  viewer.objectManager.setOpacity('building-01', 0.3);