@vctrl/embed 0.22.0 → 0.25.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 +23 -0
- package/README.md +10 -3
- package/embed.d.ts +86 -0
- package/index.cjs +1 -0
- package/index.d.ts +4 -0
- package/index.js +209 -0
- package/package.json +3 -3
- package/protocol.d.ts +52 -0
- package/vectreal-embed.umd.js +1 -0
- package/project.json +0 -63
- package/src/embed.ts +0 -362
- package/src/index.ts +0 -28
- package/src/protocol.ts +0 -149
- package/tsconfig.json +0 -19
- package/tsconfig.lib.json +0 -15
- package/vite.config.ts +0 -49
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.0](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.24.1...embed-v0.25.0) (2026-08-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **packages:** match repository.url org casing for npm provenance ([455caa9](https://github.com/Vectreal/vectreal-platform/commit/455caa927ff7a668ea2b6af6a633d9057d69f203))
|
|
9
|
+
* **packages:** match repository.url org casing for npm provenance ([7289fdc](https://github.com/Vectreal/vectreal-platform/commit/7289fdcbc5678588acccaf5b650d62a4d70a7e10))
|
|
10
|
+
|
|
11
|
+
## [0.24.1](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.24.0...embed-v0.24.1) (2026-08-02)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **ci:** resolve workspace protocol at publish time and publish via OIDC ([07e4b62](https://github.com/Vectreal/vectreal-platform/commit/07e4b628be16b61f3ddea9c91cc36c052d60bc8f))
|
|
17
|
+
* **ci:** resolve workspace protocol at publish time and publish via OIDC ([03b9305](https://github.com/Vectreal/vectreal-platform/commit/03b9305719b1cf67548f62730493ca6e3a84c823))
|
|
18
|
+
|
|
19
|
+
## [0.24.0](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.22.0...embed-v0.24.0) (2026-08-01)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **platform:** split /embed from /preview ([#662](https://github.com/Vectreal/vectreal-platform/issues/662)) ([19d9ec5](https://github.com/Vectreal/vectreal-platform/commit/19d9ec5cfa63a83fd04efbebb16ccac588fc325b))
|
|
25
|
+
|
|
3
26
|
## [0.22.0](https://github.com/Vectreal/vectreal-platform/compare/embed-v0.21.0...embed-v0.22.0) (2026-07-01)
|
|
4
27
|
|
|
5
28
|
|
package/README.md
CHANGED
|
@@ -11,19 +11,26 @@ Framework-agnostic JavaScript SDK for controlling Vectreal embedded 3D scene pre
|
|
|
11
11
|
npm install @vctrl/embed
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
**CDN (UMD
|
|
14
|
+
**CDN (UMD, no bundler needed):** the package ships a UMD build that any npm CDN can serve. Because the entry point uses named exports, the global is a namespace object and the class is `VectrealEmbed.VectrealEmbed`.
|
|
15
15
|
|
|
16
16
|
```html
|
|
17
|
-
<script src="https://
|
|
17
|
+
<script src="https://unpkg.com/@vctrl/embed/vectreal-embed.umd.js"></script>
|
|
18
|
+
<script>
|
|
19
|
+
const embed = new VectrealEmbed.VectrealEmbed(
|
|
20
|
+
document.getElementById('vectreal-scene')
|
|
21
|
+
)
|
|
22
|
+
</script>
|
|
18
23
|
```
|
|
19
24
|
|
|
25
|
+
For production, pin a version (`@vctrl/embed@<version>`) and add a [Subresource Integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hash for it.
|
|
26
|
+
|
|
20
27
|
## Quick start
|
|
21
28
|
|
|
22
29
|
```html
|
|
23
30
|
<div style="width: 100%; height: 400px;">
|
|
24
31
|
<iframe
|
|
25
32
|
id="vectreal-scene"
|
|
26
|
-
src="https://vectreal.com/
|
|
33
|
+
src="https://vectreal.com/embed/<projectId>/<sceneId>?token=YOUR_PREVIEW_API_KEY"
|
|
27
34
|
style="width: 100%; height: 100%; border: 0;"
|
|
28
35
|
allow="autoplay; xr-spatial-tracking"
|
|
29
36
|
allowfullscreen
|
package/embed.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { EmbedCameraDescriptor } from './protocol';
|
|
2
|
+
export interface EmbedOptions {
|
|
3
|
+
/** Override the detected iframe origin for postMessage targeting. */
|
|
4
|
+
iframeOrigin?: string;
|
|
5
|
+
/** Milliseconds before ready() rejects. Default: 15 000. */
|
|
6
|
+
readyTimeout?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EmbedReadyInfo {
|
|
9
|
+
sceneId: string | undefined;
|
|
10
|
+
cameras: EmbedCameraDescriptor[];
|
|
11
|
+
}
|
|
12
|
+
export interface SetTransitionOptions {
|
|
13
|
+
type: 'none' | 'linear' | 'object_avoidance';
|
|
14
|
+
duration?: number;
|
|
15
|
+
easing?: 'linear' | 'ease_in' | 'ease_out' | 'ease_in_out';
|
|
16
|
+
}
|
|
17
|
+
export type EmbedEventMap = {
|
|
18
|
+
viewer_ready: void;
|
|
19
|
+
model_loaded: void;
|
|
20
|
+
camera_changed: {
|
|
21
|
+
cameraId: string;
|
|
22
|
+
};
|
|
23
|
+
auto_rotate_changed: {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
};
|
|
26
|
+
interaction_event: {
|
|
27
|
+
interactionId?: string;
|
|
28
|
+
eventName: string;
|
|
29
|
+
payload?: Record<string, unknown>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export type EmbedEventType = keyof EmbedEventMap;
|
|
33
|
+
export type EmbedEventHandler<K extends EmbedEventType> = (data: EmbedEventMap[K]) => void;
|
|
34
|
+
export declare class VectrealEmbed {
|
|
35
|
+
private readonly iframe;
|
|
36
|
+
private readonly targetOrigin;
|
|
37
|
+
private readonly readyTimeout;
|
|
38
|
+
private handlers;
|
|
39
|
+
private pendingCommands;
|
|
40
|
+
private isReady;
|
|
41
|
+
private boundListener;
|
|
42
|
+
private pingIntervalId;
|
|
43
|
+
constructor(iframe: HTMLIFrameElement, options?: EmbedOptions);
|
|
44
|
+
/**
|
|
45
|
+
* Resolves when the embedded viewer emits viewer_ready.
|
|
46
|
+
* Rejects if no response within readyTimeout.
|
|
47
|
+
*/
|
|
48
|
+
ready(): Promise<EmbedReadyInfo>;
|
|
49
|
+
/** Remove all listeners and stop responding to messages. */
|
|
50
|
+
destroy(): void;
|
|
51
|
+
/** Switch to a named camera. */
|
|
52
|
+
activateCamera(cameraId: string): void;
|
|
53
|
+
/** Override the transition behaviour for subsequent camera switches. */
|
|
54
|
+
setTransition(options: SetTransitionOptions): void;
|
|
55
|
+
/** Enable or disable orbit controls. */
|
|
56
|
+
setControlsEnabled(enabled: boolean): void;
|
|
57
|
+
/** Toggle auto-rotate. */
|
|
58
|
+
setAutoRotate(enabled: boolean, speed?: number): void;
|
|
59
|
+
/** Toggle scroll-zoom. */
|
|
60
|
+
setZoomEnabled(enabled: boolean): void;
|
|
61
|
+
/** Toggle right-click pan. */
|
|
62
|
+
setPanEnabled(enabled: boolean): void;
|
|
63
|
+
/**
|
|
64
|
+
* Drive scroll-triggered interactions defined in the Publisher.
|
|
65
|
+
* @param progress 0 (page top) – 1 (page bottom)
|
|
66
|
+
*/
|
|
67
|
+
sendScrollProgress(progress: number): void;
|
|
68
|
+
/**
|
|
69
|
+
* Trigger a named host_message interaction defined in the Publisher.
|
|
70
|
+
*/
|
|
71
|
+
sendMessage(message: string, payload?: Record<string, unknown>): void;
|
|
72
|
+
/** Subscribe to a viewer event. Returns an unsubscribe function. */
|
|
73
|
+
on<K extends EmbedEventType>(type: K, handler: EmbedEventHandler<K>): () => void;
|
|
74
|
+
/** Remove a specific handler. */
|
|
75
|
+
off<K extends EmbedEventType>(type: K, handler: EmbedEventHandler<K>): void;
|
|
76
|
+
private sceneId;
|
|
77
|
+
private cameras;
|
|
78
|
+
private sendCommand;
|
|
79
|
+
private postToIframe;
|
|
80
|
+
private startPingPolling;
|
|
81
|
+
private stopPingPolling;
|
|
82
|
+
private flushPendingCommands;
|
|
83
|
+
private emit;
|
|
84
|
+
private handleMessage;
|
|
85
|
+
private handleViewerEvent;
|
|
86
|
+
}
|
package/index.cjs
ADDED
|
@@ -0,0 +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;
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { VectrealEmbed } from './embed';
|
|
2
|
+
export type { EmbedEventHandler, EmbedEventMap, EmbedEventType, EmbedOptions, EmbedReadyInfo, SetTransitionOptions } from './embed';
|
|
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';
|
package/index.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
//#region packages/embed/src/protocol.ts
|
|
2
|
+
var e = "vectreal-host", t = "vectreal-preview";
|
|
3
|
+
function n(e) {
|
|
4
|
+
return typeof e == "object" && !!e && !Array.isArray(e);
|
|
5
|
+
}
|
|
6
|
+
function r(e) {
|
|
7
|
+
if (!n(e) || typeof e.type != "string") return !1;
|
|
8
|
+
switch (e.type) {
|
|
9
|
+
case "activate_camera": return typeof e.cameraId == "string" && e.cameraId.trim().length > 0;
|
|
10
|
+
case "set_controls_enabled": return typeof e.enabled == "boolean";
|
|
11
|
+
case "set_auto_rotate": return typeof e.enabled == "boolean" && (e.speed === void 0 || typeof e.speed == "number");
|
|
12
|
+
case "set_controls_options": return (e.zoom === void 0 || typeof e.zoom == "boolean") && (e.pan === void 0 || typeof e.pan == "boolean");
|
|
13
|
+
case "set_transition": return typeof e.transitionType == "string" && [
|
|
14
|
+
"none",
|
|
15
|
+
"linear",
|
|
16
|
+
"object_avoidance"
|
|
17
|
+
].includes(e.transitionType);
|
|
18
|
+
default: return !1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function i(e) {
|
|
22
|
+
if (!n(e) || e.source !== "vectreal-host" || typeof e.type != "string") return !1;
|
|
23
|
+
switch (e.type) {
|
|
24
|
+
case "ping": return !0;
|
|
25
|
+
case "viewer_command": return r(e.command);
|
|
26
|
+
case "host_scroll_progress": return typeof e.progress == "number" && Number.isFinite(e.progress);
|
|
27
|
+
case "host_message": return typeof e.message == "string" && e.message.trim().length > 0 && (e.payload === void 0 || n(e.payload));
|
|
28
|
+
default: return !1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region packages/embed/src/embed.ts
|
|
33
|
+
var a = 15e3, o = class {
|
|
34
|
+
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;
|
|
36
|
+
let n = e.src || e.getAttribute("src") || "", r;
|
|
37
|
+
if (n) try {
|
|
38
|
+
r = new URL(n, window.location.href).origin;
|
|
39
|
+
} catch {}
|
|
40
|
+
if (!t.iframeOrigin && !r) throw Error("VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.");
|
|
41
|
+
this.targetOrigin = t.iframeOrigin ?? r, this.boundListener = this.handleMessage.bind(this), window.addEventListener("message", this.boundListener), this.startPingPolling();
|
|
42
|
+
}
|
|
43
|
+
ready() {
|
|
44
|
+
return new Promise((e, t) => {
|
|
45
|
+
let n = window.setTimeout(() => {
|
|
46
|
+
this.off("viewer_ready", r), t(/* @__PURE__ */ Error(`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`));
|
|
47
|
+
}, this.readyTimeout), r = () => {
|
|
48
|
+
window.clearTimeout(n), e({
|
|
49
|
+
sceneId: this.sceneId,
|
|
50
|
+
cameras: this.cameras
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
if (this.isReady) {
|
|
54
|
+
window.clearTimeout(n), e({
|
|
55
|
+
sceneId: this.sceneId,
|
|
56
|
+
cameras: this.cameras
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.on("viewer_ready", r);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
destroy() {
|
|
64
|
+
this.stopPingPolling(), window.removeEventListener("message", this.boundListener), this.handlers.clear(), this.pendingCommands = [];
|
|
65
|
+
}
|
|
66
|
+
activateCamera(e) {
|
|
67
|
+
this.sendCommand({
|
|
68
|
+
type: "activate_camera",
|
|
69
|
+
cameraId: e
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
setTransition(e) {
|
|
73
|
+
this.sendCommand({
|
|
74
|
+
type: "set_transition",
|
|
75
|
+
transitionType: e.type,
|
|
76
|
+
duration: e.duration,
|
|
77
|
+
easing: e.easing
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
setControlsEnabled(e) {
|
|
81
|
+
this.sendCommand({
|
|
82
|
+
type: "set_controls_enabled",
|
|
83
|
+
enabled: e
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
setAutoRotate(e, t) {
|
|
87
|
+
this.sendCommand({
|
|
88
|
+
type: "set_auto_rotate",
|
|
89
|
+
enabled: e,
|
|
90
|
+
speed: t
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
setZoomEnabled(e) {
|
|
94
|
+
this.sendCommand({
|
|
95
|
+
type: "set_controls_options",
|
|
96
|
+
zoom: e
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
setPanEnabled(e) {
|
|
100
|
+
this.sendCommand({
|
|
101
|
+
type: "set_controls_options",
|
|
102
|
+
pan: e
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
sendScrollProgress(t) {
|
|
106
|
+
this.postToIframe({
|
|
107
|
+
source: e,
|
|
108
|
+
type: "host_scroll_progress",
|
|
109
|
+
progress: Math.min(1, Math.max(0, t))
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
sendMessage(t, n) {
|
|
113
|
+
this.postToIframe({
|
|
114
|
+
source: e,
|
|
115
|
+
type: "host_message",
|
|
116
|
+
message: t,
|
|
117
|
+
payload: n
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
on(e, t) {
|
|
121
|
+
return this.handlers.has(e) || this.handlers.set(e, /* @__PURE__ */ new Set()), this.handlers.get(e).add(t), () => this.off(e, t);
|
|
122
|
+
}
|
|
123
|
+
off(e, t) {
|
|
124
|
+
this.handlers.get(e)?.delete(t);
|
|
125
|
+
}
|
|
126
|
+
sendCommand(t) {
|
|
127
|
+
let n = {
|
|
128
|
+
source: e,
|
|
129
|
+
type: "viewer_command",
|
|
130
|
+
command: t
|
|
131
|
+
};
|
|
132
|
+
if (!this.isReady) {
|
|
133
|
+
this.pendingCommands.push(n);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.postToIframe(n);
|
|
137
|
+
}
|
|
138
|
+
postToIframe(e) {
|
|
139
|
+
try {
|
|
140
|
+
this.iframe.contentWindow?.postMessage(e, this.targetOrigin);
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
startPingPolling() {
|
|
144
|
+
this.postToIframe({
|
|
145
|
+
source: e,
|
|
146
|
+
type: "ping"
|
|
147
|
+
}), this.pingIntervalId = window.setInterval(() => {
|
|
148
|
+
if (this.isReady) {
|
|
149
|
+
this.stopPingPolling();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.postToIframe({
|
|
153
|
+
source: e,
|
|
154
|
+
type: "ping"
|
|
155
|
+
});
|
|
156
|
+
}, 500);
|
|
157
|
+
}
|
|
158
|
+
stopPingPolling() {
|
|
159
|
+
this.pingIntervalId !== null && (window.clearInterval(this.pingIntervalId), this.pingIntervalId = null);
|
|
160
|
+
}
|
|
161
|
+
flushPendingCommands() {
|
|
162
|
+
for (let e of this.pendingCommands) this.postToIframe(e);
|
|
163
|
+
this.pendingCommands = [];
|
|
164
|
+
}
|
|
165
|
+
emit(e, t) {
|
|
166
|
+
this.handlers.get(e)?.forEach((e) => {
|
|
167
|
+
e(t);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
handleMessage(e) {
|
|
171
|
+
let t = e.data;
|
|
172
|
+
if (!(!t || typeof t != "object" || t.source !== "vectreal-preview") && !(this.targetOrigin !== "*" && e.origin !== this.targetOrigin)) switch (t.type) {
|
|
173
|
+
case "pong":
|
|
174
|
+
this.stopPingPolling(), this.sceneId = t.sceneId, this.cameras = t.cameras, this.isReady = !0, this.flushPendingCommands();
|
|
175
|
+
break;
|
|
176
|
+
case "viewer_event":
|
|
177
|
+
this.handleViewerEvent(t.event);
|
|
178
|
+
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;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
handleViewerEvent(e) {
|
|
189
|
+
switch (e.type) {
|
|
190
|
+
case "viewer_ready":
|
|
191
|
+
this.isReady = !0, this.flushPendingCommands(), this.emit("viewer_ready", void 0);
|
|
192
|
+
break;
|
|
193
|
+
case "model_loaded":
|
|
194
|
+
this.emit("model_loaded", void 0);
|
|
195
|
+
break;
|
|
196
|
+
case "camera_changed":
|
|
197
|
+
this.emit("camera_changed", { cameraId: e.cameraId });
|
|
198
|
+
break;
|
|
199
|
+
case "auto_rotate_changed":
|
|
200
|
+
this.emit("auto_rotate_changed", { enabled: e.enabled });
|
|
201
|
+
break;
|
|
202
|
+
case "initial_framing_completed":
|
|
203
|
+
this.emit("model_loaded", void 0);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
//#endregion
|
|
209
|
+
export { e as HOSTED_PREVIEW_HOST_SOURCE, t as HOSTED_PREVIEW_VIEWER_SOURCE, o as VectrealEmbed, i as isHostedPreviewIncomingMessage, r as isViewerCommand };
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.25.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": {
|
|
6
|
-
"url": "https://github.com/
|
|
6
|
+
"url": "https://github.com/Vectreal/vectreal-platform/issues"
|
|
7
7
|
},
|
|
8
8
|
"homepage": "https://vectreal.com",
|
|
9
9
|
"keywords": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/
|
|
22
|
+
"url": "git+https://github.com/Vectreal/vectreal-platform.git"
|
|
23
23
|
},
|
|
24
24
|
"license": "AGPL-3.0-only",
|
|
25
25
|
"main": "./index.cjs",
|
package/protocol.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ViewerCommand, ViewerInteractionEvent } from '../../viewer/src/index.ts';
|
|
2
|
+
export declare const HOSTED_PREVIEW_HOST_SOURCE = "vectreal-host";
|
|
3
|
+
export declare const HOSTED_PREVIEW_VIEWER_SOURCE = "vectreal-preview";
|
|
4
|
+
export interface HostedPreviewPingMessage {
|
|
5
|
+
source: typeof HOSTED_PREVIEW_HOST_SOURCE;
|
|
6
|
+
type: 'ping';
|
|
7
|
+
}
|
|
8
|
+
export interface HostedPreviewViewerCommandMessage {
|
|
9
|
+
source: typeof HOSTED_PREVIEW_HOST_SOURCE;
|
|
10
|
+
type: 'viewer_command';
|
|
11
|
+
command: ViewerCommand;
|
|
12
|
+
}
|
|
13
|
+
export interface HostedPreviewScrollProgressMessage {
|
|
14
|
+
source: typeof HOSTED_PREVIEW_HOST_SOURCE;
|
|
15
|
+
type: 'host_scroll_progress';
|
|
16
|
+
progress: number;
|
|
17
|
+
}
|
|
18
|
+
export interface HostedPreviewHostMessage {
|
|
19
|
+
source: typeof HOSTED_PREVIEW_HOST_SOURCE;
|
|
20
|
+
type: 'host_message';
|
|
21
|
+
message: string;
|
|
22
|
+
payload?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export type HostedPreviewIncomingMessage = HostedPreviewHostMessage | HostedPreviewPingMessage | HostedPreviewScrollProgressMessage | HostedPreviewViewerCommandMessage;
|
|
25
|
+
export interface EmbedCameraDescriptor {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
fov?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface HostedPreviewPongMessage {
|
|
31
|
+
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE;
|
|
32
|
+
type: 'pong';
|
|
33
|
+
sceneId?: string;
|
|
34
|
+
cameras: EmbedCameraDescriptor[];
|
|
35
|
+
}
|
|
36
|
+
export interface HostedPreviewViewerEventMessage {
|
|
37
|
+
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE;
|
|
38
|
+
type: 'viewer_event';
|
|
39
|
+
sceneId?: string;
|
|
40
|
+
event: ViewerInteractionEvent;
|
|
41
|
+
}
|
|
42
|
+
export interface HostedPreviewCustomEventMessage {
|
|
43
|
+
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE;
|
|
44
|
+
type: 'interaction_event';
|
|
45
|
+
sceneId?: string;
|
|
46
|
+
interactionId?: string;
|
|
47
|
+
eventName: string;
|
|
48
|
+
payload?: Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
export type HostedPreviewOutgoingMessage = HostedPreviewCustomEventMessage | HostedPreviewPongMessage | HostedPreviewViewerEventMessage;
|
|
51
|
+
export declare function isViewerCommand(value: unknown): value is ViewerCommand;
|
|
52
|
+
export declare function isHostedPreviewIncomingMessage(value: unknown): value is HostedPreviewIncomingMessage;
|
|
@@ -0,0 +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});
|
package/project.json
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "vctrl/embed",
|
|
3
|
-
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
-
"sourceRoot": "packages/embed/src",
|
|
5
|
-
"projectType": "library",
|
|
6
|
-
"tags": [],
|
|
7
|
-
"targets": {
|
|
8
|
-
"lint": {
|
|
9
|
-
"executor": "@nx/eslint:lint",
|
|
10
|
-
"outputs": ["{options.outputFile}"],
|
|
11
|
-
"options": {
|
|
12
|
-
"fix": true,
|
|
13
|
-
"lintFilePatterns": [
|
|
14
|
-
"packages/embed/src/**/*.{ts,js}",
|
|
15
|
-
"packages/embed/vite.config.ts",
|
|
16
|
-
"packages/embed/package.json"
|
|
17
|
-
]
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
"typecheck": {
|
|
21
|
-
"executor": "nx:run-commands",
|
|
22
|
-
"outputs": [],
|
|
23
|
-
"options": {
|
|
24
|
-
"command": "tsc --noEmit -p packages/embed/tsconfig.json"
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"build": {
|
|
28
|
-
"executor": "@nx/vite:build",
|
|
29
|
-
"outputs": ["{options.outputPath}"],
|
|
30
|
-
"options": {
|
|
31
|
-
"outputPath": "build/packages/vctrl/embed",
|
|
32
|
-
"configFile": "packages/embed/vite.config.ts"
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
"build-ci": {
|
|
36
|
-
"executor": "@nx/vite:build",
|
|
37
|
-
"outputs": ["{options.outputPath}"],
|
|
38
|
-
"options": {
|
|
39
|
-
"outputPath": "build/packages/vctrl/embed",
|
|
40
|
-
"configFile": "packages/embed/vite.config.ts"
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"copy-md": {
|
|
44
|
-
"dependsOn": ["build"],
|
|
45
|
-
"executor": "nx:run-commands",
|
|
46
|
-
"outputs": [],
|
|
47
|
-
"options": {
|
|
48
|
-
"commands": [
|
|
49
|
-
"rsync -rat packages/embed/*.md build/packages/vctrl/embed/"
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
"publish": {
|
|
54
|
-
"dependsOn": ["copy-md"],
|
|
55
|
-
"executor": "nx:run-commands",
|
|
56
|
-
"outputs": [],
|
|
57
|
-
"options": {
|
|
58
|
-
"command": "pnpm publish --no-git-checks --access public",
|
|
59
|
-
"cwd": "build/packages/vctrl/embed"
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
package/src/embed.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
HOSTED_PREVIEW_HOST_SOURCE,
|
|
3
|
-
HOSTED_PREVIEW_VIEWER_SOURCE,
|
|
4
|
-
type EmbedCameraDescriptor,
|
|
5
|
-
type HostedPreviewOutgoingMessage
|
|
6
|
-
} from './protocol'
|
|
7
|
-
|
|
8
|
-
import type { ViewerCommand, ViewerInteractionEvent } from '@vctrl/viewer'
|
|
9
|
-
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
// Public types
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
export interface EmbedOptions {
|
|
15
|
-
/** Override the detected iframe origin for postMessage targeting. */
|
|
16
|
-
iframeOrigin?: string
|
|
17
|
-
/** Milliseconds before ready() rejects. Default: 15 000. */
|
|
18
|
-
readyTimeout?: number
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface EmbedReadyInfo {
|
|
22
|
-
sceneId: string | undefined
|
|
23
|
-
cameras: EmbedCameraDescriptor[]
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SetTransitionOptions {
|
|
27
|
-
type: 'none' | 'linear' | 'object_avoidance'
|
|
28
|
-
duration?: number
|
|
29
|
-
easing?: 'linear' | 'ease_in' | 'ease_out' | 'ease_in_out'
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type EmbedEventMap = {
|
|
33
|
-
viewer_ready: void
|
|
34
|
-
model_loaded: void
|
|
35
|
-
camera_changed: { cameraId: string }
|
|
36
|
-
auto_rotate_changed: { enabled: boolean }
|
|
37
|
-
interaction_event: {
|
|
38
|
-
interactionId?: string
|
|
39
|
-
eventName: string
|
|
40
|
-
payload?: Record<string, unknown>
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export type EmbedEventType = keyof EmbedEventMap
|
|
45
|
-
export type EmbedEventHandler<K extends EmbedEventType> = (
|
|
46
|
-
data: EmbedEventMap[K]
|
|
47
|
-
) => void
|
|
48
|
-
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
// VectrealEmbed
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
|
-
|
|
53
|
-
const DEFAULT_READY_TIMEOUT_MS = 15_000
|
|
54
|
-
|
|
55
|
-
export class VectrealEmbed {
|
|
56
|
-
private readonly iframe: HTMLIFrameElement
|
|
57
|
-
private readonly targetOrigin: string
|
|
58
|
-
private readonly readyTimeout: number
|
|
59
|
-
|
|
60
|
-
private handlers = new Map<
|
|
61
|
-
EmbedEventType,
|
|
62
|
-
Set<EmbedEventHandler<EmbedEventType>>
|
|
63
|
-
>()
|
|
64
|
-
private pendingCommands: Array<{
|
|
65
|
-
source: string
|
|
66
|
-
type: string
|
|
67
|
-
[key: string]: unknown
|
|
68
|
-
}> = []
|
|
69
|
-
private isReady = false
|
|
70
|
-
private boundListener: (event: MessageEvent<unknown>) => void
|
|
71
|
-
private pingIntervalId: number | null = null
|
|
72
|
-
|
|
73
|
-
constructor(iframe: HTMLIFrameElement, options: EmbedOptions = {}) {
|
|
74
|
-
this.iframe = iframe
|
|
75
|
-
this.readyTimeout = options.readyTimeout ?? DEFAULT_READY_TIMEOUT_MS
|
|
76
|
-
|
|
77
|
-
const src = iframe.src || iframe.getAttribute('src') || ''
|
|
78
|
-
let detectedOrigin: string | undefined
|
|
79
|
-
if (src) {
|
|
80
|
-
try {
|
|
81
|
-
detectedOrigin = new URL(src, window.location.href).origin
|
|
82
|
-
} catch {
|
|
83
|
-
// unparseable src - fall through to require explicit iframeOrigin
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
if (!options.iframeOrigin && !detectedOrigin) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
'VectrealEmbed: cannot determine iframe origin from src. Pass options.iframeOrigin explicitly.'
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
this.targetOrigin = options.iframeOrigin ?? detectedOrigin!
|
|
92
|
-
|
|
93
|
-
this.boundListener = this.handleMessage.bind(this)
|
|
94
|
-
window.addEventListener('message', this.boundListener)
|
|
95
|
-
|
|
96
|
-
// Retry ping every 500 ms until we receive a pong. The iframe's React app
|
|
97
|
-
// may not have hydrated and registered its message listener yet when the
|
|
98
|
-
// constructor runs, so a single fire-and-forget ping is unreliable.
|
|
99
|
-
this.startPingPolling()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Lifecycle
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Resolves when the embedded viewer emits viewer_ready.
|
|
108
|
-
* Rejects if no response within readyTimeout.
|
|
109
|
-
*/
|
|
110
|
-
ready(): Promise<EmbedReadyInfo> {
|
|
111
|
-
return new Promise((resolve, reject) => {
|
|
112
|
-
const timer = window.setTimeout(() => {
|
|
113
|
-
this.off('viewer_ready', onReady as EmbedEventHandler<'viewer_ready'>)
|
|
114
|
-
reject(
|
|
115
|
-
new Error(
|
|
116
|
-
`VectrealEmbed: viewer did not become ready within ${this.readyTimeout}ms`
|
|
117
|
-
)
|
|
118
|
-
)
|
|
119
|
-
}, this.readyTimeout)
|
|
120
|
-
|
|
121
|
-
const onReady = () => {
|
|
122
|
-
window.clearTimeout(timer)
|
|
123
|
-
resolve({
|
|
124
|
-
sceneId: this.sceneId,
|
|
125
|
-
cameras: this.cameras
|
|
126
|
-
})
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (this.isReady) {
|
|
130
|
-
window.clearTimeout(timer)
|
|
131
|
-
resolve({ sceneId: this.sceneId, cameras: this.cameras })
|
|
132
|
-
return
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
this.on('viewer_ready', onReady as EmbedEventHandler<'viewer_ready'>)
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** Remove all listeners and stop responding to messages. */
|
|
140
|
-
destroy(): void {
|
|
141
|
-
this.stopPingPolling()
|
|
142
|
-
window.removeEventListener('message', this.boundListener)
|
|
143
|
-
this.handlers.clear()
|
|
144
|
-
this.pendingCommands = []
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ---------------------------------------------------------------------------
|
|
148
|
-
// Commands
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
|
|
151
|
-
/** Switch to a named camera. */
|
|
152
|
-
activateCamera(cameraId: string): void {
|
|
153
|
-
this.sendCommand({ type: 'activate_camera', cameraId })
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Override the transition behaviour for subsequent camera switches. */
|
|
157
|
-
setTransition(options: SetTransitionOptions): void {
|
|
158
|
-
this.sendCommand({
|
|
159
|
-
type: 'set_transition',
|
|
160
|
-
transitionType: options.type,
|
|
161
|
-
duration: options.duration,
|
|
162
|
-
easing: options.easing
|
|
163
|
-
})
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Enable or disable orbit controls. */
|
|
167
|
-
setControlsEnabled(enabled: boolean): void {
|
|
168
|
-
this.sendCommand({ type: 'set_controls_enabled', enabled })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Toggle auto-rotate. */
|
|
172
|
-
setAutoRotate(enabled: boolean, speed?: number): void {
|
|
173
|
-
this.sendCommand({ type: 'set_auto_rotate', enabled, speed })
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Toggle scroll-zoom. */
|
|
177
|
-
setZoomEnabled(enabled: boolean): void {
|
|
178
|
-
this.sendCommand({ type: 'set_controls_options', zoom: enabled })
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Toggle right-click pan. */
|
|
182
|
-
setPanEnabled(enabled: boolean): void {
|
|
183
|
-
this.sendCommand({ type: 'set_controls_options', pan: enabled })
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Drive scroll-triggered interactions defined in the Publisher.
|
|
188
|
-
* @param progress 0 (page top) – 1 (page bottom)
|
|
189
|
-
*/
|
|
190
|
-
sendScrollProgress(progress: number): void {
|
|
191
|
-
this.postToIframe({
|
|
192
|
-
source: HOSTED_PREVIEW_HOST_SOURCE,
|
|
193
|
-
type: 'host_scroll_progress',
|
|
194
|
-
progress: Math.min(1, Math.max(0, progress))
|
|
195
|
-
})
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Trigger a named host_message interaction defined in the Publisher.
|
|
200
|
-
*/
|
|
201
|
-
sendMessage(message: string, payload?: Record<string, unknown>): void {
|
|
202
|
-
this.postToIframe({
|
|
203
|
-
source: HOSTED_PREVIEW_HOST_SOURCE,
|
|
204
|
-
type: 'host_message',
|
|
205
|
-
message,
|
|
206
|
-
payload
|
|
207
|
-
})
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// Events
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
/** Subscribe to a viewer event. Returns an unsubscribe function. */
|
|
215
|
-
on<K extends EmbedEventType>(
|
|
216
|
-
type: K,
|
|
217
|
-
handler: EmbedEventHandler<K>
|
|
218
|
-
): () => void {
|
|
219
|
-
if (!this.handlers.has(type)) {
|
|
220
|
-
this.handlers.set(type, new Set())
|
|
221
|
-
}
|
|
222
|
-
this.handlers.get(type)!.add(handler as EmbedEventHandler<EmbedEventType>)
|
|
223
|
-
return () => this.off(type, handler)
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/** Remove a specific handler. */
|
|
227
|
-
off<K extends EmbedEventType>(type: K, handler: EmbedEventHandler<K>): void {
|
|
228
|
-
this.handlers
|
|
229
|
-
.get(type)
|
|
230
|
-
?.delete(handler as EmbedEventHandler<EmbedEventType>)
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// ---------------------------------------------------------------------------
|
|
234
|
-
// Internal state (populated via pong)
|
|
235
|
-
// ---------------------------------------------------------------------------
|
|
236
|
-
|
|
237
|
-
private sceneId: string | undefined = undefined
|
|
238
|
-
private cameras: EmbedCameraDescriptor[] = []
|
|
239
|
-
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
// Private helpers
|
|
242
|
-
// ---------------------------------------------------------------------------
|
|
243
|
-
|
|
244
|
-
private sendCommand(command: ViewerCommand): void {
|
|
245
|
-
const message = {
|
|
246
|
-
source: HOSTED_PREVIEW_HOST_SOURCE,
|
|
247
|
-
type: 'viewer_command' as const,
|
|
248
|
-
command
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (!this.isReady) {
|
|
252
|
-
this.pendingCommands.push(message)
|
|
253
|
-
return
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
this.postToIframe(message)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
private postToIframe(message: Record<string, unknown>): void {
|
|
260
|
-
try {
|
|
261
|
-
this.iframe.contentWindow?.postMessage(message, this.targetOrigin)
|
|
262
|
-
} catch {
|
|
263
|
-
// iframe may not be loaded yet; silently discard
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private startPingPolling(): void {
|
|
268
|
-
this.postToIframe({ source: HOSTED_PREVIEW_HOST_SOURCE, type: 'ping' })
|
|
269
|
-
this.pingIntervalId = window.setInterval(() => {
|
|
270
|
-
if (this.isReady) {
|
|
271
|
-
this.stopPingPolling()
|
|
272
|
-
return
|
|
273
|
-
}
|
|
274
|
-
this.postToIframe({ source: HOSTED_PREVIEW_HOST_SOURCE, type: 'ping' })
|
|
275
|
-
}, 500)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
private stopPingPolling(): void {
|
|
279
|
-
if (this.pingIntervalId !== null) {
|
|
280
|
-
window.clearInterval(this.pingIntervalId)
|
|
281
|
-
this.pingIntervalId = null
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private flushPendingCommands(): void {
|
|
286
|
-
for (const cmd of this.pendingCommands) {
|
|
287
|
-
this.postToIframe(cmd)
|
|
288
|
-
}
|
|
289
|
-
this.pendingCommands = []
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private emit<K extends EmbedEventType>(
|
|
293
|
-
type: K,
|
|
294
|
-
data: EmbedEventMap[K]
|
|
295
|
-
): void {
|
|
296
|
-
this.handlers.get(type)?.forEach((handler) => {
|
|
297
|
-
;(handler as EmbedEventHandler<K>)(data)
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private handleMessage(event: MessageEvent<unknown>): void {
|
|
302
|
-
const data = event.data as HostedPreviewOutgoingMessage
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
!data ||
|
|
306
|
-
typeof data !== 'object' ||
|
|
307
|
-
data.source !== HOSTED_PREVIEW_VIEWER_SOURCE
|
|
308
|
-
) {
|
|
309
|
-
return
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Validate the message origin matches our iframe
|
|
313
|
-
if (this.targetOrigin !== '*' && event.origin !== this.targetOrigin) {
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
switch (data.type) {
|
|
318
|
-
case 'pong':
|
|
319
|
-
this.stopPingPolling()
|
|
320
|
-
this.sceneId = data.sceneId
|
|
321
|
-
this.cameras = data.cameras
|
|
322
|
-
this.isReady = true
|
|
323
|
-
this.flushPendingCommands()
|
|
324
|
-
break
|
|
325
|
-
|
|
326
|
-
case 'viewer_event':
|
|
327
|
-
this.handleViewerEvent(data.event)
|
|
328
|
-
break
|
|
329
|
-
|
|
330
|
-
case 'interaction_event':
|
|
331
|
-
this.emit('interaction_event', {
|
|
332
|
-
interactionId: data.interactionId,
|
|
333
|
-
eventName: data.eventName,
|
|
334
|
-
payload: data.payload
|
|
335
|
-
})
|
|
336
|
-
break
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private handleViewerEvent(event: ViewerInteractionEvent): void {
|
|
341
|
-
switch (event.type) {
|
|
342
|
-
case 'viewer_ready':
|
|
343
|
-
this.isReady = true
|
|
344
|
-
this.flushPendingCommands()
|
|
345
|
-
this.emit('viewer_ready', undefined as void)
|
|
346
|
-
break
|
|
347
|
-
case 'model_loaded':
|
|
348
|
-
this.emit('model_loaded', undefined as void)
|
|
349
|
-
break
|
|
350
|
-
case 'camera_changed':
|
|
351
|
-
this.emit('camera_changed', { cameraId: event.cameraId })
|
|
352
|
-
break
|
|
353
|
-
case 'auto_rotate_changed':
|
|
354
|
-
this.emit('auto_rotate_changed', { enabled: event.enabled })
|
|
355
|
-
break
|
|
356
|
-
case 'initial_framing_completed':
|
|
357
|
-
// Translate to model_loaded for embed consumers - signals scene is visible
|
|
358
|
-
this.emit('model_loaded', undefined as void)
|
|
359
|
-
break
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export { VectrealEmbed } from './embed'
|
|
2
|
-
export type {
|
|
3
|
-
EmbedEventHandler,
|
|
4
|
-
EmbedEventMap,
|
|
5
|
-
EmbedEventType,
|
|
6
|
-
EmbedOptions,
|
|
7
|
-
EmbedReadyInfo,
|
|
8
|
-
SetTransitionOptions
|
|
9
|
-
} from './embed'
|
|
10
|
-
|
|
11
|
-
export {
|
|
12
|
-
HOSTED_PREVIEW_HOST_SOURCE,
|
|
13
|
-
HOSTED_PREVIEW_VIEWER_SOURCE,
|
|
14
|
-
isHostedPreviewIncomingMessage,
|
|
15
|
-
isViewerCommand
|
|
16
|
-
} from './protocol'
|
|
17
|
-
export type {
|
|
18
|
-
EmbedCameraDescriptor,
|
|
19
|
-
HostedPreviewCustomEventMessage,
|
|
20
|
-
HostedPreviewHostMessage,
|
|
21
|
-
HostedPreviewIncomingMessage,
|
|
22
|
-
HostedPreviewOutgoingMessage,
|
|
23
|
-
HostedPreviewPingMessage,
|
|
24
|
-
HostedPreviewPongMessage,
|
|
25
|
-
HostedPreviewScrollProgressMessage,
|
|
26
|
-
HostedPreviewViewerCommandMessage,
|
|
27
|
-
HostedPreviewViewerEventMessage
|
|
28
|
-
} from './protocol'
|
package/src/protocol.ts
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { ViewerCommand, ViewerInteractionEvent } from '@vctrl/viewer'
|
|
2
|
-
|
|
3
|
-
export const HOSTED_PREVIEW_HOST_SOURCE = 'vectreal-host'
|
|
4
|
-
export const HOSTED_PREVIEW_VIEWER_SOURCE = 'vectreal-preview'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Incoming messages (parent page → iframe)
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
export interface HostedPreviewPingMessage {
|
|
11
|
-
source: typeof HOSTED_PREVIEW_HOST_SOURCE
|
|
12
|
-
type: 'ping'
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface HostedPreviewViewerCommandMessage {
|
|
16
|
-
source: typeof HOSTED_PREVIEW_HOST_SOURCE
|
|
17
|
-
type: 'viewer_command'
|
|
18
|
-
command: ViewerCommand
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface HostedPreviewScrollProgressMessage {
|
|
22
|
-
source: typeof HOSTED_PREVIEW_HOST_SOURCE
|
|
23
|
-
type: 'host_scroll_progress'
|
|
24
|
-
progress: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface HostedPreviewHostMessage {
|
|
28
|
-
source: typeof HOSTED_PREVIEW_HOST_SOURCE
|
|
29
|
-
type: 'host_message'
|
|
30
|
-
message: string
|
|
31
|
-
payload?: Record<string, unknown>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export type HostedPreviewIncomingMessage =
|
|
35
|
-
| HostedPreviewHostMessage
|
|
36
|
-
| HostedPreviewPingMessage
|
|
37
|
-
| HostedPreviewScrollProgressMessage
|
|
38
|
-
| HostedPreviewViewerCommandMessage
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Outgoing messages (iframe → parent page)
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
export interface EmbedCameraDescriptor {
|
|
45
|
-
id: string
|
|
46
|
-
name: string
|
|
47
|
-
fov?: number
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface HostedPreviewPongMessage {
|
|
51
|
-
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE
|
|
52
|
-
type: 'pong'
|
|
53
|
-
sceneId?: string
|
|
54
|
-
cameras: EmbedCameraDescriptor[]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface HostedPreviewViewerEventMessage {
|
|
58
|
-
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE
|
|
59
|
-
type: 'viewer_event'
|
|
60
|
-
sceneId?: string
|
|
61
|
-
event: ViewerInteractionEvent
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export interface HostedPreviewCustomEventMessage {
|
|
65
|
-
source: typeof HOSTED_PREVIEW_VIEWER_SOURCE
|
|
66
|
-
type: 'interaction_event'
|
|
67
|
-
sceneId?: string
|
|
68
|
-
interactionId?: string
|
|
69
|
-
eventName: string
|
|
70
|
-
payload?: Record<string, unknown>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type HostedPreviewOutgoingMessage =
|
|
74
|
-
| HostedPreviewCustomEventMessage
|
|
75
|
-
| HostedPreviewPongMessage
|
|
76
|
-
| HostedPreviewViewerEventMessage
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Type guards
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
83
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function isViewerCommand(value: unknown): value is ViewerCommand {
|
|
87
|
-
if (!isRecord(value) || typeof value.type !== 'string') {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
switch (value.type) {
|
|
92
|
-
case 'activate_camera':
|
|
93
|
-
return (
|
|
94
|
-
typeof value.cameraId === 'string' && value.cameraId.trim().length > 0
|
|
95
|
-
)
|
|
96
|
-
case 'set_controls_enabled':
|
|
97
|
-
return typeof value.enabled === 'boolean'
|
|
98
|
-
case 'set_auto_rotate':
|
|
99
|
-
return (
|
|
100
|
-
typeof value.enabled === 'boolean' &&
|
|
101
|
-
(value.speed === undefined || typeof value.speed === 'number')
|
|
102
|
-
)
|
|
103
|
-
case 'set_controls_options':
|
|
104
|
-
return (
|
|
105
|
-
(value.zoom === undefined || typeof value.zoom === 'boolean') &&
|
|
106
|
-
(value.pan === undefined || typeof value.pan === 'boolean')
|
|
107
|
-
)
|
|
108
|
-
case 'set_transition':
|
|
109
|
-
return (
|
|
110
|
-
typeof value.transitionType === 'string' &&
|
|
111
|
-
['none', 'linear', 'object_avoidance'].includes(
|
|
112
|
-
value.transitionType as string
|
|
113
|
-
)
|
|
114
|
-
)
|
|
115
|
-
default:
|
|
116
|
-
return false
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function isHostedPreviewIncomingMessage(
|
|
121
|
-
value: unknown
|
|
122
|
-
): value is HostedPreviewIncomingMessage {
|
|
123
|
-
if (
|
|
124
|
-
!isRecord(value) ||
|
|
125
|
-
value.source !== HOSTED_PREVIEW_HOST_SOURCE ||
|
|
126
|
-
typeof value.type !== 'string'
|
|
127
|
-
) {
|
|
128
|
-
return false
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
switch (value.type) {
|
|
132
|
-
case 'ping':
|
|
133
|
-
return true
|
|
134
|
-
case 'viewer_command':
|
|
135
|
-
return isViewerCommand(value.command)
|
|
136
|
-
case 'host_scroll_progress':
|
|
137
|
-
return (
|
|
138
|
-
typeof value.progress === 'number' && Number.isFinite(value.progress)
|
|
139
|
-
)
|
|
140
|
-
case 'host_message':
|
|
141
|
-
return (
|
|
142
|
-
typeof value.message === 'string' &&
|
|
143
|
-
value.message.trim().length > 0 &&
|
|
144
|
-
(value.payload === undefined || isRecord(value.payload))
|
|
145
|
-
)
|
|
146
|
-
default:
|
|
147
|
-
return false
|
|
148
|
-
}
|
|
149
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"jsx": "react-jsx",
|
|
4
|
-
"allowJs": false,
|
|
5
|
-
"esModuleInterop": false,
|
|
6
|
-
"allowSyntheticDefaultImports": true,
|
|
7
|
-
"strict": true,
|
|
8
|
-
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
9
|
-
"types": ["vite/client"]
|
|
10
|
-
},
|
|
11
|
-
"files": [],
|
|
12
|
-
"include": [],
|
|
13
|
-
"references": [
|
|
14
|
-
{
|
|
15
|
-
"path": "./tsconfig.lib.json"
|
|
16
|
-
}
|
|
17
|
-
],
|
|
18
|
-
"extends": "../../tsconfig.base.json"
|
|
19
|
-
}
|
package/tsconfig.lib.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"outDir": "../../dist/out-tsc",
|
|
5
|
-
"declaration": true,
|
|
6
|
-
"types": ["node", "vite/client"]
|
|
7
|
-
},
|
|
8
|
-
"exclude": [
|
|
9
|
-
"**/*.spec.ts",
|
|
10
|
-
"**/*.test.ts",
|
|
11
|
-
"**/*.spec.js",
|
|
12
|
-
"**/*.test.js"
|
|
13
|
-
],
|
|
14
|
-
"include": ["src/**/*.ts"]
|
|
15
|
-
}
|
package/vite.config.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import * as path from 'path'
|
|
2
|
-
|
|
3
|
-
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'
|
|
4
|
-
import { defineConfig } from 'vite'
|
|
5
|
-
import dts from 'vite-plugin-dts'
|
|
6
|
-
|
|
7
|
-
export default defineConfig({
|
|
8
|
-
root: __dirname,
|
|
9
|
-
cacheDir: '../../node_modules/.vite/packages/@vctrl/embed',
|
|
10
|
-
plugins: [
|
|
11
|
-
nxViteTsPaths(),
|
|
12
|
-
dts({
|
|
13
|
-
entryRoot: 'src',
|
|
14
|
-
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json')
|
|
15
|
-
})
|
|
16
|
-
],
|
|
17
|
-
|
|
18
|
-
build: {
|
|
19
|
-
emptyOutDir: true,
|
|
20
|
-
reportCompressedSize: true,
|
|
21
|
-
commonjsOptions: {
|
|
22
|
-
transformMixedEsModules: true
|
|
23
|
-
},
|
|
24
|
-
lib: {
|
|
25
|
-
entry: 'src/index.ts',
|
|
26
|
-
name: '@vctrl/embed',
|
|
27
|
-
fileName: 'index',
|
|
28
|
-
formats: ['es', 'cjs']
|
|
29
|
-
},
|
|
30
|
-
rollupOptions: {
|
|
31
|
-
output: [
|
|
32
|
-
{
|
|
33
|
-
format: 'es',
|
|
34
|
-
entryFileNames: 'index.js'
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
format: 'cjs',
|
|
38
|
-
entryFileNames: 'index.cjs'
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
format: 'umd',
|
|
42
|
-
name: 'VectrealEmbed',
|
|
43
|
-
entryFileNames: 'vectreal-embed.umd.js',
|
|
44
|
-
exports: 'named'
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
})
|