MobTranslate styleguide

XR and Spatial Interfaces

Carry familiar tasks into spatial contexts with explicit units, stable relationships, understandable input and an operable ordinary-screen path.

Choose the experience before the surface

A useful spatial experience changes how a task is understood or performed. Additional floating windows alone do not improve a source-reading or permission task. Identify the content that benefits from space, the actions that must stay familiar, and the context that must survive returning to an ordinary screen.

Distinguish a conventional two-dimensional app presented by a system, a spatial interface inside a native XR platform, and an immersive WebXR experience. The runtime owns presentation, tracking and available input. This React/DOM package does not supply those services; its cards, shadows and portals remain ordinary web constructs.

ExperienceDesign decisionImplementation evidence
System-presented 2D appKeep the existing task hierarchy and readable adaptive layoutActual platform support for the chosen app and window mode
Spatial panelsGive each panel a purpose and stable relationship to related controlsNative panel sizing, movement, anchoring and input contracts
Immersive sceneUse spatial content when its relationships matter to the taskSession, reference space, rendering loop and accessible alternatives
Ordinary-screen fallbackRetain the same content identity and meaningful actionsComplete keyboard/DOM flow without a headset

Native home space, full space and environment ownership

The captured component pages describe spatial orbiters and panels as full-space features. Keep the ordinary 2D task when that mode is unavailable or the person changes presentation. The web support checker separately queries its explicit immersive-ar/immersive-vr mode; neither result proves an Android space transition.

These are design-reference statements, not installed SDK capabilities. Component pages include implementation-availability caveats, including a spatial-dialog color/elevation customization note. Verify the selected SDK/version and actual supported component before treating a design-kit appearance as shipped behavior.

Reference conceptMeaning in the captured Android XR guidanceWeb boundary
Home spaceConventional app content with ordinary coplanar bars/rails/toolbarsNot an alias for a WebXR session mode.
Full spaceSpatial panels, orbiters, models and environmentsRequires the chosen native runtime; a DOM portal does not provide it.
PassthroughA system-presented view of physical surroundingsDoes not grant the application unrestricted camera/pose access.
Spatial environmentVirtual surrounding context, potentially occluding the physical worldKeep orientation, exit and environmental safety with the runtime contract.

Native launch, movement and field-of-view references

Panels can be grouped flat for comparison, curved for wide media/dashboards or more freely for multitasking. Keep relationships and a predictable arrangement when presentation changes. World-locked placement and a supported lazy-follow policy are different anchoring choices; do not rigidly attach large content to every head movement.

The native reference describes distance-dependent panel scaling and contrast/dimming through standard surface roles. This tool performs only its documented frontal geometry calculation. It does not run those native policies, calibrate an optical display or infer safe placement from a numeric angle.

Captured layout parameterReference valueReview boundary
Initial panel1024 × 720dp at 1.75m; 32dp cornersNative launch contract, not the HTML model’s default or a px conversion.
Maximum panel2560 × 1800dp; no stated full-space minimumContent, input and platform limits still apply.
Movement depths0.75–5mFollow runtime movement/scaling; these are not universal comfort distances.
Vertical center5° below eye levelAdapt to posture and the actual viewing setup.
Primary contentCentral 41° field of viewKeep important controls/reading reachable, not merely inside a wide canvas.

Coordinate frames and anchoring

Define what each object is relative to: the viewer, a stable scene location, another panel, or a real-world anchor. Movement should follow that choice. A control attached to a panel should move with its panel; a scene object should not unexpectedly chase the viewer. Re-centering must preserve a comprehensible task relationship.

The runtime establishes reference spaces and poses. A CSS transform in this document does not establish world tracking. Keep the distinction in design records and implementation tests.

RelationshipUse whenFailure to prevent
Panel-relativeControls manipulate that panel’s contentControls detach or obscure the information they act on
Viewer-relativeThe platform supports an essential reachable controlLarge content follows every head motion and hides the environment
Scene-relativeSpatial position itself helps the taskUnexpected drift, lost location or stale coordinate assumptions
Real-world anchorA verified placement relates to a real surface or objectClaiming reliable anchoring from an untracked browser preview

Physical size, angular size and platform units

A physical size becomes a different angular size when the observer moves. For a frontal panel of width W centered at distance D, its horizontal angle is 2·atan(W/(2D)). Apply the same relation to height or target size. Double both size and distance to preserve the same angle; double distance alone and the angle decreases. This geometric relation does not establish comfortable or readable presentation.

Keep metres, angles, CSS pixels and native layout units separate. Android XR documents its own dp/dmm scale contract. A 56 CSS-pixel button is not automatically a 56dp native XR target, and this tool does not convert between those units. Measure the actual rendering and input boundary in the chosen runtime.

The model below assumes a centered point observer and a perpendicular rectangular panel. It does not include stereo separation, oblique viewing, optical calibration or the runtime’s automatic scaling policies. Its numeric limits keep the tool usable; they are not recommended headset distances.

QuantityMeaningKeep separate from
Physical width / heightPanel dimensions in metresDOM width/height in CSS pixels
Viewing distanceObserver-to-panel-center distance in metresA platform’s default spawn distance or camera clip planes
Angular sizeGeometric visual angle in degreesA guarantee of text legibility or comfort
Target sidePhysical square hit area in this modelIcon size, dwell duration or native dp targets
CSS drawingNormalized explanatory top viewCalibrated physical or stereoscopic headset output

Working geometry and support tool

Change physical dimensions and compare the angular results. Capture a local geometry proposal, edit the inputs and verify that the proposal stays unchanged until captured again. Invalid values remove the current result while retaining the earlier proposal. The support button performs an explicit capability query only; it does not launch immersive content.

Frontal rectangular panel; point observer centered on its normal. These are physical metres and geometric angles. The drawing is a normalized top view, not a calibrated headset image. Height is reported numerically and is not drawn in this top view.

Tool range 0.01–10 m; this range is not a comfort recommendation.

Tool range 0.01–10 m; this range is not a comfort recommendation.

Tool range 0.1–10 m; this range is not a comfort recommendation.

Tool range 0.01–1 m; this range is not a comfort recommendation.

Geometry available. Angles update from the entered physical dimensions.

Horizontal angular size
29.863°
Vertical angular size
18.925°
Target angular size
2.292°
Panel width / height
1.600
Top-view panel geometryA point observer faces the center of a panel. Two rays reach the panel edges. The angle between them is 29.863 degrees. The diagram rescales to fit; its screen pixels are not physical metres.

Solid segment: panel width. Point: observer. Dashed segment: viewing distance. Rays: panel-edge sight lines.

Not checked for immersive-vr.

The check calls isSessionSupported only after activation. It does not request an immersive session, read poses or start camera, audio or sensor capture. Changing the selected mode invalidates a pending result.

Panels, depth and information hierarchy

Give a panel one coherent task or content scope. Keep the primary reading area stable, retain space around related actions and preserve attribution beside the content it qualifies. Depth can separate related layers, but extra layers must not hide essential source, permission or recovery information.

Platform-specific movement, scaling and elevation rules belong to the selected SDK and version. Document those rules instead of turning a CSS z-index or shadow value into a claimed physical depth. Check occlusion from several positions and preserve a deliberate route back to the main task.

  • Establish a readable starting arrangement, then allow supported repositioning without losing task state.
  • Keep a predictable way to return or re-center after a panel has moved out of view.
  • Avoid overlapping critical information or creating a collection of independent floating actions.
  • Preserve content identity when the user resizes, moves or changes the presentation mode.
  • Use explicit hierarchy and labels in addition to depth; users may experience depth cues differently.

Orbiters and the five adapted component families

The captured Material XR component index identifies app bars, dialogs, navigation bars, navigation rails and toolbars. Adapt their task meaning first. The local web equivalents below remain DOM components/compositions; a native XR runtime supplies spatial placement and tracked interaction.

An orbiter is a supporting control surface associated with a panel or entity. Keep that relationship visible and predictable, and reserve the space needed by the main content. A CSS-positioned toolbar is not proof of an anchored spatial control.

FamilyRetain from the familiar taskSpatial responsibility
App barsCurrent task/context, Back and named actionsStable relationship to the content; avoid obstructing its reading area
DialogsNamed decision, deliberate modality, cancel and focus returnVisible/reachable decision surface; system/session dismissal and depth ownership
Navigation barActual destination and retained stateReachable destinations without blocking primary content
Navigation railPersistent orientation among related destinationsPlacement stays associated with the workspace and survives resizing
ToolbarsCommand grouping, availability and meaningful outcomeGroup stays near its subject; avoid excessive detached controls

Native component variants and anatomy

App bars use surface-container/high/highest treatments. Navigation bars and toolbars may also use a tertiary container. Rails describe matched FAB/container combinations: tertiary on surface-container, tertiary-fixed-dim with higher containers, or primary on tertiary-container. Keep matching content colors and verify the actual native scheme. The web equivalents use their own role contracts.

FamilyVariant/parts in the captured chapterRetained web meaning
App barOne small-bar-like orbiter; centered or leading headline; container, leading/trailing actionsCurrent task and named actions; a local bar acts only on its panel.
DialogBasic decision; prominent surface and separate scrimName, decision, cancel, modality and focus return.
Navigation barContainer, icons, labels, active indicator; optional badgesCurrent destination is distinct from focus or a batch-selected record.
Navigation railContained or separately spatialized FAB; optional menu/badges/labelsGlobal orientation and a deliberately emphasized action.
ToolbarHorizontal or vertical floating group with placed controlsCommands near their subject; an actual expansion route for additional commands.

Global versus panel-local orbiters

The captured rail page explicitly requires global navigation but also includes generic wording about inset local actions. Preserve that inconsistency in the design review; the explicit global-only behavior is the conservative reading, not a verified SDK resolution. Placement can be adjusted for ergonomics and RTL without changing what the controls affect.

Toolbars with more than five items may expand/collapse or be split into meaningful groups. Additional bars/toolbars should have a demonstrated task benefit. For more than two panels, the reference suggests considering a navigation bar rather than a rail that fragments the workspace.

FamilyReference context and placementFailure to prevent
App barUsually one global bar above the app; uncommon local bar anchored to its panelLocal controls that unexpectedly affect the entire app; width outside panel/FOV.
Navigation barGlobal app-bottom context or deliberate panel-local contextApp-wide destinations disguised as panel-local commands; placement at panel top.
Navigation railGlobal left/right app edge; no rail between panelsLocal navigation hard to find; rail taller than adjacent panels.
ToolbarUsually panel-local, centered below its subject; uncommon app-global contextDetached commands; vertical toolbar between panels; expansion outside its panel.
Spatial dialogProminent basic dialog in FOV, with supported centering/follow behaviorFull-screen decisions or required actions outside reach/view.

Orbiter dimensions, offsets and figure annotations

Dimension annotations were inspected in the captured native measurement diagrams. They use the reference’s native design scale, not this library’s CSS-pixel tokens. The 20dp/12dp placement guidance is separate from the shapes’ internal padding and target layers. Resolve figure/version/unit details with the selected native implementation rather than copying the labels into inline CSS.

The accessible target specification is 56×56dp or larger, with a visual affordance of at least 48×48dp and a 4dp offset. Do not call a 48-unit action drawing a sufficient 56dp hit target. The existing web component specifications separately expose actual CSS dimensions.

Reference subjectCaptured specification/figure labelsImplementation interpretation
App bar figure72 overall height; 48 action allocation; 24 icon; 12 vertical padding; 6 headline gapsThe action drawing and accessible hit target are distinct.
Navigation bar figure80 height; 112 item allocation; 64 indicator width; 24 icon; 4 indicator/label gapKeep labels/badges readable; do not infer DOM pixels.
Rail figuresContained variant shows 24 icon/spacing labels; separate-FAB variant labels 96 FAB/rail extent, 36 FAB icon and 56 indicator widthRetain contained versus separately spatialized structure; confirm each figure part in the native kit.
Toolbar figures72 container height, 56 adjacent action, 8 separating gap; 12 padding/separator labelsPanel bounds constrain both orientations.
Bar/rail/toolbar placement20dp offset or 12dp inset; bar overlap no more than half its height, rail no more than half its widthKeep primary content unobstructed and global/local meaning visible.
Separate rail FAB20dp association gap; above/below and within adjacent-panel heightDo not turn emphasis into an unrelated floating action.

Spatial dialog hierarchy and motion ownership

Use a basic dialog with the most prominent resting surface/elevation; the captured reference offers surface-container-high/highest and cautions against another surface outranking the active decision. The scrim remains at the app content layer while the dialog is elevated. Visual dimming and actual input/focus blocking are separate responsibilities.

The reference depicts uniform dialog scale/fade and a fade-only scrim, with standard easing/long duration tokens for spatial movement. Keep the decision in FOV, use supported lazy follow when applicable, and preserve cancellation, reduced-motion alternatives and focus return. This does not apply a physical Z transform to the web Dialog or override its existing duration/focus contract.

Multimodal, physical and system accessibility contract

Support repeated comfortable one-handed actions and alternatives for seated, standing or reclined use. Direct reach, distant rays/pinch, gaze/dwell, voice, keyboard/mouse and 6DoF controllers have different targeting/commitment contracts. Keep system Back, launcher and recents reachable through the platform; an app should not trap that navigation.

The reference names TalkBack, voice-to-text, captions, dwell, magnification and color adjustments as system features. It specifies a 14dp minimum body font and nonoverlapping 56dp targets that scale with their parent/text context. Native dp guidance is not an instruction to shrink the web font or equate it with a CSS-pixel minimum.

Use a stable horizon and limit continuous locomotion. Teleport/jump or supported vignetting can be platform choices; none establishes comfort without actual hardware/user testing. Optional spatial audio and user-controlled controller haptics can complement visible state. Allow people to recenter or bring controls closer without losing identity, input or permission context.

Input, feedback and accessible alternatives

Separate pointing or highlighting from commitment. Gaze, ray input, hand interaction, a controller and a keyboard do not share an identical event contract. Choose the runtime’s supported activation model, then verify one activation produces one task effect. Hover or gaze must not silently publish a change.

Preserve understandable names, selected/disabled/busy state, order and recovery. A highlight or haptic can support the result, but the task needs a visible or otherwise available explanation. Do not infer that an application can read all system eye-tracking or hover data.

  • Make targets large enough for the actual interaction and leave useful separation; a visual glyph and its hit area are different things.
  • Offer an alternative to precision dragging, sustained reaching, voice-only interaction or a specific hand gesture.
  • Provide a usable ordinary-screen path for essential source reading, data entry and permission decisions.
  • Keep exit, cancel and return actions reachable when input availability changes.
  • Test selection versus focus/highlight, canceled activation and duplicate event paths separately.
  • Handle interrupted input without losing the draft or treating interruption as consent.

Typography, color and viewing conditions

Evaluate text at the distances, angles and scale settings people can actually use. Keep source spelling and qualifications readable; do not shorten evidence text merely to fit a floating surface. Use a stable reading column and allow text enlargement or a more suitable reading surface.

Opaque displays and optical see-through displays do not render backgrounds in the same way. On an additive display, dark pixels cannot be assumed to create an opaque dark panel, and the physical environment can change apparent contrast. Use the platform’s actual presentation guidance and test the intended conditions. A browser light/dark screenshot does not simulate that optical behavior.

  • Use color roles together with their matched foregrounds, and retain text or shape cues for meaning.
  • Keep essential prose on a stable surface rather than attaching it to an unnecessarily moving object.
  • Check text size, target size and density together instead of solving one by crowding the others.
  • Preserve descriptions and alternatives for meaningful images, diagrams and media.
  • Evaluate the actual hardware/environment as well as a simulator; document which setup produced each observation.

Movement, interruption and user control

Use movement to explain a change in task state. Avoid unexpected movement of the viewer’s frame, large content that constantly follows the head, or an exit that depends on finishing an animation. Provide a predictable way to pause, stop or return. Motion preference and presentation-mode changes should preserve the task result.

Treat visibility loss, reference-space changes, session end and device disconnection as normal lifecycle paths. Cancel stale rendering/input work and retain the relevant application state. On return, reconcile the scene with the current record and permission state rather than resuming an old approval snapshot.

  • A geometry calculation is not a comfort or performance test. Keep those acceptance criteria explicit.
  • Let people stop or use another presentation without a penalty or loss of their draft.
  • Measure frame timing and interaction latency in the actual runtime on representative hardware.
  • Keep camera/pose ownership with the runtime’s current contract; do not invent a universal re-centering transform.
  • Make restoration idempotent so resumed input does not repeat a save, review or publication.

Capability, permission and privacy boundaries

Support detection, session permission, a running session and a successfully completed task are separate facts. The tool reads the selected mode only after an explicit check. Changing the mode invalidates an unfinished result. A failed or unavailable query leaves the HTML geometry tool usable.

For a real WebXR application, use the current specification’s activation/session flow and selected feature requirements. Preserve a visible exit and handle request rejection or session loss. Never start immersive content solely because support was reported.

Request only the information and features needed by the task. Keep spatial observations, recordings, identity, source rights and publication purpose distinct. A browser permission does not establish cultural authority or permission to retain/publish content.

  • State what starts, what is collected and what remains available without immersive presentation.
  • Avoid probing or recording pose/environment data for unrelated analytics.
  • Respect permissions-policy and device/browser restrictions; offer an operable fallback.
  • Do not present a local role picker, capability result or successful animation as authentication or qualified review.

Geometry model source

This shared TypeScript module defines the physical input ranges, target containment and trigonometric calculation used by the live tool. Keep it at lib/xr-geometry.ts when using the checked example in this workspace.

typescript
export const geometryFields = [
  {key:'width',label:'Panel width',unit:'m',min:0.01,max:10},
  {key:'height',label:'Panel height',unit:'m',min:0.01,max:10},
  {key:'distance',label:'Viewing distance',unit:'m',min:0.1,max:10},
  {key:'target',label:'Square target side',unit:'m',min:0.01,max:1},
] as const;
export type GeometryKey = typeof geometryFields[number]['key'];
export type GeometryDraft = Record<GeometryKey,string>;
export const initialGeometry:GeometryDraft={width:'0.8',height:'0.5',distance:'1.5',target:'0.06'};
export interface XRGeometry {width:number;height:number;distance:number;target:number;horizontalDegrees:number;verticalDegrees:number;targetDegrees:number;aspect:number}
export function angularSize(size:number,distance:number) {return 2*Math.atan(size/(2*distance))*180/Math.PI;}
export function resolveGeometry(draft:GeometryDraft):{errors:Partial<Record<GeometryKey,string>>;geometry:XRGeometry|null} {
  const values={} as Record<GeometryKey,number>;
  const errors:Partial<Record<GeometryKey,string>>={};
  for(const field of geometryFields) {
    const raw=draft[field.key].trim(),value=Number(raw);
    if(!raw||!Number.isFinite(value)||value<field.min||value>field.max)errors[field.key]=`Enter ${field.label.toLowerCase()} from ${field.min} to ${field.max} ${field.unit}.`;
    values[field.key]=value;
  }
  if(!Object.keys(errors).length&&(values.target>values.width||values.target>values.height))errors.target='The target must fit within both panel dimensions.';
  if(Object.keys(errors).length)return {errors,geometry:null};
  return {errors,geometry:{...values,horizontalDegrees:angularSize(values.width,values.distance),verticalDegrees:angularSize(values.height,values.distance),targetDegrees:angularSize(values.target,values.distance),aspect:values.width/values.height}};
}

Implementation and source

The compiled tool below uses the same controlled draft for validation, geometric output and drawing. Capturing copies the numeric proposal; later edits do not mutate it. Support checks use a request identity because the WebXR capability promise has no abort argument in this adapter. A superseded result cannot overwrite the current mode’s status.

The display drawing is an explanatory vector plot with a text description and equivalent numeric outputs. It does not allocate a WebXR session or render stereo views.

tsx
'use client';
import {useEffect,useId,useRef,useState} from 'react';
import {Button} from '@mobtranslate/ui';
import {CodeBlock} from '../components/code-block';
import {geometryFields,initialGeometry,resolveGeometry,type XRGeometry} from '../lib/xr-geometry';

const actionStyle={maxWidth:'100%',height:'auto',minHeight:44,whiteSpace:'normal',overflowWrap:'anywhere'} as const;
type Mode='immersive-vr'|'immersive-ar';
type SupportAPI={isSessionSupported(mode:Mode):Promise<boolean>};
export function XRGeometryExample() {
  const [ready,setReady]=useState(false);
  const [draft,setDraft]=useState({...initialGeometry});
  const [proposal,setProposal]=useState<XRGeometry|null>(null);
  const [mode,setMode]=useState<Mode>('immersive-vr');
  const [probe,setProbe]=useState('Not checked for immersive-vr.');
  const [busy,setBusy]=useState(false);
  const epoch=useRef(0),pending=useRef<number|null>(null);
  const id=useId();
  const {errors,geometry}=resolveGeometry(draft);
  useEffect(()=>setReady(true),[]);
  useEffect(()=>{epoch.current++;pending.current=null;setBusy(false);setProbe(`Not checked for ${mode}.`);return ()=>{epoch.current++;pending.current=null;};},[mode]);
  async function checkSupport() {
    if(pending.current!==null)return;
    const request=++epoch.current;pending.current=request;setBusy(true);setProbe(`Checking ${mode} support…`);
    try {
      const xr=(navigator as Navigator&{xr?:SupportAPI}).xr;
      let outcome;
      if(!window.isSecureContext)outcome='A secure context is required for WebXR. The HTML geometry tool remains available.';
      else if(!xr)outcome='WebXR is not exposed by this browser. The HTML geometry tool remains available.';
      else outcome=await xr.isSessionSupported(mode)?`Support reported for ${mode}. No session started; tracking, input, rendering and comfort are not verified.`:`No support reported for ${mode}. The HTML geometry tool remains available.`;
      if(epoch.current===request)setProbe(outcome);
    } catch {if(epoch.current===request)setProbe(`Could not check ${mode}. Retry after checking browser, device and permissions-policy availability.`);}
    finally {if(epoch.current===request){pending.current=null;setBusy(false);}}
  }
  const scale=geometry?Math.min(250/(geometry.width/2),240/geometry.distance):0;
  const half=geometry?geometry.width/2*scale:0;
  const panelY=geometry?270-geometry.distance*scale:0;
  return <div data-xr-ready={ready} className="space-y-5">
    <p>Frontal rectangular panel; point observer centered on its normal. These are physical metres and geometric angles. The drawing is a normalized top view, not a calibrated headset image. Height is reported numerically and is not drawn in this top view.</p>
    <div className="grid gap-4 sm:grid-cols-2">{geometryFields.map(field=><div key={field.key}>
      <label htmlFor={`xr-${field.key}`} className="block mb-2">{field.label} ({field.unit})</label>
      <input id={`xr-${field.key}`} type="number" step="any" min={field.min} max={field.max} className="mt-input w-full" disabled={!ready} value={draft[field.key]} onChange={event=>setDraft(current=>({...current,[field.key]:event.target.value}))} aria-invalid={Boolean(errors[field.key])} aria-describedby={`xr-${field.key}-help${errors[field.key]?` xr-${field.key}-error`:''}`} />
      <p id={`xr-${field.key}-help`} className="mt-2 text-sm">Tool range {field.min}–{field.max} {field.unit}; this range is not a comfort recommendation.</p>
      {errors[field.key]&&<p id={`xr-${field.key}-error`} className="mt-2">{errors[field.key]}</p>}
    </div>)}</div>
    <p role="status" data-xr-validity>{geometry?'Geometry available. Angles update from the entered physical dimensions.':'Geometry unavailable. Correct the marked values; an existing captured proposal is retained.'}</p>
    {geometry&&<div data-xr-results>
      <dl className="grid gap-4 sm:grid-cols-2">
        <div><dt>Horizontal angular size</dt><dd data-xr-horizontal className="font-mono">{geometry.horizontalDegrees.toFixed(3)}°</dd></div>
        <div><dt>Vertical angular size</dt><dd data-xr-vertical className="font-mono">{geometry.verticalDegrees.toFixed(3)}°</dd></div>
        <div><dt>Target angular size</dt><dd data-xr-target className="font-mono">{geometry.targetDegrees.toFixed(3)}°</dd></div>
        <div><dt>Panel width / height</dt><dd data-xr-aspect className="font-mono">{geometry.aspect.toFixed(3)}</dd></div>
      </dl>
      <svg className="w-full mt-4" style={{background:'var(--color-card)',border:'1px solid var(--color-muted-foreground)',borderRadius:'var(--radius-md)'}} viewBox="0 0 600 300" role="img" aria-labelledby={`${id}-title ${id}-description`}>
        <title id={`${id}-title`}>Top-view panel geometry</title>
        <desc id={`${id}-description`}>A point observer faces the center of a panel. Two rays reach the panel edges. The angle between them is {geometry.horizontalDegrees.toFixed(3)} degrees. The diagram rescales to fit; its screen pixels are not physical metres.</desc>
        <line x1="300" y1="270" x2="300" y2={panelY} stroke="var(--color-muted-foreground)" strokeWidth="2" strokeDasharray="5 5" />
        <path d={`M${300-half} ${panelY} L300 270 L${300+half} ${panelY}`} stroke="var(--color-primary)" strokeWidth="3" fill="none" />
        <line x1={300-half} y1={panelY} x2={300+half} y2={panelY} stroke="var(--color-foreground)" strokeWidth="8" />
        <circle cx="300" cy="270" r="7" fill="var(--color-foreground)" />
      </svg>
      <p className="mt-2">Solid segment: panel width. Point: observer. Dashed segment: viewing distance. Rays: panel-edge sight lines.</p>
    </div>}
    <div className="flex flex-wrap gap-3">
      <Button data-xr-capture style={actionStyle} type="button" disabled={!ready||!geometry} onClick={()=>{if(geometry)setProposal({...geometry});}}>Capture geometry proposal</Button>
      <Button style={actionStyle} type="button" variant="outline" disabled={!ready} onClick={()=>setDraft({...initialGeometry})}>Reset geometry inputs</Button>
    </div>
    {proposal&&<div data-xr-proposal><p className="font-semibold">Local geometry proposal</p><p>This frozen copy remains unchanged while you edit. It is not applied to a device, saved remotely or a comfort approval.</p><CodeBlock language="json" code={JSON.stringify({units:'metres',assumption:'Frontal panel centered on a point observer',...proposal},null,2)} /></div>}
    <div className="border-t border-[var(--color-border)] pt-5 space-y-3">
      <label htmlFor="xr-mode" className="block">WebXR mode to check</label>
      <select id="xr-mode" className="mt-input w-full" disabled={!ready} value={mode} onChange={event=>setMode(event.target.value as Mode)}><option value="immersive-vr">Immersive VR</option><option value="immersive-ar">Immersive AR</option></select>
      <Button style={actionStyle} type="button" disabled={!ready||busy} loading={busy} onClick={()=>void checkSupport()}>Check browser support</Button>
      <p role="status" data-xr-support>{probe}</p>
      <p style={{overflowWrap:'anywhere'}}>The check calls isSessionSupported only after activation. It does not request an immersive session, read poses or start camera, audio or sensor capture. Changing the selected mode invalidates a pending result.</p>
    </div>
  </div>;
}

Verification and failure cases

  • Use a known geometric case: width twice distance gives a 90° horizontal angle. Scale both together and verify the angle stays equal.
  • Check invalid/empty inputs, an oversized target, reset and preservation of the captured proposal.
  • Use keyboard-only controls, narrow reflow and both themes; ensure the diagram’s equivalent values remain readable outside its pixels.
  • Exercise support true/false, absent API, rejection and a held result superseded by a mode change. Verify that no requestSession or sensor/camera operation is invoked.
  • For a real XR implementation, test its reference-space/anchor stability, modality, tracked input, frame timing, text/target accessibility, permission rejection, restoration and ordinary-screen fallback.
  • Do not call this geometry tool a headset emulator, treat physical dimensions as CSS pixels, infer runtime readiness from support detection or use a source/review badge to imply authority.