From 538cd0f398141e4e58bd047939a5e5f638c39b2e Mon Sep 17 00:00:00 2001 From: Frank3K <8014077+Frank3K@users.noreply.github.com> Date: Tue, 16 Jun 2026 22:03:20 +0200 Subject: [PATCH] feat: support length units in extra-model offset `updateModelTransforms` parsed `` with a plain `split`/`Number`, so only unitless numbers (meters) worked. Parse the value with `parseExpressions`/`normalizeUnit` instead, mirroring the `orientation` handling, so each component may carry a length unit (`m`/`cm`/`mm`) while bare numbers continue to be treated as meters (e.g. `offset="-2.0 0 0"` and `offset="-200cm 0 0"` are equivalent). Document offset unit support and the existing roll/pitch/yaw orientation convention on the multimodel example, and update the property JSDoc. Add a test covering unit-suffixed offsets and that bare numbers stay in meters. --- .../model-viewer/src/features/extra-model.ts | 3 ++- .../src/test/features/extra-model-spec.ts | 24 +++++++++++++++++++ .../src/three-components/ModelScene.ts | 13 +++++----- .../examples/multimodel/index.html | 14 +++++++++++ 4 files changed, 47 insertions(+), 7 deletions(-) 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 @@

Interact with Multiple Models

Each model has its own position offset, orientation, and scale, and can be manipulated individually.

+

+ 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: