@virtualdisplay.io/client 4.0.0-alpha.25 → 4.0.0-alpha.27

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
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.0-alpha.27
4
+
5
+ ### Minor Changes
6
+
7
+ Add the per-app annotation visibility modes to the `viewerSettings.annotations` override: `'hide'`, `'show'`, `'toggle-shown'` and `'toggle-hidden'`, besides the existing booleans (`true` maps to `'show'`, `false` to `'hide'`). Markers now follow the mode configured on the app, and an override only speaks up when it differs from that mode.
8
+
9
+ ## 4.0.0-alpha.26
10
+
11
+ ### Minor Changes
12
+
13
+ Add `app` option to `ClientOptions` as an alternative to the `license` and `model` pair, mounting one specific app by its link key. The pair keeps working unchanged; when both are supplied, `app` wins and a warning is logged that the pair was ignored.
14
+
15
+ Add `AppType` with the known app types. `ResolvedAppInfo.appType` is typed as a plain string so an app type newer than your bundle still reaches `onAppResolved` for explicit handling.
16
+
17
+ Add a measurement slider for size attributes. A model that offers a size (width, depth, diameter) now renders it as a slider with the formatted value and unit beside it, and the model follows the drag live while only the released value counts as the chosen size.
18
+
19
+ Add `slug` to the `onAppResolved` callback payload: the resolved app's public slug, so a host page can address the app it embeds without knowing the slug in advance.
20
+
21
+ Add `onAppResolved(callback)`. The viewer reports which app it resolved (`appType`, `name`, and public submit settings as `ResolvedAppInfo`), and the callback fires immediately when the report already arrived.
22
+
3
23
  ## 4.0.0-alpha.25
4
24
 
5
25
  ### Minor Changes
package/README.md CHANGED
@@ -55,6 +55,26 @@ The server defines which attributes and values are available for each model.
55
55
  The client receives this configuration automatically via postMessage. No
56
56
  manual mapping required.
57
57
 
58
+ ### Mounting a specific app
59
+
60
+ A model can have several apps (a plain viewer, a configurator), each with its
61
+ own link and its own configuration. Pass the app's link key as `app` to mount
62
+ that exact app:
63
+
64
+ ```typescript
65
+ import { VirtualdisplayClient } from '@virtualdisplay.io/client';
66
+
67
+ const client = new VirtualdisplayClient({
68
+ parent: '#product-container',
69
+ app: 'sofa-deluxe-k3f9dm',
70
+ });
71
+ ```
72
+
73
+ `license` + `model` keeps working unchanged and shows the model's default
74
+ viewer. If both forms are supplied, `app` wins and a warning is logged that the
75
+ pair was ignored, so adding `app` to an existing integration is additive. One
76
+ of the two forms is required; supplying neither is a compile error.
77
+
58
78
  ## How it works
59
79
 
60
80
  The 3D server hosts the viewer in an iframe, keeping WebGL complexity isolated
@@ -229,7 +249,7 @@ exposes an API to toggle their visibility.
229
249
 
230
250
  ```typescript
231
251
  client.annotations.setEnabled(false); // Hide all markers
232
- client.annotations.setEnabled(true); // Show them again
252
+ client.annotations.setEnabled(true); // Show them again
233
253
  ```
234
254
 
235
255
  Visibility can also be controlled via `viewerSettings.annotations` (see below).
@@ -243,15 +263,15 @@ settings configured in the Virtualdisplay portal.
243
263
 
244
264
  ### Available settings
245
265
 
246
- | Field | Type | Effect when `false` |
247
- | ------------------- | --------- | ---------------------------------------------------------------------------------------------- |
248
- | `ar` | `boolean` | Hides the AR button |
249
- | `fullscreen` | `boolean` | Hides the fullscreen button |
250
- | `screenshot` | `boolean` | Hides the screenshot button |
251
- | `cameraControls` | `boolean` | Hides the camera control buttons |
252
- | `annotations` | `boolean` | Hides all annotation markers |
253
- | `language` | `string` | Sets the viewer UI language (BCP 47, e.g. `'nl'`, `'en'`, `'de'`) |
254
- | `configuratorLayout`| `string` | Where the attribute controls sit, or whether they show: `'inframe-bottom-left'` (default), `'sidebar-right'` (a docked, scrollable right-hand panel), or `'hidden'` (no built-in selector, `client.select()` and `client.onChange()` keep working). |
266
+ | Field | Type | Effect when `false` |
267
+ | -------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
268
+ | `ar` | `boolean` | Hides the AR button |
269
+ | `fullscreen` | `boolean` | Hides the fullscreen button |
270
+ | `screenshot` | `boolean` | Hides the screenshot button |
271
+ | `cameraControls` | `boolean` | Hides the camera control buttons |
272
+ | `annotations` | `boolean` | Hides all annotation markers |
273
+ | `language` | `string` | Sets the viewer UI language (BCP 47, e.g. `'nl'`, `'en'`, `'de'`) |
274
+ | `configuratorLayout` | `string` | Where the attribute controls sit, or whether they show: `'inframe-bottom-left'` (default), `'sidebar-right'` (a docked, scrollable right-hand panel), or `'hidden'` (no built-in selector, `client.select()` and `client.onChange()` keep working). |
255
275
 
256
276
  ### Overriding settings
257
277
 
@@ -290,7 +310,10 @@ automatically.
290
310
  ```typescript
291
311
  client.onReady(() => {
292
312
  const values = client.getValues('Color');
293
- console.log('Available colours:', values.map(v => v.value));
313
+ console.log(
314
+ 'Available colours:',
315
+ values.map((v) => v.value)
316
+ );
294
317
  });
295
318
  ```
296
319
 
@@ -306,15 +329,26 @@ client.destroy();
306
329
  import type { ViewerSettings } from '@virtualdisplay.io/client';
307
330
 
308
331
  interface ClientOptions {
309
- parent: string | HTMLElement; // Where to embed the 3D viewer
310
- license: string; // License key from your Virtualdisplay account
311
- model: string; // Which 3D model to load
312
- debug?: boolean; // Shows console logs for troubleshooting
313
- language?: string; // Viewer language: 'nl', 'en', or 'de'
332
+ parent: string | HTMLElement; // Where to embed the 3D viewer
333
+ app?: string; // An app's link key: the alternative to license + model (see Mounting a specific app)
334
+ license: string; // License key from your Virtualdisplay account; with model, the classic address form
335
+ model: string; // Which 3D model to load
336
+ debug?: boolean; // Shows console logs for troubleshooting
337
+ language?: string; // Viewer language: 'nl', 'en', or 'de'
314
338
  viewerSettings?: Partial<ViewerSettings>; // Override fields from model metadata (see Standard interface)
315
339
  }
316
340
  ```
317
341
 
342
+ One of the two address forms is required: `app`, or `license` + `model`. The
343
+ real type is a union that enforces this at compile time; the block above is the
344
+ flattened reference.
345
+
346
+ ```typescript
347
+ type ClientAddress =
348
+ | { app: string } // an app's link key
349
+ | { license: string; model: string }; // the classic address form
350
+ ```
351
+
318
352
  ## Error codes
319
353
 
320
354
  The client throws `VirtualdisplayError` for known error conditions:
package/dist/index.d.ts CHANGED
@@ -22,7 +22,59 @@ declare const MESSAGE_TYPES: {
22
22
  readonly QR_SHOWN: "qr_shown";
23
23
  readonly SYNC: "sync";
24
24
  readonly SELECTABLE: "selectable";
25
+ readonly APP_RESOLVED: "app_resolved";
25
26
  };
27
+ /** The public submit settings of a configurator-type app, as the form renders
28
+ * them. Never the recipient: that section is removed in SQL on the public
29
+ * read and cannot ride any message. Sparse like every config section: an
30
+ * absent field means the renderer's default (the backend stores the section
31
+ * as written and omits empty keys), so nothing here is guaranteed present. */
32
+ interface AppSubmitSettings$1 {
33
+ readonly fields?: string[];
34
+ readonly ctaLabel?: string;
35
+ readonly privacyUrl?: string;
36
+ }
37
+ /**
38
+ * Viewer -> client: which app this viewer resolved, reported up so the parent
39
+ * (the embed host) can load the matching shell without its own resolve (the
40
+ * apps spec's one-read inversion). Sent once, after the resolve, before or
41
+ * with MAPPING.
42
+ */
43
+ interface AppResolvedMessage {
44
+ readonly type: typeof MESSAGE_TYPES.APP_RESOLVED;
45
+ /** Known values: {@link AppType}. An unknown value means a newer server; the host reports it, never downgrades. */
46
+ readonly appType: string;
47
+ readonly name: string;
48
+ /** The app's public slug, the address a submit POSTs to. Optional on the
49
+ * wire: a pre-task-60 server does not send it, and the pair-form host has
50
+ * no other way to learn it, so the shell treats absence as "submit
51
+ * unavailable" rather than the guard dropping the whole report. */
52
+ readonly slug?: string;
53
+ readonly submit?: AppSubmitSettings$1;
54
+ }
55
+ /** One mesh or material variant in a model, as reported by STATE. */
56
+ interface ModelNodeDto {
57
+ readonly id: string;
58
+ readonly name: string;
59
+ readonly type: string;
60
+ readonly visible: boolean;
61
+ }
62
+ /**
63
+ * State (viewer → client): the meshes and material variants of one instance,
64
+ * with their current visibility.
65
+ *
66
+ * Its shape used to live in server-core, marked "server-only", while its type
67
+ * value sat in the public MESSAGE_TYPES and the client read it: a public
68
+ * message whose shape only one side declared, which is how two shapes of one
69
+ * thing start to drift.
70
+ */
71
+ interface StateMessage {
72
+ readonly type: typeof MESSAGE_TYPES.STATE;
73
+ /** Whose meshes and variants these are. */
74
+ readonly instance: string;
75
+ readonly nodes: ModelNodeDto[];
76
+ readonly isInitial: boolean;
77
+ }
26
78
  interface MutationDto {
27
79
  readonly name: string;
28
80
  readonly value: string;
@@ -46,6 +98,8 @@ interface SyncStateSnapshot {
46
98
  }
47
99
  interface MutationMessage {
48
100
  readonly type: typeof MESSAGE_TYPES.MUTATION;
101
+ /** Which instance's attributes these mutations change. */
102
+ readonly instance: string;
49
103
  readonly mutations: MutationDto[];
50
104
  readonly isInitial: boolean;
51
105
  /**
@@ -61,11 +115,19 @@ interface MutationMessage {
61
115
  * authoritative folded family state: the sync module diffs it against the
62
116
  * channel's last-seen state and applies the difference, so a catch-up
63
117
  * (empty `mutations`) or a broadcast the hub dropped still converges the
64
- * subscriber. None of these fields are read by `MutationRouter`.
118
+ * subscriber. None of these fields are read by `MutationPipeline`.
65
119
  */
66
120
  readonly origin?: string;
67
121
  readonly seq?: number;
68
122
  readonly state?: SyncStateSnapshot;
123
+ /**
124
+ * A drag's intermediate step (parametric dimensions): the viewer applies it
125
+ * so the model follows the hand, but never tracks it, the settled value is
126
+ * the one recorded choice and it arrives as an ordinary mutation without
127
+ * this flag. Only the client knows when a drag settles, which is why this
128
+ * travels on the message instead of being guessed viewer-side.
129
+ */
130
+ readonly transient?: boolean;
69
131
  }
70
132
  /**
71
133
  * Sync channel status (server → client, ar-session-handoff, sync-channel.md):
@@ -80,9 +142,18 @@ interface SyncMessage {
80
142
  }
81
143
  interface MutationResultMessage {
82
144
  readonly type: typeof MESSAGE_TYPES.MUTATION;
145
+ /** Which instance these results are for; without it a failure cannot say whose. */
146
+ readonly instance: string;
83
147
  readonly results: MutationResult[];
84
148
  readonly isInitial: boolean;
85
149
  }
150
+ /**
151
+ * How a model's annotations present to the Shopper: never (hide), always
152
+ * (show), or under the Shopper's own control via a viewer toggle, starting
153
+ * visible (toggle-shown) or hidden (toggle-hidden). Resolved per app; the
154
+ * backend's `Resolve` owns the `hide` default.
155
+ */
156
+ type AnnotationVisibility$1 = 'hide' | 'show' | 'toggle-shown' | 'toggle-hidden';
86
157
  /**
87
158
  * Viewer settings from model metadata (server → client via MAPPING).
88
159
  * Customer-configured in the portal.
@@ -92,7 +163,12 @@ interface ViewerSettings$1 {
92
163
  readonly fullscreen?: boolean;
93
164
  readonly screenshot?: boolean;
94
165
  readonly cameraControls?: boolean;
95
- readonly annotations?: boolean;
166
+ /**
167
+ * The resolved per-app annotation mode. Present only when the model has
168
+ * annotations, so a control can never render with nothing to govern;
169
+ * absent means there is nothing to show or toggle.
170
+ */
171
+ readonly annotations?: AnnotationVisibility$1;
96
172
  readonly language?: string;
97
173
  /** Where the attribute controls sit: in-frame bottom-left (default) or a docked right panel. */
98
174
  readonly configuratorLayout?: 'inframe-bottom-left' | 'sidebar-right' | 'hidden';
@@ -136,19 +212,46 @@ interface SnapshotResultMessage {
136
212
  readonly data?: string;
137
213
  readonly error?: string;
138
214
  }
215
+ /**
216
+ * Marks an attribute as a measurement (a parametric dimension): the values
217
+ * are millimetre integer strings, and this carries what a client needs to
218
+ * render a measurement control and format the unit. Absent for ordinary
219
+ * attributes; a client that ignores it still renders the value list.
220
+ */
221
+ interface DimensionDescriptor {
222
+ readonly displayUnit: 'mm' | 'cm' | 'm' | 'in';
223
+ readonly minMm: number;
224
+ readonly maxMm: number;
225
+ readonly stepMm: number;
226
+ }
139
227
  interface AttributeDefinition {
140
228
  readonly name: string;
141
229
  readonly values: string[];
142
230
  readonly defaultValue?: string;
231
+ /** Present when this attribute is a measurement; see DimensionDescriptor. */
232
+ readonly dimension?: DimensionDescriptor;
143
233
  }
144
234
  interface MappingMessage {
145
235
  readonly type: typeof MESSAGE_TYPES.MAPPING;
236
+ /**
237
+ * Whose attributes these are. One MAPPING per instance, and an instance with
238
+ * no attributes sends an empty one rather than none: silence would leave the
239
+ * client unable to name it, click it or report its interactions.
240
+ */
241
+ readonly instance: string;
146
242
  readonly attributes: AttributeDefinition[];
147
243
  }
148
244
  /** Per selector, the values the buyer can still pick right now. */
149
245
  interface SelectableAttribute {
150
246
  readonly name: string;
151
247
  readonly selectable: string[];
248
+ /**
249
+ * The control should not render at all: a dimension whose component is
250
+ * currently hidden by its show-when conditions (parametric dimensions).
251
+ * Different from an empty `selectable`, which greys values out but keeps
252
+ * the control visible. Absent means visible.
253
+ */
254
+ readonly hidden?: boolean;
152
255
  }
153
256
  /**
154
257
  * Selectability snapshot (server → client): sent after the mapping and after
@@ -160,11 +263,30 @@ interface SelectableAttribute {
160
263
  */
161
264
  interface SelectableMessage {
162
265
  readonly type: typeof MESSAGE_TYPES.SELECTABLE;
266
+ /** Whose attributes these are, mirroring the MAPPING it follows. */
267
+ readonly instance: string;
163
268
  readonly attributes: SelectableAttribute[];
164
269
  }
165
270
  /**
166
271
  * Loading indicator (server → client): progress percentage, phase label, or error.
167
272
  * Sent during model download and parse so the client loading indicator can update.
273
+ *
274
+ * This message concerns one model at five of its seven call sites (download
275
+ * progress, a load error, the rendering phase) and is fatal with no model to
276
+ * blame at the other two (no WebGL, the boot-failure path), so by the
277
+ * message-scope rule it takes an instance id optionally.
278
+ *
279
+ * It does not carry one yet, on purpose. `publishLoadingProgress` is the only
280
+ * publisher and knows no instance, so the field would be absent at all seven
281
+ * sites while its documented meaning is "absent means the viewer could not
282
+ * start". An integrator branching on that would read every progress update as
283
+ * a viewer failure, which is worse than no field at all.
284
+ *
285
+ * Nothing is lost by waiting: an optional field is additive, so unlike the
286
+ * required ids in this file it carries no alpha-window pressure. Composition
287
+ * task 10 brings a second model and with it the choice the spec parked, thread
288
+ * the id through the five model sites, or split the fatal case onto its own
289
+ * message. Decide it there, with a real second instance to test against.
168
290
  */
169
291
  interface LoadingIndicatorMessage {
170
292
  readonly type: typeof MESSAGE_TYPES.LOADING_INDICATOR;
@@ -226,6 +348,13 @@ interface SessionMessage {
226
348
  */
227
349
  interface AnnotationToggleMessage {
228
350
  readonly type: typeof MESSAGE_TYPES.ANNOTATION_TOGGLE;
351
+ /**
352
+ * Whose pins to show. This looks scene-wide because the client draws one
353
+ * button, and it is not: a pin's position is stored in its model's local
354
+ * space, so it means nothing without the root it belongs to. A client that
355
+ * wants one button loops over the instances, which is a UI decision.
356
+ */
357
+ readonly instance: string;
229
358
  readonly enabled: boolean;
230
359
  }
231
360
  /** Focus actions (client → server). */
@@ -238,8 +367,8 @@ declare const FOCUS_STATES: {
238
367
  readonly FOCUSED: "focused";
239
368
  readonly UNFOCUSED: "unfocused";
240
369
  };
241
- type FocusAction = typeof FOCUS_ACTIONS[keyof typeof FOCUS_ACTIONS];
242
- type FocusStateValue = typeof FOCUS_STATES[keyof typeof FOCUS_STATES];
370
+ type FocusAction = (typeof FOCUS_ACTIONS)[keyof typeof FOCUS_ACTIONS];
371
+ type FocusStateValue = (typeof FOCUS_STATES)[keyof typeof FOCUS_STATES];
243
372
  /**
244
373
  * Focus (client → server): isolate and frame a component, or return to overview.
245
374
  * Programmatic — the API, the (future) component menu, and the portal editors all
@@ -248,6 +377,8 @@ type FocusStateValue = typeof FOCUS_STATES[keyof typeof FOCUS_STATES];
248
377
  */
249
378
  interface FocusMessage {
250
379
  readonly type: typeof MESSAGE_TYPES.FOCUS;
380
+ /** Which instance the component name resolves inside. */
381
+ readonly instance: string;
251
382
  readonly action: FocusAction;
252
383
  readonly component?: string;
253
384
  }
@@ -258,6 +389,8 @@ interface FocusMessage {
258
389
  */
259
390
  interface FocusStateMessage {
260
391
  readonly type: typeof MESSAGE_TYPES.FOCUS;
392
+ /** Which instance this state is about. */
393
+ readonly instance: string;
261
394
  readonly state: FocusStateValue;
262
395
  readonly component?: string;
263
396
  readonly attributes?: string[];
@@ -274,7 +407,7 @@ declare const AVAILABILITY$1: {
274
407
  /** Behaves exactly like an unpriced combination: no price, no button. */
275
408
  readonly HIDDEN: "hidden";
276
409
  };
277
- type Availability$1 = typeof AVAILABILITY$1[keyof typeof AVAILABILITY$1];
410
+ type Availability$1 = (typeof AVAILABILITY$1)[keyof typeof AVAILABILITY$1];
278
411
  /**
279
412
  * One pricing rule: a subset of attribute values mapped to a reference + price.
280
413
  * Matches the backend pricing read contract (`GET /api/public/models/.../pricing`).
@@ -315,9 +448,16 @@ interface PricingDocument$1 {
315
448
  */
316
449
  interface PricingMessage {
317
450
  readonly type: typeof MESSAGE_TYPES.PRICING;
451
+ /**
452
+ * Which instance is priced. Only one is today, so the id has no alternative
453
+ * value to hold; it carries one anyway, because a rule with a "not needed
454
+ * yet" exception erodes, and a scene of separately priced products needs
455
+ * exactly this field.
456
+ */
457
+ readonly instance: string;
318
458
  readonly pricing: PricingDocument$1;
319
459
  }
320
- type Message = HandshakeMessage | SessionMessage | MutationMessage | MutationResultMessage | CameraCommandMessage | CameraPositionMessage | SnapshotRequestMessage | SnapshotResultMessage | MappingMessage | LoadingIndicatorMessage | ArActivateMessage | FullscreenMessage | ConfigMessage | AnnotationToggleMessage | FocusMessage | FocusStateMessage | PricingMessage | QrShownMessage | SyncMessage | SelectableMessage;
460
+ type Message = HandshakeMessage | SessionMessage | MutationMessage | MutationResultMessage | CameraCommandMessage | CameraPositionMessage | SnapshotRequestMessage | SnapshotResultMessage | MappingMessage | LoadingIndicatorMessage | ArActivateMessage | FullscreenMessage | ConfigMessage | AnnotationToggleMessage | FocusMessage | FocusStateMessage | PricingMessage | QrShownMessage | SyncMessage | SelectableMessage | AppResolvedMessage | StateMessage;
321
461
  type MessageCallback = (message: Message) => void | Promise<void>;
322
462
 
323
463
  /**
@@ -432,14 +572,6 @@ declare class IframeMessenger {
432
572
  * Stop listening and clean up all state
433
573
  */
434
574
  destroy(): void;
435
- /**
436
- * Whether the messenger is ready to send
437
- */
438
- get ready(): boolean;
439
- /**
440
- * Number of messages waiting in the outgoing queue
441
- */
442
- get pendingCount(): number;
443
575
  private post;
444
576
  private routeToHandler;
445
577
  }
@@ -453,7 +585,11 @@ type SendFn$1 = (message: Message) => void;
453
585
  */
454
586
  declare class Annotations {
455
587
  private readonly send;
456
- constructor(send: SendFn$1);
588
+ /** Whose pins this facade toggles: a pin lives in one model's local space. */
589
+ private readonly instance;
590
+ constructor(send: SendFn$1,
591
+ /** Whose pins this facade toggles: a pin lives in one model's local space. */
592
+ instance: string);
457
593
  setEnabled(enabled: boolean): void;
458
594
  }
459
595
 
@@ -685,10 +821,14 @@ type DispatchFn = (state: FocusChange) => void;
685
821
  declare class Focus {
686
822
  private readonly send;
687
823
  private readonly dispatch;
824
+ /** Which instance this facade addresses; every message it sends carries it. */
825
+ private readonly instance;
688
826
  private focused;
689
827
  private attributes;
690
828
  private readonly changeCallbacks;
691
- constructor(send: MessageCallback, dispatch: DispatchFn);
829
+ constructor(send: MessageCallback, dispatch: DispatchFn,
830
+ /** Which instance this facade addresses; every message it sends carries it. */
831
+ instance: string);
692
832
  /** The focused component name, or null in overview. */
693
833
  get current(): string | null;
694
834
  /**
@@ -863,12 +1003,19 @@ declare class Attribute {
863
1003
  }
864
1004
 
865
1005
  /**
866
- * Configuration options for VirtualdisplayClient
1006
+ * The integrator-facing viewer settings override. Identical to the wire
1007
+ * `ViewerSettings`, except `annotations` also accepts the legacy booleans:
1008
+ * they normalise before the merge (`true` → `show`, `false` → `hide`), so
1009
+ * existing mounts keep compiling while the package is in alpha.
1010
+ */
1011
+ type ViewerSettingsOverride = Omit<Partial<ViewerSettings$1>, 'annotations'> & {
1012
+ readonly annotations?: boolean | AnnotationVisibility$1;
1013
+ };
1014
+ /**
1015
+ * Options shared by both address forms.
867
1016
  */
868
- interface ClientOptions {
1017
+ interface ClientOptionsBase {
869
1018
  readonly parent: string | HTMLElement;
870
- readonly model: string;
871
- readonly license: string;
872
1019
  readonly debug?: boolean | undefined;
873
1020
  readonly language?: string | undefined;
874
1021
  /** W3C traceparent string to propagate to the viewer iframe for end-to-end tracing. */
@@ -884,8 +1031,34 @@ interface ClientOptions {
884
1031
  * Override viewer settings from metadata. Omitted fields use the metadata value.
885
1032
  * A warning is logged when an override differs from the metadata value.
886
1033
  */
887
- readonly viewerSettings?: Partial<ViewerSettings$1>;
1034
+ readonly viewerSettings?: ViewerSettingsOverride;
888
1035
  }
1036
+ /**
1037
+ * How the client addresses what it shows, one of two forms:
1038
+ *
1039
+ * - `{ app }`: an app's public link key (the slug in its URL). The viewer
1040
+ * resolves that app and renders it with its own configuration.
1041
+ * - `{ license, model }`: the classic pair, which resolves to the model's
1042
+ * default app. Every existing integration keeps working unchanged.
1043
+ *
1044
+ * Supplying both is allowed so a migration is additive (add `app`, done):
1045
+ * `app` wins and a warning is logged that the pair was ignored. Supplying
1046
+ * neither is a compile error, which is the guarantee this union buys over a
1047
+ * bag of three optional fields.
1048
+ */
1049
+ type ClientAddress = {
1050
+ readonly app: string;
1051
+ readonly license?: string | undefined;
1052
+ readonly model?: string | undefined;
1053
+ } | {
1054
+ readonly license: string;
1055
+ readonly model: string;
1056
+ readonly app?: undefined;
1057
+ };
1058
+ /**
1059
+ * Configuration options for VirtualdisplayClient
1060
+ */
1061
+ type ClientOptions = ClientOptionsBase & ClientAddress;
889
1062
 
890
1063
  /**
891
1064
  * The Virtualdisplay client's DOM event names, in one registry.
@@ -913,6 +1086,20 @@ interface MutationFailedEvent {
913
1086
  readonly attribute: string;
914
1087
  readonly value: string;
915
1088
  }
1089
+ /**
1090
+ * Which app this viewer resolved (the apps spec's one-read inversion): the
1091
+ * `onAppResolved` callback payload. The protocol message minus its wire
1092
+ * discriminator; only the parent-safe submit settings ride along, never the
1093
+ * delivery recipient (removed in SQL on the public read).
1094
+ */
1095
+ interface ResolvedAppInfo {
1096
+ /** Known values: `AppType`. An unknown value means a newer server; report it, never downgrade to a viewer. */
1097
+ readonly appType: string;
1098
+ readonly name: string;
1099
+ /** The app's public slug (the address a submit POSTs to). Absent from a pre-slug server. */
1100
+ readonly slug?: string;
1101
+ readonly submit?: AppSubmitSettings$1;
1102
+ }
916
1103
  interface VirtualdisplaySelectEvent {
917
1104
  readonly attribute: string;
918
1105
  readonly value: string;
@@ -1020,9 +1207,16 @@ declare class BaseClient {
1020
1207
  private readonly mutationFailedCallbacks;
1021
1208
  /** The viewer's tracking session id, once the server announces it (SESSION). */
1022
1209
  private viewerSessionId;
1210
+ /** Which app the viewer resolved, once reported (APP_RESOLVED). */
1211
+ private resolvedApp;
1212
+ private readonly appResolvedCallbacks;
1023
1213
  constructor(options: ClientOptions, extensions?: BaseClientExtensions);
1214
+ /** The configurator UI, every callback bridging it to the facade that owns the action. */
1215
+ private createStandardInterface;
1024
1216
  /** Wire the inbound viewer protocol to the facades. */
1025
1217
  private wireIncomingMessages;
1218
+ /** Store the resolved app and notify every registered `onAppResolved` callback. */
1219
+ private handleAppResolved;
1026
1220
  /** Store the tracking session id and hand it to the AR flow, which waits on it for the QR. */
1027
1221
  private handleSession;
1028
1222
  /** Forward a failed mutation to every registered `onMutationFailed` callback. */
@@ -1042,6 +1236,14 @@ declare class BaseClient {
1042
1236
  /** Dispatch a bubbling `vd:configuration-submit` event when the CTA is used. */
1043
1237
  private dispatchConfigurationSubmit;
1044
1238
  private handleConfig;
1239
+ /**
1240
+ * The difference echo: the viewer already applied the app-config mode
1241
+ * itself, so the client only speaks up when the merged mode (config plus
1242
+ * integrator override) differs from what CONFIG announced, in either
1243
+ * direction. Equal modes echo nothing, so a boot without overrides sends
1244
+ * no toggle at all.
1245
+ */
1246
+ private echoAnnotationsDifference;
1045
1247
  /** Tell the viewer the user entered fullscreen, so it records the engagement. */
1046
1248
  private trackFullscreen;
1047
1249
  private initializeLogging;
@@ -1062,6 +1264,12 @@ declare class BaseClient {
1062
1264
  * fires.
1063
1265
  */
1064
1266
  onSelectableChange(callback: (snapshot: SelectableSnapshot) => void): void;
1267
+ /**
1268
+ * Subscribe to which app this viewer resolved (type, name, public submit
1269
+ * settings). Reported once by the viewer after its resolve; fires immediately
1270
+ * when the report already arrived, so registration order does not matter.
1271
+ */
1272
+ onAppResolved(callback: (app: ResolvedAppInfo) => void): void;
1065
1273
  /** The selectable values for one attribute right now, or null when unrestricted. */
1066
1274
  getSelectable(name: string): readonly string[] | null;
1067
1275
  /**
@@ -1086,12 +1294,17 @@ declare class BaseClient {
1086
1294
  }
1087
1295
 
1088
1296
  type Attrs = Record<string, unknown> | unknown;
1089
- declare let logger: {
1297
+ /**
1298
+ * The logger the client exposes: the same four levels as the base logger, but
1299
+ * accepting any value as attributes so callers can pass an Error or a bare value.
1300
+ */
1301
+ interface ClientLogger {
1090
1302
  debug: (msg: string, attrs?: Attrs) => void;
1091
1303
  info: (msg: string, attrs?: Attrs) => void;
1092
1304
  warn: (msg: string, attrs?: Attrs) => void;
1093
1305
  error: (msg: string, attrs?: Attrs) => void;
1094
- };
1306
+ }
1307
+ declare let logger: ClientLogger;
1095
1308
  declare function enableDebugMode(): void;
1096
1309
  declare function isDebugMode(): boolean;
1097
1310
 
@@ -1113,6 +1326,27 @@ declare class VirtualdisplayClient extends BaseClient {
1113
1326
  onSyncChange(callback: (connected: boolean) => void): void;
1114
1327
  }
1115
1328
 
1329
+ /** The app types this protocol version knows. The wire field stays a plain
1330
+ * string on purpose: a newer viewer may report a type this bundle predates,
1331
+ * and the host must receive it to fail visibly (never a quiet downgrade). */
1332
+ type AppType = 'viewer' | 'configurator';
1333
+ /** The public submit settings of a configurator-type app, as the form renders
1334
+ * them. Never the recipient: that section is removed in SQL on the public
1335
+ * read and cannot ride any message. Sparse like every config section: an
1336
+ * absent field means the renderer's default (the backend stores the section
1337
+ * as written and omits empty keys), so nothing here is guaranteed present. */
1338
+ interface AppSubmitSettings {
1339
+ readonly fields?: string[];
1340
+ readonly ctaLabel?: string;
1341
+ readonly privacyUrl?: string;
1342
+ }
1343
+ /**
1344
+ * How a model's annotations present to the Shopper: never (hide), always
1345
+ * (show), or under the Shopper's own control via a viewer toggle, starting
1346
+ * visible (toggle-shown) or hidden (toggle-hidden). Resolved per app; the
1347
+ * backend's `Resolve` owns the `hide` default.
1348
+ */
1349
+ type AnnotationVisibility = 'hide' | 'show' | 'toggle-shown' | 'toggle-hidden';
1116
1350
  /**
1117
1351
  * Viewer settings from model metadata (server → client via MAPPING).
1118
1352
  * Customer-configured in the portal.
@@ -1122,7 +1356,12 @@ interface ViewerSettings {
1122
1356
  readonly fullscreen?: boolean;
1123
1357
  readonly screenshot?: boolean;
1124
1358
  readonly cameraControls?: boolean;
1125
- readonly annotations?: boolean;
1359
+ /**
1360
+ * The resolved per-app annotation mode. Present only when the model has
1361
+ * annotations, so a control can never render with nothing to govern;
1362
+ * absent means there is nothing to show or toggle.
1363
+ */
1364
+ readonly annotations?: AnnotationVisibility;
1126
1365
  readonly language?: string;
1127
1366
  /** Where the attribute controls sit: in-frame bottom-left (default) or a docked right panel. */
1128
1367
  readonly configuratorLayout?: 'inframe-bottom-left' | 'sidebar-right' | 'hidden';
@@ -1139,7 +1378,7 @@ declare const AVAILABILITY: {
1139
1378
  /** Behaves exactly like an unpriced combination: no price, no button. */
1140
1379
  readonly HIDDEN: "hidden";
1141
1380
  };
1142
- type Availability = typeof AVAILABILITY[keyof typeof AVAILABILITY];
1381
+ type Availability = (typeof AVAILABILITY)[keyof typeof AVAILABILITY];
1143
1382
  /**
1144
1383
  * One pricing rule: a subset of attribute values mapped to a reference + price.
1145
1384
  * Matches the backend pricing read contract (`GET /api/public/models/.../pricing`).
@@ -1176,4 +1415,4 @@ interface PricingDocument {
1176
1415
  }
1177
1416
 
1178
1417
  export { Annotations, Attribute, AttributeValue, Camera, Configuration, ERROR_CODES, Focus, Photo, Snapshot, VD_EVENTS, VirtualdisplayClient, VirtualdisplayError, enableDebugMode, isDebugMode, logger };
1179
- export type { Availability, ClientOptions, ErrorCode, FocusState, MutationFailedEvent, PhotoData, PricingDocument, PricingRule, ResolvedConfiguration, SubmitOptions, SubmittedConfiguration, VdEvent, ViewerSettings, VirtualdisplaySelectEvent, VirtualdisplaySelectedEvent, VirtualdisplayUiEventMap, VirtualdisplayUiEventType };
1418
+ export type { AppSubmitSettings, AppType, Availability, ClientOptions, ErrorCode, FocusState, MutationFailedEvent, PhotoData, PricingDocument, PricingRule, ResolvedAppInfo, ResolvedConfiguration, SubmitOptions, SubmittedConfiguration, VdEvent, ViewerSettings, VirtualdisplaySelectEvent, VirtualdisplaySelectedEvent, VirtualdisplayUiEventMap, VirtualdisplayUiEventType };