@neurodyn/react-model-viewer 0.0.2 → 0.0.4
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/dist/index.d.ts +49 -5
- package/dist/index.js +612 -70
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -3,15 +3,33 @@ import { JSX } from 'react/jsx-runtime';
|
|
|
3
3
|
import type { default as React_2 } from 'react';
|
|
4
4
|
import * as React_3 from 'react';
|
|
5
5
|
|
|
6
|
+
/** What the host needs to open its own AR experience for the shown model. */
|
|
7
|
+
export declare interface ArLaunchTarget {
|
|
8
|
+
/** Material collection currently shown, which may differ from the entry url. */
|
|
9
|
+
hid?: string;
|
|
10
|
+
tinuuid: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
declare type DialogProps = React_3.ComponentProps<typeof DialogPrimitive.Root>
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Lets the host open its own AR experience instead of the native one.
|
|
17
|
+
*
|
|
18
|
+
* Returning `true` means the host took the launch over: the viewer then skips
|
|
19
|
+
* both the native handoff and its own `ar_launched`, so one tap stays one
|
|
20
|
+
* event. Anything else falls through to the usual behaviour.
|
|
21
|
+
*/
|
|
22
|
+
export declare type LaunchArOverride = (target: ArLaunchTarget) => boolean | Promise<boolean>;
|
|
23
|
+
|
|
24
|
+
export declare function ModelViewer({ tinuuid, apiKey, appClipBundleId, arExperience, hid, apiBaseUrl, legacyApiBaseUrl, mode, initialWidgetSessionToken, handoffUrl, launchAr, sizeType, layout, background, viewerSurface, className, ...props }: ModelViewerProps & React_2.ComponentProps<'div'>): JSX.Element;
|
|
25
|
+
|
|
26
|
+
export declare type ModelViewerArExperience = 'advanced' | 'standard';
|
|
9
27
|
|
|
10
28
|
declare type ModelViewerBackground = 'default' | 'transparent';
|
|
11
29
|
|
|
12
|
-
export declare function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }: ModelViewerDialogProps): JSX.Element;
|
|
30
|
+
export declare function ModelViewerDialog({ tinuuid, apiKey, apiBaseUrl, legacyApiBaseUrl, initialWidgetSessionToken, handoffUrl, hid, sizeType, ...props }: ModelViewerDialogProps): JSX.Element;
|
|
13
31
|
|
|
14
|
-
declare interface ModelViewerDialogProps extends Pick<ModelViewerRootProps, '
|
|
32
|
+
declare interface ModelViewerDialogProps extends Pick<ModelViewerRootProps, 'apiBaseUrl' | 'apiKey' | 'handoffUrl' | 'hid' | 'initialWidgetSessionToken' | 'legacyApiBaseUrl' | 'sizeType' | 'tinuuid'>, Omit<DialogProps, 'children'> {
|
|
15
33
|
}
|
|
16
34
|
|
|
17
35
|
declare type ModelViewerLayout = 'default' | 'model';
|
|
@@ -22,16 +40,42 @@ export declare interface ModelViewerProps extends Omit<ModelViewerRootProps, 'sh
|
|
|
22
40
|
declare interface ModelViewerRootProps {
|
|
23
41
|
/** The unique identifier of a Vizbl object. */
|
|
24
42
|
tinuuid: string;
|
|
25
|
-
/** Public API key. */
|
|
26
|
-
apiKey
|
|
43
|
+
/** Public API key. Without it, the viewer uses the legacy `/obj/Fetch` API. */
|
|
44
|
+
apiKey?: string | null;
|
|
45
|
+
/** App Clip bundle used by automatic Advanced AR handoffs. */
|
|
46
|
+
appClipBundleId?: string;
|
|
47
|
+
/** Standard or Advanced launch behavior for the shared QR backend mode. */
|
|
48
|
+
arExperience?: ModelViewerArExperience;
|
|
27
49
|
/** The identifier of a material variant. */
|
|
28
50
|
hid?: string | null;
|
|
51
|
+
/** Node Public API base URL. */
|
|
52
|
+
apiBaseUrl?: string;
|
|
53
|
+
/** Legacy Go API base URL used only when `apiKey` is missing. */
|
|
54
|
+
legacyApiBaseUrl?: string;
|
|
55
|
+
/** Widget integration mode recorded by the backend session. */
|
|
56
|
+
mode?: ModelViewerWidgetMode;
|
|
57
|
+
/** Existing token used to preserve the same widget session across a handoff. */
|
|
58
|
+
initialWidgetSessionToken?: string;
|
|
59
|
+
/** Hosted viewer URL encoded into the desktop-to-mobile QR handoff. */
|
|
60
|
+
handoffUrl?: string;
|
|
61
|
+
/** Lets the host open its own AR experience instead of the native handoff. */
|
|
62
|
+
launchAr?: LaunchArOverride;
|
|
63
|
+
/** Display unit for model dimensions. */
|
|
64
|
+
sizeType?: SizeType;
|
|
29
65
|
/** Controls which viewer UI elements are shown. */
|
|
30
66
|
layout?: ModelViewerLayout;
|
|
31
67
|
/** Controls the viewer container background. */
|
|
32
68
|
background?: ModelViewerBackground;
|
|
33
69
|
/** Enables dialog-only close controls. */
|
|
34
70
|
showDialogCloseButton?: boolean;
|
|
71
|
+
/** Controls open-event semantics for inline, dialog, and hosted-page viewers. */
|
|
72
|
+
viewerSurface?: ModelViewerSurface;
|
|
35
73
|
}
|
|
36
74
|
|
|
75
|
+
declare type ModelViewerSurface = 'dialog' | 'inline' | 'page';
|
|
76
|
+
|
|
77
|
+
export declare type ModelViewerWidgetMode = 'advanced-ar-view' | 'ar-view' | 'embedded-3d-viewer' | 'ar-viewer' | 'qr-code';
|
|
78
|
+
|
|
79
|
+
declare type SizeType = 'cm' | 'inch';
|
|
80
|
+
|
|
37
81
|
export { }
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, memo, use as external_react_use, useCallback, useEffect, useMemo as external_react_useMemo, useState } from "react";
|
|
2
|
+
import { createContext, memo, use as external_react_use, useCallback, useEffect, useMemo as external_react_useMemo, useRef, useState } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import react_shadow from "react-shadow";
|
|
5
5
|
import { QueryClient, QueryClientProvider, useQuery as react_query_useQuery } from "@tanstack/react-query";
|
|
@@ -919,6 +919,10 @@ button, input:where([type="button"], [type="reset"], [type="submit"]) {
|
|
|
919
919
|
height: auto;
|
|
920
920
|
}
|
|
921
921
|
|
|
922
|
+
.h-dvh {
|
|
923
|
+
height: 100dvh;
|
|
924
|
+
}
|
|
925
|
+
|
|
922
926
|
.h-full {
|
|
923
927
|
height: 100%;
|
|
924
928
|
}
|
|
@@ -927,10 +931,6 @@ button, input:where([type="button"], [type="reset"], [type="submit"]) {
|
|
|
927
931
|
height: 1px;
|
|
928
932
|
}
|
|
929
933
|
|
|
930
|
-
.h-svh {
|
|
931
|
-
height: 100svh;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
934
|
.max-h-\\(--radix-select-content-available-height\\) {
|
|
935
935
|
max-height: var(--radix-select-content-available-height);
|
|
936
936
|
}
|
|
@@ -5724,18 +5724,28 @@ function ThemeProvider({ children, defaultTheme = 'system', storageKey = 'v-them
|
|
|
5724
5724
|
}
|
|
5725
5725
|
var fontsinline = __webpack_require__("../ui/src/styles/fonts.css?inline");
|
|
5726
5726
|
const PROPERTY_BLOCK_RE = /@property\s+(--[^{\s]+)\s*\{([^}]*)\}/g;
|
|
5727
|
-
const INITIAL_VALUE_RE = /initial-value:([
|
|
5727
|
+
const INITIAL_VALUE_RE = /initial-value:([^;}]+)/i;
|
|
5728
|
+
const NON_INHERITING_RE = /inherits:\s*false(?:\s*;|\s*$)/i;
|
|
5728
5729
|
const ROOT_TO_HOST_SELECTOR_RE = /:root\s*,\s*:host|:root/g;
|
|
5729
5730
|
function createShadowRootCssText(cssText) {
|
|
5730
5731
|
const cssForHost = cssText.replace(ROOT_TO_HOST_SELECTOR_RE, ':host');
|
|
5731
5732
|
const propertyBlocks = Array.from(cssText.matchAll(PROPERTY_BLOCK_RE));
|
|
5732
5733
|
const propertyDefaults = propertyBlocks.map(([, name, body])=>{
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
return ` ${name}: ${initialValue};`;
|
|
5734
|
+
if (!NON_INHERITING_RE.test(body)) return null;
|
|
5735
|
+
const initialValue = body.match(INITIAL_VALUE_RE)?.[1].trim() ?? 'initial';
|
|
5736
|
+
return ` ${name}: ${initialValue};`;
|
|
5737
5737
|
}).filter((value)=>null !== value).join('\n');
|
|
5738
|
-
|
|
5738
|
+
if (!propertyDefaults) return cssForHost;
|
|
5739
|
+
return `${cssForHost}\n\n@layer base {
|
|
5740
|
+
:host,
|
|
5741
|
+
*,
|
|
5742
|
+
::before,
|
|
5743
|
+
::after,
|
|
5744
|
+
::backdrop,
|
|
5745
|
+
::file-selector-button {
|
|
5746
|
+
${propertyDefaults}
|
|
5747
|
+
}
|
|
5748
|
+
}`;
|
|
5739
5749
|
}
|
|
5740
5750
|
var styleinline = __webpack_require__("./src/style.css?inline");
|
|
5741
5751
|
const shadowRootCssText = createShadowRootCssText(styleinline.A);
|
|
@@ -6475,7 +6485,7 @@ const runtime = createApiClientRuntime({
|
|
|
6475
6485
|
configureFunctionName: 'configureNodeApiClient',
|
|
6476
6486
|
defaultBaseURL: "https://api.vizbl.com"
|
|
6477
6487
|
});
|
|
6478
|
-
|
|
6488
|
+
runtime.configureClientRuntime;
|
|
6479
6489
|
runtime.configureAccessTokenResolverRuntime;
|
|
6480
6490
|
runtime.configureHeadersResolverRuntime;
|
|
6481
6491
|
runtime.configureUnauthorizedHandlerRuntime;
|
|
@@ -6485,12 +6495,30 @@ runtime.resetClientForTestsRuntime;
|
|
|
6485
6495
|
const requestNodeRuntime = runtime.requestRuntime;
|
|
6486
6496
|
runtime.requestWithoutUnauthorizedRecovery;
|
|
6487
6497
|
runtime.addResponseInterceptorRuntime;
|
|
6488
|
-
function configureNodeApiClient(config) {
|
|
6489
|
-
configureNodeApiClientRuntime(config);
|
|
6490
|
-
}
|
|
6491
6498
|
async function node_client_node_client(config) {
|
|
6492
6499
|
return requestNodeRuntime(config);
|
|
6493
6500
|
}
|
|
6501
|
+
async function publicApiControllerActivateCoreAR(params, config = {}) {
|
|
6502
|
+
const { client: request = node_client_node_client, ...requestConfig } = config;
|
|
6503
|
+
const res = await request({
|
|
6504
|
+
method: "POST",
|
|
6505
|
+
url: "/public/widget/activate-core",
|
|
6506
|
+
params,
|
|
6507
|
+
...requestConfig
|
|
6508
|
+
});
|
|
6509
|
+
return res.data;
|
|
6510
|
+
}
|
|
6511
|
+
async function publicApiControllerIssueWidgetToken(data, config = {}) {
|
|
6512
|
+
const { client: request = node_client_node_client, ...requestConfig } = config;
|
|
6513
|
+
const requestData = data;
|
|
6514
|
+
const res = await request({
|
|
6515
|
+
method: "POST",
|
|
6516
|
+
url: "/public/widget/token",
|
|
6517
|
+
data: requestData,
|
|
6518
|
+
...requestConfig
|
|
6519
|
+
});
|
|
6520
|
+
return res.data;
|
|
6521
|
+
}
|
|
6494
6522
|
async function publicApiControllerReadObjectByTinuuid({ tinuuid }, params, config = {}) {
|
|
6495
6523
|
const { client: request = node_client_node_client, ...requestConfig } = config;
|
|
6496
6524
|
const res = await request({
|
|
@@ -6501,33 +6529,227 @@ async function publicApiControllerReadObjectByTinuuid({ tinuuid }, params, confi
|
|
|
6501
6529
|
});
|
|
6502
6530
|
return res.data;
|
|
6503
6531
|
}
|
|
6532
|
+
const WIDGET_SESSION_TOKEN_HEADER = 'x-widget-session-token';
|
|
6533
|
+
const DEFAULT_REFRESH_THRESHOLD_MS = 30000;
|
|
6534
|
+
const ALLOWED_PRODUCT_TYPES = new Set([
|
|
6535
|
+
'core-ar',
|
|
6536
|
+
'core-ar-shopify'
|
|
6537
|
+
]);
|
|
6538
|
+
class ModelViewerWidgetSession {
|
|
6539
|
+
apiBaseUrl;
|
|
6540
|
+
apiKey;
|
|
6541
|
+
issueWidgetToken;
|
|
6542
|
+
mode;
|
|
6543
|
+
now;
|
|
6544
|
+
refreshThresholdMs;
|
|
6545
|
+
refreshPromise = null;
|
|
6546
|
+
session;
|
|
6547
|
+
constructor(activationData, options){
|
|
6548
|
+
this.apiBaseUrl = options.apiBaseUrl;
|
|
6549
|
+
this.apiKey = options.apiKey;
|
|
6550
|
+
this.issueWidgetToken = options.issueWidgetToken ?? publicApiControllerIssueWidgetToken;
|
|
6551
|
+
this.mode = options.mode;
|
|
6552
|
+
this.now = options.now ?? Date.now;
|
|
6553
|
+
this.refreshThresholdMs = options.refreshThresholdMs ?? DEFAULT_REFRESH_THRESHOLD_MS;
|
|
6554
|
+
this.session = this.toSnapshot(activationData);
|
|
6555
|
+
}
|
|
6556
|
+
get token() {
|
|
6557
|
+
return this.session.token;
|
|
6558
|
+
}
|
|
6559
|
+
get sid() {
|
|
6560
|
+
return this.session.sid;
|
|
6561
|
+
}
|
|
6562
|
+
get snapshot() {
|
|
6563
|
+
return {
|
|
6564
|
+
...this.session
|
|
6565
|
+
};
|
|
6566
|
+
}
|
|
6567
|
+
async getTokenForRequest() {
|
|
6568
|
+
if (this.session.expiresAt - this.now() > this.refreshThresholdMs) return this.session.token;
|
|
6569
|
+
return (await this.refresh()).token;
|
|
6570
|
+
}
|
|
6571
|
+
async getRequestConfig(signal) {
|
|
6572
|
+
return {
|
|
6573
|
+
baseURL: this.apiBaseUrl,
|
|
6574
|
+
headers: {
|
|
6575
|
+
[WIDGET_SESSION_TOKEN_HEADER]: await this.getTokenForRequest()
|
|
6576
|
+
},
|
|
6577
|
+
signal
|
|
6578
|
+
};
|
|
6579
|
+
}
|
|
6580
|
+
refresh() {
|
|
6581
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
6582
|
+
this.refreshPromise = this.issueWidgetToken({
|
|
6583
|
+
mode: this.mode,
|
|
6584
|
+
token: this.session.token
|
|
6585
|
+
}, {
|
|
6586
|
+
baseURL: this.apiBaseUrl,
|
|
6587
|
+
headers: {
|
|
6588
|
+
'x-api-key': this.apiKey
|
|
6589
|
+
}
|
|
6590
|
+
}).then((data)=>this.toSnapshot(parseWidgetSession(data))).then((session)=>{
|
|
6591
|
+
this.session = session;
|
|
6592
|
+
return this.snapshot;
|
|
6593
|
+
}).finally(()=>{
|
|
6594
|
+
this.refreshPromise = null;
|
|
6595
|
+
});
|
|
6596
|
+
return this.refreshPromise;
|
|
6597
|
+
}
|
|
6598
|
+
toSnapshot(data) {
|
|
6599
|
+
return {
|
|
6600
|
+
...data,
|
|
6601
|
+
expiresAt: this.now() + 1000 * data.expiresIn
|
|
6602
|
+
};
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
async function createModelViewerWidgetSession({ apiBaseUrl, apiKey, initialWidgetSessionToken, mode, signal }) {
|
|
6606
|
+
const requestConfig = {
|
|
6607
|
+
baseURL: apiBaseUrl,
|
|
6608
|
+
headers: {
|
|
6609
|
+
'x-api-key': apiKey
|
|
6610
|
+
},
|
|
6611
|
+
signal
|
|
6612
|
+
};
|
|
6613
|
+
if (initialWidgetSessionToken) try {
|
|
6614
|
+
const refreshed = await publicApiControllerIssueWidgetToken({
|
|
6615
|
+
mode,
|
|
6616
|
+
token: initialWidgetSessionToken
|
|
6617
|
+
}, requestConfig);
|
|
6618
|
+
return new ModelViewerWidgetSession(parseWidgetSession(refreshed), {
|
|
6619
|
+
apiBaseUrl,
|
|
6620
|
+
apiKey,
|
|
6621
|
+
mode
|
|
6622
|
+
});
|
|
6623
|
+
} catch (error) {
|
|
6624
|
+
if (signal?.aborted) throw error;
|
|
6625
|
+
}
|
|
6626
|
+
const activation = await publicApiControllerActivateCoreAR({
|
|
6627
|
+
mode
|
|
6628
|
+
}, requestConfig);
|
|
6629
|
+
if ('string' != typeof activation.productType || !ALLOWED_PRODUCT_TYPES.has(activation.productType)) throw new TypeError('The API key is not enabled for Core AR');
|
|
6630
|
+
return new ModelViewerWidgetSession(parseWidgetSession(activation.widgetSession), {
|
|
6631
|
+
apiBaseUrl,
|
|
6632
|
+
apiKey,
|
|
6633
|
+
mode
|
|
6634
|
+
});
|
|
6635
|
+
}
|
|
6636
|
+
async function fetchModelViewerObject(tinuuid, variantId, session, signal) {
|
|
6637
|
+
return publicApiControllerReadObjectByTinuuid({
|
|
6638
|
+
tinuuid
|
|
6639
|
+
}, {
|
|
6640
|
+
productType: 'core-ar',
|
|
6641
|
+
variantId: variantId ?? void 0
|
|
6642
|
+
}, await session.getRequestConfig(signal));
|
|
6643
|
+
}
|
|
6644
|
+
async function sendModelViewerWidgetEvent(session, event, options = {}) {
|
|
6645
|
+
if (!session) return;
|
|
6646
|
+
try {
|
|
6647
|
+
const config = await session.getRequestConfig();
|
|
6648
|
+
const url = new URL('/public/widget/events', config.baseURL ?? window.location.origin);
|
|
6649
|
+
const response = await fetch(url, {
|
|
6650
|
+
body: JSON.stringify({
|
|
6651
|
+
event,
|
|
6652
|
+
objectId: options.objectId ?? void 0,
|
|
6653
|
+
timestamp: new Date().toISOString(),
|
|
6654
|
+
variantId: options.variantId ?? void 0
|
|
6655
|
+
}),
|
|
6656
|
+
headers: {
|
|
6657
|
+
'Content-Type': 'application/json',
|
|
6658
|
+
...config.headers
|
|
6659
|
+
},
|
|
6660
|
+
keepalive: true,
|
|
6661
|
+
method: 'POST'
|
|
6662
|
+
});
|
|
6663
|
+
if (!response.ok) throw new Error(`Widget event request failed with ${response.status}`);
|
|
6664
|
+
} catch {}
|
|
6665
|
+
}
|
|
6666
|
+
function parseWidgetSession(value) {
|
|
6667
|
+
const session = value;
|
|
6668
|
+
if (!session || 'object' != typeof session || 'string' != typeof session.token || !session.token.trim() || 'string' != typeof session.sid || !session.sid.trim() || 'number' != typeof session.expiresIn || !Number.isFinite(session.expiresIn) || session.expiresIn <= 0) throw new TypeError('Invalid widget session response');
|
|
6669
|
+
return session;
|
|
6670
|
+
}
|
|
6671
|
+
const client_runtime_runtime = createApiClientRuntime({
|
|
6672
|
+
clientName: 'API client',
|
|
6673
|
+
configureFunctionName: 'configureApiClient',
|
|
6674
|
+
defaultBaseURL: "https://api.vizbl.com/v1"
|
|
6675
|
+
});
|
|
6676
|
+
client_runtime_runtime.configureClientRuntime;
|
|
6677
|
+
client_runtime_runtime.configureAccessTokenResolverRuntime;
|
|
6678
|
+
client_runtime_runtime.configureHeadersResolverRuntime;
|
|
6679
|
+
client_runtime_runtime.configureUnauthorizedHandlerRuntime;
|
|
6680
|
+
client_runtime_runtime.isClientConfiguredRuntime;
|
|
6681
|
+
client_runtime_runtime.assertClientConfiguredRuntime;
|
|
6682
|
+
client_runtime_runtime.resetClientForTestsRuntime;
|
|
6683
|
+
const client_runtime_requestRuntime = client_runtime_runtime.requestRuntime;
|
|
6684
|
+
client_runtime_runtime.requestWithoutUnauthorizedRecovery;
|
|
6685
|
+
client_runtime_runtime.addResponseInterceptorRuntime;
|
|
6686
|
+
async function client_client(config) {
|
|
6687
|
+
return client_runtime_requestRuntime(config);
|
|
6688
|
+
}
|
|
6689
|
+
async function objFetch(data, config = {}) {
|
|
6690
|
+
const { client: request = client_client, ...requestConfig } = config;
|
|
6691
|
+
const requestData = data;
|
|
6692
|
+
const res = await request({
|
|
6693
|
+
method: "POST",
|
|
6694
|
+
url: "/obj/Fetch",
|
|
6695
|
+
data: requestData,
|
|
6696
|
+
...requestConfig
|
|
6697
|
+
});
|
|
6698
|
+
return res.data;
|
|
6699
|
+
}
|
|
6504
6700
|
function useObjectData(tinuuid, initialHid, options) {
|
|
6505
6701
|
const [materialHid, setMaterialHid] = useState(initialHid ?? null);
|
|
6506
6702
|
const { data = null, error, isLoading } = react_query_useQuery({
|
|
6507
6703
|
queryKey: [
|
|
6508
6704
|
'modelViewer',
|
|
6509
6705
|
'object',
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6706
|
+
{
|
|
6707
|
+
apiBaseUrl: options.apiBaseUrl ?? null,
|
|
6708
|
+
apiKey: options.apiKey,
|
|
6709
|
+
initialHid: initialHid ?? null,
|
|
6710
|
+
initialWidgetSessionToken: options.initialWidgetSessionToken ?? null,
|
|
6711
|
+
legacyApiBaseUrl: options.legacyApiBaseUrl ?? null,
|
|
6712
|
+
mode: options.mode,
|
|
6515
6713
|
tinuuid
|
|
6516
|
-
}
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6714
|
+
}
|
|
6715
|
+
],
|
|
6716
|
+
enabled: Boolean(tinuuid),
|
|
6717
|
+
gcTime: 0,
|
|
6718
|
+
refetchOnMount: 'always',
|
|
6719
|
+
queryFn: async ({ signal })=>{
|
|
6720
|
+
if (!options.apiKey) {
|
|
6721
|
+
const object = await objFetch({
|
|
6722
|
+
tinuuid
|
|
6723
|
+
}, {
|
|
6724
|
+
baseURL: options.legacyApiBaseUrl,
|
|
6725
|
+
signal
|
|
6726
|
+
});
|
|
6727
|
+
return {
|
|
6728
|
+
object,
|
|
6729
|
+
widgetSession: null
|
|
6730
|
+
};
|
|
6731
|
+
}
|
|
6732
|
+
const widgetSession = await createModelViewerWidgetSession({
|
|
6733
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
6734
|
+
apiKey: options.apiKey,
|
|
6735
|
+
initialWidgetSessionToken: options.initialWidgetSessionToken,
|
|
6736
|
+
mode: options.mode,
|
|
6522
6737
|
signal
|
|
6523
|
-
})
|
|
6738
|
+
});
|
|
6739
|
+
const object = await fetchModelViewerObject(tinuuid, initialHid, widgetSession, signal);
|
|
6740
|
+
return {
|
|
6741
|
+
object,
|
|
6742
|
+
widgetSession
|
|
6743
|
+
};
|
|
6744
|
+
}
|
|
6524
6745
|
});
|
|
6746
|
+
const object = data?.object ?? null;
|
|
6525
6747
|
const material = external_react_useMemo(()=>{
|
|
6526
|
-
if (!
|
|
6527
|
-
return materialHid ?
|
|
6748
|
+
if (!object?.materials?.length) return null;
|
|
6749
|
+
return materialHid ? object.materials.find((m)=>m.hid === materialHid) || object.materials[0] : object.materials?.[0] ?? null;
|
|
6528
6750
|
}, [
|
|
6529
|
-
|
|
6530
|
-
|
|
6751
|
+
materialHid,
|
|
6752
|
+
object
|
|
6531
6753
|
]);
|
|
6532
6754
|
useEffect(()=>{
|
|
6533
6755
|
setMaterialHid(initialHid ?? null);
|
|
@@ -6536,11 +6758,12 @@ function useObjectData(tinuuid, initialHid, options) {
|
|
|
6536
6758
|
tinuuid
|
|
6537
6759
|
]);
|
|
6538
6760
|
return {
|
|
6539
|
-
object
|
|
6761
|
+
object,
|
|
6540
6762
|
material,
|
|
6541
6763
|
error,
|
|
6542
6764
|
isLoading,
|
|
6543
|
-
setMaterialHid
|
|
6765
|
+
setMaterialHid,
|
|
6766
|
+
widgetSession: data?.widgetSession ?? null
|
|
6544
6767
|
};
|
|
6545
6768
|
}
|
|
6546
6769
|
function getErrorResponseStatus(error) {
|
|
@@ -6551,36 +6774,119 @@ function getModelViewerApiError(error) {
|
|
|
6551
6774
|
if (!isAxiosError(error)) return null;
|
|
6552
6775
|
const status = error.response?.status;
|
|
6553
6776
|
if (401 === status) return 'unauthorized';
|
|
6777
|
+
if (403 === status) return 'forbidden';
|
|
6554
6778
|
if (402 === status) return 'payment-required';
|
|
6555
6779
|
return null;
|
|
6556
6780
|
}
|
|
6781
|
+
const RECOVERY_STORAGE_KEY = 'vizbl-model-viewer-blank-canvas-recovery';
|
|
6782
|
+
const RENDER_CHECK_DELAY_MS = 2000;
|
|
6783
|
+
const SAMPLE_SIZE = 64;
|
|
6784
|
+
const NON_ZERO_CHANNEL_THRESHOLD = 8;
|
|
6785
|
+
function scheduleBlankCanvasRecovery(modelViewerEl) {
|
|
6786
|
+
window.setTimeout(()=>{
|
|
6787
|
+
if (isCanvasBlank(modelViewerEl) && !hasAlreadyRecovered()) {
|
|
6788
|
+
markRecovered();
|
|
6789
|
+
window.location.reload();
|
|
6790
|
+
}
|
|
6791
|
+
}, RENDER_CHECK_DELAY_MS);
|
|
6792
|
+
}
|
|
6793
|
+
function hasAlreadyRecovered() {
|
|
6794
|
+
try {
|
|
6795
|
+
return '1' === window.sessionStorage.getItem(RECOVERY_STORAGE_KEY);
|
|
6796
|
+
} catch {
|
|
6797
|
+
return false;
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
function markRecovered() {
|
|
6801
|
+
try {
|
|
6802
|
+
window.sessionStorage.setItem(RECOVERY_STORAGE_KEY, '1');
|
|
6803
|
+
} catch {}
|
|
6804
|
+
}
|
|
6805
|
+
function isCanvasBlank(modelViewerEl) {
|
|
6806
|
+
const canvas = modelViewerEl.shadowRoot?.querySelector('canvas.show');
|
|
6807
|
+
if (!canvas || 0 === canvas.width || 0 === canvas.height) return false;
|
|
6808
|
+
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl');
|
|
6809
|
+
if (!gl) return false;
|
|
6810
|
+
const size = Math.min(SAMPLE_SIZE, canvas.width, canvas.height);
|
|
6811
|
+
const x = Math.max(0, Math.floor((canvas.width - size) / 2));
|
|
6812
|
+
const y = Math.max(0, Math.floor((canvas.height - size) / 2));
|
|
6813
|
+
const pixels = new Uint8Array(size * size * 4);
|
|
6814
|
+
gl.readPixels(x, y, size, size, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
6815
|
+
for(let i = 0; i < pixels.length; i += 4)if (pixels[i] > NON_ZERO_CHANNEL_THRESHOLD || pixels[i + 1] > NON_ZERO_CHANNEL_THRESHOLD || pixels[i + 2] > NON_ZERO_CHANNEL_THRESHOLD || pixels[i + 3] > NON_ZERO_CHANNEL_THRESHOLD) return false;
|
|
6816
|
+
return true;
|
|
6817
|
+
}
|
|
6557
6818
|
const ModelViewerContext = /*#__PURE__*/ createContext(null);
|
|
6558
|
-
function ModelViewerProvider({ children, tinuuid, apiKey, hid, layout, background }) {
|
|
6559
|
-
const { object, material, error, isLoading, setMaterialHid } = useObjectData(tinuuid, hid, {
|
|
6560
|
-
|
|
6819
|
+
function ModelViewerProvider({ children, tinuuid, apiKey, appClipBundleId, arExperience, apiBaseUrl, legacyApiBaseUrl, mode, initialWidgetSessionToken, handoffUrl, hid, launchAr, layout, background, sizeType, viewerSurface }) {
|
|
6820
|
+
const { object, material, error, isLoading, setMaterialHid: setObjectMaterialHid, widgetSession } = useObjectData(tinuuid, hid, {
|
|
6821
|
+
apiBaseUrl,
|
|
6822
|
+
apiKey,
|
|
6823
|
+
initialWidgetSessionToken,
|
|
6824
|
+
legacyApiBaseUrl,
|
|
6825
|
+
mode
|
|
6561
6826
|
});
|
|
6562
6827
|
const [isModelLoading, setIsModelLoading] = useState(true);
|
|
6563
6828
|
const [containerEl, setContainerEl] = useState(null);
|
|
6564
6829
|
const [modelViewerEl, setModelViewerEl] = useState(null);
|
|
6565
|
-
const
|
|
6830
|
+
const trackedCameraInteractionSidRef = useRef(null);
|
|
6831
|
+
const trackedOpenSidRef = useRef(null);
|
|
6566
6832
|
const glbUrl = material?.native?.glbUrl ?? null;
|
|
6567
6833
|
const usdzUrl = material?.native?.usdzUrl ?? null;
|
|
6568
6834
|
const previewUrl = material?.previews?.[0]?.url ?? null;
|
|
6569
6835
|
const state = external_react_useMemo(()=>{
|
|
6570
|
-
if (!hasNonEmptyApiKey) return 'unauthorized';
|
|
6571
6836
|
if (isLoading) return 'loading';
|
|
6572
6837
|
const accessError = getModelViewerApiError(error);
|
|
6573
6838
|
if (accessError) return accessError;
|
|
6839
|
+
const instantArExperience = 'ar-view' === mode || 'qr-code' === mode && 'standard' === arExperience;
|
|
6840
|
+
const advancedArExperience = 'advanced-ar-view' === mode || 'qr-code' === mode && 'advanced' === arExperience;
|
|
6841
|
+
if (instantArExperience && object?.policy?.instantArView === false) return 'forbidden';
|
|
6842
|
+
if ((advancedArExperience || 'ar-viewer' === mode) && object?.policy?.appClip === false) return 'forbidden';
|
|
6574
6843
|
if (null != error || !object || !material || !glbUrl) return 'not-found';
|
|
6575
6844
|
return 'ready';
|
|
6576
6845
|
}, [
|
|
6846
|
+
arExperience,
|
|
6577
6847
|
error,
|
|
6578
6848
|
glbUrl,
|
|
6579
|
-
hasNonEmptyApiKey,
|
|
6580
6849
|
isLoading,
|
|
6581
6850
|
material,
|
|
6851
|
+
mode,
|
|
6582
6852
|
object
|
|
6583
6853
|
]);
|
|
6854
|
+
const trackEvent = useCallback((event)=>sendModelViewerWidgetEvent(widgetSession, event, {
|
|
6855
|
+
objectId: object?.uuid,
|
|
6856
|
+
variantId: material?.hid
|
|
6857
|
+
}), [
|
|
6858
|
+
material?.hid,
|
|
6859
|
+
object?.uuid,
|
|
6860
|
+
widgetSession
|
|
6861
|
+
]);
|
|
6862
|
+
const setMaterialHid = useCallback((nextHid)=>{
|
|
6863
|
+
if (nextHid === material?.hid) return;
|
|
6864
|
+
setObjectMaterialHid(nextHid);
|
|
6865
|
+
sendModelViewerWidgetEvent(widgetSession, 'interacted', {
|
|
6866
|
+
objectId: object?.uuid,
|
|
6867
|
+
variantId: nextHid
|
|
6868
|
+
});
|
|
6869
|
+
}, [
|
|
6870
|
+
material?.hid,
|
|
6871
|
+
object?.uuid,
|
|
6872
|
+
setObjectMaterialHid,
|
|
6873
|
+
widgetSession
|
|
6874
|
+
]);
|
|
6875
|
+
useEffect(()=>{
|
|
6876
|
+
if ('inline' === viewerSurface || 'ready' !== state || !widgetSession || trackedOpenSidRef.current === widgetSession.sid) return;
|
|
6877
|
+
trackEvent('widget_opened');
|
|
6878
|
+
trackedOpenSidRef.current = widgetSession.sid;
|
|
6879
|
+
}, [
|
|
6880
|
+
state,
|
|
6881
|
+
trackEvent,
|
|
6882
|
+
viewerSurface,
|
|
6883
|
+
widgetSession
|
|
6884
|
+
]);
|
|
6885
|
+
useEffect(()=>{
|
|
6886
|
+
trackedCameraInteractionSidRef.current = null;
|
|
6887
|
+
}, [
|
|
6888
|
+
widgetSession?.sid
|
|
6889
|
+
]);
|
|
6584
6890
|
useEffect(()=>{
|
|
6585
6891
|
setIsModelLoading('ready' === state && Boolean(glbUrl));
|
|
6586
6892
|
}, [
|
|
@@ -6596,53 +6902,82 @@ function ModelViewerProvider({ children, tinuuid, apiKey, hid, layout, backgroun
|
|
|
6596
6902
|
};
|
|
6597
6903
|
const handleLoad = ()=>{
|
|
6598
6904
|
setIsModelLoading(false);
|
|
6905
|
+
scheduleBlankCanvasRecovery(modelViewerEl);
|
|
6599
6906
|
};
|
|
6600
6907
|
const handleError = ()=>{
|
|
6601
6908
|
setIsModelLoading(false);
|
|
6602
6909
|
};
|
|
6910
|
+
const handleCameraChange = (event)=>{
|
|
6911
|
+
const source = event.detail?.source;
|
|
6912
|
+
if ('user-interaction' !== source || !widgetSession || trackedCameraInteractionSidRef.current === widgetSession.sid) return;
|
|
6913
|
+
trackEvent('interacted');
|
|
6914
|
+
trackedCameraInteractionSidRef.current = widgetSession.sid;
|
|
6915
|
+
};
|
|
6603
6916
|
modelViewerEl.addEventListener('progress', handleProgress);
|
|
6604
6917
|
modelViewerEl.addEventListener('load', handleLoad);
|
|
6605
6918
|
modelViewerEl.addEventListener('error', handleError);
|
|
6919
|
+
modelViewerEl.addEventListener('camera-change', handleCameraChange);
|
|
6606
6920
|
return ()=>{
|
|
6607
6921
|
modelViewerEl.removeEventListener('progress', handleProgress);
|
|
6608
6922
|
modelViewerEl.removeEventListener('load', handleLoad);
|
|
6609
6923
|
modelViewerEl.removeEventListener('error', handleError);
|
|
6924
|
+
modelViewerEl.removeEventListener('camera-change', handleCameraChange);
|
|
6610
6925
|
};
|
|
6611
6926
|
}, [
|
|
6612
|
-
modelViewerEl
|
|
6927
|
+
modelViewerEl,
|
|
6928
|
+
trackEvent,
|
|
6929
|
+
widgetSession
|
|
6613
6930
|
]);
|
|
6614
6931
|
const contextValue = external_react_useMemo(()=>({
|
|
6932
|
+
apiKey,
|
|
6933
|
+
appClipBundleId,
|
|
6934
|
+
arExperience,
|
|
6615
6935
|
tinuuid,
|
|
6616
6936
|
object,
|
|
6617
6937
|
material,
|
|
6618
6938
|
glbUrl,
|
|
6619
6939
|
usdzUrl,
|
|
6620
6940
|
previewUrl,
|
|
6941
|
+
handoffUrl,
|
|
6942
|
+
launchAr,
|
|
6621
6943
|
layout,
|
|
6622
6944
|
background,
|
|
6945
|
+
mode,
|
|
6946
|
+
sizeType,
|
|
6623
6947
|
isLoading,
|
|
6624
6948
|
isModelLoading,
|
|
6625
6949
|
state,
|
|
6626
6950
|
modelViewerEl,
|
|
6627
6951
|
containerEl,
|
|
6952
|
+
widgetSession,
|
|
6628
6953
|
setMaterialHid,
|
|
6629
6954
|
setContainerEl,
|
|
6630
|
-
setModelViewerEl
|
|
6955
|
+
setModelViewerEl,
|
|
6956
|
+
trackEvent
|
|
6631
6957
|
}), [
|
|
6958
|
+
apiKey,
|
|
6959
|
+
appClipBundleId,
|
|
6960
|
+
arExperience,
|
|
6632
6961
|
tinuuid,
|
|
6633
6962
|
object,
|
|
6634
6963
|
material,
|
|
6635
6964
|
glbUrl,
|
|
6636
6965
|
usdzUrl,
|
|
6637
6966
|
previewUrl,
|
|
6967
|
+
handoffUrl,
|
|
6968
|
+
launchAr,
|
|
6638
6969
|
layout,
|
|
6639
6970
|
background,
|
|
6971
|
+
mode,
|
|
6972
|
+
sizeType,
|
|
6640
6973
|
isLoading,
|
|
6641
6974
|
isModelLoading,
|
|
6642
6975
|
state,
|
|
6643
6976
|
modelViewerEl,
|
|
6644
6977
|
containerEl,
|
|
6645
|
-
|
|
6978
|
+
widgetSession,
|
|
6979
|
+
setMaterialHid,
|
|
6980
|
+
trackEvent
|
|
6646
6981
|
]);
|
|
6647
6982
|
return /*#__PURE__*/ jsx(ModelViewerContext, {
|
|
6648
6983
|
value: contextValue,
|
|
@@ -6654,27 +6989,204 @@ function useModelViewer() {
|
|
|
6654
6989
|
if (!context) throw new Error('useModelViewer must be used within a ModelViewerProvider');
|
|
6655
6990
|
return context;
|
|
6656
6991
|
}
|
|
6992
|
+
function getUserAgent() {
|
|
6993
|
+
if ("u" < typeof navigator) return '';
|
|
6994
|
+
return navigator.userAgent || navigator.vendor || '';
|
|
6995
|
+
}
|
|
6996
|
+
function isIOSDevice() {
|
|
6997
|
+
if ("u" < typeof window || "u" < typeof navigator) return false;
|
|
6998
|
+
const isIPadOS = 'MacIntel' === navigator.platform && navigator.maxTouchPoints > 1;
|
|
6999
|
+
return (/iPad|iPhone|iPod/.test(getUserAgent()) || isIPadOS) && !('MSStream' in window);
|
|
7000
|
+
}
|
|
7001
|
+
function isMobileArDevice() {
|
|
7002
|
+
if ("u" < typeof window || "u" < typeof navigator) return false;
|
|
7003
|
+
const mobileUserAgent = /Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(getUserAgent());
|
|
7004
|
+
const coarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false;
|
|
7005
|
+
return navigator.maxTouchPoints > 1 && coarsePointer && (mobileUserAgent || isIOSDevice());
|
|
7006
|
+
}
|
|
7007
|
+
function isSupportedAppClipDevice() {
|
|
7008
|
+
if (!isIOSDevice()) return false;
|
|
7009
|
+
const versionText = getUserAgent().match(/\bOS (\d+)/)?.[1] ?? getUserAgent().match(/Version\/(\d+)/)?.[1];
|
|
7010
|
+
if (!versionText) return false;
|
|
7011
|
+
return Number.parseInt(versionText, 10) >= 17;
|
|
7012
|
+
}
|
|
7013
|
+
function buildAppClipUrl(bundleId, tinuuid, hostname, hid) {
|
|
7014
|
+
const url = new URL('https://appclip.apple.com/id');
|
|
7015
|
+
url.searchParams.set('p', bundleId);
|
|
7016
|
+
url.searchParams.set('tinuuid', tinuuid);
|
|
7017
|
+
if (hid) url.searchParams.set('hid', hid);
|
|
7018
|
+
url.searchParams.set('region', hostname.endsWith('arizo.ru') ? 'ru' : 'en');
|
|
7019
|
+
return url.toString();
|
|
7020
|
+
}
|
|
7021
|
+
function buildNativeArUrl(glbUrl, usdzUrl, objectType, ios = isIOSDevice()) {
|
|
7022
|
+
if (ios) return `${new URL(usdzUrl).toString()}#allowsContentScaling=0`;
|
|
7023
|
+
const verticalPlacement = 'wall' === objectType || 'ceiling' === objectType ? '&enable_vertical_placement=true' : '';
|
|
7024
|
+
return `intent://arvr.google.com/scene-viewer/1.0?file=${encodeURIComponent(new URL(glbUrl).toString())}&resizable=false${verticalPlacement}&mode=ar_preferred#Intent;scheme=https;package=com.google.android.googlequicksearchbox;action=android.intent.action.VIEW;end;`;
|
|
7025
|
+
}
|
|
7026
|
+
function launchDirectAr(options) {
|
|
7027
|
+
if ('advanced' === options.experience && isSupportedAppClipDevice()) return void window.location.assign(buildAppClipUrl(options.appClipBundleId, options.tinuuid, options.hostname, options.hid));
|
|
7028
|
+
const link = document.createElement('a');
|
|
7029
|
+
const image = document.createElement('img');
|
|
7030
|
+
link.rel = 'ar';
|
|
7031
|
+
link.href = buildNativeArUrl(options.glbUrl, options.usdzUrl, options.objectType);
|
|
7032
|
+
link.hidden = true;
|
|
7033
|
+
image.alt = '';
|
|
7034
|
+
image.hidden = true;
|
|
7035
|
+
link.appendChild(image);
|
|
7036
|
+
document.body.appendChild(link);
|
|
7037
|
+
link.click();
|
|
7038
|
+
link.remove();
|
|
7039
|
+
}
|
|
7040
|
+
async function launchArOverride(launchAr, target) {
|
|
7041
|
+
if (!launchAr || !target.tinuuid) return false;
|
|
7042
|
+
try {
|
|
7043
|
+
return await launchAr(target);
|
|
7044
|
+
} catch {
|
|
7045
|
+
return false;
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
7048
|
+
function buildModelViewerHandoffUrl({ apiKey, arExperience, baseUrl, hid, mode, tinuuid, widgetSessionToken }) {
|
|
7049
|
+
if (!baseUrl || !tinuuid) return null;
|
|
7050
|
+
let url;
|
|
7051
|
+
try {
|
|
7052
|
+
url = new URL(baseUrl);
|
|
7053
|
+
} catch {
|
|
7054
|
+
return null;
|
|
7055
|
+
}
|
|
7056
|
+
url.searchParams.set('tinuuid', tinuuid);
|
|
7057
|
+
url.searchParams.set('mode', mode);
|
|
7058
|
+
if (arExperience) url.searchParams.set('ar-experience', arExperience);
|
|
7059
|
+
else url.searchParams.delete('ar-experience');
|
|
7060
|
+
if (hid) url.searchParams.set('hid', hid);
|
|
7061
|
+
else url.searchParams.delete('hid');
|
|
7062
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
7063
|
+
if (apiKey) fragment.set('api-key', apiKey);
|
|
7064
|
+
else fragment.delete('api-key');
|
|
7065
|
+
if (widgetSessionToken) fragment.set('widget-session-token', widgetSessionToken);
|
|
7066
|
+
else fragment.delete('widget-session-token');
|
|
7067
|
+
url.hash = fragment.toString();
|
|
7068
|
+
return url.toString();
|
|
7069
|
+
}
|
|
7070
|
+
const EVENT_FLUSH_TIMEOUT_MS = 300;
|
|
7071
|
+
async function flushWidgetEvent(eventRequest) {
|
|
7072
|
+
await new Promise((resolve)=>{
|
|
7073
|
+
const timeout = window.setTimeout(resolve, EVENT_FLUSH_TIMEOUT_MS);
|
|
7074
|
+
eventRequest.finally(()=>{
|
|
7075
|
+
window.clearTimeout(timeout);
|
|
7076
|
+
resolve();
|
|
7077
|
+
});
|
|
7078
|
+
});
|
|
7079
|
+
}
|
|
6657
7080
|
function AR({ disabled, onClick, onBeforeActivateAr, size = 'lg', ...props }) {
|
|
6658
|
-
const { containerEl, isLoading, isModelLoading, material, modelViewerEl, object, tinuuid } = useModelViewer();
|
|
7081
|
+
const { apiKey, appClipBundleId, arExperience, containerEl, handoffUrl, isLoading, isModelLoading, launchAr, material, mode, modelViewerEl, object, tinuuid, trackEvent, widgetSession } = useModelViewer();
|
|
7082
|
+
const [handoffSession, setHandoffSession] = useState(null);
|
|
6659
7083
|
const [open, setOpen] = useState(false);
|
|
7084
|
+
const autoLaunchKeyRef = useRef(null);
|
|
7085
|
+
const widgetSessionSid = widgetSession?.sid;
|
|
7086
|
+
const widgetSessionToken = widgetSession?.token;
|
|
6660
7087
|
const isDisabled = isLoading || isModelLoading || null === material;
|
|
6661
|
-
const
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6665
|
-
if (material?.hid) url.searchParams.set('hid', material.hid);
|
|
6666
|
-
return url.toString();
|
|
6667
|
-
}, [
|
|
7088
|
+
const arTarget = external_react_useMemo(()=>({
|
|
7089
|
+
hid: material?.hid,
|
|
7090
|
+
tinuuid: object?.tinuuid ?? tinuuid
|
|
7091
|
+
}), [
|
|
6668
7092
|
material?.hid,
|
|
6669
7093
|
object?.tinuuid,
|
|
6670
7094
|
tinuuid
|
|
6671
7095
|
]);
|
|
7096
|
+
const directArExperience = 'ar-view' === mode ? 'standard' : 'advanced-ar-view' === mode ? 'advanced' : 'qr-code' === mode ? arExperience : void 0;
|
|
7097
|
+
const directArLaunchOptions = external_react_useMemo(()=>{
|
|
7098
|
+
const native = material?.native;
|
|
7099
|
+
if (!directArExperience || !object || !native?.glbUrl || !native.usdzUrl || "u" < typeof window) return null;
|
|
7100
|
+
return {
|
|
7101
|
+
appClipBundleId,
|
|
7102
|
+
experience: directArExperience,
|
|
7103
|
+
glbUrl: native.glbUrl,
|
|
7104
|
+
hid: material?.hid,
|
|
7105
|
+
hostname: window.location.hostname,
|
|
7106
|
+
objectType: object.type,
|
|
7107
|
+
tinuuid: object.tinuuid ?? tinuuid,
|
|
7108
|
+
usdzUrl: native.usdzUrl
|
|
7109
|
+
};
|
|
7110
|
+
}, [
|
|
7111
|
+
appClipBundleId,
|
|
7112
|
+
directArExperience,
|
|
7113
|
+
material?.hid,
|
|
7114
|
+
material?.native,
|
|
7115
|
+
object,
|
|
7116
|
+
tinuuid
|
|
7117
|
+
]);
|
|
7118
|
+
const url = external_react_useMemo(()=>buildModelViewerHandoffUrl({
|
|
7119
|
+
apiKey,
|
|
7120
|
+
arExperience,
|
|
7121
|
+
baseUrl: handoffUrl || MODEL_VIEWER_URL,
|
|
7122
|
+
hid: material?.hid,
|
|
7123
|
+
mode,
|
|
7124
|
+
tinuuid: object?.tinuuid ?? tinuuid,
|
|
7125
|
+
widgetSessionToken: handoffSession && handoffSession.sid === widgetSessionSid ? handoffSession.token : widgetSessionToken
|
|
7126
|
+
}), [
|
|
7127
|
+
apiKey,
|
|
7128
|
+
arExperience,
|
|
7129
|
+
handoffSession,
|
|
7130
|
+
handoffUrl,
|
|
7131
|
+
material?.hid,
|
|
7132
|
+
mode,
|
|
7133
|
+
object?.tinuuid,
|
|
7134
|
+
tinuuid,
|
|
7135
|
+
widgetSessionSid,
|
|
7136
|
+
widgetSessionToken
|
|
7137
|
+
]);
|
|
7138
|
+
useEffect(()=>{
|
|
7139
|
+
if (!directArLaunchOptions || isLoading || !isMobileArDevice()) return;
|
|
7140
|
+
const launchKey = [
|
|
7141
|
+
widgetSessionSid ?? 'legacy',
|
|
7142
|
+
directArLaunchOptions.tinuuid,
|
|
7143
|
+
material?.hid,
|
|
7144
|
+
directArLaunchOptions.experience
|
|
7145
|
+
].join(':');
|
|
7146
|
+
if (autoLaunchKeyRef.current === launchKey) return;
|
|
7147
|
+
autoLaunchKeyRef.current = launchKey;
|
|
7148
|
+
const timer = window.setTimeout(()=>{
|
|
7149
|
+
(async ()=>{
|
|
7150
|
+
if (autoLaunchKeyRef.current !== launchKey) return;
|
|
7151
|
+
if (await launchArOverride(launchAr, arTarget)) return;
|
|
7152
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
7153
|
+
if (autoLaunchKeyRef.current === launchKey) launchDirectAr(directArLaunchOptions);
|
|
7154
|
+
})();
|
|
7155
|
+
}, 0);
|
|
7156
|
+
return ()=>{
|
|
7157
|
+
window.clearTimeout(timer);
|
|
7158
|
+
if (autoLaunchKeyRef.current === launchKey) autoLaunchKeyRef.current = null;
|
|
7159
|
+
};
|
|
7160
|
+
}, [
|
|
7161
|
+
arTarget,
|
|
7162
|
+
directArLaunchOptions,
|
|
7163
|
+
isLoading,
|
|
7164
|
+
launchAr,
|
|
7165
|
+
material?.hid,
|
|
7166
|
+
trackEvent,
|
|
7167
|
+
widgetSessionSid
|
|
7168
|
+
]);
|
|
6672
7169
|
const handleClick = async ()=>{
|
|
6673
7170
|
if (!modelViewerEl || isDisabled) return;
|
|
6674
|
-
if (
|
|
7171
|
+
if (await launchArOverride(launchAr, arTarget)) return void onBeforeActivateAr?.();
|
|
7172
|
+
if (directArLaunchOptions && isMobileArDevice()) {
|
|
6675
7173
|
onBeforeActivateAr?.();
|
|
7174
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
7175
|
+
launchDirectAr(directArLaunchOptions);
|
|
7176
|
+
} else if (modelViewerEl.canActivateAR) {
|
|
7177
|
+
onBeforeActivateAr?.();
|
|
7178
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
6676
7179
|
await modelViewerEl.activateAR();
|
|
6677
|
-
} else if (url)
|
|
7180
|
+
} else if (url) {
|
|
7181
|
+
const activeSession = widgetSession;
|
|
7182
|
+
const sessionToken = await activeSession?.getTokenForRequest().catch(()=>activeSession.token);
|
|
7183
|
+
setHandoffSession(activeSession && sessionToken ? {
|
|
7184
|
+
sid: activeSession.sid,
|
|
7185
|
+
token: sessionToken
|
|
7186
|
+
} : null);
|
|
7187
|
+
trackEvent('interacted');
|
|
7188
|
+
setOpen(true);
|
|
7189
|
+
}
|
|
6678
7190
|
};
|
|
6679
7191
|
const handleButtonClick = async (event)=>{
|
|
6680
7192
|
onClick?.(event);
|
|
@@ -7097,6 +7609,7 @@ function MaterialVariants() {
|
|
|
7097
7609
|
});
|
|
7098
7610
|
}
|
|
7099
7611
|
const viewerErrorTitle = {
|
|
7612
|
+
forbidden: 'Viewer unavailable for this domain',
|
|
7100
7613
|
'not-found': 'Object not found',
|
|
7101
7614
|
unauthorized: 'Invalid API key',
|
|
7102
7615
|
'payment-required': 'Viewer temporarily unavailable'
|
|
@@ -7164,14 +7677,14 @@ function getHotspotConfig(slot) {
|
|
|
7164
7677
|
if (!config) throw new Error(`Missing hotspot config for slot "${slot}"`);
|
|
7165
7678
|
return config;
|
|
7166
7679
|
}
|
|
7167
|
-
function createDimensionLabels(modelViewerEl) {
|
|
7680
|
+
function createDimensionLabels(modelViewerEl, sizeType) {
|
|
7168
7681
|
const info = calculateBoundingBoxInfo(modelViewerEl);
|
|
7169
7682
|
return DIMENSION_LABEL_CONFIGS.map(({ slot, sizeAxis, offsetMultiplier })=>{
|
|
7170
7683
|
const hotspotConfig = getHotspotConfig(slot);
|
|
7171
7684
|
return {
|
|
7172
7685
|
name: slot,
|
|
7173
7686
|
position: calculateDimensionLabelPosition(info, hotspotConfig, offsetMultiplier),
|
|
7174
|
-
text: formatSize(info.size[sizeAxis],
|
|
7687
|
+
text: formatSize(info.size[sizeAxis], sizeType)
|
|
7175
7688
|
};
|
|
7176
7689
|
});
|
|
7177
7690
|
}
|
|
@@ -7184,7 +7697,7 @@ function syncLabelHotspots(modelViewerEl, labels) {
|
|
|
7184
7697
|
});
|
|
7185
7698
|
}
|
|
7186
7699
|
function useDimensions({ isVisible }) {
|
|
7187
|
-
const { glbUrl, isModelLoading, modelViewerEl } = useModelViewer();
|
|
7700
|
+
const { glbUrl, isModelLoading, modelViewerEl, sizeType } = useModelViewer();
|
|
7188
7701
|
const [lines, setLines] = useState([]);
|
|
7189
7702
|
const [labels, setLabels] = useState([]);
|
|
7190
7703
|
const [hiddenHotspots, setHiddenHotspots] = useState(()=>new Set());
|
|
@@ -7201,7 +7714,7 @@ function useDimensions({ isVisible }) {
|
|
|
7201
7714
|
]);
|
|
7202
7715
|
const calculateLabels = useCallback(()=>{
|
|
7203
7716
|
if (!modelViewerEl) return [];
|
|
7204
|
-
const dimensionLabels = createDimensionLabels(modelViewerEl);
|
|
7717
|
+
const dimensionLabels = createDimensionLabels(modelViewerEl, sizeType);
|
|
7205
7718
|
syncLabelHotspots(modelViewerEl, dimensionLabels);
|
|
7206
7719
|
return dimensionLabels.map((label)=>({
|
|
7207
7720
|
slot: label.name,
|
|
@@ -7209,7 +7722,8 @@ function useDimensions({ isVisible }) {
|
|
|
7209
7722
|
position: label.position
|
|
7210
7723
|
}));
|
|
7211
7724
|
}, [
|
|
7212
|
-
modelViewerEl
|
|
7725
|
+
modelViewerEl,
|
|
7726
|
+
sizeType
|
|
7213
7727
|
]);
|
|
7214
7728
|
const updateLines = useCallback(()=>{
|
|
7215
7729
|
if (!modelViewerEl) return;
|
|
@@ -7282,7 +7796,7 @@ function useDimensions({ isVisible }) {
|
|
|
7282
7796
|
};
|
|
7283
7797
|
}
|
|
7284
7798
|
function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
7285
|
-
const { glbUrl, isModelLoading, object, previewUrl, state, setContainerEl, setModelViewerEl, usdzUrl, layout, background } = useModelViewer();
|
|
7799
|
+
const { glbUrl, isModelLoading, object, previewUrl, state, setContainerEl, setModelViewerEl, usdzUrl, layout, background, trackEvent } = useModelViewer();
|
|
7286
7800
|
const [isDimensionsVisible, setDimensionsVisible] = useState(false);
|
|
7287
7801
|
const isModelLayout = 'model' === layout;
|
|
7288
7802
|
const showViewerLoader = 'loading' === state || 'ready' === state && isModelLoading;
|
|
@@ -7293,7 +7807,7 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7293
7807
|
const { lines, labels, hiddenHotspots } = useDimensions({
|
|
7294
7808
|
isVisible: isDimensionsActive
|
|
7295
7809
|
});
|
|
7296
|
-
const errorState = 'not-found' === state || 'unauthorized' === state || 'payment-required' === state ? state : null;
|
|
7810
|
+
const errorState = 'forbidden' === state || 'not-found' === state || 'unauthorized' === state || 'payment-required' === state ? state : null;
|
|
7297
7811
|
if (errorState) return /*#__PURE__*/ jsx("div", {
|
|
7298
7812
|
ref: setContainerEl,
|
|
7299
7813
|
className: utils_cn('relative size-full overflow-hidden text-foreground', 'default' === background && 'bg-background-layout'),
|
|
@@ -7323,6 +7837,7 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7323
7837
|
arScale: "fixed",
|
|
7324
7838
|
shadowIntensity: 1,
|
|
7325
7839
|
interactionPromptThreshold: 100,
|
|
7840
|
+
loading: "eager",
|
|
7326
7841
|
children: [
|
|
7327
7842
|
/*#__PURE__*/ jsx("div", {
|
|
7328
7843
|
slot: "progress-bar"
|
|
@@ -7366,7 +7881,10 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7366
7881
|
disabled: !canToggleDimensions,
|
|
7367
7882
|
"aria-label": "Toggle dimensions",
|
|
7368
7883
|
"aria-pressed": isDimensionsVisible,
|
|
7369
|
-
onClick: ()=>
|
|
7884
|
+
onClick: ()=>{
|
|
7885
|
+
setDimensionsVisible((current)=>!current);
|
|
7886
|
+
trackEvent('interacted');
|
|
7887
|
+
},
|
|
7370
7888
|
children: [
|
|
7371
7889
|
isDimensionsVisible ? /*#__PURE__*/ jsx(Eye, {
|
|
7372
7890
|
className: "size-6"
|
|
@@ -7391,7 +7909,7 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7391
7909
|
});
|
|
7392
7910
|
}
|
|
7393
7911
|
function normalizeApiKey(apiKey) {
|
|
7394
|
-
return apiKey
|
|
7912
|
+
return apiKey?.trim() ?? '';
|
|
7395
7913
|
}
|
|
7396
7914
|
const root_queryClient = new QueryClient({
|
|
7397
7915
|
defaultOptions: {
|
|
@@ -7408,26 +7926,33 @@ const root_queryClient = new QueryClient({
|
|
|
7408
7926
|
}
|
|
7409
7927
|
}
|
|
7410
7928
|
});
|
|
7411
|
-
function ModelViewerRoot({ tinuuid, apiKey, hid, layout = 'default', background = 'default', showDialogCloseButton = false }) {
|
|
7929
|
+
function ModelViewerRoot({ tinuuid, apiKey, appClipBundleId = 'com.us.vizbl.Clip', arExperience, hid, apiBaseUrl = "https://api.vizbl.com", legacyApiBaseUrl = "https://api.vizbl.com/v1", mode = 'embedded-3d-viewer', initialWidgetSessionToken, handoffUrl = "https://integrations.vizbl.com/model-viewer", launchAr, sizeType = 'cm', layout = 'default', background = 'default', showDialogCloseButton = false, viewerSurface = 'inline' }) {
|
|
7412
7930
|
const normalizedApiKey = normalizeApiKey(apiKey);
|
|
7413
7931
|
return /*#__PURE__*/ jsx(QueryClientProvider, {
|
|
7414
7932
|
client: root_queryClient,
|
|
7415
7933
|
children: /*#__PURE__*/ jsx(ModelViewerProvider, {
|
|
7416
7934
|
tinuuid: tinuuid,
|
|
7417
7935
|
apiKey: normalizedApiKey,
|
|
7936
|
+
appClipBundleId: appClipBundleId,
|
|
7937
|
+
arExperience: arExperience,
|
|
7938
|
+
apiBaseUrl: apiBaseUrl,
|
|
7939
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
7940
|
+
mode: mode,
|
|
7941
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
7942
|
+
handoffUrl: handoffUrl,
|
|
7418
7943
|
hid: hid,
|
|
7944
|
+
launchAr: launchAr,
|
|
7419
7945
|
layout: layout,
|
|
7420
7946
|
background: background,
|
|
7947
|
+
sizeType: sizeType,
|
|
7948
|
+
viewerSurface: viewerSurface,
|
|
7421
7949
|
children: /*#__PURE__*/ jsx(ModelViewerContainer, {
|
|
7422
7950
|
showDialogCloseButton: showDialogCloseButton
|
|
7423
7951
|
})
|
|
7424
7952
|
})
|
|
7425
7953
|
});
|
|
7426
7954
|
}
|
|
7427
|
-
|
|
7428
|
-
baseURL: "https://api.vizbl.com"
|
|
7429
|
-
});
|
|
7430
|
-
function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
7955
|
+
function ModelViewerDialog({ tinuuid, apiKey, apiBaseUrl, legacyApiBaseUrl, initialWidgetSessionToken, handoffUrl, hid, sizeType, ...props }) {
|
|
7431
7956
|
const [container, setContainer] = useState(null);
|
|
7432
7957
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
7433
7958
|
children: [
|
|
@@ -7440,7 +7965,7 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7440
7965
|
children: /*#__PURE__*/ jsx(Dialog, {
|
|
7441
7966
|
...props,
|
|
7442
7967
|
children: /*#__PURE__*/ jsxs(DialogContent, {
|
|
7443
|
-
className: "min-w-full h-
|
|
7968
|
+
className: "min-w-full h-dvh p-0 rounded-none sm:p-0",
|
|
7444
7969
|
portalContainer: container,
|
|
7445
7970
|
showCloseButton: false,
|
|
7446
7971
|
onInteractOutside: (e)=>{
|
|
@@ -7461,8 +7986,15 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7461
7986
|
/*#__PURE__*/ jsx(ModelViewerRoot, {
|
|
7462
7987
|
tinuuid: tinuuid,
|
|
7463
7988
|
apiKey: apiKey,
|
|
7989
|
+
apiBaseUrl: apiBaseUrl,
|
|
7990
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
7991
|
+
mode: "ar-viewer",
|
|
7992
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
7993
|
+
handoffUrl: handoffUrl,
|
|
7464
7994
|
hid: hid,
|
|
7465
|
-
|
|
7995
|
+
sizeType: sizeType,
|
|
7996
|
+
showDialogCloseButton: true,
|
|
7997
|
+
viewerSurface: "dialog"
|
|
7466
7998
|
})
|
|
7467
7999
|
]
|
|
7468
8000
|
})
|
|
@@ -7486,7 +8018,7 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7486
8018
|
]
|
|
7487
8019
|
});
|
|
7488
8020
|
}
|
|
7489
|
-
function ModelViewer({ tinuuid, apiKey, hid, layout, background, className, ...props }) {
|
|
8021
|
+
function ModelViewer({ tinuuid, apiKey, appClipBundleId, arExperience, hid, apiBaseUrl, legacyApiBaseUrl, mode, initialWidgetSessionToken, handoffUrl, launchAr, sizeType, layout, background, viewerSurface, className, ...props }) {
|
|
7490
8022
|
const [container, setContainer] = useState(null);
|
|
7491
8023
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
7492
8024
|
children: [
|
|
@@ -7517,9 +8049,19 @@ function ModelViewer({ tinuuid, apiKey, hid, layout, background, className, ...p
|
|
|
7517
8049
|
children: /*#__PURE__*/ jsx(ModelViewerRoot, {
|
|
7518
8050
|
tinuuid: tinuuid,
|
|
7519
8051
|
apiKey: apiKey,
|
|
8052
|
+
appClipBundleId: appClipBundleId,
|
|
8053
|
+
arExperience: arExperience,
|
|
8054
|
+
apiBaseUrl: apiBaseUrl,
|
|
8055
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
8056
|
+
mode: mode,
|
|
8057
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
8058
|
+
handoffUrl: handoffUrl,
|
|
7520
8059
|
hid: hid,
|
|
8060
|
+
launchAr: launchAr,
|
|
8061
|
+
sizeType: sizeType,
|
|
7521
8062
|
layout: layout,
|
|
7522
|
-
background: background
|
|
8063
|
+
background: background,
|
|
8064
|
+
viewerSurface: viewerSurface
|
|
7523
8065
|
})
|
|
7524
8066
|
})
|
|
7525
8067
|
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neurodyn/react-model-viewer",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.4",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"radix-ui": "1.4.3",
|
|
37
37
|
"react-shadow": "^20.6.0",
|
|
38
38
|
"tailwind-merge": "3.4.0",
|
|
39
|
-
"three": "
|
|
39
|
+
"three": "0.182.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@microsoft/api-extractor": "^7.58.0",
|