@preference-sl/pref-viewer 2.16.0-beta.14 → 2.16.0-beta.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.
- package/README.md +9 -5
- package/index.d.ts +373 -12
- package/package.json +2 -1
- package/src/babylonjs-controller.js +105 -5
- package/src/panzoom-controller.js +28 -15
- package/src/pref-viewer-2d.js +384 -3
- package/src/pref-viewer.js +79 -11
package/README.md
CHANGED
|
@@ -235,19 +235,23 @@ Storage descriptors are passed through `model.storage`, `scene.storage`, `materi
|
|
|
235
235
|
|
|
236
236
|
| Method | Description |
|
|
237
237
|
| --- | --- |
|
|
238
|
-
| `setSelectableElements(elements)` | Defines
|
|
239
|
-
| `setSelectedElements(elements)` | Defines selected
|
|
240
|
-
| `applySelectionState(state)` | Applies selectable and selected state in one call. Accepts `selectableIds`/`selectables` and `selectedItems`/`selectedIds`/`selected
|
|
238
|
+
| `setSelectableElements(elements)` | Defines and renders selectable SVG ids in yellow (`#FFFF00`, opacity `0.5`). Accepts strings or objects with `id`/`Id`. |
|
|
239
|
+
| `setSelectedElements(elements)` | Defines and renders selected highlights in red (`#FF0000`, opacity `0.5`). Accepts strings or objects with `id`/`Id` and optional `highlights`/`Highlights`. |
|
|
240
|
+
| `applySelectionState(state)` | Applies selectable and selected state to the semantic snapshot and 2D drawing in one call. Accepts `selectableIds`/`selectables` and `selectedItems`/`selectedIds`/`selected`; state supplied before a drawing is reapplied after load. |
|
|
241
241
|
| `clearSelectedElements()` | Clears selected elements while preserving the selectable set. |
|
|
242
242
|
| `clearSelectableElements()` | Clears selectable elements while preserving the selected set. |
|
|
243
243
|
| `clearSelectionState()` | Clears all semantic selection state. |
|
|
244
244
|
| `getSelectionState()` | Returns `{ selectableIds, selectedIds, selectedItems }`. |
|
|
245
|
-
| `notifyElementClick(element)` | Emits `pref-viewer-element-click` from a
|
|
245
|
+
| `notifyElementClick(element)` | Emits `pref-viewer-element-click` with exactly `{ ids: string[] }` from a string, descriptor, collection, or `{ ids }` input. This replaces the provisional `detail.element` payload. |
|
|
246
|
+
| `appendSvgNode(id, svgId, svg)` | Adds a host-owned SVG node to the interactive 2D drawing. The node's identified geometry participates in hit-testing and selection. Returns `false` for a duplicate `svgId` or absent node. |
|
|
247
|
+
| `removeAppendedSvgNodes()` | Removes every SVG node previously added through `appendSvgNode` and reconciles the current visual selection layers. |
|
|
246
248
|
| `startTurnable(options)` | Starts semantic turnable state and emits `pref-viewer-3d-turnable-changed`. |
|
|
247
249
|
| `stopTurnable()` | Stops semantic turnable state. |
|
|
248
250
|
| `toggleTurnable(options)` | Toggles semantic turnable state. |
|
|
249
251
|
| `getTurnableState()` | Returns `{ running, speed, direction }`. |
|
|
250
252
|
|
|
253
|
+
Selection colors are rendered through disposable SVG clones; the loaded base SVG and shared definitions are not modified. Selectable clones are inserted before selected clones so red selection remains visible above yellow availability. Reapplying state is idempotent, and replacing the drawing removes dynamic nodes and prior visual layers before the current semantic snapshot is rendered again.
|
|
254
|
+
|
|
251
255
|
### Animation controls
|
|
252
256
|
|
|
253
257
|
| Method | Description |
|
|
@@ -309,7 +313,7 @@ The component emits custom events so host applications can integrate loading ind
|
|
|
309
313
|
| `pref-viewer-loaded` | `<pref-viewer>` | None | A 2D or 3D load operation completed. Includes `detail.mode` with the current root mode. |
|
|
310
314
|
| `pref-viewer-error` | `<pref-viewer>` | None | A 2D or 3D operation failed. Includes `detail.mode` with the current root mode. |
|
|
311
315
|
| `pref-viewer-selection-state-changed` | `<pref-viewer>` | None | The semantic selection state changed. |
|
|
312
|
-
| `pref-viewer-element-click` | `<pref-viewer>` | None | A
|
|
316
|
+
| `pref-viewer-element-click` | `<pref-viewer>` | None | A semantic SVG element was clicked or forwarded by the host. Geometry without its own id resolves the nearest id-bearing ancestor inside the drawing. `detail` is exactly `{ ids: string[] }`; clicks are emitted independently of the current selectable set. |
|
|
313
317
|
|
|
314
318
|
### 3D Events
|
|
315
319
|
|
package/index.d.ts
CHANGED
|
@@ -1,24 +1,385 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
|
|
3
|
+
/** Render mode. `loadConfig`/`loadDrawing` pick the matching one on their own. */
|
|
4
|
+
export type PrefViewerMode = "2d" | "3d";
|
|
5
|
+
|
|
6
|
+
/** Animation state as reported by `getAnimations()` and `prefviewer-animation-changed`. */
|
|
7
|
+
export type PrefViewerAnimationState = 0 | 1 | 2 | 3 | 4;
|
|
8
|
+
|
|
9
|
+
/** Action accepted by `playAnimation`. */
|
|
10
|
+
export type PrefViewerAnimationAction = "open" | "close" | "pause" | "goToOpened" | "goToClosed";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An IndexedDB pointer. The SDK writes the asset and hands you the record id;
|
|
14
|
+
* the viewer reads it back itself, so you never fetch or decode the bytes.
|
|
15
|
+
*/
|
|
16
|
+
export interface PrefViewerStoragePointer {
|
|
17
|
+
db: string;
|
|
18
|
+
table: string;
|
|
19
|
+
id: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A storage pointer or a plain URL — every asset slot accepts either. */
|
|
23
|
+
export type PrefViewerSource = PrefViewerStoragePointer | { url: string };
|
|
24
|
+
|
|
25
|
+
export interface PrefViewerAsset {
|
|
26
|
+
storage: PrefViewerSource;
|
|
27
|
+
visible?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Payload for `loadConfig`. Pass the object; a JSON string is also accepted. */
|
|
31
|
+
export interface PrefViewerConfig {
|
|
32
|
+
model?: PrefViewerAsset;
|
|
33
|
+
scene?: PrefViewerAsset;
|
|
34
|
+
materials?: PrefViewerAsset;
|
|
35
|
+
drawing?: PrefViewerAsset;
|
|
36
|
+
options?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PrefViewerAnimation {
|
|
40
|
+
name: string;
|
|
41
|
+
state: PrefViewerAnimationState;
|
|
42
|
+
progress: number;
|
|
43
|
+
loop: boolean;
|
|
44
|
+
nodes: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `<pref-viewer>` shadows the standard `Element.getAnimations()` with its own snapshot of the
|
|
49
|
+
* model's opening animations. The two signatures are incompatible, so the method is not declared
|
|
50
|
+
* on `PrefViewer` — reach it through this view:
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const animations = (viewer as unknown as PrefViewerAnimationsApi).getAnimations()
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export interface PrefViewerAnimationsApi {
|
|
57
|
+
getAnimations(): PrefViewerAnimation[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Semantic selection and turnable state ──
|
|
61
|
+
|
|
62
|
+
export interface PrefViewerSelectionItem {
|
|
63
|
+
id: string;
|
|
64
|
+
highlights: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface PrefViewerSelectionDescriptor {
|
|
68
|
+
id?: string;
|
|
69
|
+
Id?: string;
|
|
70
|
+
highlights?: string[];
|
|
71
|
+
Highlights?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface PrefViewerSelectionState {
|
|
75
|
+
selectableIds: string[];
|
|
76
|
+
selectedIds: string[];
|
|
77
|
+
selectedItems: PrefViewerSelectionItem[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type PrefViewerElementDescriptor = string | { id?: string; Id?: string };
|
|
81
|
+
|
|
82
|
+
export interface PrefViewerSelectionInput {
|
|
83
|
+
selectableIds?: PrefViewerElementDescriptor[];
|
|
84
|
+
selectables?: PrefViewerElementDescriptor[];
|
|
85
|
+
selectedItems?: Array<string | PrefViewerSelectionDescriptor>;
|
|
86
|
+
selectedIds?: PrefViewerElementDescriptor[];
|
|
87
|
+
selected?: Array<string | PrefViewerSelectionDescriptor>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PrefViewerElementClickDetail {
|
|
91
|
+
ids: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Optional speed/direction for `startTurnable` / `toggleTurnable`. */
|
|
95
|
+
export interface PrefViewerTurnableOptions {
|
|
96
|
+
speed?: number;
|
|
97
|
+
direction?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface PrefViewerTurnableState {
|
|
101
|
+
running: boolean;
|
|
102
|
+
speed: number;
|
|
103
|
+
direction: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Event details ──
|
|
107
|
+
|
|
108
|
+
/** Which containers a load touched, and which of them succeeded. */
|
|
109
|
+
export interface PrefViewerContainerFlags {
|
|
110
|
+
model?: boolean;
|
|
111
|
+
scene?: boolean;
|
|
112
|
+
materials?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface PrefViewerSceneLoadedDetail {
|
|
116
|
+
tried: PrefViewerContainerFlags;
|
|
117
|
+
success: PrefViewerContainerFlags;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PrefViewerSceneOptionsDetail {
|
|
121
|
+
success: boolean;
|
|
122
|
+
detail: PrefViewerSceneLoadedDetail;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface PrefViewerDrawingLoadedDetail {
|
|
126
|
+
success: boolean;
|
|
127
|
+
error: Error | null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PrefViewerErrorDetail {
|
|
131
|
+
error: Error;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface PrefViewerAnimationChangedDetail {
|
|
135
|
+
name: string;
|
|
136
|
+
state: PrefViewerAnimationState;
|
|
137
|
+
progress: number;
|
|
138
|
+
loop: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface PrefViewerInitializedDetail {
|
|
142
|
+
isInitialized: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The 2D pan/zoom viewport, as reported after every pan or zoom. */
|
|
146
|
+
export interface PrefViewerViewportChangedDetail {
|
|
147
|
+
x: number;
|
|
148
|
+
y: number;
|
|
149
|
+
scale: number;
|
|
150
|
+
moved: boolean;
|
|
151
|
+
scaled: boolean;
|
|
152
|
+
maximized: boolean;
|
|
153
|
+
minimized: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Root-scoped events (`pref-viewer-loading` / `-loaded` / `-error`) fire for both modes and
|
|
158
|
+
* carry the mode they happened in; the 2D- and 3D-scoped events do not.
|
|
159
|
+
*/
|
|
160
|
+
export interface PrefViewerRootLoadingDetail {
|
|
161
|
+
mode: PrefViewerMode;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface PrefViewerRootLoadedDetail {
|
|
165
|
+
mode: PrefViewerMode;
|
|
166
|
+
result: PrefViewerSceneLoadedDetail | PrefViewerDrawingLoadedDetail | null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface PrefViewerRootErrorDetail {
|
|
170
|
+
mode: PrefViewerMode;
|
|
171
|
+
error: Error | null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Events dispatched by `<pref-viewer>`. All bubble and are composed, so a listener
|
|
176
|
+
* on an ancestor outside the shadow root still receives them.
|
|
177
|
+
*
|
|
178
|
+
* The `scene-*` / `drawing-*` names are the legacy aliases: each is still emitted
|
|
179
|
+
* alongside its `pref-viewer-3d-*` / `pref-viewer-2d-*` equivalent, with the same detail.
|
|
180
|
+
*/
|
|
181
|
+
export interface PrefViewerEventMap {
|
|
182
|
+
"pref-viewer-initialized": CustomEvent<PrefViewerInitializedDetail>;
|
|
183
|
+
|
|
184
|
+
// Root scope — either mode, and the detail says which.
|
|
185
|
+
"pref-viewer-loading": CustomEvent<PrefViewerRootLoadingDetail>;
|
|
186
|
+
"pref-viewer-loaded": CustomEvent<PrefViewerRootLoadedDetail>;
|
|
187
|
+
"pref-viewer-error": CustomEvent<PrefViewerRootErrorDetail>;
|
|
188
|
+
|
|
189
|
+
// 3D scope (and its legacy aliases).
|
|
190
|
+
"pref-viewer-3d-loading": CustomEvent<undefined>;
|
|
191
|
+
"pref-viewer-3d-loaded": CustomEvent<PrefViewerSceneLoadedDetail | Record<string, never>>;
|
|
192
|
+
"pref-viewer-3d-error": CustomEvent<PrefViewerErrorDetail | Record<string, never>>;
|
|
193
|
+
"pref-viewer-3d-turnable-changed": CustomEvent<{ state: PrefViewerTurnableState }>;
|
|
194
|
+
"scene-loading": CustomEvent<undefined>;
|
|
195
|
+
"scene-loaded": CustomEvent<PrefViewerSceneLoadedDetail | undefined>;
|
|
196
|
+
"scene-error": CustomEvent<PrefViewerErrorDetail>;
|
|
197
|
+
|
|
198
|
+
// 2D scope (and its legacy aliases).
|
|
199
|
+
"pref-viewer-2d-loading": CustomEvent<undefined>;
|
|
200
|
+
"pref-viewer-2d-loaded": CustomEvent<PrefViewerDrawingLoadedDetail | Record<string, never>>;
|
|
201
|
+
"pref-viewer-2d-error": CustomEvent<PrefViewerErrorDetail | Record<string, never>>;
|
|
202
|
+
"pref-viewer-2d-viewport-changed": CustomEvent<PrefViewerViewportChangedDetail>;
|
|
203
|
+
"pref-viewer-2d-element-click": CustomEvent<PrefViewerElementClickDetail>;
|
|
204
|
+
"drawing-loading": CustomEvent<undefined>;
|
|
205
|
+
"drawing-loaded": CustomEvent<PrefViewerDrawingLoadedDetail>;
|
|
206
|
+
"drawing-error": CustomEvent<PrefViewerErrorDetail>;
|
|
207
|
+
"drawing-zoom-changed": CustomEvent<{ zoom: number }>;
|
|
208
|
+
|
|
209
|
+
// Semantic selection.
|
|
210
|
+
"pref-viewer-element-click": CustomEvent<PrefViewerElementClickDetail>;
|
|
211
|
+
"pref-viewer-selection-state-changed": CustomEvent<PrefViewerSelectionState>;
|
|
212
|
+
|
|
213
|
+
/** Re-dispatched from the 3D component; not emitted by the root element. */
|
|
214
|
+
"prefviewer-animation-changed": CustomEvent<PrefViewerAnimationChangedDetail>;
|
|
215
|
+
}
|
|
216
|
+
|
|
3
217
|
export declare class PrefViewer extends HTMLElement {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
set
|
|
218
|
+
// ── Property setters (each mirrors the attribute of the same name) ──
|
|
219
|
+
set config(v: PrefViewerConfig | string | null);
|
|
220
|
+
set culture(v: string | null);
|
|
221
|
+
set drawing(v: PrefViewerAsset | string | null);
|
|
222
|
+
set materials(v: PrefViewerAsset | string | null);
|
|
223
|
+
set mode(v: PrefViewerMode | null);
|
|
224
|
+
/** The URL or storage pointer of the glTF model. */
|
|
225
|
+
set model(v: PrefViewerAsset | string | null);
|
|
226
|
+
set options(v: Record<string, unknown> | string | null);
|
|
227
|
+
set scene(v: PrefViewerAsset | string | null);
|
|
228
|
+
/** Supported style overrides; currently `{ colorPrimary }`. */
|
|
229
|
+
set styles(v: Record<string, unknown> | string | null);
|
|
230
|
+
|
|
231
|
+
// ── Read-only state ──
|
|
232
|
+
get culture(): string;
|
|
233
|
+
get isInitialized(): boolean;
|
|
234
|
+
get isLoaded(): boolean;
|
|
235
|
+
get isLoading(): boolean;
|
|
236
|
+
|
|
237
|
+
// ── Loading ──
|
|
238
|
+
/** Returns false when the config is unusable; otherwise queues the work. */
|
|
239
|
+
loadConfig(config: PrefViewerConfig | string): boolean | void;
|
|
240
|
+
loadDrawing(drawing: PrefViewerAsset | string): boolean | void;
|
|
241
|
+
loadMaterials(materials: PrefViewerAsset | string): boolean | void;
|
|
242
|
+
loadModel(model: PrefViewerAsset | string): boolean | void;
|
|
243
|
+
loadScene(scene: PrefViewerAsset | string): boolean | void;
|
|
244
|
+
|
|
245
|
+
// ── Runtime ──
|
|
246
|
+
setCulture(cultureId: string): void;
|
|
247
|
+
setMode(mode?: PrefViewerMode): void;
|
|
248
|
+
/** Cheaper than a full `loadConfig` when only options changed — no visible reload. */
|
|
249
|
+
setOptions(options: Record<string, unknown> | string): void;
|
|
250
|
+
/** Convenience wrapper for `setOptions({ ibl })`. False when the payload is unusable. */
|
|
251
|
+
setIBL(ibl: Record<string, unknown> | string): boolean | void;
|
|
252
|
+
/** Runtime style overrides; currently `{ colorPrimary }`. False when unusable. */
|
|
253
|
+
setStyles(styles: Record<string, unknown> | string): boolean | void;
|
|
254
|
+
/** 3D runtime diagnostics, or null before the 3D component exists. */
|
|
255
|
+
getDebugInfo(): Record<string, unknown> | null;
|
|
256
|
+
|
|
257
|
+
// ── Visibility and framing ──
|
|
258
|
+
showModel(): void;
|
|
259
|
+
hideModel(): void;
|
|
260
|
+
showScene(): void;
|
|
261
|
+
hideScene(): void;
|
|
262
|
+
zoomCenter(): void;
|
|
263
|
+
zoomExtentsAll(): void;
|
|
264
|
+
zoomIn(): void;
|
|
265
|
+
zoomOut(): void;
|
|
266
|
+
|
|
267
|
+
// ── Semantic selection ──
|
|
268
|
+
setSelectableElements(elements?: PrefViewerElementDescriptor[]): PrefViewerSelectionState;
|
|
269
|
+
setSelectedElements(elements?: Array<string | PrefViewerSelectionDescriptor>): PrefViewerSelectionState;
|
|
270
|
+
applySelectionState(state?: PrefViewerSelectionInput): PrefViewerSelectionState;
|
|
271
|
+
clearSelectedElements(): PrefViewerSelectionState;
|
|
272
|
+
clearSelectableElements(): PrefViewerSelectionState;
|
|
273
|
+
clearSelectionState(): PrefViewerSelectionState;
|
|
274
|
+
getSelectionState(): PrefViewerSelectionState;
|
|
275
|
+
notifyElementClick(
|
|
276
|
+
element: PrefViewerElementDescriptor | PrefViewerElementDescriptor[] | { ids: PrefViewerElementDescriptor[] },
|
|
277
|
+
): void;
|
|
278
|
+
appendSvgNode(id: string, svgId: string, svg: SVGElement): boolean;
|
|
279
|
+
removeAppendedSvgNodes(): void;
|
|
280
|
+
|
|
281
|
+
// ── Turnable (auto-rotate) state ──
|
|
282
|
+
startTurnable(options?: PrefViewerTurnableOptions): PrefViewerTurnableState;
|
|
283
|
+
stopTurnable(): PrefViewerTurnableState;
|
|
284
|
+
toggleTurnable(options?: PrefViewerTurnableOptions): PrefViewerTurnableState;
|
|
285
|
+
getTurnableState(): PrefViewerTurnableState;
|
|
286
|
+
|
|
287
|
+
// ── Dialog ──
|
|
288
|
+
openDialog(title: string, content: string | Node, footer?: string | Node): void;
|
|
289
|
+
closeDialog(): void;
|
|
290
|
+
|
|
291
|
+
// ── Animations (3D only) ──
|
|
292
|
+
// `getAnimations()` is deliberately absent here: the component shadows the standard
|
|
293
|
+
// `Element.getAnimations()` with an incompatible return, and TypeScript cannot model an
|
|
294
|
+
// incompatible override without making PrefViewer unassignable to HTMLElement. Read it
|
|
295
|
+
// through PrefViewerAnimationsApi instead. Renaming the runtime method would fix this
|
|
296
|
+
// properly, but that is a breaking change for the component's API (TEC-3490).
|
|
297
|
+
/** False when no animation of that name exists. */
|
|
298
|
+
playAnimation(name: string, action: PrefViewerAnimationAction): boolean;
|
|
299
|
+
setAnimationProgress(name: string, progress: number): boolean;
|
|
300
|
+
setAnimationLoop(name: string, loop: boolean): boolean;
|
|
301
|
+
|
|
302
|
+
// ── Downloads ──
|
|
303
|
+
downloadModelGLB(): void;
|
|
304
|
+
downloadModelGLTF(): void;
|
|
305
|
+
downloadModelUSDZ(): void;
|
|
306
|
+
downloadSceneGLB(): void;
|
|
307
|
+
downloadSceneGLTF(): void;
|
|
308
|
+
downloadSceneUSDZ(): void;
|
|
309
|
+
downloadModelAndSceneGLB(): void;
|
|
310
|
+
downloadModelAndSceneGLTF(): void;
|
|
311
|
+
downloadModelAndSceneUSDZ(): void;
|
|
312
|
+
|
|
313
|
+
addEventListener<K extends keyof PrefViewerEventMap>(
|
|
314
|
+
type: K,
|
|
315
|
+
listener: (this: PrefViewer, ev: PrefViewerEventMap[K]) => unknown,
|
|
316
|
+
options?: boolean | AddEventListenerOptions,
|
|
317
|
+
): void;
|
|
318
|
+
addEventListener(
|
|
319
|
+
type: string,
|
|
320
|
+
listener: EventListenerOrEventListenerObject,
|
|
321
|
+
options?: boolean | AddEventListenerOptions,
|
|
322
|
+
): void;
|
|
323
|
+
removeEventListener<K extends keyof PrefViewerEventMap>(
|
|
324
|
+
type: K,
|
|
325
|
+
listener: (this: PrefViewer, ev: PrefViewerEventMap[K]) => unknown,
|
|
326
|
+
options?: boolean | EventListenerOptions,
|
|
327
|
+
): void;
|
|
328
|
+
removeEventListener(
|
|
329
|
+
type: string,
|
|
330
|
+
listener: EventListenerOrEventListenerObject,
|
|
331
|
+
options?: boolean | EventListenerOptions,
|
|
332
|
+
): void;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The observed attributes. Structured payloads (`config`, `model`, `scene`,
|
|
337
|
+
* `materials`, `drawing`, `options`, `styles`) are JSON strings when written as attributes —
|
|
338
|
+
* pass the objects through the matching method or property instead.
|
|
339
|
+
*/
|
|
340
|
+
export interface PrefViewerAttributes {
|
|
341
|
+
config?: string;
|
|
342
|
+
culture?: string;
|
|
343
|
+
drawing?: string;
|
|
344
|
+
materials?: string;
|
|
345
|
+
mode?: PrefViewerMode;
|
|
346
|
+
model?: string;
|
|
347
|
+
scene?: string;
|
|
348
|
+
options?: string;
|
|
349
|
+
styles?: string;
|
|
350
|
+
"show-model"?: boolean | "true" | "false";
|
|
351
|
+
"show-scene"?: boolean | "true" | "false";
|
|
352
|
+
"show-render-menu"?: boolean | "true" | "false";
|
|
8
353
|
}
|
|
9
354
|
|
|
355
|
+
/** <pref-viewer mode="3d" style="display:block; width:100%; height:600px" /> */
|
|
356
|
+
type PrefViewerJsxProps = React.DetailedHTMLProps<
|
|
357
|
+
React.HTMLAttributes<PrefViewer> & PrefViewerAttributes,
|
|
358
|
+
PrefViewer
|
|
359
|
+
>;
|
|
360
|
+
|
|
10
361
|
declare global {
|
|
362
|
+
interface HTMLElementTagNameMap {
|
|
363
|
+
"pref-viewer": PrefViewer;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// React <= 18 (@types/react 18) resolves intrinsics through the global JSX namespace.
|
|
367
|
+
namespace JSX {
|
|
368
|
+
interface IntrinsicElements {
|
|
369
|
+
"pref-viewer": PrefViewerJsxProps;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// React 19 (@types/react 19) dropped the global JSX namespace: intrinsics resolve through
|
|
375
|
+
// React.JSX instead. Declaring only the global one above leaves the element unknown on any
|
|
376
|
+
// modern setup, which is why `<pref-viewer mode="3d" />` failed to compile (TEC-3490).
|
|
377
|
+
declare module "react" {
|
|
11
378
|
namespace JSX {
|
|
12
379
|
interface IntrinsicElements {
|
|
13
|
-
|
|
14
|
-
* <pref-viewer model="…" />
|
|
15
|
-
*/
|
|
16
|
-
"pref-viewer": React.DetailedHTMLProps<
|
|
17
|
-
React.HTMLAttributes<HTMLElement> & { model?: string },
|
|
18
|
-
HTMLElement
|
|
19
|
-
>;
|
|
380
|
+
"pref-viewer": PrefViewerJsxProps;
|
|
20
381
|
}
|
|
21
382
|
}
|
|
22
383
|
}
|
|
23
384
|
|
|
24
|
-
export {};
|
|
385
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.16.0-beta.
|
|
3
|
+
"version": "2.16.0-beta.18",
|
|
4
4
|
"description": "Web Component to preview GLTF models with Babylon.js",
|
|
5
5
|
"author": "Alex Moreno Palacio <amoreno@preference.es>",
|
|
6
6
|
"scripts": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"types": "index.d.ts",
|
|
26
26
|
"exports": {
|
|
27
27
|
".": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
28
29
|
"import": "./src/index.js",
|
|
29
30
|
"require": "./src/index.js"
|
|
30
31
|
}
|
|
@@ -192,6 +192,11 @@ export default class BabylonJSController {
|
|
|
192
192
|
resize: {
|
|
193
193
|
throttleMs: 50, // Cap resize work to ~20 Hz while dragging/resizing containers
|
|
194
194
|
},
|
|
195
|
+
defaultCameraFraming: {
|
|
196
|
+
margin: 1.25, // Breathing room so the model doesn't touch the viewport edges
|
|
197
|
+
minRadiusFactor: 0.25, // Zoom in on detail
|
|
198
|
+
maxRadiusFactor: 4, // Pull back past the whole model
|
|
199
|
+
},
|
|
195
200
|
// Hardware-adaptive quality settings (applied based on GPU capabilities)
|
|
196
201
|
quality: {
|
|
197
202
|
// Settings for different hardware tiers: 'low', 'medium', 'high'
|
|
@@ -213,6 +218,24 @@ export default class BabylonJSController {
|
|
|
213
218
|
hemiLightIntensity: 0.52,
|
|
214
219
|
dirLightIntensity: 0.56,
|
|
215
220
|
cameraLightIntensity: 0.16,
|
|
221
|
+
// Daylight background, in the LINEAR domain the tonemapper consumes — NOT sRGB.
|
|
222
|
+
//
|
|
223
|
+
// The clear colour goes through the same exposure + ACES + gamma chain as the
|
|
224
|
+
// render, so the value that reads correctly here is not the value that reaches the
|
|
225
|
+
// screen. The previous literal was an sRGB 0.98 ("almost white") and it arrived as
|
|
226
|
+
// #C0C0C0, a mid grey.
|
|
227
|
+
//
|
|
228
|
+
// This one is solved against the ACES fit at exposure 0.92 and verified on screen:
|
|
229
|
+
// a light studio grey, #D9D9D9. Other targets, if this needs revisiting —
|
|
230
|
+
// #F4F3F0 warm off-white (matches the host app's 2D/PR surface) -> 4.289 3.991 3.304
|
|
231
|
+
// #D9D9D9 light studio (current) -> 1.424
|
|
232
|
+
// #CCCCCC neutral studio -> 1.112
|
|
233
|
+
// #B3B3B3 darker -> 0.688
|
|
234
|
+
// Note how steeply the required input climbs near white: that is the ACES shoulder,
|
|
235
|
+
// and it is why a near-white background needs a value well above 1.
|
|
236
|
+
//
|
|
237
|
+
// Re-solve these if exposure, contrast or the tonemapping curve change.
|
|
238
|
+
backgroundDay: [1.424, 1.424, 1.424],
|
|
216
239
|
},
|
|
217
240
|
},
|
|
218
241
|
};
|
|
@@ -1637,7 +1660,8 @@ export default class BabylonJSController {
|
|
|
1637
1660
|
const showroom = this.#config.look?.showroom || {};
|
|
1638
1661
|
const imageProcessing = this.#scene.imageProcessingConfiguration;
|
|
1639
1662
|
|
|
1640
|
-
|
|
1663
|
+
const bg = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1664
|
+
this.#scene.clearColor = new Color4(bg[0], bg[1], bg[2], 1);
|
|
1641
1665
|
imageProcessing.exposure = showroom.exposure ?? 0.95;
|
|
1642
1666
|
imageProcessing.contrast = showroom.contrast ?? 1.08;
|
|
1643
1667
|
imageProcessing.toneMappingEnabled = showroom.toneMappingEnabled ?? true;
|
|
@@ -1677,12 +1701,15 @@ export default class BabylonJSController {
|
|
|
1677
1701
|
imageProcessing.colorCurvesEnabled = false;
|
|
1678
1702
|
}
|
|
1679
1703
|
|
|
1704
|
+
// Night end stays in low linear values (a dark blue sky); the day end is the calibrated
|
|
1705
|
+
// HDR off-white. Both are linear already, so no toLinearSpace() on either.
|
|
1706
|
+
const bgDay = showroom.backgroundDay ?? this.#config.look.showroom.backgroundDay;
|
|
1680
1707
|
this.#scene.clearColor = new Color4(
|
|
1681
|
-
lerp(0.
|
|
1682
|
-
lerp(0.
|
|
1683
|
-
lerp(0.
|
|
1708
|
+
lerp(0.000627, bgDay[0], daylight),
|
|
1709
|
+
lerp(0.001089, bgDay[1], daylight),
|
|
1710
|
+
lerp(0.004413, bgDay[2], daylight),
|
|
1684
1711
|
1,
|
|
1685
|
-
)
|
|
1712
|
+
);
|
|
1686
1713
|
|
|
1687
1714
|
if ("environmentIntensity" in this.#scene) {
|
|
1688
1715
|
this.#scene.environmentIntensity = lerp(0.42, showroom.environmentIntensity ?? 1.08, peakLight);
|
|
@@ -1754,6 +1781,78 @@ export default class BabylonJSController {
|
|
|
1754
1781
|
this.#engine.setHardwareScalingLevel(scalingLevel);
|
|
1755
1782
|
}
|
|
1756
1783
|
|
|
1784
|
+
/**
|
|
1785
|
+
* World-space bounds of the loaded model's renderable geometry.
|
|
1786
|
+
* @private
|
|
1787
|
+
* @returns {{center: Vector3, radius: number}|null} Centre and bounding-sphere radius, or null when nothing renderable is loaded.
|
|
1788
|
+
*/
|
|
1789
|
+
#getModelBounds() {
|
|
1790
|
+
const meshes = (this.#containers?.model?.assetContainer?.meshes ?? []).filter((mesh) => (mesh?.getTotalVertices?.() ?? 0) > 0);
|
|
1791
|
+
if (meshes.length === 0) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
let min = null;
|
|
1796
|
+
let max = null;
|
|
1797
|
+
meshes.forEach((mesh) => {
|
|
1798
|
+
mesh.computeWorldMatrix(true);
|
|
1799
|
+
const box = mesh.getBoundingInfo?.()?.boundingBox;
|
|
1800
|
+
if (!box) {
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
min = min ? Vector3.Minimize(min, box.minimumWorld) : box.minimumWorld.clone();
|
|
1804
|
+
max = max ? Vector3.Maximize(max, box.maximumWorld) : box.maximumWorld.clone();
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
if (!min || !max) {
|
|
1808
|
+
return null;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
const radius = Vector3.Distance(min, max) / 2;
|
|
1812
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return { center: Vector3.Center(min, max), radius };
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
/**
|
|
1820
|
+
* Frames the default camera on the loaded model: aims it at the model's centre and pulls it
|
|
1821
|
+
* back far enough for the whole model to fit the viewport.
|
|
1822
|
+
*
|
|
1823
|
+
* Only ever applied to the viewer's own camera — a camera authored in the model or the scene
|
|
1824
|
+
* is a composition choice and is left untouched. Orbit angles are preserved so a model reload
|
|
1825
|
+
* (an option or dimension change) re-centres without snapping the user's viewpoint back.
|
|
1826
|
+
* @private
|
|
1827
|
+
* @param {Camera|null|undefined} camera - The camera about to become active.
|
|
1828
|
+
* @returns {void}
|
|
1829
|
+
*/
|
|
1830
|
+
#fitDefaultCameraToModel(camera) {
|
|
1831
|
+
if (camera !== this.#camera || !(camera instanceof ArcRotateCamera)) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
const bounds = this.#getModelBounds();
|
|
1836
|
+
if (!bounds) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
// fovMode is FOVMODE_HORIZONTAL_FIXED (see #applyShowroomCameraFraming), so camera.fov is
|
|
1841
|
+
// the horizontal angle and the vertical one follows the aspect ratio. Fitting to the
|
|
1842
|
+
// horizontal angle alone clips the model top and bottom on any viewport wider than tall.
|
|
1843
|
+
const aspect = this.#engine?.getAspectRatio?.(camera) || 1;
|
|
1844
|
+
const horizontalFov = camera.fov;
|
|
1845
|
+
const verticalFov = 2 * Math.atan(Math.tan(horizontalFov / 2) / aspect);
|
|
1846
|
+
const limitingFov = Math.min(horizontalFov, verticalFov);
|
|
1847
|
+
const distance = (bounds.radius / Math.sin(limitingFov / 2)) * this.#config.defaultCameraFraming.margin;
|
|
1848
|
+
|
|
1849
|
+
camera.setTarget(bounds.center);
|
|
1850
|
+
camera.radius = distance;
|
|
1851
|
+
// The 5-20 m defaults were sized for a room; left in place they clamp the fit itself.
|
|
1852
|
+
camera.lowerRadiusLimit = distance * this.#config.defaultCameraFraming.minRadiusFactor;
|
|
1853
|
+
camera.upperRadiusLimit = distance * this.#config.defaultCameraFraming.maxRadiusFactor;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1757
1856
|
/**
|
|
1758
1857
|
* Keeps the showroom camera framing stable when the viewport aspect ratio changes.
|
|
1759
1858
|
* The 3D showroom is meant to feel compositionally stable, so we prefer a horizontal-fixed
|
|
@@ -2328,6 +2427,7 @@ export default class BabylonJSController {
|
|
|
2328
2427
|
}
|
|
2329
2428
|
}
|
|
2330
2429
|
this.#applyShowroomCameraFraming(camera);
|
|
2430
|
+
this.#fitDefaultCameraToModel(camera);
|
|
2331
2431
|
this.#scene.activeCamera?.detachControl();
|
|
2332
2432
|
camera.detachControl();
|
|
2333
2433
|
if (!cameraState.locked) {
|