@neurodyn/react-model-viewer 0.0.2 → 0.0.3
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 +573 -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,82 @@ 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
|
}
|
|
6557
6781
|
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
|
-
|
|
6782
|
+
function ModelViewerProvider({ children, tinuuid, apiKey, appClipBundleId, arExperience, apiBaseUrl, legacyApiBaseUrl, mode, initialWidgetSessionToken, handoffUrl, hid, launchAr, layout, background, sizeType, viewerSurface }) {
|
|
6783
|
+
const { object, material, error, isLoading, setMaterialHid: setObjectMaterialHid, widgetSession } = useObjectData(tinuuid, hid, {
|
|
6784
|
+
apiBaseUrl,
|
|
6785
|
+
apiKey,
|
|
6786
|
+
initialWidgetSessionToken,
|
|
6787
|
+
legacyApiBaseUrl,
|
|
6788
|
+
mode
|
|
6561
6789
|
});
|
|
6562
6790
|
const [isModelLoading, setIsModelLoading] = useState(true);
|
|
6563
6791
|
const [containerEl, setContainerEl] = useState(null);
|
|
6564
6792
|
const [modelViewerEl, setModelViewerEl] = useState(null);
|
|
6565
|
-
const
|
|
6793
|
+
const trackedCameraInteractionSidRef = useRef(null);
|
|
6794
|
+
const trackedOpenSidRef = useRef(null);
|
|
6566
6795
|
const glbUrl = material?.native?.glbUrl ?? null;
|
|
6567
6796
|
const usdzUrl = material?.native?.usdzUrl ?? null;
|
|
6568
6797
|
const previewUrl = material?.previews?.[0]?.url ?? null;
|
|
6569
6798
|
const state = external_react_useMemo(()=>{
|
|
6570
|
-
if (!hasNonEmptyApiKey) return 'unauthorized';
|
|
6571
6799
|
if (isLoading) return 'loading';
|
|
6572
6800
|
const accessError = getModelViewerApiError(error);
|
|
6573
6801
|
if (accessError) return accessError;
|
|
6802
|
+
const instantArExperience = 'ar-view' === mode || 'qr-code' === mode && 'standard' === arExperience;
|
|
6803
|
+
const advancedArExperience = 'advanced-ar-view' === mode || 'qr-code' === mode && 'advanced' === arExperience;
|
|
6804
|
+
if (instantArExperience && object?.policy?.instantArView === false) return 'forbidden';
|
|
6805
|
+
if ((advancedArExperience || 'ar-viewer' === mode) && object?.policy?.appClip === false) return 'forbidden';
|
|
6574
6806
|
if (null != error || !object || !material || !glbUrl) return 'not-found';
|
|
6575
6807
|
return 'ready';
|
|
6576
6808
|
}, [
|
|
6809
|
+
arExperience,
|
|
6577
6810
|
error,
|
|
6578
6811
|
glbUrl,
|
|
6579
|
-
hasNonEmptyApiKey,
|
|
6580
6812
|
isLoading,
|
|
6581
6813
|
material,
|
|
6814
|
+
mode,
|
|
6582
6815
|
object
|
|
6583
6816
|
]);
|
|
6817
|
+
const trackEvent = useCallback((event)=>sendModelViewerWidgetEvent(widgetSession, event, {
|
|
6818
|
+
objectId: object?.uuid,
|
|
6819
|
+
variantId: material?.hid
|
|
6820
|
+
}), [
|
|
6821
|
+
material?.hid,
|
|
6822
|
+
object?.uuid,
|
|
6823
|
+
widgetSession
|
|
6824
|
+
]);
|
|
6825
|
+
const setMaterialHid = useCallback((nextHid)=>{
|
|
6826
|
+
if (nextHid === material?.hid) return;
|
|
6827
|
+
setObjectMaterialHid(nextHid);
|
|
6828
|
+
sendModelViewerWidgetEvent(widgetSession, 'interacted', {
|
|
6829
|
+
objectId: object?.uuid,
|
|
6830
|
+
variantId: nextHid
|
|
6831
|
+
});
|
|
6832
|
+
}, [
|
|
6833
|
+
material?.hid,
|
|
6834
|
+
object?.uuid,
|
|
6835
|
+
setObjectMaterialHid,
|
|
6836
|
+
widgetSession
|
|
6837
|
+
]);
|
|
6838
|
+
useEffect(()=>{
|
|
6839
|
+
if ('inline' === viewerSurface || 'ready' !== state || !widgetSession || trackedOpenSidRef.current === widgetSession.sid) return;
|
|
6840
|
+
trackEvent('widget_opened');
|
|
6841
|
+
trackedOpenSidRef.current = widgetSession.sid;
|
|
6842
|
+
}, [
|
|
6843
|
+
state,
|
|
6844
|
+
trackEvent,
|
|
6845
|
+
viewerSurface,
|
|
6846
|
+
widgetSession
|
|
6847
|
+
]);
|
|
6848
|
+
useEffect(()=>{
|
|
6849
|
+
trackedCameraInteractionSidRef.current = null;
|
|
6850
|
+
}, [
|
|
6851
|
+
widgetSession?.sid
|
|
6852
|
+
]);
|
|
6584
6853
|
useEffect(()=>{
|
|
6585
6854
|
setIsModelLoading('ready' === state && Boolean(glbUrl));
|
|
6586
6855
|
}, [
|
|
@@ -6600,49 +6869,77 @@ function ModelViewerProvider({ children, tinuuid, apiKey, hid, layout, backgroun
|
|
|
6600
6869
|
const handleError = ()=>{
|
|
6601
6870
|
setIsModelLoading(false);
|
|
6602
6871
|
};
|
|
6872
|
+
const handleCameraChange = (event)=>{
|
|
6873
|
+
const source = event.detail?.source;
|
|
6874
|
+
if ('user-interaction' !== source || !widgetSession || trackedCameraInteractionSidRef.current === widgetSession.sid) return;
|
|
6875
|
+
trackEvent('interacted');
|
|
6876
|
+
trackedCameraInteractionSidRef.current = widgetSession.sid;
|
|
6877
|
+
};
|
|
6603
6878
|
modelViewerEl.addEventListener('progress', handleProgress);
|
|
6604
6879
|
modelViewerEl.addEventListener('load', handleLoad);
|
|
6605
6880
|
modelViewerEl.addEventListener('error', handleError);
|
|
6881
|
+
modelViewerEl.addEventListener('camera-change', handleCameraChange);
|
|
6606
6882
|
return ()=>{
|
|
6607
6883
|
modelViewerEl.removeEventListener('progress', handleProgress);
|
|
6608
6884
|
modelViewerEl.removeEventListener('load', handleLoad);
|
|
6609
6885
|
modelViewerEl.removeEventListener('error', handleError);
|
|
6886
|
+
modelViewerEl.removeEventListener('camera-change', handleCameraChange);
|
|
6610
6887
|
};
|
|
6611
6888
|
}, [
|
|
6612
|
-
modelViewerEl
|
|
6889
|
+
modelViewerEl,
|
|
6890
|
+
trackEvent,
|
|
6891
|
+
widgetSession
|
|
6613
6892
|
]);
|
|
6614
6893
|
const contextValue = external_react_useMemo(()=>({
|
|
6894
|
+
apiKey,
|
|
6895
|
+
appClipBundleId,
|
|
6896
|
+
arExperience,
|
|
6615
6897
|
tinuuid,
|
|
6616
6898
|
object,
|
|
6617
6899
|
material,
|
|
6618
6900
|
glbUrl,
|
|
6619
6901
|
usdzUrl,
|
|
6620
6902
|
previewUrl,
|
|
6903
|
+
handoffUrl,
|
|
6904
|
+
launchAr,
|
|
6621
6905
|
layout,
|
|
6622
6906
|
background,
|
|
6907
|
+
mode,
|
|
6908
|
+
sizeType,
|
|
6623
6909
|
isLoading,
|
|
6624
6910
|
isModelLoading,
|
|
6625
6911
|
state,
|
|
6626
6912
|
modelViewerEl,
|
|
6627
6913
|
containerEl,
|
|
6914
|
+
widgetSession,
|
|
6628
6915
|
setMaterialHid,
|
|
6629
6916
|
setContainerEl,
|
|
6630
|
-
setModelViewerEl
|
|
6917
|
+
setModelViewerEl,
|
|
6918
|
+
trackEvent
|
|
6631
6919
|
}), [
|
|
6920
|
+
apiKey,
|
|
6921
|
+
appClipBundleId,
|
|
6922
|
+
arExperience,
|
|
6632
6923
|
tinuuid,
|
|
6633
6924
|
object,
|
|
6634
6925
|
material,
|
|
6635
6926
|
glbUrl,
|
|
6636
6927
|
usdzUrl,
|
|
6637
6928
|
previewUrl,
|
|
6929
|
+
handoffUrl,
|
|
6930
|
+
launchAr,
|
|
6638
6931
|
layout,
|
|
6639
6932
|
background,
|
|
6933
|
+
mode,
|
|
6934
|
+
sizeType,
|
|
6640
6935
|
isLoading,
|
|
6641
6936
|
isModelLoading,
|
|
6642
6937
|
state,
|
|
6643
6938
|
modelViewerEl,
|
|
6644
6939
|
containerEl,
|
|
6645
|
-
|
|
6940
|
+
widgetSession,
|
|
6941
|
+
setMaterialHid,
|
|
6942
|
+
trackEvent
|
|
6646
6943
|
]);
|
|
6647
6944
|
return /*#__PURE__*/ jsx(ModelViewerContext, {
|
|
6648
6945
|
value: contextValue,
|
|
@@ -6654,27 +6951,204 @@ function useModelViewer() {
|
|
|
6654
6951
|
if (!context) throw new Error('useModelViewer must be used within a ModelViewerProvider');
|
|
6655
6952
|
return context;
|
|
6656
6953
|
}
|
|
6954
|
+
function getUserAgent() {
|
|
6955
|
+
if ("u" < typeof navigator) return '';
|
|
6956
|
+
return navigator.userAgent || navigator.vendor || '';
|
|
6957
|
+
}
|
|
6958
|
+
function isIOSDevice() {
|
|
6959
|
+
if ("u" < typeof window || "u" < typeof navigator) return false;
|
|
6960
|
+
const isIPadOS = 'MacIntel' === navigator.platform && navigator.maxTouchPoints > 1;
|
|
6961
|
+
return (/iPad|iPhone|iPod/.test(getUserAgent()) || isIPadOS) && !('MSStream' in window);
|
|
6962
|
+
}
|
|
6963
|
+
function isMobileArDevice() {
|
|
6964
|
+
if ("u" < typeof window || "u" < typeof navigator) return false;
|
|
6965
|
+
const mobileUserAgent = /Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(getUserAgent());
|
|
6966
|
+
const coarsePointer = window.matchMedia?.('(pointer: coarse)').matches ?? false;
|
|
6967
|
+
return navigator.maxTouchPoints > 1 && coarsePointer && (mobileUserAgent || isIOSDevice());
|
|
6968
|
+
}
|
|
6969
|
+
function isSupportedAppClipDevice() {
|
|
6970
|
+
if (!isIOSDevice()) return false;
|
|
6971
|
+
const versionText = getUserAgent().match(/\bOS (\d+)/)?.[1] ?? getUserAgent().match(/Version\/(\d+)/)?.[1];
|
|
6972
|
+
if (!versionText) return false;
|
|
6973
|
+
return Number.parseInt(versionText, 10) >= 17;
|
|
6974
|
+
}
|
|
6975
|
+
function buildAppClipUrl(bundleId, tinuuid, hostname, hid) {
|
|
6976
|
+
const url = new URL('https://appclip.apple.com/id');
|
|
6977
|
+
url.searchParams.set('p', bundleId);
|
|
6978
|
+
url.searchParams.set('tinuuid', tinuuid);
|
|
6979
|
+
if (hid) url.searchParams.set('hid', hid);
|
|
6980
|
+
url.searchParams.set('region', hostname.endsWith('arizo.ru') ? 'ru' : 'en');
|
|
6981
|
+
return url.toString();
|
|
6982
|
+
}
|
|
6983
|
+
function buildNativeArUrl(glbUrl, usdzUrl, objectType, ios = isIOSDevice()) {
|
|
6984
|
+
if (ios) return `${new URL(usdzUrl).toString()}#allowsContentScaling=0`;
|
|
6985
|
+
const verticalPlacement = 'wall' === objectType || 'ceiling' === objectType ? '&enable_vertical_placement=true' : '';
|
|
6986
|
+
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;`;
|
|
6987
|
+
}
|
|
6988
|
+
function launchDirectAr(options) {
|
|
6989
|
+
if ('advanced' === options.experience && isSupportedAppClipDevice()) return void window.location.assign(buildAppClipUrl(options.appClipBundleId, options.tinuuid, options.hostname, options.hid));
|
|
6990
|
+
const link = document.createElement('a');
|
|
6991
|
+
const image = document.createElement('img');
|
|
6992
|
+
link.rel = 'ar';
|
|
6993
|
+
link.href = buildNativeArUrl(options.glbUrl, options.usdzUrl, options.objectType);
|
|
6994
|
+
link.hidden = true;
|
|
6995
|
+
image.alt = '';
|
|
6996
|
+
image.hidden = true;
|
|
6997
|
+
link.appendChild(image);
|
|
6998
|
+
document.body.appendChild(link);
|
|
6999
|
+
link.click();
|
|
7000
|
+
link.remove();
|
|
7001
|
+
}
|
|
7002
|
+
async function launchArOverride(launchAr, target) {
|
|
7003
|
+
if (!launchAr || !target.tinuuid) return false;
|
|
7004
|
+
try {
|
|
7005
|
+
return await launchAr(target);
|
|
7006
|
+
} catch {
|
|
7007
|
+
return false;
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
function buildModelViewerHandoffUrl({ apiKey, arExperience, baseUrl, hid, mode, tinuuid, widgetSessionToken }) {
|
|
7011
|
+
if (!baseUrl || !tinuuid) return null;
|
|
7012
|
+
let url;
|
|
7013
|
+
try {
|
|
7014
|
+
url = new URL(baseUrl);
|
|
7015
|
+
} catch {
|
|
7016
|
+
return null;
|
|
7017
|
+
}
|
|
7018
|
+
url.searchParams.set('tinuuid', tinuuid);
|
|
7019
|
+
url.searchParams.set('mode', mode);
|
|
7020
|
+
if (arExperience) url.searchParams.set('ar-experience', arExperience);
|
|
7021
|
+
else url.searchParams.delete('ar-experience');
|
|
7022
|
+
if (hid) url.searchParams.set('hid', hid);
|
|
7023
|
+
else url.searchParams.delete('hid');
|
|
7024
|
+
const fragment = new URLSearchParams(url.hash.slice(1));
|
|
7025
|
+
if (apiKey) fragment.set('api-key', apiKey);
|
|
7026
|
+
else fragment.delete('api-key');
|
|
7027
|
+
if (widgetSessionToken) fragment.set('widget-session-token', widgetSessionToken);
|
|
7028
|
+
else fragment.delete('widget-session-token');
|
|
7029
|
+
url.hash = fragment.toString();
|
|
7030
|
+
return url.toString();
|
|
7031
|
+
}
|
|
7032
|
+
const EVENT_FLUSH_TIMEOUT_MS = 300;
|
|
7033
|
+
async function flushWidgetEvent(eventRequest) {
|
|
7034
|
+
await new Promise((resolve)=>{
|
|
7035
|
+
const timeout = window.setTimeout(resolve, EVENT_FLUSH_TIMEOUT_MS);
|
|
7036
|
+
eventRequest.finally(()=>{
|
|
7037
|
+
window.clearTimeout(timeout);
|
|
7038
|
+
resolve();
|
|
7039
|
+
});
|
|
7040
|
+
});
|
|
7041
|
+
}
|
|
6657
7042
|
function AR({ disabled, onClick, onBeforeActivateAr, size = 'lg', ...props }) {
|
|
6658
|
-
const { containerEl, isLoading, isModelLoading, material, modelViewerEl, object, tinuuid } = useModelViewer();
|
|
7043
|
+
const { apiKey, appClipBundleId, arExperience, containerEl, handoffUrl, isLoading, isModelLoading, launchAr, material, mode, modelViewerEl, object, tinuuid, trackEvent, widgetSession } = useModelViewer();
|
|
7044
|
+
const [handoffSession, setHandoffSession] = useState(null);
|
|
6659
7045
|
const [open, setOpen] = useState(false);
|
|
7046
|
+
const autoLaunchKeyRef = useRef(null);
|
|
7047
|
+
const widgetSessionSid = widgetSession?.sid;
|
|
7048
|
+
const widgetSessionToken = widgetSession?.token;
|
|
6660
7049
|
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
|
-
}, [
|
|
7050
|
+
const arTarget = external_react_useMemo(()=>({
|
|
7051
|
+
hid: material?.hid,
|
|
7052
|
+
tinuuid: object?.tinuuid ?? tinuuid
|
|
7053
|
+
}), [
|
|
6668
7054
|
material?.hid,
|
|
6669
7055
|
object?.tinuuid,
|
|
6670
7056
|
tinuuid
|
|
6671
7057
|
]);
|
|
7058
|
+
const directArExperience = 'ar-view' === mode ? 'standard' : 'advanced-ar-view' === mode ? 'advanced' : 'qr-code' === mode ? arExperience : void 0;
|
|
7059
|
+
const directArLaunchOptions = external_react_useMemo(()=>{
|
|
7060
|
+
const native = material?.native;
|
|
7061
|
+
if (!directArExperience || !object || !native?.glbUrl || !native.usdzUrl || "u" < typeof window) return null;
|
|
7062
|
+
return {
|
|
7063
|
+
appClipBundleId,
|
|
7064
|
+
experience: directArExperience,
|
|
7065
|
+
glbUrl: native.glbUrl,
|
|
7066
|
+
hid: material?.hid,
|
|
7067
|
+
hostname: window.location.hostname,
|
|
7068
|
+
objectType: object.type,
|
|
7069
|
+
tinuuid: object.tinuuid ?? tinuuid,
|
|
7070
|
+
usdzUrl: native.usdzUrl
|
|
7071
|
+
};
|
|
7072
|
+
}, [
|
|
7073
|
+
appClipBundleId,
|
|
7074
|
+
directArExperience,
|
|
7075
|
+
material?.hid,
|
|
7076
|
+
material?.native,
|
|
7077
|
+
object,
|
|
7078
|
+
tinuuid
|
|
7079
|
+
]);
|
|
7080
|
+
const url = external_react_useMemo(()=>buildModelViewerHandoffUrl({
|
|
7081
|
+
apiKey,
|
|
7082
|
+
arExperience,
|
|
7083
|
+
baseUrl: handoffUrl || MODEL_VIEWER_URL,
|
|
7084
|
+
hid: material?.hid,
|
|
7085
|
+
mode,
|
|
7086
|
+
tinuuid: object?.tinuuid ?? tinuuid,
|
|
7087
|
+
widgetSessionToken: handoffSession && handoffSession.sid === widgetSessionSid ? handoffSession.token : widgetSessionToken
|
|
7088
|
+
}), [
|
|
7089
|
+
apiKey,
|
|
7090
|
+
arExperience,
|
|
7091
|
+
handoffSession,
|
|
7092
|
+
handoffUrl,
|
|
7093
|
+
material?.hid,
|
|
7094
|
+
mode,
|
|
7095
|
+
object?.tinuuid,
|
|
7096
|
+
tinuuid,
|
|
7097
|
+
widgetSessionSid,
|
|
7098
|
+
widgetSessionToken
|
|
7099
|
+
]);
|
|
7100
|
+
useEffect(()=>{
|
|
7101
|
+
if (!directArLaunchOptions || isLoading || !isMobileArDevice()) return;
|
|
7102
|
+
const launchKey = [
|
|
7103
|
+
widgetSessionSid ?? 'legacy',
|
|
7104
|
+
directArLaunchOptions.tinuuid,
|
|
7105
|
+
material?.hid,
|
|
7106
|
+
directArLaunchOptions.experience
|
|
7107
|
+
].join(':');
|
|
7108
|
+
if (autoLaunchKeyRef.current === launchKey) return;
|
|
7109
|
+
autoLaunchKeyRef.current = launchKey;
|
|
7110
|
+
const timer = window.setTimeout(()=>{
|
|
7111
|
+
(async ()=>{
|
|
7112
|
+
if (autoLaunchKeyRef.current !== launchKey) return;
|
|
7113
|
+
if (await launchArOverride(launchAr, arTarget)) return;
|
|
7114
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
7115
|
+
if (autoLaunchKeyRef.current === launchKey) launchDirectAr(directArLaunchOptions);
|
|
7116
|
+
})();
|
|
7117
|
+
}, 0);
|
|
7118
|
+
return ()=>{
|
|
7119
|
+
window.clearTimeout(timer);
|
|
7120
|
+
if (autoLaunchKeyRef.current === launchKey) autoLaunchKeyRef.current = null;
|
|
7121
|
+
};
|
|
7122
|
+
}, [
|
|
7123
|
+
arTarget,
|
|
7124
|
+
directArLaunchOptions,
|
|
7125
|
+
isLoading,
|
|
7126
|
+
launchAr,
|
|
7127
|
+
material?.hid,
|
|
7128
|
+
trackEvent,
|
|
7129
|
+
widgetSessionSid
|
|
7130
|
+
]);
|
|
6672
7131
|
const handleClick = async ()=>{
|
|
6673
7132
|
if (!modelViewerEl || isDisabled) return;
|
|
6674
|
-
if (
|
|
7133
|
+
if (await launchArOverride(launchAr, arTarget)) return void onBeforeActivateAr?.();
|
|
7134
|
+
if (directArLaunchOptions && isMobileArDevice()) {
|
|
6675
7135
|
onBeforeActivateAr?.();
|
|
7136
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
7137
|
+
launchDirectAr(directArLaunchOptions);
|
|
7138
|
+
} else if (modelViewerEl.canActivateAR) {
|
|
7139
|
+
onBeforeActivateAr?.();
|
|
7140
|
+
await flushWidgetEvent(trackEvent('ar_launched'));
|
|
6676
7141
|
await modelViewerEl.activateAR();
|
|
6677
|
-
} else if (url)
|
|
7142
|
+
} else if (url) {
|
|
7143
|
+
const activeSession = widgetSession;
|
|
7144
|
+
const sessionToken = await activeSession?.getTokenForRequest().catch(()=>activeSession.token);
|
|
7145
|
+
setHandoffSession(activeSession && sessionToken ? {
|
|
7146
|
+
sid: activeSession.sid,
|
|
7147
|
+
token: sessionToken
|
|
7148
|
+
} : null);
|
|
7149
|
+
trackEvent('interacted');
|
|
7150
|
+
setOpen(true);
|
|
7151
|
+
}
|
|
6678
7152
|
};
|
|
6679
7153
|
const handleButtonClick = async (event)=>{
|
|
6680
7154
|
onClick?.(event);
|
|
@@ -7097,6 +7571,7 @@ function MaterialVariants() {
|
|
|
7097
7571
|
});
|
|
7098
7572
|
}
|
|
7099
7573
|
const viewerErrorTitle = {
|
|
7574
|
+
forbidden: 'Viewer unavailable for this domain',
|
|
7100
7575
|
'not-found': 'Object not found',
|
|
7101
7576
|
unauthorized: 'Invalid API key',
|
|
7102
7577
|
'payment-required': 'Viewer temporarily unavailable'
|
|
@@ -7164,14 +7639,14 @@ function getHotspotConfig(slot) {
|
|
|
7164
7639
|
if (!config) throw new Error(`Missing hotspot config for slot "${slot}"`);
|
|
7165
7640
|
return config;
|
|
7166
7641
|
}
|
|
7167
|
-
function createDimensionLabels(modelViewerEl) {
|
|
7642
|
+
function createDimensionLabels(modelViewerEl, sizeType) {
|
|
7168
7643
|
const info = calculateBoundingBoxInfo(modelViewerEl);
|
|
7169
7644
|
return DIMENSION_LABEL_CONFIGS.map(({ slot, sizeAxis, offsetMultiplier })=>{
|
|
7170
7645
|
const hotspotConfig = getHotspotConfig(slot);
|
|
7171
7646
|
return {
|
|
7172
7647
|
name: slot,
|
|
7173
7648
|
position: calculateDimensionLabelPosition(info, hotspotConfig, offsetMultiplier),
|
|
7174
|
-
text: formatSize(info.size[sizeAxis],
|
|
7649
|
+
text: formatSize(info.size[sizeAxis], sizeType)
|
|
7175
7650
|
};
|
|
7176
7651
|
});
|
|
7177
7652
|
}
|
|
@@ -7184,7 +7659,7 @@ function syncLabelHotspots(modelViewerEl, labels) {
|
|
|
7184
7659
|
});
|
|
7185
7660
|
}
|
|
7186
7661
|
function useDimensions({ isVisible }) {
|
|
7187
|
-
const { glbUrl, isModelLoading, modelViewerEl } = useModelViewer();
|
|
7662
|
+
const { glbUrl, isModelLoading, modelViewerEl, sizeType } = useModelViewer();
|
|
7188
7663
|
const [lines, setLines] = useState([]);
|
|
7189
7664
|
const [labels, setLabels] = useState([]);
|
|
7190
7665
|
const [hiddenHotspots, setHiddenHotspots] = useState(()=>new Set());
|
|
@@ -7201,7 +7676,7 @@ function useDimensions({ isVisible }) {
|
|
|
7201
7676
|
]);
|
|
7202
7677
|
const calculateLabels = useCallback(()=>{
|
|
7203
7678
|
if (!modelViewerEl) return [];
|
|
7204
|
-
const dimensionLabels = createDimensionLabels(modelViewerEl);
|
|
7679
|
+
const dimensionLabels = createDimensionLabels(modelViewerEl, sizeType);
|
|
7205
7680
|
syncLabelHotspots(modelViewerEl, dimensionLabels);
|
|
7206
7681
|
return dimensionLabels.map((label)=>({
|
|
7207
7682
|
slot: label.name,
|
|
@@ -7209,7 +7684,8 @@ function useDimensions({ isVisible }) {
|
|
|
7209
7684
|
position: label.position
|
|
7210
7685
|
}));
|
|
7211
7686
|
}, [
|
|
7212
|
-
modelViewerEl
|
|
7687
|
+
modelViewerEl,
|
|
7688
|
+
sizeType
|
|
7213
7689
|
]);
|
|
7214
7690
|
const updateLines = useCallback(()=>{
|
|
7215
7691
|
if (!modelViewerEl) return;
|
|
@@ -7282,7 +7758,7 @@ function useDimensions({ isVisible }) {
|
|
|
7282
7758
|
};
|
|
7283
7759
|
}
|
|
7284
7760
|
function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
7285
|
-
const { glbUrl, isModelLoading, object, previewUrl, state, setContainerEl, setModelViewerEl, usdzUrl, layout, background } = useModelViewer();
|
|
7761
|
+
const { glbUrl, isModelLoading, object, previewUrl, state, setContainerEl, setModelViewerEl, usdzUrl, layout, background, trackEvent } = useModelViewer();
|
|
7286
7762
|
const [isDimensionsVisible, setDimensionsVisible] = useState(false);
|
|
7287
7763
|
const isModelLayout = 'model' === layout;
|
|
7288
7764
|
const showViewerLoader = 'loading' === state || 'ready' === state && isModelLoading;
|
|
@@ -7293,7 +7769,7 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7293
7769
|
const { lines, labels, hiddenHotspots } = useDimensions({
|
|
7294
7770
|
isVisible: isDimensionsActive
|
|
7295
7771
|
});
|
|
7296
|
-
const errorState = 'not-found' === state || 'unauthorized' === state || 'payment-required' === state ? state : null;
|
|
7772
|
+
const errorState = 'forbidden' === state || 'not-found' === state || 'unauthorized' === state || 'payment-required' === state ? state : null;
|
|
7297
7773
|
if (errorState) return /*#__PURE__*/ jsx("div", {
|
|
7298
7774
|
ref: setContainerEl,
|
|
7299
7775
|
className: utils_cn('relative size-full overflow-hidden text-foreground', 'default' === background && 'bg-background-layout'),
|
|
@@ -7366,7 +7842,10 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7366
7842
|
disabled: !canToggleDimensions,
|
|
7367
7843
|
"aria-label": "Toggle dimensions",
|
|
7368
7844
|
"aria-pressed": isDimensionsVisible,
|
|
7369
|
-
onClick: ()=>
|
|
7845
|
+
onClick: ()=>{
|
|
7846
|
+
setDimensionsVisible((current)=>!current);
|
|
7847
|
+
trackEvent('interacted');
|
|
7848
|
+
},
|
|
7370
7849
|
children: [
|
|
7371
7850
|
isDimensionsVisible ? /*#__PURE__*/ jsx(Eye, {
|
|
7372
7851
|
className: "size-6"
|
|
@@ -7391,7 +7870,7 @@ function ModelViewerContainer({ showDialogCloseButton = false }) {
|
|
|
7391
7870
|
});
|
|
7392
7871
|
}
|
|
7393
7872
|
function normalizeApiKey(apiKey) {
|
|
7394
|
-
return apiKey
|
|
7873
|
+
return apiKey?.trim() ?? '';
|
|
7395
7874
|
}
|
|
7396
7875
|
const root_queryClient = new QueryClient({
|
|
7397
7876
|
defaultOptions: {
|
|
@@ -7408,26 +7887,33 @@ const root_queryClient = new QueryClient({
|
|
|
7408
7887
|
}
|
|
7409
7888
|
}
|
|
7410
7889
|
});
|
|
7411
|
-
function ModelViewerRoot({ tinuuid, apiKey, hid, layout = 'default', background = 'default', showDialogCloseButton = false }) {
|
|
7890
|
+
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
7891
|
const normalizedApiKey = normalizeApiKey(apiKey);
|
|
7413
7892
|
return /*#__PURE__*/ jsx(QueryClientProvider, {
|
|
7414
7893
|
client: root_queryClient,
|
|
7415
7894
|
children: /*#__PURE__*/ jsx(ModelViewerProvider, {
|
|
7416
7895
|
tinuuid: tinuuid,
|
|
7417
7896
|
apiKey: normalizedApiKey,
|
|
7897
|
+
appClipBundleId: appClipBundleId,
|
|
7898
|
+
arExperience: arExperience,
|
|
7899
|
+
apiBaseUrl: apiBaseUrl,
|
|
7900
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
7901
|
+
mode: mode,
|
|
7902
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
7903
|
+
handoffUrl: handoffUrl,
|
|
7418
7904
|
hid: hid,
|
|
7905
|
+
launchAr: launchAr,
|
|
7419
7906
|
layout: layout,
|
|
7420
7907
|
background: background,
|
|
7908
|
+
sizeType: sizeType,
|
|
7909
|
+
viewerSurface: viewerSurface,
|
|
7421
7910
|
children: /*#__PURE__*/ jsx(ModelViewerContainer, {
|
|
7422
7911
|
showDialogCloseButton: showDialogCloseButton
|
|
7423
7912
|
})
|
|
7424
7913
|
})
|
|
7425
7914
|
});
|
|
7426
7915
|
}
|
|
7427
|
-
|
|
7428
|
-
baseURL: "https://api.vizbl.com"
|
|
7429
|
-
});
|
|
7430
|
-
function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
7916
|
+
function ModelViewerDialog({ tinuuid, apiKey, apiBaseUrl, legacyApiBaseUrl, initialWidgetSessionToken, handoffUrl, hid, sizeType, ...props }) {
|
|
7431
7917
|
const [container, setContainer] = useState(null);
|
|
7432
7918
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
7433
7919
|
children: [
|
|
@@ -7440,7 +7926,7 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7440
7926
|
children: /*#__PURE__*/ jsx(Dialog, {
|
|
7441
7927
|
...props,
|
|
7442
7928
|
children: /*#__PURE__*/ jsxs(DialogContent, {
|
|
7443
|
-
className: "min-w-full h-
|
|
7929
|
+
className: "min-w-full h-dvh p-0 rounded-none sm:p-0",
|
|
7444
7930
|
portalContainer: container,
|
|
7445
7931
|
showCloseButton: false,
|
|
7446
7932
|
onInteractOutside: (e)=>{
|
|
@@ -7461,8 +7947,15 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7461
7947
|
/*#__PURE__*/ jsx(ModelViewerRoot, {
|
|
7462
7948
|
tinuuid: tinuuid,
|
|
7463
7949
|
apiKey: apiKey,
|
|
7950
|
+
apiBaseUrl: apiBaseUrl,
|
|
7951
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
7952
|
+
mode: "ar-viewer",
|
|
7953
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
7954
|
+
handoffUrl: handoffUrl,
|
|
7464
7955
|
hid: hid,
|
|
7465
|
-
|
|
7956
|
+
sizeType: sizeType,
|
|
7957
|
+
showDialogCloseButton: true,
|
|
7958
|
+
viewerSurface: "dialog"
|
|
7466
7959
|
})
|
|
7467
7960
|
]
|
|
7468
7961
|
})
|
|
@@ -7486,7 +7979,7 @@ function ModelViewerDialog({ tinuuid, apiKey, hid, ...props }) {
|
|
|
7486
7979
|
]
|
|
7487
7980
|
});
|
|
7488
7981
|
}
|
|
7489
|
-
function ModelViewer({ tinuuid, apiKey, hid, layout, background, className, ...props }) {
|
|
7982
|
+
function ModelViewer({ tinuuid, apiKey, appClipBundleId, arExperience, hid, apiBaseUrl, legacyApiBaseUrl, mode, initialWidgetSessionToken, handoffUrl, launchAr, sizeType, layout, background, viewerSurface, className, ...props }) {
|
|
7490
7983
|
const [container, setContainer] = useState(null);
|
|
7491
7984
|
return /*#__PURE__*/ jsxs(Fragment, {
|
|
7492
7985
|
children: [
|
|
@@ -7517,9 +8010,19 @@ function ModelViewer({ tinuuid, apiKey, hid, layout, background, className, ...p
|
|
|
7517
8010
|
children: /*#__PURE__*/ jsx(ModelViewerRoot, {
|
|
7518
8011
|
tinuuid: tinuuid,
|
|
7519
8012
|
apiKey: apiKey,
|
|
8013
|
+
appClipBundleId: appClipBundleId,
|
|
8014
|
+
arExperience: arExperience,
|
|
8015
|
+
apiBaseUrl: apiBaseUrl,
|
|
8016
|
+
legacyApiBaseUrl: legacyApiBaseUrl,
|
|
8017
|
+
mode: mode,
|
|
8018
|
+
initialWidgetSessionToken: initialWidgetSessionToken,
|
|
8019
|
+
handoffUrl: handoffUrl,
|
|
7520
8020
|
hid: hid,
|
|
8021
|
+
launchAr: launchAr,
|
|
8022
|
+
sizeType: sizeType,
|
|
7521
8023
|
layout: layout,
|
|
7522
|
-
background: background
|
|
8024
|
+
background: background,
|
|
8025
|
+
viewerSurface: viewerSurface
|
|
7523
8026
|
})
|
|
7524
8027
|
})
|
|
7525
8028
|
})
|
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.3",
|
|
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": "^0.
|
|
39
|
+
"three": "^0.185.1"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@microsoft/api-extractor": "^7.58.0",
|