s3m-three 0.3.3 → 0.3.4

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.
@@ -21,6 +21,7 @@
21
21
  * MapLibre / Mapbox 的胶水层在 s3m-maplibre 包的 S3MLayer.ts。
22
22
  */
23
23
  import * as THREE from 'three';
24
+ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
24
25
  import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
26
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
27
  import { setMaxAnisotropy } from './TextureFactory.js';
@@ -321,6 +322,8 @@ export class S3MViewer {
321
322
  private _selectionEdges: THREE.Mesh | null = null;
322
323
  private _selectionEdgeMeshes: THREE.Mesh[] = [];
323
324
  private _edgesEnabled = false;
325
+ /** 预制棱线缓存(Crease Cache):从外部 source-creases 载入的全局统一 LineSegments,性能最高、视觉最干净 */
326
+ private _creaseLines: THREE.LineSegments | null = null;
324
327
 
325
328
  /** 业务属性查询(可选,配了 `attributes` 才有) */
326
329
  attributeQuery: AttributeQuery | null = null;
@@ -413,6 +416,11 @@ export class S3MViewer {
413
416
  width: this.style.outlineWidthPx ?? 3,
414
417
  });
415
418
 
419
+ // 关键:style.edges 是个开关,但构造函数从没据此启用过棱线,导致
420
+ // `style: { edges: true }` 形同虚设、_edgesEnabled 永远是 false。
421
+ // 这里按 style 实际值调一次 setEdges,让开关真正生效(与 setEdges 后来的手工调用一致)。
422
+ this.setEdges(!!this.style.edges);
423
+
416
424
  this._fps.last = performance.now();
417
425
  this._datasetQueue = this._normalizeDatasets(options);
418
426
  if (options.attributes) this.attributeQuery = new AttributeQuery(options.attributes);
@@ -495,6 +503,7 @@ export class S3MViewer {
495
503
  /** 解绑场景并释放全部 GPU 资源。对应 MapLibre 图层的 onRemove()。 */
496
504
  unmount(): void {
497
505
  for (const id of [...this.datasets.keys()]) this.removeDataset(id);
506
+ this.clearCreases();
498
507
  this.groundManager.dispose();
499
508
  this.picker?.dispose();
500
509
  this.picker = null;
@@ -996,7 +1005,11 @@ export class S3MViewer {
996
1005
  this._indexMeshSmids(mesh);
997
1006
  this._addMeshHeight(mesh);
998
1007
  // 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
999
- if (this._edgesEnabled && !hasNativeEdges && !isPoints) this._ensureEdges(mesh);
1008
+ // 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义。
1009
+ // 自动描边受三道开关控制:全局 style.edges / 数据集 enableEdges / 地形绿地等关键字跳过。
1010
+ if (!hasNativeEdges && !isPoints && this._datasetEdgesEnabled(entry) && !this._shouldSkipAutoEdges(ud, entry.name)) {
1011
+ this._ensureEdges(mesh);
1012
+ }
1000
1013
 
1001
1014
  // 新瓦片要继承当前的不透明度与着色方式
1002
1015
  if (isPoints) {
@@ -1166,8 +1179,12 @@ export class S3MViewer {
1166
1179
  // 点云不走 StyleMaterial 的 uMode,模式切换要单独同步它的材质
1167
1180
  if ('mode' in patch) this._applyPointCloudStyles();
1168
1181
  if ('shading' in patch) this._applyShading();
1169
- if ('edges' in patch) this.setEdges(!!this.style.edges);
1170
- else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
1182
+ // 临界角变了要重建 CPU 棱线(EdgesGeometry 参数),开/关走 setEdges 整体刷新
1183
+ if ('edgeThreshold' in patch && this._edgesEnabled) {
1184
+ this.setEdges(true);
1185
+ } else if ('edges' in patch) {
1186
+ this.setEdges(!!this.style.edges);
1187
+ } else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
1171
1188
  this._refreshEdgeLines();
1172
1189
  }
1173
1190
  if (this._selection) {
@@ -1409,14 +1426,66 @@ export class S3MViewer {
1409
1426
  step();
1410
1427
  }
1411
1428
 
1429
+ /**
1430
+ * 挂载预制棱线缓存(Crease Cache)。
1431
+ *
1432
+ * 直接把 `source-creases.f32` 这类 Float32Array 缓冲载入为一组全局统一的
1433
+ * LineSegments —— 相比逐 Mesh 现算 EdgesGeometry,性能高一个量级,视觉也最干净。
1434
+ * 坐标按「场景本地坐标」(相对场景锚点的米)处理,与瓦片几何同一坐标系。
1435
+ *
1436
+ * @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
1437
+ */
1438
+ async loadCreases(url: string): Promise<void> {
1439
+ const resp = await fetch(url);
1440
+ if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
1441
+ const buf = await resp.arrayBuffer();
1442
+ const arr = new Float32Array(buf);
1443
+ if (arr.length < 6) return;
1444
+
1445
+ const geo = new THREE.BufferGeometry();
1446
+ geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
1447
+ const s = this.style;
1448
+ const mat = new THREE.LineBasicMaterial({
1449
+ color: new THREE.Color(s.edgeColor),
1450
+ transparent: true,
1451
+ opacity: Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1)),
1452
+ depthWrite: false,
1453
+ });
1454
+ const lines = new THREE.LineSegments(geo, mat);
1455
+ lines.renderOrder = 2;
1456
+ lines.frustumCulled = false;
1457
+ lines.raycast = () => {};
1458
+
1459
+ this.clearCreases();
1460
+ this._creaseLines = lines;
1461
+ this.scene?.add(lines);
1462
+ this._requestRepaint();
1463
+ }
1464
+
1465
+ /** 移除并释放预制棱线缓存 */
1466
+ clearCreases(): void {
1467
+ if (!this._creaseLines) return;
1468
+ this.scene?.remove(this._creaseLines);
1469
+ this._creaseLines.geometry.dispose();
1470
+ (this._creaseLines.material as THREE.Material)?.dispose?.();
1471
+ this._creaseLines = null;
1472
+ this._requestRepaint();
1473
+ }
1474
+
1412
1475
  /** 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity */
1413
1476
  private _syncEdgeStyle(): void {
1414
1477
  const s = this.style;
1415
1478
  const u = this.edgeMaterial.uniforms;
1416
1479
  u.uColor.value.set(s.edgeColor);
1417
- u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.2);
1480
+ u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.5);
1418
1481
  u.uOpacity.value =
1419
- Math.min(1, Math.max(0, s.edgeOpacity ?? 0.45)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
1482
+ Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
1483
+ // 预制棱线缓存(LineBasicMaterial)同步颜色 / 不透明度
1484
+ if (this._creaseLines) {
1485
+ const m = this._creaseLines.material as THREE.LineBasicMaterial;
1486
+ m.color.set(s.edgeColor);
1487
+ m.opacity = Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
1488
+ }
1420
1489
  }
1421
1490
 
1422
1491
  /** 让数据自带的棱线跟着 style.edgeColor / opacity 走(这些是 LineBasicMaterial,改不了粗细) */
@@ -1424,7 +1493,7 @@ export class S3MViewer {
1424
1493
  const mat = lines.material as THREE.LineBasicMaterial;
1425
1494
  if (!mat || !mat.color) return;
1426
1495
  mat.color.set(this.style.edgeColor);
1427
- const baseOpacity = this.style.edgeOpacity ?? 0.45;
1496
+ const baseOpacity = this.style.edgeOpacity ?? 0.6;
1428
1497
  mat.opacity = baseOpacity * Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
1429
1498
  mat.transparent = true;
1430
1499
  mat.polygonOffset = true;
@@ -1442,14 +1511,46 @@ export class S3MViewer {
1442
1511
  }
1443
1512
  }
1444
1513
 
1514
+ /** 地形 / 绿地 / 道路 / 地面铺装等不参与自动描边(数据自带棱线包另说) */
1515
+ private static readonly _EDGE_SKIP_KEYWORDS = [
1516
+ 'terrain', 'ground', 'vegetation', 'green', 'road', 'dem',
1517
+ '绿地', '道路', '地面', '地形', '植被', '铺装',
1518
+ ];
1519
+
1520
+ /** 数据集级别的棱线开关:enableEdges 优先,否则跟随全局 style.edges */
1521
+ private _datasetEdgesEnabled(entry: S3MDatasetEntry): boolean {
1522
+ const opt = entry.options?.enableEdges;
1523
+ if (opt === true) return true;
1524
+ if (opt === false) return false;
1525
+ return this._edgesEnabled;
1526
+ }
1527
+
1528
+ /** 该 mesh 是否命中「跳过自动描边」的关键字(skeletonName / 数据集名) */
1529
+ private _shouldSkipAutoEdges(ud: MeshUserData, datasetName: string): boolean {
1530
+ const hay = `${ud.skeletonName ?? ''} ${datasetName}`.toLowerCase();
1531
+ return S3MViewer._EDGE_SKIP_KEYWORDS.some((k) => hay.includes(k));
1532
+ }
1533
+
1445
1534
  private _ensureEdges(mesh: THREE.Mesh): void {
1446
1535
  const ud = mesh.userData as MeshUserData;
1447
1536
  if (ud.edges || ud.nativeEdges || !mesh.geometry) return;
1448
- const indexCount = mesh.geometry.index?.count ?? 0;
1537
+ // 地形 / 绿地 / 道路等不参与自动描边(即使全局开棱线也要跳过)
1538
+ if (this._shouldSkipAutoEdges(ud, '')) return;
1539
+
1540
+ const posAttr = mesh.geometry.getAttribute('position');
1541
+ const indexCount = mesh.geometry.index?.count ?? (posAttr ? posAttr.count : 0);
1449
1542
  // 棱线是 CPU 端建的,超大几何直接跳过,避免拖慢加载
1450
1543
  if (indexCount > 120000) return;
1451
1544
 
1452
- const source = new THREE.EdgesGeometry(mesh.geometry, 40);
1545
+ // 1. 先做顶点容差焊合,让相邻三角形正确共享边索引。
1546
+ // S3M 几何常为「每面独立顶点」,不焊合的话 EdgesGeometry 会把每条三角形边都当成边界,棱线爆炸。
1547
+ // tolerance 取米级模型下的 5mm,足以合并重合顶点又不会误并真实相邻面。
1548
+ const welded = BufferGeometryUtils.mergeVertices(mesh.geometry, 0.005);
1549
+ // 2. 调大临界角(默认 55°,可在 style.edgeThreshold 调),过滤浅坡与曲面细分,只保留真正的棱。
1550
+ const thresholdAngle = this.style.edgeThreshold ?? 55;
1551
+ const source = new THREE.EdgesGeometry(welded, thresholdAngle);
1552
+ welded.dispose();
1553
+
1453
1554
  const attr = source.getAttribute('position') as THREE.BufferAttribute | undefined;
1454
1555
  const segments = attr ? (attr.array as Float32Array) : null;
1455
1556
  source.dispose();
@@ -110,6 +110,8 @@ export interface S3MStyle {
110
110
  edgeWidth?: number;
111
111
  edgeColor: string;
112
112
  edgeOpacity?: number;
113
+ /** 棱线临界角(度):相邻面法线夹角超过该值才描边,过滤掉浅坡与曲面细分。默认 55° */
114
+ edgeThreshold?: number;
113
115
  roofThreshold?: number;
114
116
  /** 是否与模型原色贴图叠加混合(0 = 纯色覆盖,1 = 叠乘原色贴图) */
115
117
  texMix?: number;
@@ -139,9 +141,10 @@ export const DEFAULT_STYLE: S3MStyle = {
139
141
  outlineWidth: 0.9,
140
142
  outlineWidthPx: 3,
141
143
  edges: false,
142
- edgeWidth: 1.2,
144
+ edgeWidth: 1.5,
143
145
  edgeColor: '#334155',
144
- edgeOpacity: 0.45,
146
+ edgeOpacity: 0.6,
147
+ edgeThreshold: 55,
145
148
  roofThreshold: 0.65,
146
149
  texMix: 0,
147
150
  depthWrite: true,