u-space 0.0.31 → 0.0.32

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.
@@ -20670,7 +20670,7 @@ class Watermarker extends Scene {
20670
20670
  return this.mesh.material.map ? Promise.resolve(this.mesh.material.map) : (this.#r ??= TextureLoaderManager.loadAsyncTexture(logo).then((e) => (e.colorSpace = SRGBColorSpace, e)), this.#r);
20671
20671
  }
20672
20672
  }
20673
- const _m4 = new Matrix4(), SEMANTIC_ID_KEYS = [
20673
+ const _m4 = new Matrix4(), MAX_CONCURRENT_MODEL_LOADS = 1, SEMANTIC_ID_KEYS = [
20674
20674
  "Buildings",
20675
20675
  "Stories",
20676
20676
  "Walls",
@@ -20743,7 +20743,7 @@ class SceneParser {
20743
20743
  }
20744
20744
  await Promise.all(t.children.map((o) => l(o, u ?? h)));
20745
20745
  };
20746
- return await Promise.all(e.map((t) => l(t, a))), await this.#a(a), a;
20746
+ return await Promise.all(e.map((t) => l(t, a))), await this.#n(a), a;
20747
20747
  }
20748
20748
  #r(e) {
20749
20749
  const n = new BaseGroup();
@@ -20790,7 +20790,7 @@ class SceneParser {
20790
20790
  instances: []
20791
20791
  }, this.#e.set(e, n)), n;
20792
20792
  }
20793
- async #a(e) {
20793
+ async #n(e) {
20794
20794
  if (this.#e.size === 0) return;
20795
20795
  if (this.editableBatchOptions) {
20796
20796
  const a = new SceneEditableBatchLayer(this.editableBatchOptions);
@@ -20852,9 +20852,11 @@ class SceneLoader extends Loader {
20852
20852
  editableBatchOptions = null;
20853
20853
  _resourceData = null;
20854
20854
  _treeData = [];
20855
- #e;
20855
+ #e = 0;
20856
+ #r = [];
20857
+ #t;
20856
20858
  constructor(e) {
20857
- super(), this.viewer = e, this.#e = new Watermarker(e), this.manager = new LoadingManager();
20859
+ super(), this.viewer = e, this.#t = new Watermarker(e), this.manager = new LoadingManager();
20858
20860
  }
20859
20861
  setKey(e) {
20860
20862
  return this.key = e, this;
@@ -20867,19 +20869,37 @@ class SceneLoader extends Loader {
20867
20869
  setEditableBatching(e = !0) {
20868
20870
  return this.editableBatchOptions = e === !1 ? null : e === !0 ? {} : e, this;
20869
20871
  }
20872
+ async #n() {
20873
+ if (this.#e < MAX_CONCURRENT_MODEL_LOADS) {
20874
+ this.#e++;
20875
+ return;
20876
+ }
20877
+ await new Promise((e) => {
20878
+ this.#r.push(e);
20879
+ });
20880
+ }
20881
+ #i() {
20882
+ const e = this.#r.shift();
20883
+ e ? e() : this.#e--;
20884
+ }
20870
20885
  async loadModel(e) {
20871
- const n = new Model();
20872
- this.manager.itemStart(e);
20886
+ await this.#n();
20873
20887
  try {
20874
- await n.loadAsync({
20875
- url: e
20876
- });
20877
- } catch (a) {
20878
- throw this.manager.itemError(e), a;
20888
+ const n = new Model();
20889
+ this.manager.itemStart(e);
20890
+ try {
20891
+ await n.loadAsync({
20892
+ url: e
20893
+ });
20894
+ } catch (a) {
20895
+ throw this.manager.itemError(e), a;
20896
+ } finally {
20897
+ this.manager.itemEnd(e);
20898
+ }
20899
+ return n;
20879
20900
  } finally {
20880
- this.manager.itemEnd(e);
20901
+ this.#i();
20881
20902
  }
20882
- return n;
20883
20903
  }
20884
20904
  async _preareTreeData() {
20885
20905
  if (!this.path)
@@ -20894,7 +20914,7 @@ class SceneLoader extends Loader {
20894
20914
  if (this._resourceData = a, l)
20895
20915
  if (n.encryptWith === "DEBUG")
20896
20916
  try {
20897
- this._treeData = decryptWithDebugKey(a), this.#e.enable();
20917
+ this._treeData = decryptWithDebugKey(a), this.#t.enable();
20898
20918
  } catch {
20899
20919
  throw new Error("u-space SceneLoader: decrypt with debug key failed");
20900
20920
  }
@@ -20948,7 +20968,7 @@ class SceneLoader extends Loader {
20948
20968
  }), this.cachedIds.clear();
20949
20969
  }
20950
20970
  dispose() {
20951
- this.clearCache(), this.#e.dispose();
20971
+ this.clearCache(), this.#t.dispose();
20952
20972
  }
20953
20973
  }
20954
20974
  class VerticalShaftMeshError extends Error {
@@ -21183,12 +21203,12 @@ class FloorSemanticInstanceObject extends InstanceObject {
21183
21203
  return this.#t = !0, super.setInstanceHighlight(e, l, a), this.floorMesh.setColorAt(this.semanticIndex, new Color(e)), this.floorMesh.setOpacityAt(this.semanticIndex, l), this;
21184
21204
  }
21185
21205
  clearInstanceHighlight() {
21186
- return this.#t ? (this.#t = !1, super.clearInstanceHighlight(), this.#a(), this) : this;
21206
+ return this.#t ? (this.#t = !1, super.clearInstanceHighlight(), this.#n(), this) : this;
21187
21207
  }
21188
21208
  getInstanceBoundingBox(e = new Box3(), n = !1) {
21189
21209
  return this.floorMesh.getBoundingBoxAt(this.semanticIndex, e, n) ?? e.makeEmpty();
21190
21210
  }
21191
- #a() {
21211
+ #n() {
21192
21212
  this.#e ? this.floorMesh.setColorAt(this.semanticIndex, this.#e) : this.floorMesh.resetColorAt(this.semanticIndex), this.floorMesh.setOpacityAt(this.semanticIndex, this.#r);
21193
21213
  }
21194
21214
  }
@@ -21215,7 +21235,7 @@ class FloorMesh extends BaseMesh {
21215
21235
  #e;
21216
21236
  #r;
21217
21237
  #t = [];
21218
- #a;
21238
+ #n;
21219
21239
  #i;
21220
21240
  #o;
21221
21241
  #s;
@@ -21232,7 +21252,7 @@ class FloorMesh extends BaseMesh {
21232
21252
  Doors: this.doorsInstances,
21233
21253
  Windows: this.windowsInstances,
21234
21254
  Staircases: this.staircasesInstances
21235
- }, this.#a = h, this.#i = u, this.#o = o, this.#s = s, this.#u = new Mesh(), this.#u.matrixAutoUpdate = !1, this.frustumCulled = !1, this.#r.forEach((c) => {
21255
+ }, this.#n = h, this.#i = u, this.#o = o, this.#s = s, this.#u = new Mesh(), this.#u.matrixAutoUpdate = !1, this.frustumCulled = !1, this.#r.forEach((c) => {
21236
21256
  const d = new FloorSemanticInstanceObject(this, c.index);
21237
21257
  d.name = c.name || c.id, d.visible = c.visible, d.setInstanceIdentity({
21238
21258
  id: c.id,
@@ -21276,13 +21296,13 @@ class FloorMesh extends BaseMesh {
21276
21296
  return this.getSemantics().filter((n) => n.kind === e);
21277
21297
  }
21278
21298
  getSemanticIdAt(e) {
21279
- return this.#n(e).id;
21299
+ return this.#a(e).id;
21280
21300
  }
21281
21301
  getSemanticKindAt(e) {
21282
- return this.#n(e).kind;
21302
+ return this.#a(e).kind;
21283
21303
  }
21284
21304
  getSemanticNameAt(e) {
21285
- return this.#n(e).name;
21305
+ return this.#a(e).name;
21286
21306
  }
21287
21307
  getSemanticObjectAt(e) {
21288
21308
  return this.#t[e];
@@ -21337,33 +21357,33 @@ class FloorMesh extends BaseMesh {
21337
21357
  return typeof e == "string" ? this.getFacilityById(e) : e;
21338
21358
  }
21339
21359
  setColorAt(e, n) {
21340
- const a = this.#n(e);
21360
+ const a = this.#a(e);
21341
21361
  return copySemanticColor(n, a.color), this.#d(e), this.#l(), this;
21342
21362
  }
21343
21363
  resetColorAt(e) {
21344
- const n = this.#n(e), a = n.color.w;
21364
+ const n = this.#a(e), a = n.color.w;
21345
21365
  return n.color.copy(n.initialColor), n.color.w = a, this.#d(e), this.#l(), this;
21346
21366
  }
21347
21367
  getColorAt(e, n = new Vector4()) {
21348
- const a = this.#n(e).color;
21368
+ const a = this.#a(e).color;
21349
21369
  return n.copy(a), n;
21350
21370
  }
21351
21371
  setOpacityAt(e, n) {
21352
- const a = this.#n(e);
21372
+ const a = this.#a(e);
21353
21373
  return a.color.w = Math.max(0, Math.min(1, n)), this.#d(e), this.#l(), this;
21354
21374
  }
21355
21375
  getOpacityAt(e) {
21356
- return this.#n(e).color.w;
21376
+ return this.#a(e).color.w;
21357
21377
  }
21358
21378
  setVisibleAt(e, n) {
21359
- const a = this.#n(e);
21379
+ const a = this.#a(e);
21360
21380
  return a.visible = n, this.#g(e), this;
21361
21381
  }
21362
21382
  getVisibleAt(e) {
21363
- return this.#n(e).visible;
21383
+ return this.#a(e).visible;
21364
21384
  }
21365
21385
  setMatrixAt(e, n) {
21366
- const a = this.#n(e);
21386
+ const a = this.#a(e);
21367
21387
  return a.matrix.copy(n), a.finalMatrix.multiplyMatrices(a.baseMatrix, a.matrix), this.#y(e), this.#p(e), this;
21368
21388
  }
21369
21389
  setScaleAt(e, n, a, l) {
@@ -21377,10 +21397,10 @@ class FloorMesh extends BaseMesh {
21377
21397
  });
21378
21398
  }
21379
21399
  getMatrixAt(e, n = new Matrix4()) {
21380
- return n.copy(this.#n(e).matrix), n;
21400
+ return n.copy(this.#a(e).matrix), n;
21381
21401
  }
21382
21402
  getFinalMatrixAt(e, n = new Matrix4()) {
21383
- return n.copy(this.#n(e).finalMatrix), n;
21403
+ return n.copy(this.#a(e).finalMatrix), n;
21384
21404
  }
21385
21405
  computeBoundingBox() {
21386
21406
  this.boundingBox === null && (this.boundingBox = new Box3()), this.boundingBox.makeEmpty();
@@ -21393,11 +21413,11 @@ class FloorMesh extends BaseMesh {
21393
21413
  this.getBoundingSphereAt(e.index, _boundsSphere, !1) && this.boundingSphere.union(_boundsSphere);
21394
21414
  }
21395
21415
  getBoundingBoxAt(e, n = new Box3(), a = !0) {
21396
- const l = this.#n(e);
21416
+ const l = this.#a(e);
21397
21417
  return l.geometry.boundingBox || l.geometry.computeBoundingBox(), l.geometry.boundingBox ? (n.copy(l.geometry.boundingBox).applyMatrix4(l.finalMatrix), a && (this.updateWorldMatrix(!0, !1), n.applyMatrix4(this.matrixWorld)), n) : null;
21398
21418
  }
21399
21419
  getBoundingSphereAt(e, n = new Sphere(), a = !0) {
21400
- const l = this.#n(e);
21420
+ const l = this.#a(e);
21401
21421
  return l.geometry.boundingSphere || l.geometry.computeBoundingSphere(), l.geometry.boundingSphere ? (n.copy(l.geometry.boundingSphere), a ? (this.updateWorldMatrix(!0, !1), _boundsMatrix.multiplyMatrices(this.matrixWorld, l.finalMatrix), n.applyMatrix4(_boundsMatrix)) : n.applyMatrix4(l.finalMatrix), n) : null;
21402
21422
  }
21403
21423
  raycast(e, n) {
@@ -21414,7 +21434,7 @@ class FloorMesh extends BaseMesh {
21414
21434
  }
21415
21435
  }
21416
21436
  dispose() {
21417
- this.geometry.dispose(), Array.isArray(this.material) ? this.material.forEach((e) => e.dispose()) : this.material.dispose(), this.#a.dispose(), this.#i.dispose(), this.#o.dispose(), this.#s.dispose(), this.#r.forEach((e) => e.geometry.dispose());
21437
+ this.geometry.dispose(), Array.isArray(this.material) ? this.material.forEach((e) => e.dispose()) : this.material.dispose(), this.#n.dispose(), this.#i.dispose(), this.#o.dispose(), this.#s.dispose(), this.#r.forEach((e) => e.geometry.dispose());
21418
21438
  }
21419
21439
  #v(e) {
21420
21440
  for (const [n, a] of this.facilityObjectsById)
@@ -21440,7 +21460,7 @@ class FloorMesh extends BaseMesh {
21440
21460
  const { facility: n } = e.userData;
21441
21461
  return e.instanceName ? e.instanceName : n && typeof n.Name == "string" && n.Name ? n.Name : e.name || void 0;
21442
21462
  }
21443
- #n(e) {
21463
+ #a(e) {
21444
21464
  const n = this.#r[e];
21445
21465
  if (!n)
21446
21466
  throw new Error(`u-space FloorMesh: invalid semantic index ${e}.`);
@@ -21483,7 +21503,7 @@ class FloorMesh extends BaseMesh {
21483
21503
  n(a);
21484
21504
  }
21485
21505
  #B(e) {
21486
- const n = this.#n(e), a = this.#f.get(e) ?? n.matrix;
21506
+ const n = this.#a(e), a = this.#f.get(e) ?? n.matrix;
21487
21507
  this.#f.has(e) || this.#f.set(e, n.matrix.clone()), a.decompose(_dummyPosition, _dummyQuaternion, _dummyScale), _dummyScale.y *= PLANISH_SCALE_Y, n.matrix.compose(_dummyPosition, _dummyQuaternion, _dummyScale), n.finalMatrix.multiplyMatrices(n.baseMatrix, n.matrix), n.geometry.boundingBox || n.geometry.computeBoundingBox(), n.geometry.boundingBox && (_box.copy(n.geometry.boundingBox).applyMatrix4(n.finalMatrix), _dummyPosition.y += this.pivot.y + getSemanticPlanishOffset(n) - _box.min.y, n.matrix.compose(_dummyPosition, _dummyQuaternion, _dummyScale), n.finalMatrix.multiplyMatrices(n.baseMatrix, n.matrix)), this.#y(e), this.#p(e);
21488
21508
  }
21489
21509
  #A(e) {
@@ -21516,21 +21536,21 @@ class FloorMesh extends BaseMesh {
21516
21536
  }), n.isEmpty() ? null : n;
21517
21537
  }
21518
21538
  #m(e, n) {
21519
- const a = this.#n(e);
21539
+ const a = this.#a(e);
21520
21540
  return a.matrix.decompose(_dummyPosition, _dummyQuaternion, _dummyScale), n(_dummyScale), a.matrix.compose(_dummyPosition, _dummyQuaternion, _dummyScale), a.finalMatrix.multiplyMatrices(a.baseMatrix, a.matrix), this.#y(e), this.#p(e), this;
21521
21541
  }
21522
21542
  #d(e) {
21523
- this.#n(e).color.toArray(this.#a.image.data, e * 4), this.#a.needsUpdate = !0;
21543
+ this.#a(e).color.toArray(this.#n.image.data, e * 4), this.#n.needsUpdate = !0;
21524
21544
  }
21525
21545
  #g(e) {
21526
- const n = this.#n(e), a = this.#i.image.data;
21546
+ const n = this.#a(e), a = this.#i.image.data;
21527
21547
  a[e * 4] = n.visible ? 1 : 0, a[e * 4 + 1] = 0, a[e * 4 + 2] = 0, a[e * 4 + 3] = 1, this.#i.needsUpdate = !0;
21528
21548
  }
21529
21549
  #p(e) {
21530
- this.#n(e).finalMatrix.toArray(this.#o.image.data, e * 16), this.#o.needsUpdate = !0;
21550
+ this.#a(e).finalMatrix.toArray(this.#o.image.data, e * 16), this.#o.needsUpdate = !0;
21531
21551
  }
21532
21552
  #I(e) {
21533
- const n = this.#n(e), a = this.#s.image.data;
21553
+ const n = this.#a(e), a = this.#s.image.data;
21534
21554
  a[e * 4] = getSemanticRenderOffset(n), a[e * 4 + 1] = 0, a[e * 4 + 2] = 0, a[e * 4 + 3] = 1, this.#s.needsUpdate = !0;
21535
21555
  }
21536
21556
  }
@@ -21582,7 +21602,7 @@ class BuildingGroup extends BaseGroup {
21582
21602
  #e = /* @__PURE__ */ new Set();
21583
21603
  #r = /* @__PURE__ */ new Map();
21584
21604
  #t = /* @__PURE__ */ new Map();
21585
- #a = /* @__PURE__ */ new Map();
21605
+ #n = /* @__PURE__ */ new Map();
21586
21606
  #i = /* @__PURE__ */ new Map();
21587
21607
  #o = /* @__PURE__ */ new Map();
21588
21608
  #s = /* @__PURE__ */ new Map();
@@ -21624,17 +21644,17 @@ class BuildingGroup extends BaseGroup {
21624
21644
  return n !== -1 && this.elevatorMeshes.splice(n, 1), rebuildObjectIndex(this.#t, this.elevatorMeshes), this.remove(e), this;
21625
21645
  }
21626
21646
  addVent(e) {
21627
- return this.ventMeshes.includes(e) || (this.add(e), this.ventMeshes.push(e), indexObject(this.#a, e)), this;
21647
+ return this.ventMeshes.includes(e) || (this.add(e), this.ventMeshes.push(e), indexObject(this.#n, e)), this;
21628
21648
  }
21629
21649
  getVentAt(e) {
21630
21650
  return this.ventMeshes[e];
21631
21651
  }
21632
21652
  getVentById(e) {
21633
- return this.#a.get(e);
21653
+ return this.#n.get(e);
21634
21654
  }
21635
21655
  removeVent(e) {
21636
21656
  const n = this.ventMeshes.indexOf(e);
21637
- return n !== -1 && this.ventMeshes.splice(n, 1), rebuildObjectIndex(this.#a, this.ventMeshes), this.remove(e), this;
21657
+ return n !== -1 && this.ventMeshes.splice(n, 1), rebuildObjectIndex(this.#n, this.ventMeshes), this.remove(e), this;
21638
21658
  }
21639
21659
  isolateFloor(e) {
21640
21660
  return this.isolateFloors(getFloorReferences(e));
@@ -21703,7 +21723,7 @@ class SemanticGroup extends BaseGroup {
21703
21723
  #e = /* @__PURE__ */ new Set();
21704
21724
  #r = /* @__PURE__ */ new Map();
21705
21725
  #t = /* @__PURE__ */ new Map();
21706
- #a = /* @__PURE__ */ new Map();
21726
+ #n = /* @__PURE__ */ new Map();
21707
21727
  #i = /* @__PURE__ */ new Map();
21708
21728
  #o = !0;
21709
21729
  constructor() {
@@ -21766,12 +21786,12 @@ class SemanticGroup extends BaseGroup {
21766
21786
  let e = this.#o || this.#i.size !== this.buildings.length;
21767
21787
  for (const n of this.buildings)
21768
21788
  n.syncFacilityIndexes(), this.#i.get(n) !== n.facilityIndexVersion && (e = !0);
21769
- e && (this.#t.clear(), this.#a.clear(), this.#i.clear(), this.buildings.forEach((n) => {
21789
+ e && (this.#t.clear(), this.#n.clear(), this.#i.clear(), this.buildings.forEach((n) => {
21770
21790
  n.syncFacilityIndexes(), this.#i.set(n, n.facilityIndexVersion);
21771
21791
  for (const a of n.getFacilityIndexIds())
21772
21792
  this.#t.has(a) || this.#t.set(a, n);
21773
21793
  for (const a of n.getFacilityIndexObjects())
21774
- this.#a.has(a) || this.#a.set(a, n);
21794
+ this.#n.has(a) || this.#n.set(a, n);
21775
21795
  }), this.#o = !1);
21776
21796
  }
21777
21797
  }
@@ -22247,7 +22267,7 @@ class SemanticParser {
22247
22267
  h.push(...o);
22248
22268
  continue;
22249
22269
  }
22250
- const r = o.map((d) => this.#a(d));
22270
+ const r = o.map((d) => this.#n(d));
22251
22271
  l.addInstances(u, s, r) ? r.forEach((d, p) => {
22252
22272
  const v = o[p];
22253
22273
  this.#o(d, v.facility, a);
@@ -22257,7 +22277,7 @@ class SemanticParser {
22257
22277
  }
22258
22278
  l.children.length > 0 && n.setFacilityLayer(l), await Promise.all(h.map((u) => this.#i(u, a)));
22259
22279
  }
22260
- #a(e) {
22280
+ #n(e) {
22261
22281
  const { facility: n, story: a, floorMesh: l, treeNode: t, worldMatrix: h } = e, u = new FacilityInstanceObject();
22262
22282
  return u.name = n.Name || t.name || "", u.visible = t.visible, u.setInstanceIdentity({
22263
22283
  id: n.ID,
@@ -22277,7 +22297,7 @@ class SemanticParser {
22277
22297
  async #i(e, n) {
22278
22298
  const { facility: a, story: l, treeNode: t } = e, h = await createFacilityModel(a, t, n);
22279
22299
  if (!h) return;
22280
- const u = this.#a(e);
22300
+ const u = this.#n(e);
22281
22301
  u.userData.instanced = !1, Object.assign(h.userData, {
22282
22302
  storyId: l.ID,
22283
22303
  floorIndex: l.FloorIndex,
@@ -1,6 +1,6 @@
1
1
  # Effects API
2
2
 
3
- `u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式。
3
+ `u-space` 提供了两个基于 Three.js 着色语言(TSL/WebGPU 节点)的静态特效工具:`MaterialEffects` 用于为对象应用高亮、呼吸等材质特效,`TSLEffects` 用于生成动态颜色节点模式和可组合的 Outline 输出效果。
4
4
 
5
5
  ## `MaterialEffects`
6
6
 
@@ -123,7 +123,71 @@ await MaterialEffects.fadeIn(myModel, { duration: 500 });
123
123
 
124
124
  ## `TSLEffects`
125
125
 
126
- 静态工厂类,返回 TSL 颜色节点。将返回值赋给 `NodeMaterial` 的 `material.colorNode` 即可应用动态着色器特效。持续动画需要 `viewer.frameloop = 'always'`。
126
+ 静态工厂类。`flow()`、`breathe()` 和 `fluid()` 返回可赋给 `NodeMaterial.colorNode` 的 TSL 颜色节点;`outline()` 返回接入 `RenderPipeline` 的输出效果。持续动画需要 `viewer.frameloop = 'always'`。
127
+
128
+ ### `TSLEffects.outline(options?)`
129
+
130
+ 创建基于 Three.js `OutlineNode` 的屏幕空间描边效果。它不会修改对象材质,而是通过 `RenderPipeline.addOutputEffect()` 叠加在默认渲染结果上,因此可以与内置 Bloom、SSGI、SSR 以及其他 output effects 组合。
131
+
132
+ ```typescript
133
+ import { TSLEffects } from 'u-space';
134
+
135
+ const outline = TSLEffects.outline({
136
+ selectedObjects: [model],
137
+ edgeStrength: 3,
138
+ edgeThickness: 1.5,
139
+ edgeGlow: 0,
140
+ visibleEdgeColor: 0x00e5ff,
141
+ hiddenEdgeColor: 0xff4d6d,
142
+ downSampleRatio: 2,
143
+ instanceBoundsProxy: true,
144
+ instanceBoundsPadding: 0.05,
145
+ });
146
+
147
+ viewer.renderPipeline.addOutputEffect(outline);
148
+
149
+ // 替换当前描边对象,不需要重建后处理链。
150
+ outline.setSelectedObjects([anotherObject]);
151
+
152
+ // 运行时更新样式与 InstanceObject bounds proxy 参数。
153
+ outline.update({ edgeStrength: 4, edgeGlow: 0.25 });
154
+ viewer.render();
155
+
156
+ // 销毁前先从 RenderPipeline 移除。
157
+ viewer.renderPipeline.removeOutputEffect(outline);
158
+ outline.dispose();
159
+ ```
160
+
161
+ #### `TSLOutlineOptions`
162
+
163
+ | 属性 | 类型 | 默认值 | 说明 |
164
+ | :--- | :--- | :--- | :--- |
165
+ | `selectedObjects` | `readonly Object3D[]` | `[]` | 初始描边对象。普通 Mesh、Group 与核心 `InstanceObject` 均可传入。 |
166
+ | `edgeStrength` | `number` | `3` | 边缘颜色强度。 |
167
+ | `edgeThickness` | `number` | `1` | 边缘厚度。 |
168
+ | `edgeGlow` | `number` | `0` | 描边向外扩散的发光强度。 |
169
+ | `visibleEdgeColor` | `ColorRepresentation` | `0xffffff` | 可见边缘颜色。 |
170
+ | `hiddenEdgeColor` | `ColorRepresentation` | `0x4e3636` | 被遮挡边缘颜色。 |
171
+ | `downSampleRatio` | `number` | `2` | Outline pass 降采样比例;非有限值或 `<= 0` 时恢复为 `2`。值越大成本越低,但边缘精度也越低。 |
172
+ | `instanceBoundsProxy` | `boolean` | `true` | `InstanceObject` 没有可直接描边的渲染子树时,是否用世界包围盒代理生成轮廓。 |
173
+ | `instanceBoundsPadding` | `number` | `0` | bounds proxy 每个方向额外扩张的世界单位。 |
174
+ | `instanceBoundsMinSize` | `number` | `0.001` | bounds proxy 每个轴向的最小尺寸,避免退化包围盒无法描边。 |
175
+
176
+ #### 返回的 `TSLOutlineEffect`
177
+
178
+ | 成员 | 说明 |
179
+ | :--- | :--- |
180
+ | `selectedObjects` | 当前逻辑选择对象的只读快照。 |
181
+ | `outlineNode` | 当前内部 `OutlineNode`;在 RenderPipeline 首次构建前为 `null`,Scene 或 Camera 变化后可能被替换。 |
182
+ | `setSelectedObjects(objects)` | 替换当前选择并返回自身,便于链式调用。空数组会清除描边。 |
183
+ | `update(options)` | 更新除 `selectedObjects` 外的全部样式和 bounds proxy 选项,并返回自身。 |
184
+ | `dispose()` | 释放内部 OutlineNode、代理 Geometry/Material 和 dirty 订阅。调用前应先 `removeOutputEffect()`。 |
185
+
186
+ 对于 `InstanceObject`,效果会优先描边 `getInstanceRenderObject()` 返回的可渲染对象;如果逻辑对象自身包含 Mesh/Sprite,则直接使用该对象;只有两者都不可用时才根据 `getInstanceBoundingBox()` 创建不可见 bounds proxy。代理会监听 `onInstanceRenderDirty()`,在实例显隐、变换或包围盒变化后同步,而且不会参与拾取、颜色输出或阴影。
187
+
188
+ > 如果业务设置了自定义 `viewer.renderPipeline.setOutputComposer()`,RenderPipeline 会按自定义 composer 语义跳过所有 output effects,Outline 也不会显示。更新选择或参数后,`frameloop = 'demand'` 的 Viewer 需要调用 `viewer.render()` 或 `viewer.invalidate()` 请求新帧。
189
+
190
+ [在线示例](https://u-space-phi.vercel.app/examples/test_outline.html)
127
191
 
128
192
  ### `TSLEffects.flow(parameters?)`
129
193
 
package/docs/changelog.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## 未发布
4
4
 
5
+ ## 0.0.32
6
+
7
+ ### 修复与优化
8
+
9
+ - **Offscreen 在线示例场景源** — 保留本地 `examples/scenes/飞龙吾悦广场` 及其匹配密钥;GitHub 发布流程会在 Vercel 构建前拉取真实 Git LFS 对象,构建脚本也会拒绝仅含 pointer 的场景。超过 Vercel 单文件上限的场景资源会在部署产物中拆成 90 MiB 分片,由 render/decode Worker 流式还原,既避免把 LFS pointer 当作 JSON,也不替换原场景。
10
+
11
+ ### 文档
12
+
13
+ - **TSLEffects Outline** — 补齐 `TSLEffects.outline()`、`TSLOutlineOptions`、`TSLOutlineEffect`、RenderPipeline output-effect 接入、`InstanceObject` 渲染对象/bounds proxy 策略、资源释放、`test_outline.html` 在线示例及 MCP/LLM 检索入口。该效果最早随 `0.0.30` 发布,本次补全此前遗漏的文档发布面。
14
+
5
15
  ## 0.0.31
6
16
 
7
17
  ### 新增
@@ -19,6 +29,7 @@
19
29
 
20
30
  ### 新增
21
31
 
32
+ - **TSLEffects Outline** — 新增 `TSLEffects.outline()` 可组合 output effect,支持普通 `Object3D` 与核心 `InstanceObject` 的可见/隐藏边缘描边、运行时选择和样式更新,以及在实例没有直接可描边渲染子树时使用世界包围盒代理;`examples/test_outline.html` 提供交互示例。
22
33
  - **AO 示例** — 新增 `examples/test_ao.html`,通过 SSGI 的 `giIntensity: 0` 默认开启纯 AO,并提供遮蔽强度、半径和厚度实时调节。
23
34
  - **RenderPipeline SSR** — 新增 `enableSSR()`、`updateSSR()` 和 `disableSSR()`,封装 Stochastic SSR、Temporal Reproject、Recurrent Denoise、velocity 以及双向历史反馈;提供完整 SSR 与降噪参数配置。
24
35
  - **Viewer** — 新增可选 `pixelRatio` 构造参数,省略时继续使用 `Math.min(window.devicePixelRatio, 1.5)`。
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 运行方式与版本
6
6
 
7
- 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.31`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.31` 作为直接托管 `examples/` 时的 fallback。
7
+ 示例统一通过 `examples/importmap.js` 注入 Import Map。本地通过 `localhost`、`127.0.0.1`、`0.0.0.0` 或 `192.168.x.x` 访问时会加载仓库里的 `../dist/` 构建产物;在线部署或非本地域名访问时会从 jsDelivr 加载当前发布版本 `u-space@0.0.32`。Vercel 文档部署会继续把 `__VERSION__` 占位符替换为 `package.json` 中的版本号,源码里的 `0.0.32` 作为直接托管 `examples/` 时的 fallback。
8
8
 
9
9
  插件示例可以在页面加载 `importmap.js` 前通过 `window.__IMPORTS__` 声明额外依赖。将 `u-space/plugins/<name>` 的值设为 `true` 时,`importmap.js` 会自动在本地和 CDN 路径之间切换。
10
10
 
@@ -224,7 +224,21 @@ pnpm example:offscreen
224
224
  - 使用 `MaterialEffects.fadeOut(object)` / `fadeIn(object)` 执行淡入淡出动画。
225
225
  - 使用 `MaterialEffects.removeHighlightColor(object)` 恢复原始材质状态。
226
226
 
227
- ## 16. `test_selection.html`:Selection 选择系统
227
+ ## 16. `test_outline.html`:TSLEffects Outline 描边
228
+
229
+ [在线演示](https://u-space-phi.vercel.app/examples/test_outline.html)
230
+
231
+ 该示例演示 `TSLEffects.outline()` 作为 RenderPipeline output effect 对普通 Mesh、Group 和核心 `InstanceObject` 进行屏幕空间描边。
232
+
233
+ ### 核心要点:
234
+
235
+ - 使用 `viewer.renderPipeline.addOutputEffect(outline)` 叠加 Outline,不修改原始材质,也不占用业务侧 `setOutputComposer()`。
236
+ - 使用 `outline.setSelectedObjects()` 在 Box、Sphere、`InstanceObject`、全部对象和空选择之间切换。
237
+ - 使用 `outline.update()` 实时调整强度、厚度、Glow 和 `instanceBoundsPadding`。
238
+ - `InstanceObject` 有实际渲染对象时直接描边;没有可渲染子树时可通过 `getInstanceBoundingBox()` 自动生成不可见 bounds proxy。
239
+ - 销毁时先调用 `removeOutputEffect(outline)`,再调用 `outline.dispose()` 释放 OutlineNode 与代理资源。
240
+
241
+ ## 17. `test_selection.html`:Selection 选择系统
228
242
 
229
243
  该示例演示了对象选择系统的单选和框选功能。
230
244
 
@@ -235,7 +249,7 @@ pnpm example:offscreen
235
249
  - 使用 `selection.enableBoxSelection()` 开启鼠标框选模式。
236
250
  - 框选模式下可配合 `viewer.controls.lock()` 避免框选与相机旋转冲突。
237
251
 
238
- ## 17. `test_measure.html`:MeasureTool 测量工具
252
+ ## 18. `test_measure.html`:MeasureTool 测量工具
239
253
 
240
254
  该示例演示了 3D 场景中的距离、面积和角度测量。
241
255
 
@@ -246,7 +260,7 @@ pnpm example:offscreen
246
260
  - 使用 `measureTool.measureAngle(pointA, vertex, pointC)` 测量夹角。
247
261
  - 测量结果以可视化线条渲染到场景中,返回的 `MeasureResult` 包含数值和单位。
248
262
 
249
- ## 18. `test_annotation.html`:AnnotationManager 标注管理
263
+ ## 19. `test_annotation.html`:AnnotationManager 标注管理
250
264
 
251
265
  该示例演示了 3D 标注的创建、更新和管理,配合 `CSSRenderer` 实现 HTML 浮动标签。
252
266
 
@@ -258,7 +272,7 @@ pnpm example:offscreen
258
272
  - 使用 `updateContent()` 动态更新标注内容,`updatePosition()` 移动标注位置。
259
273
  - 使用 `hideAll()` / `showAll()` 批量控制标注显隐。
260
274
 
261
- ## 19. `test_clipping.html`:ClippingTool 剖切工具
275
+ ## 20. `test_clipping.html`:ClippingTool 剖切工具
262
276
 
263
277
  该示例演示了剖切面的创建和实时调整。
264
278
 
@@ -269,7 +283,7 @@ pnpm example:offscreen
269
283
  - 使用滑块通过 `clippingTool.setPlaneConstant()` 实时调整剖切位置。
270
284
  - 使用 `showHelper()` / `hideHelper()` 切换剖切面可视化辅助。
271
285
 
272
- ## 20. `test_lights.html`:LightManager 灯光管理
286
+ ## 21. `test_lights.html`:LightManager 灯光管理
273
287
 
274
288
  该示例演示了灯光管理器的灯光预设和 Helper 可视化功能。
275
289
 
@@ -280,7 +294,7 @@ pnpm example:offscreen
280
294
  - 使用 `showAllHelpers()` / `hideAllHelpers()` 显示或隐藏灯光辅助可视化。
281
295
  - 每种预设会自动清除旧灯光并创建新的灯光组合。
282
296
 
283
- ## 21. `test_fire.html`:WebGPU 体积火焰
297
+ ## 22. `test_fire.html`:WebGPU 体积火焰
284
298
 
285
299
  该示例演示了 `u-space/plugins/fire` 的 `FireEffect` 插件,封装 Three.js 官方体积火焰示例,在独立 volumetric pass 中模拟火焰和烟雾后叠加到主场景。
286
300
 
@@ -292,7 +306,7 @@ pnpm example:offscreen
292
306
  - 通过 `fire.update()` 实时调整 `fireIntensity`、`smokeLifespan`、`emitterRadius` 等参数。
293
307
  - 插件使用 RenderPipeline output effect 接入,不占用业务侧的 `setOutputComposer()`。
294
308
 
295
- ## 22. `test_ssr.html`:SSR 时空降噪
309
+ ## 23. `test_ssr.html`:SSR 时空降噪
296
310
 
297
311
  [在线演示](https://u-space-phi.vercel.app/examples/test_ssr.html)
298
312
 
@@ -310,7 +324,7 @@ pnpm example:offscreen
310
324
  - `maxDistance` 从默认值 `0.4` 开始,保留滑块用于观察追踪距离对质量和性能的影响。
311
325
  - 移动物体继续生成 velocity,但不投射动态阴影;2048² 方向光阴影只更新一次。
312
326
 
313
- ## 23. `test_ao.html`:环境光遮蔽
327
+ ## 24. `test_ao.html`:环境光遮蔽
314
328
 
315
329
  [在线演示](https://u-space-phi.vercel.app/examples/test_ao.html)
316
330