UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

739 lines 35.9 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { NEEDLE_progressive } from "@needle-tools/gltf-progressive"; import { Euler, Matrix4, Object3D, Quaternion, Vector3 } from "three"; import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js"; import { hasProLicense } from "../../../engine/engine_license.js"; import { serializable } from "../../../engine/engine_serialization.js"; import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js"; import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js"; import { WebXRButtonFactory } from "../../../engine/webcomponents/WebXRButtons.js"; import { InternalUSDZRegistry } from "../../../engine/xr/usdz.js"; import { InstancingHandler } from "../../../engine-components/RendererInstancing.js"; import { Collider } from "../../Collider.js"; import { Behaviour, GameObject } from "../../Component.js"; import { ContactShadows } from "../../ContactShadows.js"; import { EventList } from "../../EventList.js"; import { GroundProjectedEnv } from "../../GroundProjection.js"; import { Renderer } from "../../Renderer.js"; import { Rigidbody } from "../../RigidBody.js"; import { SpriteRenderer } from "../../SpriteRenderer.js"; import { WebARSessionRoot } from "../../webxr/WebARSessionRoot.js"; import { WebXR } from "../../webxr/WebXR.js"; import { XRState, XRStateFlag } from "../../webxr/XRFlag.js"; import { AnimationExtension } from "./extensions/Animation.js"; import { AudioExtension } from "./extensions/behavior/AudioExtension.js"; import { BehaviorExtension } from "./extensions/behavior/Behaviour.js"; import { PhysicsExtension } from "./extensions/behavior/PhysicsExtension.js"; import { TextExtension } from "./extensions/USDZText.js"; import { USDZUIExtension } from "./extensions/USDZUI.js"; import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter.js"; import { disableObjectsAtStart, registerAnimatorsImplictly, registerAudioSourcesImplictly } from "./utils/animationutils.js"; import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js"; const debug = getParam("debugusdz"); const debugUsdzPruning = getParam("debugusdzpruning"); /** * Custom branding for the QuickLook overlay, used by {@link USDZExporter}. */ export class CustomBranding { /** The call to action button text. If not set, the button will close the QuickLook overlay. */ callToAction; /** The title of the overlay. */ checkoutTitle; /** The subtitle of the overlay. */ checkoutSubtitle; /** if assigned the call to action button in quicklook will open the URL. Otherwise it will just close quicklook. */ callToActionURL; } __decorate([ serializable() ], CustomBranding.prototype, "callToAction", void 0); __decorate([ serializable() ], CustomBranding.prototype, "checkoutTitle", void 0); __decorate([ serializable() ], CustomBranding.prototype, "checkoutSubtitle", void 0); __decorate([ serializable() ], CustomBranding.prototype, "callToActionURL", void 0); /** * USDZExporter creates USDZ files and opens them in Apple QuickLook on iOS/iPadOS/visionOS. * Enables "View in AR" functionality for Apple devices directly from web experiences. * * **Key features:** * - Auto-exports animations and audio sources * - Interactive behaviors via Needle's "Everywhere Actions" system * - RealityKit physics support (iOS 18+, visionOS 1+) * - Custom QuickLook overlay with call-to-action buttons * - Progressive texture/mesh LOD handling * * [![](https://cloud.needle.tools/-/media/YyUz1lUVlOhEY4fWZ-oMsA.gif)](https://engine.needle.tools/samples/?overlay=samples&tag=usdz) * * **Automatic setup:** * - Creates QuickLook button on compatible devices * - Respects {@link XRFlag} for AR-specific visibility * - Handles {@link WebARSessionRoot} scale * * **Custom extensions:** * Add custom behaviors by implementing {@link IUSDExporterExtension} and adding to {@link extensions} array. * * **Debug:** Use `?debugusdz` URL parameter. Press 'T' to trigger export. * * @example Basic USDZ export * ```ts * const exporter = myObject.addComponent(USDZExporter); * exporter.objectToExport = productModel; * exporter.autoExportAnimations = true; * exporter.interactive = true; // Enable QuickLook behaviors * * // Trigger export * await exporter.exportAndOpen(); * ``` * * @example Custom branding * ```ts * exporter.customBranding = { * callToAction: "Buy Now", * checkoutTitle: "Product Name", * callToActionURL: "https://shop.example.com" * }; * ``` * * @summary Export 3D objects as USDZ files for Apple QuickLook AR * @category XR * @group Components * @see {@link WebXR} for WebXR-based AR/VR * @see {@link WebARSessionRoot} for AR placement and scaling * @see {@link XRFlag} for AR-specific object visibility * @see {@link CustomBranding} for QuickLook overlay customization * @link https://engine.needle.tools/samples/?overlay=samples&tag=usdz */ export class USDZExporter extends Behaviour { /** Called before the USDZ file is exported */ static beforeExport = new EventList(); /** Called after the USDZ file has been exported */ static afterExport = new EventList(); /** Called before LOD level are exported. Can be used to override the LOD export settings */ static beforeLODExport = new EventList(); /** * Assign the object to export as USDZ file. If undefined or null, the whole scene will be exported. */ objectToExport = undefined; /** Collect all Animations/Animators automatically on export and emit them as playing at the start. * Animator state chains and loops will automatically be collected and exported in order as well. * If this setting is off, Animators need to be registered by components – for example from PlayAnimationOnClick. */ autoExportAnimations = true; /** Collect all AudioSources automatically on export and emit them as playing at the start. * They will loop according to their settings. * If this setting is off, Audio Sources need to be registered by components – for example from PlayAudioOnClick. */ autoExportAudioSources = true; exportFileName = undefined; customUsdzFile = undefined; customBranding; // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking) anchoringType = "plane"; maxTextureSize = 2048; // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking) planeAnchoringAlignment = "horizontal"; /** Enabling this option will export QuickLook-specific preliminary behaviours along with the USDZ files. * These extensions are only supported on QuickLook on iOS/visionOS/MacOS. * Keep this option off for general USDZ usage. */ interactive = true; /** Enabling this option will export the USDZ file with RealityKit physics components. * Rigidbody and Collider components will be converted to their RealityKit counterparts. * Physics are supported on QuickLook in iOS 18+ and VisionOS 1+. * Physics export is automatically turned off when there are no Rigidbody components anywhere on the exported object. */ physics = true; allowCreateQuicklookButton = true; quickLookCompatible = true; /** * Extensions to add custom behaviors and interactions to the USDZ file. * You can add your own extensions here by extending {@link IUSDExporterExtension}. */ extensions = []; link; button; /** @internal */ start() { if (debug) { console.log("USDZExporter", this); console.log("Debug USDZ Mode. Press 'T' to export"); window.addEventListener("keydown", (evt) => { switch (evt.key) { case "t": this.exportAndOpen(); break; } }); } // fall back to this object or to the scene if it's empty and doesn't have a mesh if (!this.objectToExport) this.objectToExport = this.gameObject; if (!this.objectToExport?.children?.length && !this.objectToExport?.isMesh) this.objectToExport = this.context.scene; } /** @internal */ onEnable() { const supportsQuickLook = DeviceUtilities.supportsQuickLookAR(); const ios = DeviceUtilities.isiOS() || DeviceUtilities.isiPad(); if (!this.button && (debug || supportsQuickLook || ios)) { if (this.allowCreateQuicklookButton) this.button = this.createQuicklookButton(); this.lastCallback = this.quicklookCallback.bind(this); this.link = ensureQuicklookLinkIsCreated(this.context, supportsQuickLook); this.link.addEventListener('message', this.lastCallback); } if (debug) showBalloonMessage("USDZ Exporter enabled: " + this.name); document.getElementById("open-in-ar")?.addEventListener("click", this.onClickedOpenInARElement); InternalUSDZRegistry.registerExporter(this); } /** @internal */ onDisable() { this.button?.remove(); this.link?.removeEventListener('message', this.lastCallback); if (debug) showBalloonMessage("USDZ Exporter disabled: " + this.name); document.getElementById("open-in-ar")?.removeEventListener("click", this.onClickedOpenInARElement); InternalUSDZRegistry.unregisterExporter(this); } onClickedOpenInARElement = (evt) => { evt.preventDefault(); this.exportAndOpen(); }; /** * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook. * Use the various public properties of USDZExporter to customize export behaviour. * @deprecated use {@link exportAndOpen} instead */ async exportAsync() { return this.exportAndOpen(); } /** * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook. * @returns a Promise<Blob> containing the USDZ file */ async exportAndOpen() { let name = this.exportFileName ?? this.objectToExport?.name ?? this.name; name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file if (!hasProLicense()) { if (name !== "") name += "-"; name += "MadeWithNeedle"; } if (!this.link) this.link = ensureQuicklookLinkIsCreated(this.context, DeviceUtilities.supportsQuickLookAR()); // ability to specify a custom USDZ file to be used instead of a dynamic one if (this.customUsdzFile) { if (debug) console.log("Exporting custom usdz", this.customUsdzFile); this.openInQuickLook(this.customUsdzFile, name); return null; } if (!this.objectToExport) { console.warn("No object to export", this); return null; } USDZExporter.beforeExport.invoke({ exporter: this }); const blob = await this.export(this.objectToExport) .finally(() => { USDZExporter.afterExport.invoke({ exporter: this }); }); if (!blob) { console.error("USDZ generation failed. Please report a bug", this); return null; } if (debug) console.log("USDZ generation done. Downloading as " + name); // TODO Potentially we have to detect QuickLook availability here, // and download the file instead. But browsers keep changing how they deal with non-user-initiated downloads... // https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/#:~:text=inside%20the%20anchor.-,Feature%20Detection,-To%20detect%20support /* if (!DeviceUtilities.supportsQuickLookAR()) this.download(blob, name); else */ this.openInQuickLook(blob, name); return blob; } /** * Creates an USDZ file from the current scene or assigned objectToExport and opens it in QuickLook. * @returns a Promise<Blob> containing the USDZ file */ async export(objectToExport) { // make sure we have an object to export if (!objectToExport) { console.warn("No object to export"); return null; } // if we are already exporting, wait for the current export to finish const taskForThisObject = this._currentExportTasks.get(objectToExport); if (taskForThisObject) { return taskForThisObject; } // start the export const task = this.internalExport(objectToExport); // store the task if (task instanceof Promise) { this._currentExportTasks.set(objectToExport, task); return task.then((blob) => { this._currentExportTasks.delete(objectToExport); return blob; }).catch((e) => { this._currentExportTasks.delete(objectToExport); console.error("Error during USDZ export – please report a bug!", e); return null; }); } return task; } _currentExportTasks = new Map(); _previousTimeScale = 1; async internalExport(objectToExport) { Progress.start("export-usdz", { onProgress: (progress) => { this.dispatchEvent(new CustomEvent("export-progress", { detail: { progress } })); } }); Progress.report("export-usdz", { message: "Starting export", totalSteps: 40, currentStep: 0 }); Progress.report("export-usdz", { message: "Load progressive textures", autoStep: 5 }); Progress.start("export-usdz-textures", "export-usdz"); // force Sprites to be created const sprites = GameObject.getComponentsInChildren(objectToExport, SpriteRenderer); for (const sprite of sprites) { if (sprite && sprite.enabled) { sprite.updateSprite(true); // force create } } // trigger progressive textures to be loaded: const renderers = GameObject.getComponentsInChildren(objectToExport, Renderer); const progressiveLoading = new Array(); let progressiveTasks = 0; // TODO: it would be better to directly integrate this into the exporter and *on export* request the correct LOD level for textures and meshes instead of relying on the renderer etc for (const rend of renderers) { for (const mesh of rend.sharedMeshes) { if (mesh) { let lodLevel = 0; const args = { exporter: this, type: "mesh", object: rend.gameObject, mesh: mesh, }; USDZExporter.beforeLODExport.invoke(args); if (args.overrideLevel !== undefined) { if (args.overrideLevel === -1) { if (debug) console.warn("Skipping LOD export for mesh due to overrideLevel -1", rend.gameObject, mesh); continue; // skip LOD assignment } else if (args.overrideLevel >= 0) { lodLevel = args.overrideLevel; if (debug) console.log("Overriding LOD level for mesh export to level " + lodLevel + " " + mesh.name); } } const task = NEEDLE_progressive.assignMeshLOD(mesh, lodLevel); if (task instanceof Promise) progressiveLoading.push(new Promise((resolve, reject) => { task.then(() => { progressiveTasks++; Progress.report("export-usdz-textures", { message: "Loaded progressive mesh", currentStep: progressiveTasks, totalSteps: progressiveLoading.length }); resolve(); }).catch((err) => reject(err)); })); } } for (const mat of rend.sharedMaterials) { if (mat) { let lodLevel = 0; const args = { exporter: this, type: "texture", object: rend.gameObject, material: mat }; USDZExporter.beforeLODExport.invoke(args); if (args.overrideLevel !== undefined) { if (args.overrideLevel === -1) { if (debug) console.warn("Skipping LOD assignment due to overrideLevel -1", rend.gameObject, mat); continue; // skip LOD assignment } else if (args.overrideLevel >= 0) { lodLevel = args.overrideLevel; if (debug) console.log("Overriding LOD level for texture export to level " + lodLevel + " " + mat.name); } } const task = NEEDLE_progressive.assignTextureLOD(mat, lodLevel); if (task instanceof Promise) progressiveLoading.push(new Promise((resolve, reject) => { task.then(() => { progressiveTasks++; Progress.report("export-usdz-textures", { message: "Loaded progressive texture", currentStep: progressiveTasks, totalSteps: progressiveLoading.length }); resolve(); }).catch((err) => reject(err)); })); } } } if (debug) showBalloonMessage("Progressive Loading: " + progressiveLoading.length); await Promise.all(progressiveLoading); if (debug) showBalloonMessage("Progressive Loading: done"); Progress.end("export-usdz-textures"); // apply XRFlags const currentXRState = XRState.Global.Mask; XRState.Global.Set(XRStateFlag.AR); const exporter = new ThreeUSDZExporter(); // We're creating a new animation extension on each export to avoid issues with multiple exports. // TODO we probably want to do that with all the extensions... // Ordering of extensions is important const animExt = new AnimationExtension(this.quickLookCompatible); let physicsExt = undefined; const defaultExtensions = []; if (this.interactive) { defaultExtensions.push(new BehaviorExtension()); defaultExtensions.push(new AudioExtension()); // If physics are enabled, and there are any Rigidbody components in the scene, // add the PhysicsExtension to the default extensions. if (globalThis["NEEDLE_USE_RAPIER"]) { const rigidbodies = GameObject.getComponentsInChildren(objectToExport, Rigidbody); if (rigidbodies.length > 0) { if (this.physics) { physicsExt = new PhysicsExtension(); defaultExtensions.push(physicsExt); } else if (isDevEnvironment()) { console.warn("USDZExporter: Physics export is disabled, but there are active Rigidbody components in the scene. They will not be exported."); } } } defaultExtensions.push(new TextExtension()); defaultExtensions.push(new USDZUIExtension()); } const extensions = [animExt, ...defaultExtensions, ...this.extensions]; const eventArgs = { self: this, exporter: exporter, extensions: extensions, object: objectToExport }; Progress.report("export-usdz", "Invoking before-export"); this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs })); // make sure we apply the AR scale this.applyWebARSessionRoot(); // freeze time this._previousTimeScale = this.context.time.timeScale; this.context.time.timeScale = 0; // Implicit registration and actions for Animators and Animation components // Currently, Animators properly build PlayAnimation actions, but Animation components don't. Progress.report("export-usdz", "auto export animations and audio sources"); const implicitBehaviors = new Array(); if (this.autoExportAnimations) { implicitBehaviors.push(...registerAnimatorsImplictly(objectToExport, animExt)); } const audioExt = extensions.find(ext => ext.extensionName === "Audio"); if (audioExt && this.autoExportAudioSources) implicitBehaviors.push(...registerAudioSourcesImplictly(objectToExport, audioExt)); //@ts-ignore exporter.debug = debug; exporter.pruneUnusedNodes = !debugUsdzPruning; const instancedRenderers = InstancingHandler.instance.objs.map(x => x.batchedMesh); exporter.keepObject = (object) => { let keep = true; // This explicitly removes geometry and material data from disabled renderers. // Note that this is different to the object itself being active – // here, we have an active object with a disabled renderer. const renderer = GameObject.getComponent(object, Renderer); if (renderer && !renderer.enabled) keep = false; // Check if this is an instancing container. // Instances are already included in the export. if (keep && instancedRenderers.includes(object)) keep = false; if (keep && GameObject.getComponentInParent(object, ContactShadows)) keep = false; if (keep && GameObject.getComponentInParent(object, GroundProjectedEnv)) keep = false; if (debug && !keep) console.log("USDZExporter: Discarding object", object); return keep; }; exporter.beforeWritingDocument = () => { // Warn if there are any physics components on animated objects or their children if (isDevEnvironment() && animExt && physicsExt) { const animatedObjects = animExt.animatedRoots; for (const object of animatedObjects) { const rigidBodySources = GameObject.getComponentsInChildren(object, Rigidbody).filter(c => c.enabled); const colliderSources = GameObject.getComponents(object, Collider).filter(c => c.enabled && !c.isTrigger); if (rigidBodySources.length > 0 || colliderSources.length > 0) { console.error("An animated object has physics components in its child hierarchy. This can lead to undefined behaviour due to a bug in Apple's QuickLook (FB15925487). Remove the physics components from child objects or verify that you get the expected results.", object); } } } }; // Collect invisible objects so that we can disable them if // - we're exporting for QuickLook // - and interactive behaviors are allowed. // When exporting for regular USD, we're supporting the "visibility" attribute anyways. const objectsToDisableAtSceneStart = new Array(); if (this.objectToExport && this.quickLookCompatible && this.interactive) { this.objectToExport.traverse((obj) => { if (!obj.visible) { objectsToDisableAtSceneStart.push(obj); } }); } const behaviorExt = extensions.find(ext => ext.extensionName === "Behaviour"); if (this.interactive && behaviorExt && objectsToDisableAtSceneStart.length > 0) { behaviorExt.addBehavior(disableObjectsAtStart(objectsToDisableAtSceneStart)); } let exportInvisible = true; // The only case where we want to strip out invisible objects is // when we're exporting for QuickLook and we're NOT adding interactive behaviors, // since QuickLook on iOS does not support "visibility" tokens. if (this.quickLookCompatible && !this.interactive) exportInvisible = false; // sanitize anchoring types if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face") this.anchoringType = "plane"; if (this.planeAnchoringAlignment !== "horizontal" && this.planeAnchoringAlignment !== "vertical" && this.planeAnchoringAlignment !== "any") this.planeAnchoringAlignment = "horizontal"; Progress.report("export-usdz", "Invoking exporter.parse"); const arraybuffer = await exporter.parse(this.objectToExport, { ar: { anchoring: { type: this.anchoringType, }, planeAnchoring: { alignment: this.planeAnchoringAlignment, }, }, extensions: extensions, quickLookCompatible: this.quickLookCompatible, maxTextureSize: this.maxTextureSize, exportInvisible: exportInvisible, }); const blob = new Blob([arraybuffer], { type: 'model/vnd.usdz+zip' }); this.revertWebARSessionRoot(); // unfreeze time this.context.time.timeScale = this._previousTimeScale; Progress.report("export-usdz", "Invoking after-export"); this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs })); // cleanup – implicit animation behaviors need to be removed again for (const go of implicitBehaviors) { GameObject.destroy(go); } // restore XR flags XRState.Global.Set(currentXRState); // second file: USDA (without assets) //@ts-ignore // const usda = exporter.lastUsda; // const blob2 = new Blob([usda], { type: 'text/plain' }); // this.link.download = name + ".usda"; // this.link.href = URL.createObjectURL(blob2); // this.link.click(); Progress.end("export-usdz"); return blob; } /** * Opens QuickLook on iOS/iPadOS/visionOS with the given content in AR mode. * @param content The URL to the .usdz or .reality file or a blob containing an USDZ file. * @param name Download filename */ openInQuickLook(content, name) { const url = content instanceof Blob ? URL.createObjectURL(content) : content; // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look const overlay = this.buildQuicklookOverlay(); if (debug) console.log("QuickLook Overlay", overlay); const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : ""; const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : ""; const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : ""; this.link.href = url + `#callToAction=${callToAction}&checkoutTitle=${checkoutTitle}&checkoutSubtitle=${checkoutSubtitle}&callToActionURL=${overlay.callToActionURL}`; if (!this.lastCallback) { this.lastCallback = this.quicklookCallback.bind(this); this.link.addEventListener('message', this.lastCallback); } // Open QuickLook this.link.download = name + ".usdz"; this.link.click(); // cleanup // TODO check if we can do that immediately or need to wait until the user returns // if (content instanceof Blob) URL.revokeObjectURL(url); } /** * Downloads the given blob as a file. */ download(blob, name) { USDZExporter.save(blob, name); } // Matches GltfExport.save(blob, filename) static save(blob, filename) { const link = document.createElement('a'); link.style.display = 'none'; document.body.appendChild(link); // Firefox workaround, see #6594 if (typeof blob === "string") link.href = blob; else link.href = URL.createObjectURL(blob); link.download = filename; link.click(); link.remove(); // console.log(link.href); // URL.revokeObjectURL( url ); breaks Firefox... } lastCallback; quicklookCallback(event) { if (event?.data == '_apple_ar_quicklook_button_tapped') { if (debug) showBalloonWarning("Quicklook closed via call to action button"); var evt = new CustomEvent("quicklook-button-tapped", { detail: this }); this.dispatchEvent(evt); if (!evt.defaultPrevented) { const url = new URLSearchParams(this.link.href); if (url) { const callToActionURL = url.get("callToActionURL"); if (debug) showBalloonMessage("Quicklook url: " + callToActionURL); if (callToActionURL) { if (!hasProLicense()) { console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL); } else { globalThis.open(callToActionURL, "_blank"); } } } } } } buildQuicklookOverlay() { const obj = {}; if (this.customBranding) Object.assign(obj, this.customBranding); if (!hasProLicense()) { console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing"); obj.callToAction = "Close"; obj.checkoutTitle = "🌵 Made with Needle"; obj.checkoutSubtitle = "_"; } const needsDefaultValues = obj.callToAction?.length || obj.checkoutTitle?.length || obj.checkoutSubtitle?.length; if (needsDefaultValues) { if (!obj.callToAction?.length) obj.callToAction = "\0"; if (!obj.checkoutTitle?.length) obj.checkoutTitle = "\0"; if (!obj.checkoutSubtitle?.length) obj.checkoutSubtitle = "\0"; } // Use the quicklook-overlay event to customize the overlay this.dispatchEvent(new CustomEvent("quicklook-overlay", { detail: obj })); return obj; } static invertForwardMatrix = new Matrix4().makeRotationY(Math.PI); static invertForwardQuaternion = new Quaternion().setFromEuler(new Euler(0, Math.PI, 0)); _rootSessionRootWasAppliedTo = null; _rootPositionBeforeExport = new Vector3(); _rootRotationBeforeExport = new Quaternion(); _rootScaleBeforeExport = new Vector3(); getARScaleAndTarget() { if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject, sessionRoot: null }; const xr = GameObject.findObjectOfType(WebXR); let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot); if (!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot); let arScale = 1; let _invertForward = false; const target = this.objectToExport; // Note: when USDZ is exported, SessionRoot might not have the correct AR scale, since that is populated upon EnterXR from WebXR. if (xr) { arScale = xr.arScale; } else if (sessionRoot) { arScale = sessionRoot.arScale; // eslint-disable-next-line @typescript-eslint/no-deprecated _invertForward = sessionRoot.invertForward; } const scale = 1 / arScale; const result = { scale, _invertForward, target, sessionRoot: sessionRoot?.gameObject ?? null }; return result; } applyWebARSessionRoot() { if (!this.objectToExport) return; const { scale, _invertForward, target, sessionRoot } = this.getARScaleAndTarget(); const sessionRootMatrixWorld = sessionRoot?.matrixWorld.clone().invert(); this._rootSessionRootWasAppliedTo = target; this._rootPositionBeforeExport.copy(target.position); this._rootRotationBeforeExport.copy(target.quaternion); this._rootScaleBeforeExport.copy(target.scale); target.scale.multiplyScalar(scale); if (_invertForward) target.quaternion.multiply(USDZExporter.invertForwardQuaternion); // udate childs as well target.updateMatrix(); target.updateMatrixWorld(true); if (sessionRoot && sessionRootMatrixWorld) target.matrix.premultiply(sessionRootMatrixWorld); } revertWebARSessionRoot() { if (!this.objectToExport) return; if (!this._rootSessionRootWasAppliedTo) return; const target = this._rootSessionRootWasAppliedTo; target.position.copy(this._rootPositionBeforeExport); target.quaternion.copy(this._rootRotationBeforeExport); target.scale.copy(this._rootScaleBeforeExport); // udate childs as well target.updateMatrix(); target.updateMatrixWorld(true); this._rootSessionRootWasAppliedTo = null; } createQuicklookButton() { const buttoncontainer = WebXRButtonFactory.getOrCreate(); const button = buttoncontainer.createQuicklookButton(); if (!button.parentNode) this.context.menu.appendChild(button); return button; } } __decorate([ serializable(Object3D) ], USDZExporter.prototype, "objectToExport", void 0); __decorate([ serializable() ], USDZExporter.prototype, "autoExportAnimations", void 0); __decorate([ serializable() ], USDZExporter.prototype, "autoExportAudioSources", void 0); __decorate([ serializable() ], USDZExporter.prototype, "exportFileName", void 0); __decorate([ serializable(URL) ], USDZExporter.prototype, "customUsdzFile", void 0); __decorate([ serializable(CustomBranding) ], USDZExporter.prototype, "customBranding", void 0); __decorate([ serializable() ], USDZExporter.prototype, "anchoringType", void 0); __decorate([ serializable() ], USDZExporter.prototype, "maxTextureSize", void 0); __decorate([ serializable() ], USDZExporter.prototype, "planeAnchoringAlignment", void 0); __decorate([ serializable() ], USDZExporter.prototype, "interactive", void 0); __decorate([ serializable() ], USDZExporter.prototype, "physics", void 0); __decorate([ serializable() ], USDZExporter.prototype, "allowCreateQuicklookButton", void 0); __decorate([ serializable() ], USDZExporter.prototype, "quickLookCompatible", void 0); //# sourceMappingURL=USDZExporter.js.map