Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/model-viewer/src/features/extra-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
24 changes: 24 additions & 0 deletions packages/model-viewer/src/test/features/extra-model-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 7 additions & 6 deletions packages/model-viewer/src/three-components/ModelScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
14 changes: 14 additions & 0 deletions packages/modelviewer.dev/examples/multimodel/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ <h2 class="demo-title">Interact with Multiple Models</h2>
Each model has its own position offset, orientation, and scale, and can be manipulated
individually.
</p>
<p>
The <code>offset</code> attribute accepts length units (<code>m</code>, <code>cm</code>,
<code>mm</code>) per component; unitless numbers are treated as meters. For example,
<code>offset="-2.0 0 0"</code> and <code>offset="-200cm 0 0"</code> are equivalent.
</p>
<p>
The <code>orientation</code> attribute uses the same <code>"roll pitch yaw"</code>
convention as <code>model-viewer</code>'s <code>orientation</code>: 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
(<code>deg</code>, <code>rad</code>) per component; unitless numbers are treated as
radians. For example, <code>orientation="0deg 0deg 30deg"</code> applies a 30° yaw
about the Y axis.
</p>
<p>
This example displays three models, each showcasing a different type of interaction:
</p>
Expand Down
Loading