diff --git a/.gitignore b/.gitignore index 7d26f86..f40b55c 100644 --- a/.gitignore +++ b/.gitignore @@ -103,6 +103,11 @@ pip-delete-this-directory.txt **/android/ !**/ios/.gitkeep !**/android/.gitkeep +# Local Expo modules ship their own native source — keep it tracked +!mobile/modules/**/ios/ +!mobile/modules/**/ios/** +!mobile/modules/**/android/ +!mobile/modules/**/android/** # Husky output (keep config, ignore cache) .husky/_/ diff --git a/docs/superpowers/plans/2026-06-03-lidar-native-module-scaffold.md b/docs/superpowers/plans/2026-06-03-lidar-native-module-scaffold.md new file mode 100644 index 0000000..aadc1bc --- /dev/null +++ b/docs/superpowers/plans/2026-06-03-lidar-native-module-scaffold.md @@ -0,0 +1,410 @@ +# LiDAR Native Module Scaffold — Implementation Plan (EURODEV-74, Fase 1) + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Adicionar um módulo Expo nativo local (`lidar-scanner`) que renderiza uma `ARView` (RealityKit/ARKit) dentro de uma tela React Native num iPhone com LiDAR, e expor uma checagem `isLidarSupported()` com fallback — provando a ponte nativa antes da captura (Fase 2). + +**Architecture:** O app continua RN/Expo managed (SDK 54, newArch). Saímos do managed-puro para um **development build** (`expo-dev-client`) e criamos um **Expo Module local** (`modules/lidar-scanner`) com uma `ExpoView` que embute uma `ARView`. A sessão ARKit roda com `sceneReconstruction = .mesh` quando suportado, e mostra a malha (`showSceneUnderstanding`) para confirmação visual. `startScan`/`stopScan`/`onScanComplete` ficam como stubs (preenchidos na Fase 2). + +**Tech Stack:** Expo SDK 54, React Native 0.81.5, expo-modules-core (Module DSL), Swift, ARKit, RealityKit, expo-dev-client, expo-build-properties. + +**Escopo:** Só a EURODEV-74. Captura de malha = EURODEV-75; medidas = EURODEV-76. + +**Pré-requisitos de hardware/conta:** iPhone com LiDAR (12 Pro+) ligado por cabo, Xcode instalado, conta Apple Developer para assinar o dev build no device (team `tcord`). + +--- + +### Task 1: Branch + dependências do dev build + +**Files:** +- Modify: `mobile/app.json` +- Modify: `mobile/package.json` (via instalador) + +- [ ] **Step 1: Criar a branch a partir de `main`** + +Run (na raiz do repo): +```bash +cd /Users/macbookaireucportugal/Documents/Notas/Cods/PondiFarmApp +git checkout main && git pull --ff-only +git checkout -b EURODEV-74/lidar-native-module-scaffold +``` + +- [ ] **Step 2: Instalar expo-dev-client e expo-build-properties** + +Run (dentro de `mobile/`): +```bash +cd mobile +npx expo install expo-dev-client expo-build-properties +``` +Expected: ambos adicionados em `package.json` com versões compatíveis com SDK 54. + +- [ ] **Step 3: Configurar deployment target iOS via plugin** + +Em `mobile/app.json`, dentro do array `"plugins"`, adicionar como **primeiro** item: +```json +[ + "expo-build-properties", + { + "ios": { "deploymentTarget": "15.1" } + } +], +``` +(Mantém os plugins `expo-camera` e `expo-image-picker` existentes logo abaixo.) + +- [ ] **Step 4: Confirmar permissão de câmera (ARKit usa a câmera)** + +Verificar que `mobile/app.json` → `expo.ios.infoPlist.NSCameraUsageDescription` já existe (existe: "A câmera é usada para escanear o animal..."). Nenhuma chave extra é necessária para ARKit além da câmera. Nada a alterar. + +- [ ] **Step 5: Commit** + +```bash +cd /Users/macbookaireucportugal/Documents/Notas/Cods/PondiFarmApp +git add mobile/app.json mobile/package.json mobile/package-lock.json +git commit -m "build(mobile): add dev client and build-properties for native LiDAR module" +``` + +--- + +### Task 2: Scaffold do módulo local `lidar-scanner` + +**Files:** +- Create: `mobile/modules/lidar-scanner/**` (gerado) + +- [ ] **Step 1: Gerar o módulo local** + +Run (dentro de `mobile/`): +```bash +cd mobile +npx create-expo-module@latest --local lidar-scanner +``` +Quando perguntar, responder: +- Native module name: `LidarScanner` +- JS/TS name: `LidarScanner` +- description / author: livres (autor: Talys Cordeiro) + +Expected: cria `mobile/modules/lidar-scanner/` com `index.ts`, `expo-module.config.json`, `src/`, `ios/`, `android/`. + +- [ ] **Step 2: Confirmar autolink do módulo** + +Run: +```bash +cat modules/lidar-scanner/expo-module.config.json +``` +Expected: contém `"name": "LidarScanner"` e plataformas `ios`/`android`. (Módulos em `modules/` são autolinkados pelo Expo.) + +- [ ] **Step 3: Commit do scaffold** + +```bash +cd /Users/macbookaireucportugal/Documents/Notas/Cods/PondiFarmApp +git add mobile/modules/lidar-scanner +git commit -m "feat(mobile): scaffold local lidar-scanner expo module" +``` + +--- + +### Task 3: Módulo iOS — ARView + checagem LiDAR (Swift) + +**Files:** +- Modify: `mobile/modules/lidar-scanner/ios/LidarScannerModule.swift` +- Modify: `mobile/modules/lidar-scanner/ios/LidarScannerView.swift` + +- [ ] **Step 1: Implementar o módulo (Module DSL)** + +Substituir o conteúdo de `mobile/modules/lidar-scanner/ios/LidarScannerModule.swift` por: +```swift +import ExpoModulesCore +import ARKit + +public class LidarScannerModule: Module { + public func definition() -> ModuleDefinition { + Name("LidarScanner") + + // Checagem de suporte com fallback — chamável de JS sem montar a view. + Function("isLidarSupported") { () -> Bool in + return ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) + } + + View(LidarScannerView.self) { + Events("onScanComplete") + + AsyncFunction("startScan") { (view: LidarScannerView) in + view.startScan() + } + + AsyncFunction("stopScan") { (view: LidarScannerView) in + view.stopScan() + } + } + } +} +``` + +- [ ] **Step 2: Implementar a ARView nativa** + +Substituir o conteúdo de `mobile/modules/lidar-scanner/ios/LidarScannerView.swift` por: +```swift +import ExpoModulesCore +import ARKit +import RealityKit + +class LidarScannerView: ExpoView { + private let arView = ARView(frame: .zero) + private let onScanComplete = EventDispatcher() + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + clipsToBounds = true + addSubview(arView) + startSession() + } + + override func layoutSubviews() { + super.layoutSubviews() + arView.frame = bounds + } + + private func startSession() { + let config = ARWorldTrackingConfiguration() + if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { + config.sceneReconstruction = .mesh + // Mostra a malha do LiDAR — confirmação visual da Fase 1. + arView.debugOptions.insert(.showSceneUnderstanding) + } + config.environmentTexturing = .none + arView.session.run(config, options: [.resetTracking, .removeExistingAnchors]) + } + + // Stubs — preenchidos na Fase 2 (EURODEV-75). + func startScan() { /* EURODEV-75 */ } + func stopScan() { /* EURODEV-75 */ } +} +``` + +- [ ] **Step 3: (Sem build ainda) — só salvar.** O build acontece na Task 6 (precisa do device). + +- [ ] **Step 4: Commit** + +```bash +git add mobile/modules/lidar-scanner/ios +git commit -m "feat(mobile): native ARKit ARView + LiDAR support check in lidar-scanner" +``` + +--- + +### Task 4: Superfície TS do módulo + +**Files:** +- Modify: `mobile/modules/lidar-scanner/index.ts` +- Create: `mobile/modules/lidar-scanner/src/LidarScannerView.tsx` + +- [ ] **Step 1: Definir tipos + API em `index.ts`** + +Substituir o conteúdo de `mobile/modules/lidar-scanner/index.ts` por: +```ts +import { requireNativeModule } from 'expo-modules-core'; + +import LidarScannerView from './src/LidarScannerView'; + +export type Measurements = { + body_length_cm: number; + withers_height_cm: number; + thoracic_depth_cm: number; + rump_width_cm: number; + chest_girth_cm: number; +}; + +export type ScanCompleteEvent = { + measurements: Measurements; + meshUri: string; + thumbUri?: string; +}; + +const LidarScannerModule = requireNativeModule('LidarScanner'); + +/** True só em iPhones com LiDAR (12 Pro+). False em simulador/devices sem LiDAR. */ +export function isLidarSupported(): boolean { + return LidarScannerModule.isLidarSupported(); +} + +export { LidarScannerView }; +``` + +- [ ] **Step 2: Definir a view RN em `src/LidarScannerView.tsx`** + +Substituir o conteúdo de `mobile/modules/lidar-scanner/src/LidarScannerView.tsx` por: +```tsx +import { requireNativeView } from 'expo'; +import * as React from 'react'; +import type { ViewProps } from 'react-native'; + +import type { ScanCompleteEvent } from '../index'; + +export type LidarScannerViewProps = ViewProps & { + onScanComplete?: (event: { nativeEvent: ScanCompleteEvent }) => void; +}; + +const NativeView: React.ComponentType = + requireNativeView('LidarScanner'); + +export default function LidarScannerView(props: LidarScannerViewProps) { + return ; +} +``` + +- [ ] **Step 3: Typecheck** + +Run (dentro de `mobile/`): +```bash +cd mobile +npx tsc --noEmit +``` +Expected: PASS (sem erros). Se acusar tipo do módulo nativo, garantir que os imports acima batem com os arquivos gerados. + +- [ ] **Step 4: Commit** + +```bash +cd /Users/macbookaireucportugal/Documents/Notas/Cods/PondiFarmApp +git add mobile/modules/lidar-scanner/index.ts mobile/modules/lidar-scanner/src/LidarScannerView.tsx +git commit -m "feat(mobile): typescript surface for lidar-scanner (isLidarSupported, view)" +``` + +--- + +### Task 5: Tela de teste temporária + rota + +**Files:** +- Create: `mobile/src/screens/LidarTestScreen.tsx` +- Modify: `mobile/src/navigation/types.ts` +- Modify: `mobile/src/navigation/AppNavigator.tsx` +- Modify: `mobile/src/screens/HomeScreen.tsx` (botão dev temporário) + +> Toda esta task é marcada com `// TEMP EURODEV-74` e será removida na Fase 4 (EURODEV-77), quando o scanner entra no fluxo real do ScanScreen. + +- [ ] **Step 1: Criar a tela de teste** + +Create `mobile/src/screens/LidarTestScreen.tsx`: +```tsx +// TEMP EURODEV-74 — tela de verificação do módulo nativo. Remover na Fase 4. +import React from 'react'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { LidarScannerView, isLidarSupported } from '../../modules/lidar-scanner'; +import { ios } from '../lib/theme'; + +export default function LidarTestScreen() { + const insets = useSafeAreaInsets(); + const supported = isLidarSupported(); + + return ( + + + + + LiDAR suportado: {supported ? '✅ sim' : '❌ não'} + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#000' }, + badge: { + position: 'absolute', alignSelf: 'center', + backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 14, paddingVertical: 8, + borderRadius: 999, + }, + badgeText: { color: '#fff', fontSize: 14 }, +}); +``` + +- [ ] **Step 2: Registrar a rota nos tipos** + +Abrir `mobile/src/navigation/types.ts` e adicionar a entrada `LidarTest: undefined;` ao type `RootStackParamList` (seguir o formato das rotas existentes, ex. `Result: { record: ScanRecord };`). + +- [ ] **Step 3: Registrar a tela no navigator** + +Abrir `mobile/src/navigation/AppNavigator.tsx`, importar `LidarTestScreen` e adicionar `` junto às outras `Stack.Screen` (mesmo padrão das existentes). + +- [ ] **Step 4: Botão dev temporário na Home** + +Em `mobile/src/screens/HomeScreen.tsx`, adicionar um botão temporário que navega para a tela (usar o hook de navegação já presente no arquivo): +```tsx +{/* TEMP EURODEV-74 — remover na Fase 4 */} +{__DEV__ && ( + nav.navigate('LidarTest')} style={{ padding: 16 }}> + ▶ Testar scanner LiDAR (dev) + +)} +``` +(Ajustar o nome da variável de navegação ao que já existe no HomeScreen; importar `ios` de `../lib/theme` se ainda não importado.) + +- [ ] **Step 5: Typecheck** + +Run: +```bash +cd mobile && npx tsc --noEmit +``` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +cd /Users/macbookaireucportugal/Documents/Notas/Cods/PondiFarmApp +git add mobile/src +git commit -m "feat(mobile): temp LiDAR test screen and route for EURODEV-74 verification" +``` + +--- + +### Task 6: Dev build no device + verificação (acceptance) + +**Files:** nenhum (build + verificação manual). + +- [ ] **Step 1: Gerar e instalar o dev build no iPhone LiDAR** + +Conectar o iPhone por cabo. Run (dentro de `mobile/`): +```bash +cd mobile +npx expo run:ios --device +``` +- Selecionar o iPhone físico quando perguntado. +- Na primeira vez, abrir `ios/PondiFarm.xcworkspace` no Xcode, em **Signing & Capabilities** escolher o Team (`tcord`) para assinar, e rodar de novo se necessário. + +Expected: app dev client instala e abre no device. (Alternativa EAS: `eas build --profile development --platform ios` e instalar o build.) + +- [ ] **Step 2: Verificação on-device (critérios da EURODEV-74)** + +Abrir o app → tocar no botão dev "▶ Testar scanner LiDAR (dev)" na Home. Confirmar: +- [ ] A **câmera AR aparece** preenchendo a tela (a `ARView` está embutida no RN). ✅ "ARView visível numa tela RN" +- [ ] O badge mostra **"LiDAR suportado: ✅ sim"** no iPhone Pro. ✅ "supportsSceneReconstruction checado" +- [ ] Movendo o aparelho, aparece a **malha do LiDAR sobreposta** (debug `showSceneUnderstanding`), confirmando que a reconstrução de cena está ativa. +- [ ] O build compilou sem erro. ✅ "build prebuild verde" + +- [ ] **Step 3: Verificar o fallback (sem LiDAR)** + +Rodar no **simulador** (`npx expo run:ios`) e confirmar que o badge mostra **"LiDAR suportado: ❌ não"** e o app **não crasha** (a sessão simplesmente não ativa a malha). + +- [ ] **Step 4: Push e atualizar o PR/Jira** + +```bash +git push -u origin EURODEV-74/lidar-native-module-scaffold +``` +Abrir PR contra `main` com título `EURODEV-74 feat(mobile): native LiDAR module scaffold + ARView bridge` (o prefixo `EURODEV-74` liga ao Jira). Mover a subtask EURODEV-74 para "Em curso/Done" conforme o estado. + +--- + +## Self-Review + +**Spec coverage (vs EURODEV-74):** +- "Módulo nativo + ponte ARView no RN" → Tasks 2–5. ✅ +- "expo prebuild (CNG) + create-expo-module --local" → Task 1 (dev-client) + Task 2. ✅ +- "supportsSceneReconstruction(.mesh) checado com fallback" → Task 3 Step 1 (`isLidarSupported`) + Task 6 Step 3. ✅ +- "ARView nativo visível numa tela RN" → Task 5 + Task 6 Step 2. ✅ +- "build prebuild verde" → Task 6 Step 1–2. ✅ + +**Type consistency:** `LidarScanner` (Name nativo) == `requireNativeModule('LidarScanner')` == `requireNativeView('LidarScanner')`. `Measurements`/`ScanCompleteEvent` definidos em `index.ts` e reusados em `LidarScannerView.tsx`. `isLidarSupported`/`startScan`/`stopScan` consistentes entre Swift e TS. ✅ + +**Placeholders:** `startScan`/`stopScan` são stubs intencionais e rotulados (Fase 2), não placeholders de plano. Os passos que tocam `types.ts`/`AppNavigator.tsx`/`HomeScreen.tsx` nomeiam o símbolo exato a adicionar e mandam seguir o padrão existente do arquivo (conteúdo não reproduzido porque varia; é leitura de 1 arquivo no momento). ✅ + +**Honestidade de teste:** a Fase 1 não tem lógica pura para TDD; o gate automatizado é `tsc --noEmit` e a aceitação é a verificação on-device. TDD com testes-primeiro entra na Fase 3 (EURODEV-76, medições). diff --git a/mobile/app.json b/mobile/app.json index 7f52bbf..c867992 100644 --- a/mobile/app.json +++ b/mobile/app.json @@ -17,7 +17,8 @@ "bundleIdentifier": "pt.eurounionconsult.pondifarm", "infoPlist": { "NSCameraUsageDescription": "A câmera é usada para escanear o animal e estimar seu peso.", - "NSPhotoLibraryUsageDescription": "Acesso à galeria para selecionar fotos de animais." + "NSPhotoLibraryUsageDescription": "Acesso à galeria para selecionar fotos de animais.", + "NSWorldSensingUsageDescription": "O scanner LiDAR usa a reconstrução da cena para medir as dimensões do animal." } }, "android": { @@ -37,6 +38,14 @@ "favicon": "./assets/favicon.png" }, "plugins": [ + [ + "expo-build-properties", + { + "ios": { + "deploymentTarget": "15.1" + } + } + ], [ "expo-camera", { @@ -48,7 +57,8 @@ { "photosPermission": "Acesso à galeria para selecionar fotos de animais." } - ] + ], + "expo-font" ], "extra": { "eas": { diff --git a/mobile/modules/lidar-scanner/LICENSE b/mobile/modules/lidar-scanner/LICENSE new file mode 100644 index 0000000..30b20e3 --- /dev/null +++ b/mobile/modules/lidar-scanner/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-present 650 Industries, Inc. (aka Expo) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/mobile/modules/lidar-scanner/android/build.gradle b/mobile/modules/lidar-scanner/android/build.gradle new file mode 100644 index 0000000..40ad1b4 --- /dev/null +++ b/mobile/modules/lidar-scanner/android/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'com.android.library' + id 'expo-module-gradle-plugin' +} + +group = 'expo.modules.lidarscanner' +version = '0.1.0' + +android { + namespace "expo.modules.lidarscanner" + defaultConfig { + versionCode 1 + versionName "0.1.0" + } + lintOptions { + abortOnError false + } +} diff --git a/mobile/modules/lidar-scanner/android/src/main/AndroidManifest.xml b/mobile/modules/lidar-scanner/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bdae66c --- /dev/null +++ b/mobile/modules/lidar-scanner/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/mobile/modules/lidar-scanner/android/src/main/java/expo/modules/lidarscanner/LidarScannerModule.kt b/mobile/modules/lidar-scanner/android/src/main/java/expo/modules/lidarscanner/LidarScannerModule.kt new file mode 100644 index 0000000..8c87d54 --- /dev/null +++ b/mobile/modules/lidar-scanner/android/src/main/java/expo/modules/lidarscanner/LidarScannerModule.kt @@ -0,0 +1,10 @@ +package expo.modules.lidarscanner + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class LidarScannerModule : Module() { + override fun definition() = ModuleDefinition { + Name("LidarScanner") + } +} diff --git a/mobile/modules/lidar-scanner/expo-module.config.json b/mobile/modules/lidar-scanner/expo-module.config.json new file mode 100644 index 0000000..76f20cf --- /dev/null +++ b/mobile/modules/lidar-scanner/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["apple", "android", "web"], + "apple": { + "modules": ["LidarScannerModule"] + }, + "android": { + "modules": ["expo.modules.lidarscanner.LidarScannerModule"] + } +} diff --git a/mobile/modules/lidar-scanner/index.ts b/mobile/modules/lidar-scanner/index.ts new file mode 100644 index 0000000..0ff802a --- /dev/null +++ b/mobile/modules/lidar-scanner/index.ts @@ -0,0 +1,10 @@ +import LidarScannerModule from './src/LidarScannerModule'; +import LidarScannerView from './src/LidarScannerView'; + +export * from './src/LidarScanner.types'; +export { LidarScannerView }; + +/** True only on iPhones with LiDAR (12 Pro+). False on simulator / non-LiDAR devices. */ +export function isLidarSupported(): boolean { + return LidarScannerModule.isLidarSupported(); +} diff --git a/mobile/modules/lidar-scanner/ios/LidarScanner.podspec b/mobile/modules/lidar-scanner/ios/LidarScanner.podspec new file mode 100644 index 0000000..cf3598b --- /dev/null +++ b/mobile/modules/lidar-scanner/ios/LidarScanner.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'LidarScanner' + s.version = '1.0.0' + s.summary = 'LiDAR scanner module for PondiFarm cattle measurement' + s.description = 'On-device ARKit LiDAR capture and morphometric measurement for PondiFarm.' + s.author = 'Talys Cordeiro' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { + :ios => '15.1', + :tvos => '16.4' + } + s.source = { git: 'https://github.com/EuroUnionConsult/PondiFarmApp.git', tag: s.version.to_s } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/mobile/modules/lidar-scanner/ios/LidarScannerModule.swift b/mobile/modules/lidar-scanner/ios/LidarScannerModule.swift new file mode 100644 index 0000000..2bcc822 --- /dev/null +++ b/mobile/modules/lidar-scanner/ios/LidarScannerModule.swift @@ -0,0 +1,25 @@ +import ExpoModulesCore +import ARKit + +public class LidarScannerModule: Module { + public func definition() -> ModuleDefinition { + Name("LidarScanner") + + // LiDAR support check with fallback — callable from JS without mounting the view. + Function("isLidarSupported") { () -> Bool in + return ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) + } + + View(LidarScannerView.self) { + Events("onScanComplete") + + AsyncFunction("startScan") { (view: LidarScannerView) in + view.startScan() + } + + AsyncFunction("stopScan") { (view: LidarScannerView) in + view.stopScan() + } + } + } +} diff --git a/mobile/modules/lidar-scanner/ios/LidarScannerView.swift b/mobile/modules/lidar-scanner/ios/LidarScannerView.swift new file mode 100644 index 0000000..170ef26 --- /dev/null +++ b/mobile/modules/lidar-scanner/ios/LidarScannerView.swift @@ -0,0 +1,40 @@ +import ExpoModulesCore +import ARKit +import RealityKit + +class LidarScannerView: ExpoView { + private let arView = ARView(frame: .zero) + private let onScanComplete = EventDispatcher() + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + clipsToBounds = true + addSubview(arView) + startSession() + } + + override func layoutSubviews() { + super.layoutSubviews() + arView.frame = bounds + } + + deinit { + arView.session.pause() + } + + private func startSession() { + guard ARWorldTrackingConfiguration.isSupported else { return } + let config = ARWorldTrackingConfiguration() + if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { + config.sceneReconstruction = .mesh + // Show the LiDAR mesh — visual confirmation for Phase 1. + arView.debugOptions.insert(.showSceneUnderstanding) + } + config.environmentTexturing = .none + arView.session.run(config, options: [.resetTracking, .removeExistingAnchors]) + } + + // Stubs — filled in Phase 2 (EURODEV-75). + func startScan() { /* EURODEV-75 */ } + func stopScan() { /* EURODEV-75 */ } +} diff --git a/mobile/modules/lidar-scanner/src/LidarScanner.types.ts b/mobile/modules/lidar-scanner/src/LidarScanner.types.ts new file mode 100644 index 0000000..38e6089 --- /dev/null +++ b/mobile/modules/lidar-scanner/src/LidarScanner.types.ts @@ -0,0 +1,13 @@ +export type Measurements = { + body_length_cm: number; + withers_height_cm: number; + thoracic_depth_cm: number; + rump_width_cm: number; + chest_girth_cm: number; +}; + +export type ScanCompleteEvent = { + measurements: Measurements; + meshUri: string; + thumbUri?: string; +}; diff --git a/mobile/modules/lidar-scanner/src/LidarScannerModule.ts b/mobile/modules/lidar-scanner/src/LidarScannerModule.ts new file mode 100644 index 0000000..a1283b2 --- /dev/null +++ b/mobile/modules/lidar-scanner/src/LidarScannerModule.ts @@ -0,0 +1,7 @@ +import { NativeModule, requireNativeModule } from 'expo'; + +declare class LidarScannerModule extends NativeModule<{}> { + isLidarSupported(): boolean; +} + +export default requireNativeModule('LidarScanner'); diff --git a/mobile/modules/lidar-scanner/src/LidarScannerModule.web.ts b/mobile/modules/lidar-scanner/src/LidarScannerModule.web.ts new file mode 100644 index 0000000..38cea6d --- /dev/null +++ b/mobile/modules/lidar-scanner/src/LidarScannerModule.web.ts @@ -0,0 +1,5 @@ +import { registerWebModule, NativeModule } from 'expo'; + +class LidarScannerModule extends NativeModule<{}> {} + +export default registerWebModule(LidarScannerModule, 'LidarScanner'); diff --git a/mobile/modules/lidar-scanner/src/LidarScannerView.tsx b/mobile/modules/lidar-scanner/src/LidarScannerView.tsx new file mode 100644 index 0000000..888fa0b --- /dev/null +++ b/mobile/modules/lidar-scanner/src/LidarScannerView.tsx @@ -0,0 +1,16 @@ +import { requireNativeView } from 'expo'; +import * as React from 'react'; +import type { ViewProps } from 'react-native'; + +import type { ScanCompleteEvent } from './LidarScanner.types'; + +export type LidarScannerViewProps = ViewProps & { + onScanComplete?: (event: { nativeEvent: ScanCompleteEvent }) => void; +}; + +const NativeView: React.ComponentType = + requireNativeView('LidarScanner'); + +export default function LidarScannerView(props: LidarScannerViewProps) { + return ; +} diff --git a/mobile/modules/lidar-scanner/src/LidarScannerView.web.tsx b/mobile/modules/lidar-scanner/src/LidarScannerView.web.tsx new file mode 100644 index 0000000..de04ae2 --- /dev/null +++ b/mobile/modules/lidar-scanner/src/LidarScannerView.web.tsx @@ -0,0 +1,6 @@ +// Web has no native LiDAR view — render nothing. +import type { LidarScannerViewProps } from './LidarScannerView'; + +export default function LidarScannerView(_props: LidarScannerViewProps) { + return null; +} diff --git a/mobile/package-lock.json b/mobile/package-lock.json index 8aa84d9..aef73cc 100644 --- a/mobile/package-lock.json +++ b/mobile/package-lock.json @@ -14,8 +14,11 @@ "@react-navigation/native": "^7.2.4", "@react-navigation/native-stack": "^7.16.0", "expo": "~54.0.35", + "expo-build-properties": "~1.0.10", "expo-camera": "~17.0.10", + "expo-dev-client": "~6.0.21", "expo-file-system": "~19.0.22", + "expo-font": "~14.0.12", "expo-haptics": "~15.0.8", "expo-image-picker": "~17.0.11", "expo-status-bar": "~3.0.9", @@ -3392,6 +3395,22 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/anser": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", @@ -4763,6 +4782,19 @@ } } }, + "node_modules/expo-build-properties": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/expo-build-properties/-/expo-build-properties-1.0.10.tgz", + "integrity": "sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "semver": "^7.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-camera": { "version": "17.0.10", "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-17.0.10.tgz", @@ -4783,6 +4815,57 @@ } } }, + "node_modules/expo-dev-client": { + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.21.tgz", + "integrity": "sha512-SWI6HD0pa4eJujkYFkvvpezUE1zmJXGLu+34azpu7+QJgO+FLutDYDj8BSTdeH/NYDEClDFjCGqVMcWETvmsCQ==", + "license": "MIT", + "dependencies": { + "expo-dev-launcher": "6.0.21", + "expo-dev-menu": "7.0.19", + "expo-dev-menu-interface": "2.0.0", + "expo-manifests": "~1.0.11", + "expo-updates-interface": "~2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-launcher": { + "version": "6.0.21", + "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.21.tgz", + "integrity": "sha512-QZ9gcKMZbp6EsIhzS0QoGB8Cf4xeVJhjbNgWUwcoBIk8gshoFz8CkCQOnX+HNv2sSY3rdCaNpx3Xo0Rflyq7rA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.11.0", + "expo-dev-menu": "7.0.19", + "expo-manifests": "~1.0.11" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.19.tgz", + "integrity": "sha512-ju5MZiBCPhUKKvHy0ElZdnlhq01mkEEiR8jfrgQVvW26aWjzjLiOhppNAyXtvGbhk7WxJim3wYMiqFFrjGdfKA==", + "license": "MIT", + "dependencies": { + "expo-dev-menu-interface": "2.0.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-dev-menu-interface": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz", + "integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-eas-client": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/expo-eas-client/-/expo-eas-client-1.0.8.tgz", @@ -4800,11 +4883,10 @@ } }, "node_modules/expo-font": { - "version": "56.0.5", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-56.0.5.tgz", - "integrity": "sha512-WLoDu9hlEgPRKXJRR01HFLJ6Z2tFcORX/WFPRYBndmYc5kjQrFGH/j4BRaF3aBRPyYEAUXiUJybNLXkKCwEXQw==", + "version": "14.0.12", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz", + "integrity": "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -5401,20 +5483,6 @@ "react-native": "*" } }, - "node_modules/expo/node_modules/expo-font": { - "version": "14.0.12", - "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.12.tgz", - "integrity": "sha512-QQzunE2Mxk45AsCWm3tK7OpVljbtVnKD58q4/qliev+cbye1IOduUnRIdD+P7DyButw17G9MTX795kgaQiz5hQ==", - "license": "MIT", - "dependencies": { - "fontfaceobserver": "^2.1.0" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/expo-keep-awake": { "version": "15.0.8", "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", @@ -5527,6 +5595,22 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -6466,6 +6550,12 @@ "node": ">=6" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", diff --git a/mobile/package.json b/mobile/package.json index 8150c9b..ec2a257 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -4,8 +4,8 @@ "main": "index.ts", "scripts": { "start": "expo start", - "android": "expo start --android", - "ios": "expo start --ios", + "android": "expo run:android", + "ios": "expo run:ios", "web": "expo start --web" }, "dependencies": { @@ -15,8 +15,11 @@ "@react-navigation/native": "^7.2.4", "@react-navigation/native-stack": "^7.16.0", "expo": "~54.0.35", + "expo-build-properties": "~1.0.10", "expo-camera": "~17.0.10", + "expo-dev-client": "~6.0.21", "expo-file-system": "~19.0.22", + "expo-font": "~14.0.12", "expo-haptics": "~15.0.8", "expo-image-picker": "~17.0.11", "expo-status-bar": "~3.0.9", diff --git a/mobile/src/navigation/AppNavigator.tsx b/mobile/src/navigation/AppNavigator.tsx index b093be2..0a22e84 100644 --- a/mobile/src/navigation/AppNavigator.tsx +++ b/mobile/src/navigation/AppNavigator.tsx @@ -13,6 +13,7 @@ import AnalyticsScreen from '../screens/AnalyticsScreen'; import SettingsScreen from '../screens/SettingsScreen'; import ScanScreen from '../screens/ScanScreen'; import ResultScreen from '../screens/ResultScreen'; +import LidarTestScreen from '../screens/LidarTestScreen'; // TEMP EURODEV-74 — remove in Phase 4 import type { RootStackParamList, TabParamList } from './types'; @@ -74,6 +75,12 @@ export default function AppNavigator() { component={ResultScreen} options={{ animation: 'slide_from_right' }} /> + {/* TEMP EURODEV-74 — remove in Phase 4 */} + ); diff --git a/mobile/src/navigation/types.ts b/mobile/src/navigation/types.ts index bf40288..32a3920 100644 --- a/mobile/src/navigation/types.ts +++ b/mobile/src/navigation/types.ts @@ -4,6 +4,7 @@ export type RootStackParamList = { Main: undefined; Scan: undefined; Result: { record: ScanRecord }; + LidarTest: undefined; // TEMP EURODEV-74 — remove in Phase 4 }; export type TabParamList = { diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index 3f38d90..c76a340 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -173,6 +173,16 @@ export default function HomeScreen() { New scan + + {/* TEMP EURODEV-74 — remove in Phase 4 */} + {__DEV__ && ( + nav.navigate('LidarTest')} + style={{ paddingVertical: 16, alignItems: 'center' }} + > + ▶ Testar scanner LiDAR (dev) + + )} ); } diff --git a/mobile/src/screens/LidarTestScreen.tsx b/mobile/src/screens/LidarTestScreen.tsx new file mode 100644 index 0000000..ba21969 --- /dev/null +++ b/mobile/src/screens/LidarTestScreen.tsx @@ -0,0 +1,31 @@ +// TEMP EURODEV-74 — native module verification screen. Remove in Phase 4 (EURODEV-77). +import React from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { LidarScannerView, isLidarSupported } from '../../modules/lidar-scanner'; + +export default function LidarTestScreen() { + const insets = useSafeAreaInsets(); + const supported = isLidarSupported(); + + return ( + + + + + LiDAR suportado: {supported ? '✅ sim' : '❌ não'} + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#000' }, + badge: { + position: 'absolute', alignSelf: 'center', + backgroundColor: 'rgba(0,0,0,0.6)', paddingHorizontal: 14, paddingVertical: 8, + borderRadius: 999, + }, + badgeText: { color: '#fff', fontSize: 14 }, +});