Skip to content

Add Apple AR Quick Look (iOS) so web AR works on iPhone/iPad#34

Closed
jasonkneen wants to merge 5 commits into
mainfrom
claude/clever-dijkstra-M6ykE
Closed

Add Apple AR Quick Look (iOS) so web AR works on iPhone/iPad#34
jasonkneen wants to merge 5 commits into
mainfrom
claude/clever-dijkstra-M6ykE

Conversation

@jasonkneen

@jasonkneen jasonkneen commented Jun 7, 2026

Copy link
Copy Markdown
Owner

What & why

Tiny World already supports Google AR on the web: the existing WebXR surface ("AR Desk") mode requests immersive-ar + hit-test, finds a real surface, anchors the world to it, and lets you move around with the device camera while it stays in place. That path only runs where navigator.xr exists — Android Chrome, Quest — which means iPhone/iPad users got nothing, because iOS Safari has no WebXR at all.

Apple's only on-device web AR is AR Quick Look: open a .usdz through an <a rel="ar"> and the system viewer finds a surface, drops the model on it, and lets the user walk around while it stays anchored — exactly the "locate a surface and place the world on it, then move around it" behaviour requested. This PR adds that path so the feature works on both Apple and Google web AR.

Changes

  • Vendor THREE.USDZExporter (r128) as a classic global script (vendor/three/USDZExporter.r128.js), adapted from the official three.js exporter to drop the ES-module imports and use the already-loaded global fflate. Wired in right after fflate.min.js.
  • Build the USDZ on demand from the live world (engine/world/18-scene-pick-xr.js):
    • Snapshot worldGroup into a fresh group (shared geometry, no clones).
    • Convert each MeshLambert/Basic/Shader material to MeshStandardMaterial (the only type USDZExporter understands), preserving colour / emissive / map, deduped via a cache.
    • Recentre on the origin with the base at y=0 and scale the longest horizontal edge to ~0.5 m so the first placement reads as a tabletop diorama.
    • Wrap the bytes in a model/vnd.usdz+zip Blob → object URL → <a rel="ar"> with the single <img> child Safari requires, clicked programmatically.
  • UI: new "Apple AR" panel button gated on relList.supports('ar'); WebXR buttons are now hidden where there's no WebXR runtime, so iOS shows just the one button. The panel appears if either path is available. Added a visible #xr-status line for AR progress/errors that auto-dismisses outside an immersive session.
  • Documented the Apple path in .codex/skills/tinyworld-webxr/SKILL.md.

Platform matrix

Device Path Button shown
Android Chrome / Quest WebXR immersive-ar (unchanged) AR Desk / Float / Enter world
iPhone / iPad Safari AR Quick Look (new) Apple AR
macOS Safari AR Quick Look (3D preview) Apple AR
Desktop Chrome/Firefox none panel hidden

Testing

  • npm run check ✅ and npm run smoke ✅; node --check clean on the module and the vendored exporter.
  • Added a Node harness that runs the vendored exporter end-to-end with the real fflate against a stub scene and verifies the output is a valid USDZ zip containing a well-formed model.usda (header, mesh defs, material bindings, points, no undefined leakage).
  • The pre-existing tests/db-schema-errors.test.mjs failure is environmental (the postgres dependency isn't installed) and unrelated to this change.
  • Real on-device AR (hit-test placement / Quick Look launch) needs HTTPS + an actual iOS/Android device, which can't run in CI — worth a manual smoke on an iPhone before merge.

Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added Apple AR Quick Look support, enabling iOS/iPadOS users to view TinyWorld scenes in augmented reality.
    • Enhanced XR user interface with status notifications and improved AR mode selection.
    • Automatic detection displays appropriate AR options based on device capabilities—Apple Quick Look for iOS/iPadOS or WebXR for other platforms.

The WebXR surface/float/inside flow only runs where navigator.xr exists
(Android Chrome, Quest — the "Google" AR path). iOS Safari has no WebXR,
so iPhone/iPad users could never place the world in AR.

Apple's only on-device web AR is AR Quick Look: open a .usdz through an
<a rel="ar"> and the system viewer finds a real surface, drops the model
on it, and lets the user walk around while it stays anchored — the same
"place the world on a surface and move around it" behaviour WebXR gives.

- Vendor THREE.USDZExporter (r128) as a classic global script using the
  already-loaded global fflate.
- Build the USDZ on demand from worldGroup: snapshot meshes, convert
  Lambert/Basic/Shader materials to MeshStandardMaterial (the only type
  the exporter supports), recentre with the base at y=0, and scale to a
  tabletop size for a sane first placement.
- Add an "Apple AR" panel button gated on relList.supports('ar'); hide the
  WebXR buttons where there is no WebXR runtime so iOS shows just one
  button. Show the panel if either path is available.
- Add a visible XR status line for AR progress/errors that auto-dismisses
  outside an immersive session.
@netlify

netlify Bot commented Jun 7, 2026

Copy link
Copy Markdown

Deploy Preview for tiny-world-builder ready!

Name Link
🔨 Latest commit b789c4e
🔍 Latest deploy log https://app.netlify.com/projects/tiny-world-builder/deploys/6a252c61f6a641000806cbae
😎 Deploy Preview https://deploy-preview-34--tiny-world-builder.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR adds Apple AR Quick Look support to TinyWorld as a fallback for iOS/iPadOS devices without WebXR. The changes include a new USDZ scene exporter library, UI controls and status display, integration logic that detects Quick Look capability and exports/launches AR, and developer documentation describing the feature.

Changes

Apple AR Quick Look via USDZ Export

Layer / File(s) Summary
USDZ Exporter Library
vendor/three/USDZExporter.r128.js
New Three.js USDZ exporter that traverses scenes, deduplicates geometry and materials into USD prototype definitions, generates per-object instance placements, bakes textures to JPEG, and packages everything into .usdz via fflate.zipSync. Validates THREE global and fflate availability at runtime.
UI Elements and Styling
tiny-world-builder.html, styles/tiny-world.css
Adds hidden "View in AR (Apple)" Quick Look button (#xr-quicklook) adjacent to XR mode buttons, a hidden live-region status element (#xr-status) for AR updates, and CSS styling for a fixed-position glassy status notification pill with auto-hide behavior.
Quick Look Integration and Scene Export
engine/world/18-scene-pick-xr.js
Detects Quick Look support, manages export and status state, updates setXRStatus() to auto-dismiss only when no WebXR session is active, exports scenes with converted materials and expanded instanced meshes, and launches AR by triggering a hidden rel="ar" anchor. Reworks refreshXRSupportUI() to surface Quick Look independently and conditionally enable WebXR buttons based on runtime support.
Apple AR Quick Look Documentation
.codex/skills/tinyworld-webxr/SKILL.md
Documents platform split: Apple iOS/iPadOS uses AR Quick Look linking to .usdz assets, other platforms use WebXR modes. Describes USDZ export workflow, material conversion, recentering/scaling, blob/object URL creation, and feature-detection gating with relList.supports('ar') and navigator.xr.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops through AR Quick Look's grand door,
Scene-to-USDZ, what a tour!
Three.js meshes dancing in formation,
Safari springs with AR sensation,
Cross-platform dreams, now they soar!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately describes the main change: adding Apple AR Quick Look support for iOS/iPad to enable web AR on those platforms.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/clever-dijkstra-M6ykE

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

claude added 4 commits June 7, 2026 06:46
The first iOS export came out washed to white with holes and missing
batched geometry. Root causes and fixes:

- Wrong colour: TinyWorld layers a grayscale detail/noise map over each
  flat-coloured Lambert material, but UsdPreviewSurface wires diffuseColor
  straight to the texture and ignores the base colour it should multiply,
  so every tinted surface (grass, paths, walls) exported as the grayscale
  map = white. Drop maps and export the flat base colour, which also matches
  the low-poly look. Vertex-coloured meshes now average their geometry
  colour, and ShaderMaterials pull their first colour-valued uniform.
- Holes: exported meshes are now double-sided. THREE DoubleSide alone is not
  enough — UsdPreviewSurface meshes are single-sided unless the prim sets
  `uniform bool doubleSided`, so the vendored r128 exporter now emits it.
- Missing geometry: InstancedMeshes (batched voxel builds / crops / fences)
  were skipped entirely; they are now expanded into individual meshes with
  baked instance matrices, with a safety cap on total mesh count.

Dropping textures also makes the USDZ smaller and removes any tainted-canvas
export risk.
Per-frame JS animations (sway/water) don't carry into a static USDZ, so the
AR model looked dead. Add an optional turntable to the vendored exporter:
nest every mesh under one Xform driven by an `xformOp:rotateY` time sample
(0 -> 360 deg over a 16s loop) and declare stage timeCodesPerSecond. Because
the diorama is already centred on the origin, the spin pivots about its centre.

One animated node (two keyframes) keeps the file tiny versus per-mesh
keyframing, and using rotateY in degrees sidesteps matrix-convention pitfalls.
Material/texture bindings use absolute paths, so nesting the meshes is safe.
Static export is unchanged when no turntable option is passed.
The baked rotateY animation hung AR Quick Look's place-in-room (AR) mode on
device. The Object viewer handled it, but AR mode is the priority, so revert
to the static export confirmed working in AR. The colour and double-sided
fixes are unaffected. Restores exporter + caller to their pre-turntable state.
The exporter inlined full point/normal/UV/index arrays for every mesh, and
TinyWorld expands instanced props (crops, fences, voxels) into many copies that
share one geometry object. Since USDZ must be stored uncompressed (ARKit
requirement), all that duplicated geometry text made the file large and slow
for AR Quick Look to load.

Now each unique geometry+material is emitted once as an abstract `class`
prototype, and every placement is a tiny `def Xform` that carries only its
transform and references the prototype. The class never images on its own; each
reference composes the shared mesh in at the instance's location.

Also drop primvars:st (we export untextured flat colours, so UVs are dead
weight) and trim float precision 7 -> 5. Same visuals, much smaller file.

Verified offline: 502 placements collapse to 3 prototypes (geometry written
once each), references resolve, no UV data emitted, braces balanced.
@jasonkneen jasonkneen marked this pull request as ready for review June 9, 2026 06:51
Copilot AI review requested due to automatic review settings June 9, 2026 06:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an Apple AR Quick Look fallback so Tiny World Builder’s “place on a real surface and walk around it” AR flow works on iPhone/iPad Safari (no WebXR), while keeping the existing WebXR path for Android/Quest.

Changes:

  • Vendors a classic-script THREE.USDZExporter (three.js r128) and loads it after global fflate.
  • Builds a USDZ from the live worldGroup on-demand and launches AR Quick Look via <a rel="ar"> on supported Safari.
  • Updates XR UI to show Apple AR when available, and hides WebXR buttons when no WebXR runtime exists; adds an #xr-status line with auto-dismiss behavior.

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vendor/three/USDZExporter.r128.js Adds a vendored USDZ exporter adapted to globals, plus local instancing/prototype optimizations.
engine/world/18-scene-pick-xr.js Implements Quick Look capability detection, world snapshot/export, and XR UI gating/status behavior.
tiny-world-builder.html Adds the Apple AR button/status element and loads the USDZ exporter script.
styles/tiny-world.css Styles the new XR status toast.
.codex/skills/tinyworld-webxr/SKILL.md Documents the Apple AR Quick Look path and gating patterns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1294 to +1298
worldGroup.traverse(obj => {
if (truncated || !obj.visible || !obj.isMesh) return;
const geom = obj.geometry;
if (!geom || !geom.attributes || !geom.attributes.position) return;
if (geom.attributes.position.isInterleavedBufferAttribute) return;
@jasonkneen jasonkneen closed this Jun 9, 2026
@jasonkneen jasonkneen deleted the claude/clever-dijkstra-M6ykE branch June 9, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants