diff --git a/packages/model-viewer/src/features/extra-model.ts b/packages/model-viewer/src/features/extra-model.ts index bb5a447286..dfab9947ea 100644 --- a/packages/model-viewer/src/features/extra-model.ts +++ b/packages/model-viewer/src/features/extra-model.ts @@ -33,7 +33,8 @@ export class ExtraModelElement extends ReactiveElement { /** * Position offset relative to global origin. - * Format: "x y z" in meters (e.g., "1 0 -0.5") + * Format: "x y z". Each component may carry a length unit (m, cm, mm), + * unitless numbers are treated as meters. */ @property({type: String}) offset: string|null = null; diff --git a/packages/model-viewer/src/test/features/extra-model-spec.ts b/packages/model-viewer/src/test/features/extra-model-spec.ts index f538c1550a..da68e3af60 100644 --- a/packages/model-viewer/src/test/features/extra-model-spec.ts +++ b/packages/model-viewer/src/test/features/extra-model-spec.ts @@ -78,6 +78,30 @@ suite('ExtraModel', () => { expect(scene._models[1].position.x).to.equal(5); }); + test('normalizes offset lengths with units to meters', async () => { + element.loading = 'eager'; + element.src = CUBE_GLB_PATH; + + const extra = document.createElement('extra-model'); + extra.setAttribute('src', CUBE_GLB_PATH); + // 200cm -> 2m, 1000mm -> 1m, explicit meters stay as-is. + extra.setAttribute('offset', '200cm 1000mm -1.5m'); + element.appendChild(extra); + + await waitForEvent(element, 'load'); + + const scene = (element as any)[$scene]; + expect(scene._models[1].position.x).to.be.closeTo(2, 0.001); + expect(scene._models[1].position.y).to.be.closeTo(1, 0.001); + expect(scene._models[1].position.z).to.be.closeTo(-1.5, 0.001); + + // Bare numbers are still treated as meters. + extra.setAttribute('offset', '3 0 0'); + await timePasses(); + + expect(scene._models[1].position.x).to.be.closeTo(3, 0.001); + }); + test('applies orientation as rotation on the model quaternion', async () => { element.loading = 'eager'; element.src = CUBE_GLB_PATH; diff --git a/packages/model-viewer/src/three-components/ModelScene.ts b/packages/model-viewer/src/three-components/ModelScene.ts index 9201d174eb..c12ff65dca 100644 --- a/packages/model-viewer/src/three-components/ModelScene.ts +++ b/packages/model-viewer/src/three-components/ModelScene.ts @@ -330,12 +330,13 @@ export class ModelScene extends Scene { return; if (offset) { - const parts = offset.split(' ') - .map(s => s.trim()) - .filter(s => s.length > 0) - .map(Number); - if (parts.length === 3 && !parts.some(isNaN)) { - model.position.set(parts[0], parts[1], parts[2]); + const terms = parseExpressions(offset)[0] + .terms as [NumberNode, NumberNode, NumberNode]; + if (terms.length >= 3) { + const x = normalizeUnit(terms[0]).number; + const y = normalizeUnit(terms[1]).number; + const z = normalizeUnit(terms[2]).number; + model.position.set(x, y, z); } } diff --git a/packages/modelviewer.dev/examples/multimodel/index.html b/packages/modelviewer.dev/examples/multimodel/index.html index 60c933e85a..1e55ba051e 100644 --- a/packages/modelviewer.dev/examples/multimodel/index.html +++ b/packages/modelviewer.dev/examples/multimodel/index.html @@ -60,6 +60,20 @@
+ The offset attribute accepts length units (m, cm,
+ mm) per component; unitless numbers are treated as meters. For example,
+ offset="-2.0 0 0" and offset="-200cm 0 0" are equivalent.
+
+ The orientation attribute uses the same "roll pitch yaw"
+ convention as model-viewer's orientation: yaw is first
+ applied about the Y-axis, then pitch about the new local X-axis (positive is
+ front-down), then roll about the new local Z-axis. It accepts angle units
+ (deg, rad) per component; unitless numbers are treated as
+ radians. For example, orientation="0deg 0deg 30deg" applies a 30° yaw
+ about the Y axis.
+
This example displays three models, each showcasing a different type of interaction: