diff --git a/samples/ui/index.html b/samples/ui/index.html
index 82a9fd8..5dc8267 100644
--- a/samples/ui/index.html
+++ b/samples/ui/index.html
@@ -176,7 +176,7 @@
createPlayerPanel() {
// Panel 2: Interactive Media Player (Top-Center)
const panel = new xb.SpatialPanel({
- width: 1.5,
+ width: 1.3,
height: 1.25,
backgroundColor: '#00000000',
});
@@ -189,11 +189,11 @@
this.add(panel);
const grid = panel.addGrid();
// Space for orbiter
- grid.addRow({weight: 0.25});
+ grid.addRow({weight: 0.0});
// player row
- const playerRow = grid.addRow({weight: 0.75});
+ const playerRow = grid.addRow({weight: 1.0});
const playerPanel = playerRow.addPanel({
- width: 1.0,
+ width: 1.2,
height: 1.2,
backgroundColor: '#21252baa',
});
@@ -221,7 +221,9 @@
.addIconButton({text: 'skip_next', fontSize: 0.4});
controlsRow.addCol({weight: 0.2});
}
- const orbiter = playerGrid.addOrbiter();
+ const orbiter = playerGrid.addOrbiter({
+ orbiterScale: 0.1,
+ });
orbiter.addExitButton();
panel.updateLayouts();
}
diff --git a/src/ui/layouts/Orbiter.ts b/src/ui/layouts/Orbiter.ts
index 547aeeb..42b013f 100644
--- a/src/ui/layouts/Orbiter.ts
+++ b/src/ui/layouts/Orbiter.ts
@@ -5,13 +5,119 @@ import {Grid, GridOptions} from './Grid.js';
* as an exit button or settings icon. It typically "orbits" or remains
* attached to a corner of its parent panel, outside the main content area.
*/
-export type OrbiterOptions = GridOptions;
+
+export type OrbiterPosition =
+ | 'top-right'
+ | 'top-left'
+ | 'bottom-right'
+ | 'bottom-left'
+ | 'top'
+ | 'bottom'
+ | 'left'
+ | 'right';
+
+export type OrbiterOptions = GridOptions & {
+ orbiterPosition?: OrbiterPosition;
+ orbiterScale?: number;
+ offset?: number;
+ elevation?: number;
+};
export class Orbiter extends Grid {
+ orbiterPosition: OrbiterPosition;
+ orbiterScale: number;
+ offset: number;
+ elevation: number;
+
+ // These values are based on Material Design guidelines: https://developer.android.com/design/ui/xr/guides/spatial-ui
+ private static readonly BASE_OFFSET = 0.02; // put the orbiter outside of the parent panel's "draggable region" by default
+ private static readonly BASE_ELEVATION = 0.02; // put the orbiter at 15dp above the parent panel by default
+ private static readonly MAX_OUTWARD = 0.05; // avoid the orbiter being too far away from the parent panel
+
+ constructor(options: OrbiterOptions = {}) {
+ const {
+ orbiterPosition = 'top-left',
+ orbiterScale = 0.2,
+ offset = 0.0,
+ elevation = 0.0,
+ ...gridOptions
+ } = options;
+
+ super(gridOptions);
+
+ this.orbiterPosition = orbiterPosition;
+ this.orbiterScale = orbiterScale;
+ this.offset = offset;
+ this.elevation = elevation;
+ }
+
init() {
super.init();
+ this.scale.set(this.orbiterScale, this.orbiterScale, 1.0);
+ this._place();
+ }
+
+ private _place() {
+ const hx = this.rangeX * 0.5;
+ const hy = this.rangeY * 0.5;
+
+ const rightEdge = +hx;
+ const leftEdge = -hx;
+ const topEdge = +hy;
+ const bottomEdge = -hy;
+
+ // Clamp edge spacing so the orbiter stays within the recommended range:
+ // edgeDelta == -orbiterScale / 2 corresponds to the 50% overlap boundary.
+ const edgeDelta = Math.max(
+ -this.orbiterScale / 2,
+ Math.min(Orbiter.MAX_OUTWARD, Orbiter.BASE_OFFSET + this.offset)
+ );
+
+ // Clamp elevation so the orbiter remains in front of the parent panel and doesn’t float excessively.
+ const zDelta = Math.max(
+ 0,
+ Math.min(Orbiter.MAX_OUTWARD, Orbiter.BASE_ELEVATION + this.elevation)
+ );
+
+ let x = 0.0;
+ let y = 0.0;
+
+ switch (this.orbiterPosition) {
+ case 'top':
+ x = 0.0;
+ y = topEdge + this.orbiterScale / 2 + edgeDelta;
+ break;
+ case 'bottom':
+ x = 0.0;
+ y = bottomEdge - this.orbiterScale / 2 - edgeDelta;
+ break;
+ case 'right':
+ x = rightEdge + this.orbiterScale / 2 + edgeDelta;
+ y = 0.0;
+ break;
+ case 'left':
+ x = leftEdge - this.orbiterScale / 2 - edgeDelta;
+ y = 0.0;
+ break;
+ case 'top-right':
+ x = rightEdge - this.orbiterScale / 2;
+ y = topEdge + this.orbiterScale / 2 + edgeDelta;
+ break;
+ case 'top-left':
+ x = leftEdge + this.orbiterScale / 2;
+ y = topEdge + this.orbiterScale / 2 + edgeDelta;
+ break;
+ case 'bottom-right':
+ x = rightEdge - this.orbiterScale / 2;
+ y = bottomEdge - this.orbiterScale / 2 - edgeDelta;
+ break;
+ case 'bottom-left':
+ x = leftEdge + this.orbiterScale / 2;
+ y = bottomEdge - this.orbiterScale / 2 - edgeDelta;
+ break;
+ }
- this.position.set(-0.45 * this.rangeX, 0.7 * this.rangeY, this.position.z);
- this.scale.set(0.2, 0.2, 1.0);
+ const z = this.position.z + zDelta;
+ this.position.set(x, y, z);
}
}