+
+
+
+
Augmented reality
+
+
+
+
+ {!exported && (
+
+ Converts the mesh you are looking at to USDZ so it can be placed at real scale in
+ your room — no app install, no upload to anyone else.
+
+ )}
+
+ {exported && canQuickLook && (
+
+ )}
+
+ {exported && !canQuickLook && (
+
+
+
+
+ AR placement needs Safari on an iPhone or iPad. Scan this from your phone to
+ open the model in your room, or download the file below.
+
+
+
+
+ )}
+
+ {exported && (
+
+
+ Re-rendering this model clears its AR export — the file would no longer match the mesh.
+
+ )}
+
+ );
+}
diff --git a/client/src/components/media/ArExportPanel.test.jsx b/client/src/components/media/ArExportPanel.test.jsx
new file mode 100644
index 0000000000..b0353c0a2a
--- /dev/null
+++ b/client/src/components/media/ArExportPanel.test.jsx
@@ -0,0 +1,147 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import { Group } from 'three';
+import ArExportPanel from './ArExportPanel';
+
+const uploadImageTo3dUsdz = vi.fn();
+vi.mock('../../services/api', () => ({
+ imageTo3dUsdzUrl: (id) => `/api/image-to-3d/models/${id}/usdz`,
+ uploadImageTo3dUsdz: (...a) => uploadImageTo3dUsdz(...a),
+}));
+
+// The real exporter pulls three's USDZ writer and a zip library and walks GPU
+// textures — none of which jsdom can run. What this suite covers is the contract
+// AROUND it: what gets exported, what gets persisted, and which affordance each
+// browser is offered. `supportsArQuickLook` is the switch that decides the last
+// one, so it is the only thing stubbed per-test.
+const usdz = vi.hoisted(() => ({ quickLook: false, bytes: null, error: null, triangles: 1000 }));
+vi.mock('../../lib/usdzExport.js', async (importOriginal) => ({
+ ...(await importOriginal()),
+ supportsArQuickLook: () => usdz.quickLook,
+ countSceneTriangles: () => usdz.triangles,
+ exportSceneToUsdz: vi.fn(async () => {
+ if (usdz.error) throw usdz.error;
+ return usdz.bytes;
+ }),
+}));
+vi.mock('../../lib/qrCode', () => ({
+ generateQrCodeSvg: (text) => `