diff --git a/.gitignore b/.gitignore index d914c32..da9569c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # typescript diff --git a/App.tsx b/App.tsx index 0329d0c..dc4911e 100644 --- a/App.tsx +++ b/App.tsx @@ -1,20 +1,13 @@ import { StatusBar } from 'expo-status-bar'; -import { StyleSheet, Text, View } from 'react-native'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; + +import { AppNavigator } from './src/navigation/AppNavigator'; export default function App() { return ( - - Open up App.tsx to start working on your app! - - + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#fff', - alignItems: 'center', - justifyContent: 'center', - }, -}); diff --git a/README.md b/README.md new file mode 100644 index 0000000..0573ddf --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# RoomScan AI + +RoomScan AI ist ein Starterprojekt fuer eine skalierbare iOS App zur Erstellung von 3D Raumscans. Die App soll Nutzer Schritt fuer Schritt durch einen Raumscan fuehren, Hinweise zu Geschwindigkeit, Richtung, Startpunkt und Fehlern anzeigen und am Ende ein 3D Raum-Modell visualisieren. + +Die erste Version ist bewusst als React Native und Expo TypeScript Projekt aufgebaut. Sie nutzt nur notwendige Basis- und Navigationspakete, damit spaeter per `expo prebuild` sauber in ein natives iOS/Xcode-Projekt migriert werden kann. + +## Features + +- HomeScreen mit Projektziel und Scan-Ablauf +- ScanScreen mit Mock-Kameraansicht +- Instructions Overlay fuer gefuehrte Hinweise wie `Langsamer bewegen` oder `Richtung ändern` +- Ergebnis Screen mit Mock 3D Ansicht +- Stack Navigation mit TypeScript-Typen +- Vorbereitete Ordnerstruktur fuer Komponenten, Screens, Navigation, Services und Theme + +## Tech Stack + +- React Native +- Expo +- TypeScript +- React Navigation +- iOS-Zielplattform mit spaeterer Xcode-Erweiterung +- Geplante native Integration: ARKit und RoomPlan + +## Projektstruktur + +```text +roomscan-ai/ +├── App.tsx +├── app.json +├── package.json +├── src/ +│ ├── components/ +│ │ ├── InstructionOverlay.tsx +│ │ ├── PrimaryButton.tsx +│ │ └── ScanProgressCard.tsx +│ ├── navigation/ +│ │ ├── AppNavigator.tsx +│ │ └── types.ts +│ ├── screens/ +│ │ ├── HomeScreen.tsx +│ │ ├── ResultScreen.tsx +│ │ └── ScanScreen.tsx +│ ├── services/ +│ │ └── scanGuidance.ts +│ └── theme/ +│ └── colors.ts +└── README.md +``` + +## Installation + +Voraussetzungen: + +- Node.js +- npm +- Expo CLI ueber `npx expo` +- Fuer native iOS Builds spaeter: macOS, Xcode und Apple Developer Account + +Abhaengigkeiten installieren: + +```bash +npm install +``` + +Entwicklungsserver starten: + +```bash +npm run start +``` + +iOS Preview starten: + +```bash +npm run ios +``` + +Hinweis: Auf Windows kann `npm run ios` kein lokales iOS-Simulator-Build starten. Fuer echte iOS-Entwicklung wird macOS mit Xcode benoetigt. + +## Expo Prebuild und Xcode Vorbereitung + +Das Projekt ist so vorbereitet, dass spaeter ein natives iOS-Projekt erzeugt werden kann: + +```bash +npm run prebuild:ios +``` + +Dabei wird der Ordner `ios/` generiert. Dieser Ordner ist aktuell in `.gitignore` eingetragen, weil native Dateien erst versioniert werden sollten, wenn die RoomPlan-Integration konkret umgesetzt wird. + +Wichtige iOS-Vorbereitungen in `app.json`: + +- `bundleIdentifier`: `com.roomscanai.app` +- `NSCameraUsageDescription` fuer die spaetere Kamera- und RoomPlan-Nutzung +- `supportsTablet`: aktiviert fuer iPad-Kompatibilitaet + +## RoomPlan Integration + +RoomPlan ist eine native iOS-Technologie von Apple und wird nicht direkt durch Expo Go bereitgestellt. Die geplante Integration erfolgt spaeter ueber ein natives iOS-Modul oder eine Xcode-Erweiterung nach `expo prebuild`. + +Geplanter Integrationspfad: + +- Expo App als UI- und Navigationsbasis beibehalten +- iOS-Projekt per `expo prebuild --platform ios` erzeugen +- Native RoomPlan Capture Session in Swift implementieren +- Scan-Daten an React Native uebergeben +- Ergebnisdaten als 3D Modell oder strukturierte Raumdaten anzeigen + +## Roadmap + +- Mock UI fuer Scan Flow stabilisieren +- Gefuehrte Scan-Hinweise als State Machine modellieren +- Fehler- und Qualitaetsstatus fuer Scans erweitern +- Persistenz fuer Scan-Ergebnisse vorbereiten +- Native RoomPlan Integration in iOS/Xcode umsetzen +- 3D Ergebnisansicht mit echten RoomPlan-Daten anbinden +- Exportoptionen fuer Raumdaten pruefen +- TestFlight Build vorbereiten +- iOS App Store Veröffentlichung vorbereiten + +## Ziel + +Ziel ist eine saubere, erweiterbare iOS App fuer gefuehrte 3D Raumscans mit einem klaren Pfad zur nativen ARKit- und RoomPlan-Integration und anschliessender Veröffentlichung im iOS App Store. diff --git a/app.json b/app.json index 7a5de30..dfc4fd3 100644 --- a/app.json +++ b/app.json @@ -2,6 +2,7 @@ "expo": { "name": "roomscan-ai", "slug": "roomscan-ai", + "description": "Eine iOS App zur Erstellung von 3D Raumscans mit ARKit und RoomPlan.", "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", @@ -13,7 +14,11 @@ "backgroundColor": "#ffffff" }, "ios": { - "supportsTablet": true + "supportsTablet": true, + "bundleIdentifier": "com.roomscanai.app", + "infoPlist": { + "NSCameraUsageDescription": "RoomScan AI benoetigt die Kamera, um spaeter 3D Raumscans mit RoomPlan und ARKit zu erstellen." + } }, "android": { "adaptiveIcon": { diff --git a/package-lock.json b/package-lock.json index 1b8903f..b5aafc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,14 @@ "name": "roomscan-ai", "version": "1.0.0", "dependencies": { + "@react-navigation/native": "^7.2.2", + "@react-navigation/native-stack": "^7.14.12", "expo": "~54.0.33", "expo-status-bar": "~3.0.9", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" }, "devDependencies": { "@types/react": "~19.1.0", @@ -2964,6 +2968,123 @@ "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==", "license": "MIT" }, + "node_modules/@react-navigation/core": { + "version": "7.17.2", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.17.2.tgz", + "integrity": "sha512-Rt2OZwcgOmjv401uLGAKaRM6xo0fiBce/A7LfRHI1oe5FV+KooWcgAoZ2XOtgKj6UzVMuQWt3b2e6rxo/mDJRA==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.5.3", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.5.tgz", + "integrity": "sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==", + "license": "MIT" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.15.tgz", + "integrity": "sha512-cyz/pPiyyC6gaTVLsGFc1g0MYgrmuCFqklAWGXMWPscr5YU3ui94vPI4vnZwcsEy0T758TQWLzmS5XudZeRKcA==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.2.2", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.2.2.tgz", + "integrity": "sha512-kem1Ko2BcbAjmbQIv66dNmr6EtfDut3QU0qjsVhMnLLhktwyXb6FzZYp8gTrUb6AvkAbaJoi+BF5Pl55pAUa5w==", + "license": "MIT", + "dependencies": { + "@react-navigation/core": "^7.17.2", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.14.12", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.14.12.tgz", + "integrity": "sha512-dUfpkrVeVKKV8iqXsmoUp3Rv0iH3YaB3eZwScru/FlcqAp/r3/qA6zEXkGX9hZK+/ziWAPFrf1frBSNbgOYSFQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.15", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.2.2", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/native/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", + "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, "node_modules/@sinclair/typebox": { "version": "0.27.10", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", @@ -3915,6 +4036,19 @@ "node": ">=0.8" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3930,6 +4064,34 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -4086,6 +4248,15 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4907,6 +5078,12 @@ "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", "license": "Apache-2.0" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -4934,6 +5111,15 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -5279,6 +5465,12 @@ "loose-envify": "^1.0.0" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -7226,6 +7418,24 @@ "qrcode-terminal": "bin/qrcode-terminal.js" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", @@ -7278,6 +7488,18 @@ "ws": "^7" } }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -7351,6 +7573,31 @@ "react-native": "*" } }, + "node_modules/react-native-safe-area-context": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", + "integrity": "sha512-4XGqMNj5qjUTYywJqpdWZ9IG8jgkS3h06sfVjfw5yZQZfWnRFXczi0GnYyFyCc2EBps/qFmoCH8fez//WumdVg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", + "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "license": "MIT", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", @@ -7827,6 +8074,15 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7877,6 +8133,15 @@ "plist": "^3.0.5" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -7938,6 +8203,15 @@ "node": ">=0.10.0" } }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -8001,6 +8275,15 @@ "node": ">= 0.10.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -8490,6 +8773,24 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -8541,6 +8842,12 @@ "makeerror": "1.0.12" } }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index e317f17..02d4a3e 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,18 @@ "start": "expo start", "android": "expo start --android", "ios": "expo start --ios", - "web": "expo start --web" + "web": "expo start --web", + "prebuild:ios": "expo prebuild --platform ios" }, "dependencies": { + "@react-navigation/native": "^7.2.2", + "@react-navigation/native-stack": "^7.14.12", "expo": "~54.0.33", "expo-status-bar": "~3.0.9", "react": "19.1.0", - "react-native": "0.81.5" + "react-native": "0.81.5", + "react-native-safe-area-context": "~5.6.0", + "react-native-screens": "~4.16.0" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/src/components/InstructionOverlay.tsx b/src/components/InstructionOverlay.tsx new file mode 100644 index 0000000..9359945 --- /dev/null +++ b/src/components/InstructionOverlay.tsx @@ -0,0 +1,54 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { colors } from '../theme/colors'; + +type InstructionOverlayProps = { + title: string; + detail: string; + tone?: 'info' | 'warning' | 'danger'; +}; + +export function InstructionOverlay({ title, detail, tone = 'info' }: InstructionOverlayProps) { + const toneColor = tone === 'warning' ? colors.warning : tone === 'danger' ? colors.danger : colors.primary; + + return ( + + + + {title} + {detail} + + + ); +} + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + backgroundColor: 'rgba(8, 17, 31, 0.92)', + borderRadius: 20, + borderWidth: 1, + flexDirection: 'row', + gap: 14, + padding: 16, + }, + indicator: { + borderRadius: 999, + height: 14, + width: 14, + }, + textBlock: { + flex: 1, + }, + title: { + color: colors.textPrimary, + fontSize: 16, + fontWeight: '800', + }, + detail: { + color: colors.textSecondary, + fontSize: 13, + lineHeight: 19, + marginTop: 4, + }, +}); diff --git a/src/components/PrimaryButton.tsx b/src/components/PrimaryButton.tsx new file mode 100644 index 0000000..acdb5f1 --- /dev/null +++ b/src/components/PrimaryButton.tsx @@ -0,0 +1,51 @@ +import { Pressable, StyleSheet, Text } from 'react-native'; + +import { colors } from '../theme/colors'; + +type PrimaryButtonProps = { + label: string; + onPress: () => void; + variant?: 'primary' | 'secondary'; +}; + +export function PrimaryButton({ label, onPress, variant = 'primary' }: PrimaryButtonProps) { + return ( + [ + styles.button, + variant === 'secondary' && styles.secondary, + pressed && styles.pressed, + ]} + > + {label} + + ); +} + +const styles = StyleSheet.create({ + button: { + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 18, + paddingHorizontal: 22, + paddingVertical: 16, + }, + secondary: { + backgroundColor: colors.surfaceRaised, + borderColor: colors.border, + borderWidth: 1, + }, + pressed: { + opacity: 0.82, + }, + label: { + color: colors.background, + fontSize: 16, + fontWeight: '800', + }, + secondaryLabel: { + color: colors.textPrimary, + }, +}); diff --git a/src/components/ScanProgressCard.tsx b/src/components/ScanProgressCard.tsx new file mode 100644 index 0000000..37a0f95 --- /dev/null +++ b/src/components/ScanProgressCard.tsx @@ -0,0 +1,62 @@ +import { StyleSheet, Text, View } from 'react-native'; + +import { colors } from '../theme/colors'; + +type ScanProgressCardProps = { + progress: number; +}; + +export function ScanProgressCard({ progress }: ScanProgressCardProps) { + return ( + + + Scan-Fortschritt + {progress}% + + + + + Halte das iPhone ruhig und bewege dich entlang der Wandkontur. + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 22, + borderWidth: 1, + padding: 18, + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + label: { + color: colors.textSecondary, + fontSize: 14, + }, + value: { + color: colors.textPrimary, + fontSize: 18, + fontWeight: '800', + }, + track: { + backgroundColor: colors.surfaceRaised, + borderRadius: 999, + height: 10, + marginTop: 14, + overflow: 'hidden', + }, + fill: { + backgroundColor: colors.accent, + height: '100%', + }, + hint: { + color: colors.textSecondary, + fontSize: 13, + lineHeight: 19, + marginTop: 14, + }, +}); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx new file mode 100644 index 0000000..a01bd5a --- /dev/null +++ b/src/navigation/AppNavigator.tsx @@ -0,0 +1,29 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; + +import { HomeScreen } from '../screens/HomeScreen'; +import { ResultScreen } from '../screens/ResultScreen'; +import { ScanScreen } from '../screens/ScanScreen'; +import { colors } from '../theme/colors'; +import type { RootStackParamList } from './types'; + +const Stack = createNativeStackNavigator(); + +export function AppNavigator() { + return ( + + + + + + + + ); +} diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 0000000..fd0b919 --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,5 @@ +export type RootStackParamList = { + Home: undefined; + Scan: undefined; + Result: undefined; +}; diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx new file mode 100644 index 0000000..6aec727 --- /dev/null +++ b/src/screens/HomeScreen.tsx @@ -0,0 +1,108 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { ScrollView, StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { PrimaryButton } from '../components/PrimaryButton'; +import type { RootStackParamList } from '../navigation/types'; +import { colors } from '../theme/colors'; + +type HomeScreenProps = NativeStackScreenProps; + +const steps = ['Startpunkt wählen', 'Langsam entlang der Wand bewegen', 'Hinweise beachten', '3D Modell prüfen']; + +export function HomeScreen({ navigation }: HomeScreenProps) { + return ( + + + + ARKit + RoomPlan vorbereitet + Geführte 3D Raumscans fuer iOS + + RoomScan AI fuehrt Nutzer Schritt fuer Schritt durch den Scan und bereitet die App auf eine + native RoomPlan-Integration per Expo Prebuild vor. + + + + + Scan Ablauf + {steps.map((step, index) => ( + + {index + 1} + {step} + + ))} + + + navigation.navigate('Scan')} /> + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + content: { + gap: 24, + padding: 24, + }, + hero: { + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 28, + borderWidth: 1, + padding: 24, + }, + eyebrow: { + color: colors.primary, + fontSize: 13, + fontWeight: '800', + letterSpacing: 0.5, + textTransform: 'uppercase', + }, + title: { + color: colors.textPrimary, + fontSize: 34, + fontWeight: '900', + lineHeight: 39, + marginTop: 12, + }, + subtitle: { + color: colors.textSecondary, + fontSize: 16, + lineHeight: 24, + marginTop: 14, + }, + card: { + backgroundColor: colors.surfaceRaised, + borderRadius: 24, + padding: 20, + }, + cardTitle: { + color: colors.textPrimary, + fontSize: 18, + fontWeight: '800', + marginBottom: 14, + }, + stepRow: { + alignItems: 'center', + flexDirection: 'row', + gap: 12, + paddingVertical: 9, + }, + stepNumber: { + backgroundColor: colors.primaryDark, + borderRadius: 999, + color: colors.textPrimary, + fontWeight: '800', + overflow: 'hidden', + paddingHorizontal: 10, + paddingVertical: 6, + }, + stepText: { + color: colors.textSecondary, + flex: 1, + fontSize: 15, + }, +}); diff --git a/src/screens/ResultScreen.tsx b/src/screens/ResultScreen.tsx new file mode 100644 index 0000000..a5cc917 --- /dev/null +++ b/src/screens/ResultScreen.tsx @@ -0,0 +1,130 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { PrimaryButton } from '../components/PrimaryButton'; +import type { RootStackParamList } from '../navigation/types'; +import { colors } from '../theme/colors'; + +type ResultScreenProps = NativeStackScreenProps; + +export function ResultScreen({ navigation }: ResultScreenProps) { + return ( + + + + + + + + + + Mock 3D Modell + + + + Scan bereit zur Auswertung + + Diese Ansicht ist ein Platzhalter fuer das spaetere RoomPlan-Ergebnis. Nach der nativen iOS + Integration werden hier erkannte Waende, Tueren, Fenster und Moebel angezeigt. + + + + + navigation.navigate('Scan')} /> + navigation.navigate('Home')} /> + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + flex: 1, + gap: 20, + padding: 20, + }, + modelPreview: { + alignItems: 'center', + backgroundColor: colors.surface, + borderColor: colors.border, + borderRadius: 30, + borderWidth: 1, + flex: 1, + justifyContent: 'center', + minHeight: 360, + padding: 24, + }, + floorPlan: { + borderColor: colors.primary, + borderRadius: 24, + borderWidth: 3, + height: 220, + transform: [{ rotateX: '58deg' }, { rotateZ: '-18deg' }], + width: 220, + }, + wallLong: { + backgroundColor: colors.primary, + borderRadius: 10, + height: 14, + left: 20, + position: 'absolute', + top: 72, + width: 150, + }, + wallShort: { + backgroundColor: colors.accent, + borderRadius: 10, + height: 110, + position: 'absolute', + right: 48, + top: 54, + width: 14, + }, + roomBlockLarge: { + backgroundColor: 'rgba(98, 214, 255, 0.24)', + borderRadius: 18, + bottom: 28, + height: 70, + left: 34, + position: 'absolute', + width: 88, + }, + roomBlockSmall: { + backgroundColor: 'rgba(181, 240, 109, 0.28)', + borderRadius: 14, + height: 52, + position: 'absolute', + right: 32, + top: 24, + width: 58, + }, + previewLabel: { + color: colors.textSecondary, + fontSize: 15, + marginTop: 32, + }, + summaryCard: { + backgroundColor: colors.surfaceRaised, + borderRadius: 24, + padding: 20, + }, + title: { + color: colors.textPrimary, + fontSize: 21, + fontWeight: '900', + }, + body: { + color: colors.textSecondary, + fontSize: 15, + lineHeight: 23, + marginTop: 10, + }, + actions: { + gap: 12, + }, +}); diff --git a/src/screens/ScanScreen.tsx b/src/screens/ScanScreen.tsx new file mode 100644 index 0000000..db0289b --- /dev/null +++ b/src/screens/ScanScreen.tsx @@ -0,0 +1,135 @@ +import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { useEffect, useState } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { InstructionOverlay } from '../components/InstructionOverlay'; +import { PrimaryButton } from '../components/PrimaryButton'; +import { ScanProgressCard } from '../components/ScanProgressCard'; +import type { RootStackParamList } from '../navigation/types'; +import { getGuidanceForProgress } from '../services/scanGuidance'; +import { colors } from '../theme/colors'; + +type ScanScreenProps = NativeStackScreenProps; + +export function ScanScreen({ navigation }: ScanScreenProps) { + const [progress, setProgress] = useState(10); + const guidance = getGuidanceForProgress(progress); + + // Simulates RoomPlan scan updates until native iOS capture is connected. + useEffect(() => { + const intervalId = setInterval(() => { + setProgress((current) => Math.min(current + 5, 92)); + }, 1200); + + return () => clearInterval(intervalId); + }, []); + + return ( + + + + + + + Wohnzimmer + + + + + + + + + + + + navigation.navigate('Result')} /> + setProgress(10)} /> + + + + ); +} + +const styles = StyleSheet.create({ + safeArea: { + flex: 1, + }, + container: { + flex: 1, + gap: 18, + padding: 20, + }, + cameraMock: { + backgroundColor: '#0b1628', + borderColor: colors.border, + borderRadius: 30, + borderWidth: 1, + flex: 1, + minHeight: 380, + overflow: 'hidden', + }, + gridLineVertical: { + backgroundColor: 'rgba(98, 214, 255, 0.16)', + height: '100%', + left: '50%', + position: 'absolute', + width: 1, + }, + gridLineHorizontal: { + backgroundColor: 'rgba(98, 214, 255, 0.16)', + height: 1, + position: 'absolute', + top: '50%', + width: '100%', + }, + scanFrame: { + borderColor: colors.primary, + borderRadius: 24, + borderWidth: 2, + bottom: 70, + left: 34, + position: 'absolute', + right: 34, + top: 70, + }, + roomLabel: { + alignSelf: 'center', + backgroundColor: colors.primary, + borderRadius: 999, + color: colors.background, + fontWeight: '900', + marginTop: -16, + overflow: 'hidden', + paddingHorizontal: 16, + paddingVertical: 8, + }, + cornerMarkerTop: { + backgroundColor: colors.accent, + borderRadius: 8, + height: 16, + position: 'absolute', + right: 20, + top: 42, + width: 16, + }, + cornerMarkerBottom: { + backgroundColor: colors.warning, + borderRadius: 8, + bottom: 34, + height: 16, + left: 24, + position: 'absolute', + width: 16, + }, + overlayPosition: { + bottom: 18, + left: 18, + position: 'absolute', + right: 18, + }, + actions: { + gap: 12, + }, +}); diff --git a/src/services/scanGuidance.ts b/src/services/scanGuidance.ts new file mode 100644 index 0000000..dfc0169 --- /dev/null +++ b/src/services/scanGuidance.ts @@ -0,0 +1,33 @@ +export type ScanGuidance = { + title: string; + detail: string; + tone: 'info' | 'warning' | 'danger'; +}; + +const guidanceSteps: ScanGuidance[] = [ + { + title: 'Startpunkt setzen', + detail: 'Richte die Kamera auf eine freie Ecke und beginne dort mit dem Raumscan.', + tone: 'info', + }, + { + title: 'Langsamer bewegen', + detail: 'Die Kamera verliert Details. Reduziere die Geschwindigkeit fuer stabilere Messpunkte.', + tone: 'warning', + }, + { + title: 'Richtung ändern', + detail: 'Schwenke leicht nach rechts, damit die Wandkante vollstaendig erkannt wird.', + tone: 'info', + }, + { + title: 'Bereich erneut erfassen', + detail: 'Ein Objekt wurde unklar erkannt. Gehe einen Schritt zurueck und scanne die Zone erneut.', + tone: 'danger', + }, +]; + +export function getGuidanceForProgress(progress: number) { + const index = Math.min(Math.floor(progress / 25), guidanceSteps.length - 1); + return guidanceSteps[index]; +} diff --git a/src/theme/colors.ts b/src/theme/colors.ts new file mode 100644 index 0000000..57e6b2e --- /dev/null +++ b/src/theme/colors.ts @@ -0,0 +1,13 @@ +export const colors = { + background: '#08111f', + surface: '#101d31', + surfaceRaised: '#17263d', + primary: '#62d6ff', + primaryDark: '#0f8fbf', + accent: '#b5f06d', + warning: '#ffcf5a', + danger: '#ff7a7a', + textPrimary: '#f4f8ff', + textSecondary: '#a7b5c9', + border: '#263854', +};