s3m-three 0.1.0 → 0.2.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 (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/render/DatasetManager.d.ts +10 -3
  3. package/dist/render/DatasetManager.d.ts.map +1 -1
  4. package/dist/render/DatasetManager.js +14 -5
  5. package/dist/render/DatasetManager.js.map +1 -1
  6. package/dist/render/EdgeLines.js +46 -46
  7. package/dist/render/GroundManager.d.ts +9 -0
  8. package/dist/render/GroundManager.d.ts.map +1 -1
  9. package/dist/render/GroundManager.js +13 -1
  10. package/dist/render/GroundManager.js.map +1 -1
  11. package/dist/render/Picker.js +66 -66
  12. package/dist/render/S3MViewer.d.ts +11 -3
  13. package/dist/render/S3MViewer.d.ts.map +1 -1
  14. package/dist/render/S3MViewer.js +18 -3
  15. package/dist/render/S3MViewer.js.map +1 -1
  16. package/dist/render/StyleMaterial.js +265 -265
  17. package/dist/vendor/meshopt_decoder.module.js +118 -118
  18. package/docs/API.md +20 -0
  19. package/package.json +1 -1
  20. package/src/core/BinaryReader.ts +116 -116
  21. package/src/core/DXTTextureDecode.ts +260 -260
  22. package/src/core/FeatureInfo.ts +150 -150
  23. package/src/core/MeshOpt.ts +401 -401
  24. package/src/core/S3MBParser.ts +590 -590
  25. package/src/core/types.ts +222 -222
  26. package/src/render/DatasetManager.ts +17 -6
  27. package/src/render/EdgeLines.ts +343 -343
  28. package/src/render/GeometryBuilder.ts +840 -840
  29. package/src/render/GroundManager.ts +14 -1
  30. package/src/render/Picker.ts +293 -293
  31. package/src/render/S3MViewer.ts +18 -3
  32. package/src/render/StyleMaterial.ts +602 -602
  33. package/src/render/TextureFactory.ts +134 -134
  34. package/src/tileset/S3MTileset.ts +552 -552
  35. package/src/vendor/meshopt_decoder.module.d.ts +31 -31
  36. package/src/vendor/meshopt_decoder.module.js +118 -118
  37. package/dist/render/CoordinateHelper.d.ts +0 -90
  38. package/dist/render/CoordinateHelper.d.ts.map +0 -1
  39. package/dist/render/CoordinateHelper.js +0 -85
  40. package/dist/render/CoordinateHelper.js.map +0 -1
  41. package/dist/render/S3MLayer.d.ts +0 -457
  42. package/dist/render/S3MLayer.d.ts.map +0 -1
  43. package/dist/render/S3MLayer.js +0 -1860
  44. package/dist/render/S3MLayer.js.map +0 -1
@@ -1,343 +1,343 @@
1
- /**
2
- * 屏幕空间等宽棱线
3
- *
4
- * 为什么不用 `LineBasicMaterial`:WebGL 下 `linewidth` 在绝大多数平台被忽略,
5
- * 线宽恒定 1px,想「调粗细」是做不到的。所以要自己把每条线段摊成一条面向屏幕的四边形:
6
- * 每条棱 4 个顶点,顶点着色器里把两端点投到屏幕空间,沿屏幕法线方向撑开 `uWidth` 个像素,
7
- * 再沿线段方向各外延半线宽,把相邻棱线的接缝盖住,避免拐角处出现缺口。
8
- *
9
- * 顶点布局(每段 4 个顶点,2 个三角形):
10
- * v0 = A 端, aSide = -1 v1 = A 端, aSide = +1
11
- * v2 = B 端, aSide = +1 v3 = B 端, aSide = -1
12
- * B 端的 side 之所以反号,是因为着色器里方向向量统一取「aOther → position」,
13
- * B 端算出来的法线正好和 A 端相反,反号一次才能拼成同一个四边形。
14
- */
15
- import * as THREE from 'three';
16
-
17
- /** 所有等宽线材质共享的分辨率 uniform(每帧更新一次即可) */
18
- export interface LineSharedUniforms {
19
- uResolution: { value: THREE.Vector2 };
20
- uDpr: { value: number };
21
- }
22
-
23
- export interface ThickLineMaterial extends THREE.ShaderMaterial {
24
- uniforms: LineSharedUniforms & {
25
- uColor: { value: THREE.Color };
26
- uOpacity: { value: number };
27
- uWidth: { value: number };
28
- uElevation: { value: number };
29
- [uniform: string]: THREE.IUniform;
30
- };
31
- }
32
-
33
- export function createLineSharedUniforms(): LineSharedUniforms {
34
- return {
35
- uResolution: { value: new THREE.Vector2(1, 1) },
36
- uDpr: { value: 1 },
37
- };
38
- }
39
-
40
- const VERT = /* glsl */ `
41
- attribute vec3 aOther;
42
- attribute float aSide;
43
-
44
- uniform vec2 uResolution;
45
- uniform float uDpr;
46
- uniform float uWidth;
47
- uniform float uElevation;
48
- uniform vec3 uTranslation;
49
-
50
- void main() {
51
- // 选中单体平移/拔高:与着色器里 transformed += uTranslation 保持同一口径(物体空间)
52
- vec3 offset = uTranslation + vec3( 0.0, 0.0, uElevation );
53
- vec4 clipA = projectionMatrix * modelViewMatrix * vec4( position + offset, 1.0 );
54
- vec4 clipB = projectionMatrix * modelViewMatrix * vec4( aOther + offset, 1.0 );
55
-
56
- if ( clipA.w <= 0.0 || clipB.w <= 0.0 ) {
57
- // 端点跑到相机后面,整段丢弃(裁到近平面的线段会有轻微截断,可接受)
58
- gl_Position = vec4( 2.0, 2.0, 2.0, 1.0 );
59
- return;
60
- }
61
-
62
- vec2 sA = clipA.xy / clipA.w;
63
- vec2 sB = clipB.xy / clipB.w;
64
-
65
- // 屏幕空间方向(设备像素),再取法线
66
- vec2 dirPx = ( sB - sA ) * uResolution;
67
- float len = length( dirPx );
68
- dirPx = len > 1e-6 ? dirPx / len : vec2( 1.0, 0.0 );
69
- vec2 nrm = vec2( -dirPx.y, dirPx.x );
70
-
71
- // 半线宽:一侧撑开,一端外延(-dirPx 对两端都是「往外」的,见文件头)
72
- float halfPx = 0.5 * uWidth * uDpr;
73
- vec2 offsetPx = ( nrm * aSide - dirPx ) * halfPx;
74
- vec2 ndc = clipA.xy / clipA.w + offsetPx * 2.0 / uResolution;
75
-
76
- gl_Position = vec4( ndc * clipA.w, clipA.z, clipA.w );
77
- }
78
- `;
79
-
80
- const FRAG = /* glsl */ `
81
- uniform vec3 uColor;
82
- uniform float uOpacity;
83
-
84
- void main() {
85
- if ( uOpacity <= 0.003 ) discard;
86
- gl_FragColor = vec4( uColor, uOpacity );
87
- }
88
- `;
89
-
90
- export function createThickLineMaterial(
91
- shared: LineSharedUniforms,
92
- opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {}
93
- ): ThickLineMaterial {
94
- const mat = new THREE.ShaderMaterial({
95
- uniforms: {
96
- uResolution: shared.uResolution,
97
- uDpr: shared.uDpr,
98
- uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
99
- uOpacity: { value: opts.opacity ?? 1 },
100
- uWidth: { value: opts.width ?? 1.5 },
101
- uElevation: { value: opts.elevation ?? 0 },
102
- uTranslation: { value: new THREE.Vector3(0, 0, 0) },
103
- },
104
- vertexShader: VERT,
105
- fragmentShader: FRAG,
106
- transparent: true,
107
- depthWrite: false,
108
- side: THREE.DoubleSide,
109
- // 棱线本来就和建筑表面共面,往前推一点避免 z-fighting
110
- polygonOffset: true,
111
- polygonOffsetFactor: -2,
112
- polygonOffsetUnits: -2,
113
- }) as ThickLineMaterial;
114
- (mat.userData as Record<string, unknown>).isThickLine = true;
115
- return mat;
116
- }
117
-
118
- /**
119
- * 线段对(每 6 个 float:x1 y1 z1 x2 y2 z2)→ 可绘制等宽线的几何体。
120
- */
121
- export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.BufferGeometry {
122
- const count = Math.floor(segments.length / 6);
123
- const vertexCount = count * 4;
124
-
125
- const position = new Float32Array(vertexCount * 3);
126
- const other = new Float32Array(vertexCount * 3);
127
- const side = new Float32Array(vertexCount);
128
- const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
129
-
130
- for (let i = 0; i < count; i++) {
131
- const s = i * 6;
132
- const ax = segments[s];
133
- const ay = segments[s + 1];
134
- const az = segments[s + 2];
135
- const bx = segments[s + 3];
136
- const by = segments[s + 4];
137
- const bz = segments[s + 5];
138
-
139
- const v = i * 4;
140
- // A 端两枚
141
- setVertex(v, ax, ay, az, bx, by, bz, -1);
142
- setVertex(v + 1, ax, ay, az, bx, by, bz, 1);
143
- // B 端两枚(side 反号)
144
- setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
145
- setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
146
-
147
- const o = i * 6;
148
- index[o] = v;
149
- index[o + 1] = v + 1;
150
- index[o + 2] = v + 2;
151
- index[o + 3] = v;
152
- index[o + 4] = v + 2;
153
- index[o + 5] = v + 3;
154
- }
155
-
156
- function setVertex(
157
- k: number,
158
- px: number,
159
- py: number,
160
- pz: number,
161
- ox: number,
162
- oy: number,
163
- oz: number,
164
- sd: number
165
- ): void {
166
- const p = k * 3;
167
- position[p] = px;
168
- position[p + 1] = py;
169
- position[p + 2] = pz;
170
- other[p] = ox;
171
- other[p + 1] = oy;
172
- other[p + 2] = oz;
173
- side[k] = sd;
174
- }
175
-
176
- const geometry = new THREE.BufferGeometry();
177
- geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
178
- geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
179
- geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
180
- geometry.setIndex(new THREE.BufferAttribute(index, 1));
181
- geometry.computeBoundingSphere();
182
- return geometry;
183
- }
184
-
185
- /* ------------------------------------------------------------------ 单体棱线提取 */
186
-
187
- export interface EdgeVertexRange {
188
- vertexOffset: number;
189
- vertexCount: number;
190
- }
191
-
192
- /** 单体棱线的线段上限,超过就放弃(宁可没有线,也不要把主线程卡住) */
193
- const MAX_EDGE_SEGMENTS = 60000;
194
- /** 非索引几何按位置量化去重用的小数位(0.01m 足够区分不同顶点) */
195
- const QUANT = 100;
196
-
197
- /**
198
- * 从几何里只挑出「指定顶点区间」的棱线,作为线段对返回。
199
- *
200
- * `ranges` 为空表示整块网格。判定规则与 `THREE.EdgesGeometry` 一致:
201
- * - 只被一个三角形用到的边 → 边界边,保留;
202
- * - 被两个三角形共用、且两面夹角大于 `creaseAngle` → 折边,保留;
203
- * - 其余(共面内部边)→ 丢掉,否则平面会被切成三角网格。
204
- *
205
- * 非索引几何没有共享顶点,直接用顶点号去重会让每条边都被判成边界边。
206
- * 所以这里先按位置量化把顶点归一化成 canonical id,再走同一套逻辑。
207
- */
208
- export function buildFeatureEdgeSegments(
209
- geometry: THREE.BufferGeometry,
210
- ranges?: EdgeVertexRange[] | null,
211
- creaseAngle = 40
212
- ): Float32Array | null {
213
- const position = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
214
- if (!position) return null;
215
- const total = position.count;
216
- if (total < 3) return null;
217
-
218
- const px = new Float32Array(total);
219
- const py = new Float32Array(total);
220
- const pz = new Float32Array(total);
221
- for (let i = 0; i < total; i++) {
222
- px[i] = position.getX(i);
223
- py[i] = position.getY(i);
224
- pz[i] = position.getZ(i);
225
- }
226
-
227
- // 顶点归一化:canon[i] → canonical id,rep[id] → 用于取坐标的代表顶点
228
- let canon: Uint32Array | null = null;
229
- let rep: Uint32Array | null = null;
230
- if (!geometry.index) {
231
- const lookup = new Map<string, number>();
232
- canon = new Uint32Array(total);
233
- rep = new Uint32Array(total);
234
- let n = 0;
235
- for (let i = 0; i < total; i++) {
236
- const k = `${Math.round(px[i] * QUANT)},${Math.round(py[i] * QUANT)},${Math.round(pz[i] * QUANT)}`;
237
- let id = lookup.get(k);
238
- if (id === undefined) {
239
- id = n++;
240
- lookup.set(k, id);
241
- rep[id] = i;
242
- }
243
- canon[i] = id;
244
- }
245
- }
246
- const cid = (i: number): number => (canon ? canon[i] : i);
247
- const repIdx = (c: number): number => (rep ? rep[c] : c);
248
-
249
- const index = geometry.index;
250
- const idxArr = index ? (index.array as ArrayLike<number>) : null;
251
- const triCount = index ? Math.floor(index.count / 3) : Math.floor(total / 3);
252
- if (triCount < 1) return null;
253
-
254
- let mask: Uint8Array | null = null;
255
- if (ranges && ranges.length > 0) {
256
- mask = new Uint8Array(total);
257
- for (const r of ranges) {
258
- const start = Math.max(0, r.vertexOffset | 0);
259
- const end = Math.min(total, start + (r.vertexCount | 0));
260
- if (end > start) mask.fill(1, start, end);
261
- }
262
- if (!mask.includes(1)) return null;
263
- }
264
-
265
- const normals = new Float32Array(triCount * 3);
266
- // key → 首次出现的三角形号;-1 已判定为折边,-2 已判定为平面内部边
267
- const seen = new Map<number, number>();
268
- const span = total + 1;
269
- const out: number[] = [];
270
- const cosLimit = Math.cos((creaseAngle * Math.PI) / 180);
271
- let tri = -1;
272
- let nx = 0;
273
- let ny = 0;
274
- let nz = 0;
275
-
276
- const emit = (lo: number, hi: number): boolean => {
277
- if (out.length >= MAX_EDGE_SEGMENTS * 6) return false;
278
- const a = repIdx(lo);
279
- const b = repIdx(hi);
280
- out.push(px[a], py[a], pz[a], px[b], py[b], pz[b]);
281
- return true;
282
- };
283
-
284
- const addEdge = (a: number, b: number): void => {
285
- if (a === b) return;
286
- const lo = a < b ? a : b;
287
- const hi = a < b ? b : a;
288
- const key = lo * span + hi;
289
- const first = seen.get(key);
290
- if (first === undefined) {
291
- seen.set(key, tri);
292
- return;
293
- }
294
- if (first < 0) return;
295
- // 与首个共用该边的三角形比夹角
296
- const dot = nx * normals[first * 3] + ny * normals[first * 3 + 1] + nz * normals[first * 3 + 2];
297
- if (dot < cosLimit) {
298
- emit(lo, hi);
299
- seen.set(key, -1);
300
- } else {
301
- seen.set(key, -2);
302
- }
303
- };
304
-
305
- for (tri = 0; tri < triCount; tri++) {
306
- const i0 = idxArr ? idxArr[tri * 3] : tri * 3;
307
- const i1 = idxArr ? idxArr[tri * 3 + 1] : tri * 3 + 1;
308
- const i2 = idxArr ? idxArr[tri * 3 + 2] : tri * 3 + 2;
309
- if (mask && !(mask[i0] && mask[i1] && mask[i2])) continue;
310
-
311
- const e1x = px[i1] - px[i0];
312
- const e1y = py[i1] - py[i0];
313
- const e1z = pz[i1] - pz[i0];
314
- const e2x = px[i2] - px[i0];
315
- const e2y = py[i2] - py[i0];
316
- const e2z = pz[i2] - pz[i0];
317
- nx = e1y * e2z - e1z * e2y;
318
- ny = e1z * e2x - e1x * e2z;
319
- nz = e1x * e2y - e1y * e2x;
320
- const len = Math.hypot(nx, ny, nz);
321
- if (len < 1e-12) continue; // 退化三角形,法线无意义
322
- nx /= len;
323
- ny /= len;
324
- nz /= len;
325
- normals[tri * 3] = nx;
326
- normals[tri * 3 + 1] = ny;
327
- normals[tri * 3 + 2] = nz;
328
-
329
- addEdge(cid(i0), cid(i1));
330
- addEdge(cid(i1), cid(i2));
331
- addEdge(cid(i2), cid(i0));
332
- }
333
-
334
- // 只出现一次的边 = 边界边(洞口、女儿墙顶沿等),保留
335
- for (const [key, first] of seen) {
336
- if (first < 0) continue;
337
- const lo = Math.floor(key / span);
338
- const hi = key - lo * span;
339
- if (!emit(lo, hi)) break;
340
- }
341
-
342
- return out.length > 0 ? new Float32Array(out) : null;
343
- }
1
+ /**
2
+ * 屏幕空间等宽棱线
3
+ *
4
+ * 为什么不用 `LineBasicMaterial`:WebGL 下 `linewidth` 在绝大多数平台被忽略,
5
+ * 线宽恒定 1px,想「调粗细」是做不到的。所以要自己把每条线段摊成一条面向屏幕的四边形:
6
+ * 每条棱 4 个顶点,顶点着色器里把两端点投到屏幕空间,沿屏幕法线方向撑开 `uWidth` 个像素,
7
+ * 再沿线段方向各外延半线宽,把相邻棱线的接缝盖住,避免拐角处出现缺口。
8
+ *
9
+ * 顶点布局(每段 4 个顶点,2 个三角形):
10
+ * v0 = A 端, aSide = -1 v1 = A 端, aSide = +1
11
+ * v2 = B 端, aSide = +1 v3 = B 端, aSide = -1
12
+ * B 端的 side 之所以反号,是因为着色器里方向向量统一取「aOther → position」,
13
+ * B 端算出来的法线正好和 A 端相反,反号一次才能拼成同一个四边形。
14
+ */
15
+ import * as THREE from 'three';
16
+
17
+ /** 所有等宽线材质共享的分辨率 uniform(每帧更新一次即可) */
18
+ export interface LineSharedUniforms {
19
+ uResolution: { value: THREE.Vector2 };
20
+ uDpr: { value: number };
21
+ }
22
+
23
+ export interface ThickLineMaterial extends THREE.ShaderMaterial {
24
+ uniforms: LineSharedUniforms & {
25
+ uColor: { value: THREE.Color };
26
+ uOpacity: { value: number };
27
+ uWidth: { value: number };
28
+ uElevation: { value: number };
29
+ [uniform: string]: THREE.IUniform;
30
+ };
31
+ }
32
+
33
+ export function createLineSharedUniforms(): LineSharedUniforms {
34
+ return {
35
+ uResolution: { value: new THREE.Vector2(1, 1) },
36
+ uDpr: { value: 1 },
37
+ };
38
+ }
39
+
40
+ const VERT = /* glsl */ `
41
+ attribute vec3 aOther;
42
+ attribute float aSide;
43
+
44
+ uniform vec2 uResolution;
45
+ uniform float uDpr;
46
+ uniform float uWidth;
47
+ uniform float uElevation;
48
+ uniform vec3 uTranslation;
49
+
50
+ void main() {
51
+ // 选中单体平移/拔高:与着色器里 transformed += uTranslation 保持同一口径(物体空间)
52
+ vec3 offset = uTranslation + vec3( 0.0, 0.0, uElevation );
53
+ vec4 clipA = projectionMatrix * modelViewMatrix * vec4( position + offset, 1.0 );
54
+ vec4 clipB = projectionMatrix * modelViewMatrix * vec4( aOther + offset, 1.0 );
55
+
56
+ if ( clipA.w <= 0.0 || clipB.w <= 0.0 ) {
57
+ // 端点跑到相机后面,整段丢弃(裁到近平面的线段会有轻微截断,可接受)
58
+ gl_Position = vec4( 2.0, 2.0, 2.0, 1.0 );
59
+ return;
60
+ }
61
+
62
+ vec2 sA = clipA.xy / clipA.w;
63
+ vec2 sB = clipB.xy / clipB.w;
64
+
65
+ // 屏幕空间方向(设备像素),再取法线
66
+ vec2 dirPx = ( sB - sA ) * uResolution;
67
+ float len = length( dirPx );
68
+ dirPx = len > 1e-6 ? dirPx / len : vec2( 1.0, 0.0 );
69
+ vec2 nrm = vec2( -dirPx.y, dirPx.x );
70
+
71
+ // 半线宽:一侧撑开,一端外延(-dirPx 对两端都是「往外」的,见文件头)
72
+ float halfPx = 0.5 * uWidth * uDpr;
73
+ vec2 offsetPx = ( nrm * aSide - dirPx ) * halfPx;
74
+ vec2 ndc = clipA.xy / clipA.w + offsetPx * 2.0 / uResolution;
75
+
76
+ gl_Position = vec4( ndc * clipA.w, clipA.z, clipA.w );
77
+ }
78
+ `;
79
+
80
+ const FRAG = /* glsl */ `
81
+ uniform vec3 uColor;
82
+ uniform float uOpacity;
83
+
84
+ void main() {
85
+ if ( uOpacity <= 0.003 ) discard;
86
+ gl_FragColor = vec4( uColor, uOpacity );
87
+ }
88
+ `;
89
+
90
+ export function createThickLineMaterial(
91
+ shared: LineSharedUniforms,
92
+ opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {}
93
+ ): ThickLineMaterial {
94
+ const mat = new THREE.ShaderMaterial({
95
+ uniforms: {
96
+ uResolution: shared.uResolution,
97
+ uDpr: shared.uDpr,
98
+ uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
99
+ uOpacity: { value: opts.opacity ?? 1 },
100
+ uWidth: { value: opts.width ?? 1.5 },
101
+ uElevation: { value: opts.elevation ?? 0 },
102
+ uTranslation: { value: new THREE.Vector3(0, 0, 0) },
103
+ },
104
+ vertexShader: VERT,
105
+ fragmentShader: FRAG,
106
+ transparent: true,
107
+ depthWrite: false,
108
+ side: THREE.DoubleSide,
109
+ // 棱线本来就和建筑表面共面,往前推一点避免 z-fighting
110
+ polygonOffset: true,
111
+ polygonOffsetFactor: -2,
112
+ polygonOffsetUnits: -2,
113
+ }) as ThickLineMaterial;
114
+ (mat.userData as Record<string, unknown>).isThickLine = true;
115
+ return mat;
116
+ }
117
+
118
+ /**
119
+ * 线段对(每 6 个 float:x1 y1 z1 x2 y2 z2)→ 可绘制等宽线的几何体。
120
+ */
121
+ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.BufferGeometry {
122
+ const count = Math.floor(segments.length / 6);
123
+ const vertexCount = count * 4;
124
+
125
+ const position = new Float32Array(vertexCount * 3);
126
+ const other = new Float32Array(vertexCount * 3);
127
+ const side = new Float32Array(vertexCount);
128
+ const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
129
+
130
+ for (let i = 0; i < count; i++) {
131
+ const s = i * 6;
132
+ const ax = segments[s];
133
+ const ay = segments[s + 1];
134
+ const az = segments[s + 2];
135
+ const bx = segments[s + 3];
136
+ const by = segments[s + 4];
137
+ const bz = segments[s + 5];
138
+
139
+ const v = i * 4;
140
+ // A 端两枚
141
+ setVertex(v, ax, ay, az, bx, by, bz, -1);
142
+ setVertex(v + 1, ax, ay, az, bx, by, bz, 1);
143
+ // B 端两枚(side 反号)
144
+ setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
145
+ setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
146
+
147
+ const o = i * 6;
148
+ index[o] = v;
149
+ index[o + 1] = v + 1;
150
+ index[o + 2] = v + 2;
151
+ index[o + 3] = v;
152
+ index[o + 4] = v + 2;
153
+ index[o + 5] = v + 3;
154
+ }
155
+
156
+ function setVertex(
157
+ k: number,
158
+ px: number,
159
+ py: number,
160
+ pz: number,
161
+ ox: number,
162
+ oy: number,
163
+ oz: number,
164
+ sd: number
165
+ ): void {
166
+ const p = k * 3;
167
+ position[p] = px;
168
+ position[p + 1] = py;
169
+ position[p + 2] = pz;
170
+ other[p] = ox;
171
+ other[p + 1] = oy;
172
+ other[p + 2] = oz;
173
+ side[k] = sd;
174
+ }
175
+
176
+ const geometry = new THREE.BufferGeometry();
177
+ geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
178
+ geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
179
+ geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
180
+ geometry.setIndex(new THREE.BufferAttribute(index, 1));
181
+ geometry.computeBoundingSphere();
182
+ return geometry;
183
+ }
184
+
185
+ /* ------------------------------------------------------------------ 单体棱线提取 */
186
+
187
+ export interface EdgeVertexRange {
188
+ vertexOffset: number;
189
+ vertexCount: number;
190
+ }
191
+
192
+ /** 单体棱线的线段上限,超过就放弃(宁可没有线,也不要把主线程卡住) */
193
+ const MAX_EDGE_SEGMENTS = 60000;
194
+ /** 非索引几何按位置量化去重用的小数位(0.01m 足够区分不同顶点) */
195
+ const QUANT = 100;
196
+
197
+ /**
198
+ * 从几何里只挑出「指定顶点区间」的棱线,作为线段对返回。
199
+ *
200
+ * `ranges` 为空表示整块网格。判定规则与 `THREE.EdgesGeometry` 一致:
201
+ * - 只被一个三角形用到的边 → 边界边,保留;
202
+ * - 被两个三角形共用、且两面夹角大于 `creaseAngle` → 折边,保留;
203
+ * - 其余(共面内部边)→ 丢掉,否则平面会被切成三角网格。
204
+ *
205
+ * 非索引几何没有共享顶点,直接用顶点号去重会让每条边都被判成边界边。
206
+ * 所以这里先按位置量化把顶点归一化成 canonical id,再走同一套逻辑。
207
+ */
208
+ export function buildFeatureEdgeSegments(
209
+ geometry: THREE.BufferGeometry,
210
+ ranges?: EdgeVertexRange[] | null,
211
+ creaseAngle = 40
212
+ ): Float32Array | null {
213
+ const position = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
214
+ if (!position) return null;
215
+ const total = position.count;
216
+ if (total < 3) return null;
217
+
218
+ const px = new Float32Array(total);
219
+ const py = new Float32Array(total);
220
+ const pz = new Float32Array(total);
221
+ for (let i = 0; i < total; i++) {
222
+ px[i] = position.getX(i);
223
+ py[i] = position.getY(i);
224
+ pz[i] = position.getZ(i);
225
+ }
226
+
227
+ // 顶点归一化:canon[i] → canonical id,rep[id] → 用于取坐标的代表顶点
228
+ let canon: Uint32Array | null = null;
229
+ let rep: Uint32Array | null = null;
230
+ if (!geometry.index) {
231
+ const lookup = new Map<string, number>();
232
+ canon = new Uint32Array(total);
233
+ rep = new Uint32Array(total);
234
+ let n = 0;
235
+ for (let i = 0; i < total; i++) {
236
+ const k = `${Math.round(px[i] * QUANT)},${Math.round(py[i] * QUANT)},${Math.round(pz[i] * QUANT)}`;
237
+ let id = lookup.get(k);
238
+ if (id === undefined) {
239
+ id = n++;
240
+ lookup.set(k, id);
241
+ rep[id] = i;
242
+ }
243
+ canon[i] = id;
244
+ }
245
+ }
246
+ const cid = (i: number): number => (canon ? canon[i] : i);
247
+ const repIdx = (c: number): number => (rep ? rep[c] : c);
248
+
249
+ const index = geometry.index;
250
+ const idxArr = index ? (index.array as ArrayLike<number>) : null;
251
+ const triCount = index ? Math.floor(index.count / 3) : Math.floor(total / 3);
252
+ if (triCount < 1) return null;
253
+
254
+ let mask: Uint8Array | null = null;
255
+ if (ranges && ranges.length > 0) {
256
+ mask = new Uint8Array(total);
257
+ for (const r of ranges) {
258
+ const start = Math.max(0, r.vertexOffset | 0);
259
+ const end = Math.min(total, start + (r.vertexCount | 0));
260
+ if (end > start) mask.fill(1, start, end);
261
+ }
262
+ if (!mask.includes(1)) return null;
263
+ }
264
+
265
+ const normals = new Float32Array(triCount * 3);
266
+ // key → 首次出现的三角形号;-1 已判定为折边,-2 已判定为平面内部边
267
+ const seen = new Map<number, number>();
268
+ const span = total + 1;
269
+ const out: number[] = [];
270
+ const cosLimit = Math.cos((creaseAngle * Math.PI) / 180);
271
+ let tri = -1;
272
+ let nx = 0;
273
+ let ny = 0;
274
+ let nz = 0;
275
+
276
+ const emit = (lo: number, hi: number): boolean => {
277
+ if (out.length >= MAX_EDGE_SEGMENTS * 6) return false;
278
+ const a = repIdx(lo);
279
+ const b = repIdx(hi);
280
+ out.push(px[a], py[a], pz[a], px[b], py[b], pz[b]);
281
+ return true;
282
+ };
283
+
284
+ const addEdge = (a: number, b: number): void => {
285
+ if (a === b) return;
286
+ const lo = a < b ? a : b;
287
+ const hi = a < b ? b : a;
288
+ const key = lo * span + hi;
289
+ const first = seen.get(key);
290
+ if (first === undefined) {
291
+ seen.set(key, tri);
292
+ return;
293
+ }
294
+ if (first < 0) return;
295
+ // 与首个共用该边的三角形比夹角
296
+ const dot = nx * normals[first * 3] + ny * normals[first * 3 + 1] + nz * normals[first * 3 + 2];
297
+ if (dot < cosLimit) {
298
+ emit(lo, hi);
299
+ seen.set(key, -1);
300
+ } else {
301
+ seen.set(key, -2);
302
+ }
303
+ };
304
+
305
+ for (tri = 0; tri < triCount; tri++) {
306
+ const i0 = idxArr ? idxArr[tri * 3] : tri * 3;
307
+ const i1 = idxArr ? idxArr[tri * 3 + 1] : tri * 3 + 1;
308
+ const i2 = idxArr ? idxArr[tri * 3 + 2] : tri * 3 + 2;
309
+ if (mask && !(mask[i0] && mask[i1] && mask[i2])) continue;
310
+
311
+ const e1x = px[i1] - px[i0];
312
+ const e1y = py[i1] - py[i0];
313
+ const e1z = pz[i1] - pz[i0];
314
+ const e2x = px[i2] - px[i0];
315
+ const e2y = py[i2] - py[i0];
316
+ const e2z = pz[i2] - pz[i0];
317
+ nx = e1y * e2z - e1z * e2y;
318
+ ny = e1z * e2x - e1x * e2z;
319
+ nz = e1x * e2y - e1y * e2x;
320
+ const len = Math.hypot(nx, ny, nz);
321
+ if (len < 1e-12) continue; // 退化三角形,法线无意义
322
+ nx /= len;
323
+ ny /= len;
324
+ nz /= len;
325
+ normals[tri * 3] = nx;
326
+ normals[tri * 3 + 1] = ny;
327
+ normals[tri * 3 + 2] = nz;
328
+
329
+ addEdge(cid(i0), cid(i1));
330
+ addEdge(cid(i1), cid(i2));
331
+ addEdge(cid(i2), cid(i0));
332
+ }
333
+
334
+ // 只出现一次的边 = 边界边(洞口、女儿墙顶沿等),保留
335
+ for (const [key, first] of seen) {
336
+ if (first < 0) continue;
337
+ const lo = Math.floor(key / span);
338
+ const hi = key - lo * span;
339
+ if (!emit(lo, hi)) break;
340
+ }
341
+
342
+ return out.length > 0 ? new Float32Array(out) : null;
343
+ }