{"id":50996535,"url":"https://github.com/a7alabs/apple-spatial-capture","last_synced_at":"2026-06-20T10:02:21.537Z","repository":{"id":362597452,"uuid":"1259450777","full_name":"A7ALABS/apple-spatial-capture","owner":"A7ALABS","description":"Flutter plugin for Apple spatial capture workflows on iOS, iPadOS, and macOS.","archived":false,"fork":false,"pushed_at":"2026-06-05T02:03:31.000Z","size":40157,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T04:04:06.642Z","etag":null,"topics":["flutter","lidar","photogrammetry","point-cloud","usdz"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/apple_spatial_capture","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/A7ALABS.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-04T14:18:44.000Z","updated_at":"2026-06-05T03:18:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/A7ALABS/apple-spatial-capture","commit_stats":null,"previous_names":["a7alabs/apple-spatial-capture"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/A7ALABS/apple-spatial-capture","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/A7ALABS%2Fapple-spatial-capture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/A7ALABS%2Fapple-spatial-capture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/A7ALABS%2Fapple-spatial-capture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/A7ALABS%2Fapple-spatial-capture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/A7ALABS","download_url":"https://codeload.github.com/A7ALABS/apple-spatial-capture/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/A7ALABS%2Fapple-spatial-capture/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34565244,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-20T02:00:06.407Z","response_time":98,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["flutter","lidar","photogrammetry","point-cloud","usdz"],"created_at":"2026-06-20T10:02:20.200Z","updated_at":"2026-06-20T10:02:21.530Z","avatar_url":"https://github.com/A7ALABS.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# apple_spatial_capture\n\nFlutter plugin for Apple spatial capture workflows on iOS, iPadOS, and macOS.\n\nThe package exposes:\n\n- Object Capture camera-guided photogrammetry on supported iOS 17+ and iPadOS 17+ devices.\n- Photogrammetry reconstruction from existing image paths on supported iOS 17+, iPadOS 17+, and macOS 12+ devices.\n- LiDAR mesh scanning on supported iOS 14+ and iPadOS 14+ devices.\n- RoomPlan room scanning on supported iOS 16+ and iPadOS 16+ devices.\n- Native previews for local and remote `usdz`, `obj`, `glb`, and `gltf` files.\n- Progress events for image-based photogrammetry jobs.\n\n## Installation\n\nInstall the published package from pub.dev:\n\n```sh\nflutter pub add apple_spatial_capture\n```\n\nOr add it manually to your app's `pubspec.yaml`:\n\n```yaml\ndependencies:\n  apple_spatial_capture: ^0.2.0\n```\n\nThen fetch dependencies:\n\n```sh\nflutter pub get\n```\n\nImport the package anywhere you need capture or preview APIs:\n\n```dart\nimport 'package:apple_spatial_capture/apple_spatial_capture.dart';\n```\n\n## Example app\n\nA complete Flutter example app is available in `example/`.\n\n```sh\ncd packages/apple_spatial_capture/example\nflutter create --platforms=ios,macos --project-name apple_spatial_capture_example .\nflutter pub get\nflutter run\n```\n\nThe example includes support checks, all capture entry points,\nimage-based photogrammetry options, progress events, and local/remote preview\nforms.\n\n## Screenshots\n\n### Example app\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003eCapture methods\u003c/th\u003e\n    \u003cth\u003ePhoto reconstruction\u003c/th\u003e\n    \u003cth\u003eModel previews\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/main-screens/capture-methods.png\" alt=\"Capture methods screen\" width=\"220\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/main-screens/photo-to-3d-photogrammetry.png\" alt=\"Photo to 3D photogrammetry screen\" width=\"220\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/main-screens/preview-models.png\" alt=\"Model preview screen\" width=\"220\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n### macOS example\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003eCapture methods\u003c/th\u003e\n    \u003cth\u003ePhoto reconstruction\u003c/th\u003e\n    \u003cth\u003eModel previews\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/macos/Capture%20methods.png\" alt=\"macOS capture methods screen\" width=\"260\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/macos/3d%20from%20photos%20photogrammetry.png\" alt=\"macOS photo reconstruction screen\" width=\"260\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/macos/preview.png\" alt=\"macOS model preview screen\" width=\"260\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n### Object Capture workflow\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003eStart capture\u003c/th\u003e\n    \u003cth\u003eScan object\u003c/th\u003e\n    \u003cth\u003eCapture guidance\u003c/th\u003e\n    \u003cth\u003eReconstruction feedback\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/object-capture-1.png\" alt=\"Object Capture start screen\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/object-capture-2.png\" alt=\"Object Capture scanning screen\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/object-capture-3.png\" alt=\"Object Capture guidance screen\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/reconstruction-feedback-screen.png\" alt=\"Object Capture reconstruction feedback screen\" width=\"180\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n### Capture results and device scanners\n\n\u003ctable\u003e\n  \u003ctr\u003e\n    \u003cth\u003eGenerated result\u003c/th\u003e\n    \u003cth\u003eResult preview\u003c/th\u003e\n    \u003cth\u003eLiDAR scan\u003c/th\u003e\n    \u003cth\u003eRoomPlan scan\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003ctr\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/results/object-capture-result-readme.png\" alt=\"Generated Object Capture result\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/object-capture/results/object-capture-result-2-readme.png\" alt=\"Object Capture result preview\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/lidar/lidar.png\" alt=\"LiDAR scan screen\" width=\"180\"\u003e\u003c/td\u003e\n    \u003ctd\u003e\u003cimg src=\"https://raw.githubusercontent.com/A7ALABS/apple-spatial-capture/main/screenshots/roomplan/roomplan.png\" alt=\"RoomPlan scan screen\" width=\"180\"\u003e\u003c/td\u003e\n  \u003c/tr\u003e\n\u003c/table\u003e\n\n## Apple host app requirements\n\nSet the iOS deployment target to at least `14.0`. Flutter and CocoaPods use the `ios` target for both iPhone and iPadOS apps.\n\n```ruby\n# ios/Podfile\nplatform :ios, '14.0'\n```\n\nAdd camera permission text to `ios/Runner/Info.plist`:\n\n```xml\n\u003ckey\u003eNSCameraUsageDescription\u003c/key\u003e\n\u003cstring\u003eScan objects, rooms, and LiDAR meshes.\u003c/string\u003e\n```\n\nIf your app lets the user select photos for photogrammetry, also add:\n\n```xml\n\u003ckey\u003eNSPhotoLibraryUsageDescription\u003c/key\u003e\n\u003cstring\u003eSelect photos to generate a 3D model.\u003c/string\u003e\n```\n\nThe plugin performs runtime support checks, but you should still gate capture UI in Flutter. Apple support depends on OS version and device hardware.\n\nFor macOS, set the deployment target to at least `12.0`. macOS supports reconstruction from existing photos and local/remote model preview. Guided Object Capture, LiDAR scanning, and RoomPlan capture are available only on supported iOS and iPadOS devices.\n\n## API overview\n\nUse `AppleSpatialCapture.platform` for all operations:\n\n```dart\nfinal capture = AppleSpatialCapture.platform;\n```\n\nPublic components:\n\n| Component | Purpose |\n| --- | --- |\n| `AppleSpatialCapture.platform` | Default platform implementation for method and event channels. |\n| `AppleSpatialCapturePlatform` | Interface used by the plugin and by tests/fakes. |\n| `AppleSpatialCaptureSupport` | Combined support result for photogrammetry, LiDAR, and RoomPlan. |\n| `ApplePhotogrammetryOptions` | Options for `startPhotogrammetryFromImages`. |\n| `AppleSpatialCaptureProgress` | Progress payload emitted during image-based photogrammetry. |\n| `AppleSpatialCaptureProgressStage` | Stage enum for progress events. |\n| `AppleSpatialCaptureFileType` | File type enum for model previews. |\n| `inferAppleSpatialCaptureFileType` | Helper that infers a model file type from a path, URL, or filename. |\n| `AppleSpatialCaptureError` | Exception type thrown by the Dart wrapper. |\n\n## Check platform support\n\nCall `supportStatus()` before rendering capture actions. Platforms other than iOS, iPadOS, and macOS should be gated in app code before using this plugin.\n\n```dart\nimport 'dart:io';\n\nimport 'package:apple_spatial_capture/apple_spatial_capture.dart';\nimport 'package:flutter/material.dart';\n\nclass SpatialSupportPanel extends StatefulWidget {\n  const SpatialSupportPanel({super.key});\n\n  @override\n  State\u003cSpatialSupportPanel\u003e createState() =\u003e _SpatialSupportPanelState();\n}\n\nclass _SpatialSupportPanelState extends State\u003cSpatialSupportPanel\u003e {\n  AppleSpatialCaptureSupport? _support;\n  String? _error;\n\n  @override\n  void initState() {\n    super.initState();\n    _loadSupport();\n  }\n\n  Future\u003cvoid\u003e _loadSupport() async {\n    if (!Platform.isIOS \u0026\u0026 !Platform.isMacOS) {\n      setState(() =\u003e _error = 'Apple spatial capture is only available on Apple platforms.');\n      return;\n    }\n\n    try {\n      final support = await AppleSpatialCapture.platform.supportStatus();\n      if (!mounted) return;\n      setState(() =\u003e _support = support);\n    } on AppleSpatialCaptureError catch (error) {\n      if (!mounted) return;\n      setState(() =\u003e _error = error.message);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    final support = _support;\n\n    if (_error != null) {\n      return Text(_error!);\n    }\n    if (support == null) {\n      return const CircularProgressIndicator();\n    }\n\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.start,\n      children: [\n        Text('Object Capture: ${support.photogrammetry ? \"available\" : \"unavailable\"}'),\n        Text('LiDAR: ${support.lidar ? \"available\" : \"unavailable\"}'),\n        Text('RoomPlan: ${support.roomPlan ? \"available\" : \"unavailable\"}'),\n      ],\n    );\n  }\n}\n```\n\nYou can also call the individual checks when you only need one capability:\n\n```dart\nfinal canUseObjectCapture =\n    await AppleSpatialCapture.platform.isPhotogrammetrySupported();\nfinal canUseLiDAR = await AppleSpatialCapture.platform.isLiDARSupported();\nfinal canUseRoomPlan = await AppleSpatialCapture.platform.isRoomPlanSupported();\n```\n\n## Start Object Capture\n\n`startPhotogrammetryCapture()` opens Apple's guided Object Capture flow. The native view is presented full screen and returns a local model path when the user finishes.\n\n```dart\nFuture\u003cvoid\u003e startGuidedObjectCapture(BuildContext context) async {\n  try {\n    final isSupported =\n        await AppleSpatialCapture.platform.isPhotogrammetrySupported();\n    if (!isSupported) {\n      _showMessage(context, 'Object Capture is not supported on this device.');\n      return;\n    }\n\n    final path = await AppleSpatialCapture.platform.startPhotogrammetryCapture();\n    if (path == null || path.isEmpty) {\n      _showMessage(context, 'Capture was cancelled.');\n      return;\n    }\n\n    await AppleSpatialCapture.platform.previewCapturedModel(path: path);\n  } on AppleSpatialCaptureError catch (error) {\n    _showMessage(context, error.message);\n  }\n}\n\nvoid _showMessage(BuildContext context, String message) {\n  ScaffoldMessenger.of(context).showSnackBar(\n    SnackBar(content: Text(message)),\n  );\n}\n```\n\n## Generate a model from photos\n\n`startPhotogrammetryFromImages()` accepts local image paths. It requires at least 3 images and emits progress through `progressStream`.\n\nThe example below uses `image_picker` for photo selection. Add it to your app if you use the same flow:\n\n```yaml\ndependencies:\n  image_picker: ^1.0.0\n```\n\n```dart\nimport 'dart:async';\n\nimport 'package:apple_spatial_capture/apple_spatial_capture.dart';\nimport 'package:flutter/material.dart';\nimport 'package:image_picker/image_picker.dart';\n\nclass PhotoPhotogrammetryButton extends StatefulWidget {\n  const PhotoPhotogrammetryButton({super.key});\n\n  @override\n  State\u003cPhotoPhotogrammetryButton\u003e createState() =\u003e\n      _PhotoPhotogrammetryButtonState();\n}\n\nclass _PhotoPhotogrammetryButtonState extends State\u003cPhotoPhotogrammetryButton\u003e {\n  final ImagePicker _picker = ImagePicker();\n  StreamSubscription\u003cAppleSpatialCaptureProgress\u003e? _progressSubscription;\n\n  bool _isGenerating = false;\n  String _status = '';\n  double? _progress;\n\n  @override\n  void dispose() {\n    _progressSubscription?.cancel();\n    super.dispose();\n  }\n\n  Future\u003cvoid\u003e _generate() async {\n    final images = await _picker.pickMultiImage();\n    final imagePaths = images\n        .map((image) =\u003e image.path.trim())\n        .where((path) =\u003e path.isNotEmpty)\n        .toList();\n\n    if (imagePaths.length \u003c 3) {\n      setState(() =\u003e _status = 'Select at least 3 photos.');\n      return;\n    }\n\n    final operationId = 'photos_${DateTime.now().microsecondsSinceEpoch}';\n\n    await _progressSubscription?.cancel();\n    _progressSubscription = AppleSpatialCapture.platform.progressStream.listen(\n      (event) {\n        if (event.operationId != operationId) return;\n        setState(() {\n          _status = event.stepLabel ?? event.message;\n          _progress = event.progress;\n        });\n      },\n      onError: (error) {\n        setState(() =\u003e _status = error.toString());\n      },\n    );\n\n    setState(() {\n      _isGenerating = true;\n      _status = 'Preparing photos...';\n      _progress = null;\n    });\n\n    try {\n      final path = await AppleSpatialCapture.platform.startPhotogrammetryFromImages(\n        imagePaths,\n        operationId: operationId,\n        options: const ApplePhotogrammetryOptions(\n          outputFormat: ApplePhotogrammetryOutputFormat.obj,\n          textureQuality: ApplePhotogrammetryTextureQuality.low,\n          sampleOrdering: ApplePhotogrammetrySampleOrdering.unordered,\n          featureSensitivity: ApplePhotogrammetryFeatureSensitivity.normal,\n          useObjectMasking: false,\n        ),\n      );\n\n      if (path != null \u0026\u0026 path.isNotEmpty) {\n        await AppleSpatialCapture.platform.previewCapturedModel(path: path);\n      }\n    } on AppleSpatialCaptureError catch (error) {\n      setState(() =\u003e _status = error.message);\n    } finally {\n      await _progressSubscription?.cancel();\n      _progressSubscription = null;\n      if (mounted) {\n        setState(() =\u003e _isGenerating = false);\n      }\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Column(\n      crossAxisAlignment: CrossAxisAlignment.stretch,\n      children: [\n        ElevatedButton(\n          onPressed: _isGenerating ? null : _generate,\n          child: Text(_isGenerating ? 'Generating...' : 'Generate from photos'),\n        ),\n        if (_status.isNotEmpty) Text(_status),\n        if (_progress != null) LinearProgressIndicator(value: _progress),\n      ],\n    );\n  }\n}\n```\n\n## Configure photogrammetry options\n\n`ApplePhotogrammetryOptions` controls image-based reconstruction.\n\n```dart\nconst fastObjOptions = ApplePhotogrammetryOptions(\n  outputFormat: ApplePhotogrammetryOutputFormat.obj,\n  textureQuality: ApplePhotogrammetryTextureQuality.low,\n  sampleOrdering: ApplePhotogrammetrySampleOrdering.unordered,\n  featureSensitivity: ApplePhotogrammetryFeatureSensitivity.normal,\n  useObjectMasking: false,\n);\n\nconst higherQualityUsdzOptions = ApplePhotogrammetryOptions(\n  outputFormat: ApplePhotogrammetryOutputFormat.usdz,\n  textureQuality: ApplePhotogrammetryTextureQuality.high,\n  sampleOrdering: ApplePhotogrammetrySampleOrdering.sequential,\n  featureSensitivity: ApplePhotogrammetryFeatureSensitivity.high,\n  useObjectMasking: true,\n);\n```\n\nDefaults in Dart:\n\n| Option | Default |\n| --- | --- |\n| `detail` | `ApplePhotogrammetryDetail.reduced` |\n| `featureSensitivity` | `ApplePhotogrammetryFeatureSensitivity.normal` |\n| `sampleOrdering` | `ApplePhotogrammetrySampleOrdering.unordered` |\n| `textureQuality` | `ApplePhotogrammetryTextureQuality.low` |\n| `outputFormat` | `ApplePhotogrammetryOutputFormat.obj` |\n| `useObjectMasking` | `false` |\n\nCurrent iOS export uses reduced detail for on-device processing. Passing another `detail` value is accepted by Dart, but the native implementation falls back to reduced detail and emits an informational progress event.\n\n## Listen to progress events\n\n`progressStream` is most useful with `startPhotogrammetryFromImages()`. Events include a stage, message, optional normalized progress, optional ETA, and optional step metadata.\n\n```dart\nStreamSubscription\u003cAppleSpatialCaptureProgress\u003e? _subscription;\n\nvoid listenForPhotogrammetryProgress(String operationId) {\n  _subscription = AppleSpatialCapture.platform.progressStream.listen((event) {\n    if (event.operationId != operationId) return;\n\n    final percent = event.progress == null\n        ? null\n        : (event.progress! * 100).clamp(0, 100).round();\n\n    debugPrint(\n      [\n        event.stage.name,\n        if (event.stepIndex != null \u0026\u0026 event.stepTotal != null)\n          '${event.stepIndex}/${event.stepTotal}',\n        event.stepLabel ?? event.message,\n        if (percent != null) '$percent%',\n        if (event.etaSeconds != null) '${event.etaSeconds}s remaining',\n      ].join(' - '),\n    );\n  });\n}\n```\n\nAlways cancel the subscription in `dispose()` or after the operation completes:\n\n```dart\n@override\nvoid dispose() {\n  _subscription?.cancel();\n  super.dispose();\n}\n```\n\n## Start LiDAR mesh capture\n\n`startLiDARCapture()` opens a native LiDAR mesh scanner and returns a local model path.\n\n```dart\nFuture\u003cvoid\u003e startLiDARScan(BuildContext context) async {\n  try {\n    final isSupported = await AppleSpatialCapture.platform.isLiDARSupported();\n    if (!isSupported) {\n      _showMessage(context, 'LiDAR scanning is not supported on this device.');\n      return;\n    }\n\n    final path = await AppleSpatialCapture.platform.startLiDARCapture();\n    if (path == null || path.isEmpty) return;\n\n    await Navigator.of(context).push(\n      MaterialPageRoute\u003cvoid\u003e(\n        builder: (_) =\u003e MeshPreviewScreen(path: path),\n      ),\n    );\n  } on AppleSpatialCaptureError catch (error) {\n    _showMessage(context, error.message);\n  }\n}\n```\n\n## Start RoomPlan capture\n\n`startRoomPlanCapture()` opens Apple's RoomPlan capture UI and returns a local USDZ path.\n\n```dart\nFuture\u003cvoid\u003e startRoomPlanScan(BuildContext context) async {\n  try {\n    final isSupported = await AppleSpatialCapture.platform.isRoomPlanSupported();\n    if (!isSupported) {\n      _showMessage(context, 'RoomPlan is not supported on this device.');\n      return;\n    }\n\n    final path = await AppleSpatialCapture.platform.startRoomPlanCapture();\n    if (path == null || path.isEmpty) return;\n\n    await AppleSpatialCapture.platform.previewCapturedModel(\n      path: path,\n      fileType: AppleSpatialCaptureFileType.usdz,\n    );\n  } on AppleSpatialCaptureError catch (error) {\n    _showMessage(context, error.message);\n  }\n}\n```\n\n## Preview a local model\n\nUse `previewCapturedModel()` for a model file already on the device. If `fileType` is omitted, the plugin infers it from the path extension.\n\n```dart\nclass MeshPreviewScreen extends StatelessWidget {\n  const MeshPreviewScreen({super.key, required this.path});\n\n  final String path;\n\n  Future\u003cvoid\u003e _openPreview(BuildContext context) async {\n    try {\n      await AppleSpatialCapture.platform.previewCapturedModel(\n        path: path,\n        fileType: inferAppleSpatialCaptureFileType(path),\n      );\n    } on AppleSpatialCaptureError catch (error) {\n      _showMessage(context, error.message);\n    }\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text('Review model')),\n      body: Center(\n        child: ElevatedButton(\n          onPressed: () =\u003e _openPreview(context),\n          child: const Text('Open native preview'),\n        ),\n      ),\n    );\n  }\n}\n```\n\nUSDZ uses Quick Look. `obj`, `glb`, and `gltf` use the plugin's SceneKit-based preview.\n\n## Preview a remote model\n\nUse `previewRemoteModel()` for an `http` or `https` model URL. The plugin downloads the file into a temporary location, then opens the same native preview flow as local files.\n\n```dart\nFuture\u003cvoid\u003e previewRemoteAsset({\n  required BuildContext context,\n  required String url,\n  required String fileType,\n}) async {\n  final uri = Uri.parse(url);\n  final providedFileName = uri.pathSegments.isEmpty\n      ? 'model.$fileType'\n      : uri.pathSegments.last;\n  final fileName = providedFileName.contains('.')\n      ? providedFileName\n      : '$providedFileName.$fileType';\n\n  try {\n    await AppleSpatialCapture.platform.previewRemoteModel(\n      url: url,\n      fileName: fileName,\n      fileType: inferAppleSpatialCaptureFileType(fileName),\n    );\n  } on AppleSpatialCaptureError catch (error) {\n    _showMessage(context, error.message);\n  }\n}\n```\n\n## Handle errors\n\nThe wrapper converts platform failures to `AppleSpatialCaptureError`.\n\n```dart\ntry {\n  final path = await AppleSpatialCapture.platform.startLiDARCapture();\n  debugPrint('Captured model at $path');\n} on AppleSpatialCaptureError catch (error) {\n  debugPrint('Spatial capture failed: ${error.code} ${error.message}');\n  debugPrint('Details: ${error.details}');\n}\n```\n\nCommon error codes include:\n\n| Code | Meaning |\n| --- | --- |\n| `UNSUPPORTED` | iOS version or hardware does not support the requested capture API. |\n| `NO_VC` | The plugin could not find a root view controller to present native UI. |\n| `INVALID_PATH` | Dart received an empty local model path. |\n| `INVALID_URL` | Dart received an empty remote URL. |\n| `INSUFFICIENT_IMAGES` | Fewer than 3 photo paths were passed to photogrammetry. |\n| `FILE_NOT_FOUND` | Native preview could not find the local model file. |\n| `DOWNLOAD_FAILED` | Remote preview could not download the model file. |\n\n## Use a fake platform in widget tests\n\n`AppleSpatialCapture.setPlatform()` lets tests replace the real method-channel implementation.\n\n```dart\nclass FakeAppleSpatialCapturePlatform implements AppleSpatialCapturePlatform {\n  @override\n  Stream\u003cAppleSpatialCaptureProgress\u003e get progressStream =\u003e const Stream.empty();\n\n  @override\n  Future\u003cbool\u003e isPhotogrammetrySupported() async =\u003e true;\n\n  @override\n  Future\u003cbool\u003e isLiDARSupported() async =\u003e true;\n\n  @override\n  Future\u003cbool\u003e isRoomPlanSupported() async =\u003e false;\n\n  @override\n  Future\u003cAppleSpatialCaptureSupport\u003e supportStatus() async {\n    return const AppleSpatialCaptureSupport(\n      photogrammetry: true,\n      lidar: true,\n      roomPlan: false,\n    );\n  }\n\n  @override\n  Future\u003cString?\u003e startPhotogrammetryCapture() async {\n    return '/tmp/object.usdz';\n  }\n\n  @override\n  Future\u003cString?\u003e startPhotogrammetryFromImages(\n    List\u003cString\u003e imagePaths, {\n    String? operationId,\n    ApplePhotogrammetryOptions options = const ApplePhotogrammetryOptions(),\n  }) async {\n    return '/tmp/photos.obj';\n  }\n\n  @override\n  Future\u003cString?\u003e startLiDARCapture() async =\u003e '/tmp/lidar.usdz';\n\n  @override\n  Future\u003cString?\u003e startRoomPlanCapture() async =\u003e null;\n\n  @override\n  Future\u003cvoid\u003e previewCapturedModel({\n    required String path,\n    AppleSpatialCaptureFileType? fileType,\n  }) async {}\n\n  @override\n  Future\u003cvoid\u003e previewRemoteModel({\n    required String url,\n    String? fileName,\n    AppleSpatialCaptureFileType? fileType,\n  }) async {}\n}\n\nvoid main() {\n  AppleSpatialCapture.setPlatform(FakeAppleSpatialCapturePlatform());\n}\n```\n\n## Practical notes\n\n- Test capture flows on a physical iPhone or iPad. Simulators do not provide LiDAR, Object Capture, or RoomPlan hardware support.\n- On macOS, use `startPhotogrammetryFromImages()` for reconstruction from existing photos.\n- `startPhotogrammetryCapture()`, `startLiDARCapture()`, and `startRoomPlanCapture()` present native full-screen UI.\n- `startPhotogrammetryFromImages()` can take several minutes depending on image count, texture quality, and output format.\n- Returned paths point to temporary app files. Move or upload the model if your app needs to keep it.\n- Remote preview supports only `http` and `https` URLs.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fa7alabs%2Fapple-spatial-capture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fa7alabs%2Fapple-spatial-capture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fa7alabs%2Fapple-spatial-capture/lists"}