@nebula-spatial/viewer 0.2.4 → 0.4.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.
@@ -0,0 +1,234 @@
1
+ import type { Object3D, WebGLRenderer, Scene, Camera, Box3, ColorRepresentation } from 'three';
2
+ import type { CadLoadSource, CadOpenOptions } from '../cad/types';
3
+ import type { ViewerViewMode } from '../types';
4
+ export type AssetPreviewKind = 'cad' | 'model' | 'simulation';
5
+ export type BuiltinAssetFormat = 'cad' | 'glb' | 'gltf' | 'urdf' | 'mjcf' | 'usd' | 'usda' | 'usdc' | 'usdz';
6
+ export type AssetFormat = BuiltinAssetFormat | (string & {});
7
+ export interface AssetFileEntry {
8
+ readonly path: string;
9
+ readonly file: File | Blob;
10
+ }
11
+ export type AssetSource = CadLoadSource | {
12
+ readonly kind: 'files';
13
+ readonly files: readonly AssetFileEntry[];
14
+ readonly entry?: string;
15
+ readonly signal?: AbortSignal;
16
+ };
17
+ export interface AssetResourceResolver {
18
+ resolve(path: string, options?: {
19
+ readonly signal?: AbortSignal;
20
+ }): string | URL | Blob | Promise<string | URL | Blob>;
21
+ }
22
+ export interface ViewerAssetOptions {
23
+ readonly dracoDecoderPath?: string;
24
+ readonly openUsdBaseUrl?: string;
25
+ readonly mujocoBaseUrl?: string;
26
+ }
27
+ export interface OpenAssetOptions {
28
+ readonly simulationTheme?: 'dark' | 'light';
29
+ readonly format?: AssetFormat | 'auto';
30
+ readonly filename?: string;
31
+ readonly signal?: AbortSignal;
32
+ readonly baseUrl?: string;
33
+ readonly resourceResolver?: AssetResourceResolver;
34
+ /** @deprecated Auto detection never falls back to CAD; pass an explicit format instead. */
35
+ readonly unknownFormatPolicy?: 'cad' | 'error';
36
+ readonly dracoDecoderPath?: string;
37
+ readonly mujocoBaseUrl?: string;
38
+ readonly openUsdBaseUrl?: string;
39
+ /**
40
+ * Which authored axis is "up" for a plain model asset. glTF's normative
41
+ * coordinate system is Y-up, but CAD-kernel exporters emit Z-up files with no
42
+ * in-file declaration, so this cannot be inferred reliably. Defaults to 'z',
43
+ * matching the preview world; 'y' rotates the model root into world +Z.
44
+ * Ignored by simulation assets, whose formats declare their own up axis.
45
+ */
46
+ readonly upAxis?: AssetUpAxis;
47
+ /** CAD-specific compatibility options. `filename` and `signal` above take precedence. */
48
+ readonly cad?: Omit<CadOpenOptions, 'filename' | 'signal'>;
49
+ }
50
+ /** World axis a model was authored to treat as up. See `OpenAssetOptions.upAxis`. */
51
+ export type AssetUpAxis = 'y' | 'z';
52
+ export interface AssetPreviewCapabilities {
53
+ readonly canPlay: boolean;
54
+ readonly canPause: boolean;
55
+ readonly canReset: boolean;
56
+ readonly hasPhysics: boolean;
57
+ /**
58
+ * A plain model whose authored up axis can be re-oriented at runtime.
59
+ * Optional (absent means false) so pre-existing third-party asset sessions
60
+ * that build this literal keep compiling; only the model session opts in.
61
+ */
62
+ readonly canReorientUpAxis?: boolean;
63
+ /**
64
+ * The staged ground (grid + reflection) can be hidden. Optional, same
65
+ * rationale as `canReorientUpAxis`; model and simulation sessions opt in.
66
+ */
67
+ readonly canToggleGround?: boolean;
68
+ /**
69
+ * Authored or runtime collision geometry can be shown independently of
70
+ * dynamic physics. Static USD stages may declare colliders without rigid
71
+ * bodies, so this must not be inferred from `hasPhysics`.
72
+ */
73
+ readonly canToggleCollision?: boolean;
74
+ }
75
+ export type AssetPreviewPhase = 'detecting' | 'loading' | 'ready' | 'failed' | 'closing' | 'closed';
76
+ export interface AssetErrorSnapshot {
77
+ readonly code: string;
78
+ readonly message: string;
79
+ readonly stage: string;
80
+ readonly fatal: boolean;
81
+ }
82
+ /** Non-fatal condition that changed how an otherwise usable asset is presented. */
83
+ export interface AssetWarningSnapshot {
84
+ readonly code: string;
85
+ readonly message: string;
86
+ readonly stage: string;
87
+ }
88
+ export interface AssetPreviewSnapshot {
89
+ readonly revision: number;
90
+ readonly phase: AssetPreviewPhase;
91
+ readonly stage?: string;
92
+ readonly message?: string;
93
+ readonly format: AssetFormat | null;
94
+ readonly kind: AssetPreviewKind | null;
95
+ readonly capabilities: AssetPreviewCapabilities | null;
96
+ readonly error: AssetErrorSnapshot | null;
97
+ /** Present when loading succeeded with an explicit, user-visible degradation. */
98
+ readonly warning?: AssetWarningSnapshot | null;
99
+ /**
100
+ * Display name for the asset (`OpenAssetOptions.filename`, else the file name
101
+ * derived from the normalized source). Hosts should render this rather than
102
+ * re-deriving a name from the raw URL.
103
+ */
104
+ readonly filename: string | null;
105
+ }
106
+ /**
107
+ * Control state of the active asset. Published on `asset-control-change` so UI
108
+ * (built-in or plugin) never has to keep its own copy of a pressed state —
109
+ * whichever entry point changed it, exactly one authoritative value is broadcast.
110
+ */
111
+ export interface AssetControlState {
112
+ readonly playing: boolean;
113
+ readonly groundVisible: boolean;
114
+ readonly collisionVisible: boolean;
115
+ readonly upAxis: AssetUpAxis | null;
116
+ }
117
+ export interface AssetPreview {
118
+ getPhysicsProperties(): Readonly<Record<string, unknown>> | null;
119
+ setCollisionVisible(visible: boolean): void;
120
+ /** Re-orients a plain model's authored up axis without moving the camera. */
121
+ setUpAxis(axis: AssetUpAxis): void;
122
+ getUpAxis(): AssetUpAxis | null;
123
+ /** Shows or hides the staged ground (grid + reflection). */
124
+ setGroundVisible(visible: boolean): void;
125
+ getGroundVisible(): boolean;
126
+ readonly id: string;
127
+ readonly ready: Promise<void>;
128
+ getSnapshot(): AssetPreviewSnapshot;
129
+ subscribe(listener: (snapshot: AssetPreviewSnapshot) => void): () => void;
130
+ close(): Promise<void>;
131
+ play(): void;
132
+ pause(): void;
133
+ reset(): void;
134
+ beginGrab?(event: PointerEvent, camera: import('three').Camera, canvas: HTMLElement): boolean;
135
+ moveGrab?(event: PointerEvent, camera: import('three').Camera, canvas: HTMLElement): void;
136
+ endGrab?(): void;
137
+ }
138
+ export interface AssetPresentationProfile {
139
+ readonly preferredViewMode?: ViewerViewMode;
140
+ /**
141
+ * Canvas clear color while this asset is active. Applied on profile change and
142
+ * reverted to the `createViewer({ clearColor })` value when no asset owns one,
143
+ * so hosts no longer need to encode "which asset is this" in their bootstrap.
144
+ */
145
+ readonly clearColor?: ColorRepresentation;
146
+ /** Staged dark/light variant for assets that own a themed stage. */
147
+ readonly stageTheme?: 'dark' | 'light';
148
+ readonly interactionMode?: 'cad-managed' | 'object' | 'simulation';
149
+ readonly uiProfile?: 'cad' | 'model' | 'simulation' | 'none';
150
+ readonly fitPolicy?: 'restore-then-fit' | 'fit-on-ready' | 'preserve';
151
+ readonly environmentProfile?: string;
152
+ }
153
+ export interface NormalizedAssetSource {
154
+ readonly original: AssetSource;
155
+ readonly entryPath: string | null;
156
+ readonly filename: string | null;
157
+ readonly url: string | null;
158
+ readonly signal: AbortSignal;
159
+ readonly baseUrl: string | null;
160
+ readonly hasResourceResolver: boolean;
161
+ resolve(path: string): Promise<string>;
162
+ release(): void;
163
+ }
164
+ export interface AssetDetectionResult {
165
+ readonly format: AssetFormat;
166
+ }
167
+ export interface AssetLoaderContext {
168
+ readonly signal: AbortSignal;
169
+ readonly options: OpenAssetOptions;
170
+ readonly source: NormalizedAssetSource;
171
+ readonly format: AssetFormat;
172
+ requestRender(): void;
173
+ fit(root: Object3D | Box3, options?: {
174
+ readonly viewMode?: ViewerViewMode;
175
+ readonly padding?: number;
176
+ }): boolean;
177
+ reportError(error: unknown, options: {
178
+ readonly stage: string;
179
+ readonly fatal: boolean;
180
+ readonly code?: string;
181
+ }): void;
182
+ reportProgress?(stage: string, message?: string): void;
183
+ }
184
+ export interface AssetSession {
185
+ setTheme?(theme: 'dark' | 'light'): void;
186
+ beforeRender?(renderer: WebGLRenderer, scene: Scene, camera: Camera): (() => void) | void;
187
+ getPhysicsProperties?(): Readonly<Record<string, unknown>> | null;
188
+ setCollisionVisible?(visible: boolean): void;
189
+ setUpAxis?(axis: AssetUpAxis): void;
190
+ getUpAxis?(): AssetUpAxis | null;
191
+ setGroundVisible?(visible: boolean): void;
192
+ getGroundVisible?(): boolean;
193
+ getFitBounds?(): Box3 | null;
194
+ readonly fitTarget?: Object3D | Box3 | null;
195
+ readonly id: string;
196
+ readonly opened: Promise<void>;
197
+ readonly root?: Object3D | null;
198
+ readonly capabilities?: AssetPreviewCapabilities;
199
+ readonly presentation?: AssetPresentationProfile;
200
+ /** Non-fatal degradation decided by the loader after the session opened. */
201
+ readonly warning?: AssetWarningSnapshot | null;
202
+ beginDispose(): void;
203
+ dispose(): Promise<void>;
204
+ play?(): void;
205
+ pause?(): void;
206
+ reset?(): void;
207
+ update?(deltaSeconds: number): boolean;
208
+ beginGrab?(event: PointerEvent, camera: import('three').Camera, canvas: HTMLElement): boolean;
209
+ moveGrab?(event: PointerEvent, camera: import('three').Camera, canvas: HTMLElement): void;
210
+ endGrab?(): void;
211
+ }
212
+ export interface AssetLoaderContribution {
213
+ readonly kind: 'asset-loader';
214
+ readonly id: string;
215
+ readonly assetKind: AssetPreviewKind;
216
+ readonly formats: readonly AssetFormat[];
217
+ readonly presentation?: AssetPresentationProfile;
218
+ match?(source: NormalizedAssetSource): AssetDetectionResult | null | Promise<AssetDetectionResult | null>;
219
+ createSession(source: NormalizedAssetSource, context: AssetLoaderContext): AssetSession | Promise<AssetSession>;
220
+ }
221
+ export interface AssetChangeEvent {
222
+ readonly sessionId: string;
223
+ readonly snapshot: AssetPreviewSnapshot;
224
+ }
225
+ export interface AssetControlChangeEvent {
226
+ readonly sessionId: string;
227
+ readonly state: AssetControlState;
228
+ }
229
+ export interface AssetErrorEvent extends AssetErrorSnapshot {
230
+ readonly sessionId: string;
231
+ readonly format: AssetFormat | null;
232
+ readonly error: Error;
233
+ }
234
+ export declare const EMPTY_ASSET_CAPABILITIES: AssetPreviewCapabilities;
@@ -1,6 +1,9 @@
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';
4
+ export type { ViewerUiOptions, ViewerSceneUiOptions, ViewerSceneUiParts, ViewerUiPreset, ViewerUiTheme, ViewerUiTokens } from '../ui/types';
3
5
  export type { CadSessionLoadTiming } from '@nebula-spatial/cad-loader';
6
+ export type { RenderDebugBreakdownEntry, SplitBlockLayerDetail } from '@nebula-spatial/cad-loader';
4
7
  export type CadSessionPhase = 'created' | 'header-ready' | 'initial-loading' | 'interactive' | 'failed' | 'disposed';
5
8
  export interface CadWorldBounds {
6
9
  readonly minX: number;
@@ -70,6 +73,7 @@ export interface CadOpenOptions {
70
73
  * enabled; `false` disables background glyph preloading.
71
74
  */
72
75
  textWarmup?: boolean | CadTextWarmupOptions;
76
+ signal?: AbortSignal;
73
77
  }
74
78
  export interface CadDocumentHandle {
75
79
  readonly documentKey: string | null;
@@ -77,6 +81,12 @@ export interface CadDocumentHandle {
77
81
  readonly phase: CadSessionPhase;
78
82
  readonly headerBounds: CadWorldBounds | null;
79
83
  readonly layerCount: number;
84
+ readonly units: CadDocumentUnits;
85
+ }
86
+ export interface CadDocumentUnits {
87
+ /** Source unit label. Legacy packs without metadata default to `mm`. */
88
+ readonly source: string;
89
+ readonly scaleToMeters?: number;
80
90
  }
81
91
  export interface CadLayerRow {
82
92
  readonly index: number;
@@ -87,12 +97,18 @@ export interface CadLayerRow {
87
97
  }
88
98
  export interface CadLayerModel {
89
99
  readonly rows: readonly CadLayerRow[];
90
- readonly allColor?: number | null;
100
+ readonly allColor: number | null;
101
+ readonly revision: number;
91
102
  toggle(index: number): void;
103
+ setVisible(index: number, visible: boolean): boolean;
104
+ setVisibleBatch(updates: ReadonlyArray<{
105
+ index: number;
106
+ visible: boolean;
107
+ }>): number;
92
108
  showAll(): void;
93
109
  hideAll(): void;
94
110
  setColor(index: number, color: number | null): void;
95
- setAllColor?(color: number | null): void;
111
+ setAllColor(color: number | null): void;
96
112
  }
97
113
  export interface CadBlockUsageRow {
98
114
  readonly insertId: string;
@@ -107,8 +123,9 @@ export interface CadBlockUsageRow {
107
123
  }
108
124
  export interface CadBlockModel {
109
125
  readonly usages: readonly CadBlockUsageRow[];
126
+ readonly revision: number;
110
127
  toggleInsert(insertId: string, visible?: boolean): void;
111
- batchToggleInserts?(updates: ReadonlyArray<{
128
+ batchToggleInserts(updates: ReadonlyArray<{
112
129
  insertId: string;
113
130
  visible: boolean;
114
131
  }>): void;
@@ -159,19 +176,23 @@ export interface CadInsertDetails {
159
176
  readonly maxY: number;
160
177
  };
161
178
  }
162
- export interface CadInspectorModel {
179
+ export interface CadInspectorSnapshot {
163
180
  readonly selection: CadInsertSelection | null;
164
181
  readonly details: CadInsertDetails | null;
165
- /** Optional so existing inspector consumers remain source-compatible. */
166
- readonly textSelection?: CadTextSelection | null;
167
- readonly textDetails?: CadTextDetails | null;
182
+ readonly textSelection: CadTextSelection | null;
183
+ readonly textDetails: CadTextDetails | null;
184
+ readonly revision: number;
185
+ }
186
+ /** Live inspector model. Subscribe to `inspector-change` before reading it reactively. */
187
+ export interface CadInspectorModel extends CadInspectorSnapshot {
168
188
  }
169
189
  export interface CadScaleRulerState {
170
190
  readonly visible: boolean;
171
191
  readonly lengthLabel: string;
172
192
  readonly worldLength: number;
173
193
  }
174
- export interface CadHudModel {
194
+ export interface CadHudSnapshot {
195
+ readonly revision: number;
175
196
  readonly filename: string | null;
176
197
  readonly fileCount: number;
177
198
  readonly visibleLayerCount: number;
@@ -198,6 +219,9 @@ export interface CadHudModel {
198
219
  readonly splitBlockLayerDetails: readonly SplitBlockLayerDetail[];
199
220
  readonly phase: CadSessionPhase | null;
200
221
  }
222
+ /** Live HUD model. Subscribe to `hud-change` before reading it reactively. */
223
+ export interface CadHudModel extends CadHudSnapshot {
224
+ }
201
225
  export interface CadViewerEventMap {
202
226
  'document-change': {
203
227
  document: CadDocumentHandle | null;
@@ -217,6 +241,55 @@ export interface CadViewerEventMap {
217
241
  'load-timing': {
218
242
  timing: CadSessionLoadTiming;
219
243
  };
244
+ 'load-progress': CadLoadProgress;
245
+ error: CadViewerErrorEvent;
246
+ }
247
+ export interface CadLoadProgress {
248
+ readonly phase: CadSessionPhase;
249
+ readonly progress: number;
250
+ readonly pending: number;
251
+ readonly loaded: number;
252
+ readonly settled: boolean;
253
+ }
254
+ 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';
255
+ export interface CadViewerErrorEvent {
256
+ readonly code: CadViewerErrorCode;
257
+ readonly stage: 'source' | 'header' | 'geometry' | 'worker' | 'sidecar' | 'annotation' | 'font' | 'lifecycle';
258
+ readonly fatal: boolean;
259
+ readonly source: CadLoadSource | null;
260
+ readonly error: Error;
261
+ }
262
+ export type CadSelection = {
263
+ readonly kind: 'insert';
264
+ readonly selection: CadInsertSelection;
265
+ } | {
266
+ readonly kind: 'text';
267
+ readonly selection: CadTextSelection;
268
+ };
269
+ export type CadHit = {
270
+ readonly kind: 'insert';
271
+ readonly selection: CadInsertSelection;
272
+ readonly details: CadInsertDetails | null;
273
+ } | {
274
+ readonly kind: 'text';
275
+ readonly selection: CadTextSelection;
276
+ readonly details: CadTextDetails | null;
277
+ };
278
+ export interface CadViewerSnapshot {
279
+ readonly revision: number;
280
+ readonly document: CadDocumentHandle | null;
281
+ readonly layers: {
282
+ readonly rows: readonly CadLayerRow[];
283
+ readonly allColor: number | null;
284
+ readonly revision: number;
285
+ };
286
+ readonly blocks: {
287
+ readonly usages: readonly CadBlockUsageRow[];
288
+ readonly revision: number;
289
+ };
290
+ readonly inspector: CadInspectorSnapshot;
291
+ readonly hud: CadHudSnapshot;
292
+ readonly loadTiming: CadSessionLoadTiming | null;
220
293
  }
221
294
  export interface CadViewerCapability {
222
295
  readonly document: CadDocumentHandle | null;
@@ -225,20 +298,33 @@ export interface CadViewerCapability {
225
298
  readonly inspector: CadInspectorModel;
226
299
  readonly hud: CadHudModel;
227
300
  readonly loadTiming: CadSessionLoadTiming | null;
301
+ readonly capabilities: {
302
+ readonly insertSelection: true;
303
+ readonly textSelection: true;
304
+ readonly insertPreview: true;
305
+ readonly batchVisibility: true;
306
+ };
307
+ /**
308
+ * @deprecated Use `viewer.openAsset(source, { format: 'cad', cad: options })`
309
+ * and read `viewer.cad.document` after `asset.ready`.
310
+ */
228
311
  open(source: CadLoadSource, options?: CadOpenOptions): Promise<CadDocumentHandle>;
312
+ /**
313
+ * @deprecated Use `viewer.closeAsset()` to close the active asset.
314
+ */
229
315
  close(): void;
230
- selectInsert?(insertId: string | null): void;
231
316
  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;
317
+ hitTest(clientX: number, clientY: number, options?: {
318
+ readonly kinds?: readonly ('insert' | 'text')[];
319
+ }): CadHit | null;
320
+ select(selection: CadSelection | null): void;
321
+ clearSelection(): void;
322
+ getSnapshot(): CadViewerSnapshot;
323
+ fitDocument(): boolean;
324
+ fitSelection(): boolean;
237
325
  on<K extends keyof CadViewerEventMap>(type: K, listener: (event: CadViewerEventMap[K]) => void): () => void;
238
326
  }
239
- export type ViewerUiPreset = 'cad-readonly';
240
- export type ViewerUiTheme = 'dark' | 'light';
241
- export interface ViewerUiParts {
327
+ export interface ViewerCadUiParts {
242
328
  layerPanel?: boolean;
243
329
  propertyPanel?: boolean;
244
330
  hud?: boolean;
@@ -252,13 +338,35 @@ export interface ViewerUiParts {
252
338
  /** Show the Debug details card in the infobar. Default: true. */
253
339
  infobarDebug?: boolean;
254
340
  }
255
- export interface ViewerUiOptions {
256
- root: HTMLElement;
257
- preset?: ViewerUiPreset;
258
- parts?: ViewerUiParts;
259
- theme?: ViewerUiTheme;
341
+ /** Options belonging only to the built-in CAD UI group. */
342
+ export interface ViewerCadUiOptions {
343
+ parts?: ViewerCadUiParts;
344
+ persistence?: boolean;
345
+ persistenceKey?: string;
346
+ stateStore?: ViewerCadUiStateStore;
347
+ keyboard?: ViewerCadUiKeyboardOptions;
348
+ unitFormatter?: (value: number, context: {
349
+ readonly unit: string;
350
+ readonly viewMode: ViewerViewMode;
351
+ }) => string;
352
+ }
353
+ export interface ViewerCadUiStateStore {
354
+ read(key: string): string | null;
355
+ write(key: string, value: string): void;
356
+ }
357
+ export interface ViewerCadUiKeyboardOptions {
358
+ enabled?: boolean;
359
+ /**
360
+ * Restrict shortcuts to this element. A pointer interaction inside it focuses
361
+ * the target unless the interaction began in an editable control.
362
+ */
363
+ target?: HTMLElement;
364
+ shortcuts?: {
365
+ /** Key used by the built-in fit command. Defaults to `f`; `false` disables it. */
366
+ fit?: string | false;
367
+ };
260
368
  }
261
369
  export interface CameraStateStore {
262
- read(key: string): Promise<unknown> | unknown;
263
- write(key: string, value: unknown): Promise<void> | void;
370
+ read(key: string): unknown | null;
371
+ write(key: string, value: unknown): void;
264
372
  }