diff --git a/fixtures/spatial-externalmesh-glb.ts b/fixtures/spatial-externalmesh-glb.ts
new file mode 100644
index 0000000..0ffdd02
--- /dev/null
+++ b/fixtures/spatial-externalmesh-glb.ts
@@ -0,0 +1 @@
+// TODO
\ No newline at end of file
diff --git a/fixtures/spatial-externalmesh-glb.xsml b/fixtures/spatial-externalmesh-glb.xsml
index 759f436..2bc47c1 100644
--- a/fixtures/spatial-externalmesh-glb.xsml
+++ b/fixtures/spatial-externalmesh-glb.xsml
@@ -6,7 +6,5 @@
-
+
diff --git a/src/impl-headless.ts b/src/impl-headless.ts
index c6b0fb9..3ad0f40 100644
--- a/src/impl-headless.ts
+++ b/src/impl-headless.ts
@@ -117,6 +117,8 @@ export class HeadlessNativeDocument extends EventTarget implements NativeDocumen
closed: boolean = false;
private _scene: BABYLON.Scene;
+ private _preloadMeshes: Map> = new Map();
+ private _preloadAnimationGroups: Map = new Map();
constructor() {
super();
@@ -136,11 +138,11 @@ export class HeadlessNativeDocument extends EventTarget implements NativeDocumen
getContainerPose(): XRPose {
throw new Error('Method not implemented.');
}
- getPreloadedMeshes(): Map {
- throw new Error('Method not implemented.');
+ getPreloadedMeshes(): Map> {
+ return this._preloadMeshes;
}
getPreloadedAnimationGroups(): Map {
- throw new Error('Method not implemented.');
+ return this._preloadAnimationGroups;
}
observeInputEvent(name?: string): void {
throw new Error('Method not implemented.');
diff --git a/src/impl-interfaces.ts b/src/impl-interfaces.ts
index 3ed6377..65b0690 100644
--- a/src/impl-interfaces.ts
+++ b/src/impl-interfaces.ts
@@ -184,7 +184,7 @@ export interface NativeDocument extends EventTarget {
*
* When loading a XSML document, the implementation should preload the specific meshes.
*/
- getPreloadedMeshes(): Map;
+ getPreloadedMeshes(): Map>;
/**
* It returns a map of preloaded animation groups.
*/
diff --git a/src/living/nodes/HTMLLinkElement.ts b/src/living/nodes/HTMLLinkElement.ts
index 09e33f8..c517936 100644
--- a/src/living/nodes/HTMLLinkElement.ts
+++ b/src/living/nodes/HTMLLinkElement.ts
@@ -176,21 +176,56 @@ export default class HTMLLinkElementImpl extends HTMLElementImpl implements HTML
}
private async _loadStylesheet() {
- // TODO
+ throw new DOMException('Not implemented yet.', 'NOT_SUPPORTED_ERR');
}
private async _loadSpatialModel() {
- const nativeDocument = this._hostObject;
- const url = new URL(this.href, this._ownerDocument._URL);
- const resArrayBuffer = await nativeDocument.userAgent.resourceLoader.fetch(url.href, {}, 'arraybuffer');
- const extname = path.extname(url.pathname);
- const importedResult = await BABYLON.SceneLoader.ImportMeshAsync(
- '',
- '',
- new Uint8Array(resArrayBuffer),
- nativeDocument.getNativeScene(),
- null,
- extname
- );
+ const id = this.getAttribute('id');
+ if (id == null) {
+ throw new DOMException('The id attribute is required for spatial-model.', 'INVALID_STATE_ERR');
+ }
+
+ try {
+ const nativeDocument = this._hostObject;
+ const url = new URL(this.href, this._ownerDocument._URL);
+ const resArrayBuffer = await nativeDocument.userAgent.resourceLoader.fetch(url.href, {}, 'arraybuffer');
+ const extname = path.extname(url.pathname);
+ const importedResult = await BABYLON.SceneLoader.ImportMeshAsync(
+ '',
+ '',
+ new Uint8Array(resArrayBuffer),
+ nativeDocument.getNativeScene(),
+ null,
+ extname
+ );
+
+ const transformNodesAndMeshes = importedResult.transformNodes.concat(importedResult.meshes);
+ for (const node of transformNodesAndMeshes) {
+ node.setEnabled(false);
+ /**
+ * If the extension is .glb or .gltf, we assume the nodes are in right-handed system.
+ *
+ * Babylon.js doesn't convert the mesh to left-handed system even though the loader and scene are in left-handed system, it
+ * works with root node's rotation and scaling correction and the renderer, thus it doesn't work for our case, that requires
+ * the right data in the left-handed system for mesh itself.
+ *
+ * Thus we add a tag to the node to indicate that the mesh is in right-handed system, and we will convert it to left-handed
+ * at serializer.
+ */
+ if (extname === '.glb' || extname === '.gltf') {
+ /**
+ * The tag will be copied when this node is cloned, so this is a exact match for our use case and don't need to handle the
+ * same tagging on the cloned nodes.
+ */
+ BABYLON.Tags.AddTagsTo(node, 'right-handed-system');
+ }
+ }
+ this._hostObject.getPreloadedMeshes().set(id, transformNodesAndMeshes);
+ if (importedResult.animationGroups?.length > 0) {
+ this._hostObject.getPreloadedAnimationGroups().set(id, importedResult.animationGroups);
+ }
+ } catch (err) {
+ throw new DOMException(`Failed to load spatial model(${this.href}): ${err.message}`, 'INVALID_STATE_ERR');
+ }
}
}
diff --git a/src/living/nodes/SpatialMeshElement.ts b/src/living/nodes/SpatialMeshElement.ts
index f06cde5..29c3398 100644
--- a/src/living/nodes/SpatialMeshElement.ts
+++ b/src/living/nodes/SpatialMeshElement.ts
@@ -1,6 +1,27 @@
+import BABYLON from 'babylonjs';
import { NativeDocument } from '../../impl-interfaces';
+import DOMExceptionImpl from '../domexception';
import { SpatialElement } from './SpatialElement';
+export function cloneWithOriginalRefs(
+ source: BABYLON.Node,
+ name: string,
+ newParent: BABYLON.Node,
+ onClonedObversaval?: (origin: BABYLON.Node, cloned: BABYLON.Node) => void
+) {
+ const result = source.clone(name, newParent, true);
+ result.setEnabled(true);
+
+ if (typeof onClonedObversaval === 'function') {
+ onClonedObversaval(source, result);
+ }
+ const directDescendantsOfSource = source.getDescendants(true);
+ for (let child of directDescendantsOfSource) {
+ cloneWithOriginalRefs(child, child.name, result, onClonedObversaval);
+ }
+ return result;
+}
+
export default class SpatialMeshElement extends SpatialElement {
constructor(
hostObject: NativeDocument,
@@ -12,8 +33,120 @@ export default class SpatialMeshElement extends SpatialElement {
});
}
+ get ref(): string {
+ return this.getAttribute('ref');
+ }
+ set ref(value: string) {
+ this.setAttribute('ref', value);
+ }
+
+ get selector(): string {
+ return this.getAttribute('selector') || '__root__';
+ }
+ set selector(value: string) {
+ this.setAttribute('selector', value);
+ }
+
_attach(): void {
- super._attach();
- // TODO
+ super._attach(this._instantiate());
+ }
+
+ _instantiate(): BABYLON.Node {
+ const { ref, selector } = this;
+
+ if (!ref) {
+ throw new DOMExceptionImpl('ref is required in ', 'INVALID_STATE_ERR');
+ }
+ if (!this._hostObject.getPreloadedMeshes().has(ref)) {
+ throw new DOMExceptionImpl(`No mesh with ref(${ref}) is preloaded`, 'INVALID_STATE_ERR');
+ }
+
+ const transformNodesAndMeshes = this._hostObject.getPreloadedMeshes().get(ref);
+ if (!transformNodesAndMeshes || !transformNodesAndMeshes.length) {
+ throw new DOMExceptionImpl(`No mesh or transforms with ref(${ref}) is found from preloaded resource`, 'INVALID_STATE_ERR');
+ }
+
+ const targetMesh = transformNodesAndMeshes.find((node) => node.name === selector);
+ if (!targetMesh) {
+ throw new DOMExceptionImpl(`No mesh or transform with selector(${selector}) is found from preloaded resource`, 'INVALID_STATE_ERR');
+ }
+
+ const rootName = this.id || this.getAttribute('name') || this.localName;
+ const originUidToClonedMap: { [key: number]: BABYLON.Node } = {};
+ const clonedMesh = cloneWithOriginalRefs(targetMesh, rootName, null, (origin, cloned) => {
+ originUidToClonedMap[origin.uniqueId] = cloned;
+ });
+
+ /** Set the cloned skeleton, it set the new bone's linking transforms to new. */
+ clonedMesh.getChildMeshes().forEach((childMesh) => {
+ if (childMesh.skeleton) {
+ const clonedSkeleton = childMesh.skeleton.clone(`${childMesh.skeleton.name}-cloned`);
+ clonedSkeleton.bones.forEach(bone => {
+ const linkedTransformNode = bone.getTransformNode();
+ if (!originUidToClonedMap[linkedTransformNode.uniqueId]) {
+ throw new TypeError(`Could not find the linked transform node(${linkedTransformNode.name}) for bone(${bone.name})`);
+ } else {
+ const newTransform = originUidToClonedMap[linkedTransformNode.uniqueId];
+ if (newTransform.getClassName() === 'TransformNode') {
+ bone.linkTransformNode(newTransform as BABYLON.TransformNode);
+ } else {
+ throw new TypeError(`The linked transform node(${linkedTransformNode.name}) for bone(${bone.name}) is not a transform node, actual type is "${newTransform.getClassName()}".`);
+ }
+ }
+ });
+ childMesh.skeleton = clonedSkeleton;
+ }
+ });
+
+ /**
+ * The Babylonjs won't clone the animation group when the related meshes are cloned.
+ *
+ * In this case, we need to clone the animation groups manually, and set the new targets
+ * to the cloned meshes and its related animation groups.
+ */
+ const animationGroups = this._hostObject.getPreloadedAnimationGroups().get(ref);
+ if (animationGroups) {
+ for (let i = 0; i < animationGroups.length; i++) {
+ const animationGroup = animationGroups[i];
+ if (animationGroup.targetedAnimations.length <= 0) {
+ continue;
+ }
+ let id = `${i}`;
+ if (this.id) {
+ id = `#${this.id}`;
+ }
+ const newAnimationGroup = animationGroup.clone(
+ `${animationGroup.name}/${id}`,
+ (oldTarget: BABYLON.Node) => originUidToClonedMap[oldTarget.uniqueId],
+ false
+ );
+
+ /**
+ * When an animation group is cloned into space, we added the following listeners to mark
+ * the animation group as dirty.
+ */
+ function markDirty(animationGroup: BABYLON.AnimationGroup) {
+ /** FIXME(Yorkie): we use the metadata._isDirty as the dirty-update flag for animation group */
+ if (!animationGroup.metadata) {
+ animationGroup.metadata = {};
+ }
+ animationGroup.metadata._isDirty = true;
+ }
+ newAnimationGroup.onAnimationGroupPlayObservable.add(markDirty);
+ newAnimationGroup.onAnimationGroupEndObservable.add(markDirty);
+ newAnimationGroup.onAnimationGroupPauseObservable.add(markDirty);
+
+ // If the animation group is playing, stop it and start the new one.
+ if (animationGroup.isPlaying === true) {
+ try {
+ newAnimationGroup.start(animationGroup.loopAnimation);
+ } catch (err) {
+ console.warn(`Failed to start the animation group(${newAnimationGroup.name}): ${err.message}`);
+ }
+ animationGroup.stop();
+ }
+ }
+ }
+ return clonedMesh;
}
}