@vctrl/embed 0.25.0 → 1.0.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
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.25.1...embed-v1.0.0) (2026-09-25)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * **embed:** `InfoPopoverVectrealFooter` is no longer exported from `@vctrl/viewer`. The info-popover primitives now carry no branding of their own, so a consumer importing it must delete the import and supply their own footer if they want one. The Vectreal mark on hosted embeds is rendered by the platform instead, and is gated on the scene owner's plan.
9
+ * **hooks:** `ShadowsProps` is a plain interface rather than a union discriminated on `type`, so a consumer still passing `type: 'accumulative'` is passing an unknown property.
10
+
11
+ ### Features
12
+
13
+ * **embed:** author-controlled info popover, plan-gated Vectreal mark ([#828](https://github.com/Vectreal/vectreal-platform/issues/828)) ([d557acb](https://github.com/Vectreal/vectreal-platform/commit/d557acb55ae04f878ee30838052accb3b76767e1))
14
+ * **hotspots:** let a hotspot say something ([#831](https://github.com/Vectreal/vectreal-platform/issues/831)) ([d4c9b3d](https://github.com/Vectreal/vectreal-platform/commit/d4c9b3d1d38d3cdd6e2e268b5e9732d3ad7b2970))
15
+ * **viewer:** play glTF animation clips ([11ff088](https://github.com/Vectreal/vectreal-platform/commit/11ff0880eb32ef0bda03196fe74de4955452d2f1))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **build:** make the package typecheck gates actually compile something ([#723](https://github.com/Vectreal/vectreal-platform/issues/723)) ([38c6487](https://github.com/Vectreal/vectreal-platform/commit/38c64879a00c1774df970effa09cff38396233ec))
21
+ * **hooks:** keep a failed upload from hiding a failed scene load ([5e9676f](https://github.com/Vectreal/vectreal-platform/commit/5e9676f01f61aa53ab74ec3e3fb2d78283fe7045))
22
+
23
+
24
+ ### Dependencies
25
+
26
+ * The following workspace dependencies were updated
27
+ * devDependencies
28
+ * @vctrl/viewer bumped to 1.0.0
29
+
30
+ ## [0.25.1](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.25.0...embed-v0.25.1) (2026-08-08)
31
+
32
+
33
+ ### Miscellaneous Chores
34
+
35
+ * **embed:** Synchronize vectreal-monorepo versions
36
+
3
37
  ## [0.25.0](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.24.1...embed-v0.25.0) (2026-08-04)
4
38
 
5
39
 
package/README.md CHANGED
@@ -42,8 +42,9 @@ For production, pin a version (`@vctrl/embed@<version>`) and add a [Subresource
42
42
 
43
43
  const embed = new VectrealEmbed(document.getElementById('vectreal-scene'))
44
44
 
45
- const { cameras } = await embed.ready()
45
+ const { cameras, hotspots } = await embed.ready()
46
46
  console.log('Available cameras:', cameras)
47
+ console.log('Hotspots a visitor can see:', hotspots)
47
48
 
48
49
  embed.on('camera_changed', ({ cameraId }) => {
49
50
  console.log('Camera changed to:', cameraId)
@@ -62,42 +63,33 @@ For production, pin a version (`@vctrl/embed@<version>`) and add a [Subresource
62
63
  | `iframeOrigin` | `string` | Auto-detected from `iframe.src` | Expected iframe origin for postMessage security. |
63
64
  | `readyTimeout` | `number` | `15000` | ms before `ready()` rejects. |
64
65
 
65
- ### Methods
66
-
67
- | Method | Description |
68
- | -------------------------------- | -------------------------------------------------------------- |
69
- | `ready()` | Resolves with `{ sceneId, cameras }` when the viewer is ready. |
70
- | `activateCamera(cameraId)` | Switch to a named camera. |
71
- | `setTransition(options)` | Override transition type, duration, and easing. |
72
- | `setControlsEnabled(enabled)` | Enable or disable orbit controls. |
73
- | `setAutoRotate(enabled, speed?)` | Toggle auto-rotate. |
74
- | `setZoomEnabled(enabled)` | Toggle scroll-zoom. |
75
- | `setPanEnabled(enabled)` | Toggle right-click pan. |
76
- | `sendScrollProgress(progress)` | Drive scroll-triggered interactions (0–1). |
77
- | `sendMessage(message, payload?)` | Trigger a named `host_message` interaction. |
78
- | `on(type, handler)` | Subscribe to a viewer event. Returns unsubscribe. |
79
- | `off(type, handler)` | Remove a specific handler. |
80
- | `destroy()` | Remove all listeners and stop processing messages. |
81
-
82
- ### Events
83
-
84
- | Type | Payload | When |
85
- | --------------------- | ----------------------------------------- | ------------------------------------------------------- |
86
- | `viewer_ready` | `void` | Viewer command surface is registered. |
87
- | `model_loaded` | `void` | Model finished loading and initial framing is complete. |
88
- | `camera_changed` | `{ cameraId }` | Active camera changed. |
89
- | `auto_rotate_changed` | `{ enabled }` | Auto-rotate state changed. |
90
- | `interaction_event` | `{ eventName, interactionId?, payload? }` | Publisher custom event fired. |
66
+ ### What you can control
91
67
 
92
- ## URL parameter shorthand
68
+ Cameras and transitions, orbit/zoom/pan, animation playback, hotspots, scroll-driven
69
+ interactions, and named host messages - plus events for each. The full method and event
70
+ tables live in one place, the
71
+ [Embed SDK guide](https://vectreal.com/docs/guides/embed-sdk#api-reference), rather than
72
+ being repeated here where the two copies drift apart.
73
+
74
+ Two behaviours worth knowing before you read it:
75
+
76
+ The camera, controls, hotspot and animation methods are queued and flushed once the
77
+ viewer reports ready, so you can call them immediately after constructing the SDK.
78
+ `sendScrollProgress` and `sendMessage` are not queued: they post straight to the iframe,
79
+ and a call made before the viewer is ready is silently dropped. Await `ready()` before
80
+ wiring a scroll handler or sending a host message. `destroy()` discards anything still
81
+ queued.
93
82
 
94
- For static initial configuration without JavaScript, add query parameters to the iframe `src`:
83
+ The constructor throws when it cannot determine a target origin, which happens when
84
+ `iframeOrigin` is omitted and the iframe's `src` is empty or unparseable. Construct the
85
+ SDK after the `src` is set, or pass `iframeOrigin` explicitly.
86
+
87
+ ## URL parameter shorthand
95
88
 
96
- | Parameter | Example | Effect |
97
- | -------------------- | -------------------- | ------------------------------------- |
98
- | `?camera=<id>` | `?camera=hero` | Activates a camera on `viewer_ready`. |
99
- | `?autoRotate=0` | `?autoRotate=1` | Overrides stored auto-rotate state. |
100
- | `?transition=<type>` | `?transition=linear` | Overrides stored transition type. |
89
+ Opening state, color scheme and hotspot presentation can be set with query parameters on the iframe
90
+ `src`, no JavaScript required — `?camera`, `?autoRotate`, `?transition`, `?theme`,
91
+ `?hotspots`, `?hotspotContent` and `?hotspotColor`. Values and behaviour are tabled once, in the
92
+ [Embed SDK guide](https://vectreal.com/docs/guides/embed-sdk#url-parameters).
101
93
 
102
94
  ## Documentation
103
95
 
@@ -105,6 +97,6 @@ Full guide and examples: [vectreal.com/docs/guides/embed-sdk](https://vectreal.c
105
97
 
106
98
  ## License
107
99
 
108
- AGPL-3.0-only — see [LICENSE](https://github.com/vectreal/vectreal-platform/blob/main/LICENSE).
100
+ AGPL-3.0-only. See [LICENSE.md](https://github.com/Vectreal/vectreal-platform/blob/main/LICENSE.md).
109
101
 
110
102
  Part of the [Vectreal Platform](https://github.com/vectreal/vectreal-platform) monorepo.
package/embed.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { EmbedCameraDescriptor } from './protocol';
1
+ import { EmbedCameraDescriptor, EmbedHotspotDescriptor } from './protocol';
2
2
  export interface EmbedOptions {
3
3
  /** Override the detected iframe origin for postMessage targeting. */
4
4
  iframeOrigin?: string;
@@ -8,6 +8,11 @@ export interface EmbedOptions {
8
8
  export interface EmbedReadyInfo {
9
9
  sceneId: string | undefined;
10
10
  cameras: EmbedCameraDescriptor[];
11
+ /**
12
+ * The hotspots a visitor can see. Empty for a scene with none, and also for
13
+ * an iframe older than this field - see `HostedPreviewPongMessage`.
14
+ */
15
+ hotspots: EmbedHotspotDescriptor[];
11
16
  }
12
17
  export interface SetTransitionOptions {
13
18
  type: 'none' | 'linear' | 'object_avoidance';
@@ -23,6 +28,20 @@ export type EmbedEventMap = {
23
28
  auto_rotate_changed: {
24
29
  enabled: boolean;
25
30
  };
31
+ animation_state_changed: {
32
+ playing: boolean;
33
+ /** The clip driving a sequence; null in simultaneous mode. */
34
+ activeClipId: string | null;
35
+ /** True once the program has run to its end without looping. */
36
+ complete: boolean;
37
+ };
38
+ animation_clip_finished: {
39
+ clipId: string;
40
+ };
41
+ hotspot_activated: {
42
+ hotspotId: string;
43
+ cameraId: string | null;
44
+ };
26
45
  interaction_event: {
27
46
  interactionId?: string;
28
47
  eventName: string;
@@ -50,6 +69,15 @@ export declare class VectrealEmbed {
50
69
  destroy(): void;
51
70
  /** Switch to a named camera. */
52
71
  activateCamera(cameraId: string): void;
72
+ /**
73
+ * Focus a hotspot: reveal what it says and fly its camera, as clicking the
74
+ * marker would.
75
+ *
76
+ * Ids come from `ready()`, which reports only the hotspots a visitor can
77
+ * see. A hotspot the author hid or kept internal is not drawn, and naming
78
+ * one here does nothing.
79
+ */
80
+ focusHotspot(hotspotId: string): void;
53
81
  /** Override the transition behaviour for subsequent camera switches. */
54
82
  setTransition(options: SetTransitionOptions): void;
55
83
  /** Enable or disable orbit controls. */
@@ -60,6 +88,10 @@ export declare class VectrealEmbed {
60
88
  setZoomEnabled(enabled: boolean): void;
61
89
  /** Toggle right-click pan. */
62
90
  setPanEnabled(enabled: boolean): void;
91
+ /** Start or suspend playback of the scene's animation clips. */
92
+ setAnimationPlaying(playing: boolean): void;
93
+ /** Play the animation program again from the beginning. */
94
+ restartAnimation(): void;
63
95
  /**
64
96
  * Drive scroll-triggered interactions defined in the Publisher.
65
97
  * @param progress 0 (page top) – 1 (page bottom)
@@ -75,6 +107,7 @@ export declare class VectrealEmbed {
75
107
  off<K extends EmbedEventType>(type: K, handler: EmbedEventHandler<K>): void;
76
108
  private sceneId;
77
109
  private cameras;
110
+ private hotspots;
78
111
  private sendCommand;
79
112
  private postToIframe;
80
113
  private startPingPolling;
package/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`vectreal-host`,t=`vectreal-preview`;function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function r(e){if(!n(e)||typeof e.type!=`string`)return!1;switch(e.type){case`activate_camera`:return typeof e.cameraId==`string`&&e.cameraId.trim().length>0;case`set_controls_enabled`:return typeof e.enabled==`boolean`;case`set_auto_rotate`:return typeof e.enabled==`boolean`&&(e.speed===void 0||typeof e.speed==`number`);case`set_controls_options`:return(e.zoom===void 0||typeof e.zoom==`boolean`)&&(e.pan===void 0||typeof e.pan==`boolean`);case`set_transition`:return typeof e.transitionType==`string`&&[`none`,`linear`,`object_avoidance`].includes(e.transitionType);default:return!1}}function i(e){if(!n(e)||e.source!==`vectreal-host`||typeof e.type!=`string`)return!1;switch(e.type){case`ping`:return!0;case`viewer_command`:return r(e.command);case`host_scroll_progress`:return typeof e.progress==`number`&&Number.isFinite(e.progress);case`host_message`:return typeof e.message==`string`&&e.message.trim().length>0&&(e.payload===void 0||n(e.payload));default:return!1}}var a=15e3,o=class{constructor(e,t={}){this.handlers=new Map,this.pendingCommands=[],this.isReady=!1,this.pingIntervalId=null,this.sceneId=void 0,this.cameras=[],this.iframe=e,this.readyTimeout=t.readyTimeout??a;let n=e.src||e.getAttribute(`src`)||``,r;if(n)try{r=new URL(n,window.location.href).origin}catch{}if(!t.iframeOrigin&&!r)throw Error(`VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.`);this.targetOrigin=t.iframeOrigin??r,this.boundListener=this.handleMessage.bind(this),window.addEventListener(`message`,this.boundListener),this.startPingPolling()}ready(){return new Promise((e,t)=>{let n=window.setTimeout(()=>{this.off(`viewer_ready`,r),t(Error(`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`))},this.readyTimeout),r=()=>{window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras})};if(this.isReady){window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras});return}this.on(`viewer_ready`,r)})}destroy(){this.stopPingPolling(),window.removeEventListener(`message`,this.boundListener),this.handlers.clear(),this.pendingCommands=[]}activateCamera(e){this.sendCommand({type:`activate_camera`,cameraId:e})}setTransition(e){this.sendCommand({type:`set_transition`,transitionType:e.type,duration:e.duration,easing:e.easing})}setControlsEnabled(e){this.sendCommand({type:`set_controls_enabled`,enabled:e})}setAutoRotate(e,t){this.sendCommand({type:`set_auto_rotate`,enabled:e,speed:t})}setZoomEnabled(e){this.sendCommand({type:`set_controls_options`,zoom:e})}setPanEnabled(e){this.sendCommand({type:`set_controls_options`,pan:e})}sendScrollProgress(t){this.postToIframe({source:e,type:`host_scroll_progress`,progress:Math.min(1,Math.max(0,t))})}sendMessage(t,n){this.postToIframe({source:e,type:`host_message`,message:t,payload:n})}on(e,t){return this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t),()=>this.off(e,t)}off(e,t){this.handlers.get(e)?.delete(t)}sendCommand(t){let n={source:e,type:`viewer_command`,command:t};if(!this.isReady){this.pendingCommands.push(n);return}this.postToIframe(n)}postToIframe(e){try{this.iframe.contentWindow?.postMessage(e,this.targetOrigin)}catch{}}startPingPolling(){this.postToIframe({source:e,type:`ping`}),this.pingIntervalId=window.setInterval(()=>{if(this.isReady){this.stopPingPolling();return}this.postToIframe({source:e,type:`ping`})},500)}stopPingPolling(){this.pingIntervalId!==null&&(window.clearInterval(this.pingIntervalId),this.pingIntervalId=null)}flushPendingCommands(){for(let e of this.pendingCommands)this.postToIframe(e);this.pendingCommands=[]}emit(e,t){this.handlers.get(e)?.forEach(e=>{e(t)})}handleMessage(e){let t=e.data;if(!(!t||typeof t!=`object`||t.source!==`vectreal-preview`)&&!(this.targetOrigin!==`*`&&e.origin!==this.targetOrigin))switch(t.type){case`pong`:this.stopPingPolling(),this.sceneId=t.sceneId,this.cameras=t.cameras,this.isReady=!0,this.flushPendingCommands();break;case`viewer_event`:this.handleViewerEvent(t.event);break;case`interaction_event`:this.emit(`interaction_event`,{interactionId:t.interactionId,eventName:t.eventName,payload:t.payload});break}}handleViewerEvent(e){switch(e.type){case`viewer_ready`:this.isReady=!0,this.flushPendingCommands(),this.emit(`viewer_ready`,void 0);break;case`model_loaded`:this.emit(`model_loaded`,void 0);break;case`camera_changed`:this.emit(`camera_changed`,{cameraId:e.cameraId});break;case`auto_rotate_changed`:this.emit(`auto_rotate_changed`,{enabled:e.enabled});break;case`initial_framing_completed`:this.emit(`model_loaded`,void 0);break}}};exports.HOSTED_PREVIEW_HOST_SOURCE=e,exports.HOSTED_PREVIEW_VIEWER_SOURCE=t,exports.VectrealEmbed=o,exports.isHostedPreviewIncomingMessage=i,exports.isViewerCommand=r;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=`vectreal-host`,t=`vectreal-preview`;function n(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function r(e){if(!n(e)||typeof e.type!=`string`)return!1;switch(e.type){case`activate_camera`:return typeof e.cameraId==`string`&&e.cameraId.trim().length>0;case`focus_hotspot`:return typeof e.hotspotId==`string`&&e.hotspotId.trim().length>0;case`set_controls_enabled`:return typeof e.enabled==`boolean`;case`set_auto_rotate`:return typeof e.enabled==`boolean`&&(e.speed===void 0||typeof e.speed==`number`);case`set_controls_options`:return(e.zoom===void 0||typeof e.zoom==`boolean`)&&(e.pan===void 0||typeof e.pan==`boolean`);case`set_transition`:return typeof e.transitionType==`string`&&[`none`,`linear`,`object_avoidance`].includes(e.transitionType);case`set_animation_playing`:return typeof e.playing==`boolean`;case`restart_animation`:return!0;case`seek_animation_clip`:return typeof e.clipId==`string`&&e.clipId.trim().length>0&&typeof e.time==`number`&&Number.isFinite(e.time)&&e.time>=0;default:return!1}}function i(e){if(!n(e)||e.source!==`vectreal-host`||typeof e.type!=`string`)return!1;switch(e.type){case`ping`:return!0;case`viewer_command`:return r(e.command);case`host_scroll_progress`:return typeof e.progress==`number`&&Number.isFinite(e.progress);case`host_message`:return typeof e.message==`string`&&e.message.trim().length>0&&(e.payload===void 0||n(e.payload));default:return!1}}var a=15e3,o=class{constructor(e,t={}){this.handlers=new Map,this.pendingCommands=[],this.isReady=!1,this.pingIntervalId=null,this.sceneId=void 0,this.cameras=[],this.hotspots=[],this.iframe=e,this.readyTimeout=t.readyTimeout??a;let n=e.src||e.getAttribute(`src`)||``,r;if(n)try{r=new URL(n,window.location.href).origin}catch{}if(!t.iframeOrigin&&!r)throw Error(`VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.`);this.targetOrigin=t.iframeOrigin??r,this.boundListener=this.handleMessage.bind(this),window.addEventListener(`message`,this.boundListener),this.startPingPolling()}ready(){return new Promise((e,t)=>{let n=window.setTimeout(()=>{this.off(`viewer_ready`,r),t(Error(`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`))},this.readyTimeout),r=()=>{window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras,hotspots:this.hotspots})};if(this.isReady){window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras,hotspots:this.hotspots});return}this.on(`viewer_ready`,r)})}destroy(){this.stopPingPolling(),window.removeEventListener(`message`,this.boundListener),this.handlers.clear(),this.pendingCommands=[]}activateCamera(e){this.sendCommand({type:`activate_camera`,cameraId:e})}focusHotspot(e){this.sendCommand({type:`focus_hotspot`,hotspotId:e})}setTransition(e){this.sendCommand({type:`set_transition`,transitionType:e.type,duration:e.duration,easing:e.easing})}setControlsEnabled(e){this.sendCommand({type:`set_controls_enabled`,enabled:e})}setAutoRotate(e,t){this.sendCommand({type:`set_auto_rotate`,enabled:e,speed:t})}setZoomEnabled(e){this.sendCommand({type:`set_controls_options`,zoom:e})}setPanEnabled(e){this.sendCommand({type:`set_controls_options`,pan:e})}setAnimationPlaying(e){this.sendCommand({type:`set_animation_playing`,playing:e})}restartAnimation(){this.sendCommand({type:`restart_animation`})}sendScrollProgress(t){this.postToIframe({source:e,type:`host_scroll_progress`,progress:Math.min(1,Math.max(0,t))})}sendMessage(t,n){this.postToIframe({source:e,type:`host_message`,message:t,payload:n})}on(e,t){return this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t),()=>this.off(e,t)}off(e,t){this.handlers.get(e)?.delete(t)}sendCommand(t){let n={source:e,type:`viewer_command`,command:t};if(!this.isReady){this.pendingCommands.push(n);return}this.postToIframe(n)}postToIframe(e){try{this.iframe.contentWindow?.postMessage(e,this.targetOrigin)}catch{}}startPingPolling(){this.postToIframe({source:e,type:`ping`}),this.pingIntervalId=window.setInterval(()=>{if(this.isReady){this.stopPingPolling();return}this.postToIframe({source:e,type:`ping`})},500)}stopPingPolling(){this.pingIntervalId!==null&&(window.clearInterval(this.pingIntervalId),this.pingIntervalId=null)}flushPendingCommands(){for(let e of this.pendingCommands)this.postToIframe(e);this.pendingCommands=[]}emit(e,t){this.handlers.get(e)?.forEach(e=>{e(t)})}handleMessage(e){let t=e.data;if(!(!t||typeof t!=`object`||t.source!==`vectreal-preview`)&&(this.targetOrigin===`*`||e.origin===this.targetOrigin))switch(t.type){case`pong`:this.stopPingPolling(),this.sceneId=t.sceneId,this.cameras=t.cameras,this.hotspots=t.hotspots??[],this.isReady=!0,this.flushPendingCommands();break;case`viewer_event`:this.handleViewerEvent(t.event);break;case`interaction_event`:this.emit(`interaction_event`,{interactionId:t.interactionId,eventName:t.eventName,payload:t.payload})}}handleViewerEvent(e){switch(e.type){case`viewer_ready`:this.isReady=!0,this.flushPendingCommands(),this.emit(`viewer_ready`,void 0);break;case`model_loaded`:this.emit(`model_loaded`,void 0);break;case`camera_changed`:this.emit(`camera_changed`,{cameraId:e.cameraId});break;case`hotspot_activated`:this.emit(`hotspot_activated`,{hotspotId:e.hotspotId,cameraId:e.cameraId});break;case`auto_rotate_changed`:this.emit(`auto_rotate_changed`,{enabled:e.enabled});break;case`animation_state_changed`:this.emit(`animation_state_changed`,{playing:e.playing,activeClipId:e.activeClipId,complete:e.complete});break;case`animation_clip_finished`:this.emit(`animation_clip_finished`,{clipId:e.clipId});break;case`initial_framing_completed`:this.emit(`model_loaded`,void 0)}}};exports.HOSTED_PREVIEW_HOST_SOURCE=e,exports.HOSTED_PREVIEW_VIEWER_SOURCE=t,exports.VectrealEmbed=o,exports.isHostedPreviewIncomingMessage=i,exports.isViewerCommand=r;
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { VectrealEmbed } from './embed';
2
2
  export type { EmbedEventHandler, EmbedEventMap, EmbedEventType, EmbedOptions, EmbedReadyInfo, SetTransitionOptions } from './embed';
3
3
  export { HOSTED_PREVIEW_HOST_SOURCE, HOSTED_PREVIEW_VIEWER_SOURCE, isHostedPreviewIncomingMessage, isViewerCommand } from './protocol';
4
- export type { EmbedCameraDescriptor, HostedPreviewCustomEventMessage, HostedPreviewHostMessage, HostedPreviewIncomingMessage, HostedPreviewOutgoingMessage, HostedPreviewPingMessage, HostedPreviewPongMessage, HostedPreviewScrollProgressMessage, HostedPreviewViewerCommandMessage, HostedPreviewViewerEventMessage } from './protocol';
4
+ export type { EmbedCameraDescriptor, EmbedHotspotDescriptor, HostedPreviewCustomEventMessage, HostedPreviewHostMessage, HostedPreviewIncomingMessage, HostedPreviewOutgoingMessage, HostedPreviewPingMessage, HostedPreviewPongMessage, HostedPreviewScrollProgressMessage, HostedPreviewViewerCommandMessage, HostedPreviewViewerEventMessage } from './protocol';
package/index.js CHANGED
@@ -7,6 +7,7 @@ function r(e) {
7
7
  if (!n(e) || typeof e.type != "string") return !1;
8
8
  switch (e.type) {
9
9
  case "activate_camera": return typeof e.cameraId == "string" && e.cameraId.trim().length > 0;
10
+ case "focus_hotspot": return typeof e.hotspotId == "string" && e.hotspotId.trim().length > 0;
10
11
  case "set_controls_enabled": return typeof e.enabled == "boolean";
11
12
  case "set_auto_rotate": return typeof e.enabled == "boolean" && (e.speed === void 0 || typeof e.speed == "number");
12
13
  case "set_controls_options": return (e.zoom === void 0 || typeof e.zoom == "boolean") && (e.pan === void 0 || typeof e.pan == "boolean");
@@ -15,6 +16,9 @@ function r(e) {
15
16
  "linear",
16
17
  "object_avoidance"
17
18
  ].includes(e.transitionType);
19
+ case "set_animation_playing": return typeof e.playing == "boolean";
20
+ case "restart_animation": return !0;
21
+ case "seek_animation_clip": return typeof e.clipId == "string" && e.clipId.trim().length > 0 && typeof e.time == "number" && Number.isFinite(e.time) && e.time >= 0;
18
22
  default: return !1;
19
23
  }
20
24
  }
@@ -30,9 +34,9 @@ function i(e) {
30
34
  }
31
35
  //#endregion
32
36
  //#region packages/embed/src/embed.ts
33
- var a = 15e3, o = class {
37
+ var a = class {
34
38
  constructor(e, t = {}) {
35
- this.handlers = /* @__PURE__ */ new Map(), this.pendingCommands = [], this.isReady = !1, this.pingIntervalId = null, this.sceneId = void 0, this.cameras = [], this.iframe = e, this.readyTimeout = t.readyTimeout ?? a;
39
+ this.handlers = /* @__PURE__ */ new Map(), this.pendingCommands = [], this.isReady = !1, this.pingIntervalId = null, this.sceneId = void 0, this.cameras = [], this.hotspots = [], this.iframe = e, this.readyTimeout = t.readyTimeout ?? 15e3;
36
40
  let n = e.src || e.getAttribute("src") || "", r;
37
41
  if (n) try {
38
42
  r = new URL(n, window.location.href).origin;
@@ -47,13 +51,15 @@ var a = 15e3, o = class {
47
51
  }, this.readyTimeout), r = () => {
48
52
  window.clearTimeout(n), e({
49
53
  sceneId: this.sceneId,
50
- cameras: this.cameras
54
+ cameras: this.cameras,
55
+ hotspots: this.hotspots
51
56
  });
52
57
  };
53
58
  if (this.isReady) {
54
59
  window.clearTimeout(n), e({
55
60
  sceneId: this.sceneId,
56
- cameras: this.cameras
61
+ cameras: this.cameras,
62
+ hotspots: this.hotspots
57
63
  });
58
64
  return;
59
65
  }
@@ -69,6 +75,12 @@ var a = 15e3, o = class {
69
75
  cameraId: e
70
76
  });
71
77
  }
78
+ focusHotspot(e) {
79
+ this.sendCommand({
80
+ type: "focus_hotspot",
81
+ hotspotId: e
82
+ });
83
+ }
72
84
  setTransition(e) {
73
85
  this.sendCommand({
74
86
  type: "set_transition",
@@ -102,6 +114,15 @@ var a = 15e3, o = class {
102
114
  pan: e
103
115
  });
104
116
  }
117
+ setAnimationPlaying(e) {
118
+ this.sendCommand({
119
+ type: "set_animation_playing",
120
+ playing: e
121
+ });
122
+ }
123
+ restartAnimation() {
124
+ this.sendCommand({ type: "restart_animation" });
125
+ }
105
126
  sendScrollProgress(t) {
106
127
  this.postToIframe({
107
128
  source: e,
@@ -169,20 +190,18 @@ var a = 15e3, o = class {
169
190
  }
170
191
  handleMessage(e) {
171
192
  let t = e.data;
172
- if (!(!t || typeof t != "object" || t.source !== "vectreal-preview") && !(this.targetOrigin !== "*" && e.origin !== this.targetOrigin)) switch (t.type) {
193
+ if (!(!t || typeof t != "object" || t.source !== "vectreal-preview") && (this.targetOrigin === "*" || e.origin === this.targetOrigin)) switch (t.type) {
173
194
  case "pong":
174
- this.stopPingPolling(), this.sceneId = t.sceneId, this.cameras = t.cameras, this.isReady = !0, this.flushPendingCommands();
195
+ this.stopPingPolling(), this.sceneId = t.sceneId, this.cameras = t.cameras, this.hotspots = t.hotspots ?? [], this.isReady = !0, this.flushPendingCommands();
175
196
  break;
176
197
  case "viewer_event":
177
198
  this.handleViewerEvent(t.event);
178
199
  break;
179
- case "interaction_event":
180
- this.emit("interaction_event", {
181
- interactionId: t.interactionId,
182
- eventName: t.eventName,
183
- payload: t.payload
184
- });
185
- break;
200
+ case "interaction_event": this.emit("interaction_event", {
201
+ interactionId: t.interactionId,
202
+ eventName: t.eventName,
203
+ payload: t.payload
204
+ });
186
205
  }
187
206
  }
188
207
  handleViewerEvent(e) {
@@ -196,14 +215,28 @@ var a = 15e3, o = class {
196
215
  case "camera_changed":
197
216
  this.emit("camera_changed", { cameraId: e.cameraId });
198
217
  break;
218
+ case "hotspot_activated":
219
+ this.emit("hotspot_activated", {
220
+ hotspotId: e.hotspotId,
221
+ cameraId: e.cameraId
222
+ });
223
+ break;
199
224
  case "auto_rotate_changed":
200
225
  this.emit("auto_rotate_changed", { enabled: e.enabled });
201
226
  break;
202
- case "initial_framing_completed":
203
- this.emit("model_loaded", void 0);
227
+ case "animation_state_changed":
228
+ this.emit("animation_state_changed", {
229
+ playing: e.playing,
230
+ activeClipId: e.activeClipId,
231
+ complete: e.complete
232
+ });
233
+ break;
234
+ case "animation_clip_finished":
235
+ this.emit("animation_clip_finished", { clipId: e.clipId });
204
236
  break;
237
+ case "initial_framing_completed": this.emit("model_loaded", void 0);
205
238
  }
206
239
  }
207
240
  };
208
241
  //#endregion
209
- export { e as HOSTED_PREVIEW_HOST_SOURCE, t as HOSTED_PREVIEW_VIEWER_SOURCE, o as VectrealEmbed, i as isHostedPreviewIncomingMessage, r as isViewerCommand };
242
+ export { e as HOSTED_PREVIEW_HOST_SOURCE, t as HOSTED_PREVIEW_VIEWER_SOURCE, a as VectrealEmbed, i as isHostedPreviewIncomingMessage, r as isViewerCommand };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.25.0",
2
+ "version": "1.0.0",
3
3
  "name": "@vctrl/embed",
4
4
  "description": "Framework-agnostic JavaScript SDK for controlling Vectreal embedded 3D scene previews via postMessage.",
5
5
  "bugs": {
@@ -29,7 +29,8 @@
29
29
  "type": "module",
30
30
  "sideEffects": false,
31
31
  "publishConfig": {
32
- "access": "public"
32
+ "access": "public",
33
+ "directory": "../../build/packages/vctrl/embed"
33
34
  },
34
35
  "exports": {
35
36
  ".": {
@@ -37,5 +38,8 @@
37
38
  "import": "./index.js",
38
39
  "require": "./index.cjs"
39
40
  }
41
+ },
42
+ "devDependencies": {
43
+ "@vctrl/viewer": "^1.0.0"
40
44
  }
41
45
  }
package/protocol.d.ts CHANGED
@@ -27,11 +27,36 @@ export interface EmbedCameraDescriptor {
27
27
  name: string;
28
28
  fov?: number;
29
29
  }
30
+ /**
31
+ * One hotspot, as a host page sees it.
32
+ *
33
+ * Carries no body, no link and no world position: a host builds navigation
34
+ * from these, and the content is what the viewer draws. Adding the text here
35
+ * would put a second copy of it on the page with nothing keeping the two in
36
+ * step.
37
+ */
38
+ export interface EmbedHotspotDescriptor {
39
+ id: string;
40
+ name: string;
41
+ /** The camera this hotspot flies, or null when it only reveals content. */
42
+ cameraId: string | null;
43
+ /** 1-based place in the navigation sequence, or null when it has none. */
44
+ step: number | null;
45
+ }
30
46
  export interface HostedPreviewPongMessage {
31
47
  source: typeof HOSTED_PREVIEW_VIEWER_SOURCE;
32
48
  type: 'pong';
33
49
  sceneId?: string;
34
50
  cameras: EmbedCameraDescriptor[];
51
+ /**
52
+ * Optional, unlike `cameras`.
53
+ *
54
+ * An iframe and the SDK on the page around it are two separately deployed
55
+ * artifacts and can be versions apart. A required field here would make an
56
+ * older iframe's pong fail a newer SDK's parse, and the host would then hang
57
+ * waiting for a handshake that already arrived.
58
+ */
59
+ hotspots?: EmbedHotspotDescriptor[];
35
60
  }
36
61
  export interface HostedPreviewViewerEventMessage {
37
62
  source: typeof HOSTED_PREVIEW_VIEWER_SOURCE;
@@ -1 +1 @@
1
- (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.VectrealEmbed={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`vectreal-host`,n=`vectreal-preview`;function r(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function i(e){if(!r(e)||typeof e.type!=`string`)return!1;switch(e.type){case`activate_camera`:return typeof e.cameraId==`string`&&e.cameraId.trim().length>0;case`set_controls_enabled`:return typeof e.enabled==`boolean`;case`set_auto_rotate`:return typeof e.enabled==`boolean`&&(e.speed===void 0||typeof e.speed==`number`);case`set_controls_options`:return(e.zoom===void 0||typeof e.zoom==`boolean`)&&(e.pan===void 0||typeof e.pan==`boolean`);case`set_transition`:return typeof e.transitionType==`string`&&[`none`,`linear`,`object_avoidance`].includes(e.transitionType);default:return!1}}function a(e){if(!r(e)||e.source!==`vectreal-host`||typeof e.type!=`string`)return!1;switch(e.type){case`ping`:return!0;case`viewer_command`:return i(e.command);case`host_scroll_progress`:return typeof e.progress==`number`&&Number.isFinite(e.progress);case`host_message`:return typeof e.message==`string`&&e.message.trim().length>0&&(e.payload===void 0||r(e.payload));default:return!1}}var o=15e3,s=class{constructor(e,t={}){this.handlers=new Map,this.pendingCommands=[],this.isReady=!1,this.pingIntervalId=null,this.sceneId=void 0,this.cameras=[],this.iframe=e,this.readyTimeout=t.readyTimeout??o;let n=e.src||e.getAttribute(`src`)||``,r;if(n)try{r=new URL(n,window.location.href).origin}catch{}if(!t.iframeOrigin&&!r)throw Error(`VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.`);this.targetOrigin=t.iframeOrigin??r,this.boundListener=this.handleMessage.bind(this),window.addEventListener(`message`,this.boundListener),this.startPingPolling()}ready(){return new Promise((e,t)=>{let n=window.setTimeout(()=>{this.off(`viewer_ready`,r),t(Error(`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`))},this.readyTimeout),r=()=>{window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras})};if(this.isReady){window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras});return}this.on(`viewer_ready`,r)})}destroy(){this.stopPingPolling(),window.removeEventListener(`message`,this.boundListener),this.handlers.clear(),this.pendingCommands=[]}activateCamera(e){this.sendCommand({type:`activate_camera`,cameraId:e})}setTransition(e){this.sendCommand({type:`set_transition`,transitionType:e.type,duration:e.duration,easing:e.easing})}setControlsEnabled(e){this.sendCommand({type:`set_controls_enabled`,enabled:e})}setAutoRotate(e,t){this.sendCommand({type:`set_auto_rotate`,enabled:e,speed:t})}setZoomEnabled(e){this.sendCommand({type:`set_controls_options`,zoom:e})}setPanEnabled(e){this.sendCommand({type:`set_controls_options`,pan:e})}sendScrollProgress(e){this.postToIframe({source:t,type:`host_scroll_progress`,progress:Math.min(1,Math.max(0,e))})}sendMessage(e,n){this.postToIframe({source:t,type:`host_message`,message:e,payload:n})}on(e,t){return this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t),()=>this.off(e,t)}off(e,t){this.handlers.get(e)?.delete(t)}sendCommand(e){let n={source:t,type:`viewer_command`,command:e};if(!this.isReady){this.pendingCommands.push(n);return}this.postToIframe(n)}postToIframe(e){try{this.iframe.contentWindow?.postMessage(e,this.targetOrigin)}catch{}}startPingPolling(){this.postToIframe({source:t,type:`ping`}),this.pingIntervalId=window.setInterval(()=>{if(this.isReady){this.stopPingPolling();return}this.postToIframe({source:t,type:`ping`})},500)}stopPingPolling(){this.pingIntervalId!==null&&(window.clearInterval(this.pingIntervalId),this.pingIntervalId=null)}flushPendingCommands(){for(let e of this.pendingCommands)this.postToIframe(e);this.pendingCommands=[]}emit(e,t){this.handlers.get(e)?.forEach(e=>{e(t)})}handleMessage(e){let t=e.data;if(!(!t||typeof t!=`object`||t.source!==`vectreal-preview`)&&!(this.targetOrigin!==`*`&&e.origin!==this.targetOrigin))switch(t.type){case`pong`:this.stopPingPolling(),this.sceneId=t.sceneId,this.cameras=t.cameras,this.isReady=!0,this.flushPendingCommands();break;case`viewer_event`:this.handleViewerEvent(t.event);break;case`interaction_event`:this.emit(`interaction_event`,{interactionId:t.interactionId,eventName:t.eventName,payload:t.payload});break}}handleViewerEvent(e){switch(e.type){case`viewer_ready`:this.isReady=!0,this.flushPendingCommands(),this.emit(`viewer_ready`,void 0);break;case`model_loaded`:this.emit(`model_loaded`,void 0);break;case`camera_changed`:this.emit(`camera_changed`,{cameraId:e.cameraId});break;case`auto_rotate_changed`:this.emit(`auto_rotate_changed`,{enabled:e.enabled});break;case`initial_framing_completed`:this.emit(`model_loaded`,void 0);break}}};e.HOSTED_PREVIEW_HOST_SOURCE=t,e.HOSTED_PREVIEW_VIEWER_SOURCE=n,e.VectrealEmbed=s,e.isHostedPreviewIncomingMessage=a,e.isViewerCommand=i});
1
+ (function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.VectrealEmbed={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var t=`vectreal-host`,n=`vectreal-preview`;function r(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function i(e){if(!r(e)||typeof e.type!=`string`)return!1;switch(e.type){case`activate_camera`:return typeof e.cameraId==`string`&&e.cameraId.trim().length>0;case`focus_hotspot`:return typeof e.hotspotId==`string`&&e.hotspotId.trim().length>0;case`set_controls_enabled`:return typeof e.enabled==`boolean`;case`set_auto_rotate`:return typeof e.enabled==`boolean`&&(e.speed===void 0||typeof e.speed==`number`);case`set_controls_options`:return(e.zoom===void 0||typeof e.zoom==`boolean`)&&(e.pan===void 0||typeof e.pan==`boolean`);case`set_transition`:return typeof e.transitionType==`string`&&[`none`,`linear`,`object_avoidance`].includes(e.transitionType);case`set_animation_playing`:return typeof e.playing==`boolean`;case`restart_animation`:return!0;case`seek_animation_clip`:return typeof e.clipId==`string`&&e.clipId.trim().length>0&&typeof e.time==`number`&&Number.isFinite(e.time)&&e.time>=0;default:return!1}}function a(e){if(!r(e)||e.source!==`vectreal-host`||typeof e.type!=`string`)return!1;switch(e.type){case`ping`:return!0;case`viewer_command`:return i(e.command);case`host_scroll_progress`:return typeof e.progress==`number`&&Number.isFinite(e.progress);case`host_message`:return typeof e.message==`string`&&e.message.trim().length>0&&(e.payload===void 0||r(e.payload));default:return!1}}var o=15e3,s=class{constructor(e,t={}){this.handlers=new Map,this.pendingCommands=[],this.isReady=!1,this.pingIntervalId=null,this.sceneId=void 0,this.cameras=[],this.hotspots=[],this.iframe=e,this.readyTimeout=t.readyTimeout??o;let n=e.src||e.getAttribute(`src`)||``,r;if(n)try{r=new URL(n,window.location.href).origin}catch{}if(!t.iframeOrigin&&!r)throw Error(`VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.`);this.targetOrigin=t.iframeOrigin??r,this.boundListener=this.handleMessage.bind(this),window.addEventListener(`message`,this.boundListener),this.startPingPolling()}ready(){return new Promise((e,t)=>{let n=window.setTimeout(()=>{this.off(`viewer_ready`,r),t(Error(`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`))},this.readyTimeout),r=()=>{window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras,hotspots:this.hotspots})};if(this.isReady){window.clearTimeout(n),e({sceneId:this.sceneId,cameras:this.cameras,hotspots:this.hotspots});return}this.on(`viewer_ready`,r)})}destroy(){this.stopPingPolling(),window.removeEventListener(`message`,this.boundListener),this.handlers.clear(),this.pendingCommands=[]}activateCamera(e){this.sendCommand({type:`activate_camera`,cameraId:e})}focusHotspot(e){this.sendCommand({type:`focus_hotspot`,hotspotId:e})}setTransition(e){this.sendCommand({type:`set_transition`,transitionType:e.type,duration:e.duration,easing:e.easing})}setControlsEnabled(e){this.sendCommand({type:`set_controls_enabled`,enabled:e})}setAutoRotate(e,t){this.sendCommand({type:`set_auto_rotate`,enabled:e,speed:t})}setZoomEnabled(e){this.sendCommand({type:`set_controls_options`,zoom:e})}setPanEnabled(e){this.sendCommand({type:`set_controls_options`,pan:e})}setAnimationPlaying(e){this.sendCommand({type:`set_animation_playing`,playing:e})}restartAnimation(){this.sendCommand({type:`restart_animation`})}sendScrollProgress(e){this.postToIframe({source:t,type:`host_scroll_progress`,progress:Math.min(1,Math.max(0,e))})}sendMessage(e,n){this.postToIframe({source:t,type:`host_message`,message:e,payload:n})}on(e,t){return this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(t),()=>this.off(e,t)}off(e,t){this.handlers.get(e)?.delete(t)}sendCommand(e){let n={source:t,type:`viewer_command`,command:e};if(!this.isReady){this.pendingCommands.push(n);return}this.postToIframe(n)}postToIframe(e){try{this.iframe.contentWindow?.postMessage(e,this.targetOrigin)}catch{}}startPingPolling(){this.postToIframe({source:t,type:`ping`}),this.pingIntervalId=window.setInterval(()=>{if(this.isReady){this.stopPingPolling();return}this.postToIframe({source:t,type:`ping`})},500)}stopPingPolling(){this.pingIntervalId!==null&&(window.clearInterval(this.pingIntervalId),this.pingIntervalId=null)}flushPendingCommands(){for(let e of this.pendingCommands)this.postToIframe(e);this.pendingCommands=[]}emit(e,t){this.handlers.get(e)?.forEach(e=>{e(t)})}handleMessage(e){let t=e.data;if(!(!t||typeof t!=`object`||t.source!==`vectreal-preview`)&&(this.targetOrigin===`*`||e.origin===this.targetOrigin))switch(t.type){case`pong`:this.stopPingPolling(),this.sceneId=t.sceneId,this.cameras=t.cameras,this.hotspots=t.hotspots??[],this.isReady=!0,this.flushPendingCommands();break;case`viewer_event`:this.handleViewerEvent(t.event);break;case`interaction_event`:this.emit(`interaction_event`,{interactionId:t.interactionId,eventName:t.eventName,payload:t.payload})}}handleViewerEvent(e){switch(e.type){case`viewer_ready`:this.isReady=!0,this.flushPendingCommands(),this.emit(`viewer_ready`,void 0);break;case`model_loaded`:this.emit(`model_loaded`,void 0);break;case`camera_changed`:this.emit(`camera_changed`,{cameraId:e.cameraId});break;case`hotspot_activated`:this.emit(`hotspot_activated`,{hotspotId:e.hotspotId,cameraId:e.cameraId});break;case`auto_rotate_changed`:this.emit(`auto_rotate_changed`,{enabled:e.enabled});break;case`animation_state_changed`:this.emit(`animation_state_changed`,{playing:e.playing,activeClipId:e.activeClipId,complete:e.complete});break;case`animation_clip_finished`:this.emit(`animation_clip_finished`,{clipId:e.clipId});break;case`initial_framing_completed`:this.emit(`model_loaded`,void 0)}}};e.HOSTED_PREVIEW_HOST_SOURCE=t,e.HOSTED_PREVIEW_VIEWER_SOURCE=n,e.VectrealEmbed=s,e.isHostedPreviewIncomingMessage=a,e.isViewerCommand=i});