s3m-three 0.4.27 → 0.4.28
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.
- package/dist/browser/assets/s3mWorker-B2u-xt54.js +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +40 -40
- package/dist/browser/s3m-three.iife.js +40 -40
- package/dist/core/S3M1Parser.d.ts +19 -0
- package/dist/core/S3M1Parser.d.ts.map +1 -1
- package/dist/core/S3M1Parser.js +179 -8
- package/dist/core/S3M1Parser.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +8 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +36 -6
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +15 -0
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/dist/worker/s3mWorker.js +654 -524
- package/dist/worker/s3mWorker.js.map +1 -1
- package/package.json +1 -1
- package/src/core/S3M1Parser.ts +179 -9
- package/src/render/S3MViewer.ts +34 -5
- package/src/tileset/S3MTileset.ts +14 -0
- package/dist/browser/assets/s3mWorker-BfSBNvF5.js +0 -1
package/package.json
CHANGED
package/src/core/S3M1Parser.ts
CHANGED
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
*/
|
|
63
63
|
import { BinaryReader } from './BinaryReader.js';
|
|
64
64
|
import type {
|
|
65
|
+
Geode,
|
|
65
66
|
GeoPackageEntry,
|
|
66
67
|
GroupNode,
|
|
67
68
|
IndexPackage,
|
|
@@ -124,7 +125,13 @@ interface XmlMaterial {
|
|
|
124
125
|
addressMode?: { u: number; v: number };
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
interface
|
|
128
|
+
export interface XmlGeode {
|
|
129
|
+
geoName: string;
|
|
130
|
+
matrix: number[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface XmlLod {
|
|
134
|
+
geodes?: XmlGeode[];
|
|
128
135
|
geoNames: string[];
|
|
129
136
|
matrix: number[];
|
|
130
137
|
childTile: string;
|
|
@@ -158,7 +165,7 @@ function rangeModeOf(text: string | null): number {
|
|
|
158
165
|
return 1;
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
function parseXmlLods(xml: string): XmlLod[] {
|
|
168
|
+
export function parseXmlLods(xml: string): XmlLod[] {
|
|
162
169
|
const blocks = [...xml.matchAll(/<(?:[A-Za-z0-9_-]+:)?PagedLOD[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?PagedLOD>/gi)];
|
|
163
170
|
const out: XmlLod[] = [];
|
|
164
171
|
for (const b of blocks) {
|
|
@@ -180,6 +187,39 @@ function parseXmlLods(xml: string): XmlLod[] {
|
|
|
180
187
|
radius: num(tag(sphere, 'radius')),
|
|
181
188
|
});
|
|
182
189
|
}
|
|
190
|
+
// 兜底:S3M 1.x 叶子瓦片没有 <PagedLOD>,<PageLods> 内部直接是 <Geode>
|
|
191
|
+
if (out.length === 0) {
|
|
192
|
+
const pageLodsBlock = /<(?:[A-Za-z0-9_-]+:)?PageLods[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?PageLods>/i.exec(xml);
|
|
193
|
+
const scope = pageLodsBlock ? pageLodsBlock[1] : xml;
|
|
194
|
+
const geodeBlocks = [...scope.matchAll(/<(?:[A-Za-z0-9_-]+:)?Geode[^>]*>([\s\S]*?)<\/(?:[A-Za-z0-9_-]+:)?Geode>/gi)];
|
|
195
|
+
if (geodeBlocks.length > 0) {
|
|
196
|
+
const geodes: XmlGeode[] = [];
|
|
197
|
+
for (const gb of geodeBlocks) {
|
|
198
|
+
const gs = gb[1];
|
|
199
|
+
const gName = tag(gs, "GeoName") ?? "";
|
|
200
|
+
const matStr = tag(gs, "GeoDeModMatrix");
|
|
201
|
+
const mat = (matStr ?? "1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1")
|
|
202
|
+
.split(",")
|
|
203
|
+
.map((x) => parseFloat(x.trim()))
|
|
204
|
+
.filter((x) => Number.isFinite(x));
|
|
205
|
+
geodes.push({
|
|
206
|
+
geoName: gName,
|
|
207
|
+
matrix: mat.length === 16 ? mat : [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
out.push({
|
|
211
|
+
geoNames: geodes.map((g) => g.geoName).filter(Boolean),
|
|
212
|
+
matrix: geodes[0]?.matrix ?? [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
|
213
|
+
geodes,
|
|
214
|
+
childTile: "",
|
|
215
|
+
rangeMode: rangeModeOf(null), // 1 = 屏幕像素
|
|
216
|
+
rangeValue: 0,
|
|
217
|
+
center: { x: 0, y: 0, z: 0 },
|
|
218
|
+
radius: 0,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
183
223
|
return out;
|
|
184
224
|
}
|
|
185
225
|
|
|
@@ -645,6 +685,108 @@ function readTextures(br: BinaryReader, texSize: number): Record<string, S3MText
|
|
|
645
685
|
let warnedV1Attrs = false;
|
|
646
686
|
let warnedQuantBox = false;
|
|
647
687
|
|
|
688
|
+
function computeLodBoundingSphere(
|
|
689
|
+
geodes: { matrix: number[]; skeletonNames: string[] }[],
|
|
690
|
+
meshes: MeshRecord[]
|
|
691
|
+
): { center: { x: number; y: number; z: number }; radius: number } | null {
|
|
692
|
+
const meshMap = new Map(meshes.map((m) => [`SuperMapCache#${m.geoName}`, m]));
|
|
693
|
+
let minX = Infinity, maxX = -Infinity;
|
|
694
|
+
let minY = Infinity, maxY = -Infinity;
|
|
695
|
+
let minZ = Infinity, maxZ = -Infinity;
|
|
696
|
+
let totalPoints = 0;
|
|
697
|
+
|
|
698
|
+
for (const geode of geodes) {
|
|
699
|
+
const mat = geode.matrix;
|
|
700
|
+
const isIdentity =
|
|
701
|
+
!mat ||
|
|
702
|
+
(mat[0] === 1 && mat[1] === 0 && mat[2] === 0 && mat[3] === 0 &&
|
|
703
|
+
mat[4] === 0 && mat[5] === 1 && mat[6] === 0 && mat[7] === 0 &&
|
|
704
|
+
mat[8] === 0 && mat[9] === 0 && mat[10] === 1 && mat[11] === 0 &&
|
|
705
|
+
mat[12] === 0 && mat[13] === 0 && mat[14] === 0 && mat[15] === 1);
|
|
706
|
+
|
|
707
|
+
for (const name of geode.skeletonNames) {
|
|
708
|
+
const mesh = meshMap.get(name);
|
|
709
|
+
if (!mesh || !mesh.position) continue;
|
|
710
|
+
const pos = mesh.position;
|
|
711
|
+
totalPoints += pos.length / 3;
|
|
712
|
+
|
|
713
|
+
if (isIdentity) {
|
|
714
|
+
for (let i = 0; i < pos.length; i += 3) {
|
|
715
|
+
const x = pos[i]!, y = pos[i + 1]!, z = pos[i + 2]!;
|
|
716
|
+
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
717
|
+
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
718
|
+
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
const m0 = mat[0]!, m1 = mat[1]!, m2 = mat[2]!;
|
|
722
|
+
const m4 = mat[4]!, m5 = mat[5]!, m6 = mat[6]!;
|
|
723
|
+
const m8 = mat[8]!, m9 = mat[9]!, m10 = mat[10]!;
|
|
724
|
+
const m12 = mat[12]!, m13 = mat[13]!, m14 = mat[14]!;
|
|
725
|
+
for (let i = 0; i < pos.length; i += 3) {
|
|
726
|
+
const x = pos[i]!, y = pos[i + 1]!, z = pos[i + 2]!;
|
|
727
|
+
const tx = m0 * x + m4 * y + m8 * z + m12;
|
|
728
|
+
const ty = m1 * x + m5 * y + m9 * z + m13;
|
|
729
|
+
const tz = m2 * x + m6 * y + m10 * z + m14;
|
|
730
|
+
if (tx < minX) minX = tx; if (tx > maxX) maxX = tx;
|
|
731
|
+
if (ty < minY) minY = ty; if (ty > maxY) maxY = ty;
|
|
732
|
+
if (tz < minZ) minZ = tz; if (tz > maxZ) maxZ = tz;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (totalPoints === 0 || !Number.isFinite(minX)) return null;
|
|
739
|
+
|
|
740
|
+
const cx = (minX + maxX) * 0.5;
|
|
741
|
+
const cy = (minY + maxY) * 0.5;
|
|
742
|
+
const cz = (minZ + maxZ) * 0.5;
|
|
743
|
+
|
|
744
|
+
let maxR2 = 0;
|
|
745
|
+
for (const geode of geodes) {
|
|
746
|
+
const mat = geode.matrix;
|
|
747
|
+
const isIdentity =
|
|
748
|
+
!mat ||
|
|
749
|
+
(mat[0] === 1 && mat[1] === 0 && mat[2] === 0 && mat[3] === 0 &&
|
|
750
|
+
mat[4] === 0 && mat[5] === 1 && mat[6] === 0 && mat[7] === 0 &&
|
|
751
|
+
mat[8] === 0 && mat[9] === 0 && mat[10] === 1 && mat[11] === 0 &&
|
|
752
|
+
mat[12] === 0 && mat[13] === 0 && mat[14] === 0 && mat[15] === 1);
|
|
753
|
+
|
|
754
|
+
for (const name of geode.skeletonNames) {
|
|
755
|
+
const mesh = meshMap.get(name);
|
|
756
|
+
if (!mesh || !mesh.position) continue;
|
|
757
|
+
const pos = mesh.position;
|
|
758
|
+
|
|
759
|
+
if (isIdentity) {
|
|
760
|
+
for (let i = 0; i < pos.length; i += 3) {
|
|
761
|
+
const dx = pos[i]! - cx, dy = pos[i + 1]! - cy, dz = pos[i + 2]! - cz;
|
|
762
|
+
const r2 = dx * dx + dy * dy + dz * dz;
|
|
763
|
+
if (r2 > maxR2) maxR2 = r2;
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
const m0 = mat[0]!, m1 = mat[1]!, m2 = mat[2]!;
|
|
767
|
+
const m4 = mat[4]!, m5 = mat[5]!, m6 = mat[6]!;
|
|
768
|
+
const m8 = mat[8]!, m9 = mat[9]!, m10 = mat[10]!;
|
|
769
|
+
const m12 = mat[12]!, m13 = mat[13]!, m14 = mat[14]!;
|
|
770
|
+
for (let i = 0; i < pos.length; i += 3) {
|
|
771
|
+
const x = pos[i]!, y = pos[i + 1]!, z = pos[i + 2]!;
|
|
772
|
+
const tx = m0 * x + m4 * y + m8 * z + m12;
|
|
773
|
+
const ty = m1 * x + m5 * y + m9 * z + m13;
|
|
774
|
+
const tz = m2 * x + m6 * y + m10 * z + m14;
|
|
775
|
+
const dx = tx - cx, dy = ty - cy, dz = tz - cz;
|
|
776
|
+
const r2 = dx * dx + dy * dy + dz * dz;
|
|
777
|
+
if (r2 > maxR2) maxR2 = r2;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const radius = Math.sqrt(maxR2);
|
|
784
|
+
return {
|
|
785
|
+
center: { x: cx, y: cy, z: cz },
|
|
786
|
+
radius: radius > 0 ? radius : 0.001,
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
|
|
648
790
|
export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Options = {}): Promise<ParsedTile> {
|
|
649
791
|
const inflate = options.inflate ?? defaultInflate;
|
|
650
792
|
const r = new BinaryReader(arrayBuffer);
|
|
@@ -770,17 +912,45 @@ export async function parseS3M1(arrayBuffer: ArrayBuffer, options: ParseS3M1Opti
|
|
|
770
912
|
const xmlLods = parseXmlLods(xml);
|
|
771
913
|
const pageLods: PageLOD[] = [];
|
|
772
914
|
xmlLods.forEach((lod, i) => {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
915
|
+
let geodes: Geode[];
|
|
916
|
+
if (lod.geodes && lod.geodes.length > 0) {
|
|
917
|
+
geodes = lod.geodes
|
|
918
|
+
.map((g, gi) => {
|
|
919
|
+
const key = keyOf.get(g.geoName);
|
|
920
|
+
const fallback = meshes[gi] ? [`SuperMapCache#${meshes[gi].geoName}`] : [];
|
|
921
|
+
const skeletonNames = key ? [key] : fallback;
|
|
922
|
+
return {
|
|
923
|
+
matrix: g.matrix,
|
|
924
|
+
skeletonNames,
|
|
925
|
+
};
|
|
926
|
+
})
|
|
927
|
+
.filter((g) => g.skeletonNames.length > 0);
|
|
928
|
+
} else {
|
|
929
|
+
const named = lod.geoNames.map((n) => keyOf.get(n)).filter((k): k is string => !!k);
|
|
930
|
+
// 名字对不上时退回位置匹配,并让上层有机会发现(宁可少一个 LOD 也不要挂错球)
|
|
931
|
+
const fallback = meshes[i] ? [`SuperMapCache#${meshes[i].geoName}`] : [];
|
|
932
|
+
const skeletonNames = named.length ? [...new Set(named)] : fallback;
|
|
933
|
+
geodes = skeletonNames.length ? [{ matrix: lod.matrix, skeletonNames }] : [];
|
|
934
|
+
}
|
|
935
|
+
if (!geodes.length) return;
|
|
936
|
+
|
|
937
|
+
let center = { ...lod.center };
|
|
938
|
+
let radius = lod.radius;
|
|
939
|
+
// 缺包围球(如叶子瓦片)时,从网格顶点实时计算真实几何包围球
|
|
940
|
+
if (radius <= 0) {
|
|
941
|
+
const computed = computeLodBoundingSphere(geodes, meshes);
|
|
942
|
+
if (computed) {
|
|
943
|
+
center = computed.center;
|
|
944
|
+
radius = computed.radius;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
778
948
|
pageLods.push({
|
|
779
949
|
rangeValue: lod.rangeValue,
|
|
780
950
|
rangeMode: lod.rangeMode,
|
|
781
|
-
boundingSphere: { center
|
|
951
|
+
boundingSphere: { center, radius },
|
|
782
952
|
childTile: lod.childTile,
|
|
783
|
-
geodes
|
|
953
|
+
geodes,
|
|
784
954
|
});
|
|
785
955
|
});
|
|
786
956
|
const groupNode: GroupNode = { pageLods };
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -1737,16 +1737,29 @@ export class S3MViewer {
|
|
|
1737
1737
|
|
|
1738
1738
|
private _registerTile(node: TileNode, entry: S3MDatasetEntry): void {
|
|
1739
1739
|
// 必须在 add / updateMatrixWorld 之前设置:后者会拿当前 matrix 算 matrixWorld 并缓存
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1740
|
+
//
|
|
1741
|
+
// ★ 触发条件**不能**是「有没有包围球」。S3M 1.x 的叶子瓦片 XML 是
|
|
1742
|
+
// `<PageLods><Geode>`(外面没有 `<PagedLOD>` 包装),`parseXmlLods` 解析出来
|
|
1743
|
+
// pageLods 为空 → sphere 为 null。而 `entry.root` 上挂着 align 算出来的整体偏移
|
|
1744
|
+
// (索菲亚教堂是 heightRange.min = 119.84),那个偏移是靠 placement 里的
|
|
1745
|
+
// `- datasetOffset` 抵消的。少摆一块,这块就带着偏移显示 —— 实测 L23 整层
|
|
1746
|
+
// 因此沉到地下 120 m,且 `localZ - worldZ` 恒等于偏移量。
|
|
1747
|
+
// 缺球时用几何包围盒中心兜底;连几何都没有就退回旧口径。
|
|
1748
|
+
const center = node.sphere?.center ?? this._localCenterOf(node.group);
|
|
1749
|
+
if (this._perTilePlacement && center) {
|
|
1750
|
+
this._applyTilePlacement(node.group, center, entry);
|
|
1751
|
+
}
|
|
1752
|
+
// 上面没摆成(关掉本功能 / 缺锚点 / 几何为空)时 matrixAutoUpdate 仍是 true,走旧口径
|
|
1753
|
+
if (node.group.matrixAutoUpdate) {
|
|
1743
1754
|
const sceneAnchor = this.sceneAnchor;
|
|
1744
1755
|
if (sceneAnchor) {
|
|
1745
1756
|
const sEast = sceneAnchor.meterInMercatorEast ?? sceneAnchor.meterInMercator;
|
|
1746
1757
|
const sNorth = sceneAnchor.meterInMercatorNorth ?? sceneAnchor.meterInMercator;
|
|
1747
1758
|
const sUp = sceneAnchor.meterInMercatorUp ?? sEast;
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1759
|
+
const c = node.sphere
|
|
1760
|
+
? ([node.sphere.center.x, node.sphere.center.y, node.sphere.center.z] as const)
|
|
1761
|
+
: ([0, 0, 0] as const);
|
|
1762
|
+
this._updateTileUniforms(node.group, c, [1.0 / sEast, 1.0 / sNorth, 1.0 / sUp]);
|
|
1750
1763
|
}
|
|
1751
1764
|
}
|
|
1752
1765
|
entry.root.add(node.group);
|
|
@@ -1820,6 +1833,22 @@ export class S3MViewer {
|
|
|
1820
1833
|
}
|
|
1821
1834
|
}
|
|
1822
1835
|
|
|
1836
|
+
/**
|
|
1837
|
+
* 瓦片几何在**数据集局部 ENU** 里的包围盒中心,给没有包围球的瓦片兜底。
|
|
1838
|
+
*
|
|
1839
|
+
* 调用时机固定在 `entry.root.add()` 之前:此时 group 还没挂到 root 上、matrix 还是
|
|
1840
|
+
* 单位阵,`setFromObject` 得到的就是顶点自己的坐标系,正好是摆放要的枢轴。
|
|
1841
|
+
* 挂上 root 之后再算会带上 root 的 offset,枢轴就偏了。
|
|
1842
|
+
*/
|
|
1843
|
+
private _localCenterOf(group: THREE.Object3D): { x: number; y: number; z: number } | null {
|
|
1844
|
+
if (!group.children.length) return null;
|
|
1845
|
+
const box = new THREE.Box3().setFromObject(group);
|
|
1846
|
+
if (box.isEmpty()) return null;
|
|
1847
|
+
const c = box.getCenter(new THREE.Vector3());
|
|
1848
|
+
if (!Number.isFinite(c.x) || !Number.isFinite(c.y) || !Number.isFinite(c.z)) return null;
|
|
1849
|
+
return { x: c.x, y: c.y, z: c.z };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1823
1852
|
/**
|
|
1824
1853
|
* 把一块瓦片摆到「用它自己中心纬度算出的尺度」上(`perTilePlacement` 开启时才走)。
|
|
1825
1854
|
*
|
|
@@ -951,6 +951,15 @@ export class S3MTileset {
|
|
|
951
951
|
victim = node;
|
|
952
952
|
continue;
|
|
953
953
|
}
|
|
954
|
+
// 可见性优先于时间戳:没有包围球的瓦片算不出距离,`lastSeen` 天然比别的瓦片旧,
|
|
955
|
+
// 只看时间戳会把「正在显示」的那块先踢掉(开洞 → 回补 → 再踢 = 闪)。
|
|
956
|
+
// 只有不可见的候选分不出来时,才轮到 lastSeen / distance 决定。
|
|
957
|
+
const nodeVisible = node.group.visible ? 1 : 0;
|
|
958
|
+
const victimVisible = victim.group.visible ? 1 : 0;
|
|
959
|
+
if (nodeVisible !== victimVisible) {
|
|
960
|
+
if (nodeVisible < victimVisible) victim = node;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
954
963
|
const a = node.lastSeen ?? -1;
|
|
955
964
|
const b = victim.lastSeen ?? -1;
|
|
956
965
|
if (a < b || (a === b && (node.distance ?? 0) > (victim.distance ?? 0))) victim = node;
|
|
@@ -1513,6 +1522,11 @@ export class S3MTileset {
|
|
|
1513
1522
|
|
|
1514
1523
|
for (const node of this.nodes.values()) {
|
|
1515
1524
|
if (!node.sphere) {
|
|
1525
|
+
// 没有包围球(S3M 1.x 叶子瓦片:XML 是 `<PageLods><Geode>`,没有 PagedLOD)时
|
|
1526
|
+
// 算不出距离,但**仍要记「最近用过」**:`lastSeen` 恒为 undefined 会让它在
|
|
1527
|
+
// `_pickVictim` 里永远排最前(`?? -1`),硬上限一触发 `allowUncovered`,
|
|
1528
|
+
// 第一块被踢掉的就是正在显示的那块 —— 画面立刻一个洞,下一帧又加载回来。
|
|
1529
|
+
node.lastSeen = this._frame;
|
|
1516
1530
|
node.needRefine = false;
|
|
1517
1531
|
continue;
|
|
1518
1532
|
}
|