Skip to content

Commit d8721e5

Browse files
authored
feat: support length units in extra-model offset (#5180)
`updateModelTransforms` parsed `<extra-model offset="...">` 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.
1 parent 819b798 commit d8721e5

4 files changed

Lines changed: 47 additions & 7 deletions

File tree

packages/model-viewer/src/features/extra-model.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ export class ExtraModelElement extends ReactiveElement {
3333

3434
/**
3535
* Position offset relative to global origin.
36-
* Format: "x y z" in meters (e.g., "1 0 -0.5")
36+
* Format: "x y z". Each component may carry a length unit (m, cm, mm),
37+
* unitless numbers are treated as meters.
3738
*/
3839
@property({type: String}) offset: string|null = null;
3940

packages/model-viewer/src/test/features/extra-model-spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,30 @@ suite('ExtraModel', () => {
7878
expect(scene._models[1].position.x).to.equal(5);
7979
});
8080

81+
test('normalizes offset lengths with units to meters', async () => {
82+
element.loading = 'eager';
83+
element.src = CUBE_GLB_PATH;
84+
85+
const extra = document.createElement('extra-model');
86+
extra.setAttribute('src', CUBE_GLB_PATH);
87+
// 200cm -> 2m, 1000mm -> 1m, explicit meters stay as-is.
88+
extra.setAttribute('offset', '200cm 1000mm -1.5m');
89+
element.appendChild(extra);
90+
91+
await waitForEvent(element, 'load');
92+
93+
const scene = (element as any)[$scene];
94+
expect(scene._models[1].position.x).to.be.closeTo(2, 0.001);
95+
expect(scene._models[1].position.y).to.be.closeTo(1, 0.001);
96+
expect(scene._models[1].position.z).to.be.closeTo(-1.5, 0.001);
97+
98+
// Bare numbers are still treated as meters.
99+
extra.setAttribute('offset', '3 0 0');
100+
await timePasses();
101+
102+
expect(scene._models[1].position.x).to.be.closeTo(3, 0.001);
103+
});
104+
81105
test('applies orientation as rotation on the model quaternion', async () => {
82106
element.loading = 'eager';
83107
element.src = CUBE_GLB_PATH;

packages/model-viewer/src/three-components/ModelScene.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,12 +336,13 @@ export class ModelScene extends Scene {
336336
return;
337337

338338
if (offset) {
339-
const parts = offset.split(' ')
340-
.map(s => s.trim())
341-
.filter(s => s.length > 0)
342-
.map(Number);
343-
if (parts.length === 3 && !parts.some(isNaN)) {
344-
model.position.set(parts[0], parts[1], parts[2]);
339+
const terms = parseExpressions(offset)[0]
340+
.terms as [NumberNode, NumberNode, NumberNode];
341+
if (terms.length >= 3) {
342+
const x = normalizeUnit(terms[0]).number;
343+
const y = normalizeUnit(terms[1]).number;
344+
const z = normalizeUnit(terms[2]).number;
345+
model.position.set(x, y, z);
345346
}
346347
}
347348

packages/modelviewer.dev/examples/multimodel/index.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ <h2 class="demo-title">Interact with Multiple Models</h2>
6060
Each model has its own position offset, orientation, and scale, and can be manipulated
6161
individually.
6262
</p>
63+
<p>
64+
The <code>offset</code> attribute accepts length units (<code>m</code>, <code>cm</code>,
65+
<code>mm</code>) per component; unitless numbers are treated as meters. For example,
66+
<code>offset="-2.0 0 0"</code> and <code>offset="-200cm 0 0"</code> are equivalent.
67+
</p>
68+
<p>
69+
The <code>orientation</code> attribute uses the same <code>"roll pitch yaw"</code>
70+
convention as <code>model-viewer</code>'s <code>orientation</code>: yaw is first
71+
applied about the Y-axis, then pitch about the new local X-axis (positive is
72+
front-down), then roll about the new local Z-axis. It accepts angle units
73+
(<code>deg</code>, <code>rad</code>) per component; unitless numbers are treated as
74+
radians. For example, <code>orientation="0deg 0deg 30deg"</code> applies a 30° yaw
75+
about the Y axis.
76+
</p>
6377
<p>
6478
This example displays three models, each showcasing a different type of interaction:
6579
</p>

0 commit comments

Comments
 (0)