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: