s3m-three 0.4.17 → 0.4.18

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 (38) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +24 -24
  2. package/dist/browser/s3m-three.iife.js +24 -24
  3. package/dist/core/AttributeQuery.d.ts +13 -0
  4. package/dist/core/AttributeQuery.d.ts.map +1 -1
  5. package/dist/core/AttributeQuery.js +20 -3
  6. package/dist/core/AttributeQuery.js.map +1 -1
  7. package/dist/core/AuthFetch.d.ts +52 -0
  8. package/dist/core/AuthFetch.d.ts.map +1 -0
  9. package/dist/core/AuthFetch.js +44 -0
  10. package/dist/core/AuthFetch.js.map +1 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/render/DatasetManager.d.ts +7 -1
  16. package/dist/render/DatasetManager.d.ts.map +1 -1
  17. package/dist/render/DatasetManager.js +2 -1
  18. package/dist/render/DatasetManager.js.map +1 -1
  19. package/dist/render/HostRenderer.d.ts +105 -0
  20. package/dist/render/HostRenderer.d.ts.map +1 -0
  21. package/dist/render/HostRenderer.js +170 -0
  22. package/dist/render/HostRenderer.js.map +1 -0
  23. package/dist/render/S3MViewer.d.ts +10 -0
  24. package/dist/render/S3MViewer.d.ts.map +1 -1
  25. package/dist/render/S3MViewer.js +13 -4
  26. package/dist/render/S3MViewer.js.map +1 -1
  27. package/dist/tileset/S3MTileset.d.ts +8 -0
  28. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  29. package/dist/tileset/S3MTileset.js +8 -4
  30. package/dist/tileset/S3MTileset.js.map +1 -1
  31. package/package.json +1 -1
  32. package/src/core/AttributeQuery.ts +26 -3
  33. package/src/core/AuthFetch.ts +82 -0
  34. package/src/index.ts +4 -0
  35. package/src/render/DatasetManager.ts +9 -1
  36. package/src/render/HostRenderer.ts +222 -0
  37. package/src/render/S3MViewer.ts +22 -4
  38. package/src/tileset/S3MTileset.ts +13 -4
@@ -0,0 +1,222 @@
1
+ /**
2
+ * 宿主无关渲染底座(HostRenderer)
3
+ *
4
+ * 把「宿主协议 ↔ s3m-three 渲染内核」的**宿主无关**部分抽出来,作为所有地图适配层
5
+ * (MapLibre / Mapbox / 未来的 ArcGIS / Cesium)的共同基类。它只做三件事:
6
+ *
7
+ * 1. 持有并构造 `S3MViewer`(渲染内核,宿主无关);
8
+ * 2. 提供每帧驱动(把宿主给的 world→clip 矩阵喂给内核,并按需请求下一帧)、
9
+ * 生命周期(mount / restore / unmount)、投影模式状态与近平面缩放状态;
10
+ * 3. 把「宿主能力」做成两个抽象钩子 `requestRepaint()` / `getBearing()`,
11
+ * 由具体适配器实现(maplibre → `triggerRepaint()`,arcgis → `requestRender()`)。
12
+ *
13
+ * 凡是依赖具体地图 API 的东西(CustomLayerInterface 形态、从宿主 render 参数里抽矩阵、
14
+ * 事件绑定、上下文恢复重加图层、经纬度→墨卡托的 WorldCoordinateHelper 提取)都**不在这里**——
15
+ * 那是各适配层自己的事。这样「绕开墨卡托」也成为可能:非墨卡托宿主(ECEF / 局部米制)只要
16
+ * 实现 `requestRepaint` 并提供返回自己坐标系的 GeoAdapter,内核就完全不碰墨卡托。
17
+ *
18
+ * 设计约束(对应 AGENTS.md 不变式①):本类不引入任何默认行为变更;maplibre 适配器继承它后,
19
+ * 默认路径与改动前逐字一致。
20
+ */
21
+ import { S3MViewer } from './S3MViewer.js';
22
+ import type { S3MViewerOptions, PickResult } from './S3MViewer.js';
23
+ import type { ProjectionMode } from './OrthoProjection.js';
24
+ import type { TransitionOptions } from '../anim/Animator.js';
25
+ import type { Stats } from '../core/types.js';
26
+ import type { S3MDatasetEntry } from './DatasetManager.js';
27
+
28
+ /** HostRenderer 构造选项:渲染内核选项 + 三个适配器层专属开关 */
29
+ export interface HostRendererOptions extends S3MViewerOptions {
30
+ /** 图层 / 适配器 id,默认 's3m' */
31
+ id?: string;
32
+ /** 投影模式,默认 'perspective'(跟随宿主) */
33
+ projection?: ProjectionMode;
34
+ /** 近裁剪面缩放系数(0 < scale < 1,默认 1 = 不改) */
35
+ nearPlaneScale?: number;
36
+ }
37
+
38
+ /** 每帧喂给内核的矩阵输入(宿主无关;适配器负责从宿主 render 参数里抽出来) */
39
+ export interface HostFrame {
40
+ /** world→clip 矩阵(宿主坐标系下) */
41
+ mvp: ArrayLike<number>;
42
+ /** 相机在场景 ENU 中的位置(米,相对场景锚点) */
43
+ cameraEnu: [number, number, number];
44
+ /** 垂直视场角(弧度),用于 LOD 屏幕误差 */
45
+ fovRad: number;
46
+ /** 绘图缓冲高度(像素),LOD 判定用 */
47
+ canvasHeight: number;
48
+ }
49
+
50
+ /**
51
+ * 宿主无关渲染底座。具体地图适配器(如 S3MLayer)继承它,只补宿主专属胶水。
52
+ */
53
+ export abstract class HostRenderer {
54
+ readonly id: string;
55
+ readonly options: HostRendererOptions;
56
+ readonly pickable: boolean;
57
+
58
+ /** 公开回调,可被使用方运行时重新赋值 */
59
+ onStats: (stats: Stats) => void;
60
+ onReady: (entry: S3MDatasetEntry) => void;
61
+ onPick: (result: PickResult | null) => void;
62
+ onError: (error: unknown) => void;
63
+
64
+ /** 渲染内核(宿主无关) */
65
+ readonly viewer: S3MViewer;
66
+
67
+ /** 当前投影模式 */
68
+ protected _projection: ProjectionMode = 'perspective';
69
+ /** 正交插值比例(0 = 纯透视,1 = 纯正交);过渡期间由 animator 平滑推进 */
70
+ protected _projectionT = 0;
71
+ /** 正交不可用只告警一次 */
72
+ protected _orthoWarned = false;
73
+ /** 近裁剪面缩放系数,1 = 不改(默认) */
74
+ protected _nearPlaneScale = 1;
75
+
76
+ constructor(options: HostRendererOptions = {}) {
77
+ this.id = options.id ?? 's3m';
78
+ this.options = options;
79
+ this.pickable = options.pickable !== false;
80
+ this._projection = options.projection === 'orthographic' ? 'orthographic' : 'perspective';
81
+ this._nearPlaneScale = HostRenderer.normalizeNearPlaneScale(options.nearPlaneScale);
82
+ this._projectionT = this._projection === 'orthographic' ? 1 : 0;
83
+
84
+ this.onStats = options.onStats ?? (() => {});
85
+ this.onReady = options.onReady ?? (() => {});
86
+ this.onPick = options.onPick ?? (() => {});
87
+ this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
88
+
89
+ // 把宿主能力做成钩子,内核不碰任何地图 API
90
+ const { id: _id, projection: _projection, nearPlaneScale: _nearPlaneScale, ...viewerOptions } = options;
91
+ this.viewer = new S3MViewer({
92
+ ...viewerOptions,
93
+ onRequestRepaint: () => this.requestRepaint(),
94
+ getBearing: () => this.getBearing(),
95
+ onStats: (s) => this.onStats(s),
96
+ onReady: (e) => this.onReady(e),
97
+ onPick: (r) => this.onPick(r),
98
+ onError: (e) => this.onError(e),
99
+ });
100
+ }
101
+
102
+ /**
103
+ * 宿主能力的钩子:请求下一帧重绘。
104
+ * maplibre → `map.triggerRepaint()`;arcgis → `externalRenderers.requestRender(view)`。
105
+ */
106
+ protected abstract requestRepaint(): void;
107
+
108
+ /** 宿主方位角(度);宿主无关默认 0(灯光不跟随视角) */
109
+ protected getBearing(): number {
110
+ return 0;
111
+ }
112
+
113
+ /* ----------------------------------------------------------------- 生命周期 */
114
+
115
+ /** 挂载渲染内核到宿主的 canvas + 共享 GL 上下文(上下文恢复时原地换新 renderer) */
116
+ protected mountViewer(canvas: HTMLCanvasElement, gl: WebGLRenderingContext | WebGL2RenderingContext): void {
117
+ if (this.viewer.renderer) {
118
+ // 上下文恢复路径:onAdd 被再次触发(map.addLayer),原地换新 renderer,
119
+ // 保留已加载的场景与瓦片,不重建、不重拉。
120
+ this.viewer.restoreContext(gl);
121
+ } else {
122
+ this.viewer.mount(canvas, gl);
123
+ }
124
+ }
125
+
126
+ /** 卸载渲染内核(保留已加载的瓦片,下次 mount 可复用) */
127
+ protected unmountViewer(): void {
128
+ this.viewer.unmount();
129
+ }
130
+
131
+ /* ----------------------------------------------------------------- 每帧驱动 */
132
+
133
+ /**
134
+ * 把宿主抽好的矩阵喂给内核,并按需请求下一帧。
135
+ * 返回内核是否需要继续驱动(动态着色 / 瓦片加载中 / LOD 变化)。
136
+ */
137
+ protected driveFrame(frame: HostFrame): boolean {
138
+ const needsNext = this.viewer.renderFrame(frame);
139
+ if (needsNext) this.requestRepaint();
140
+ return needsNext;
141
+ }
142
+
143
+ /* ----------------------------------------------------------------- 投影模式 */
144
+
145
+ /**
146
+ * 切换投影模式,可带平滑过渡。
147
+ * 正交只影响本层:底图瓦片、地形、DOM Marker / 弹窗仍走宿主的透视投影。
148
+ * 拾取、选中描边、按单体着色/隐藏/抬高不受影响。
149
+ */
150
+ setProjection(mode: ProjectionMode, transition?: TransitionOptions): void {
151
+ const next = mode === 'orthographic' ? 'orthographic' : 'perspective';
152
+ const targetT = next === 'orthographic' ? 1 : 0;
153
+ const duration = transition?.duration ?? 0;
154
+ const wantAnim = duration > 0 || !!transition?.spring;
155
+
156
+ this._projection = next;
157
+ this._orthoWarned = false;
158
+
159
+ if (!wantAnim) {
160
+ if (this.viewer.animator.has('projection.t')) this.viewer.animator.cancel('projection.t');
161
+ this._projectionT = targetT;
162
+ this.requestRepaint();
163
+ return;
164
+ }
165
+
166
+ this._ensureProjectionChannel();
167
+ this.viewer.animator.to('projection.t', targetT, {
168
+ ...transition,
169
+ duration,
170
+ easing: transition?.easing ?? 'easeInOutCubic',
171
+ onComplete: () => {
172
+ this._projectionT = targetT;
173
+ this.requestRepaint();
174
+ },
175
+ });
176
+ this.requestRepaint();
177
+ }
178
+
179
+ /** 当前投影模式 */
180
+ get projection(): ProjectionMode {
181
+ return this._projection;
182
+ }
183
+
184
+ /** 当前正交插值比例(0 = 纯透视,1 = 纯正交) */
185
+ get projectionT(): number {
186
+ return this._projectionT;
187
+ }
188
+
189
+ /** 正交插值参数通道:由 animator 平滑推进,写回时请求重绘 */
190
+ protected _ensureProjectionChannel(): void {
191
+ if (this.viewer.animator.has('projection.t')) return;
192
+ this.viewer.animator.register('projection.t', {
193
+ dim: 1,
194
+ read: (out) => {
195
+ out[0] = this._projectionT;
196
+ },
197
+ write: (v) => {
198
+ this._projectionT = v[0];
199
+ this.requestRepaint();
200
+ },
201
+ });
202
+ }
203
+
204
+ /* ----------------------------------------------------------------- 近平面缩放 */
205
+
206
+ /** 动态调整近裁剪面缩放(0 < scale < 1)。传 1 / 0 / 非法值 = 恢复宿主原始 near */
207
+ setNearPlaneScale(scale: number): void {
208
+ this._nearPlaneScale = HostRenderer.normalizeNearPlaneScale(scale);
209
+ this.requestRepaint();
210
+ }
211
+
212
+ /** 当前近裁剪面缩放系数(1 = 不改,用宿主的默认值) */
213
+ get nearPlaneScale(): number {
214
+ return this._nearPlaneScale;
215
+ }
216
+
217
+ /** 近裁剪缩放系数的合法化:非数字 / <=0 / >=1 一律视为「不改」 */
218
+ static normalizeNearPlaneScale(v: number | undefined): number {
219
+ if (typeof v !== 'number' || !Number.isFinite(v)) return 1;
220
+ return v > 0 && v < 1 ? v : 1;
221
+ }
222
+ }
@@ -23,6 +23,7 @@
23
23
  import * as THREE from 'three';
24
24
  import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
25
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
+ import { authFetch, type AuthenticateFn } from '../core/AuthFetch.js';
26
27
  import { setMaxAnisotropy, setCompressedTexturesSupported, setTextureOffThreadDecode, setMaxTextureSize as setTextureSizeCap } from './TextureFactory.js';
27
28
  import { getMaterialQuality, setMaterialQuality } from './StyleMaterial.js';
28
29
  import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
@@ -290,6 +291,13 @@ export interface S3MViewerOptions {
290
291
  onRequestRepaint?: () => void;
291
292
  /** 地图朝向(弧度),用于让灯光跟着视角走。不传则灯光方向固定 */
292
293
  getBearing?: () => number;
294
+ /**
295
+ * 鉴权适配函数(全局默认,数据集可用自己的 `authenticate` 覆盖)。
296
+ * 内核主线程发出的全部请求(配置 scp / 瓦片 / 子图集 / 棱线缓存 / 属性查询)
297
+ * 发出前都过一遍,可改写 URL(加 `?token=`)或附加请求头。
298
+ * 不传 = 行为与旧版完全一致。见 `AuthenticateFn`。
299
+ */
300
+ authenticate?: AuthenticateFn;
293
301
  onStats?: (stats: Stats) => void;
294
302
  onReady?: (entry: S3MDatasetEntry) => void;
295
303
  onPick?: (result: PickResult | null) => void;
@@ -572,6 +580,8 @@ export class S3MViewer {
572
580
  /** 宿主重画信号与地图朝向,构造时注入 */
573
581
  private _onRequestRepaint: () => void = () => {};
574
582
  private _getBearing: (() => number) | undefined;
583
+ /** 鉴权适配(全局默认),构造时注入 */
584
+ private readonly _authenticate: AuthenticateFn | undefined;
575
585
  private _lodRangeScale = 1.0;
576
586
  /** LOD 判定屏幕高度的 DPR 上限。Infinity = 不截断(默认,行为同旧版) */
577
587
  private _lodPixelRatioCap = Infinity;
@@ -600,6 +610,7 @@ export class S3MViewer {
600
610
  }
601
611
  this._onRequestRepaint = options.onRequestRepaint ?? (() => {});
602
612
  this._getBearing = options.getBearing;
613
+ this._authenticate = options.authenticate;
603
614
  this.onStats = options.onStats ?? (() => {});
604
615
  this.onReady = options.onReady ?? (() => {});
605
616
  this.onPick = options.onPick ?? (() => {});
@@ -670,7 +681,11 @@ export class S3MViewer {
670
681
 
671
682
  this._fps.last = performance.now();
672
683
  this._datasetQueue = this._normalizeDatasets(options);
673
- if (options.attributes) this.attributeQuery = new AttributeQuery(options.attributes);
684
+ if (options.attributes)
685
+ this.attributeQuery = new AttributeQuery({
686
+ ...options.attributes,
687
+ authenticate: options.attributes.authenticate ?? this._authenticate,
688
+ });
674
689
  }
675
690
 
676
691
  /* ---------------------------------------------------------------- 生命周期 */
@@ -950,7 +965,8 @@ export class S3MViewer {
950
965
  },
951
966
  },
952
967
  this.options.maxTriangles ?? Infinity,
953
- this.options.maxImageCache ?? 128
968
+ this.options.maxImageCache ?? 128,
969
+ this._authenticate
954
970
  );
955
971
  return entry;
956
972
  }
@@ -1616,7 +1632,9 @@ export class S3MViewer {
1616
1632
  /* ---------------------------------------------------------------- 按业务字段着色 */
1617
1633
 
1618
1634
  setAttributeSource(options: AttributeQueryOptions | null): void {
1619
- this.attributeQuery = options ? new AttributeQuery(options) : null;
1635
+ this.attributeQuery = options
1636
+ ? new AttributeQuery({ ...options, authenticate: options.authenticate ?? this._authenticate })
1637
+ : null;
1620
1638
  }
1621
1639
 
1622
1640
  getLoadedSmids(): number[] {
@@ -2683,7 +2701,7 @@ export class S3MViewer {
2683
2701
  * @param url 棱线缓存地址(二进制 Float32 序列:x,y,z,x,y,z,… 成对成段)
2684
2702
  */
2685
2703
  async loadCreases(url: string): Promise<void> {
2686
- const resp = await fetch(url);
2704
+ const resp = await authFetch(url, this._authenticate, 'creases');
2687
2705
  if (!resp.ok) throw new Error(`[s3m] 棱线缓存加载失败: ${resp.status} ${url}`);
2688
2706
  const buf = await resp.arrayBuffer();
2689
2707
  const arr = new Float32Array(buf);
@@ -7,6 +7,7 @@
7
7
  * 命中阈值就请求 childTile,实现渐进式细化。
8
8
  */
9
9
  import { parseS3MB } from '../core/S3MBParser.js';
10
+ import { authFetch, type AuthenticateFn } from '../core/AuthFetch.js';
10
11
  import { getSharedS3MWorker, S3MWorkerClient, WorkerDeadError } from '../worker/S3MWorkerClient.js';
11
12
  import {
12
13
  composeLeafAtlas,
@@ -281,6 +282,11 @@ export interface S3MTilesetOptions {
281
282
  * 度量方式见 `_tmp/atlas/rule2.mts`。
282
283
  */
283
284
  atlasLod?: boolean;
285
+ /**
286
+ * 鉴权适配函数(默认不传 = 行为与旧版一致)。每个请求(配置 scp / 瓦片 /
287
+ * 子图集)发出前过一遍,可改写 URL(加 `?token=`)或附加请求头。见 `AuthenticateFn`。
288
+ */
289
+ authenticate?: AuthenticateFn;
284
290
  }
285
291
 
286
292
  export class S3MTileset {
@@ -360,6 +366,8 @@ export class S3MTileset {
360
366
  private readonly _onError: (error: unknown) => void;
361
367
  private readonly _onProgress: (stats: Stats) => void;
362
368
  private readonly _maxRetries: number;
369
+ /** 鉴权适配函数;undefined = 不做任何改写(行为与旧版一致) */
370
+ readonly authenticate: AuthenticateFn | undefined;
363
371
  /** 失败重试计数与下次可用时间,退避期间 _enqueue 会直接跳过 */
364
372
  private readonly _retryCount = new Map<string, number>();
365
373
  private readonly _retryAt = new Map<string, number>();
@@ -388,6 +396,7 @@ export class S3MTileset {
388
396
  this._onError = options.onError ?? ((e) => console.warn(e));
389
397
  this._onProgress = options.onProgress ?? (() => {});
390
398
  this._maxRetries = Math.max(0, options.maxRetries ?? 2);
399
+ this.authenticate = options.authenticate;
391
400
 
392
401
  // 渲染层可注入:Node 环境下不需要 three,也能跑 URL / 解析链路
393
402
  this._buildTile = options.buildTile ?? null;
@@ -409,7 +418,7 @@ export class S3MTileset {
409
418
 
410
419
  async init(): Promise<this> {
411
420
  this.configUrl = toAbsolute(this.configUrl);
412
- const res = await fetch(this.configUrl);
421
+ const res = await authFetch(this.configUrl, this.authenticate, 'config');
413
422
  if (!res.ok) throw new Error(`配置读取失败 ${res.status} ${this.configUrl}`);
414
423
  const text = await res.text();
415
424
  const config = parseConfigText(text);
@@ -991,7 +1000,7 @@ export class S3MTileset {
991
1000
  const u = resolveUrl(dirOf(baseUrl), `${names[0]}.texblock?extratiles=${names.join(';')}`);
992
1001
  try {
993
1002
  if (!u) throw new Error('无法拼出 URL');
994
- const res = await fetch(u);
1003
+ const res = await authFetch(u, this.authenticate, 'texblock');
995
1004
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
996
1005
  const segments = await parseTexblockSegments(await res.arrayBuffer());
997
1006
  if (segments.length !== names.length) {
@@ -1009,7 +1018,7 @@ export class S3MTileset {
1009
1018
  const u = resolveUrl(dirOf(baseUrl), `${stem}.texblock`);
1010
1019
  try {
1011
1020
  if (!u) throw new Error('无法拼出 URL');
1012
- const res = await fetch(u);
1021
+ const res = await authFetch(u, this.authenticate, 'texblock');
1013
1022
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1014
1023
  const records = (await parseTexblockSegments(await res.arrayBuffer())).flat();
1015
1024
  this.putLeafRecords(stem, records);
@@ -1052,7 +1061,7 @@ export class S3MTileset {
1052
1061
  groupNames = cachedSet.names;
1053
1062
  tile = cachedSet.tiles[groupIndex] ?? cachedSet.tiles[0]!;
1054
1063
  } else {
1055
- const res = await fetch(baseUrl);
1064
+ const res = await authFetch(baseUrl, this.authenticate, 'tile');
1056
1065
  if (!res.ok) {
1057
1066
  throw new Error(`HTTP ${res.status} 加载瓦片失败: ${baseUrl}`);
1058
1067
  }