s3m-three 0.3.0 → 0.3.1

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 (109) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +163 -15
  3. package/dist/browser/s3m-three.iife.js +190 -42
  4. package/dist/core/AtlasAlpha.d.ts +31 -0
  5. package/dist/core/AtlasAlpha.d.ts.map +1 -0
  6. package/dist/core/AtlasAlpha.js +82 -0
  7. package/dist/core/AtlasAlpha.js.map +1 -0
  8. package/dist/core/AtlasPack.d.ts +57 -0
  9. package/dist/core/AtlasPack.d.ts.map +1 -0
  10. package/dist/core/AtlasPack.js +81 -0
  11. package/dist/core/AtlasPack.js.map +1 -0
  12. package/dist/core/AtlasUpgrade.d.ts +68 -0
  13. package/dist/core/AtlasUpgrade.d.ts.map +1 -0
  14. package/dist/core/AtlasUpgrade.js +154 -0
  15. package/dist/core/AtlasUpgrade.js.map +1 -0
  16. package/dist/core/DXTTextureDecode.d.ts.map +1 -1
  17. package/dist/core/DXTTextureDecode.js +28 -6
  18. package/dist/core/DXTTextureDecode.js.map +1 -1
  19. package/dist/core/Draco.d.ts +52 -0
  20. package/dist/core/Draco.d.ts.map +1 -0
  21. package/dist/core/Draco.js +323 -0
  22. package/dist/core/Draco.js.map +1 -0
  23. package/dist/core/MeshOpt.d.ts +14 -3
  24. package/dist/core/MeshOpt.d.ts.map +1 -1
  25. package/dist/core/MeshOpt.js +101 -18
  26. package/dist/core/MeshOpt.js.map +1 -1
  27. package/dist/core/S3M1Parser.d.ts +16 -0
  28. package/dist/core/S3M1Parser.d.ts.map +1 -0
  29. package/dist/core/S3M1Parser.js +367 -0
  30. package/dist/core/S3M1Parser.js.map +1 -0
  31. package/dist/core/S3MBParser.d.ts +43 -1
  32. package/dist/core/S3MBParser.d.ts.map +1 -1
  33. package/dist/core/S3MBParser.js +101 -23
  34. package/dist/core/S3MBParser.js.map +1 -1
  35. package/dist/core/S3MBlockParser.d.ts +209 -0
  36. package/dist/core/S3MBlockParser.d.ts.map +1 -0
  37. package/dist/core/S3MBlockParser.js +789 -0
  38. package/dist/core/S3MBlockParser.js.map +1 -0
  39. package/dist/core/TextureFormat.d.ts +0 -1
  40. package/dist/core/TextureFormat.d.ts.map +1 -1
  41. package/dist/core/TextureFormat.js +19 -2
  42. package/dist/core/TextureFormat.js.map +1 -1
  43. package/dist/core/types.d.ts +65 -0
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +2 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/render/DatasetManager.d.ts.map +1 -1
  50. package/dist/render/DatasetManager.js +5 -3
  51. package/dist/render/DatasetManager.js.map +1 -1
  52. package/dist/render/GeometryBuilder.d.ts +2 -0
  53. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  54. package/dist/render/GeometryBuilder.js +185 -16
  55. package/dist/render/GeometryBuilder.js.map +1 -1
  56. package/dist/render/GroundManager.d.ts +57 -1
  57. package/dist/render/GroundManager.d.ts.map +1 -1
  58. package/dist/render/GroundManager.js +149 -21
  59. package/dist/render/GroundManager.js.map +1 -1
  60. package/dist/render/Picker.d.ts.map +1 -1
  61. package/dist/render/Picker.js +12 -4
  62. package/dist/render/Picker.js.map +1 -1
  63. package/dist/render/PointCloudMaterial.d.ts +36 -0
  64. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  65. package/dist/render/PointCloudMaterial.js +159 -0
  66. package/dist/render/PointCloudMaterial.js.map +1 -0
  67. package/dist/render/S3MViewer.d.ts +19 -0
  68. package/dist/render/S3MViewer.d.ts.map +1 -1
  69. package/dist/render/S3MViewer.js +143 -22
  70. package/dist/render/S3MViewer.js.map +1 -1
  71. package/dist/render/StyleMaterial.d.ts +6 -0
  72. package/dist/render/StyleMaterial.d.ts.map +1 -1
  73. package/dist/render/StyleMaterial.js +125 -2
  74. package/dist/render/StyleMaterial.js.map +1 -1
  75. package/dist/render/TextureFactory.d.ts.map +1 -1
  76. package/dist/render/TextureFactory.js +64 -10
  77. package/dist/render/TextureFactory.js.map +1 -1
  78. package/dist/tileset/S3MTileset.d.ts +152 -1
  79. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  80. package/dist/tileset/S3MTileset.js +669 -53
  81. package/dist/tileset/S3MTileset.js.map +1 -1
  82. package/dist/vendor/crunch_decoder.module.js +12 -18
  83. package/dist/vendor/draco_decoder.module.js +3178 -0
  84. package/docs/API.md +110 -9
  85. package/package.json +1 -1
  86. package/src/core/AtlasAlpha.ts +82 -0
  87. package/src/core/AtlasPack.ts +127 -0
  88. package/src/core/AtlasUpgrade.ts +182 -0
  89. package/src/core/DXTTextureDecode.ts +28 -6
  90. package/src/core/Draco.ts +409 -0
  91. package/src/core/MeshOpt.ts +124 -18
  92. package/src/core/S3M1Parser.ts +430 -0
  93. package/src/core/S3MBParser.ts +112 -21
  94. package/src/core/S3MBlockParser.ts +953 -0
  95. package/src/core/TextureFormat.ts +20 -2
  96. package/src/core/types.ts +63 -0
  97. package/src/index.ts +2 -1
  98. package/src/render/DatasetManager.ts +5 -5
  99. package/src/render/GeometryBuilder.ts +207 -15
  100. package/src/render/GroundManager.ts +174 -20
  101. package/src/render/Picker.ts +13 -4
  102. package/src/render/PointCloudMaterial.ts +174 -0
  103. package/src/render/S3MViewer.ts +140 -24
  104. package/src/render/StyleMaterial.ts +135 -2
  105. package/src/render/TextureFactory.ts +67 -11
  106. package/src/tileset/S3MTileset.ts +701 -58
  107. package/src/vendor/crunch_decoder.module.js +12 -18
  108. package/src/vendor/draco_decoder.module.d.ts +128 -0
  109. package/src/vendor/draco_decoder.module.js +3178 -0
@@ -0,0 +1,430 @@
1
+ /**
2
+ * S3M 1.x(SuperMapCache,文件后缀 `.s3m`)容器解析
3
+ *
4
+ * iServer 把 OSGB 等倾斜摄影成果按这种格式发布。输出与 `parseS3MB` 同形状的
5
+ * `ParsedTile`,因此 GeometryBuilder / S3MTileset / 拾取这套下游不用改。
6
+ *
7
+ * 结构(root / L16 / L17 / L18 四块真实瓦片交叉标定,链尾全部精确闭合):
8
+ * ```
9
+ * byte[4] 's3m\0' + zlib
10
+ * 解压后:
11
+ * uint32 xmlSize; byte[] XML; <补齐 4 字节对齐>
12
+ * uint32 geoSize; 几何流
13
+ * uint32 4; uint32 0 // 固定小段,实测恒 0
14
+ * uint32 texSize; 纹理流
15
+ *
16
+ * 几何流: uint32 meshCount,然后每块网格
17
+ * uint32 geoName; uint32 7; uint32 772; uint32 V; uint32 0
18
+ * V × 16B 顶点 (x, y, z, w),实测 w 恒等于 z
19
+ * uint32[6] 数组头 [0,0,0,kind=1,count=V,flag=2]
20
+ * V × 8B UV (u, v)
21
+ * uint32[3] 索引头 [1, indexCount, 0x00040100]
22
+ * indexCount × uint16 // indexCount 恒为 3 的倍数,值域最大 V-1
23
+ * <补齐 4 字节对齐>
24
+ * uint32 1; uint32 materialId // 与 XML <material><name> 一致
25
+ *
26
+ * 纹理流: uint32 count,然后每条
27
+ * uint32 id; uint32 width; uint32 height; uint32 format; uint32 size; uint32 flag
28
+ * byte[size] // 实测 format=14、0.5B/px,即 DXT1
29
+ * ```
30
+ *
31
+ * 几何里**没有法线数组**,交给 GeometryBuilder 在绑定索引后 computeVertexNormals()。
32
+ */
33
+ import { BinaryReader } from './BinaryReader.js';
34
+ import type {
35
+ GeoPackageEntry,
36
+ GroupNode,
37
+ IndexPackage,
38
+ PageLOD,
39
+ ParsedTile,
40
+ S3MTexture,
41
+ VertexAttribute,
42
+ VertexPackage,
43
+ } from './types.js';
44
+
45
+ /** S3M 1.x 魔数:ASCII "s3m" + 结尾 0 */
46
+ const S3M_V1_MAGIC = 0x006d3373;
47
+
48
+ export interface ParseS3M1Options {
49
+ inflate?: (bytes: Uint8Array) => Uint8Array | Promise<Uint8Array>;
50
+ }
51
+
52
+ /** 是不是 S3M 1.x 容器 */
53
+ export function isS3M1Buffer(buffer: ArrayBuffer): boolean {
54
+ if (buffer.byteLength < 8) return false;
55
+ return new DataView(buffer, 0, 8).getUint32(0, true) === S3M_V1_MAGIC;
56
+ }
57
+
58
+ async function defaultInflate(bytes: Uint8Array): Promise<Uint8Array> {
59
+ const DS = (globalThis as { DecompressionStream?: unknown }).DecompressionStream as
60
+ | (new (format: string) => TransformStream)
61
+ | undefined;
62
+ if (!DS) throw new Error('[s3m] 当前环境无 DecompressionStream,请通过 options.inflate 注入解压实现');
63
+ const stream = new Blob([bytes as unknown as BlobPart])
64
+ .stream()
65
+ .pipeThrough(new DS('deflate') as unknown as TransformStream);
66
+ const out = await new Response(stream).arrayBuffer();
67
+ return new Uint8Array(out);
68
+ }
69
+
70
+ const align4 = (n: number): number => (n % 4 === 0 ? n : n + (4 - (n % 4)));
71
+
72
+ /* ------------------------------------------------------------------ XML */
73
+
74
+ interface XmlMaterial {
75
+ name: string;
76
+ diffuse: { r: number; g: number; b: number; a: number };
77
+ shininess: number;
78
+ cullMode: string;
79
+ transparentSorting: boolean;
80
+ textureName?: string;
81
+ addressMode?: { u: number; v: number };
82
+ }
83
+
84
+ interface XmlLod {
85
+ geoNames: string[];
86
+ matrix: number[];
87
+ childTile: string;
88
+ rangeMode: number;
89
+ rangeValue: number;
90
+ center: { x: number; y: number; z: number };
91
+ radius: number;
92
+ }
93
+
94
+ function num(text: string | null | undefined, fallback = 0): number {
95
+ const v = parseFloat(text ?? '');
96
+ return Number.isFinite(v) ? v : fallback;
97
+ }
98
+
99
+ /** 抓 <Tag>value</Tag>(带命名空间前缀,取第一个) */
100
+ function tag(scope: string, name: string): string | null {
101
+ const m = new RegExp(`<(?:[A-Za-z0-9_-]+:)?${name}[^>]*>([^<]*)<\\/(?:[A-Za-z0-9_-]+:)?${name}>`, 'i').exec(scope);
102
+ return m ? m[1].trim() : null;
103
+ }
104
+
105
+ function allTags(scope: string, name: string): string[] {
106
+ const re = new RegExp(`<(?:[A-Za-z0-9_-]+:)?${name}[^>]*>([^<]*)<\\/(?:[A-Za-z0-9_-]+:)?${name}>`, 'gi');
107
+ return [...scope.matchAll(re)].map((m) => m[1].trim());
108
+ }
109
+
110
+ /** RangeMode 文本 → 数值(规范:0 = 视点距离,1 = 屏幕像素) */
111
+ function rangeModeOf(text: string | null): number {
112
+ const t = (text ?? '').toUpperCase();
113
+ if (t.includes('DISTANCE')) return 0;
114
+ if (t.includes('GEOMETRIC') || t === '2') return 2;
115
+ return 1;
116
+ }
117
+
118
+ function parseXmlLods(xml: string): XmlLod[] {
119
+ const blocks = [...xml.matchAll(/<(?:[A-Za-z0-9_-]+:)?PagedLOD[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?PagedLOD>/gi)];
120
+ const out: XmlLod[] = [];
121
+ for (const b of blocks) {
122
+ const s = b[1];
123
+ const matrix = (tag(s, 'GeoDeModMatrix') ?? '1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1')
124
+ .split(',')
125
+ .map((x) => parseFloat(x.trim()))
126
+ .filter((x) => Number.isFinite(x));
127
+ // 中心/半径只在 <BoundingSphere> 作用域里取,否则 <x> 会误吃到别处的同名标签
128
+ const sphere =
129
+ /<(?:[A-Za-z0-9_-]+:)?BoundingSphere[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?BoundingSphere>/i.exec(s)?.[1] ?? '';
130
+ out.push({
131
+ geoNames: allTags(s, 'GeoName'),
132
+ matrix: matrix.length === 16 ? matrix : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
133
+ childTile: tag(s, 'RangeDataList') ?? '',
134
+ rangeMode: rangeModeOf(tag(s, 'RangeMode')),
135
+ rangeValue: num(tag(s, 'RangeList')),
136
+ center: { x: num(tag(sphere, 'x')), y: num(tag(sphere, 'y')), z: num(tag(sphere, 'z')) },
137
+ radius: num(tag(sphere, 'radius')),
138
+ });
139
+ }
140
+ return out;
141
+ }
142
+
143
+ function parseXmlMaterials(xml: string): XmlMaterial[] {
144
+ const blocks = [...xml.matchAll(/<(?:[A-Za-z0-9_-]+:)?material[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?material>/gi)];
145
+ const out: XmlMaterial[] = [];
146
+ for (const b of blocks) {
147
+ const s = b[1];
148
+ const rgba = (name: string) => {
149
+ const inner = new RegExp(`<(?:[A-Za-z0-9_-]+:)?${name}[^>]*>([\\s\\S]*?)<\\/(?:[A-Za-z0-9_-]+:)?${name}>`, 'i').exec(s);
150
+ if (!inner) return { r: 1, g: 1, b: 1, a: 1 };
151
+ const t = inner[1];
152
+ // 1.x 的取值键名实测是小写 r/g/b/a,个别版本写 A/R/G/B
153
+ return { r: num(tag(t, 'r'), 1), g: num(tag(t, 'g'), 1), b: num(tag(t, 'b'), 1), a: num(tag(t, 'a'), 1) };
154
+ };
155
+ out.push({
156
+ name: tag(s, 'name') ?? '',
157
+ diffuse: rgba('Diffuse'),
158
+ shininess: num(tag(s, 'Shininess')),
159
+ cullMode: tag(s, 'CullMode') ?? tag(s, 'cullmode') ?? 'none',
160
+ transparentSorting: (tag(s, 'TransparentSorting') ?? 'false').toLowerCase() === 'true',
161
+ textureName: undefined,
162
+ addressMode: undefined,
163
+ });
164
+ // 纹理单元:<texture><name>纹理号</name><AddressMode><u><v>…</texture>
165
+ const tex = /<(?:[A-Za-z0-9_-]+:)?texture[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?texture>/i.exec(s);
166
+ if (tex) {
167
+ const t = tex[1];
168
+ const last = out[out.length - 1];
169
+ last.textureName = tag(t, 'name') ?? undefined;
170
+ const am = /<(?:[A-Za-z0-9_-]+:)?AddressMode[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?AddressMode>/i.exec(t);
171
+ if (am) last.addressMode = { u: num(tag(am[1], 'u')), v: num(tag(am[1], 'v')) };
172
+ }
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /* ------------------------------------------------------------------ 几何 */
178
+
179
+ interface MeshRecord {
180
+ geoName: string;
181
+ vertexCount: number;
182
+ position: Float32Array;
183
+ uv: Float32Array;
184
+ indexCount: number;
185
+ indices: Uint16Array;
186
+ materialId: string;
187
+ }
188
+
189
+ function readGeometry(br: BinaryReader, geoSize: number): MeshRecord[] {
190
+ const meshCount = br.u32();
191
+ if (meshCount > 4096) throw new Error(`[s3m] S3M 1.x 网格数异常:${meshCount}`);
192
+ const out: MeshRecord[] = [];
193
+
194
+ for (let m = 0; m < meshCount; m++) {
195
+ const geoName = String(br.u32());
196
+ const seven = br.u32();
197
+ const seven72 = br.u32();
198
+ const V = br.u32();
199
+ br.u32(); // 恒 0
200
+ if (!(V > 0 && V <= 8_000_000)) {
201
+ throw new Error(`[s3m] S3M 1.x 顶点数异常:网格 ${geoName} V=${V}(字段 7=${seven} 772=${seven72})`);
202
+ }
203
+
204
+ // 顶点 (x, y, z, w);w 实测恒等于 z
205
+ const position = new Float32Array(V * 3);
206
+ for (let i = 0; i < V; i++) {
207
+ const x = br.f32();
208
+ const y = br.f32();
209
+ const z = br.f32();
210
+ br.f32(); // w == z
211
+ position[i * 3] = x;
212
+ position[i * 3 + 1] = y;
213
+ position[i * 3 + 2] = z;
214
+ }
215
+
216
+ // UV 数组头 [0,0,0,kind=1,count=V,flag=2]
217
+ br.u32(); br.u32(); br.u32();
218
+ const uvKind = br.u32();
219
+ const uvCount = br.u32();
220
+ br.u32(); // flag
221
+ if (uvKind !== 1 || uvCount !== V) {
222
+ throw new Error(`[s3m] S3M 1.x UV 头不符:网格 ${geoName} kind=${uvKind} count=${uvCount} 期望 1/${V}`);
223
+ }
224
+ const uv = new Float32Array(V * 2);
225
+ for (let i = 0; i < V; i++) {
226
+ uv[i * 2] = br.f32();
227
+ uv[i * 2 + 1] = br.f32();
228
+ }
229
+
230
+ // 索引头 [1, indexCount, 0x00040100]
231
+ const idxKind = br.u32();
232
+ const indexCount = br.u32();
233
+ br.u32(); // 常量标记
234
+ if (idxKind !== 1 || indexCount === 0 || indexCount % 3 !== 0) {
235
+ throw new Error(`[s3m] S3M 1.x 索引头不符:网格 ${geoName} kind=${idxKind} count=${indexCount}`);
236
+ }
237
+ const indices = new Uint16Array(indexCount);
238
+ for (let i = 0; i < indexCount; i++) indices[i] = br.u16();
239
+ if (br.offset > geoSize) throw new Error(`[s3m] S3M 1.x 索引越界:网格 ${geoName}`);
240
+ br.align4();
241
+
242
+ const one = br.u32();
243
+ const materialId = String(br.u32());
244
+ if (one !== 1) {
245
+ throw new Error(`[s3m] S3M 1.x 记录尾标记异常:网格 ${geoName} 期望 1 实得 ${one}`);
246
+ }
247
+ out.push({ geoName, vertexCount: V, position, uv, indexCount, indices, materialId });
248
+ }
249
+ return out;
250
+ }
251
+
252
+ /**
253
+ * 按「字节/像素」反推 S3MPixelFormat。
254
+ *
255
+ * v1 头里那个 format 数字不能直接当 pixelFormat 用(实测恒为 14,而本库枚举里
256
+ * 14 = BGRA = 4 字节/像素,照搬会把 0.5 字节/像素的数据当 4 字节读 → 满屏彩噪)。
257
+ * 压缩格式与密度是一一对应的,所以用密度判定,并把原始数字留在告警里。
258
+ */
259
+ export function pixelFormatOfBpp(bpp: number, rawFormat: number): number {
260
+ if (!Number.isFinite(bpp) || bpp <= 0) return rawFormat;
261
+ if (Math.abs(bpp - 0.5) < 0.02) return 17; // DXT1
262
+ if (Math.abs(bpp - 1) < 0.02) return 21; // DXT5
263
+ if (Math.abs(bpp - 4) < 0.02) return 14; // BGRA
264
+ if (Math.abs(bpp - 3) < 0.02) return 10; // RGB
265
+ console.warn(`[s3m] S3M 1.x 纹理密度 ${bpp.toFixed(3)} B/px 不在已知档位,按原 format=${rawFormat} 交给上层判定`);
266
+ return rawFormat;
267
+ }
268
+
269
+ /* ------------------------------------------------------------------ 纹理 */
270
+
271
+ function readTextures(br: BinaryReader, texSize: number): Record<string, S3MTexture> {
272
+ const count = br.u32();
273
+ const out: Record<string, S3MTexture> = {};
274
+ if (count > 1024) throw new Error(`[s3m] S3M 1.x 纹理数异常:${count}`);
275
+ for (let i = 0; i < count; i++) {
276
+ const id = String(br.u32());
277
+ const width = br.u32();
278
+ const height = br.u32();
279
+ const format = br.u32();
280
+ const size = br.u32();
281
+ br.u32(); // flag
282
+ if (br.offset + size > texSize + 4) throw new Error(`[s3m] S3M 1.x 纹理越界:${id}`);
283
+ out[id] = {
284
+ code: id,
285
+ level: 0,
286
+ width,
287
+ height,
288
+ compressType: 0,
289
+ // v1 头里的 format 不是 S3MPixelFormat 枚举,照搬会把纹理解成一片彩噪:
290
+ // 实测四块瓦片(512×512 / 256×128 / 512×256 / 1024×512)的 format 都是 14,
291
+ // 而 data 长度恒等于 width*height*0.5 —— 只有 DXT1 是这个密度
292
+ // (DXT5 是 1 B/px,BGRA 是 4 B/px,而本库枚举里 14 恰恰是 BGRA)。
293
+ // 所以按「字节/像素」反推,别信那个数字。
294
+ pixelFormat: pixelFormatOfBpp(size / (width * height), format),
295
+ // 必须翻回来:DXT 解码器内部按 GL 约定做了一次 Y 翻转(DecompressImage 里的
296
+ // height - 1 - sy),而 v1 的 UV 是「v = 0 对应文件首行」。
297
+ // 实测这块 512×512 的图集内容在文件行 232~511(前 232 行是空的纯黑),
298
+ // 而几何 UV 的 v 落在 0.483~0.983(= 行 245~503):
299
+ // 按文件行采 → 区域均值亮度 76.1(有内容)
300
+ // 被内部翻转后再采 → 均值 5.6(正好采到空的那半张 → 模型一片黑块)
301
+ // 交给 TextureFactory 的 flipRows 翻一次,就回到前者。
302
+ flipY: true,
303
+ data: br.bytesCopy(size),
304
+ };
305
+ }
306
+ return out;
307
+ }
308
+
309
+ /* ------------------------------------------------------------------ 入口 */
310
+
311
+ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Options = {}): Promise<ParsedTile> {
312
+ const inflate = options.inflate ?? defaultInflate;
313
+ const r = new BinaryReader(arrayBuffer);
314
+ r.skip(4); // 魔数
315
+ const zipped = new Uint8Array(arrayBuffer, r.base + r.offset, arrayBuffer.byteLength - r.base - r.offset);
316
+ const bytes = await inflate(zipped);
317
+ const raw = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes as ArrayBuffer);
318
+
319
+ const br = new BinaryReader(
320
+ raw.buffer.slice(raw.byteOffset, raw.byteOffset + raw.byteLength) as ArrayBuffer
321
+ );
322
+ const xmlSize = br.u32();
323
+ if (xmlSize <= 0 || xmlSize > raw.byteLength) throw new Error(`[s3m] S3M 1.x XML 段长度异常:${xmlSize}`);
324
+ const xml = new TextDecoder('utf-8').decode(br.bytes(xmlSize));
325
+ br.offset = align4(br.offset);
326
+
327
+ const geoSize = br.u32();
328
+ const geoStart = br.offset;
329
+ const meshes = readGeometry(new BinaryReader(br.buffer, br.base + geoStart, geoSize), geoSize);
330
+ br.offset = geoStart + geoSize;
331
+
332
+ const smallSize = br.u32();
333
+ br.skip(smallSize);
334
+ const texSize = br.u32();
335
+ const textures = readTextures(
336
+ new BinaryReader(br.buffer, br.base + br.offset, texSize),
337
+ texSize
338
+ );
339
+
340
+ /* ---- 组装成与 parseS3MB 一致的结构 ---- */
341
+ const geoPackage: Record<string, GeoPackageEntry> = {};
342
+ for (const mesh of meshes) {
343
+ const name = `SuperMapCache#${mesh.geoName}`;
344
+ const positionAttr: VertexAttribute = {
345
+ semantic: 'position',
346
+ data: new Uint8Array(mesh.position.buffer, mesh.position.byteOffset, mesh.position.byteLength),
347
+ components: 3,
348
+ stride: 12,
349
+ offsetInBytes: 0,
350
+ dtype: 'f32',
351
+ };
352
+ const uvAttr: VertexAttribute = {
353
+ semantic: 'uv',
354
+ data: new Uint8Array(mesh.uv.buffer, mesh.uv.byteOffset, mesh.uv.byteLength),
355
+ components: 2,
356
+ stride: 8,
357
+ offsetInBytes: 0,
358
+ dtype: 'f32',
359
+ };
360
+ const vp: VertexPackage = {
361
+ verticesCount: mesh.vertexCount,
362
+ vertexAttributes: [positionAttr, uvAttr],
363
+ attrLocation: { aposition: 0, auv: 1 },
364
+ instanceCount: 0,
365
+ instanceMode: 0,
366
+ instanceIndex: -1,
367
+ };
368
+ const pack: IndexPackage = {
369
+ indexCount: mesh.indexCount,
370
+ indexType: 0, // 0 = UNSIGNED_SHORT
371
+ useIndex: 1,
372
+ primitiveType: 4, // TRIANGLES
373
+ indices: new Uint8Array(mesh.indices.buffer, mesh.indices.byteOffset, mesh.indices.byteLength),
374
+ passNames: [mesh.materialId],
375
+ };
376
+ geoPackage[name] = { name, vertexPackage: vp, indexPackages: [pack] };
377
+ }
378
+
379
+ // pageLods 与网格必须按 GeoName 绑定,不能按出现顺序:
380
+ // 实测 L18 的 XML 列出 [2690529118, 1463699937, 451097586, 4063742103],
381
+ // 而二进制里网格是 [451097586, 1463699937, 2690529118, 4063742103] —— 同批不同序。
382
+ // 按位置配会把某个 LOD 的包围球/矩阵挂到别的网格上(单网格样本看不出问题)。
383
+ const keyOf = new Map(meshes.map((m) => [m.geoName, `SuperMapCache#${m.geoName}`]));
384
+ const xmlLods = parseXmlLods(xml);
385
+ const pageLods: PageLOD[] = [];
386
+ xmlLods.forEach((lod, i) => {
387
+ const named = lod.geoNames.map((n) => keyOf.get(n)).filter((k): k is string => !!k);
388
+ // 名字对不上时退回位置匹配,并让上层有机会发现(宁可少一个 LOD 也不要挂错球)
389
+ const fallback = meshes[i] ? [`SuperMapCache#${meshes[i].geoName}`] : [];
390
+ const skeletonNames = named.length ? [...new Set(named)] : fallback;
391
+ if (!skeletonNames.length) return;
392
+ pageLods.push({
393
+ rangeValue: lod.rangeValue,
394
+ rangeMode: lod.rangeMode,
395
+ boundingSphere: { center: { ...lod.center }, radius: lod.radius },
396
+ childTile: lod.childTile,
397
+ geodes: [{ matrix: lod.matrix, skeletonNames }],
398
+ });
399
+ });
400
+ const groupNode: GroupNode = { pageLods };
401
+
402
+ // 材质:拼成 3.x 那套 JSON 形状,下游 makeMaterial / resolveSide 直接吃
403
+ const materials = parseXmlMaterials(xml).map((m) => ({
404
+ material: {
405
+ name: m.name,
406
+ diffuse: m.diffuse,
407
+ shininess: m.shininess,
408
+ cullMode: m.cullMode,
409
+ transparentsorting: m.transparentSorting,
410
+ textureStates: m.textureName
411
+ ? [{ textureunitstate: { name: m.textureName, ...(m.addressMode ? { addressmode: m.addressMode } : {}) } }]
412
+ : [],
413
+ },
414
+ }));
415
+
416
+ return {
417
+ version: 1,
418
+ // 1.x 的整文件压缩就是 zlib,对齐 2.x/3.x 的 compressedType 语义(1 = zip)
419
+ compressedType: 1,
420
+ options: 0,
421
+ groupNode,
422
+ geoPackage,
423
+ textures,
424
+ materials,
425
+ pickInfo: null,
426
+ extensions: { s3m1: true },
427
+ bytesRead: br.offset + texSize,
428
+ bytesTotal: raw.byteLength,
429
+ };
430
+ }
@@ -7,12 +7,15 @@
7
7
  * 与官方实现的差异:
8
8
  * 1. 不依赖 Cesium —— 输出纯数据对象,几何上传交给渲染层。
9
9
  * 2. 修复了官方 v3 非压缩分支把整个 ArrayBuffer(含文件头)当数据流的缺陷。
10
- * 3. Draco / MeshOpt 几何压缩只做识别,不做解码(需要额外的 wasm 解码器)。
10
+ * 3. Draco 点云 / MeshOpt 几何压缩都用内联 wasm 解码器还原,与标准骨架同路输出。
11
11
  */
12
12
  import { BinaryReader } from './BinaryReader.js';
13
- import { parseMeshOptSkeleton, parseMeshOptIndexPackages } from './MeshOpt.js';
13
+ import { parseDracoGeometry } from './Draco.js';
14
+ import { parseMeshOptGeometry } from './MeshOpt.js';
15
+ import { isS3M1Buffer, parseS3M1 } from './S3M1Parser.js';
14
16
  import type {
15
17
  AttrDType,
18
+ BoundingBox,
16
19
  GeoPackageEntry,
17
20
  Geode,
18
21
  GroupNode,
@@ -127,8 +130,11 @@ function parsePageLOD(r: BinaryReader, version: number): PageLOD {
127
130
  return pageLod;
128
131
  }
129
132
 
130
- function parseGroupNode(r: BinaryReader, version: number): GroupNode {
131
- r.u32(); // streamSize
133
+ /**
134
+ * pageLOD 条目序列(无 streamSize 前缀)。
135
+ * 经典 s3mb 的 groupNode 与 S3MBLOCK 的 shell 段共用这一段。
136
+ */
137
+ export function parseGroupNodeEntries(r: BinaryReader, version: number): GroupNode {
132
138
  const count = r.u32();
133
139
  const pageLods: PageLOD[] = [];
134
140
  for (let i = 0; i < count; i++) pageLods.push(parsePageLOD(r, version));
@@ -136,6 +142,11 @@ function parseGroupNode(r: BinaryReader, version: number): GroupNode {
136
142
  return { pageLods };
137
143
  }
138
144
 
145
+ export function parseGroupNode(r: BinaryReader, version: number): GroupNode {
146
+ r.u32(); // streamSize
147
+ return parseGroupNodeEntries(r, version);
148
+ }
149
+
139
150
  /* ------------------------------------------------------------ 顶点数据包 */
140
151
 
141
152
  function parseVertex(r: BinaryReader, vp: VertexPackage): void {
@@ -330,42 +341,66 @@ function parseIndexPackages(r: BinaryReader, version: number): IndexPackage[] {
330
341
 
331
342
  /* ------------------------------------------------------------------ 骨架 */
332
343
 
344
+ /** v3 骨架条目末尾的包围盒大小:center + xExtent/yExtent/zExtent 各 3 个 float64 */
345
+ const OBB_V3_SIZE = 96;
346
+
333
347
  /**
334
348
  * v3 骨架末尾的包围盒。
335
349
  *
336
350
  * v3.01 的实际数据在索引包之后、包围盒之前多了一个 4 字节字段(实测恒为 0,
337
- * 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证,
338
- * 所以这里做一次「读一遍看像不像包围盒」的判断,不像就跳 4 字节重读。
351
+ * 推测是线框几何的长度)。规范里没有写,标准骨架是否也有无法验证。
352
+ *
353
+ * 首选路径是 parseSkeletonEntries 里按 skeletonStreamSize 直接定位到「条目末尾
354
+ * 96 字节」——那是生产端写死的权威边界,不用猜。这里只保留 streamSize 不可用时
355
+ * (S3MBLOCK 内层、末条越界)的启发式兜底:读一遍看像不像包围盒,不像就跳 4 字节重读。
356
+ *
357
+ * 注意「不像」的判定必须只针对垃圾值(非有限 / 超出地球量级):
358
+ * 点云叶子瓦片只有一个点,extent 合法地为 0,退化包围盒是正常数据,
359
+ * 不能因为 extent≈0 就认定错位 —— 否则流会整体偏 4 字节,
360
+ * 下一个骨架的名字长度会读到 ASCII 数字上(如 858992690 = "2033"),
361
+ * 直接 RangeError。
339
362
  */
340
- function readV3BoundingBox(r: BinaryReader): void {
363
+ function readV3BoundingBox(r: BinaryReader): BoundingBox {
341
364
  const start = r.offset;
342
365
  const box = r.boundingBox();
343
- if (isPlausibleBox(box)) return;
366
+ if (isFiniteBox(box)) return box;
344
367
  r.offset = start + 4;
345
- r.boundingBox();
368
+ return r.boundingBox();
346
369
  }
347
370
 
348
- function isPlausibleBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
371
+ /** 只排除垃圾值; extent 为 0 的退化包围盒(单点点云)是合法数据 */
372
+ function isFiniteBox(box: { center: Vec3; xExtent: Vec3; yExtent: Vec3; zExtent: Vec3 }): boolean {
349
373
  const all = [box.center, box.xExtent, box.yExtent, box.zExtent];
350
374
  for (const v of all) {
351
375
  if (!Number.isFinite(v.x) || !Number.isFinite(v.y) || !Number.isFinite(v.z)) return false;
352
376
  if (Math.abs(v.x) > 1e7 || Math.abs(v.y) > 1e7 || Math.abs(v.z) > 1e7) return false;
353
377
  }
354
- const len = (v: Vec3): number => Math.hypot(v.x, v.y, v.z);
355
- return len(box.xExtent) > 1e-6 && len(box.yExtent) > 1e-6 && len(box.zExtent) > 1e-6;
378
+ return true;
356
379
  }
357
380
 
358
- async function parseSkeletons(
381
+ /**
382
+ * 骨架条目序列(count 已由调用方语境决定是否读过)。
383
+ * 经典 s3mb 与 S3MBLOCK 的骨架段都长这样,区别只是外层有没有 streamSize / 附加流。
384
+ */
385
+ export async function parseSkeletonEntries(
359
386
  r: BinaryReader,
360
387
  version: number,
361
388
  trace?: ParseOptions['trace']
362
389
  ): Promise<Record<string, GeoPackageEntry>> {
363
- r.u32(); // streamSize
364
390
  const count = r.u32();
365
391
  const geoPackage: Record<string, GeoPackageEntry> = {};
366
392
 
367
393
  for (let i = 0; i < count; i++) {
368
- if (version >= 3) r.u32(); // skeletonStreamSize
394
+ // v3 起每块骨架自带 streamSize,覆盖「本字段之后 → 下一条记录开头」整段。
395
+ // 这是唯一可靠的条目边界:几何段与末尾 OBB 之间可能还有对齐填充
396
+ // (实测高斯泼溅/点云的多块瓦片里,第 3 块就比别块多 4 字节 pad),
397
+ // 靠「读够固定长度」往后推会整条流错位。
398
+ let entryEnd = -1;
399
+ if (version >= 3) {
400
+ const streamSize = r.u32();
401
+ const end = r.offset + streamSize;
402
+ if (streamSize >= OBB_V3_SIZE && end <= r.view.byteLength) entryEnd = end;
403
+ }
369
404
 
370
405
  const name = r.string();
371
406
  r.align4();
@@ -387,28 +422,55 @@ async function parseSkeletons(
387
422
  indexPackages = parseIndexPackages(r, version);
388
423
  } else if (tag === VertexTag.COMPRESSED) {
389
424
  if (version >= 3) {
390
- await parseMeshOptSkeleton(r, vp);
391
- indexPackages = await parseMeshOptIndexPackages(r);
425
+ // 索引包夹在块之间,不能拆成「先骨架后索引」两步读
426
+ indexPackages = (await parseMeshOptGeometry(r, vp)).indexPackages;
392
427
  } else {
393
428
  parseCompressedSkeleton(r, vp, version);
394
429
  indexPackages = parseIndexPackages(r, version);
395
430
  }
396
431
  } else if (tag === VertexTag.DRACO) {
397
- throw new Error(`[s3m] 骨架 "${name}" 使用 Draco 几何压缩,需接入 draco3d 解码器`);
432
+ // 点云分支:容器自带属性 uid 表,解出来的顶点即点,索引包由解析器合成
433
+ indexPackages = await parseDracoGeometry(r, vp);
398
434
  } else {
399
435
  throw new Error(`[s3m] 未知骨架标记 ${tag}(${name})`);
400
436
  }
401
437
 
402
- if (version >= 3) readV3BoundingBox(r);
438
+ let obb: BoundingBox | undefined;
439
+ if (version >= 3) {
440
+ if (entryEnd >= 0 && entryEnd - r.offset >= OBB_V3_SIZE) {
441
+ // OBB 恒定在条目末尾 96 字节:中间多出来的按填充丢掉,
442
+ // 这样即使几何段里还有个我们没认出来的字段,也不会波及后面的记录
443
+ const pad = entryEnd - r.offset - OBB_V3_SIZE;
444
+ if (pad > 0) trace?.(' skelPad', pad, r.offset);
445
+ r.offset = entryEnd - OBB_V3_SIZE;
446
+ obb = readV3BoundingBox(r);
447
+ r.offset = entryEnd;
448
+ } else {
449
+ // 没有可用的 streamSize(S3MBLOCK 内层、或末条越界),只能按启发式读
450
+ obb = readV3BoundingBox(r);
451
+ }
452
+ }
403
453
  trace?.(' skel', name, r.offset);
404
454
 
405
- geoPackage[name] = { name, vertexPackage: vp, indexPackages };
455
+ geoPackage[name] = { name, vertexPackage: vp, indexPackages, obb };
406
456
  }
407
457
 
458
+ return geoPackage;
459
+ }
460
+
461
+ export async function parseSkeletons(
462
+ r: BinaryReader,
463
+ version: number,
464
+ trace?: ParseOptions['trace']
465
+ ): Promise<Record<string, GeoPackageEntry>> {
466
+ r.u32(); // streamSize
467
+ const geoPackage = await parseSkeletonEntries(r, version, trace);
468
+
408
469
  // v1/v2 在骨架数组之后还有一段附加流。官方写法是
409
470
  // bytesOffset += secColorSize; bytesOffset += 4;
410
471
  // 其中读取 secColorSize 时不推进指针,等价于「起点 + 长度 + 4」。
411
472
  // 此时 r.offset 已在长度字段之后,所以只补长度即可。
473
+ // (S3MBLOCK 没有这一段——secondColor 在段目录里是独立条目)
412
474
  if (version < 3) {
413
475
  const extra = r.u32();
414
476
  r.skip(extra);
@@ -458,7 +520,7 @@ function parseMaterials(r: BinaryReader): unknown {
458
520
  * 单体化信息。非实例化数据里每个单体对应一段连续的顶点区间,
459
521
  * 这里把它展开成逐顶点的 batchId 属性,渲染层可直接做 GPU picking。
460
522
  */
461
- function parsePickInfo(
523
+ export function parsePickInfo(
462
524
  r: BinaryReader,
463
525
  options: number,
464
526
  geoPackage: Record<string, GeoPackageEntry>,
@@ -471,6 +533,18 @@ function parsePickInfo(
471
533
  if ((flags & 1) !== 1) return null;
472
534
 
473
535
  r.u32(); // streamSize
536
+ return parsePickInfoEntries(r, geoPackage, version);
537
+ }
538
+
539
+ /**
540
+ * 单体化条目序列(无 flags / streamSize 前缀)。
541
+ * 经典 s3mb 在流内自带 streamSize;S3MBLOCK 的段目录里没有,由调用方按目录切好传入。
542
+ */
543
+ export function parsePickInfoEntries(
544
+ r: BinaryReader,
545
+ geoPackage: Record<string, GeoPackageEntry>,
546
+ version: number
547
+ ): Record<string, unknown> {
474
548
  const count = r.u32();
475
549
  const result: Record<string, unknown> = {};
476
550
 
@@ -530,7 +604,24 @@ function parsePickInfo(
530
604
 
531
605
  /* ------------------------------------------------------------------ 入口 */
532
606
 
607
+ /**
608
+ * 容器层面的永久错误(格式不认识、重试一万次也不会变好)。
609
+ * 瓦片调度看到 permanent 就不再退避重试,免得反复重下一整块瓦片。
610
+ */
611
+ export class S3MFormatError extends Error {
612
+ readonly permanent = true;
613
+ constructor(message: string) {
614
+ super(message);
615
+ this.name = 'S3MFormatError';
616
+ }
617
+ }
618
+
533
619
  export async function parseS3MB(arrayBuffer: ArrayBuffer, options: ParseOptions = {}): Promise<ParsedTile> {
620
+ // S3M 1.x(SuperMapCache,.s3m)不是「float32 版本号开头」的容器:头 4 字节是魔数
621
+ // s3m\0,紧接 zlib。不当场分流的话,这 4 字节会被读成 1e-38 的「版本号」,再把 zlib
622
+ // 头当压缩长度,最后报出 RangeError: Invalid typed array length 这种看不出根因的错。
623
+ // 容器与结构标定见 S3M1Parser.ts。
624
+ if (isS3M1Buffer(arrayBuffer)) return parseS3M1(arrayBuffer, options);
534
625
  const inflate = options.inflate ?? defaultInflate;
535
626
  const trace = options.trace;
536
627
  const r = new BinaryReader(arrayBuffer);