@nebula-spatial/viewer 0.2.4 → 0.3.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this package are documented in this file.
4
4
 
5
+ ## 0.3.0
6
+
7
+ - 新增 `ViewerPlugin`、`ViewerPluginContext`、8 类贡献点及 `viewer.use()` / `viewer.plugins` 生命周期管理。
8
+ - 新增 `cadInteraction: 'managed' | 'manual' | 'disabled'`、统一 `hitTest/select/clearSelection`、overlay 原语和 camera state API。
9
+ - `camera.setState()` 当前只承诺立即应用经过校验的快照;动画相机切换保留到后续 P2 能力。
10
+ - **Breaking:** 删除 `pickTextAtScreen`、`selectInsert`、`setTextSelection` 三套旧选择入口;迁移为 `hitTest()` 获取 `CadHit`,再调用 `select(hit ? { kind: hit.kind, selection: hit.selection } : null)` 或 `clearSelection()`。
11
+ - 补齐 CAD 公共类型导出、状态 `revision`、图层确定性可见性命令、同步 `CameraStateStore`、加载进度/结构化错误事件。
12
+ - 无参 `fitView()` 现在适配全部内容;需要聚焦 CAD 选择时使用 `viewer.cad.fitSelection()`。
13
+ - `cad-readonly` UI 作为内置插件注册,并支持语义 tokens、style 逃生舱、实例隔离持久化和 `ui-layout-change` 事件。
14
+ - 内置 UI 文案支持 `locale`、`messages`、`messageResolver`,属性面板与比例尺支持 `unitFormatter`。
15
+ - `addObject(object, { ownership: 'borrowed' })` 明确由调用方持有并释放 Object3D 的 GPU 资源。
16
+
5
17
  ## 0.2.4
6
18
 
7
19
  - Keep `@nebula-spatial/cad-loader` as an external runtime dependency so compatible loader updates can be installed independently.
package/README.md CHANGED
@@ -5,6 +5,54 @@
5
5
 
6
6
  Viewer 不解析 DXF/DWG,也不提供编辑命令、历史记录或业务状态管理。
7
7
 
8
+ ## 插件与 Headless 模式
9
+
10
+ Viewer 的扩展首选插件化:插件通过受限的 `ViewerPluginContext` 读取不可变快照、调用
11
+ commands、订阅事件并注册贡献点,不能访问内部 scene、renderer 或 camera。
12
+
13
+ ```ts
14
+ let stop: (() => void) | undefined;
15
+ const annotationPlugin = {
16
+ id: 'annotation',
17
+ activate(ctx) {
18
+ stop = ctx.events.on('camera-change', () => updateAnnotations(ctx.cad.$read()));
19
+ ctx.contribute({
20
+ kind: 'command',
21
+ id: 'annotation.refresh',
22
+ handler: () => updateAnnotations(ctx.cad.$read()),
23
+ });
24
+ },
25
+ deactivate() { stop?.(); /* 清理业务资源 */ },
26
+ };
27
+
28
+ const viewer = createViewer({ viewport, canvas, cadInteraction: 'manual' });
29
+ viewer.use([annotationPlugin]);
30
+ ```
31
+
32
+ `viewer.use()` 会立即激活插件;`viewer.dispose()` 按注册逆序停用插件并回收插件注册的
33
+ 事件、贡献点和 DOM。重复的插件或贡献点 id 会被拒绝。
34
+
35
+ CAD 点击策略由 `cadInteraction` 统一表达:`managed`(默认)由 Viewer 完成拾取和选择,
36
+ `manual` 只提供 `viewer.cad.hitTest/select/clearSelection` 原语,`disabled` 关闭 CAD 点击语义
37
+ 只保留导航。`cad-readonly` UI 仅在 `managed` 模式下挂载。
38
+
39
+ `viewer.overlays.set/remove` 是手动模式的受控 overlay 原语;传入的 `Object3D` 默认由业务
40
+ 拥有,只有显式传 `{ owned: true }` 时 Viewer 才在移除时负责释放。
41
+
42
+ `viewer.cad.open()` 在头部解析完成时 resolve;增量几何和 sidecar 仍通过
43
+ `load-progress`、`document-change` 与 `hud-change` 继续报告。替换中的 open 会以
44
+ `E_SUPERSEDED` 结束,外部 `AbortSignal` 取消会以 `E_ABORTED` 结束。
45
+
46
+ `viewer.cad.getSnapshot()` 返回深冻结快照(包括独立的 `CadInspectorSnapshot` / `CadHudSnapshot` 数据形状),适合状态管理;图层可用
47
+ `layers.setVisible(index, visible)` / `setVisibleBatch()` 做确定性更新。无参
48
+ `viewer.fitView()` 与 `viewer.fitAll()` 适配全部内容,`viewer.cad.fitSelection()` 只适配当前
49
+ CAD 选择。
50
+
51
+ 高层相机状态通过 `viewer.camera.getState()`、`setState()` 和 `reset()` 访问,不暴露底层
52
+ Three.js Camera。应用成功后会同步发布 `camera-change` 和 `viewport-change`;跨 2D/3D 时还会
53
+ 发布 `view-mode-change`。`camera-change.reason` 区分 `pointer`、`wheel`、`fit`、`restore`、
54
+ `api` 和 `view-mode`。当前 `setState()` 立即应用快照,动画切换属于后续能力。
55
+
8
56
  ## CAD 快速开始
9
57
 
10
58
  ```ts
@@ -72,7 +120,7 @@ Troika 全局、模块级、一次性配置语义。字体资源和 `resolveText
72
120
  `viewer.cad` 暴露以下只读模型,可通过 `document-change`、`layer-change`、
73
121
  `block-change`、`inspector-change` 和 `hud-change` 绑定到产品 UI:
74
122
 
75
- - `document`:文档信息和加载阶段。
123
+ - `document`:文档信息、加载阶段和 `units`(`source`;若 pack 提供则包含 `scaleToMeters`)。
76
124
  - `layers`:图层可见性与颜色覆盖。
77
125
  - `blocks`:INSERT 使用项和实例可见性。
78
126
  - `inspector`:当前 CAD INSERT 或文本选择。
@@ -83,12 +131,40 @@ CAD 文档的外部 block sidecar 按初始视口需求并行加载,并在首
83
131
 
84
132
  可选的 `ui` 配置会挂载 `cad-readonly` 预设 UI;产品层也可以完全自行呈现这些模型。
85
133
 
134
+ 内置 UI 的文案和单位格式化通过稳定配置入口覆盖,不需要依赖 Shadow DOM 内部 class:
135
+
136
+ ```ts
137
+ const viewer = createViewer({
138
+ viewport,
139
+ canvas,
140
+ ui: {
141
+ root: document.querySelector('#viewer-ui')!,
142
+ locale: 'en-US',
143
+ messages: { 'toolbar.fit': 'Frame' },
144
+ unitFormatter: (value, { unit }) => `${value.toFixed(2)} ${unit}`,
145
+ },
146
+ });
147
+ ```
148
+
149
+ 消息解析优先级为 `messageResolver`、`messages`、内置 locale fallback;`unitFormatter` 会
150
+ 覆盖属性面板和比例尺的数值标签。没有传入这些配置时,默认保持简体中文和图纸单位的现有显示。
151
+
152
+ 面板状态默认使用 `nebula-viewer:<instance>:*` 命名空间持久化;可通过 `persistenceKey`
153
+ 指定稳定实例标识,或通过同步的 `stateStore.read/write` 接入业务状态存储。设置
154
+ `persistence: false` 会完全关闭状态读写。
155
+
156
+ 内置快捷键默认只绑定到 `viewport`;Viewer 会在该区域内的鼠标/触控操作后将焦点移入该元素,
157
+ 因此不会污染页面其它区域的快捷键。不会响应 `input`、`textarea`、`select` 或
158
+ `contenteditable` 内的按键。`keyboard.target` 可指定其它作用域,
159
+ `keyboard.shortcuts.fit` 可修改适应视图按键或设为 `false` 单独关闭。内置 UI 的“适应”按钮和
160
+ 快捷键会优先适应当前 CAD 选择;没有选择时才适应整张图纸。
161
+
86
162
  ## 通用 Three.js 对象
87
163
 
88
164
  Viewer 同样可管理非 CAD 的 `Object3D`:
89
165
 
90
166
  ```ts
91
- viewer.addObject(model);
167
+ viewer.addObject(model, { ownership: 'borrowed' });
92
168
  viewer.fitView(model);
93
169
 
94
170
  const stopPicking = viewer.on('pick', ({ result }) => {
@@ -99,6 +175,10 @@ viewer.setHighlight(model, { color: 0x5b8cff });
99
175
  stopPicking();
100
176
  ```
101
177
 
178
+ `addObject()`、成功的 `removeObject()` 和实际改变可见性的 `setVisible()` 都会发布一次
179
+ `content-change`(`source: 'object'`)。`camera-change` 与 `viewport-change` 同样覆盖没有
180
+ 打开 CAD 文档时的用户导航和 resize,便于上层同步小地图、比例尺或协同视角。
181
+
102
182
  默认点击会执行通用 Raycaster 拾取。格式专用的交互可通过 `pointerPick: false` 关闭它,
103
183
  并监听 `pointer-click`、`pointer-move` 和 `pointer-leave`。`screenToWorldOnPlane()` 用于
104
184
  通用的屏幕坐标到世界平面转换。
@@ -108,6 +188,25 @@ stopPicking();
108
188
  Viewer 借用 `addObject()` 传入的 `Object3D`。`removeObject()` 和 `dispose()` 只解除场景
109
189
  挂载,不会释放调用方的 Geometry、Material 或 Texture。
110
190
 
191
+ 例如,GLTF 资源应在从 Viewer 移除后由加载它的业务统一释放:
192
+
193
+ ```ts
194
+ import { Material, Mesh, Texture } from 'three';
195
+
196
+ viewer.removeObject(gltf.scene);
197
+ gltf.scene.traverse((object) => {
198
+ if (!(object instanceof Mesh)) return;
199
+ object.geometry.dispose();
200
+ const materials = Array.isArray(object.material) ? object.material : [object.material];
201
+ for (const material of materials as Material[]) {
202
+ for (const value of Object.values(material)) {
203
+ if (value instanceof Texture) value.dispose();
204
+ }
205
+ material.dispose();
206
+ }
207
+ });
208
+ ```
209
+
111
210
  Viewer 自行拥有 SceneRuntime、Renderer、Camera、Controls、事件监听和 CAD 会话;调用
112
211
  `dispose()` 会统一释放这些资源。运行时依赖为 `three` peer dependency,要求 Node.js 18
113
212
  或更高版本。
@@ -1,6 +1,8 @@
1
1
  import type { RenderDebugBreakdownEntry, SplitBlockLayerDetail } from '@nebula-spatial/cad-loader';
2
2
  import type { CadSessionLoadTiming } from '@nebula-spatial/cad-loader';
3
+ import type { ViewerViewMode } from '../types';
3
4
  export type { CadSessionLoadTiming } from '@nebula-spatial/cad-loader';
5
+ export type { RenderDebugBreakdownEntry, SplitBlockLayerDetail } from '@nebula-spatial/cad-loader';
4
6
  export type CadSessionPhase = 'created' | 'header-ready' | 'initial-loading' | 'interactive' | 'failed' | 'disposed';
5
7
  export interface CadWorldBounds {
6
8
  readonly minX: number;
@@ -70,6 +72,7 @@ export interface CadOpenOptions {
70
72
  * enabled; `false` disables background glyph preloading.
71
73
  */
72
74
  textWarmup?: boolean | CadTextWarmupOptions;
75
+ signal?: AbortSignal;
73
76
  }
74
77
  export interface CadDocumentHandle {
75
78
  readonly documentKey: string | null;
@@ -77,6 +80,12 @@ export interface CadDocumentHandle {
77
80
  readonly phase: CadSessionPhase;
78
81
  readonly headerBounds: CadWorldBounds | null;
79
82
  readonly layerCount: number;
83
+ readonly units: CadDocumentUnits;
84
+ }
85
+ export interface CadDocumentUnits {
86
+ /** Source unit label. Legacy packs without metadata default to `mm`. */
87
+ readonly source: string;
88
+ readonly scaleToMeters?: number;
80
89
  }
81
90
  export interface CadLayerRow {
82
91
  readonly index: number;
@@ -87,12 +96,18 @@ export interface CadLayerRow {
87
96
  }
88
97
  export interface CadLayerModel {
89
98
  readonly rows: readonly CadLayerRow[];
90
- readonly allColor?: number | null;
99
+ readonly allColor: number | null;
100
+ readonly revision: number;
91
101
  toggle(index: number): void;
102
+ setVisible(index: number, visible: boolean): boolean;
103
+ setVisibleBatch(updates: ReadonlyArray<{
104
+ index: number;
105
+ visible: boolean;
106
+ }>): number;
92
107
  showAll(): void;
93
108
  hideAll(): void;
94
109
  setColor(index: number, color: number | null): void;
95
- setAllColor?(color: number | null): void;
110
+ setAllColor(color: number | null): void;
96
111
  }
97
112
  export interface CadBlockUsageRow {
98
113
  readonly insertId: string;
@@ -107,8 +122,9 @@ export interface CadBlockUsageRow {
107
122
  }
108
123
  export interface CadBlockModel {
109
124
  readonly usages: readonly CadBlockUsageRow[];
125
+ readonly revision: number;
110
126
  toggleInsert(insertId: string, visible?: boolean): void;
111
- batchToggleInserts?(updates: ReadonlyArray<{
127
+ batchToggleInserts(updates: ReadonlyArray<{
112
128
  insertId: string;
113
129
  visible: boolean;
114
130
  }>): void;
@@ -159,19 +175,23 @@ export interface CadInsertDetails {
159
175
  readonly maxY: number;
160
176
  };
161
177
  }
162
- export interface CadInspectorModel {
178
+ export interface CadInspectorSnapshot {
163
179
  readonly selection: CadInsertSelection | null;
164
180
  readonly details: CadInsertDetails | null;
165
- /** Optional so existing inspector consumers remain source-compatible. */
166
- readonly textSelection?: CadTextSelection | null;
167
- readonly textDetails?: CadTextDetails | null;
181
+ readonly textSelection: CadTextSelection | null;
182
+ readonly textDetails: CadTextDetails | null;
183
+ readonly revision: number;
184
+ }
185
+ /** Live inspector model. Subscribe to `inspector-change` before reading it reactively. */
186
+ export interface CadInspectorModel extends CadInspectorSnapshot {
168
187
  }
169
188
  export interface CadScaleRulerState {
170
189
  readonly visible: boolean;
171
190
  readonly lengthLabel: string;
172
191
  readonly worldLength: number;
173
192
  }
174
- export interface CadHudModel {
193
+ export interface CadHudSnapshot {
194
+ readonly revision: number;
175
195
  readonly filename: string | null;
176
196
  readonly fileCount: number;
177
197
  readonly visibleLayerCount: number;
@@ -198,6 +218,9 @@ export interface CadHudModel {
198
218
  readonly splitBlockLayerDetails: readonly SplitBlockLayerDetail[];
199
219
  readonly phase: CadSessionPhase | null;
200
220
  }
221
+ /** Live HUD model. Subscribe to `hud-change` before reading it reactively. */
222
+ export interface CadHudModel extends CadHudSnapshot {
223
+ }
201
224
  export interface CadViewerEventMap {
202
225
  'document-change': {
203
226
  document: CadDocumentHandle | null;
@@ -217,6 +240,55 @@ export interface CadViewerEventMap {
217
240
  'load-timing': {
218
241
  timing: CadSessionLoadTiming;
219
242
  };
243
+ 'load-progress': CadLoadProgress;
244
+ error: CadViewerErrorEvent;
245
+ }
246
+ export interface CadLoadProgress {
247
+ readonly phase: CadSessionPhase;
248
+ readonly progress: number;
249
+ readonly pending: number;
250
+ readonly loaded: number;
251
+ readonly settled: boolean;
252
+ }
253
+ export type CadViewerErrorCode = 'E_SOURCE' | 'E_FETCH' | 'E_PARSE_HEADER' | 'E_PARSE_GEOMETRY' | 'E_WORKER' | 'E_SIDECAR_FETCH' | 'E_ANNOTATION' | 'E_FONT' | 'E_ABORTED' | 'E_SUPERSEDED';
254
+ export interface CadViewerErrorEvent {
255
+ readonly code: CadViewerErrorCode;
256
+ readonly stage: 'source' | 'header' | 'geometry' | 'worker' | 'sidecar' | 'annotation' | 'font' | 'lifecycle';
257
+ readonly fatal: boolean;
258
+ readonly source: CadLoadSource | null;
259
+ readonly error: Error;
260
+ }
261
+ export type CadSelection = {
262
+ readonly kind: 'insert';
263
+ readonly selection: CadInsertSelection;
264
+ } | {
265
+ readonly kind: 'text';
266
+ readonly selection: CadTextSelection;
267
+ };
268
+ export type CadHit = {
269
+ readonly kind: 'insert';
270
+ readonly selection: CadInsertSelection;
271
+ readonly details: CadInsertDetails | null;
272
+ } | {
273
+ readonly kind: 'text';
274
+ readonly selection: CadTextSelection;
275
+ readonly details: CadTextDetails | null;
276
+ };
277
+ export interface CadViewerSnapshot {
278
+ readonly revision: number;
279
+ readonly document: CadDocumentHandle | null;
280
+ readonly layers: {
281
+ readonly rows: readonly CadLayerRow[];
282
+ readonly allColor: number | null;
283
+ readonly revision: number;
284
+ };
285
+ readonly blocks: {
286
+ readonly usages: readonly CadBlockUsageRow[];
287
+ readonly revision: number;
288
+ };
289
+ readonly inspector: CadInspectorSnapshot;
290
+ readonly hud: CadHudSnapshot;
291
+ readonly loadTiming: CadSessionLoadTiming | null;
220
292
  }
221
293
  export interface CadViewerCapability {
222
294
  readonly document: CadDocumentHandle | null;
@@ -225,15 +297,23 @@ export interface CadViewerCapability {
225
297
  readonly inspector: CadInspectorModel;
226
298
  readonly hud: CadHudModel;
227
299
  readonly loadTiming: CadSessionLoadTiming | null;
300
+ readonly capabilities: {
301
+ readonly insertSelection: true;
302
+ readonly textSelection: true;
303
+ readonly insertPreview: true;
304
+ readonly batchVisibility: true;
305
+ };
228
306
  open(source: CadLoadSource, options?: CadOpenOptions): Promise<CadDocumentHandle>;
229
307
  close(): void;
230
- selectInsert?(insertId: string | null): void;
231
308
  renderInsertPreview(canvas: HTMLCanvasElement): boolean;
232
- pickTextAtScreen?(clientX: number, clientY: number): {
233
- selection: CadTextSelection;
234
- details: CadTextDetails | null;
235
- } | null;
236
- setTextSelection?(selection: CadTextSelection | null, details: CadTextDetails | null): void;
309
+ hitTest(clientX: number, clientY: number, options?: {
310
+ readonly kinds?: readonly ('insert' | 'text')[];
311
+ }): CadHit | null;
312
+ select(selection: CadSelection | null): void;
313
+ clearSelection(): void;
314
+ getSnapshot(): CadViewerSnapshot;
315
+ fitDocument(): boolean;
316
+ fitSelection(): boolean;
237
317
  on<K extends keyof CadViewerEventMap>(type: K, listener: (event: CadViewerEventMap[K]) => void): () => void;
238
318
  }
239
319
  export type ViewerUiPreset = 'cad-readonly';
@@ -257,8 +337,46 @@ export interface ViewerUiOptions {
257
337
  preset?: ViewerUiPreset;
258
338
  parts?: ViewerUiParts;
259
339
  theme?: ViewerUiTheme;
340
+ tokens?: ViewerUiTokens;
341
+ style?: string | ((root: ShadowRoot) => void);
342
+ persistence?: boolean;
343
+ persistenceKey?: string;
344
+ stateStore?: ViewerUiStateStore;
345
+ keyboard?: ViewerUiKeyboardOptions;
346
+ locale?: string;
347
+ messages?: Readonly<Record<string, string>>;
348
+ messageResolver?: (key: string, params?: Readonly<Record<string, string | number>>) => string | undefined;
349
+ unitFormatter?: (value: number, context: {
350
+ readonly unit: string;
351
+ readonly viewMode: ViewerViewMode;
352
+ }) => string;
353
+ }
354
+ export interface ViewerUiStateStore {
355
+ read(key: string): string | null;
356
+ write(key: string, value: string): void;
357
+ }
358
+ export interface ViewerUiKeyboardOptions {
359
+ enabled?: boolean;
360
+ /**
361
+ * Restrict shortcuts to this element. A pointer interaction inside it focuses
362
+ * the target unless the interaction began in an editable control.
363
+ */
364
+ target?: HTMLElement;
365
+ shortcuts?: {
366
+ /** Key used by the built-in fit command. Defaults to `f`; `false` disables it. */
367
+ fit?: string | false;
368
+ };
369
+ }
370
+ export interface ViewerUiTokens {
371
+ accent?: string;
372
+ panelBackground?: string;
373
+ background?: string;
374
+ text?: string;
375
+ textDim?: string;
376
+ border?: string;
377
+ hudBackground?: string;
260
378
  }
261
379
  export interface CameraStateStore {
262
- read(key: string): Promise<unknown> | unknown;
263
- write(key: string, value: unknown): Promise<void> | void;
380
+ read(key: string): unknown | null;
381
+ write(key: string, value: unknown): void;
264
382
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { createViewer } from './viewer';
2
+ export type { Disposable, ToolbarItem, ViewerState, ViewerInteractionHooks, ViewerContributionPoint, ViewerPlugin, ViewerPluginContext, ViewerPluginRegistry, } from './plugins';
2
3
  export { configureViewerText } from './cad/text';
3
4
  export type { ViewerTextBuilderOptions } from './cad/text';
4
- export type { CadBlockModel, CadDocumentHandle, CadFontResolver, CadResolvedTextFont, CadHudModel, CadInspectorModel, CadLayerModel, CadLoadSource, CadOpenOptions, CadSessionLoadTiming, CadTextDetails, CadTextSelection, CadTextSelectionId, CadViewerCapability, ResolveTextFont, CadViewerEventMap, CameraStateStore, Viewer, ViewerEventMap, ViewerEnvironment, ViewerFitViewOptions, ViewerHighlightOptions, ViewerOptions, ViewerPickOptions, ViewerPickResult, ViewerPointerEvent, ViewerUiOptions, ViewerUiParts, ViewerUiPreset, ViewerUiTheme, ViewerViewMode, } from './types';
5
+ export type { CadBlockModel, CadDocumentHandle, CadDocumentUnits, CadFontResolver, CadFontRequest, CadResolvedFont, CadResolvedTextFont, CadTextWarmupOptions, CadHudModel, CadHudSnapshot, CadInspectorModel, CadInspectorSnapshot, CadLayerModel, CadLayerRow, CadBlockUsageRow, CadSessionPhase, CadWorldBounds, CadScaleRulerState, CadInsertDetails, CadInsertSelection, CadInsertSelectionId, CadTextDetails, CadTextSelection, CadTextSelectionId, CadSelection, CadHit, CadViewerSnapshot, CadLoadProgress, CadViewerErrorCode, CadViewerErrorEvent, CadLoadSource, CadOpenOptions, CadSessionLoadTiming, RenderDebugBreakdownEntry, SplitBlockLayerDetail, CadViewerCapability, ResolveTextFont, CadViewerEventMap, CameraStateStore, Viewer, ViewerEventMap, ViewerEnvironment, ViewerFitViewOptions, ViewerHighlightOptions, ViewerOptions, ViewerPickOptions, ViewerPickResult, ViewerPointerEvent, ViewerObjectOptions, ViewerCameraSnapshot, ViewerOrthographicSnapshot, ViewerOrbitSnapshot, ViewerRenderStats, ViewerDocumentRect, ViewerUiKeyboardOptions, ViewerUiOptions, ViewerUiParts, ViewerUiPreset, ViewerUiStateStore, ViewerUiTheme, ViewerViewMode, CadInteractionMode, CameraChangeReason, ViewerUiTokens, } from './types';