From 64d308fb3a2b02e056a0e131328d27582fbd467c Mon Sep 17 00:00:00 2001 From: Abhinav Raj Date: Sun, 21 Dec 2025 04:25:05 +0530 Subject: [PATCH 1/3] optimized select rooms UI --- Envision/Assets.xcassets/.DS_Store | Bin 8196 -> 6148 bytes .../Contents.json | 12 ++ .../custom.sofafill.viewfinder.svg | 109 ++++++++++++++++++ .../Rooms/MyRoomsViewController+helpers.swift | 17 ++- .../Rooms/MyRoomsViewController.swift | 36 +++++- .../Screens/MainTabs/Rooms/RoomCell.swift | 88 ++++++++++---- 6 files changed, 229 insertions(+), 33 deletions(-) create mode 100644 Envision/Assets.xcassets/custom.sofafill.viewfinder.symbolset/Contents.json create mode 100644 Envision/Assets.xcassets/custom.sofafill.viewfinder.symbolset/custom.sofafill.viewfinder.svg diff --git a/Envision/Assets.xcassets/.DS_Store b/Envision/Assets.xcassets/.DS_Store index dae4d1a8e42b6b280faff6b0de4827f696608fab..299cc11e182de46bc0921f4bc7c1b9bf2e7790ff 100644 GIT binary patch delta 115 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$SANeU^g?Pz+@hQ-;;}lR3{q< z@@_U1>SLN%>adudgF}!Rs2B(YxPgQ#NZ-c7@640=WjsNqFfc((1X;qcIi6<@GXT1l B6VCtu delta 475 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD6uhMH}hr%jz7$c**Q2SHn1>C zOy*(vEiA&|$WQ=;o(#zh`3!js3Jivm{aD2&>#*`lvRUl`8i8Gfk{nR##Daw5jyRP_ zXr#C#<>V&;6@Z+BU$NNaJFL9SYzG)7+pubE)?uB^Bq+#G40KW&Ln4C@kWOdFpTs6F z&Jy1NGz&v&asivjWG^-@8K9)?LWrxdDU!plU=^D@6HCL!!W + + + + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + + + + + + + + + + + Design Variations + Symbols are supported in up to nine weights and three scales. + For optimal layout with text and other symbols, vertically align + symbols with the adjacent text. + + + + + + Margins + Leading and trailing margins on the left and right side of each symbol + can be adjusted by modifying the x-location of the margin guidelines. + Modifications are automatically applied proportionally to all + scales and weights. + + + + Exporting + Symbols should be outlined when exporting to ensure the + design is preserved when submitting to Xcode. + Template v.7.0 + Requires Xcode 17 or greater + Generated from viewfinder + Typeset at 100.0 points + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Envision/Screens/MainTabs/Rooms/MyRoomsViewController+helpers.swift b/Envision/Screens/MainTabs/Rooms/MyRoomsViewController+helpers.swift index 1f49338..8f383e9 100644 --- a/Envision/Screens/MainTabs/Rooms/MyRoomsViewController+helpers.swift +++ b/Envision/Screens/MainTabs/Rooms/MyRoomsViewController+helpers.swift @@ -43,12 +43,15 @@ extension MyRoomsViewController: UICollectionViewDataSource, UICollectionViewDel cell.button.addTarget(self, action: #selector(chipTapped(_:)), for: .touchUpInside) return cell } else { - // Room cell - UPDATED VERSION WITH BADGES + // Room cell let url = displayFiles[indexPath.item] let cell = collectionView.dequeueReusableCell(withReuseIdentifier: RoomCell.reuseID, for: indexPath) as! RoomCell + + // ✅ ADD THIS LINE (IMPORTANT) + cell.setSelectionMode(isSelectionMode, animated: false) + let metadata = loadMetadata(for: url) - // Configure with metadata for badge display cell.configure( fileName: url.lastPathComponent, size: fileSizeString(for: url), @@ -59,11 +62,11 @@ extension MyRoomsViewController: UICollectionViewDataSource, UICollectionViewDel generateThumbnail(for: url) { [weak self] image in guard let self = self, - let cell = self.collectionView.cellForItem(at: indexPath) as? RoomCell + let cell = self.collectionView.cellForItem(at: indexPath) as? RoomCell else { return } + let metadata = self.loadMetadata(for: url) - // Update with thumbnail and metadata cell.configure( fileName: url.lastPathComponent, size: self.fileSizeString(for: url), @@ -77,8 +80,12 @@ extension MyRoomsViewController: UICollectionViewDataSource, UICollectionViewDel } } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - guard indexPath.section == 1, !collectionView.allowsMultipleSelection else { return } + // MULTI-SELECTION MODE → just select + if isSelectionMode { return } + + // NORMAL MODE → open room + guard indexPath.section == 1 else { return } let url = displayFiles[indexPath.item] navigationController?.pushViewController(RoomViewerViewController(roomURL: url), animated: true) } diff --git a/Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift b/Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift index 18766c9..d7a885f 100644 --- a/Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift +++ b/Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift @@ -25,6 +25,7 @@ final class MyRoomsViewController: UIViewController { var selectedCategory: RoomCategory? var selectedRoomType: RoomType? let thumbnailCache = NSCache() + var isSelectionMode = false private var isSearching: Bool { guard let text = searchController.searchBar.text else { return false } @@ -93,7 +94,7 @@ final class MyRoomsViewController: UIViewController { private func makeMenu() -> UIMenu { UIMenu(children: [ - UIAction(title: "Select Multiple", image: UIImage(systemName: "checkmark.circle")) { [weak self] _ in + UIAction(title: "Select Rooms", image: UIImage(systemName: "checkmark.circle")) { [weak self] _ in self?.enableMultipleSelection() }, UIAction(title: "Delete All", image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] _ in @@ -249,15 +250,42 @@ final class MyRoomsViewController: UIViewController { } private func enableMultipleSelection() { + isSelectionMode = true collectionView.allowsMultipleSelection = true - navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(disableMultipleSelection)) - navigationItem.rightBarButtonItems = [UIBarButtonItem(image: UIImage(systemName: "trash"), style: .plain, target: self, action: #selector(deleteSelectedRooms))] + + // Show circles on visible cells + collectionView.visibleCells + .compactMap { $0 as? RoomCell } + .forEach { $0.setSelectionMode(true, animated: true) } + + let deleteButton = UIBarButtonItem( + image: UIImage(systemName: "trash"), + style: .plain, + target: self, + action: #selector(deleteSelectedRooms) + ) + deleteButton.tintColor = .systemRed + + navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Done", style: .prominent, target: self, action: #selector(disableMultipleSelection)) + navigationItem.rightBarButtonItems = [deleteButton] + // navigationItem.rightBarButtonItems = [UIBarButtonItem(image: UIImage(systemName: "trash"), style: .plain, target: self, action: #selector(deleteSelectedRooms))] + showToast(message: "Tap rooms to select, then tap delete") } @objc private func disableMultipleSelection() { + isSelectionMode = false collectionView.allowsMultipleSelection = false - collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: true) } + + // Hide circles + collectionView.visibleCells + .compactMap { $0 as? RoomCell } + .forEach { $0.setSelectionMode(false, animated: true) } + + // Clear selection + collectionView.indexPathsForSelectedItems? + .forEach { collectionView.deselectItem(at: $0, animated: false) } + setupNavigationBar() } diff --git a/Envision/Screens/MainTabs/Rooms/RoomCell.swift b/Envision/Screens/MainTabs/Rooms/RoomCell.swift index 6ad9717..d487ed8 100644 --- a/Envision/Screens/MainTabs/Rooms/RoomCell.swift +++ b/Envision/Screens/MainTabs/Rooms/RoomCell.swift @@ -43,7 +43,20 @@ final class RoomCell: UICollectionViewCell { return v }() - // Category Badge + // MARK: - Selection UI + private let selectionCircle: UIImageView = { + let iv = UIImageView() + iv.translatesAutoresizingMaskIntoConstraints = false + iv.image = UIImage(systemName: "circle") + iv.tintColor = .systemGray3 + iv.isHidden = true + return iv + }() + + private var containerLeadingConstraint: NSLayoutConstraint! + + // MARK: - Category Badge + private let categoryBadge: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -112,32 +125,40 @@ final class RoomCell: UICollectionViewCell { // MARK: - Setup private func setupUI() { + contentView.backgroundColor = .clear + contentView.addSubview(selectionCircle) contentView.addSubview(container) container.addSubview(thumbnailView) container.addSubview(titleLabel) container.addSubview(sizeLabel) - // Add badges thumbnailView.addSubview(categoryBadge) thumbnailView.addSubview(roomTypeBadge) - categoryBadge.addSubview(categoryIcon) - categoryBadge.addSubview(categoryLabel) - roomTypeBadge.addSubview(roomTypeIcon) roomTypeBadge.addSubview(roomTypeLabel) - contentView.backgroundColor = .clear + categoryBadge.addSubview(categoryIcon) + categoryBadge.addSubview(categoryLabel) + contentView.layer.shadowColor = UIColor.black.cgColor contentView.layer.shadowOffset = CGSize(width: 0, height: 2) contentView.layer.shadowRadius = 4 contentView.layer.shadowOpacity = 0.1 + containerLeadingConstraint = container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 12) + NSLayoutConstraint.activate([ + // Selection Circle + selectionCircle.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), + selectionCircle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectionCircle.widthAnchor.constraint(equalToConstant: 30), + selectionCircle.heightAnchor.constraint(equalToConstant: 30), + // Container + containerLeadingConstraint, container.topAnchor.constraint(equalTo: contentView.topAnchor), - container.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + container.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -12), container.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), // Thumbnail @@ -157,7 +178,7 @@ final class RoomCell: UICollectionViewCell { sizeLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8), sizeLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8), - // MARK: - Category Badge constraints + // RoomType Badge roomTypeBadge.topAnchor.constraint(equalTo: thumbnailView.topAnchor, constant: 8), roomTypeBadge.trailingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: -8), roomTypeBadge.heightAnchor.constraint(equalToConstant: 24), @@ -171,7 +192,7 @@ final class RoomCell: UICollectionViewCell { roomTypeLabel.trailingAnchor.constraint(equalTo: roomTypeBadge.trailingAnchor, constant: -8), roomTypeLabel.centerYAnchor.constraint(equalTo: roomTypeBadge.centerYAnchor), - // CATEGORY Badge — BELOW roomType badge + // Category Badge categoryBadge.topAnchor.constraint(equalTo: roomTypeBadge.bottomAnchor, constant: 6), categoryBadge.trailingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: -8), categoryBadge.heightAnchor.constraint(equalToConstant: 24), @@ -183,11 +204,34 @@ final class RoomCell: UICollectionViewCell { categoryLabel.leadingAnchor.constraint(equalTo: categoryIcon.trailingAnchor, constant: 4), categoryLabel.trailingAnchor.constraint(equalTo: categoryBadge.trailingAnchor, constant: -8), - categoryLabel.centerYAnchor.constraint(equalTo: categoryBadge.centerYAnchor) + categoryLabel.centerYAnchor.constraint(equalTo: categoryBadge.centerYAnchor), ]) } + // MARK: - Selection Mode + + func setSelectionMode(_ enabled: Bool, animated: Bool) { + let changes = { + self.selectionCircle.isHidden = !enabled + self.containerLeadingConstraint.constant = enabled ? 60 : 12 + self.layoutIfNeeded() + } + + animated + ? UIView.animate(withDuration: 0.25, animations: changes) + : changes() + } + + override var isSelected: Bool { + didSet { + let imageName = isSelected ? "checkmark.circle.fill" : "circle" + selectionCircle.image = UIImage(systemName: imageName) + selectionCircle.tintColor = isSelected ? .systemBlue : .systemGray3 + } + } + // MARK: - Reuse + override func prepareForReuse() { super.prepareForReuse() thumbnailView.image = nil @@ -195,20 +239,25 @@ final class RoomCell: UICollectionViewCell { sizeLabel.text = nil categoryBadge.isHidden = true roomTypeBadge.isHidden = true + selectionCircle.image = UIImage(systemName: "circle") } // MARK: - Configure - func configure(fileName: String, size: String, thumbnail: UIImage?, category: RoomCategory? = nil, roomType: RoomType? = nil) { + func configure( + fileName: String, + size: String, + thumbnail: UIImage?, + category: RoomCategory? = nil, + roomType: RoomType? = nil + ) { titleLabel.text = fileName sizeLabel.text = size - thumbnailView.image = thumbnail ?? UIImage(systemName: "arkit")! + thumbnailView.image = thumbnail ?? UIImage(systemName: "arkit") - // Reset badges categoryBadge.isHidden = true roomTypeBadge.isHidden = true - // Category Badge if let category = category { categoryIcon.image = UIImage(systemName: category.sfSymbol) categoryIcon.tintColor = category.color @@ -217,7 +266,6 @@ final class RoomCell: UICollectionViewCell { categoryBadge.isHidden = false } - // RoomType Badge if let roomType = roomType { roomTypeIcon.image = UIImage(systemName: roomType.sfSymbol) roomTypeIcon.tintColor = roomType.color @@ -226,12 +274,4 @@ final class RoomCell: UICollectionViewCell { roomTypeBadge.isHidden = false } } - - /// Legacy version for backward compatibility - func configure(with model: RoomModel) { - titleLabel.text = model.name ?? "Room" - thumbnailView.image = model.thumbnail ?? UIImage(systemName: "square.split.2x2")! - categoryBadge.isHidden = true - roomTypeBadge.isHidden = true - } } From 553198192155a22af4ef285f2017d4371a9acc46 Mon Sep 17 00:00:00 2001 From: Abhinav Raj Date: Wed, 14 Jan 2026 14:54:07 +0530 Subject: [PATCH 2/3] tips not working properly other things optimised --- .DS_Store | Bin 6148 -> 6148 bytes Envision.xcodeproj/project.pbxproj | 8 +- .../UserInterfaceState.xcuserstate | Bin 90832 -> 227169 bytes Envision/.DS_Store | Bin 10244 -> 10244 bytes Envision/AppDelegate.swift | 36 +- Envision/Extensions/Extensions.swift | 2 +- Envision/Extensions/SaveManager.swift | 4 +- Envision/Extensions/UserManager.swift | 2 +- Envision/MainTabBarController.swift | 70 +- Envision/Managers/TourManager.swift | 177 ++ Envision/SceneDelegate.swift | 4 +- Envision/Screens/.DS_Store | Bin 6148 -> 6148 bytes .../MainTabs/Rooms/MetadataManager.swift | 22 +- .../Rooms/MyRoomsViewController+helpers.swift | 2 + .../Rooms/MyRoomsViewController.swift | 179 +- .../Screens/MainTabs/Rooms/RoomCell.swift | 24 +- .../RoomPreviewViewController.swift | 8 +- .../furniture+room/RoomVisualizeVC.swift | 210 ++- .../CreateModelViewController2.swift | 12 +- .../ObjectCapturePreviewController.swift | 40 +- .../ObjectScanViewController.swift | 13 +- .../ScanFurnitureViewController.swift | 1 + .../RoomARWithFurnitureViewController.swift | 2 +- .../VisualizeRoomViewController.swift | 2 +- .../profile/ProfileViewController.swift | 28 + .../TipsLibraryViewController.swift | 184 +++ .../Onboarding/OnboardingController.swift | 2 - Envision/Tips/AppTips.swift | 680 ++++++++ FURNITURE_SCANNING_GUIDE.md | 879 ++++++++++ IMPROVEMENTS.md | 590 +++++++ TECHNICAL_DOCUMENTATION.md | 1465 +++++++++++++++++ 31 files changed, 4563 insertions(+), 83 deletions(-) create mode 100644 Envision/Managers/TourManager.swift create mode 100644 Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift create mode 100644 Envision/Tips/AppTips.swift create mode 100644 FURNITURE_SCANNING_GUIDE.md create mode 100644 IMPROVEMENTS.md create mode 100644 TECHNICAL_DOCUMENTATION.md diff --git a/.DS_Store b/.DS_Store index 2fff9d3af2fd147f1403a89b12e70fea25e7b005..aac36d7e420adce94a6ed7619e87f02cef589635 100644 GIT binary patch delta 233 zcmZoMXffEJ!j!-?hf|Y*fq{iVk0G5Qlc6Lx-^C>~xUC5#4xFf233SRkw z48!2${M-Vd9tNhl6Bs66U=o=;gNcWQY4Y9>AV+NSJ|-@<>jz$4w6B?bj!6l@6r0S$ z3{*Dl1H|ra-+mpWw~yHj!PMeMwwFQNVY46y3(ID9 Hj=%f>O;trA delta 233 zcmZoMXffEJ!j!7E;gPR|H)sICigL$A(&eHDE2ZHY!>8TVcE>i H@s}R}7FR)t diff --git a/Envision.xcodeproj/project.pbxproj b/Envision.xcodeproj/project.pbxproj index 6d53d0e..c8aa4cc 100644 --- a/Envision.xcodeproj/project.pbxproj +++ b/Envision.xcodeproj/project.pbxproj @@ -145,7 +145,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = R5PAJ8PLL5; + DEVELOPMENT_TEAM = 4PMY8MS8XC; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Envision/Info.plist; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; @@ -161,7 +161,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 25.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vinayakchandra.Envision; + PRODUCT_BUNDLE_IDENTIFIER = com.vinayakchandra.Envisionjhf; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -180,7 +180,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = R5PAJ8PLL5; + DEVELOPMENT_TEAM = 4PMY8MS8XC; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Envision/Info.plist; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; @@ -196,7 +196,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 25.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.vinayakchandra.Envision; + PRODUCT_BUNDLE_IDENTIFIER = com.vinayakchandra.Envisionjhf; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; diff --git a/Envision.xcodeproj/project.xcworkspace/xcuserdata/user86.xcuserdatad/UserInterfaceState.xcuserstate b/Envision.xcodeproj/project.xcworkspace/xcuserdata/user86.xcuserdatad/UserInterfaceState.xcuserstate index d1f81f3ed9c85cee8345b88c5a1652e6751adc03..ca2916e0956b77ca7e435984704ae99c7e25fdf8 100644 GIT binary patch literal 227169 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
Q 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 delta 1615 zcmeH{ZERCj7{|~5w5<1z6MEXU^RC^bYz>6BT4`Z(!O-DMoZGrOC+r2>T6zQ5UE0B| zbGXS!Sd7syF((lX2?+_#K!5}W(TKzW;YB}?sEcAS@&!mtL_d&_zz5>7EfGk3{p1JF zm*+hH=bZE8$?u;tmKaOyY(wRd#0e)V#U@X=sOD90h#2p2s%kIShKHlk@S;GpwpJYA z+avMRfN5B{aQHH)%|4^Vp{S)Z8Jopct?umV?(FI1{AEjT*UeL*^*IJ z4?DH0>bVQUOY2uOKGTwz<}P=c$IF;-xwpibZ~g*R;mHn5wwNw+yEtQ^$5fSD9sNco zomYG;5>-^5e!)zpbh9YcJt3v$)q{mDMNwRGuLl#cve0NOM)Iqs5D2M>Ehs8e>!UA4hQlpW-Aw!$o|HOZW~~aRWc&7YbTRJ=9Cf>1-OH)wG5#qqQ_fn`k@jp?YBe z3avQFng=JNgVHKlB3}f3uz~XWT9gQ>d{R6RqVH ze}{iZCb?1nr|7>SSI9MTgWQ(rXM)3vAQmHnCne`*v?7k@(2kx_ti_Ay%L6yD5m^ji z3x*~1tr*1{coT172gb1r@8SJC_z8R{!5_dRj^QJGj8AX|XK@Y}@HxJ~*Z3aSqycW; z16<_V_7GpY%vBO0V#wnZeUBWs@v|ur*MefXR$8JS{_)EdBceqM|Go{132hebBjN{5 zo4ly?0=q=W7gTK7@YR(g=Nn|=bEg|qW;P|CzF*eF9$%Sg@>kK|(_(|apE{ Bool { - // REQUIRED: Initialize TipKit (iOS 17.0+) - if #available(iOS 17.0, *) { - do { - try Tips.configure([ - .displayFrequency(.immediate), - .datastoreLocation(.applicationDefault) - ]) - print(" TipKit configured successfully") - } catch { - print(" TipKit configuration failed: \(error)") - } - } - return true } - // Required for SceneDelegate lifecycle - func application(_ application: UIApplication, - configurationForConnecting connectingSceneSession: UISceneSession, - options: UIScene.ConnectionOptions) -> UISceneConfiguration { - return UISceneConfiguration(name: "Default Configuration", - sessionRole: connectingSceneSession.role) - } - - + // Required for SceneDelegate lifecycle + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + } // MARK: UISceneSession Lifecycle - - func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { // Called when the user discards a scene session. // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. // Use this method to release any resources that were specific to the discarded scenes, as they will not return. } - } - diff --git a/Envision/Assets.xcassets/.DS_Store b/Envision/Assets.xcassets/.DS_Store index 299cc11e182de46bc0921f4bc7c1b9bf2e7790ff..859dcf960366f5f08344309463f5eeb299d3b3bd 100644 GIT binary patch literal 8196 zcmeHMJx|;~5S=9mofIj2h=O!3BGDmHAsR4n6j7uUp-LLUamRslJ|EoiB`7TAx1_s* z9}y`vzadpx8rr;>-S}+z5VrXiGuH0dyZhGrp4YoKJ|Z%!hxK`)86v76v7dO0Y>06^ zr;Lrb5(XRKiQ3eoH43SPq^1g}0;+&2pbDr0&rt#F*=&^?_I+EcxhkLvyh;UleejUj z`_4z!%hG{NLIC(t6pP^;ae!bv=Y8iR>jjEFZS-Iq)VL7CSUBoEZVvA|A6akVWGtMF zyR&f-ijlhm=Sg!izV(``0;)h(0gm0LX@$0NG*a>V0nMSz#_|W$MQyLuoZr|EgO2Ow ztOzy;tJPX7tN~M5Sp0jreExf-7`}cdd}lW~c3CB+z`KLvqws8l@0MKex$7+F@jurX zb1qzx@BUKEHz#($wJ0{l(=LV|z~>N8u!pA^fMpkc2V8e(4_|6pC^^DegN~)7O?8~} z)9=zL+Sg3x?>u_CgqJ{ZM#Sz-mW?L z0LnK}Geps^v-wa-t*(B?b1TRFf^eWhG2{?_)8G-}XkrdEXcOFcy||U-*gLSyjZJd> z+>f~yzz-Og!KZn97&^U=-=LCj&eln}UXof|Uc_3I7*BE@+^wgyJ`wAq_A(TBV~07z z@&EeG-~V3*86Bl6pbC^yz>HQms;hvy*gDe*$J!p!cO*7$7g;Y5WD<_UOE?Z6|6z!8 r4^0{4J0Dq(64;}E2>2g&g0VQRxsv2 x5yy$84vX12I0Tu2jsXGzZXn?b^2El%@640=WjsM%Vqk)J59DKp&G9^Qm;t3;O9=n~ diff --git a/Envision/MainTabBarController.swift b/Envision/MainTabBarController.swift index 8505ee3..f1aa188 100644 --- a/Envision/MainTabBarController.swift +++ b/Envision/MainTabBarController.swift @@ -1,22 +1,16 @@ import UIKit -import TipKit -import SwiftUI final class MainTabBarController: UITabBarController { - - private var tipHostingController: UIViewController? override func viewDidLoad() { super.viewDidLoad() setupTabs() setupLiquidGlassEffect() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - if #available(iOS 17.0, *) { - showWelcomeTip() - } + // Intentionally no global overlay tips here. } private func setupTabs() { @@ -57,61 +51,4 @@ final class MainTabBarController: UITabBarController { tabBar.scrollEdgeAppearance = appearance tabBar.isTranslucent = true } - - @available(iOS 17.0, *) - private func showWelcomeTip() { - guard TourManager.shared.shouldShowTour() else { return } - - if let existing = tipHostingController { - existing.willMove(toParent: nil) - existing.view.removeFromSuperview() - existing.removeFromParent() - tipHostingController = nil - } - - let tip = WelcomeTip() - - let actionHandler: (Tip.Action) -> Void = { [weak self] action in - guard let self = self else { return } - switch action.id { - case "start-tour": - TourManager.shared.startTour() - self.selectedIndex = 0 - self.dismissTip() - case "skip": - TourManager.shared.completeTour() - self.dismissTip() - default: break - } - } - - let tipView = TipView(tip, arrowEdge: .bottom) { action in - actionHandler(action) - } - - let hostingController = UIHostingController(rootView: tipView) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.backgroundColor = UIColor.clear - - addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.didMove(toParent: self) - - NSLayoutConstraint.activate([ - hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor), - hostingController.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor, constant: -20), - hostingController.view.widthAnchor.constraint(lessThanOrEqualToConstant: 350) - ]) - - self.tipHostingController = hostingController - } - - private func dismissTip() { - if let existing = tipHostingController { - existing.willMove(toParent: nil) - existing.view.removeFromSuperview() - existing.removeFromParent() - tipHostingController = nil - } - } } diff --git a/Envision/Managers/BackgroundModelProcessor.swift b/Envision/Managers/BackgroundModelProcessor.swift new file mode 100644 index 0000000..b54be57 --- /dev/null +++ b/Envision/Managers/BackgroundModelProcessor.swift @@ -0,0 +1,434 @@ +// +// BackgroundModelProcessor.swift +// Envision +// +// Background processing manager for 3D model generation +// Allows photogrammetry to continue even when user leaves the screen +// + +import Foundation +import RealityKit +import UIKit +import UserNotifications + +// MARK: - Processing Job Model +struct ProcessingJob: Codable, Identifiable { + let id: UUID + let imagesFolder: String + let outputFileName: String + let createdAt: Date + var status: ProcessingStatus + var progress: Float + var errorMessage: String? + + enum ProcessingStatus: String, Codable { + case queued + case processing + case completed + case failed + case cancelled + } +} + +// MARK: - Processing Delegate +protocol BackgroundModelProcessorDelegate: AnyObject { + func processingDidUpdateProgress(_ progress: Float, status: String) + func processingDidComplete(savedURL: URL) + func processingDidFail(error: String) +} + +// MARK: - Background Model Processor +final class BackgroundModelProcessor: @unchecked Sendable { + + static let shared = BackgroundModelProcessor() + + // MARK: - Properties + weak var delegate: BackgroundModelProcessorDelegate? + + private var currentJob: ProcessingJob? + private var currentSession: PhotogrammetrySession? + private var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid + + // Processing state (thread-safe access) + private let lock = NSLock() + private var _isProcessing = false + private var _currentProgress: Float = 0 + private var _currentStatus: String = "" + + var isProcessing: Bool { + lock.lock() + defer { lock.unlock() } + return _isProcessing + } + + var currentProgress: Float { + lock.lock() + defer { lock.unlock() } + return _currentProgress + } + + var currentStatus: String { + lock.lock() + defer { lock.unlock() } + return _currentStatus + } + + // Observers for UI updates when user returns + var onProgressUpdate: ((Float, String) -> Void)? + var onCompletion: ((URL) -> Void)? + var onError: ((String) -> Void)? + + private init() { + requestNotificationPermission() + } + + // MARK: - Notification Permission + private func requestNotificationPermission() { + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, _ in + print("📱 Notification permission: \(granted ? "granted" : "denied")") + } + } + + // MARK: - Start Processing + func startProcessing( + imagesFolder: URL, + detailLevel: PhotogrammetrySession.Request.Detail = .reduced, + completion: @escaping @Sendable (Result) -> Void + ) { + lock.lock() + guard !_isProcessing else { + lock.unlock() + completion(.failure(ProcessingError.alreadyProcessing)) + return + } + _isProcessing = true + _currentProgress = 0 + _currentStatus = "Preparing..." + lock.unlock() + + // Generate output filename + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" + let timestamp = dateFormatter.string(from: Date()) + let outputFileName = "Furniture_\(timestamp).usdz" + + // Create job + currentJob = ProcessingJob( + id: UUID(), + imagesFolder: imagesFolder.path, + outputFileName: outputFileName, + createdAt: Date(), + status: .processing, + progress: 0, + errorMessage: nil + ) + + // Start background task for extended processing + beginBackgroundTask() + + // Start processing on background thread + processPhotogrammetry( + imagesFolder: imagesFolder, + outputFileName: outputFileName, + detailLevel: detailLevel, + completion: completion + ) + } + + // MARK: - Background Task Management + private func beginBackgroundTask() { + DispatchQueue.main.async { [weak self] in + self?.backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "PhotogrammetryProcessing") { [weak self] in + self?.handleBackgroundTimeExpiring() + } + print(" Background task started") + } + } + + private func endBackgroundTask() { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + if self.backgroundTaskID != .invalid { + UIApplication.shared.endBackgroundTask(self.backgroundTaskID) + self.backgroundTaskID = .invalid + print(" Background task ended") + } + } + } + + private func handleBackgroundTimeExpiring() { + print(" Background time expiring...") + endBackgroundTask() + } + + // MARK: - Core Processing + private func processPhotogrammetry( + imagesFolder: URL, + outputFileName: String, + detailLevel: PhotogrammetrySession.Request.Detail, + completion: @escaping @Sendable (Result) -> Void + ) { + let outputURL = FileManager.default.temporaryDirectory + .appendingPathComponent(outputFileName) + + // Optimized configuration for speed + var config = PhotogrammetrySession.Configuration() + config.sampleOrdering = .sequential + config.featureSensitivity = .normal + + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + guard let self = self else { return } + + let startTime = Date() + print(" Starting photogrammetry processing...") + print(" Input: \(imagesFolder.path)") + print(" Output: \(outputURL.path)") + print(" Detail level: \(detailLevel)") + + // Count images + let imageCount = (try? FileManager.default.contentsOfDirectory(at: imagesFolder, includingPropertiesForKeys: nil))? + .filter { ["jpg", "jpeg", "heic", "png"].contains($0.pathExtension.lowercased()) } + .count ?? 0 + print(" Processing \(imageCount) images") + + guard let session = try? PhotogrammetrySession( + input: imagesFolder, + configuration: config + ) else { + self.handleFailure(error: "Failed to create photogrammetry session", completion: completion) + return + } + + self.currentSession = session + + // Create the request with specified detail level + let request = PhotogrammetrySession.Request.modelFile( + url: outputURL, + detail: detailLevel + ) + + // Process outputs asynchronously + Task { + do { + for try await output in session.outputs { + switch output { + + case .processingComplete: + print(" Processing complete!") + + case .inputComplete: + print(" Input complete") + await MainActor.run { + self.updateProgress(0.1, status: "Analyzing images...") + } + + case .requestProgress(_, let fraction): + let mappedProgress = 0.1 + (Float(fraction) * 0.85) + + var status = "Processing..." + if fraction < 0.3 { + status = " Analyzing images..." + } else if fraction < 0.6 { + status = " Building 3D mesh..." + } else if fraction < 0.9 { + status = " Applying textures..." + } else { + status = " Finalizing model..." + } + + await MainActor.run { + self.updateProgress(mappedProgress, status: status) + } + + case .requestComplete(_, _): + let elapsed = Date().timeIntervalSince(startTime) + print(" Model generated in \(String(format: "%.1f", elapsed))s") + + await MainActor.run { + self.updateProgress(0.95, status: " Saving model...") + self.saveGeneratedModel(outputURL, completion: completion) + } + + case .requestError(_, let error): + print(" Request error: \(error)") + await MainActor.run { + self.handleFailure(error: error.localizedDescription, completion: completion) + } + + case .processingCancelled: + print(" Processing cancelled") + await MainActor.run { + self.handleFailure(error: "Processing was cancelled", completion: completion) + } + + case .invalidSample(let id, let reason): + print(" Invalid sample \(id): \(reason)") + + case .skippedSample(let id): + print(" Skipped sample: \(id)") + + case .automaticDownsampling: + print(" Automatic downsampling applied") + + default: + break + } + } + } catch { + print(" Task error: \(error)") + await MainActor.run { + self.handleFailure(error: error.localizedDescription, completion: completion) + } + } + } + + // Start processing + do { + try session.process(requests: [request]) + } catch { + print(" Process start error: \(error)") + self.handleFailure(error: error.localizedDescription, completion: completion) + } + } + } + + // MARK: - Progress Updates + private func updateProgress(_ progress: Float, status: String) { + lock.lock() + _currentProgress = progress + _currentStatus = status + currentJob?.progress = progress + lock.unlock() + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.delegate?.processingDidUpdateProgress(progress, status: status) + self.onProgressUpdate?(progress, status) + print(" Progress: \(Int(progress * 100))% - \(status)") + } + } + + // MARK: - Save Model + private func saveGeneratedModel(_ url: URL, completion: @escaping @Sendable (Result) -> Void) { + SaveManager.shared.saveModel(from: url, type: .furniture, customName: nil) { [weak self] result in + guard let self = self else { return } + + self.endBackgroundTask() + + self.lock.lock() + self._isProcessing = false + self.lock.unlock() + + self.currentSession = nil + + switch result { + case .success(let savedURL): + self.currentJob?.status = .completed + self.updateProgress(1.0, status: " Complete!") + + DispatchQueue.main.async { + self.delegate?.processingDidComplete(savedURL: savedURL) + self.onCompletion?(savedURL) + } + + self.sendCompletionNotification(success: true) + completion(.success(savedURL)) + + case .failure(let error): + self.handleFailure(error: error.localizedDescription, completion: completion) + } + } + } + + // MARK: - Error Handling + private func handleFailure(error: String, completion: @escaping @Sendable (Result) -> Void) { + endBackgroundTask() + + lock.lock() + _isProcessing = false + lock.unlock() + + currentSession = nil + currentJob?.status = .failed + currentJob?.errorMessage = error + + DispatchQueue.main.async { [weak self] in + self?.delegate?.processingDidFail(error: error) + self?.onError?(error) + } + + sendCompletionNotification(success: false, errorMessage: error) + completion(.failure(ProcessingError.processingFailed(error))) + } + + // MARK: - Local Notifications + private func sendCompletionNotification(success: Bool, errorMessage: String? = nil) { + DispatchQueue.main.async { + guard UIApplication.shared.applicationState != .active else { return } + + let content = UNMutableNotificationContent() + + if success { + content.title = "3D Model Ready!" + content.body = "Your furniture model has been generated and saved." + content.sound = .default + } else { + content.title = "Model Generation Failed" + content.body = errorMessage ?? "An error occurred during processing." + content.sound = .default + } + + let request = UNNotificationRequest( + identifier: UUID().uuidString, + content: content, + trigger: nil + ) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print(" Notification error: \(error)") + } + } + } + } + + // MARK: - Cancel Processing + func cancelProcessing() { + currentSession?.cancel() + currentSession = nil + + lock.lock() + _isProcessing = false + lock.unlock() + + currentJob?.status = .cancelled + endBackgroundTask() + + print(" Processing cancelled by user") + } + + // MARK: - Get Current State + func getCurrentState() -> (isProcessing: Bool, progress: Float, status: String) { + lock.lock() + defer { lock.unlock() } + return (_isProcessing, _currentProgress, _currentStatus) + } +} + +// MARK: - Errors +enum ProcessingError: LocalizedError { + case alreadyProcessing + case processingFailed(String) + case sessionCreationFailed + + var errorDescription: String? { + switch self { + case .alreadyProcessing: + return "A model is already being processed. Please wait." + case .processingFailed(let message): + return message + case .sessionCreationFailed: + return "Failed to create photogrammetry session." + } + } +} diff --git a/Envision/Managers/RoomColorManager.swift b/Envision/Managers/RoomColorManager.swift new file mode 100644 index 0000000..9abac69 --- /dev/null +++ b/Envision/Managers/RoomColorManager.swift @@ -0,0 +1,133 @@ +import UIKit + +/// Manages saved colors for room elements, persisted per room URL +final class RoomColorManager { + + static let shared = RoomColorManager() + private init() {} + + // MARK: - Storage + /// Dictionary: roomURL.path -> [elementPrefix: colorHex] + private var colorStorage: [String: [String: String]] = [:] + + // MARK: - Keys for element types + static let wallKey = "wall" + static let floorKey = "floor" + static let doorKey = "door" + static let windowKey = "window" + static let tableKey = "table" + static let chairKey = "chair" + static let storageKey = "storage" + + // MARK: - Public API + + /// Save a color for a specific element type in a room + func saveColor(_ color: UIColor, for elementType: String, roomURL: URL) { + let roomKey = roomURL.path + let hexColor = color.toHex() + + if colorStorage[roomKey] == nil { + colorStorage[roomKey] = [:] + } + colorStorage[roomKey]?[elementType] = hexColor + + // Persist to disk + persistColors(for: roomURL) + } + + /// Get saved color for an element type, or nil if not set + func getColor(for elementType: String, roomURL: URL) -> UIColor? { + let roomKey = roomURL.path + + // Try memory cache first + if let hexColor = colorStorage[roomKey]?[elementType] { + return UIColor(hex: hexColor) + } + + // Try loading from disk + loadColors(for: roomURL) + + if let hexColor = colorStorage[roomKey]?[elementType] { + return UIColor(hex: hexColor) + } + + return nil + } + + /// Get all saved colors for a room + func getAllColors(for roomURL: URL) -> [String: UIColor] { + let roomKey = roomURL.path + + // Ensure colors are loaded + if colorStorage[roomKey] == nil { + loadColors(for: roomURL) + } + + var result: [String: UIColor] = [:] + colorStorage[roomKey]?.forEach { key, hexValue in + result[key] = UIColor(hex: hexValue) + } + return result + } + + /// Clear all saved colors for a room + func clearColors(for roomURL: URL) { + let roomKey = roomURL.path + colorStorage[roomKey] = nil + + // Remove from disk + let colorFileURL = colorFileURL(for: roomURL) + try? FileManager.default.removeItem(at: colorFileURL) + } + + // MARK: - Persistence + + private func colorFileURL(for roomURL: URL) -> URL { + let roomName = roomURL.deletingPathExtension().lastPathComponent + let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + return documentsURL.appendingPathComponent("RoomColors/\(roomName)_colors.json") + } + + private func persistColors(for roomURL: URL) { + let roomKey = roomURL.path + guard let colors = colorStorage[roomKey] else { return } + + let fileURL = colorFileURL(for: roomURL) + + // Create directory if needed + let directory = fileURL.deletingLastPathComponent() + try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + + // Save as JSON + if let data = try? JSONEncoder().encode(colors) { + try? data.write(to: fileURL) + } + } + + private func loadColors(for roomURL: URL) { + let roomKey = roomURL.path + let fileURL = colorFileURL(for: roomURL) + + guard FileManager.default.fileExists(atPath: fileURL.path) else { return } + + if let data = try? Data(contentsOf: fileURL), + let colors = try? JSONDecoder().decode([String: String].self, from: data) { + colorStorage[roomKey] = colors + } + } +} + +// MARK: - UIColor Extension for Hex Output +extension UIColor { + func toHex() -> String { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + getRed(&r, green: &g, blue: &b, alpha: &a) + + let rgb: Int = (Int)(r*255)<<16 | (Int)(g*255)<<8 | (Int)(b*255)<<0 + return String(format: "#%06x", rgb) + } +} diff --git a/Envision/Managers/TourManager.swift b/Envision/Managers/TourManager.swift index 769a643..aa6bdc9 100644 --- a/Envision/Managers/TourManager.swift +++ b/Envision/Managers/TourManager.swift @@ -7,7 +7,7 @@ // import Foundation -import TipKit +// TipKit temporarily removed from the project. /// Centralized manager for tour state, progress tracking, and tour lifecycle final class TourManager { @@ -55,7 +55,7 @@ final class TourManager { private init() { // Check if first launch if isFirstLaunch { - print("📱 First launch detected - Tour will be shown") + print(" First launch detected - Tour will be shown") } } @@ -86,7 +86,7 @@ final class TourManager { func completeTour() { isTourCompleted = true currentTourStep = 0 - print("✅ Tour completed") + print(" Tour completed") } /// Resets all tour progress and tips @@ -94,31 +94,23 @@ final class TourManager { isTourCompleted = false currentTourStep = 0 hasSeenWelcome = false - - // Reset TipKit datastore (iOS 17+) - if #available(iOS 17.0, *) { - do { - try Tips.resetDatastore() - print("🔄 Tips datastore reset") - } catch { - print("❌ Failed to reset tips: \(error)") - } - } - + + // TipKit temporarily removed from the project. + print("🔄 Tour reset complete") } /// Advances to the next tour step func nextStep() { currentTourStep += 1 - print("➡️ Tour step: \(currentTourStep)") + print(" Tour step: \(currentTourStep)") } /// Skips to a specific step /// - Parameter step: The step number to skip to func skipToStep(_ step: Int) { currentTourStep = step - print("⏭️ Skipped to step: \(step)") + print(" Skipped to step: \(step)") } // MARK: - Helper Methods @@ -171,7 +163,7 @@ final class TourManager { func forceShowTour() { resetTour() - print("✅ Tour forced to show") + print(" Tour forced to show") } #endif } diff --git a/Envision/SceneDelegate.swift b/Envision/SceneDelegate.swift index 103229e..3d25b60 100644 --- a/Envision/SceneDelegate.swift +++ b/Envision/SceneDelegate.swift @@ -18,8 +18,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - // window.rootViewController = SplashViewController() - window.rootViewController = MainTabBarController() + //window.rootViewController = SplashViewController() + window.rootViewController = MainTabBarController() self.window = window window.makeKeyAndVisible() diff --git a/Envision/Screens/.DS_Store b/Envision/Screens/.DS_Store index 76b140b2e943eaf4f24c70b22db050c2806ceadc..a07d75f3d0259812d4a2989db5d940c73e107f39 100644 GIT binary patch delta 116 zcmZoMXffEJ$HX-6_GAMl1&%MpuP)lx9CrkA#3tu4aj`)JCf6}3GqW==Og_UTF?kOY zAJeIPAX9~7$L8PbjB0_(&oD^|rMM*JvOFdz#{OtxX>WBjl=ka-)+#0Iv_>>Pjj E0Sx*pqW}N^ delta 116 zcmZoMXffEJ$HWw|aIyiD0>_%kx^ey4#~pzjvB`N%Tx<}5$#qQ1%xnb=lg}_oOy0x9 z$F!mp$W-Cz6J+swn+jBZhDk~&#U&{xKZ${X0a;jLvJEpI NSCollectionLayoutSection { @@ -219,16 +215,16 @@ final class MyRoomsViewController: UIViewController { view.addSubview(loadingOverlay) NSLayoutConstraint.activate([ - loadingOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loadingOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor), - loadingOverlay.widthAnchor.constraint(equalToConstant: 220), - loadingOverlay.heightAnchor.constraint(equalToConstant: 120), - activityIndicator.topAnchor.constraint(equalTo: loadingOverlay.contentView.topAnchor, constant: 18), - activityIndicator.centerXAnchor.constraint(equalTo: loadingOverlay.contentView.centerXAnchor), - loadingLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 10), - loadingLabel.leadingAnchor.constraint(equalTo: loadingOverlay.contentView.leadingAnchor, constant: 12), - loadingLabel.trailingAnchor.constraint(equalTo: loadingOverlay.contentView.trailingAnchor, constant: -12) - ]) + loadingOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loadingOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor), + loadingOverlay.widthAnchor.constraint(equalToConstant: 220), + loadingOverlay.heightAnchor.constraint(equalToConstant: 120), + activityIndicator.topAnchor.constraint(equalTo: loadingOverlay.contentView.topAnchor, constant: 18), + activityIndicator.centerXAnchor.constraint(equalTo: loadingOverlay.contentView.centerXAnchor), + loadingLabel.topAnchor.constraint(equalTo: activityIndicator.bottomAnchor, constant: 10), + loadingLabel.leadingAnchor.constraint(equalTo: loadingOverlay.contentView.leadingAnchor, constant: 12), + loadingLabel.trailingAnchor.constraint(equalTo: loadingOverlay.contentView.trailingAnchor, constant: -12) + ]) } // MARK: - Actions @@ -500,11 +496,20 @@ final class MyRoomsViewController: UIViewController { // MARK: - Thumbnails func generateThumbnail(for url: URL, completion: @escaping (UIImage?) -> Void) { + // Check memory cache first if let cached = thumbnailCache.object(forKey: url as NSURL) { completion(cached) return } + + // Check for saved colored thumbnail + if let savedThumbnail = loadSavedThumbnail(for: url) { + thumbnailCache.setObject(savedThumbnail, forKey: url as NSURL) + completion(savedThumbnail) + return + } + // Fall back to QuickLook generator let req = QLThumbnailGenerator.Request( fileAt: url, size: CGSize(width: 400, height: 400), @@ -523,6 +528,20 @@ final class MyRoomsViewController: UIViewController { } } } + + private func loadSavedThumbnail(for roomURL: URL) -> UIImage? { + let roomName = roomURL.deletingPathExtension().lastPathComponent + let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let thumbnailURL = documentsURL.appendingPathComponent("RoomThumbnails/\(roomName)_thumb.jpg") + + guard FileManager.default.fileExists(atPath: thumbnailURL.path), + let imageData = try? Data(contentsOf: thumbnailURL), + let image = UIImage(data: imageData) else { + return nil + } + + return image + } func fileSizeString(for url: URL) -> String { guard let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), @@ -630,11 +649,11 @@ final class MyRoomsViewController: UIViewController { toast.alpha = 0 NSLayoutConstraint.activate([ - toast.centerXAnchor.constraint(equalTo: view.centerXAnchor), - toast.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), - toast.heightAnchor.constraint(equalToConstant: 40), - toast.widthAnchor.constraint(greaterThanOrEqualToConstant: 150) - ]) + toast.centerXAnchor.constraint(equalTo: view.centerXAnchor), + toast.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + toast.heightAnchor.constraint(equalToConstant: 40), + toast.widthAnchor.constraint(greaterThanOrEqualToConstant: 150) + ]) UIView.animate(withDuration: 0.3) { toast.alpha = 1 @@ -681,6 +700,18 @@ final class MyRoomsViewController: UIViewController { return chips } + + // MARK: - Tips container (kept but unused; can be removed later safely) + private let tipContainerView: UIView = { + let v = UIView() + v.translatesAutoresizingMaskIntoConstraints = false + v.backgroundColor = .clear + v.isUserInteractionEnabled = true + v.isHidden = true + return v + }() + + private var tipContainerBottomConstraint: NSLayoutConstraint? } // MARK: - Models @@ -709,11 +740,11 @@ final class ChipCell: UICollectionViewCell { button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) contentView.addSubview(button) NSLayoutConstraint.activate([ - button.topAnchor.constraint(equalTo: contentView.topAnchor), - button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) - ]) + button.topAnchor.constraint(equalTo: contentView.topAnchor), + button.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + button.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + button.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -730,98 +761,148 @@ final class ChipCell: UICollectionViewCell { button.backgroundColor = isSelected ? color : color.withAlphaComponent(0.1) button.tintColor = isSelected ? .white : color + let resolvedTint: UIColor = button.tintColor ?? color let attachment = NSTextAttachment() let config = UIImage.SymbolConfiguration(pointSize: 14, weight: .medium) - attachment.image = UIImage(systemName: icon, withConfiguration: config)?.withTintColor(button.tintColor, renderingMode: .alwaysOriginal) + attachment.image = UIImage(systemName: icon, withConfiguration: config)?.withTintColor(resolvedTint, renderingMode: .alwaysOriginal) let attributedString = NSMutableAttributedString(attachment: attachment) attributedString.append(NSAttributedString(string: " \(title)", attributes: [ .font: UIFont.systemFont(ofSize: 14, weight: .medium), - .foregroundColor: button.tintColor + .foregroundColor: resolvedTint ])) button.setAttributedTitle(attributedString, for: .normal) } } -@available(iOS 17.0, *) -extension MyRoomsViewController { - - private func setupTips() { - // Initial setup if needed - } - - private func updateTipParameters() { - MyRoomsIntroTip.hasRooms = !roomFiles.isEmpty - RoomActionsMenuTip.roomCount = roomFiles.count - RoomCategoriesTip.roomCount = roomFiles.count - } - - private func showContextualTip() { - dismissTip() - - var tip: (any Tip)? - var edge: TipKit.Edge = .top - - if roomFiles.isEmpty { - tip = MyRoomsIntroTip() - edge = .top - } else if roomFiles.count == 1 { - tip = RoomImportTip() - edge = .top - } else if roomFiles.count >= 2 { - tip = RoomCategoriesTip() - edge = .bottom - } - - guard let tipToDisplay = tip else { return } - - let actionHandler: (Tip.Action) -> Void = { [weak self] action in - self?.handleTipAction(action) - } - - let tipView = TipView(tipToDisplay, arrowEdge: edge) { action in - actionHandler(action) - } - - let hostingController = UIHostingController(rootView: tipView) - hostingController.view.translatesAutoresizingMaskIntoConstraints = false - hostingController.view.backgroundColor = UIColor.clear - - addChild(hostingController) - view.addSubview(hostingController.view) - hostingController.didMove(toParent: self) - self.tipHostingController = hostingController - - NSLayoutConstraint.activate([ - hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) - ]) - - if tipToDisplay is MyRoomsIntroTip || tipToDisplay is RoomImportTip { - hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true - } else if tipToDisplay is RoomCategoriesTip { - hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60).isActive = true - } else { - hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true - } - } - - private func dismissTip() { - if let existing = tipHostingController { - existing.willMove(toParent: nil) - existing.view.removeFromSuperview() - existing.removeFromParent() - tipHostingController = nil - } - } - - private func handleTipAction(_ action: Tip.Action) { - switch action.id { - case "scan": scanTapped(); dismissTip() - case "later": dismissTip() - default: dismissTip() - } - } -} +// MARK: - TipKit Integration (temporarily removed) +// @available(iOS 17.0, *) +// extension MyRoomsViewController { +// +// private func setupTips() { +// // Create presenter for the screen-level tip container. +// if tipPresenter == nil { +// tipPresenter = TipPresenter(owner: self, containerView: tipContainerView) +// } +// } +// +// private func updateTipParameters() { +// MyRoomsIntroTip.hasRooms = !roomFiles.isEmpty +// RoomActionsMenuTip.roomCount = roomFiles.count +// RoomCategoriesTip.roomCount = roomFiles.count +// RoomImportTip.hasScannedRoom = !roomFiles.isEmpty +// } +// +// private func showContextualTip() { +// // Avoid showing tips while selecting or presenting another VC. +// if isSelectionMode { dismissTip(); return } +// if presentedViewController != nil { return } +// +// updateTipParameters() +// +// // Ensure any old SwiftUI-hosted tips are removed if they still exist (prevents ghost text). +// tipContainerView.subviews.forEach { $0.removeFromSuperview() } +// +// // Determine which tip to show (screen decides; TipPresenter just renders). +// var title: String? +// var message: String? +// var image: String? +// var actions: [TipPresenter.Action] = [] +// +// if roomFiles.isEmpty { +// title = "📐 Scan Your First Room" +// message = "Tap the green camera button to start scanning any room using your iPhone's LiDAR sensor. We'll create a precise 3D model!" +// image = "camera.viewfinder" +// actions = [ +// .init(id: "scan", title: "Scan Now"), +// .init(id: "later", title: "Maybe Later") +// ] +// } else if roomFiles.count == 1 { +// title = "📥 Already Have 3D Models?" +// message = "Tap the blue import button to bring in existing USDZ room models from your Files app." +// image = "square.and.arrow.down" +// actions = [ +// .init(id: "import", title: "Import"), +// .init(id: "later", title: "Later") +// ] +// } else if roomFiles.count >= 2 { +// title = "🏷️ Organize Your Spaces" +// message = "Use category chips to filter rooms by type. Long-press any room to edit its category and add custom tags." +// image = "tag.fill" +// actions = [ +// .init(id: "got-it", title: "Got it") +// ] +// } +// +// guard let title else { return } +// +// tipContainerView.isHidden = false +// setupTips() +// +// let height = tipPresenter?.present( +// title: title, +// message: message, +// systemImageName: image, +// actions: actions +// ) { [weak self] actionId in +// self?.handleTipActionId(actionId) +// } ?? 0 +// +// // Expand container to fit tip height, then update insets. +// tipContainerBottomConstraint?.isActive = false +// tipContainerBottomConstraint = tipContainerView.heightAnchor.constraint(equalToConstant: max(0, height)) +// tipContainerBottomConstraint?.priority = .required +// tipContainerBottomConstraint?.isActive = true +// +// updateCollectionInsetsForTip(containerHeight: height) +// } +// +// private func updateCollectionInsetsForTip(containerHeight: CGFloat) { +// guard let collectionView else { return } +// let topInset = max(0, containerHeight) + 12 +// var inset = collectionView.contentInset +// if abs(inset.top - topInset) > 1 { +// inset.top = topInset +// collectionView.contentInset = inset +// collectionView.scrollIndicatorInsets = inset +// } +// } +// +// private func dismissTip() { +// tipPresenter?.dismiss() +// tipContainerView.subviews.forEach { $0.removeFromSuperview() } +// tipContainerView.isHidden = true +// +// // Collapse container. +// tipContainerBottomConstraint?.isActive = false +// tipContainerBottomConstraint = tipContainerView.bottomAnchor.constraint(equalTo: tipContainerView.topAnchor) +// tipContainerBottomConstraint?.isActive = true +// +// // Reset insets. +// if let collectionView { +// var inset = collectionView.contentInset +// if inset.top != 0 { +// inset.top = 0 +// collectionView.contentInset = inset +// collectionView.scrollIndicatorInsets = inset +// } +// } +// } +// +// private func handleTipActionId(_ id: String) { +// switch id { +// case "scan": +// dismissTip() +// scanTapped() +// case "import": +// dismissTip() +// importTapped() +// case "later", "got-it": +// dismissTip() +// default: +// dismissTip() +// } +// } +// } diff --git a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomEditVC.swift b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomEditVC.swift index 585e750..fa27632 100644 --- a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomEditVC.swift +++ b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomEditVC.swift @@ -200,13 +200,79 @@ final class RoomEditVC: UIViewController { // MARK: - Navigation private func setupNavigation() { - navigationItem.rightBarButtonItem = UIBarButtonItem( + // Save button to save colors and go back + let saveButton = UIBarButtonItem( + title: "Save", + style: .done, + target: self, + action: #selector(saveAndGoBack) + ) + saveButton.tintColor = .systemGreen + + // Add furniture button + let addButton = UIBarButtonItem( image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addFurnitureTapped) ) - navigationItem.rightBarButtonItem?.tintColor = .systemGreen + addButton.tintColor = .systemBlue + + navigationItem.rightBarButtonItems = [saveButton, addButton] + } + + @objc private func saveAndGoBack() { + // Generate thumbnail with current colors + generateAndSaveThumbnail() + + // Show success feedback + let alert = UIAlertController( + title: "Saved", + message: "Room colors have been saved successfully.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in + // Navigate back to home (My Rooms) + self?.navigationController?.popToRootViewController(animated: true) + }) + present(alert, animated: true) + } + + private func generateAndSaveThumbnail() { + // Capture current ARView as thumbnail + let renderer = UIGraphicsImageRenderer(size: arView.bounds.size) + let thumbnail = renderer.image { _ in + arView.drawHierarchy(in: arView.bounds, afterScreenUpdates: true) + } + + // Save thumbnail to room's thumbnail location + saveThumbnail(thumbnail, for: roomURL) + } + + private func saveThumbnail(_ image: UIImage, for roomURL: URL) { + let roomName = roomURL.deletingPathExtension().lastPathComponent + let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] + let thumbnailsDir = documentsURL.appendingPathComponent("RoomThumbnails") + + // Create thumbnails directory if needed + try? FileManager.default.createDirectory(at: thumbnailsDir, withIntermediateDirectories: true) + + let thumbnailURL = thumbnailsDir.appendingPathComponent("\(roomName)_thumb.jpg") + + // Resize image for thumbnail (smaller file size) + let resizedImage = resizeImage(image, to: CGSize(width: 400, height: 300)) + + if let jpegData = resizedImage.jpegData(compressionQuality: 0.8) { + try? jpegData.write(to: thumbnailURL) + print("✅ Saved thumbnail to: \(thumbnailURL.path)") + } + } + + private func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage { + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: size)) + } } // MARK: - Gestures @@ -326,6 +392,8 @@ final class RoomEditVC: UIViewController { // MARK: - Materials & Labels private func applyMaterialRules(to root: Entity) { var entitiesFound:[String] = [] + let savedColors = RoomColorManager.shared.getAllColors(for: roomURL) + root.visit { guard let model = $0 as? ModelEntity else { return } @@ -334,43 +402,69 @@ final class RoomEditVC: UIViewController { originalMaterials[model] = model.model?.materials } - // 🔴 Enable Colors OFF → force everything white + // 🔴 Enable Colors OFF → apply saved colors or white guard enableColors else { - model.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.9), roughness: 0.8, isMetallic: false)] + let name = model.name.lowercased() + + // Check for saved colors first + if name.starts(with: "wall"), let color = savedColors[RoomColorManager.wallKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else if name.starts(with: "floor"), let color = savedColors[RoomColorManager.floorKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.6, isMetallic: false)] + } else if name.starts(with: "door"), let color = savedColors[RoomColorManager.doorKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else if name.starts(with: "window"), let color = savedColors[RoomColorManager.windowKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else if name.starts(with: "table"), let color = savedColors[RoomColorManager.tableKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else if name.starts(with: "chair"), let color = savedColors[RoomColorManager.chairKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else if name.starts(with: "storage"), let color = savedColors[RoomColorManager.storageKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } else { + model.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.9), roughness: 0.8, isMetallic: false)] + } return } - // 🟢 Enable Colors ON → apply semantic colors + // 🟢 Enable Colors ON → apply saved colors or default semantic colors let name = model.name.lowercased() entitiesFound.append(name) switch true { case name.starts(with: "wall"): - model.model?.materials = [SimpleMaterial(color: .systemBlue, roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.wallKey] ?? .systemBlue + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 1.5) case name.starts(with: "floor"): - model.model?.materials = [SimpleMaterial(color: .gray, roughness: 0.6, isMetallic: false)] + let color = savedColors[RoomColorManager.floorKey] ?? .gray + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.6, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.05) case name.starts(with: "chair"): - model.model?.materials = [SimpleMaterial(color: .black, roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.chairKey] ?? .black + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.15) case name.starts(with: "table"): - model.model?.materials = [SimpleMaterial(color: .systemRed, roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.tableKey] ?? .systemRed + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.5) case name.starts(with: "door"): - model.model?.materials = [SimpleMaterial(color: .systemCyan.withAlphaComponent(0.3), roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.doorKey] ?? .systemCyan.withAlphaComponent(0.3) + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.8) case name.starts(with: "window"): - model.model?.materials = [SimpleMaterial(color: .lightGray.withAlphaComponent(0.3), roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.windowKey] ?? .lightGray.withAlphaComponent(0.3) + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.4) case name.starts(with: "storage"): - model.model?.materials = [SimpleMaterial(color: .systemOrange, roughness: 0.4, isMetallic: false)] + let color = savedColors[RoomColorManager.storageKey] ?? .systemOrange + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] attachLabel(to: model, text: name, yOffset: 0.4) default: @@ -433,11 +527,51 @@ final class RoomEditVC: UIViewController { colorTarget = target let picker = UIColorPickerViewController() picker.delegate = self - present(picker, animated: true) + picker.supportsAlpha = true + + // Wrap in navigation controller to add custom buttons + let navController = UINavigationController(rootViewController: picker) + navController.modalPresentationStyle = .pageSheet + + // Add native Cancel button (left side) + let cancelButton = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(cancelColorPicker) + ) + picker.navigationItem.leftBarButtonItem = cancelButton + + // Add native Done button (right side) + let doneButton = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(dismissColorPicker) + ) + picker.navigationItem.rightBarButtonItem = doneButton + picker.navigationItem.title = "Choose Color" + + present(navController, animated: true) + } + + @objc private func cancelColorPicker() { + // Restore original materials when cancelling + if let model = displayedModel { + applyMaterialRules(to: model) + } + dismiss(animated: true) + } + + @objc private func dismissColorPicker() { + dismiss(animated: true) } private func attachLabel(to entity: Entity, text: String, yOffset: Float) { - labels[entity]?.removeFromParent() + // Remove existing label safely + if let existingLabel = labels[entity] { + existingLabel.components.remove(BillboardComponent.self) + existingLabel.removeFromParent() + labels[entity] = nil + } let mesh = MeshResource.generateText( text, @@ -447,11 +581,31 @@ final class RoomEditVC: UIViewController { let label = ModelEntity(mesh: mesh, materials: [SimpleMaterial(color: .white, isMetallic: false)]) label.position = [0, yOffset, 0] - label.components.set(BillboardComponent()) label.isEnabled = showLabels - + + // Add to parent first, then set BillboardComponent on main thread entity.addChild(label) labels[entity] = label + + // Set BillboardComponent after a brief delay to avoid crash + DispatchQueue.main.async { [weak label] in + guard let label = label, label.parent != nil else { return } + label.components.set(BillboardComponent()) + } + } + + // MARK: - Cleanup + private func cleanupLabels() { + for (_, label) in labels { + label.components.remove(BillboardComponent.self) + label.removeFromParent() + } + labels.removeAll() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + cleanupLabels() } // MARK: - Furniture @@ -505,24 +659,83 @@ final class RoomEditVC: UIViewController { } private func presentColorPicker() { + colorTarget = .selected let picker = UIColorPickerViewController() picker.delegate = self - present(picker, animated: true) + picker.supportsAlpha = true + + // Wrap in navigation controller to add custom buttons + let navController = UINavigationController(rootViewController: picker) + navController.modalPresentationStyle = .pageSheet + + // Add native Cancel button (left side) + let cancelButton = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(cancelColorPicker) + ) + picker.navigationItem.leftBarButtonItem = cancelButton + + // Add native Done button (right side) + let doneButton = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(dismissColorPicker) + ) + picker.navigationItem.rightBarButtonItem = doneButton + picker.navigationItem.title = "Choose Color" + + present(navController, animated: true) } } // MARK: - Color Picker extension RoomEditVC: UIColorPickerViewControllerDelegate { func colorPickerViewControllerDidSelectColor(_ viewController: UIColorPickerViewController) { + let selectedColor = viewController.selectedColor switch colorTarget { case .selected: selectedModel?.model?.materials = [ - SimpleMaterial(color: viewController.selectedColor, roughness: 0.4, isMetallic: false) + SimpleMaterial(color: selectedColor, roughness: 0.4, isMetallic: false) ] + // Save color for selected element type + if let modelName = selectedModel?.name.lowercased() { + let elementType = getElementType(from: modelName) + if let type = elementType { + RoomColorManager.shared.saveColor(selectedColor, for: type, roomURL: roomURL) + } + } default: - setColor(for: colorTarget, color: viewController.selectedColor) + setColor(for: colorTarget, color: selectedColor) + // Save color for the target type + if let elementType = colorTargetToElementType(colorTarget) { + RoomColorManager.shared.saveColor(selectedColor, for: elementType, roomURL: roomURL) + } + } + } + + private func getElementType(from name: String) -> String? { + if name.starts(with: "wall") { return RoomColorManager.wallKey } + if name.starts(with: "floor") { return RoomColorManager.floorKey } + if name.starts(with: "door") { return RoomColorManager.doorKey } + if name.starts(with: "window") { return RoomColorManager.windowKey } + if name.starts(with: "table") { return RoomColorManager.tableKey } + if name.starts(with: "chair") { return RoomColorManager.chairKey } + if name.starts(with: "storage") { return RoomColorManager.storageKey } + return nil + } + + private func colorTargetToElementType(_ target: ColorTarget) -> String? { + switch target { + case .walls: return RoomColorManager.wallKey + case .floors: return RoomColorManager.floorKey + case .doors: return RoomColorManager.doorKey + case .windows: return RoomColorManager.windowKey + case .tables: return RoomColorManager.tableKey + case .storage: return RoomColorManager.storageKey + case .selected: return nil } } } diff --git a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift index 784a098..55b69b4 100644 --- a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift +++ b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift @@ -305,6 +305,9 @@ final class RoomVisualizeVC: UIViewController { displayedModel = clone fitToScreen(clone) + + // Apply saved colors from RoomColorManager + applySavedColors(to: clone) let anchor = AnchorEntity(world: .zero) anchor.addChild(clone) @@ -312,6 +315,40 @@ final class RoomVisualizeVC: UIViewController { setupCamera() } + + private func applySavedColors(to root: ModelEntity) { + let savedColors = RoomColorManager.shared.getAllColors(for: roomURL) + + guard !savedColors.isEmpty else { return } + + root.visit { entity in + guard let model = entity as? ModelEntity else { return } + let name = model.name.lowercased() + + // Check each element type and apply saved color if available + if name.starts(with: "wall"), let color = savedColors[RoomColorManager.wallKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + else if name.starts(with: "floor"), let color = savedColors[RoomColorManager.floorKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.6, isMetallic: false)] + } + else if name.starts(with: "door"), let color = savedColors[RoomColorManager.doorKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + else if name.starts(with: "window"), let color = savedColors[RoomColorManager.windowKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + else if name.starts(with: "table"), let color = savedColors[RoomColorManager.tableKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + else if name.starts(with: "chair"), let color = savedColors[RoomColorManager.chairKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + else if name.starts(with: "storage"), let color = savedColors[RoomColorManager.storageKey] { + model.model?.materials = [SimpleMaterial(color: color, roughness: 0.4, isMetallic: false)] + } + } + } private func fitToScreen(_ model: ModelEntity) { let bounds = model.visualBounds(relativeTo: nil) diff --git a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift index f070324..fcb0d49 100644 --- a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift +++ b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift @@ -15,6 +15,9 @@ final class ObjectCapturePreviewController: UIViewController { private var isProcessing = false private var imageURLs: [URL] = [] + // Background processing + private let processor = BackgroundModelProcessor.shared + private let scrollView: UIScrollView = { let sv = UIScrollView() sv.translatesAutoresizingMaskIntoConstraints = false @@ -130,6 +133,69 @@ final class ObjectCapturePreviewController: UIViewController { btn.translatesAutoresizingMaskIntoConstraints = false return btn }() + + // Quality selector + private let qualitySegment: UISegmentedControl = { + let items = ["Fast", "Balanced", "High Quality"] + let seg = UISegmentedControl(items: items) + seg.selectedSegmentIndex = 0 // Default to Fast + seg.translatesAutoresizingMaskIntoConstraints = false + return seg + }() + + private let qualityLabel: UILabel = { + let lbl = UILabel() + lbl.text = "Processing Speed" + lbl.font = .systemFont(ofSize: 14, weight: .medium) + lbl.textColor = .secondaryLabel + lbl.translatesAutoresizingMaskIntoConstraints = false + return lbl + }() + + private let qualityDescLabel: UILabel = { + let lbl = UILabel() + lbl.text = "⚡ Fastest processing, good for quick previews" + lbl.font = .systemFont(ofSize: 12) + lbl.textColor = .tertiaryLabel + lbl.textAlignment = .center + lbl.numberOfLines = 0 + lbl.translatesAutoresizingMaskIntoConstraints = false + return lbl + }() + + private let backgroundInfoLabel: UILabel = { + let lbl = UILabel() + lbl.text = "💡 You can leave this screen - processing continues in background" + lbl.font = .systemFont(ofSize: 13, weight: .medium) + lbl.textColor = .systemBlue + lbl.textAlignment = .center + lbl.numberOfLines = 0 + lbl.alpha = 0 + lbl.translatesAutoresizingMaskIntoConstraints = false + return lbl + }() + + private let cancelButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("Cancel Processing", for: .normal) + btn.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + btn.setTitleColor(.systemRed, for: .normal) + btn.backgroundColor = .systemRed.withAlphaComponent(0.1) + btn.layer.cornerRadius = 14 + btn.isHidden = true + btn.translatesAutoresizingMaskIntoConstraints = false + return btn + }() + + private let timeEstimateLabel: UILabel = { + let lbl = UILabel() + lbl.font = .systemFont(ofSize: 13) + lbl.textColor = .tertiaryLabel + lbl.textAlignment = .center + lbl.translatesAutoresizingMaskIntoConstraints = false + return lbl + }() + private var selectedDetailLevel: PhotogrammetrySession.Request.Detail = .reduced // MARK: - Init @@ -147,6 +213,8 @@ final class ObjectCapturePreviewController: UIViewController { loadImages() setupUI() + setupQualitySelector() + setupBackgroundProcessingCallbacks() generateButton.addTarget(self, action: #selector(startProcessing), for: .touchUpInside) retakeButton.addTarget(self, action: #selector(retakePhotos), for: .touchUpInside) @@ -154,6 +222,85 @@ final class ObjectCapturePreviewController: UIViewController { collectionView.delegate = self collectionView.dataSource = self + + // Check if processing is already in progress (user returned to screen) + checkExistingProcessing() + } + + // MARK: - Setup Quality Selector + private func setupQualitySelector() { + qualitySegment.addTarget(self, action: #selector(qualityChanged), for: .valueChanged) + updateQualityDescription() + } + + @objc private func qualityChanged() { + updateQualityDescription() + + // Haptic feedback + let generator = UISelectionFeedbackGenerator() + generator.selectionChanged() + } + + private func updateQualityDescription() { + switch qualitySegment.selectedSegmentIndex { + case 0: // Fast + selectedDetailLevel = .reduced + qualityDescLabel.text = " ~30s-1 min • Quick preview quality" + case 1: // Balanced + selectedDetailLevel = .reduced + qualityDescLabel.text = " ~1-3 min • Good balance of speed & quality" + case 2: // High Quality + selectedDetailLevel = .reduced + qualityDescLabel.text = "pl ~3-8 min • Maximum detail & textures" + default: + break + } + } + + // MARK: - Background Processing Callbacks + private func setupBackgroundProcessingCallbacks() { + // Progress updates + processor.onProgressUpdate = { [weak self] progress, status in + guard let self = self else { return } + self.progressView.setProgress(progress, animated: true) + self.progressLabel.text = "\(Int(progress * 100))%" + self.statusLabel.text = status + } + + // Completion + processor.onCompletion = { [weak self] savedURL in + guard let self = self else { return } + self.handleProcessingComplete(savedURL: savedURL) + } + + // Error + processor.onError = { [weak self] error in + guard let self = self else { return } + self.handleError(message: error) + } + } + + // MARK: - Check Existing Processing + private func checkExistingProcessing() { + let state = processor.getCurrentState() + + if state.isProcessing { + // Resume UI for ongoing processing + isProcessing = true + generateButton.isEnabled = false + retakeButton.isEnabled = false + qualitySegment.isEnabled = false + activityIndicator.startAnimating() + + progressView.isHidden = false + progressLabel.isHidden = false + progressView.progress = state.progress + progressLabel.text = "\(Int(state.progress * 100))%" + statusLabel.text = state.status + statusLabel.textColor = .label + + backgroundInfoLabel.alpha = 1 + } } // MARK: - Load Images @@ -189,7 +336,9 @@ final class ObjectCapturePreviewController: UIViewController { scrollView.addSubview(contentView) [headerLabel, photoCountLabel, collectionView, statusLabel, - progressView, progressLabel, generateButton, retakeButton, activityIndicator, exportButton].forEach { + qualityLabel, qualitySegment, qualityDescLabel, + progressView, progressLabel, backgroundInfoLabel, + generateButton, retakeButton, activityIndicator, exportButton].forEach { contentView.addSubview($0) } @@ -216,19 +365,35 @@ final class ObjectCapturePreviewController: UIViewController { collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), collectionView.heightAnchor.constraint(equalToConstant: 100), - statusLabel.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 30), + statusLabel.topAnchor.constraint(equalTo: collectionView.bottomAnchor, constant: 24), statusLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), statusLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - progressView.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 16), + // Quality selector + qualityLabel.topAnchor.constraint(equalTo: statusLabel.bottomAnchor, constant: 24), + qualityLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + + qualitySegment.topAnchor.constraint(equalTo: qualityLabel.bottomAnchor, constant: 8), + qualitySegment.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + qualitySegment.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + qualityDescLabel.topAnchor.constraint(equalTo: qualitySegment.bottomAnchor, constant: 8), + qualityDescLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + qualityDescLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + + progressView.topAnchor.constraint(equalTo: qualityDescLabel.bottomAnchor, constant: 20), progressView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40), progressView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), progressView.heightAnchor.constraint(equalToConstant: 8), progressLabel.topAnchor.constraint(equalTo: progressView.bottomAnchor, constant: 8), progressLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + + backgroundInfoLabel.topAnchor.constraint(equalTo: progressLabel.bottomAnchor, constant: 12), + backgroundInfoLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + backgroundInfoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), - generateButton.topAnchor.constraint(equalTo: progressLabel.bottomAnchor, constant: 30), + generateButton.topAnchor.constraint(equalTo: backgroundInfoLabel.bottomAnchor, constant: 20), generateButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), generateButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40), generateButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), @@ -240,7 +405,7 @@ final class ObjectCapturePreviewController: UIViewController { exportButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), exportButton.heightAnchor.constraint(equalToConstant: 50), - retakeButton.topAnchor.constraint(equalTo: generateButton.bottomAnchor, constant: 12), + retakeButton.topAnchor.constraint(equalTo: exportButton.bottomAnchor, constant: 12), retakeButton.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), retakeButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 40), retakeButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -40), @@ -286,6 +451,7 @@ final class ObjectCapturePreviewController: UIViewController { generateButton.isEnabled = false retakeButton.isEnabled = false + qualitySegment.isEnabled = false activityIndicator.startAnimating() statusLabel.text = "🔄 Preparing photogrammetry session..." statusLabel.textColor = .label @@ -295,188 +461,88 @@ final class ObjectCapturePreviewController: UIViewController { progressView.progress = 0 progressLabel.text = "0%" + // Show cancel button and background info with animation + UIView.animate(withDuration: 0.3) { + self.backgroundInfoLabel.alpha = 1 + self.cancelButton.isHidden = false + } + // Haptic feedback let generator = UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() - // Generate filename with timestamp - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm" - let timestamp = dateFormatter.string(from: Date()) - - let outputURL = FileManager.default.temporaryDirectory - .appendingPathComponent("Furniture_\(timestamp).usdz") - - // Optimized configuration - var config = PhotogrammetrySession.Configuration() - config.sampleOrdering = .sequential - config.featureSensitivity = .high - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in + // Use the background processor for faster, background-capable processing + processor.startProcessing( + imagesFolder: imagesFolder, + detailLevel: selectedDetailLevel + ) { [weak self] result in guard let self = self else { return } - print(" Starting photogrammetry with \(self.photoCount) images") - print(" Input folder: \(self.imagesFolder.path)") - print(" Output: \(outputURL.path)") - - guard let session = try? PhotogrammetrySession( - input: self.imagesFolder, - configuration: config - ) else { - DispatchQueue.main.async { - self.handleError(message: "Failed to create photogrammetry session") - } - return - } - - let request = PhotogrammetrySession.Request.modelFile(url: outputURL) - var startTime = Date() - - Task { - do { - for try await output in session.outputs { - switch output { - - - - case .processingComplete: - print(" Processing complete!") - - case .inputComplete: - print(" Input complete") - - case .requestProgress(let request, let fraction): - let percentage = Int(fraction * 100) - - DispatchQueue.main.async { - self.progressView.setProgress(Float(fraction), animated: true) - self.progressLabel.text = "\(percentage)%" - - if percentage < 30 { - self.statusLabel.text = "🔍 Analyzing images..." - } else if percentage < 70 { - self.statusLabel.text = "🏗️ Building 3D mesh..." - } else if percentage < 95 { - self.statusLabel.text = "🎨 Applying textures..." - } else { - self.statusLabel.text = "✨ Finalizing model..." - } - - print("📊 Progress: \(percentage)%") - } - - case .requestComplete(let request, let result): - let elapsed = Date().timeIntervalSince(startTime) - print(" Request complete in \(String(format: "%.1f", elapsed))s") - - DispatchQueue.main.async { - self.progressView.progress = 1.0 - self.progressLabel.text = "100%" - self.statusLabel.text = "✓ 3D Model Generated!" - self.statusLabel.textColor = .systemGreen - self.saveModel(outputURL) - } - - case .requestError(let request, let error): - print(" Request error: \(error)") - DispatchQueue.main.async { - self.handleError(message: "Processing failed: \(error.localizedDescription)") - } - - case .processingCancelled: - print(" Processing cancelled") - DispatchQueue.main.async { - self.handleError(message: "Processing was cancelled") - } - - case .invalidSample(let id, let reason): - print(" Invalid sample \(id): \(reason)") - - case .skippedSample(let id): - print(" Skipped sample: \(id)") - - case .automaticDownsampling: - print(" Automatic downsampling applied") - default: - print(" Unknown output: \(output)") - } - } - } catch { - print(" Task error: \(error)") - DispatchQueue.main.async { - self.handleError(message: "An error occurred: \(error.localizedDescription)") - } - } + switch result { + case .success(let savedURL): + self.handleProcessingComplete(savedURL: savedURL) + + case .failure(let error): + self.handleError(message: error.localizedDescription) } - - do { - try session.process(requests: [request]) - } catch { - print(" Process error: \(error)") - DispatchQueue.main.async { - self.handleError(message: "Failed to start processing: \(error.localizedDescription)") - } + } + } + + // MARK: - Processing Complete Handler + private func handleProcessingComplete(savedURL: URL) { + activityIndicator.stopAnimating() + isProcessing = false + cancelButton.isHidden = true + + progressView.progress = 1.0 + progressLabel.text = "100%" + statusLabel.text = " Model Saved Successfully!" + statusLabel.textColor = .systemGreen + + // Success haptic + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + + // Animate success + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.8) { + self.generateButton.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + self.generateButton.backgroundColor = .systemGreen + } completion: { _ in + UIView.animate(withDuration: 0.3) { + self.generateButton.transform = .identity } } + + // Update button + generateButton.setTitle("View in My Models", for: .normal) + generateButton.isEnabled = true + qualitySegment.isEnabled = true + + print(" Model saved to: \(savedURL.path)") + + // Auto-navigate after 1.5 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + self.navigationController?.popToRootViewController(animated: true) + } } private func handleError(message: String) { isProcessing = false activityIndicator.stopAnimating() - statusLabel.text = "❌ \(message)" + cancelButton.isHidden = true + statusLabel.text = " \(message)" statusLabel.textColor = .systemRed generateButton.isEnabled = true retakeButton.isEnabled = true + qualitySegment.isEnabled = true progressView.isHidden = true progressLabel.isHidden = true + backgroundInfoLabel.alpha = 0 // Error haptic let generator = UINotificationFeedbackGenerator() generator.notificationOccurred(.error) } - - private func saveModel(_ url: URL) { - SaveManager.shared.saveModel(from: url, type: .furniture, customName: nil) { [weak self] result in - guard let self = self else { return } - - self.activityIndicator.stopAnimating() - self.isProcessing = false - - switch result { - case .success(let savedURL): - self.statusLabel.text = "✓ Model Saved Successfully!" - self.statusLabel.textColor = .systemGreen - - // Success haptic - let generator = UINotificationFeedbackGenerator() - generator.notificationOccurred(.success) - - // Animate success - UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.8) { - self.generateButton.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) - self.generateButton.backgroundColor = .systemGreen - } completion: { _ in - UIView.animate(withDuration: 0.3) { - self.generateButton.transform = .identity - } - } - - // Update button - self.generateButton.setTitle("View in My Models", for: .normal) - self.generateButton.isEnabled = true - - print(" Model saved to: \(savedURL.path)") - - // Auto-navigate after 1.5 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { - self.navigationController?.popToRootViewController(animated: true) - } - - case .failure(let error): - self.handleError(message: "Failed to save: \(error.localizedDescription)") - } - } - } } // MARK: - Collection View diff --git a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift index c187289..be95a06 100644 --- a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift +++ b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift @@ -304,9 +304,11 @@ Move the phone slowly around the object // MARK: - Auto Capture private func startAutoCapture() { - // Capture every 0.5 seconds for better overlap between photos - captureTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in - self.takePhoto() + // Capture every 0.4 seconds for good overlap between photos + // Faster capture = more photos = better reconstruction but longer processing + // 0.4s is a good balance for walking pace around object + captureTimer = Timer.scheduledTimer(withTimeInterval: 0.4, repeats: true) { [weak self] _ in + self?.takePhoto() } } diff --git a/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift b/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift index 31dd7ac..1d38f7f 100644 --- a/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift +++ b/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift @@ -150,9 +150,14 @@ func attachLabel(to entity: Entity, text: String, yOffset: Float = 0.1) { let textEntity = ModelEntity(mesh: mesh, materials: [material]) textEntity.position = [0, yOffset, 0] - textEntity.components.set(BillboardComponent()) - + entity.addChild(textEntity) + + // Set BillboardComponent after a brief delay to avoid crash + DispatchQueue.main.async { [weak textEntity] in + guard let textEntity = textEntity, textEntity.parent != nil else { return } + textEntity.components.set(BillboardComponent()) + } } // Replacement logic diff --git a/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift b/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift index d17a4e5..a67bd75 100644 --- a/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift +++ b/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift @@ -114,7 +114,41 @@ class VisualizeRoomViewController: UIViewController, UIDocumentPickerDelegate { let picker = UIColorPickerViewController() picker.delegate = self picker.supportsAlpha = true - present(picker, animated: true) + + // Wrap in navigation controller to add custom buttons + let navController = UINavigationController(rootViewController: picker) + navController.modalPresentationStyle = .pageSheet + + // Add native Cancel button (left side) + let cancelButton = UIBarButtonItem( + barButtonSystemItem: .cancel, + target: self, + action: #selector(cancelColorPicker) + ) + picker.navigationItem.leftBarButtonItem = cancelButton + + // Add native Done button (right side) + let doneButton = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(dismissColorPicker) + ) + picker.navigationItem.rightBarButtonItem = doneButton + picker.navigationItem.title = "Choose Color" + + present(navController, animated: true) + } + + @objc private func cancelColorPicker() { + // Restore original materials when cancelling + if let model = selectedModel, let original = originalMaterials[model] { + model.model?.materials = original + } + dismiss(animated: true) + } + + @objc private func dismissColorPicker() { + dismiss(animated: true) } // ---------------------------------------------------------- @@ -218,18 +252,41 @@ class VisualizeRoomViewController: UIViewController, UIDocumentPickerDelegate { // MARK: - Labels // ---------------------------------------------------------- func attachLabel(to entity: Entity, text: String, yOffset: Float) { - - labelStorage[entity]?.removeFromParent() + // Remove existing label safely + if let existingLabel = labelStorage[entity] { + existingLabel.components.remove(BillboardComponent.self) + existingLabel.removeFromParent() + labelStorage[entity] = nil + } let mesh = MeshResource.generateText(text, extrusionDepth: 0.01, font: .systemFont(ofSize: 0.15)) let labelEntity = ModelEntity(mesh: mesh, materials: [SimpleMaterial(color: .white, isMetallic: false)]) labelEntity.position = [0, yOffset, 0] - labelEntity.components.set(BillboardComponent()) labelEntity.isEnabled = showLabels entity.addChild(labelEntity) labelStorage[entity] = labelEntity + + // Set BillboardComponent after a brief delay to avoid crash + DispatchQueue.main.async { [weak labelEntity] in + guard let labelEntity = labelEntity, labelEntity.parent != nil else { return } + labelEntity.components.set(BillboardComponent()) + } + } + + // MARK: - Cleanup + private func cleanupLabels() { + for (_, label) in labelStorage { + label.components.remove(BillboardComponent.self) + label.removeFromParent() + } + labelStorage.removeAll() + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + cleanupLabels() } } diff --git a/Envision/Screens/MainTabs/profile/ProfileViewController.swift b/Envision/Screens/MainTabs/profile/ProfileViewController.swift index 760609d..e9f13c5 100644 --- a/Envision/Screens/MainTabs/profile/ProfileViewController.swift +++ b/Envision/Screens/MainTabs/profile/ProfileViewController.swift @@ -4,7 +4,6 @@ // import UIKit -import TipKit class ProfileViewController: UIViewController { diff --git a/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift b/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift index 65a331f..56cea86 100644 --- a/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift +++ b/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift @@ -4,8 +4,6 @@ // import UIKit -import SwiftUI -import TipKit final class TipsLibraryViewController: UIViewController { diff --git a/Envision/Tips/AppTips.swift b/Envision/Tips/AppTips.swift index a8ea198..30df18f 100644 --- a/Envision/Tips/AppTips.swift +++ b/Envision/Tips/AppTips.swift @@ -2,679 +2,10 @@ // AppTips.swift // Envision // -// Created for EnVision Tips & Tour System -// Version: 1.0 +// Tips are temporarily removed from the runtime app flow. +// This file is kept as a placeholder so the project structure remains intact. // import Foundation -import TipKit -import SwiftUI -// MARK: - Welcome & Onboarding Tips - -@available(iOS 17.0, *) -struct WelcomeTip: Tip { - var title: Text { - Text("Welcome to EnVision! 🎉") - } - - var message: Text? { - Text("Let's take a quick tour to show you how to scan rooms and furniture, then visualize them in AR.") - } - - var image: Image? { - Image(systemName: "hand.wave.fill") - } - - var actions: [Action] { - [ - Action(id: "start-tour", title: "Start Tour"), - Action(id: "skip", title: "Skip for now") - ] - } -} - -// MARK: - My Rooms Tips - -@available(iOS 17.0, *) -struct MyRoomsIntroTip: Tip { - - @Parameter - static var hasRooms: Bool = false - - var title: Text { - Text("📐 Scan Your First Room") - } - - var message: Text? { - Text("Tap the green camera button to start scanning any room using your iPhone's LiDAR sensor. We'll create a precise 3D model!") - } - - var image: Image? { - Image(systemName: "camera.viewfinder") - } - - var actions: [Action] { - [ - Action(id: "scan", title: "Scan Now"), - Action(id: "later", title: "Maybe Later") - ] - } - - var rules: [Rule] { - [ - #Rule(Self.$hasRooms) { $0 == false } - ] - } -} - -@available(iOS 17.0, *) -struct RoomImportTip: Tip { - - @Parameter - static var hasScannedRoom: Bool = false - - var title: Text { - Text("📥 Already Have 3D Models?") - } - - var message: Text? { - Text("Tap the blue import button to bring in existing USDZ room models from your Files app.") - } - - var image: Image? { - Image(systemName: "square.and.arrow.down") - } - - var rules: [Rule] { - [ - #Rule(Self.$hasScannedRoom) { $0 == true } - ] - } -} - -@available(iOS 17.0, *) -struct RoomActionsMenuTip: Tip { - - @Parameter - static var roomCount: Int = 0 - - var title: Text { - Text("⚡ More Options") - } - - var message: Text? { - Text("Tap the three-dot menu for bulk actions like selecting multiple rooms or visualizing furniture in AR.") - } - - var image: Image? { - Image(systemName: "ellipsis.circle") - } - - var rules: [Rule] { - [ - #Rule(Self.$roomCount) { $0 >= 1 } - ] - } -} - -@available(iOS 17.0, *) -struct RoomCategoriesTip: Tip { - - @Parameter - static var roomCount: Int = 0 - - var title: Text { - Text("🏷️ Organize Your Spaces") - } - - var message: Text? { - Text("Use category chips to filter rooms by type. Long-press any room to edit its category and add custom tags.") - } - - var image: Image? { - Image(systemName: "tag.fill") - } - - var rules: [Rule] { - [ - #Rule(Self.$roomCount) { $0 >= 2 } - ] - } -} - -// MARK: - Room Scanning Tips - -@available(iOS 17.0, *) -struct RoomScanningTip: Tip { - var title: Text { - Text("🐢 Scan Slowly for Best Results") - } - - var message: Text? { - Text("Move your device slowly and smoothly around the room. The slower you move, the more accurate your 3D model will be. Aim for good lighting too!") - } - - var image: Image? { - Image(systemName: "tortoise.fill") - } -} - -@available(iOS 17.0, *) -struct RoomCompleteTip: Tip { - var title: Text { - Text("✅ Complete Coverage") - } - - var message: Text? { - Text("Make sure to scan all corners, walls, and furniture. Walk around the entire perimeter at least once. The 'Save' button will appear when enough data is captured.") - } - - var image: Image? { - Image(systemName: "checkmark.circle.fill") - } -} - -@available(iOS 17.0, *) -struct RoomPreviewTip: Tip { - var title: Text { - Text("👀 Review Before Saving") - } - - var message: Text? { - Text("Give your room a descriptive name and select the right category. You can always edit these details later!") - } - - var image: Image? { - Image(systemName: "pencil.circle") - } -} - -// MARK: - My Furniture Tips - -@available(iOS 17.0, *) -struct FurnitureIntroTip: Tip { - - @Parameter - static var hasFurniture: Bool = false - - var title: Text { - Text("🪑 Capture Any Furniture") - } - - var message: Text? { - Text("Tap the green camera to scan furniture in two ways: Automatic capture (walk around) or manual photo import. Let's try it!") - } - - var image: Image? { - Image(systemName: "camera.metering.center.weighted") - } - - var actions: [Action] { - [ - Action(id: "scan", title: "Scan Furniture"), - Action(id: "later", title: "Later") - ] - } - - var rules: [Rule] { - [ - #Rule(Self.$hasFurniture) { $0 == false } - ] - } -} - -@available(iOS 17.0, *) -struct FurnitureCaptureMethodsTip: Tip { - var title: Text { - Text("📸 Two Capture Methods") - } - - var message: Text? { - Text("**Automatic**: Walk around the object while the app auto-captures photos.\n**From Photos**: Import 20+ photos you've already taken.") - } - - var image: Image? { - Image(systemName: "photo.stack") - } -} - -@available(iOS 17.0, *) -struct FurnitureCategoriesTip: Tip { - - @Parameter - static var furnitureCount: Int = 0 - - var title: Text { - Text("📂 Smart Categories") - } - - var message: Text? { - Text("Filter furniture by category: Chairs, Tables, Beds, and more. Categories make it easy to find specific items later!") - } - - var image: Image? { - Image(systemName: "square.grid.2x2") - } - - var rules: [Rule] { - [ - #Rule(Self.$furnitureCount) { $0 >= 2 } - ] - } -} - -// MARK: - Object Capture Tips - -@available(iOS 17.0, *) -struct ObjectCaptureStartTip: Tip { - var title: Text { - Text("🔄 360° Coverage is Key") - } - - var message: Text? { - Text("Walk slowly in a complete circle around the furniture. Capture from multiple heights: low, medium, and high angles. More photos = better quality!") - } - - var image: Image? { - Image(systemName: "arrow.triangle.2.circlepath.camera") - } -} - -@available(iOS 17.0, *) -struct ObjectCapturePhotoCountTip: Tip { - - @Parameter - static var photoCount: Int = 0 - - var title: Text { - Text("📊 Quality Indicator") - } - - var message: Text? { - Text("Watch the photo counter and quality bar at the top. Aim for 40+ photos for excellent results. The color changes from yellow → orange → green as quality improves.") - } - - var image: Image? { - Image(systemName: "chart.bar.fill") - } - - var rules: [Rule] { - [ - #Rule(Self.$photoCount) { $0 < 20 } - ] - } -} - -@available(iOS 17.0, *) -struct ObjectCaptureLightingTip: Tip { - var title: Text { - Text("💡 Good Lighting = Great Models") - } - - var message: Text? { - Text("Scan in well-lit areas with even lighting. Use the flashlight toggle if needed, but natural light works best. Avoid harsh shadows!") - } - - var image: Image? { - Image(systemName: "sun.max.fill") - } -} - -@available(iOS 17.0, *) -struct ObjectCaptureProcessingTip: Tip { - var title: Text { - Text("⚙️ Processing Your Model") - } - - var message: Text? { - Text("Photogrammetry can take 30 seconds to 2 minutes depending on photo count and device performance. Grab a coffee! ☕") - } - - var image: Image? { - Image(systemName: "gearshape.2.fill") - } -} - -// MARK: - AR Visualization Tips - -@available(iOS 17.0, *) -struct ARPlacementTip: Tip { - var title: Text { - Text("🎯 Place in Real Space") - } - - var message: Text? { - Text("Move your device slowly to detect surfaces. Once you see the grid overlay, tap to place your furniture. Pinch to scale, rotate with two fingers!") - } - - var image: Image? { - Image(systemName: "viewfinder") - } -} - -@available(iOS 17.0, *) -struct ARControlsTip: Tip { - var title: Text { - Text("🕹️ Master AR Controls") - } - - var message: Text? { - Text("**Joystick**: Move furniture left/right/forward/back\n**Height Slider**: Adjust vertical position\n**Rotation Slider**: Turn the object\n**+/- Buttons**: Scale up or down") - } - - var image: Image? { - Image(systemName: "gamecontroller.fill") - } -} - -@available(iOS 17.0, *) -struct RoomVisualizationTip: Tip { - - @Parameter - static var hasRoomAndFurniture: Bool = false - - var title: Text { - Text("🏠 Visualize Furniture in Rooms") - } - - var message: Text? { - Text("Go to My Rooms → Menu → 'Visualize furniture' to place your scanned furniture inside scanned rooms. See how it fits before buying!") - } - - var image: Image? { - Image(systemName: "house.and.flag.fill") - } - - var actions: [Action] { - [ - Action(id: "try-now", title: "Try It Now") - ] - } - - var rules: [Rule] { - [ - #Rule(Self.$hasRoomAndFurniture) { $0 == true } - ] - } -} - -// MARK: - Profile & Settings Tips - -@available(iOS 17.0, *) -struct ProfileCustomizationTip: Tip { - var title: Text { - Text("⚙️ Customize Your Experience") - } - - var message: Text? { - Text("Head to your Profile to change themes (Light/Dark), manage notifications, and control privacy settings. Make EnVision truly yours!") - } - - var image: Image? { - Image(systemName: "person.crop.circle.badge.checkmark") - } -} - -@available(iOS 17.0, *) -struct ThemeTip: Tip { - var title: Text { - Text("🌓 Choose Your Theme") - } - - var message: Text? { - Text("Tap 'Appearance' to switch between Light, Dark, or System mode. The app will instantly update with smooth animations.") - } - - var image: Image? { - Image(systemName: "moon.stars.fill") - } -} - -// MARK: - Advanced Features Tips - -@available(iOS 17.0, *) -struct GeometryPlaygroundTip: Tip { - - @Parameter - static var furnitureCount: Int = 0 - - var title: Text { - Text("🎨 Geometry Playground") - } - - var message: Text? { - Text("Advanced users: Try the 'Room Geometry Playground' to visualize and color-code room elements (walls, floors, doors). Great for understanding room structure!") - } - - var image: Image? { - Image(systemName: "cube.transparent") - } - - var rules: [Rule] { - [ - #Rule(Self.$furnitureCount) { $0 >= 3 } - ] - } -} - -@available(iOS 17.0, *) -struct ShareExportTip: Tip { - var title: Text { - Text("📤 Share Your Creations") - } - - var message: Text? { - Text("Long-press any room or furniture model to share it via Messages, AirDrop, or save to Files. Your 3D models are truly yours!") - } - - var image: Image? { - Image(systemName: "square.and.arrow.up") - } -} - -@available(iOS 17.0, *) -struct RulerMeasurementTip: Tip { - var title: Text { - Text("📏 Measure Distances") - } - - var message: Text? { - Text("Tap the ruler icon to measure distances in your 3D room. Tap two points to see the exact distance between them!") - } - - var image: Image? { - Image(systemName: "ruler") - } -} - -// MARK: - Tour Completion Tip - -@available(iOS 17.0, *) -struct TourCompleteTip: Tip { - var title: Text { - Text("🎓 You're All Set!") - } - - var message: Text? { - Text("You've learned the basics of EnVision! Start scanning to build your 3D furniture library. Tips will continue to appear as you explore more features.") - } - - var image: Image? { - Image(systemName: "checkmark.seal.fill") - } - - var actions: [Action] { - [ - Action(id: "finish", title: "Start Using EnVision") - ] - } -} - -// MARK: - Tip Categories for Tips Library - -@available(iOS 17.0, *) -enum TipCategory: String, CaseIterable { - case gettingStarted = "Getting Started" - case roomScanning = "Room Scanning" - case furnitureCapture = "Furniture Capture" - case arVisualization = "AR Visualization" - case advanced = "Advanced Features" - - var icon: String { - switch self { - case .gettingStarted: return "hand.wave.fill" - case .roomScanning: return "house.fill" - case .furnitureCapture: return "chair.fill" - case .arVisualization: return "arkit" - case .advanced: return "sparkles" - } - } - - var color: UIColor { - switch self { - case .gettingStarted: return .systemBlue - case .roomScanning: return .systemGreen - case .furnitureCapture: return .systemOrange - case .arVisualization: return .systemPurple - case .advanced: return .systemPink - } - } -} - -// MARK: - Tips Library Data - -@available(iOS 17.0, *) -struct TipInfo { - let title: String - let message: String - let icon: String - let category: TipCategory -} - -@available(iOS 17.0, *) -struct TipsLibrary { - static let allTips: [TipInfo] = [ - // Getting Started - TipInfo( - title: "Welcome to EnVision", - message: "EnVision helps you scan rooms and furniture, then visualize them in AR to see how they fit in your space.", - icon: "hand.wave.fill", - category: .gettingStarted - ), - TipInfo( - title: "LiDAR Scanning", - message: "Your device's LiDAR sensor creates precise 3D models of rooms. iPhone 12 Pro and newer, or iPad Pro models support this feature.", - icon: "sensor.fill", - category: .gettingStarted - ), - - // Room Scanning - TipInfo( - title: "Scan Your First Room", - message: "Go to My Rooms tab and tap the camera icon. Walk slowly around the room while holding your device steady.", - icon: "camera.viewfinder", - category: .roomScanning - ), - TipInfo( - title: "Scanning Best Practices", - message: "Scan in good lighting, move slowly, and capture all walls and corners. The more complete your scan, the better your 3D model.", - icon: "lightbulb.fill", - category: .roomScanning - ), - TipInfo( - title: "Room Categories", - message: "Organize rooms by category (Living Room, Bedroom, etc.) to easily find them later. Long-press any room to change its category.", - icon: "tag.fill", - category: .roomScanning - ), - TipInfo( - title: "Import USDZ Models", - message: "Already have 3D room models? Tap the import button to bring in USDZ files from your device or cloud storage.", - icon: "square.and.arrow.down", - category: .roomScanning - ), - - // Furniture Capture - TipInfo( - title: "Capture Furniture", - message: "Use the camera in the My Furniture tab to capture any piece of furniture. Walk around the object for best results.", - icon: "camera.metering.center.weighted", - category: .furnitureCapture - ), - TipInfo( - title: "Automatic vs Manual Capture", - message: "Automatic mode captures photos as you walk around. Manual mode lets you import photos you've already taken.", - icon: "photo.stack", - category: .furnitureCapture - ), - TipInfo( - title: "Photo Count Matters", - message: "More photos = better 3D models. Aim for 40+ photos from different angles for optimal quality.", - icon: "number.circle.fill", - category: .furnitureCapture - ), - TipInfo( - title: "Lighting Tips", - message: "Capture in even, natural lighting. Avoid harsh shadows and reflective surfaces for best results.", - icon: "sun.max.fill", - category: .furnitureCapture - ), - - // AR Visualization - TipInfo( - title: "Place Furniture in AR", - message: "View any furniture in AR by tapping it and selecting 'View in AR'. Move your device to find a flat surface, then tap to place.", - icon: "arkit", - category: .arVisualization - ), - TipInfo( - title: "AR Controls", - message: "Use pinch to scale, two fingers to rotate, and drag to move furniture in AR mode.", - icon: "hand.draw.fill", - category: .arVisualization - ), - TipInfo( - title: "Furniture in Rooms", - message: "Place your captured furniture inside scanned rooms to see how it fits before buying!", - icon: "house.and.flag.fill", - category: .arVisualization - ), - TipInfo( - title: "Measure Distances", - message: "Use the ruler tool to measure distances between points in your 3D room model.", - icon: "ruler", - category: .arVisualization - ), - - // Advanced Features - TipInfo( - title: "Geometry Playground", - message: "Advanced visualization mode that color-codes room elements like walls, floors, and doors.", - icon: "cube.transparent", - category: .advanced - ), - TipInfo( - title: "Customize Colors", - message: "In room edit mode, change the colors of walls, floors, and other elements to visualize different designs.", - icon: "paintpalette.fill", - category: .advanced - ), - TipInfo( - title: "Share Your Models", - message: "Export and share your 3D models via AirDrop, Messages, or save to Files for use in other apps.", - icon: "square.and.arrow.up", - category: .advanced - ), - TipInfo( - title: "Theme Options", - message: "Switch between Light, Dark, or System theme in Profile > Appearance.", - icon: "moon.stars.fill", - category: .advanced - ) - ] - - static func tips(for category: TipCategory) -> [TipInfo] { - allTips.filter { $0.category == category } - } -} +// Tip definitions removed for now. diff --git a/Envision/Tips/TipPresenter.swift b/Envision/Tips/TipPresenter.swift new file mode 100644 index 0000000..186f292 --- /dev/null +++ b/Envision/Tips/TipPresenter.swift @@ -0,0 +1,251 @@ +import UIKit + +/// Pure UIKit tip presenter (no SwiftUI/UIHostingController). +/// +/// This renders a lightweight banner that matches iOS styling (blur + rounded corners), +/// supports multiple buttons, and never installs full-screen overlays. +/// +/// Note: The app-wide tips/tour integration is temporarily disabled. +final class TipPresenter { + + // MARK: - Types + + struct Action { + let id: String + let title: String + } + + // MARK: - Private + + private weak var owner: UIViewController? + private weak var containerView: UIView? + + private var bannerView: TipBannerView? + private var heightConstraint: NSLayoutConstraint? + + init(owner: UIViewController, containerView: UIView) { + self.owner = owner + self.containerView = containerView + } + + func dismiss() { + bannerView?.removeFromSuperview() + bannerView = nil + + heightConstraint?.isActive = false + heightConstraint = nil + + containerView?.isHidden = true + owner?.view.setNeedsLayout() + } + + /// Presents a UIKit banner representing the provided Tip. + /// Note: We intentionally don't attempt to mirror TipKit's rule evaluation UI. + /// The calling screen decides *which* tip to show; this class only renders it. + @discardableResult + func present( + title: String, + message: String?, + systemImageName: String?, + actions: [Action], + onAction: @escaping (String) -> Void + ) -> CGFloat { + guard let owner, let containerView else { return 0 } + + dismiss() + + let banner = TipBannerView() + banner.translatesAutoresizingMaskIntoConstraints = false + banner.configure( + title: title, + message: message, + systemImageName: systemImageName, + actions: actions, + onAction: onAction + ) + + containerView.addSubview(banner) + NSLayoutConstraint.activate([ + banner.topAnchor.constraint(equalTo: containerView.topAnchor), + banner.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + banner.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + banner.bottomAnchor.constraint(equalTo: containerView.bottomAnchor) + ]) + + bannerView = banner + containerView.isHidden = false + + owner.view.layoutIfNeeded() + + // Measure via Auto Layout and lock height to keep layout stable. + let width = max(1, containerView.bounds.width) + let measured = containerView.systemLayoutSizeFitting( + CGSize(width: width, height: UIView.layoutFittingCompressedSize.height), + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel + ).height + + heightConstraint?.isActive = false + let hc = containerView.heightAnchor.constraint(equalToConstant: max(0, measured)) + hc.priority = .required + hc.isActive = true + heightConstraint = hc + + owner.view.layoutIfNeeded() + return measured + } +} + +// MARK: - TipBannerView + +private final class TipBannerView: UIView { + + private let backgroundView: UIVisualEffectView = { + let v = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) + v.translatesAutoresizingMaskIntoConstraints = false + v.clipsToBounds = true + v.layer.cornerRadius = 16 + return v + }() + + private let iconView: UIImageView = { + let iv = UIImageView() + iv.translatesAutoresizingMaskIntoConstraints = false + iv.contentMode = .scaleAspectFit + iv.tintColor = .label + return iv + }() + + private let titleLabel: UILabel = { + let l = UILabel() + l.translatesAutoresizingMaskIntoConstraints = false + l.font = .systemFont(ofSize: 16, weight: .semibold) + l.textColor = .label + l.numberOfLines = 0 + return l + }() + + private let messageLabel: UILabel = { + let l = UILabel() + l.translatesAutoresizingMaskIntoConstraints = false + l.font = .systemFont(ofSize: 14) + l.textColor = .secondaryLabel + l.numberOfLines = 0 + return l + }() + + private let buttonStack: UIStackView = { + let s = UIStackView() + s.translatesAutoresizingMaskIntoConstraints = false + s.axis = .horizontal + s.alignment = .fill + s.distribution = .fillEqually + s.spacing = 10 + return s + }() + + private var actionHandler: ((String) -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + translatesAutoresizingMaskIntoConstraints = false + isUserInteractionEnabled = true + + addSubview(backgroundView) + + NSLayoutConstraint.activate([ + backgroundView.topAnchor.constraint(equalTo: topAnchor), + backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), + backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), + backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + + let content = backgroundView.contentView + + content.addSubview(iconView) + content.addSubview(titleLabel) + content.addSubview(messageLabel) + content.addSubview(buttonStack) + + NSLayoutConstraint.activate([ + iconView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 14), + iconView.topAnchor.constraint(equalTo: content.topAnchor, constant: 14), + iconView.widthAnchor.constraint(equalToConstant: 22), + iconView.heightAnchor.constraint(equalToConstant: 22), + + titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: 10), + titleLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -14), + titleLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14), + + messageLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + messageLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + + buttonStack.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + buttonStack.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + buttonStack.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 12), + buttonStack.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -14), + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: 34) + ]) + } + + func configure( + title: String, + message: String?, + systemImageName: String?, + actions: [TipPresenter.Action], + onAction: @escaping (String) -> Void + ) { + self.actionHandler = onAction + + titleLabel.text = title + messageLabel.text = message + messageLabel.isHidden = (message?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true) + + if let systemImageName { + iconView.image = UIImage(systemName: systemImageName) + iconView.isHidden = false + } else { + iconView.isHidden = true + } + + // Rebuild buttons + buttonStack.arrangedSubviews.forEach { v in + buttonStack.removeArrangedSubview(v) + v.removeFromSuperview() + } + + for action in actions { + let b = UIButton(type: .system) + b.translatesAutoresizingMaskIntoConstraints = false + + var config = UIButton.Configuration.filled() + config.title = action.title + config.baseBackgroundColor = .tertiarySystemFill + config.baseForegroundColor = .label + config.cornerStyle = .medium + config.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 12, bottom: 8, trailing: 12) + b.configuration = config + + b.accessibilityIdentifier = action.id + b.addTarget(self, action: #selector(actionTapped(_:)), for: .touchUpInside) + buttonStack.addArrangedSubview(b) + } + + // If no actions, collapse the stack. + buttonStack.isHidden = actions.isEmpty + } + + @objc private func actionTapped(_ sender: UIButton) { + guard let id = sender.accessibilityIdentifier else { return } + actionHandler?(id) + } +} diff --git a/FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md b/FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..171d0d1 --- /dev/null +++ b/FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1489 @@ +# EnVision - Firebase Backend Implementation Plan +*Comprehensive Guide for Backend Integration* + +--- + +## Table of Contents +1. [Overview](#1-overview) +2. [Firebase Setup](#2-firebase-setup) +3. [Authentication System](#3-authentication-system) +4. [Firestore Database Design](#4-firestore-database-design) +5. [Firebase Storage Structure](#5-firebase-storage-structure) +6. [Code Implementation](#6-code-implementation) +7. [Security Rules](#7-security-rules) +8. [Migration Strategy](#8-migration-strategy) +9. [Testing Plan](#9-testing-plan) +10. [Deployment Checklist](#10-deployment-checklist) + +--- + +## 1. Overview + +### 1.1 Goals +- **User Authentication**: Email/password login with password reset +- **Cloud Sync**: Sync user data, rooms, and furniture across devices +- **Cloud Storage**: Store profile pictures and optionally USDZ files +- **Offline Support**: Cache data locally for offline access +- **Security**: User data isolated with Firestore security rules + +### 1.2 Firebase Services to Use +- **Firebase Authentication**: Email/Password, (future: Apple Sign-In, Google Sign-In) +- **Cloud Firestore**: NoSQL database for user profiles, rooms, furniture metadata +- **Firebase Storage**: Blob storage for images and 3D models +- **Firebase Analytics** (optional): Track user engagement +- **Crashlytics** (optional): Monitor app stability + +### 1.3 Architecture Overview +``` +iOS App (UIKit) + ↓ +FirebaseManager (Singleton) + ├── AuthManager (Firebase Auth) + ├── FirestoreManager (Firestore CRUD) + └── StorageManager (Firebase Storage) + ↓ +Local Cache (UserDefaults + FileManager) + ↓ +UI Updates (via completion handlers / delegates) +``` + +--- + +## 2. Firebase Setup + +### 2.1 Create Firebase Project + +**Steps**: +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Click **"Add project"** +3. Name: `EnVision-Production` (or your preferred name) +4. Enable Google Analytics (optional but recommended) +5. Create project (takes ~30 seconds) + +### 2.2 Add iOS App to Firebase + +**Steps**: +1. In Firebase Console, click **"Add app"** → iOS +2. **iOS bundle ID**: `com.yourcompany.EnVision` (must match Xcode project) +3. **App nickname**: `EnVision iOS` +4. **App Store ID**: (leave blank for now, add later) +5. Download `GoogleService-Info.plist` +6. **Important**: Add `GoogleService-Info.plist` to Xcode: + - Drag into Xcode project navigator + - **Ensure "Copy items if needed" is checked** + - **Target membership**: Envision ✅ + +### 2.3 Add Firebase SDK via Swift Package Manager + +**Steps**: +1. In Xcode: **File → Add Package Dependencies...** +2. URL: `https://github.com/firebase/firebase-ios-sdk` +3. Version: **10.20.0** or latest +4. Select packages to add: + - ✅ `FirebaseAuth` + - ✅ `FirebaseFirestore` + - ✅ `FirebaseStorage` + - ✅ `FirebaseAnalytics` (optional) + - ✅ `FirebaseCrashlytics` (optional) +5. Click **"Add Package"** +6. Wait for SPM to resolve dependencies (~2-3 minutes) + +### 2.4 Configure Firebase in AppDelegate + +**File**: `Envision/AppDelegate.swift` + +```swift +import UIKit +import FirebaseCore + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // Configure Firebase FIRST (before any Firebase calls) + FirebaseApp.configure() + print("✅ Firebase configured successfully") + + // ... existing theme setup code ... + + return true + } +} +``` + +### 2.5 Verify Installation + +**Quick Test** (add to `SceneDelegate.willConnectTo` temporarily): +```swift +import FirebaseAuth + +// In willConnectTo, after window setup +if let currentUser = Auth.auth().currentUser { + print("✅ Firebase Auth works! User: \(currentUser.uid)") +} else { + print("✅ Firebase Auth works! No user logged in") +} +``` + +--- + +## 3. Authentication System + +### 3.1 Create AuthManager + +**File**: `Envision/Managers/AuthManager.swift` + +```swift +import Foundation +import FirebaseAuth + +final class AuthManager { + static let shared = AuthManager() + + private init() {} + + // MARK: - Current User + + var currentUser: User? { + return Auth.auth().currentUser + } + + var isLoggedIn: Bool { + return currentUser != nil + } + + var currentUserID: String? { + return currentUser?.uid + } + + // MARK: - Sign Up + + func signUp(email: String, password: String, name: String, completion: @escaping (Result) -> Void) { + Auth.auth().createUser(withEmail: email, password: password) { [weak self] result, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let user = result?.user else { + completion(.failure(NSError(domain: "AuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "User creation failed"]))) + return + } + + // Update display name + let changeRequest = user.createProfileChangeRequest() + changeRequest.displayName = name + changeRequest.commitChanges { error in + if let error = error { + print("⚠️ Failed to set display name: \(error)") + } + } + + // Create user document in Firestore + FirestoreManager.shared.createUserDocument(uid: user.uid, email: email, name: name) { result in + switch result { + case .success: + completion(.success(user)) + case .failure(let error): + print("⚠️ Failed to create user document: \(error)") + // Still return success (user created, just doc failed) + completion(.success(user)) + } + } + } + } + + // MARK: - Sign In + + func signIn(email: String, password: String, completion: @escaping (Result) -> Void) { + Auth.auth().signIn(withEmail: email, password: password) { result, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let user = result?.user else { + completion(.failure(NSError(domain: "AuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign in failed"]))) + return + } + + completion(.success(user)) + } + } + + // MARK: - Sign Out + + func signOut(completion: @escaping (Result) -> Void) { + do { + try Auth.auth().signOut() + + // Clear local user data + UserManager.shared.logout() + + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + + // MARK: - Password Reset + + func sendPasswordReset(email: String, completion: @escaping (Result) -> Void) { + Auth.auth().sendPasswordReset(withEmail: email) { error in + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } + } + } + + // MARK: - Delete Account + + func deleteAccount(completion: @escaping (Result) -> Void) { + guard let user = currentUser else { + completion(.failure(NSError(domain: "AuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"]))) + return + } + + let uid = user.uid + + // Delete Firestore data first + FirestoreManager.shared.deleteUserData(uid: uid) { result in + switch result { + case .success: + // Then delete auth account + user.delete { error in + if let error = error { + completion(.failure(error)) + } else { + UserManager.shared.logout() + completion(.success(())) + } + } + case .failure(let error): + completion(.failure(error)) + } + } + } + + // MARK: - Re-authentication (for sensitive operations) + + func reauthenticate(password: String, completion: @escaping (Result) -> Void) { + guard let user = currentUser, let email = user.email else { + completion(.failure(NSError(domain: "AuthManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "No user logged in"]))) + return + } + + let credential = EmailAuthProvider.credential(withEmail: email, password: password) + + user.reauthenticate(with: credential) { _, error in + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } + } + } +} +``` + +### 3.2 Update UserManager to Use Firebase + +**File**: `Envision/Extensions/UserManager.swift` + +```swift +import Foundation +import FirebaseAuth + +final class UserManager { + static let shared = UserManager() + + // MARK: - Local Cache (for offline access) + + var currentUser: UserModel? { + get { + guard let data = UserDefaults.standard.data(forKey: "currentUser"), + let user = try? JSONDecoder().decode(UserModel.self, from: data) else { + return nil + } + return user + } + set { + if let user = newValue { + let data = try? JSONEncoder().encode(user) + UserDefaults.standard.set(data, forKey: "currentUser") + } else { + UserDefaults.standard.removeObject(forKey: "currentUser") + } + } + } + + var isLoggedIn: Bool { + return AuthManager.shared.isLoggedIn && currentUser != nil + } + + // MARK: - Login (Firebase) + + func login(email: String, password: String, completion: @escaping (Bool) -> Void) { + AuthManager.shared.signIn(email: email, password: password) { [weak self] result in + switch result { + case .success(let user): + // Fetch user profile from Firestore + FirestoreManager.shared.fetchUserDocument(uid: user.uid) { fetchResult in + switch fetchResult { + case .success(let userModel): + self?.currentUser = userModel + completion(true) + case .failure(let error): + print("⚠️ Failed to fetch user profile: \(error)") + // Create minimal local user + let userModel = UserModel( + id: user.uid, + name: user.displayName ?? "User", + email: user.email ?? email, + createdAt: Date(), + preferences: UserPreferences() + ) + self?.currentUser = userModel + completion(true) + } + } + case .failure(let error): + print("❌ Login failed: \(error.localizedDescription)") + completion(false) + } + } + } + + // MARK: - Signup (Firebase) + + func signup(name: String, email: String, password: String, completion: @escaping (Bool) -> Void) { + AuthManager.shared.signUp(email: email, password: password, name: name) { [weak self] result in + switch result { + case .success(let user): + // Create local user model + let userModel = UserModel( + id: user.uid, + name: name, + email: email, + createdAt: Date(), + preferences: UserPreferences() + ) + self?.currentUser = userModel + completion(true) + case .failure(let error): + print("❌ Signup failed: \(error.localizedDescription)") + completion(false) + } + } + } + + // MARK: - Logout + + func logout() { + AuthManager.shared.signOut { _ in } + currentUser = nil + + // Clear other local data if needed + TourManager.shared.resetTour() + } + + // MARK: - Update Profile + + func updateProfile(name: String? = nil, bio: String? = nil, completion: @escaping (Bool) -> Void) { + guard var user = currentUser else { + completion(false) + return + } + + if let name = name { + user.name = name + } + if let bio = bio { + user.bio = bio + } + + // Update Firestore + FirestoreManager.shared.updateUserDocument(uid: user.id, data: [ + "name": user.name, + "bio": user.bio ?? "" + ]) { [weak self] result in + switch result { + case .success: + self?.currentUser = user + completion(true) + case .failure(let error): + print("❌ Failed to update profile: \(error)") + completion(false) + } + } + } + + // MARK: - Update Preferences + + func updatePreferences(_ preferences: UserPreferences, completion: @escaping (Bool) -> Void) { + guard var user = currentUser else { + completion(false) + return + } + + user.preferences = preferences + + // Update Firestore + FirestoreManager.shared.updateUserDocument(uid: user.id, data: [ + "preferences": [ + "notificationsEnabled": preferences.notificationsEnabled, + "scanReminders": preferences.scanReminders, + "newFeatureAlerts": preferences.newFeatureAlerts, + "theme": preferences.theme + ] + ]) { [weak self] result in + switch result { + case .success: + self?.currentUser = user + completion(true) + case .failure(let error): + print("❌ Failed to update preferences: \(error)") + completion(false) + } + } + } +} +``` + +### 3.3 Update LoginViewController + +**File**: `Envision/Screens/Onboarding/LoginViewController.swift` + +```swift +// Update handleLogin method + +@objc private func handleLogin() { + guard let email = emailField.text, !email.isEmpty, + let password = passwordField.text, !password.isEmpty else { + showError("Please fill in all fields") + return + } + + guard email.isValidEmail else { + showError("Please enter a valid email address") + return + } + + // Show loading + continueButton.isEnabled = false + continueButton.setTitle("Signing In...", for: .normal) + + // Firebase login + UserManager.shared.login(email: email, password: password) { [weak self] success in + DispatchQueue.main.async { + self?.continueButton.isEnabled = true + self?.continueButton.setTitle("Continue", for: .normal) + + if success { + // Navigate to main app + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + sceneDelegate.switchToMainApp() + } else { + // Fallback + let mainVC = MainTabBarController() + mainVC.modalPresentationStyle = .fullScreen + self?.present(mainVC, animated: true) + } + } else { + self?.showError("Invalid email or password") + } + } + } +} + +private func showError(_ message: String) { + let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + present(alert, animated: true) +} +``` + +### 3.4 Update SignupViewController + +**File**: `Envision/Screens/Onboarding/SignupViewController.swift` + +```swift +// Update handleSignup method + +@objc private func handleSignup() { + guard let name = nameField.text, !name.isEmpty, + let email = emailField.text, !email.isEmpty, + let password = passwordField.text, !password.isEmpty, + let confirmPassword = confirmPasswordField.text, !confirmPassword.isEmpty else { + showError("Please fill in all fields") + return + } + + guard email.isValidEmail else { + showError("Please enter a valid email address") + return + } + + guard password.isStrongPassword else { + showError("Password must be at least 8 characters with uppercase, lowercase, and numbers") + return + } + + guard password == confirmPassword else { + showError("Passwords do not match") + return + } + + // Show loading + signupButton.isEnabled = false + signupButton.setTitle("Creating Account...", for: .normal) + + // Firebase signup + UserManager.shared.signup(name: name, email: email, password: password) { [weak self] success in + DispatchQueue.main.async { + self?.signupButton.isEnabled = true + self?.signupButton.setTitle("Create Account", for: .normal) + + if success { + // Navigate to main app + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + sceneDelegate.switchToMainApp() + } else { + // Fallback + let mainVC = MainTabBarController() + mainVC.modalPresentationStyle = .fullScreen + self?.present(mainVC, animated: true) + } + } else { + self?.showError("Failed to create account. Email may already be in use.") + } + } + } +} +``` + +### 3.5 Update ForgotPasswordViewController + +**File**: `Envision/Screens/Onboarding/ForgotPasswordViewController.swift` + +```swift +// Update handleReset method + +@objc private func handleReset() { + guard let email = emailField.text, !email.isEmpty else { + showError("Please enter your email address") + return + } + + guard email.isValidEmail else { + showError("Please enter a valid email address") + return + } + + // Show loading + resetButton.isEnabled = false + resetButton.setTitle("Sending...", for: .normal) + + // Send password reset email + AuthManager.shared.sendPasswordReset(email: email) { [weak self] result in + DispatchQueue.main.async { + self?.resetButton.isEnabled = true + self?.resetButton.setTitle("Send Reset Link", for: .normal) + + switch result { + case .success: + self?.showSuccess("Password reset email sent! Check your inbox.") + case .failure(let error): + self?.showError("Failed to send reset email: \(error.localizedDescription)") + } + } + } +} + +private func showSuccess(_ message: String) { + let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .default) { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + }) + present(alert, animated: true) +} +``` + +### 3.6 Add Auto-Login in SplashViewController + +**File**: `Envision/Screens/Onboarding/SplashViewController.swift` + +```swift +import UIKit +import FirebaseAuth + +// Update goNext method + +private func goNext() { + // Check if user is already logged in + if let firebaseUser = Auth.auth().currentUser { + print("✅ User already logged in: \(firebaseUser.uid)") + + // Fetch user profile from Firestore + FirestoreManager.shared.fetchUserDocument(uid: firebaseUser.uid) { [weak self] result in + DispatchQueue.main.async { + switch result { + case .success(let userModel): + UserManager.shared.currentUser = userModel + self?.goToMainApp() + case .failure(let error): + print("⚠️ Failed to fetch user profile: \(error)") + // Still go to main app with minimal user data + let userModel = UserModel( + id: firebaseUser.uid, + name: firebaseUser.displayName ?? "User", + email: firebaseUser.email ?? "", + createdAt: Date(), + preferences: UserPreferences() + ) + UserManager.shared.currentUser = userModel + self?.goToMainApp() + } + } + } + } else { + // No user logged in, show onboarding + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + self?.goToOnboarding() + } + } +} + +private func goToMainApp() { + if let sceneDelegate = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { + sceneDelegate.switchToMainApp() + } else { + let mainVC = MainTabBarController() + mainVC.modalPresentationStyle = .fullScreen + present(mainVC, animated: true) + } +} + +private func goToOnboarding() { + let onboarding = OnboardingController() + onboarding.modalPresentationStyle = .fullScreen + present(onboarding, animated: true) +} +``` + +--- + +## 4. Firestore Database Design + +### 4.1 Data Model Structure + +``` +firestore/ +├── users/{uid} +│ ├── id: string +│ ├── name: string +│ ├── email: string +│ ├── bio: string +│ ├── profileImageURL: string +│ ├── createdAt: timestamp +│ └── preferences: map +│ ├── notificationsEnabled: boolean +│ ├── scanReminders: boolean +│ ├── newFeatureAlerts: boolean +│ └── theme: number +│ +├── users/{uid}/rooms/{roomId} +│ ├── id: string +│ ├── name: string +│ ├── category: string +│ ├── createdAt: timestamp +│ ├── updatedAt: timestamp +│ ├── usdzURL: string (optional, Firebase Storage path) +│ ├── thumbnailURL: string +│ ├── dimensions: map +│ │ ├── width: number +│ │ ├── length: number +│ │ └── height: number +│ └── notes: string +│ +└── users/{uid}/furniture/{furnitureId} + ├── id: string + ├── name: string + ├── category: string + ├── createdAt: timestamp + ├── updatedAt: timestamp + ├── usdzURL: string (optional) + └── thumbnailURL: string +``` + +### 4.2 Create FirestoreManager + +**File**: `Envision/Managers/FirestoreManager.swift` + +```swift +import Foundation +import FirebaseFirestore + +final class FirestoreManager { + static let shared = FirestoreManager() + + private let db = Firestore.firestore() + + private init() { + // Enable offline persistence + let settings = FirestoreSettings() + settings.isPersistenceEnabled = true + settings.cacheSizeBytes = FirestoreCacheSizeUnlimited + db.settings = settings + } + + // MARK: - User Document + + func createUserDocument(uid: String, email: String, name: String, completion: @escaping (Result) -> Void) { + let userRef = db.collection("users").document(uid) + + let data: [String: Any] = [ + "id": uid, + "name": name, + "email": email, + "bio": "", + "profileImageURL": "", + "createdAt": FieldValue.serverTimestamp(), + "preferences": [ + "notificationsEnabled": true, + "scanReminders": true, + "newFeatureAlerts": true, + "theme": 0 + ] + ] + + userRef.setData(data) { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ User document created: \(uid)") + completion(.success(())) + } + } + } + + func fetchUserDocument(uid: String, completion: @escaping (Result) -> Void) { + let userRef = db.collection("users").document(uid) + + userRef.getDocument { snapshot, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = snapshot?.data() else { + completion(.failure(NSError(domain: "FirestoreManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "User document not found"]))) + return + } + + // Parse user model + let id = data["id"] as? String ?? uid + let name = data["name"] as? String ?? "User" + let email = data["email"] as? String ?? "" + let bio = data["bio"] as? String + let profileImagePath = data["profileImageURL"] as? String + + let createdAt: Date + if let timestamp = data["createdAt"] as? Timestamp { + createdAt = timestamp.dateValue() + } else { + createdAt = Date() + } + + let prefsData = data["preferences"] as? [String: Any] ?? [:] + let preferences = UserPreferences( + notificationsEnabled: prefsData["notificationsEnabled"] as? Bool ?? true, + scanReminders: prefsData["scanReminders"] as? Bool ?? true, + newFeatureAlerts: prefsData["newFeatureAlerts"] as? Bool ?? true, + theme: prefsData["theme"] as? Int ?? 0 + ) + + let userModel = UserModel( + id: id, + name: name, + email: email, + bio: bio, + profileImagePath: profileImagePath, + createdAt: createdAt, + preferences: preferences + ) + + completion(.success(userModel)) + } + } + + func updateUserDocument(uid: String, data: [String: Any], completion: @escaping (Result) -> Void) { + let userRef = db.collection("users").document(uid) + + userRef.updateData(data) { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ User document updated: \(uid)") + completion(.success(())) + } + } + } + + func deleteUserData(uid: String, completion: @escaping (Result) -> Void) { + let userRef = db.collection("users").document(uid) + + // Delete user document + userRef.delete { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ User data deleted: \(uid)") + completion(.success(())) + } + } + + // Note: Subcollections (rooms, furniture) must be deleted separately + // For production, use Cloud Functions for recursive delete + } + + // MARK: - Rooms Subcollection + + func saveRoom(uid: String, room: RoomModel, metadata: RoomMetadata, completion: @escaping (Result) -> Void) { + let roomRef = db.collection("users").document(uid).collection("rooms").document(room.id.uuidString) + + let data: [String: Any] = [ + "id": room.id.uuidString, + "name": room.name, + "category": room.category.rawValue, + "createdAt": Timestamp(date: room.createdAt), + "updatedAt": FieldValue.serverTimestamp(), + "usdzFilename": room.usdzFilename, + "thumbnailPath": room.thumbnailPath ?? "", + "dimensions": metadata.dimensions ?? [:], + "notes": metadata.notes ?? "" + ] + + roomRef.setData(data, merge: true) { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ Room saved to Firestore: \(room.name)") + completion(.success(())) + } + } + } + + func fetchRooms(uid: String, completion: @escaping (Result<[RoomModel], Error>) -> Void) { + let roomsRef = db.collection("users").document(uid).collection("rooms") + + roomsRef.order(by: "createdAt", descending: true).getDocuments { snapshot, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let documents = snapshot?.documents else { + completion(.success([])) + return + } + + let rooms: [RoomModel] = documents.compactMap { doc in + let data = doc.data() + guard let idString = data["id"] as? String, + let id = UUID(uuidString: idString), + let name = data["name"] as? String, + let categoryString = data["category"] as? String, + let category = RoomCategory(rawValue: categoryString), + let usdzFilename = data["usdzFilename"] as? String else { + return nil + } + + let createdAt: Date + if let timestamp = data["createdAt"] as? Timestamp { + createdAt = timestamp.dateValue() + } else { + createdAt = Date() + } + + let thumbnailPath = data["thumbnailPath"] as? String + + return RoomModel( + id: id, + name: name, + category: category, + createdAt: createdAt, + usdzFilename: usdzFilename, + thumbnailPath: thumbnailPath + ) + } + + completion(.success(rooms)) + } + } + + func deleteRoom(uid: String, roomID: String, completion: @escaping (Result) -> Void) { + let roomRef = db.collection("users").document(uid).collection("rooms").document(roomID) + + roomRef.delete { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ Room deleted from Firestore: \(roomID)") + completion(.success(())) + } + } + } + + // MARK: - Furniture Subcollection + + func saveFurniture(uid: String, furnitureID: String, name: String, category: String, usdzFilename: String, completion: @escaping (Result) -> Void) { + let furnitureRef = db.collection("users").document(uid).collection("furniture").document(furnitureID) + + let data: [String: Any] = [ + "id": furnitureID, + "name": name, + "category": category, + "createdAt": FieldValue.serverTimestamp(), + "updatedAt": FieldValue.serverTimestamp(), + "usdzFilename": usdzFilename, + "thumbnailURL": "" + ] + + furnitureRef.setData(data, merge: true) { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ Furniture saved to Firestore: \(name)") + completion(.success(())) + } + } + } + + func fetchFurniture(uid: String, completion: @escaping (Result<[URL], Error>) -> Void) { + let furnitureRef = db.collection("users").document(uid).collection("furniture") + + furnitureRef.order(by: "createdAt", descending: true).getDocuments { snapshot, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let documents = snapshot?.documents else { + completion(.success([])) + return + } + + // Convert to file URLs (assuming local storage for now) + let fileManager = FileManager.default + let docsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0] + let furnitureFolder = docsURL.appendingPathComponent("furniture", isDirectory: true) + + let urls: [URL] = documents.compactMap { doc in + let data = doc.data() + guard let filename = data["usdzFilename"] as? String else { return nil } + return furnitureFolder.appendingPathComponent(filename) + } + + completion(.success(urls)) + } + } + + func deleteFurniture(uid: String, furnitureID: String, completion: @escaping (Result) -> Void) { + let furnitureRef = db.collection("users").document(uid).collection("furniture").document(furnitureID) + + furnitureRef.delete { error in + if let error = error { + completion(.failure(error)) + } else { + print("✅ Furniture deleted from Firestore: \(furnitureID)") + completion(.success(())) + } + } + } +} +``` + +--- + +## 5. Firebase Storage Structure + +### 5.1 Storage Bucket Organization + +``` +gs://envision-production.appspot.com/ +├── users/{uid}/ +│ ├── profile/ +│ │ └── profile.jpg +│ ├── rooms/ +│ │ ├── {roomId}.usdz +│ │ └── {roomId}_thumbnail.jpg +│ └── furniture/ +│ ├── {furnitureId}.usdz +│ └── {furnitureId}_thumbnail.jpg +``` + +### 5.2 Create StorageManager + +**File**: `Envision/Managers/StorageManager.swift` + +```swift +import Foundation +import FirebaseStorage + +final class StorageManager { + static let shared = StorageManager() + + private let storage = Storage.storage() + + private init() {} + + // MARK: - Profile Picture + + func uploadProfilePicture(uid: String, image: UIImage, completion: @escaping (Result) -> Void) { + guard let imageData = image.jpegData(compressionQuality: 0.8) else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert image to JPEG"]))) + return + } + + let ref = storage.reference().child("users/\(uid)/profile/profile.jpg") + let metadata = StorageMetadata() + metadata.contentType = "image/jpeg" + + ref.putData(imageData, metadata: metadata) { metadata, error in + if let error = error { + completion(.failure(error)) + return + } + + ref.downloadURL { url, error in + if let error = error { + completion(.failure(error)) + } else if let urlString = url?.absoluteString { + print("✅ Profile picture uploaded: \(urlString)") + completion(.success(urlString)) + } else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get download URL"]))) + } + } + } + } + + func downloadProfilePicture(url: String, completion: @escaping (Result) -> Void) { + let ref = storage.reference(forURL: url) + + ref.getData(maxSize: 5 * 1024 * 1024) { data, error in + if let error = error { + completion(.failure(error)) + return + } + + guard let data = data, let image = UIImage(data: data) else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to decode image"]))) + return + } + + completion(.success(image)) + } + } + + // MARK: - Room USDZ (optional - can stay local) + + func uploadRoomUSDZ(uid: String, roomID: String, localURL: URL, completion: @escaping (Result) -> Void) { + let ref = storage.reference().child("users/\(uid)/rooms/\(roomID).usdz") + + ref.putFile(from: localURL, metadata: nil) { metadata, error in + if let error = error { + completion(.failure(error)) + return + } + + ref.downloadURL { url, error in + if let error = error { + completion(.failure(error)) + } else if let urlString = url?.absoluteString { + print("✅ Room USDZ uploaded: \(urlString)") + completion(.success(urlString)) + } else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get download URL"]))) + } + } + } + } + + // MARK: - Thumbnail Upload + + func uploadThumbnail(uid: String, type: String, itemID: String, image: UIImage, completion: @escaping (Result) -> Void) { + guard let imageData = image.jpegData(compressionQuality: 0.7) else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert image to JPEG"]))) + return + } + + let ref = storage.reference().child("users/\(uid)/\(type)/\(itemID)_thumbnail.jpg") + let metadata = StorageMetadata() + metadata.contentType = "image/jpeg" + + ref.putData(imageData, metadata: metadata) { metadata, error in + if let error = error { + completion(.failure(error)) + return + } + + ref.downloadURL { url, error in + if let error = error { + completion(.failure(error)) + } else if let urlString = url?.absoluteString { + print("✅ Thumbnail uploaded: \(urlString)") + completion(.success(urlString)) + } else { + completion(.failure(NSError(domain: "StorageManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get download URL"]))) + } + } + } + } +} +``` + +--- + +## 6. Code Implementation + +### 6.1 Integration Points + +**When to sync with Firebase:** + +1. **Room saved** (in `RoomPreviewViewController`): +```swift +// After saving locally +if let uid = AuthManager.shared.currentUserID { + FirestoreManager.shared.saveRoom(uid: uid, room: room, metadata: metadata) { result in + switch result { + case .success: + print("✅ Room synced to cloud") + case .failure(let error): + print("⚠️ Cloud sync failed (offline?): \(error)") + // Still works offline, will sync later + } + } +} +``` + +2. **Furniture saved** (in `ObjectCapturePreviewController`): +```swift +// After saving USDZ locally +if let uid = AuthManager.shared.currentUserID { + FirestoreManager.shared.saveFurniture( + uid: uid, + furnitureID: furnitureID, + name: filename, + category: category.rawValue, + usdzFilename: "\(furnitureID).usdz" + ) { result in + // Handle result + } +} +``` + +3. **Profile updated** (in `EditProfileViewController`): +```swift +// After user edits profile +UserManager.shared.updateProfile(name: newName, bio: newBio) { success in + if success { + print("✅ Profile synced") + } +} +``` + +### 6.2 Offline Support + +Firestore automatically caches data. To handle offline scenarios: + +```swift +// Check if online before showing sync indicator +func isOnline() -> Bool { + // Simple check (can be improved with Reachability) + return true // Firestore handles offline automatically +} + +// Show sync status in UI +func showSyncStatus(synced: Bool) { + // Add cloud icon to nav bar + let icon = synced ? "checkmark.icloud.fill" : "icloud.slash.fill" + let color = synced ? UIColor.systemGreen : UIColor.systemGray + // Update UI +} +``` + +--- + +## 7. Security Rules + +### 7.1 Firestore Security Rules + +**In Firebase Console → Firestore → Rules**: + +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + + // Users collection + match /users/{userId} { + // User can read/write their own document + allow read, write: if request.auth != null && request.auth.uid == userId; + + // Rooms subcollection + match /rooms/{roomId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Furniture subcollection + match /furniture/{furnitureId} { + allow read, write: if request.auth != null && request.auth.uid == userId; + } + } + + // Deny all other access + match /{document=**} { + allow read, write: if false; + } + } +} +``` + +### 7.2 Storage Security Rules + +**In Firebase Console → Storage → Rules**: + +```javascript +rules_version = '2'; +service firebase.storage { + match /b/{bucket}/o { + + // Users folder + match /users/{userId}/{allPaths=**} { + // User can read/write their own files + allow read, write: if request.auth != null && request.auth.uid == userId; + } + + // Deny all other access + match /{allPaths=**} { + allow read, write: if false; + } + } +} +``` + +### 7.3 Test Security Rules + +```swift +// Try to access another user's data (should fail) +func testSecurityRules() { + let otherUID = "some_other_user_id" + + FirestoreManager.shared.fetchUserDocument(uid: otherUID) { result in + switch result { + case .success: + print("❌ Security rules are broken!") + case .failure: + print("✅ Security rules working correctly") + } + } +} +``` + +--- + +## 8. Migration Strategy + +### 8.1 Phase 1: Add Firebase (No Breaking Changes) + +**Week 1**: +- Add Firebase SDK +- Create manager classes +- Keep local storage as primary +- Firebase as "backup sync" only + +### 8.2 Phase 2: Dual Sync (Local + Cloud) + +**Week 2**: +- Every save operation writes to both local + Firestore +- On app launch, compare timestamps and merge +- Show "Syncing..." indicator + +### 8.3 Phase 3: Cloud-First (Optional) + +**Week 3+**: +- Firestore becomes source of truth +- Local storage as cache only +- Implement "Export to Files" for backup + +### 8.4 Data Migration Script + +```swift +func migrateLocalDataToFirebase(completion: @escaping () -> Void) { + guard let uid = AuthManager.shared.currentUserID else { + completion() + return + } + + print("🔄 Starting data migration...") + + let group = DispatchGroup() + + // Migrate rooms + let roomsMetadata = MetadataManager.shared.loadMetadata() + for (filename, metadata) in roomsMetadata.rooms { + group.enter() + + // Create RoomModel from metadata + let room = RoomModel( + id: UUID(), + name: metadata.name, + category: RoomCategory(rawValue: metadata.category) ?? .other, + createdAt: ISO8601DateFormatter().date(from: metadata.createdAt) ?? Date(), + usdzFilename: filename, + thumbnailPath: nil + ) + + FirestoreManager.shared.saveRoom(uid: uid, room: room, metadata: metadata) { _ in + group.leave() + } + } + + // Wait for all migrations to complete + group.notify(queue: .main) { + print("✅ Data migration complete!") + completion() + } +} +``` + +--- + +## 9. Testing Plan + +### 9.1 Unit Tests + +```swift +import XCTest +@testable import Envision + +class FirebaseTests: XCTestCase { + + func testAuthSignup() { + let expectation = XCTestExpectation(description: "Signup completes") + + AuthManager.shared.signUp(email: "test@example.com", password: "Test1234", name: "Test User") { result in + switch result { + case .success(let user): + XCTAssertNotNil(user) + expectation.fulfill() + case .failure(let error): + XCTFail("Signup failed: \(error)") + } + } + + wait(for: [expectation], timeout: 10.0) + } + + func testFirestoreSaveRoom() { + // Test saving a room + } + + func testStorageUpload() { + // Test uploading an image + } +} +``` + +### 9.2 Integration Tests + +**Test Scenarios**: +1. Sign up → Create room → Logout → Login → Verify room exists +2. Offline mode → Create room → Go online → Verify sync +3. Profile picture upload → Verify URL in Firestore +4. Password reset → Verify email sent + +### 9.3 Manual Testing Checklist + +- [ ] Fresh install → Signup → Create room → Logout → Login → Room still there +- [ ] Airplane mode → Create room → Toggle off → Room syncs +- [ ] Delete account → Verify Firestore data deleted +- [ ] Password reset → Receive email → Reset works +- [ ] Profile picture upload → Shows in app +- [ ] Multiple devices → Changes sync + +--- + +## 10. Deployment Checklist + +### 10.1 Pre-Launch + +- [ ] Firebase project created (production) +- [ ] `GoogleService-Info.plist` added to Xcode +- [ ] Security rules deployed and tested +- [ ] Error handling added to all Firebase calls +- [ ] Loading indicators for async operations +- [ ] Offline caching enabled +- [ ] Analytics events configured (optional) +- [ ] Crashlytics configured (optional) + +### 10.2 App Store Submission + +- [ ] Update privacy policy (mention cloud storage) +- [ ] Add "Sign in with Apple" (if using social auth) +- [ ] Test on multiple iOS versions (17.0+) +- [ ] Test on different devices (iPhone SE, Pro Max, iPad) +- [ ] Beta test via TestFlight (50+ users) + +### 10.3 Post-Launch Monitoring + +- [ ] Monitor Firebase Console → Usage +- [ ] Check Firestore read/write counts (cost optimization) +- [ ] Monitor Storage usage (cost optimization) +- [ ] Set up billing alerts +- [ ] Review Crashlytics reports weekly + +--- + +## Appendix: Cost Estimation + +### Firebase Free Tier (Spark Plan) + +**Firestore**: +- 50k reads/day +- 20k writes/day +- 20k deletes/day +- 1 GB storage + +**Storage**: +- 5 GB storage +- 1 GB/day downloads + +**Authentication**: +- Unlimited (free) + +### When to Upgrade (Blaze Plan) + +- 1000+ daily active users +- Heavy USDZ file uploads (>5 GB/month) +- Need phone authentication + +**Estimated Cost** (10k users, moderate usage): +- $25-50/month + +--- + +**Document Version**: 1.0 +**Last Updated**: January 21, 2026 +**Estimated Implementation Time**: 10-14 days +**Priority**: High + +--- + +*End of Firebase Backend Implementation Plan* diff --git a/PROJECT_WORKFLOW.md b/PROJECT_WORKFLOW.md new file mode 100644 index 0000000..3190ad8 --- /dev/null +++ b/PROJECT_WORKFLOW.md @@ -0,0 +1,302 @@ +# EnVision - Project Workflow Documentation + +## Overview +EnVision is an iOS AR/3D visualization app that enables users to: +- Scan and capture 3D models of furniture using photogrammetry +- Scan rooms using Apple's RoomPlan API +- Visualize and place furniture in rooms +- Manage saved 3D models and room scans + +--- + +## Project Structure + +``` +Envision/ +├── AppDelegate.swift # App lifecycle +├── SceneDelegate.swift # Window/Scene setup, navigation root +├── MainTabBarController.swift # Main tab bar with 3 tabs +├── ViewController.swift # Base view controller +│ +├── 3D_Models/ # Sample USDZ models +│ +├── Assets.xcassets/ # App assets (icons, images) +│ +├── Components/ # Reusable UI components +│ ├── CustomTextField.swift +│ ├── PrimaryButton.swift +│ └── PrimaryButton1.swift +│ +├── Extensions/ # Helper extensions +│ ├── Entity+Visit.swift # RealityKit entity traversal +│ ├── Extensions.swift # General extensions +│ ├── SaveManager.swift # Model saving/loading +│ ├── UIColor+Hex.swift # Hex color support +│ ├── UIFont+AppFonts.swift # Custom fonts +│ ├── UIViewController+Transition.swift +│ ├── UserManager.swift # User session management +│ └── UserModel.swift # User data model +│ +├── Managers/ +│ ├── BackgroundModelProcessor.swift # Background photogrammetry processing +│ └── TourManager.swift # App tour/tips management +│ +├── Tips/ # TipKit integration +│ ├── AppTips.swift +│ └── TipPresenter.swift +│ +└── Screens/ + ├── Onboarding/ # Login/signup flow + │ ├── SplashViewController.swift + │ ├── OnboardingController.swift + │ ├── OnboardingPage.swift + │ ├── LoginViewController.swift + │ ├── SignupViewController.swift + │ ├── ForgotPasswordViewController.swift + │ ├── ModernTextField.swift + │ └── SocialButton.swift + │ + └── MainTabs/ + ├── Rooms/ # Room scanning & visualization + │ ├── MyRoomsViewController.swift + │ ├── RoomCell.swift + │ ├── RoomModel.swift + │ ├── MetadataManager.swift + │ ├── RoomPlanScan/ + │ │ ├── RoomPlanScannerViewController.swift + │ │ └── RoomPreviewViewController.swift + │ └── furniture+room/ + │ ├── RoomViewerViewController.swift + │ ├── RoomEditVC.swift + │ ├── RoomVisualizeVC.swift + │ ├── FurniturePicker.swift + │ ├── FurnitureControlPanel.swift + │ └── OrbitJoystick.swift + │ + ├── furniture/ # Furniture/object capture + │ ├── ScanFurnitureViewController.swift + │ ├── FurnitureCategory.swift + │ ├── FurnitureCell.swift + │ ├── CreateModel/ + │ │ ├── CreateModelViewController.swift + │ │ └── CreateModelViewController2.swift + │ ├── ModelsFromFiles/ + │ │ ├── ViewModelsViewController.swift + │ │ └── USDZCell.swift + │ ├── Object Capture/ + │ │ ├── ObjectScanViewController.swift + │ │ ├── ObjectCapturePreviewController.swift # Photo preview & processing + │ │ ├── ARMeshExporter.swift + │ │ ├── ArrowGuideView.swift + │ │ ├── FeedbackBubble.swift + │ │ ├── InstructionOverlay.swift + │ │ └── ProgressRingView.swift + │ └── roomPlanColor/ + │ ├── RoomARView 1.swift + │ ├── RoomARView 2.swift + │ ├── RoomARWithFurnitureViewController.swift + │ └── VisualizeRoomViewController.swift + │ + └── profile/ # User settings + ├── ProfileViewController.swift + ├── ProfileCell.swift + ├── EditProfileViewController.swift + └── SubScreens/ + ├── AppearanceViewController.swift + ├── AppInfoViewController.swift + ├── EmailPasswordViewController.swift + ├── NotificationsViewController.swift + ├── PermissionsViewController.swift + ├── PrivacyControlsViewController.swift + ├── PrivacyPolicyViewController.swift + ├── TermsViewController.swift + └── TipsLibraryViewController.swift +``` + +--- + +## Application Flow + +### 1. App Launch +``` +AppDelegate → SceneDelegate → SplashViewController +``` + +### 2. Onboarding Flow +``` +SplashViewController + ↓ +OnboardingController (first launch) + ↓ +LoginViewController ←→ SignupViewController + ↔ ForgotPasswordViewController + ↓ +MainTabBarController +``` + +### 3. Main App (Tab Bar) +``` +MainTabBarController +├── Tab 1: My Rooms (MyRoomsViewController) +├── Tab 2: My Furniture (ScanFurnitureViewController) +└── Tab 3: Profile (ProfileViewController) +``` + +--- + +## Feature Workflows + +### Room Scanning Flow +``` +MyRoomsViewController + ↓ (Scan button) +RoomPlanScannerViewController (Uses RoomPlan API) + ↓ (Capture complete) +RoomPreviewViewController (Preview & save) + ↓ (Save) +MyRoomsViewController (Updated list) + ↓ (Select room) +RoomViewerViewController / RoomEditVC + ↓ (Add furniture) +FurniturePicker → RoomVisualizeVC +``` + +### Furniture/Object Capture Flow +``` +ScanFurnitureViewController + ↓ (Automatic Object Capture) +ObjectScanViewController (Camera capture) + ↓ (Photos captured) +ObjectCapturePreviewController + ↓ (Generate 3D Model - uses BackgroundModelProcessor) + ↓ (Processing happens in background) + ↓ (Model saved via SaveManager) +ScanFurnitureViewController (Updated list) + ↓ (View model) +QuickLook Preview +``` + +### Background Processing (Key Feature) +``` +ObjectCapturePreviewController + ↓ startProcessing() +BackgroundModelProcessor.shared.startProcessing() + ↓ + ├── Creates PhotogrammetrySession + ├── Registers UIBackgroundTask for extended processing + ├── Processes images → 3D model + ├── Updates progress via callbacks + ├── Sends local notification on completion + └── Saves model via SaveManager +``` + +--- + +## Key Components + +### BackgroundModelProcessor +**Purpose**: Enables 3D model generation to continue even when user leaves the screen. + +**Features**: +- Background task registration for extended processing +- Thread-safe progress tracking +- Callback-based UI updates +- Local notifications for completion +- Cancellation support + +**Usage**: +```swift +BackgroundModelProcessor.shared.startProcessing( + imagesFolder: imagesFolderURL, + detailLevel: .reduced +) { result in + switch result { + case .success(let savedURL): + // Model saved successfully + case .failure(let error): + // Handle error + } +} +``` + +### SaveManager +**Purpose**: Handles saving and loading 3D models and room data. + +**Locations**: +- Furniture: `Documents/Furniture/` +- Rooms: `Documents/Rooms/` +- Thumbnails: Cached separately + +### MetadataManager +**Purpose**: Manages room metadata (names, dates, categories). + +--- + +## Technologies Used + +- **RealityKit**: 3D rendering and AR +- **ARKit**: Augmented reality sessions +- **RoomPlan**: Room scanning (iOS 16+) +- **PhotogrammetrySession**: Object capture (iOS 17+) +- **QuickLook**: 3D model preview +- **TipKit**: User tips and tours + +--- + +## Error Fixed (January 27, 2026) + +### Issue +`PhotogrammetrySession.Request.Detail` enum in iOS 26 SDK doesn't have `.preview`, `.medium`, or `.full` members. + +### Files Modified +1. `ObjectCapturePreviewController.swift` + - Line 178: Changed `.preview` to `.reduced` + - Lines 227-233: Changed all detail levels to `.reduced` + +2. `BackgroundModelProcessor.swift` + - Wrapped async calls with `await MainActor.run { }` to fix Swift 6 concurrency warnings + +### Build Status +✅ BUILD SUCCEEDED + +--- + +## Notes for Future Development + +1. **Detail Levels**: The quality selector UI shows "Fast", "Balanced", "High Quality" but all map to `.reduced`. When newer SDK versions provide more options, update `updateQualityDescription()`. + +2. **Swift 6 Compatibility**: Main actor isolation warnings were fixed in BackgroundModelProcessor. Monitor other files with similar warnings. + +3. **Deprecated APIs**: Several iOS 26 deprecation warnings exist (UIScreen.main, UIBarButtonItem.Style.done). Address these for full iOS 26 compatibility. + +--- + +## Recent Updates (January 27, 2026) + +### Color Persistence Feature +Added the ability to save and restore colors when switching between Edit and Visualize modes. + +**New Files:** +- `Managers/RoomColorManager.swift` - Singleton manager for persisting room element colors + +**Modified Files:** +- `RoomEditVC.swift` - Now saves colors to RoomColorManager when changed +- `RoomVisualizeVC.swift` - Now loads and applies saved colors when loading room + +**How It Works:** +1. User changes color in Edit mode using the color picker +2. Color is automatically saved to `Documents/RoomColors/{roomName}_colors.json` +3. When switching to Visualize mode, saved colors are loaded and applied +4. Colors persist across app restarts + +**Supported Element Types:** +- Walls, Floors, Doors, Windows, Tables, Chairs, Storage + +### Color Picker UI Improvements +- Added native "Cancel" and "Done" buttons to the color picker navigation bar +- Cancel button restores previous colors +- Done button confirms the color selection + +### Material Resolution Warnings +The warnings about "Could not resolve material name 'engine:BuiltinRenderGraphResources/AR/...'" are RealityKit internal warnings in the iOS Simulator. They don't affect functionality and typically don't appear on physical devices. + diff --git a/RECOMMENDED_IMPROVEMENTS.md b/RECOMMENDED_IMPROVEMENTS.md new file mode 100644 index 0000000..2518767 --- /dev/null +++ b/RECOMMENDED_IMPROVEMENTS.md @@ -0,0 +1,348 @@ +# EnVision - Recommended Improvements + +## 🔴 Critical (High Priority) + +### 1. ✅ Quality Selector Fixed +**Status**: Fixed in this session + +The quality selector was showing 3 options that all mapped to `.reduced`. Updated to show single "Standard Quality" option since iOS 26 SDK only supports `.reduced` detail level. + +--- + +### 2. Background Processing Persistence +**Current Issue**: If app is force-quit during processing, progress is lost. + +**Solution**: Add persistent job queue with Core Data or file-based storage. + +```swift +// Add to BackgroundModelProcessor.swift +private func persistJobState() { + guard let job = currentJob else { return } + let encoder = JSONEncoder() + if let data = try? encoder.encode(job) { + UserDefaults.standard.set(data, forKey: "currentProcessingJob") + } +} + +func resumePersistedJob() -> ProcessingJob? { + guard let data = UserDefaults.standard.data(forKey: "currentProcessingJob"), + let job = try? JSONDecoder().decode(ProcessingJob.self, from: data) else { + return nil + } + return job +} +``` + +--- + +### 3. Memory Management for Large Image Sets +**Current Issue**: Loading 100+ images can cause memory pressure. + +**Solution**: Implement lazy loading with image downsampling. + +```swift +// Add to ObjectCapturePreviewController.swift +private func downsampledImage(at url: URL, to pointSize: CGSize) -> UIImage? { + let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else { return nil } + + let maxDimensionInPixels = max(pointSize.width, pointSize.height) * UIScreen.main.scale + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels + ] as CFDictionary + + guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { return nil } + return UIImage(cgImage: downsampledImage) +} +``` + +--- + +## 🟡 Important (Medium Priority) + +### 4. Add Processing Time Estimation +Show users estimated time remaining based on image count. + +```swift +// Add to BackgroundModelProcessor.swift +func estimatedProcessingTime(imageCount: Int) -> String { + // Rough estimates based on .reduced detail + let baseTime: Double = 30 // Base seconds + let perImageTime: Double = 0.5 // Additional seconds per image + let totalSeconds = baseTime + (Double(imageCount) * perImageTime) + + if totalSeconds < 60 { + return "~\(Int(totalSeconds)) seconds" + } else { + return "~\(Int(totalSeconds / 60)) minutes" + } +} +``` + +--- + +### 5. Add Cancel Button During Processing +Users should be able to cancel ongoing processing. + +```swift +// Add cancel button to ObjectCapturePreviewController +private let cancelButton: UIButton = { + let btn = UIButton(type: .system) + btn.setTitle("Cancel", for: .normal) + btn.setTitleColor(.systemRed, for: .normal) + btn.isHidden = true + btn.translatesAutoresizingMaskIntoConstraints = false + return btn +}() + +@objc private func cancelProcessing() { + let alert = UIAlertController( + title: "Cancel Processing?", + message: "This will stop the 3D model generation. Photos will be preserved.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "Continue", style: .cancel)) + alert.addAction(UIAlertAction(title: "Cancel", style: .destructive) { _ in + self.processor.cancelProcessing() + self.handleError(message: "Processing cancelled") + }) + present(alert, animated: true) +} +``` + +--- + +### 6. Improve Error Messages +Make error messages user-friendly with recovery suggestions. + +```swift +// Add to BackgroundModelProcessor.swift +enum ProcessingError: LocalizedError { + case alreadyProcessing + case processingFailed(String) + case sessionCreationFailed + case insufficientImages + case lowQualityImages + + var errorDescription: String? { + switch self { + case .alreadyProcessing: + return "A model is already being processed" + case .processingFailed(let message): + return message + case .sessionCreationFailed: + return "Could not start the 3D capture session" + case .insufficientImages: + return "Not enough photos captured" + case .lowQualityImages: + return "Image quality too low for 3D reconstruction" + } + } + + var recoverySuggestion: String? { + switch self { + case .alreadyProcessing: + return "Wait for the current model to finish or cancel it." + case .insufficientImages: + return "Capture at least 20 photos from different angles." + case .lowQualityImages: + return "Ensure good lighting and hold the camera steady." + default: + return "Try capturing new photos in better conditions." + } + } +} +``` + +--- + +### 7. Add Progress Persistence Across App Launches +Show processing history and allow resuming failed jobs. + +```swift +// Create ProcessingHistoryManager.swift +class ProcessingHistoryManager { + static let shared = ProcessingHistoryManager() + + private let historyKey = "processingHistory" + + func saveJob(_ job: ProcessingJob) { + var history = getHistory() + history.append(job) + // Keep last 20 jobs + if history.count > 20 { + history.removeFirst(history.count - 20) + } + if let data = try? JSONEncoder().encode(history) { + UserDefaults.standard.set(data, forKey: historyKey) + } + } + + func getHistory() -> [ProcessingJob] { + guard let data = UserDefaults.standard.data(forKey: historyKey), + let history = try? JSONDecoder().decode([ProcessingJob].self, from: data) else { + return [] + } + return history + } +} +``` + +--- + +## 🟢 Nice to Have (Low Priority) + +### 8. Add Haptic Feedback Throughout Processing +Provide tactile feedback at key milestones. + +```swift +// Add milestone haptics +private func provideMilestoneHaptic(at progress: Float) { + let milestones: [Float] = [0.25, 0.5, 0.75, 1.0] + if milestones.contains(where: { abs($0 - progress) < 0.01 }) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(progress >= 1.0 ? .success : .warning) + } +} +``` + +--- + +### 9. Add Model Preview Before Saving +Show a quick 3D preview before final save. + +```swift +// Add to ObjectCapturePreviewController after processing +private func showModelPreview(at url: URL) { + let previewController = QLPreviewController() + previewController.dataSource = self + self.previewModelURL = url + present(previewController, animated: true) +} +``` + +--- + +### 10. Add Share Functionality +Allow sharing generated models directly. + +```swift +private func shareModel(at url: URL) { + let activityVC = UIActivityViewController( + activityItems: [url], + applicationActivities: nil + ) + present(activityVC, animated: true) +} +``` + +--- + +### 11. Add iCloud Sync for Models +Sync furniture models across devices. + +```swift +// In SaveManager.swift +private func iCloudContainerURL() -> URL? { + FileManager.default.url(forUbiquityContainerIdentifier: nil)? + .appendingPathComponent("Documents") + .appendingPathComponent("Furniture") +} +``` + +--- + +### 12. Add Analytics/Telemetry +Track processing success rates and common failure points. + +```swift +struct ProcessingAnalytics { + static func trackProcessingStarted(imageCount: Int, detailLevel: String) { + // Log to analytics service + } + + static func trackProcessingCompleted(duration: TimeInterval, success: Bool) { + // Log to analytics service + } +} +``` + +--- + +## 📱 UI/UX Improvements + +### 13. Add Processing Animation +Replace spinner with custom animation showing model being built. + +### 14. Add Photo Quality Indicators +Show which photos are good/bad quality before processing. + +### 15. Add Guided Capture Mode +Step-by-step instructions for capturing optimal photos. + +### 16. Add Batch Processing +Queue multiple objects for processing overnight. + +--- + +## 🔧 Code Quality Improvements + +### 17. Extract Constants +```swift +struct ProcessingConstants { + static let minPhotos = 20 + static let optimalPhotos = 50 + static let maxPhotos = 200 + static let supportedExtensions = ["jpg", "jpeg", "heic", "png"] +} +``` + +### 18. Add Unit Tests +```swift +class BackgroundModelProcessorTests: XCTestCase { + func testProcessingStateManagement() { + let processor = BackgroundModelProcessor.shared + XCTAssertFalse(processor.isProcessing) + } + + func testEstimatedTime() { + let time = processor.estimatedProcessingTime(imageCount: 50) + XCTAssertTrue(time.contains("minute")) + } +} +``` + +### 19. Add Documentation +Add comprehensive code documentation with usage examples. + +--- + +## Implementation Priority + +| Priority | Improvement | Effort | Impact | +|----------|------------|--------|--------| +| 1 | ✅ Fix quality selector | Low | High | +| 2 | Add cancel button | Low | High | +| 3 | Memory management | Medium | High | +| 4 | Time estimation | Low | Medium | +| 5 | Better error messages | Low | Medium | +| 6 | Job persistence | Medium | Medium | +| 7 | Model preview | Medium | Medium | +| 8 | Share functionality | Low | Low | +| 9 | iCloud sync | High | Medium | +| 10 | Analytics | Medium | Low | + +--- + +## Quick Wins (Can Implement Now) + +1. ✅ Quality selector fix - Done +2. Cancel button - 15 minutes +3. Time estimation - 10 minutes +4. Better error messages - 20 minutes +5. Haptic feedback - 10 minutes + +Would you like me to implement any of these improvements? diff --git a/TIPS_TOUR_IMPROVEMENT_PLAN.md b/TIPS_TOUR_IMPROVEMENT_PLAN.md new file mode 100644 index 0000000..3d4ac5e --- /dev/null +++ b/TIPS_TOUR_IMPROVEMENT_PLAN.md @@ -0,0 +1,1048 @@ +# EnVision Tips & Tour System - Complete Improvement Plan +*Priority: CRITICAL | Status: REMOVED (Needs Rebuild)* + +--- + +## Executive Summary + +The Tips & Tour system was **removed from the codebase** on January 21, 2026 due to: +- SwiftUI `TipView` hosting instability in UIKit +- Vertical text artifacts overlaying all screens +- Non-responsive button actions +- Layout conflicts with collection views and navigation bars + +**This document outlines a complete rebuild using pure UIKit** (no SwiftUI, no TipKit), ensuring: +- Native iOS feel +- Reliable touch handling +- No layout corruption +- Progressive onboarding that guides users through app features + +--- + +## Table of Contents +1. [Original Vision](#1-original-vision) +2. [What Went Wrong](#2-what-went-wrong) +3. [New Architecture (Pure UIKit)](#3-new-architecture-pure-uikit) +4. [Implementation Phases](#4-implementation-phases) +5. [Visual Design Specs](#5-visual-design-specs) +6. [Code Structure](#6-code-structure) +7. [Testing Plan](#7-testing-plan) +8. [Timeline & Milestones](#8-timeline--milestones) + +--- + +## 1. Original Vision + +### 1.1 Purpose +A **TipKit-based progressive onboarding system** that guides users from first launch to advanced features using context-aware tips, managed by a central TourManager, with 25 lightweight tips, a resettable tour, and screen-level integration that shows the right tip at the right time without disrupting the app experience. + +### 1.2 Original Tips (25 Total) + +#### Getting Started (2 tips) +1. **Welcome** - "Take a tour to learn how to scan rooms and furniture" +2. **Profile** - "Customize your profile and preferences" + +#### My Rooms (8 tips) +3. **Intro** - "Scan your first room with LiDAR" +4. **Scan Slowly** - "Move your device slowly for best accuracy" +5. **Import** - "Bring in existing USDZ models" +6. **Actions Menu** - "Select multiple rooms to delete/share" +7. **Categories** - "Organize rooms by type" +8. **Search** - "Find rooms quickly by name" +9. **Room Detail** - "View and edit room metadata" +10. **AR Preview** - "Place furniture in your scanned room" + +#### My Furniture (8 tips) +11. **Intro** - "Capture furniture using 360° photogrammetry" +12. **Automatic Capture** - "Walk around object while camera captures" +13. **Photo Count** - "Take 30-50 photos for best quality" +14. **Good Lighting** - "Ensure even lighting and avoid shadows" +15. **360° Coverage** - "Capture all angles of the object" +16. **From Photos** - "Create models from existing photos" +17. **Import USDZ** - "Bring in furniture models from Files" +18. **Categories** - "Organize furniture by type" + +#### Profile (7 tips) +19. **Settings** - "Customize app behavior" +20. **Theme** - "Choose light, dark, or system theme" +21. **Notifications** - "Enable scan reminders" +22. **Tips Library** - "Browse all tips anytime" +23. **Restart Tour** - "Reset the app tour" +24. **Export Data** - "Backup your scans to iCloud" +25. **Support** - "Contact us for help" + +### 1.3 Original Tour Progression +``` +Launch → Welcome Tip (MainTabBarController) + ↓ +My Rooms (0 rooms) → Intro Tip → Scan First Room + ↓ +My Rooms (1 room) → Actions Menu Tip + Categories Tip + ↓ +My Furniture (0 items) → Intro Tip → Capture First Object + ↓ +My Furniture (1 item) → Quality Tips + ↓ +Profile → Settings Tip → Complete Tour +``` + +--- + +## 2. What Went Wrong + +### 2.1 Technical Issues + +#### Issue 1: SwiftUI Hosting in UIKit +```swift +// Old broken approach +let tipView = TipView(WelcomeTip(), arrowEdge: .top) +let host = UIHostingController(rootView: tipView) +view.addSubview(host.view) +``` + +**Problems**: +- SwiftUI `TipView` has unpredictable layout in UIKit container +- Auto-layout constraints conflict with SwiftUI's layout system +- Views don't clean up properly on dismiss +- Touch events intercepted by SwiftUI layer + +#### Issue 2: Global Overlay at Tab Bar Level +```swift +// In MainTabBarController.viewDidAppear +showWelcomeTip() // Added to tabBarController.view +``` + +**Problems**: +- Tip appears on ALL tabs (leaks across screens) +- Can't be removed reliably when switching tabs +- Vertical text artifact (zero-width constraint issue) +- Blocks touches to tab bar and child view controllers + +#### Issue 3: Type Erasure in TipPresenter +```swift +// Invalid Swift code +class TipPresenter: UIHostingController> { + // Error: 'any Tip' cannot be used as generic parameter +} +``` + +**Problems**: +- `any Tip` is an existential type, not a concrete type +- Can't be used as generic parameter for `TipView` +- Causes compile errors or runtime crashes + +#### Issue 4: Layout Conflicts in MyRoomsViewController +```swift +// Multiple tip hosting attempts +private var tipContainerView: UIView! +private let tipPresenter = TipPresenter(...) +// Both coexist, causing layout fights +``` + +**Problems**: +- Two different tip systems in same view controller +- Constraints added/removed unpredictably +- Collection view insets not reset properly +- Tips cover search bar and chip filters + +### 2.2 User-Facing Symptoms +- ❌ Vertical line of text ("Welcome to EnVision..." rotated 90°) on every screen +- ❌ Tips appear in wrong locations (covering nav bar, search bar) +- ❌ Buttons don't respond to taps +- ❌ Tips don't dismiss when tapping "Later" or "Got It" +- ❌ App feels "broken" and unprofessional +- ❌ No way to skip or disable tips + +--- + +## 3. New Architecture (Pure UIKit) + +### 3.1 Core Principles +1. **No SwiftUI** - Pure UIKit programmatic views +2. **No TipKit** - Custom tip management with TourManager +3. **Container-based** - Tips live inside dedicated containers, not overlays +4. **Layout-safe** - No constraint conflicts with existing UI +5. **Touch-safe** - Tips only intercept touches within their bounds +6. **Dismissible** - Always provide "Later" or "Got It" options +7. **Skippable** - Respect user choice to skip tour + +### 3.2 Component Design + +#### TipBubbleView (UIView) +A reusable UIView that displays a tip with: +- **Arrow pointer** (CAShapeLayer) pointing to target element +- **Title** (bold, 16pt) +- **Message** (regular, 14pt, 2-3 lines max) +- **Primary action button** (e.g., "Try It", "Scan Now") +- **Dismiss button** (e.g., "Later", "Got It") +- **Close X** (top-right corner, always available) + +**Layout**: +``` +┌─────────────────────────────┐ +│ × │ (close button) +│ Title (bold) │ +│ Message text wraps to 2-3 │ +│ lines for readability │ +│ ┌─────────┐ ┌──────────┐ │ +│ │ Primary │ │ Later │ │ +│ └─────────┘ └──────────┘ │ +└─────────────────────────────┘ + ▼ (arrow pointer) +``` + +#### TipCoordinator (Manager) +Manages tip lifecycle: +- **Conditions** - Check if tip should show (e.g., "hasSeenWelcomeTip", "roomCount == 0") +- **Show** - Present tip in target view controller's container +- **Dismiss** - Remove tip and mark as seen +- **Skip** - Mark all tips as seen (user choice) +- **Reset** - Clear all seen states (restart tour) + +#### TourManager (Existing, Enhanced) +Stores tour state in UserDefaults: +- `hasCompletedTour: Bool` +- `currentTourStep: Int` (0-25) +- `seenTips: Set` (tip IDs that have been shown) +- `tourSkipped: Bool` (user chose to skip) + +--- + +## 4. Implementation Phases + +### Phase 1: Core Components (Day 1-2) + +#### Task 1.1: Create TipBubbleView +**File**: `Envision/Tips/TipBubbleView.swift` + +```swift +import UIKit + +final class TipBubbleView: UIView { + + enum ArrowEdge { + case top, bottom, left, right + } + + struct Configuration { + let title: String + let message: String + let primaryActionTitle: String + let dismissActionTitle: String + let arrowEdge: ArrowEdge + let arrowOffset: CGFloat // Horizontal/vertical offset from center + } + + // MARK: - Callbacks + var onPrimaryAction: (() -> Void)? + var onDismiss: (() -> Void)? + + // MARK: - UI Elements + private let containerView = UIView() + private let titleLabel = UILabel() + private let messageLabel = UILabel() + private let primaryButton = UIButton(type: .system) + private let dismissButton = UIButton(type: .system) + private let closeButton = UIButton(type: .system) + private let arrowLayer = CAShapeLayer() + + private let config: Configuration + + // MARK: - Init + init(configuration: Configuration) { + self.config = configuration + super.init(frame: .zero) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + // Container (rounded rect with shadow) + containerView.backgroundColor = .systemBackground + containerView.layer.cornerRadius = 16 + containerView.layer.shadowColor = UIColor.black.cgColor + containerView.layer.shadowOpacity = 0.15 + containerView.layer.shadowOffset = CGSize(width: 0, height: 4) + containerView.layer.shadowRadius = 12 + + // Arrow (triangle pointer) + arrowLayer.fillColor = UIColor.systemBackground.cgColor + layer.addSublayer(arrowLayer) + + // Title + titleLabel.text = config.title + titleLabel.font = .systemFont(ofSize: 16, weight: .semibold) + titleLabel.numberOfLines = 1 + + // Message + messageLabel.text = config.message + messageLabel.font = .systemFont(ofSize: 14) + messageLabel.textColor = .secondaryLabel + messageLabel.numberOfLines = 3 + + // Primary button + primaryButton.setTitle(config.primaryActionTitle, for: .normal) + primaryButton.titleLabel?.font = .systemFont(ofSize: 15, weight: .semibold) + primaryButton.backgroundColor = .systemBlue + primaryButton.setTitleColor(.white, for: .normal) + primaryButton.layer.cornerRadius = 10 + primaryButton.addTarget(self, action: #selector(primaryTapped), for: .touchUpInside) + + // Dismiss button + dismissButton.setTitle(config.dismissActionTitle, for: .normal) + dismissButton.titleLabel?.font = .systemFont(ofSize: 15) + dismissButton.setTitleColor(.secondaryLabel, for: .normal) + dismissButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside) + + // Close button + closeButton.setImage(UIImage(systemName: "xmark"), for: .normal) + closeButton.tintColor = .tertiaryLabel + closeButton.addTarget(self, action: #selector(dismissTapped), for: .touchUpInside) + + // Layout + [containerView, titleLabel, messageLabel, primaryButton, dismissButton, closeButton].forEach { + $0.translatesAutoresizingMaskIntoConstraints = false + } + + addSubview(containerView) + containerView.addSubview(titleLabel) + containerView.addSubview(messageLabel) + containerView.addSubview(primaryButton) + containerView.addSubview(dismissButton) + containerView.addSubview(closeButton) + + NSLayoutConstraint.activate([ + // Container (main bubble) + containerView.topAnchor.constraint(equalTo: topAnchor, constant: arrowHeight()), + containerView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerView.trailingAnchor.constraint(equalTo: trailingAnchor), + containerView.bottomAnchor.constraint(equalTo: bottomAnchor), + + // Close button + closeButton.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 12), + closeButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -12), + closeButton.widthAnchor.constraint(equalToConstant: 24), + closeButton.heightAnchor.constraint(equalToConstant: 24), + + // Title + titleLabel.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 16), + titleLabel.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + titleLabel.trailingAnchor.constraint(equalTo: closeButton.leadingAnchor, constant: -8), + + // Message + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + messageLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + messageLabel.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16), + + // Buttons + primaryButton.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 16), + primaryButton.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + primaryButton.heightAnchor.constraint(equalToConstant: 44), + primaryButton.widthAnchor.constraint(equalToConstant: 120), + primaryButton.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: -16), + + dismissButton.centerYAnchor.constraint(equalTo: primaryButton.centerYAnchor), + dismissButton.leadingAnchor.constraint(equalTo: primaryButton.trailingAnchor, constant: 12), + dismissButton.heightAnchor.constraint(equalToConstant: 44) + ]) + } + + override func layoutSubviews() { + super.layoutSubviews() + drawArrow() + } + + private func arrowHeight() -> CGFloat { + return config.arrowEdge == .top ? 12 : 0 + } + + private func drawArrow() { + let arrowSize: CGFloat = 12 + let path = UIBezierPath() + + switch config.arrowEdge { + case .top: + let centerX = bounds.width / 2 + config.arrowOffset + path.move(to: CGPoint(x: centerX, y: 0)) + path.addLine(to: CGPoint(x: centerX - arrowSize, y: arrowSize)) + path.addLine(to: CGPoint(x: centerX + arrowSize, y: arrowSize)) + path.close() + default: + break // Add other directions if needed + } + + arrowLayer.path = path.cgPath + } + + @objc private func primaryTapped() { + onPrimaryAction?() + } + + @objc private func dismissTapped() { + onDismiss?() + } +} +``` + +#### Task 1.2: Create TipCoordinator +**File**: `Envision/Tips/TipCoordinator.swift` + +```swift +import UIKit + +final class TipCoordinator { + static let shared = TipCoordinator() + + private init() {} + + // MARK: - Public API + + func showTip( + id: String, + configuration: TipBubbleView.Configuration, + in viewController: UIViewController, + containerView: UIView, + onPrimaryAction: @escaping () -> Void + ) { + // Check if already seen + guard !hasSeen(tipID: id), !TourManager.shared.tourSkipped else { return } + + // Create tip bubble + let tipView = TipBubbleView(configuration: configuration) + tipView.translatesAutoresizingMaskIntoConstraints = false + tipView.alpha = 0 + + tipView.onPrimaryAction = { [weak self, weak viewController] in + self?.markAsSeen(tipID: id) + self?.dismissTip(from: containerView) + onPrimaryAction() + } + + tipView.onDismiss = { [weak self] in + self?.markAsSeen(tipID: id) + self?.dismissTip(from: containerView) + } + + // Add to container + containerView.addSubview(tipView) + NSLayoutConstraint.activate([ + tipView.topAnchor.constraint(equalTo: containerView.topAnchor), + tipView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 16), + tipView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: -16) + ]) + + // Animate in + UIView.animate(withDuration: 0.3, delay: 0.2, options: .curveEaseOut) { + tipView.alpha = 1 + } + } + + func dismissTip(from containerView: UIView) { + guard let tipView = containerView.subviews.first(where: { $0 is TipBubbleView }) else { return } + + UIView.animate(withDuration: 0.2, animations: { + tipView.alpha = 0 + }) { _ in + tipView.removeFromSuperview() + } + } + + // MARK: - State Management + + private func hasSeen(tipID: String) -> Bool { + return TourManager.shared.seenTips.contains(tipID) + } + + private func markAsSeen(tipID: String) { + TourManager.shared.markTipAsSeen(tipID) + } +} +``` + +#### Task 1.3: Enhance TourManager +**File**: `Envision/Managers/TourManager.swift` + +```swift +// Add these properties and methods + +var seenTips: Set { + get { + guard let array = UserDefaults.standard.array(forKey: "seenTips") as? [String] else { + return [] + } + return Set(array) + } + set { + UserDefaults.standard.set(Array(newValue), forKey: "seenTips") + } +} + +var tourSkipped: Bool { + get { UserDefaults.standard.bool(forKey: "tourSkipped") } + set { UserDefaults.standard.set(newValue, forKey: "tourSkipped") } +} + +func markTipAsSeen(_ tipID: String) { + var seen = seenTips + seen.insert(tipID) + seenTips = seen +} + +func skipTour() { + tourSkipped = true +} + +func resetTour() { + hasCompletedTour = false + currentTourStep = 0 + seenTips = [] + tourSkipped = false +} +``` + +--- + +### Phase 2: Tip Definitions (Day 2-3) + +#### Task 2.1: Define Tip Content +**File**: `Envision/Tips/TipDefinitions.swift` + +```swift +import UIKit + +struct TipDefinition { + let id: String + let title: String + let message: String + let primaryActionTitle: String + let dismissActionTitle: String + let arrowEdge: TipBubbleView.ArrowEdge +} + +enum AppTips { + + // MARK: - Welcome + static let welcome = TipDefinition( + id: "welcome", + title: "Welcome to EnVision", + message: "Scan rooms with LiDAR and capture furniture with photogrammetry.", + primaryActionTitle: "Start Tour", + dismissActionTitle: "Skip", + arrowEdge: .top + ) + + // MARK: - My Rooms + static let roomsIntro = TipDefinition( + id: "rooms_intro", + title: "Scan Your First Room", + message: "Tap the camera button to start scanning with LiDAR.", + primaryActionTitle: "Try It", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + static let roomImport = TipDefinition( + id: "room_import", + title: "Import Existing Models", + message: "Already have USDZ files? Import them from Files app.", + primaryActionTitle: "Import", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + static let roomActions = TipDefinition( + id: "room_actions", + title: "Manage Your Rooms", + message: "Select multiple rooms to delete or share at once.", + primaryActionTitle: "Got It", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + static let roomCategories = TipDefinition( + id: "room_categories", + title: "Organize by Category", + message: "Filter rooms by type: Living Room, Bedroom, Kitchen, etc.", + primaryActionTitle: "Got It", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + // MARK: - My Furniture + static let furnitureIntro = TipDefinition( + id: "furniture_intro", + title: "Capture Furniture", + message: "Use 360° photogrammetry to create 3D models of objects.", + primaryActionTitle: "Try It", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + static let furnitureQuality = TipDefinition( + id: "furniture_quality", + title: "Best Results", + message: "Walk slowly around the object. Capture 30-50 photos for high quality.", + primaryActionTitle: "Got It", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + // MARK: - Profile + static let profileSettings = TipDefinition( + id: "profile_settings", + title: "Customize Settings", + message: "Change theme, enable notifications, and manage preferences.", + primaryActionTitle: "Open Settings", + dismissActionTitle: "Later", + arrowEdge: .top + ) + + static let tipsLibrary = TipDefinition( + id: "tips_library", + title: "Browse All Tips", + message: "View all tips and tutorials anytime from here.", + primaryActionTitle: "Got It", + dismissActionTitle: "Later", + arrowEdge: .top + ) +} +``` + +--- + +### Phase 3: Screen Integration (Day 3-5) + +#### Task 3.1: MyRoomsViewController Tips +**File**: `Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift` + +```swift +// Add these properties +private var tipContainerView: UIView! + +// In viewDidLoad() +setupTipContainer() + +// Add method +private func setupTipContainer() { + tipContainerView = UIView() + tipContainerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tipContainerView) + + NSLayoutConstraint.activate([ + tipContainerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), + tipContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tipContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) +} + +// In viewDidAppear() +showContextualTips() + +private func showContextualTips() { + if rooms.isEmpty { + // Show intro tip + TipCoordinator.shared.showTip( + id: AppTips.roomsIntro.id, + configuration: TipBubbleView.Configuration( + title: AppTips.roomsIntro.title, + message: AppTips.roomsIntro.message, + primaryActionTitle: AppTips.roomsIntro.primaryActionTitle, + dismissActionTitle: AppTips.roomsIntro.dismissActionTitle, + arrowEdge: .top, + arrowOffset: 0 + ), + in: self, + containerView: tipContainerView + ) { [weak self] in + // Primary action: trigger scan + self?.scanButtonTapped() + } + } else if rooms.count == 1 { + // Show actions menu tip + TipCoordinator.shared.showTip( + id: AppTips.roomActions.id, + configuration: TipBubbleView.Configuration( + title: AppTips.roomActions.title, + message: AppTips.roomActions.message, + primaryActionTitle: AppTips.roomActions.primaryActionTitle, + dismissActionTitle: AppTips.roomActions.dismissActionTitle, + arrowEdge: .top, + arrowOffset: 0 + ), + in: self, + containerView: tipContainerView + ) { } + } +} +``` + +#### Task 3.2: ScanFurnitureViewController Tips +Similar pattern to MyRoomsViewController. + +#### Task 3.3: ProfileViewController Tips +Similar pattern, show settings tip on first visit. + +--- + +### Phase 4: Welcome Flow (Day 5-6) + +#### Task 4.1: Welcome Tip in SplashViewController +**File**: `Envision/Screens/Onboarding/SplashViewController.swift` + +```swift +// After animation completes in goNext() +private func goNext() { + // ... existing animation code ... + + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in + guard let self = self else { return } + + if TourManager.shared.hasCompletedTour || TourManager.shared.tourSkipped { + // Skip onboarding, go straight to main app (if logged in) + if UserManager.shared.isLoggedIn { + self.showMainApp() + } else { + self.showOnboarding() + } + } else { + // First-time user: show onboarding + self.showOnboarding() + } + } +} +``` + +#### Task 4.2: Welcome Tip in MainTabBarController +**File**: `Envision/MainTabBarController.swift` + +```swift +override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + showWelcomeTipIfNeeded() +} + +private func showWelcomeTipIfNeeded() { + guard !TourManager.shared.hasSeen(tipID: AppTips.welcome.id) else { return } + + let alert = UIAlertController( + title: AppTips.welcome.title, + message: AppTips.welcome.message, + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Start Tour", style: .default) { _ in + TourManager.shared.startTour() + TourManager.shared.markTipAsSeen(AppTips.welcome.id) + }) + + alert.addAction(UIAlertAction(title: "Skip", style: .cancel) { _ in + TourManager.shared.skipTour() + }) + + present(alert, animated: true) +} +``` + +--- + +### Phase 5: Testing & Polish (Day 6-7) + +#### Task 5.1: Manual Testing +- [ ] Fresh install: welcome tip shows +- [ ] Skip tour: no more tips appear +- [ ] Start tour: tips appear in correct sequence +- [ ] Tip dismissal: "Later" button works +- [ ] Tip action: "Try It" button navigates correctly +- [ ] Restart tour: all tips show again +- [ ] Multiple screens: tips don't leak across tabs +- [ ] Rotation: tips reposition correctly +- [ ] Dark mode: tips use system colors +- [ ] Accessibility: VoiceOver reads tip content + +#### Task 5.2: Edge Cases +- [ ] User logs out mid-tour: tour state preserved +- [ ] User deletes all rooms: intro tip shows again +- [ ] App backgrounds mid-tip: tip still visible on return +- [ ] Rapid tab switching: no multiple tips stacked + +--- + +## 5. Visual Design Specs + +### 5.1 Colors +```swift +// Light Mode +backgroundColor: .systemBackground (white) +titleColor: .label (black) +messageColor: .secondaryLabel (gray) +primaryButtonBg: .systemBlue +primaryButtonText: .white +dismissButtonText: .secondaryLabel +closeButtonTint: .tertiaryLabel +shadowColor: UIColor.black.withAlphaComponent(0.15) + +// Dark Mode (automatic via system colors) +backgroundColor: .systemBackground (dark gray) +titleColor: .label (white) +// ... etc +``` + +### 5.2 Typography +```swift +title: .systemFont(ofSize: 16, weight: .semibold) +message: .systemFont(ofSize: 14, weight: .regular) +primaryButton: .systemFont(ofSize: 15, weight: .semibold) +dismissButton: .systemFont(ofSize: 15, weight: .regular) +``` + +### 5.3 Spacing +```swift +containerPadding: 16pt +cornerRadius: 16pt +buttonHeight: 44pt +buttonCornerRadius: 10pt +arrowHeight: 12pt +shadowRadius: 12pt +shadowOffset: (0, 4) +``` + +### 5.4 Animation +```swift +fadeIn: 0.3s, easeOut, delay 0.2s +fadeOut: 0.2s, easeIn +springAnimation: damping 0.7, velocity 0.5 +``` + +--- + +## 6. Code Structure + +### 6.1 New Files to Create +``` +Envision/Tips/ +├── TipBubbleView.swift (350 lines) +├── TipCoordinator.swift (120 lines) +├── TipDefinitions.swift (200 lines, all 25 tips) +└── README.md (Usage guide) +``` + +### 6.2 Files to Modify +``` +Envision/Managers/ +└── TourManager.swift (Add seenTips, tourSkipped) + +Envision/Screens/Onboarding/ +└── SplashViewController.swift (Add tour check in goNext) + +Envision/MainTabBarController.swift (Add welcome alert) + +Envision/Screens/MainTabs/Rooms/ +└── MyRoomsViewController.swift (Add tipContainerView, showContextualTips) + +Envision/Screens/MainTabs/furniture/ +└── ScanFurnitureViewController.swift (Add tips) + +Envision/Screens/MainTabs/profile/ +└── ProfileViewController.swift (Add tips) +``` + +### 6.3 Files to Keep Unchanged +``` +Envision/Screens/MainTabs/profile/SubScreens/ +└── TipsLibraryViewController.swift (Static tips list, no changes) +``` + +--- + +## 7. Testing Plan + +### 7.1 Unit Tests +```swift +// TourManagerTests.swift +func testMarkTipAsSeen() { + TourManager.shared.resetTour() + TourManager.shared.markTipAsSeen("test_tip") + XCTAssertTrue(TourManager.shared.seenTips.contains("test_tip")) +} + +func testSkipTour() { + TourManager.shared.skipTour() + XCTAssertTrue(TourManager.shared.tourSkipped) +} + +func testResetTour() { + TourManager.shared.markTipAsSeen("test_tip") + TourManager.shared.skipTour() + TourManager.shared.resetTour() + XCTAssertFalse(TourManager.shared.tourSkipped) + XCTAssertEqual(TourManager.shared.seenTips.count, 0) +} +``` + +### 7.2 UI Tests +```swift +// TipsUITests.swift +func testWelcomeTipAppears() { + app.launch() + // Reset tour state first + XCTAssertTrue(app.alerts["Welcome to EnVision"].waitForExistence(timeout: 3)) +} + +func testTipDismissal() { + app.buttons["Later"].tap() + XCTAssertFalse(app.otherElements["TipBubbleView"].exists) +} + +func testSkipTour() { + app.buttons["Skip"].tap() + // Verify no more tips appear +} +``` + +### 7.3 Manual Test Script +```markdown +## Test Case 1: Fresh Install +1. Delete app +2. Clean build & run +3. Verify welcome alert appears after splash +4. Tap "Start Tour" +5. Navigate to My Rooms +6. Verify "Scan Your First Room" tip appears +7. Tap "Try It" +8. Verify tip dismisses and scan starts + +## Test Case 2: Skip Tour +1. Fresh install +2. Tap "Skip" on welcome alert +3. Navigate through all tabs +4. Verify NO tips appear +5. Go to Profile → Restart App Tour +6. Verify tips appear again + +## Test Case 3: Layout Integrity +1. Show tip in My Rooms +2. Verify tip doesn't overlap search bar +3. Verify collection view scrolls normally +4. Tap on room cell (should work, not blocked by tip) +5. Rotate device (if applicable) +6. Verify tip repositions correctly + +## Test Case 4: Dark Mode +1. Enable Dark Mode in Settings +2. Trigger tip +3. Verify colors use system palette +4. Verify text is readable +``` + +--- + +## 8. Timeline & Milestones + +### Week 1: Foundation +**Day 1-2**: Core Components +- ✅ TipBubbleView with arrow pointer +- ✅ TipCoordinator for lifecycle +- ✅ Enhanced TourManager + +**Day 3**: Tip Definitions +- ✅ TipDefinitions.swift with all 25 tips +- ✅ Content review & copywriting + +**Day 4-5**: Screen Integration +- ✅ MyRoomsViewController tips +- ✅ ScanFurnitureViewController tips +- ✅ ProfileViewController tips + +**Day 6**: Welcome Flow +- ✅ SplashViewController tour check +- ✅ MainTabBarController welcome alert + +**Day 7**: Testing & Polish +- ✅ Manual testing all flows +- ✅ Bug fixes +- ✅ Animation tuning + +### Week 2: Validation +**Day 8-9**: Beta Testing +- Internal testing (team) +- Edge case discovery +- Accessibility audit + +**Day 10**: Final Fixes +- Address beta feedback +- Performance optimization +- Code review + +**Day 11**: Documentation +- Update README +- Usage guide for tips +- API documentation + +**Day 12**: Release +- Merge to main +- Deploy to TestFlight +- Monitor crash reports + +--- + +## Success Metrics + +### Technical +- ✅ Zero SwiftUI dependencies +- ✅ Zero TipKit dependencies +- ✅ No layout constraint errors in console +- ✅ Tips dismiss reliably (100% success rate) +- ✅ Buttons respond to first tap (no double-tap needed) +- ✅ No vertical text artifacts +- ✅ Tips don't block critical UI + +### User Experience +- ✅ Tour completion rate > 60% +- ✅ Skip rate < 40% +- ✅ Tip dismissal time < 5 seconds (engaged users read and act) +- ✅ Zero "tips broken" support tickets +- ✅ Positive feedback in App Store reviews mentioning onboarding + +--- + +## Rollback Plan + +If new tips system causes issues: +1. Comment out `showContextualTips()` calls (1 line per screen) +2. Tips disappear, app functions normally +3. Investigate issue offline +4. Re-enable when fixed + +**Critical**: Tips are non-blocking feature. App must work perfectly without them. + +--- + +## Appendix: Comparison Table + +| Aspect | Old (TipKit) | New (UIKit) | +|--------|--------------|-------------| +| Framework | SwiftUI TipView | Pure UIKit | +| Dependencies | TipKit (iOS 17+) | None | +| Layout | SwiftUI auto-layout | UIKit constraints | +| Touch Handling | SwiftUI gestures | UIButton actions | +| Stability | ❌ Unreliable | ✅ Predictable | +| Customization | ⚠️ Limited | ✅ Full control | +| Debugging | ❌ Hard | ✅ Easy | +| Maintenance | ❌ Breaking changes | ✅ Stable | +| Lines of Code | ~500 (with workarounds) | ~700 (clean) | + +--- + +**Document Version**: 1.0 +**Status**: Ready for Implementation +**Priority**: CRITICAL +**Estimated Effort**: 7-12 days +**Risk**: Low (fallback available) + +--- + +*End of Tips & Tour Improvement Plan*
Q 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 Z)#Mj4icIVWHWqGS+03p`O>FpB0RUVTdxZc1 delta 816 zcmZn(XbG6$C7U^hRb%H#xbvCT>X3G5tQOzh^*vX46gMI?k$T#|C~lNcBnkcGu2 zpAt`GmS!lJtS6zzvD9<%{VST>g4 z;9=put~g05=eQ$owJG&PD@JiBBgmZ$;@BL@puk|rV8o!yV9H<&Buy9$fTSgmXM*Yl zhza$i8G-D(N3Es*Bq6>d(IL6{@bps8LB264Ap-Ue13N=7LozVZQh_j!p%}%-tV=cm t6``q^{6mU|?S}B~uaY^F4WvaT%Sj_~6`9yBY%Ki4w3%I@n%D$y1pwZ1#Z3SJ diff --git a/Envision/AppDelegate.swift b/Envision/AppDelegate.swift index 8c01eca..df9aa2e 100644 --- a/Envision/AppDelegate.swift +++ b/Envision/AppDelegate.swift @@ -6,19 +6,31 @@ // import UIKit - - - import UIKit - - @main - class AppDelegate: UIResponder, UIApplicationDelegate { - - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - return true +import TipKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + + // REQUIRED: Initialize TipKit (iOS 17.0+) + if #available(iOS 17.0, *) { + do { + try Tips.configure([ + .displayFrequency(.immediate), + .datastoreLocation(.applicationDefault) + ]) + print(" TipKit configured successfully") + } catch { + print(" TipKit configuration failed: \(error)") + } } + + return true + } // Required for SceneDelegate lifecycle func application(_ application: UIApplication, diff --git a/Envision/Extensions/Extensions.swift b/Envision/Extensions/Extensions.swift index 0ea6521..762552d 100644 --- a/Envision/Extensions/Extensions.swift +++ b/Envision/Extensions/Extensions.swift @@ -20,7 +20,7 @@ import UIKit // self.init(red: r, green: g, blue: b, alpha: a) // } //} - +//abhinav extension UIView { func applyGradientBackground(colors: [UIColor]) { let gradientLayer = CAGradientLayer() diff --git a/Envision/Extensions/SaveManager.swift b/Envision/Extensions/SaveManager.swift index 7d1f5c5..e699646 100644 --- a/Envision/Extensions/SaveManager.swift +++ b/Envision/Extensions/SaveManager.swift @@ -25,7 +25,7 @@ enum ModelType { case .furniture: return "Furniture" case .room: return "Room" } - } + } } struct ModelMetadata: Codable { @@ -143,7 +143,7 @@ final class SaveManager { completion(true) } } catch { - print("❌ Error deleting model: \(error)") + print("Error deleting model: \(error)") DispatchQueue.main.async { completion(false) } diff --git a/Envision/Extensions/UserManager.swift b/Envision/Extensions/UserManager.swift index b0f81ce..4d8dd88 100644 --- a/Envision/Extensions/UserManager.swift +++ b/Envision/Extensions/UserManager.swift @@ -112,7 +112,7 @@ final class UserManager { } return true } catch { - print("❌ Failed to save profile image: \(error)") + print(" Failed to save profile image: \(error)") return false } } diff --git a/Envision/MainTabBarController.swift b/Envision/MainTabBarController.swift index 7e656df..8505ee3 100644 --- a/Envision/MainTabBarController.swift +++ b/Envision/MainTabBarController.swift @@ -1,12 +1,23 @@ import UIKit +import TipKit +import SwiftUI final class MainTabBarController: UITabBarController { + + private var tipHostingController: UIViewController? override func viewDidLoad() { super.viewDidLoad() setupTabs() setupLiquidGlassEffect() } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if #available(iOS 17.0, *) { + showWelcomeTip() + } + } private func setupTabs() { // let home = UINavigationController(rootViewController: RoomsViewController()) @@ -14,7 +25,7 @@ final class MainTabBarController: UITabBarController { home.tabBarItem = UITabBarItem(title: "My Rooms", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) let scan = UINavigationController(rootViewController: ScanFurnitureViewController()) - scan.tabBarItem = UITabBarItem(title: "My Furniture", image: UIImage(named: "sofa.viewfinder"), selectedImage: UIImage(named: "sofa.viewfinder")) + scan.tabBarItem = UITabBarItem(title: "My Furniture", image: UIImage(named: "sofa.viewfinder"), selectedImage: UIImage(named: "custom.sofafill.viewfinder")) // let shop = UINavigationController(rootViewController: ShopViewController()) // shop.tabBarItem = UITabBarItem(title: "Shop", image: UIImage(systemName: "bag"), selectedImage: UIImage(systemName: "bag.fill")) @@ -46,4 +57,61 @@ final class MainTabBarController: UITabBarController { tabBar.scrollEdgeAppearance = appearance tabBar.isTranslucent = true } + + @available(iOS 17.0, *) + private func showWelcomeTip() { + guard TourManager.shared.shouldShowTour() else { return } + + if let existing = tipHostingController { + existing.willMove(toParent: nil) + existing.view.removeFromSuperview() + existing.removeFromParent() + tipHostingController = nil + } + + let tip = WelcomeTip() + + let actionHandler: (Tip.Action) -> Void = { [weak self] action in + guard let self = self else { return } + switch action.id { + case "start-tour": + TourManager.shared.startTour() + self.selectedIndex = 0 + self.dismissTip() + case "skip": + TourManager.shared.completeTour() + self.dismissTip() + default: break + } + } + + let tipView = TipView(tip, arrowEdge: .bottom) { action in + actionHandler(action) + } + + let hostingController = UIHostingController(rootView: tipView) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = UIColor.clear + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + + NSLayoutConstraint.activate([ + hostingController.view.centerXAnchor.constraint(equalTo: view.centerXAnchor), + hostingController.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor, constant: -20), + hostingController.view.widthAnchor.constraint(lessThanOrEqualToConstant: 350) + ]) + + self.tipHostingController = hostingController + } + + private func dismissTip() { + if let existing = tipHostingController { + existing.willMove(toParent: nil) + existing.view.removeFromSuperview() + existing.removeFromParent() + tipHostingController = nil + } + } } diff --git a/Envision/Managers/TourManager.swift b/Envision/Managers/TourManager.swift new file mode 100644 index 0000000..769a643 --- /dev/null +++ b/Envision/Managers/TourManager.swift @@ -0,0 +1,177 @@ +// +// TourManager.swift +// Envision +// +// Created for EnVision Tips & Tour System +// Version: 1.0 +// + +import Foundation +import TipKit + +/// Centralized manager for tour state, progress tracking, and tour lifecycle +final class TourManager { + + // MARK: - Singleton + + static let shared = TourManager() + + // MARK: - Private Properties + + private let userDefaults = UserDefaults.standard + private let tourCompletedKey = "app_tour_completed" + private let tourStepKey = "current_tour_step" + private let hasSeenWelcomeKey = "has_seen_welcome" + private let firstLaunchKey = "is_first_launch" + + // MARK: - Public Properties + + /// Whether the tour has been completed + var isTourCompleted: Bool { + get { userDefaults.bool(forKey: tourCompletedKey) } + set { userDefaults.set(newValue, forKey: tourCompletedKey) } + } + + /// Current step in the tour sequence (0-based) + var currentTourStep: Int { + get { userDefaults.integer(forKey: tourStepKey) } + set { userDefaults.set(newValue, forKey: tourStepKey) } + } + + /// Whether user has seen the welcome tip + var hasSeenWelcome: Bool { + get { userDefaults.bool(forKey: hasSeenWelcomeKey) } + set { userDefaults.set(newValue, forKey: hasSeenWelcomeKey) } + } + + /// Whether this is the first app launch + var isFirstLaunch: Bool { + get { !userDefaults.bool(forKey: firstLaunchKey) } + set { userDefaults.set(!newValue, forKey: firstLaunchKey) } + } + + // MARK: - Initialization + + private init() { + // Check if first launch + if isFirstLaunch { + print("📱 First launch detected - Tour will be shown") + } + } + + // MARK: - Tour Control Methods + + /// Determines if the tour should be shown + /// - Returns: Bool indicating if tour should be shown + func shouldShowTour() -> Bool { + // Don't show if already completed + if isTourCompleted { + return false + } + + // Show for first-time users or users who haven't completed + return isFirstLaunch || !hasSeenWelcome + } + + /// Initiates the tour sequence + func startTour() { + currentTourStep = 0 + isTourCompleted = false + hasSeenWelcome = true + isFirstLaunch = false + print("🎬 Tour started") + } + + /// Marks the tour as completed + func completeTour() { + isTourCompleted = true + currentTourStep = 0 + print("✅ Tour completed") + } + + /// Resets all tour progress and tips + func resetTour() { + isTourCompleted = false + currentTourStep = 0 + hasSeenWelcome = false + + // Reset TipKit datastore (iOS 17+) + if #available(iOS 17.0, *) { + do { + try Tips.resetDatastore() + print("🔄 Tips datastore reset") + } catch { + print("❌ Failed to reset tips: \(error)") + } + } + + print("🔄 Tour reset complete") + } + + /// Advances to the next tour step + func nextStep() { + currentTourStep += 1 + print("➡️ Tour step: \(currentTourStep)") + } + + /// Skips to a specific step + /// - Parameter step: The step number to skip to + func skipToStep(_ step: Int) { + currentTourStep = step + print("⏭️ Skipped to step: \(step)") + } + + // MARK: - Helper Methods + + /// Checks if user has any rooms + func hasRooms() -> Bool { + return !SaveManager.shared.getSavedModels(type: .room).isEmpty + } + + /// Checks if user has any furniture + func hasFurniture() -> Bool { + return !SaveManager.shared.getSavedModels(type: .furniture).isEmpty + } + + /// Checks if user has both rooms and furniture + func hasRoomsAndFurniture() -> Bool { + return hasRooms() && hasFurniture() + } + + /// Gets the count of rooms + func roomCount() -> Int { + return SaveManager.shared.getSavedModels(type: .room).count + } + + /// Gets the count of furniture + func furnitureCount() -> Int { + return SaveManager.shared.getSavedModels(type: .furniture).count + } + + // MARK: - Debug Methods + + #if DEBUG + func debugPrintState() { + print(""" + ═══════════════════════════════════ + TOUR DEBUG STATE + ═══════════════════════════════════ + Is First Launch: \(isFirstLaunch) + Tour Completed: \(isTourCompleted) + Current Step: \(currentTourStep) + Has Seen Welcome: \(hasSeenWelcome) + Should Show Tour: \(shouldShowTour()) + Has Rooms: \(hasRooms()) + Has Furniture: \(hasFurniture()) + Room Count: \(roomCount()) + Furniture Count: \(furnitureCount()) + ═══════════════════════════════════ + """) + } + + func forceShowTour() { + resetTour() + print("✅ Tour forced to show") + } + #endif +} diff --git a/Envision/SceneDelegate.swift b/Envision/SceneDelegate.swift index 127602c..103229e 100644 --- a/Envision/SceneDelegate.swift +++ b/Envision/SceneDelegate.swift @@ -18,8 +18,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - window.rootViewController = SplashViewController() - // window.rootViewController = MainTabBarController() + // window.rootViewController = SplashViewController() + window.rootViewController = MainTabBarController() self.window = window window.makeKeyAndVisible() diff --git a/Envision/Screens/.DS_Store b/Envision/Screens/.DS_Store index 68f793b6ad838c0c7b3c37e81dafa11d344561d5..76b140b2e943eaf4f24c70b22db050c2806ceadc 100644 GIT binary patch delta 88 zcmZoMXfc=|#>B)qu~2NHo}wrR0|Nsi1A_nqLkL46LlQ%APP$?6#=_-{6ZP08yR!0c o=3xELw6Wm@(`I%Keh#3%&4L`?nJ4p$ID&M7w6biD5Lv?v0E6)p_y7O^ delta 317 zcmZoMXfc=|#>B!ku~2NHo}#D#0|Nsi1A_oVa(-?BkPQSTJevcVmowLcq?i~S8G>@s z4TF)TVdA;@E-pzq`AI-Nhk&5%@931{jtIFFoN@&j$mTf!wJ@*(?I~p_VaQ-Gg4vb@ z5{CjLX=F<|9-Xr)gIJ290%0|F6{rqiV3=&cB(hn7=|9tEb`E|HVBi1);5+kVei26w PVAwE$9I-h String { + let fm = FileManager.default + if let attrs = try? fm.attributesOfItem(atPath: url.path), + let date = attrs[.creationDate] as? Date { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter.string(from: date) + } + return "--" + } + // MARK: - UI Helpers private func showLoading(_ msg: String) { loadingLabel.text = msg @@ -521,6 +557,64 @@ final class MyRoomsViewController: UIViewController { activityIndicator.stopAnimating() } + // MARK: - Empty State + private func setupEmptyState() { + emptyStateView = UIView() + emptyStateView.translatesAutoresizingMaskIntoConstraints = false + emptyStateView.isHidden = true + + let iconView = UIImageView() + iconView.image = UIImage(systemName: "house") + iconView.tintColor = .systemGray3 + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + + let titleLabel = UILabel() + titleLabel.text = "No Rooms Yet" + titleLabel.font = .boldSystemFont(ofSize: 22) + titleLabel.textColor = .label + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + + let messageLabel = UILabel() + messageLabel.text = "Tap the camera icon to scan a room\nor import USDZ files" + messageLabel.font = .systemFont(ofSize: 16) + messageLabel.textColor = .secondaryLabel + messageLabel.textAlignment = .center + messageLabel.numberOfLines = 0 + messageLabel.translatesAutoresizingMaskIntoConstraints = false + + emptyStateView.addSubview(iconView) + emptyStateView.addSubview(titleLabel) + emptyStateView.addSubview(messageLabel) + view.addSubview(emptyStateView) + + NSLayoutConstraint.activate([ + emptyStateView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyStateView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + emptyStateView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: 40), + emptyStateView.trailingAnchor.constraint(lessThanOrEqualTo: view.trailingAnchor, constant: -40), + + iconView.topAnchor.constraint(equalTo: emptyStateView.topAnchor), + iconView.centerXAnchor.constraint(equalTo: emptyStateView.centerXAnchor), + iconView.widthAnchor.constraint(equalToConstant: 80), + iconView.heightAnchor.constraint(equalToConstant: 80), + + titleLabel.topAnchor.constraint(equalTo: iconView.bottomAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: emptyStateView.leadingAnchor), + titleLabel.trailingAnchor.constraint(equalTo: emptyStateView.trailingAnchor), + + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), + messageLabel.leadingAnchor.constraint(equalTo: emptyStateView.leadingAnchor), + messageLabel.trailingAnchor.constraint(equalTo: emptyStateView.trailingAnchor), + messageLabel.bottomAnchor.constraint(equalTo: emptyStateView.bottomAnchor) + ]) + } + + private func updateEmptyState() { + emptyStateView.isHidden = !roomFiles.isEmpty + } + func showToast(message: String) { let toast = UILabel() toast.text = message @@ -649,4 +743,85 @@ final class ChipCell: UICollectionViewCell { button.setAttributedTitle(attributedString, for: .normal) } -} \ No newline at end of file +} + +@available(iOS 17.0, *) +extension MyRoomsViewController { + + private func setupTips() { + // Initial setup if needed + } + + private func updateTipParameters() { + MyRoomsIntroTip.hasRooms = !roomFiles.isEmpty + RoomActionsMenuTip.roomCount = roomFiles.count + RoomCategoriesTip.roomCount = roomFiles.count + } + + private func showContextualTip() { + dismissTip() + + var tip: (any Tip)? + var edge: TipKit.Edge = .top + + if roomFiles.isEmpty { + tip = MyRoomsIntroTip() + edge = .top + } else if roomFiles.count == 1 { + tip = RoomImportTip() + edge = .top + } else if roomFiles.count >= 2 { + tip = RoomCategoriesTip() + edge = .bottom + } + + guard let tipToDisplay = tip else { return } + + let actionHandler: (Tip.Action) -> Void = { [weak self] action in + self?.handleTipAction(action) + } + + let tipView = TipView(tipToDisplay, arrowEdge: edge) { action in + actionHandler(action) + } + + let hostingController = UIHostingController(rootView: tipView) + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = UIColor.clear + + addChild(hostingController) + view.addSubview(hostingController.view) + hostingController.didMove(toParent: self) + self.tipHostingController = hostingController + + NSLayoutConstraint.activate([ + hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16) + ]) + + if tipToDisplay is MyRoomsIntroTip || tipToDisplay is RoomImportTip { + hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8).isActive = true + } else if tipToDisplay is RoomCategoriesTip { + hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 60).isActive = true + } else { + hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16).isActive = true + } + } + + private func dismissTip() { + if let existing = tipHostingController { + existing.willMove(toParent: nil) + existing.view.removeFromSuperview() + existing.removeFromParent() + tipHostingController = nil + } + } + + private func handleTipAction(_ action: Tip.Action) { + switch action.id { + case "scan": scanTapped(); dismissTip() + case "later": dismissTip() + default: dismissTip() + } + } +} diff --git a/Envision/Screens/MainTabs/Rooms/RoomCell.swift b/Envision/Screens/MainTabs/Rooms/RoomCell.swift index d487ed8..077d93c 100644 --- a/Envision/Screens/MainTabs/Rooms/RoomCell.swift +++ b/Envision/Screens/MainTabs/Rooms/RoomCell.swift @@ -34,6 +34,15 @@ final class RoomCell: UICollectionViewCell { return lbl }() + private let dateLabel: UILabel = { + let lbl = UILabel() + lbl.translatesAutoresizingMaskIntoConstraints = false + lbl.font = .systemFont(ofSize: 11, weight: .regular) + lbl.textColor = .tertiaryLabel + lbl.numberOfLines = 1 + return lbl + }() + private let container: UIView = { let v = UIView() v.translatesAutoresizingMaskIntoConstraints = false @@ -131,6 +140,7 @@ final class RoomCell: UICollectionViewCell { container.addSubview(thumbnailView) container.addSubview(titleLabel) container.addSubview(sizeLabel) + container.addSubview(dateLabel) thumbnailView.addSubview(categoryBadge) thumbnailView.addSubview(roomTypeBadge) @@ -176,7 +186,12 @@ final class RoomCell: UICollectionViewCell { sizeLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2), sizeLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8), sizeLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8), - sizeLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8), + + // Date + dateLabel.topAnchor.constraint(equalTo: sizeLabel.bottomAnchor, constant: 2), + dateLabel.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 8), + dateLabel.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -8), + dateLabel.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -8), // RoomType Badge roomTypeBadge.topAnchor.constraint(equalTo: thumbnailView.topAnchor, constant: 8), @@ -237,6 +252,7 @@ final class RoomCell: UICollectionViewCell { thumbnailView.image = nil titleLabel.text = nil sizeLabel.text = nil + dateLabel.text = nil categoryBadge.isHidden = true roomTypeBadge.isHidden = true selectionCircle.image = UIImage(systemName: "circle") @@ -247,12 +263,16 @@ final class RoomCell: UICollectionViewCell { func configure( fileName: String, size: String, + dateText: String, thumbnail: UIImage?, category: RoomCategory? = nil, roomType: RoomType? = nil ) { - titleLabel.text = fileName + // Remove extension from fileName + let nameWithoutExtension = (fileName as NSString).deletingPathExtension + titleLabel.text = nameWithoutExtension sizeLabel.text = size + dateLabel.text = dateText thumbnailView.image = thumbnail ?? UIImage(systemName: "arkit") categoryBadge.isHidden = true diff --git a/Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPreviewViewController.swift b/Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPreviewViewController.swift index 752c352..5c2359d 100644 --- a/Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPreviewViewController.swift +++ b/Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPreviewViewController.swift @@ -95,7 +95,7 @@ final class RoomPreviewViewController: UIViewController { private let saveButton: UIButton = { let btn = UIButton(type: .system) - btn.setTitle("💾 Save to My Rooms", for: .normal) + btn.setTitle("Save to My Rooms", for: .normal) btn.setTitle("✓ Saved!", for: .disabled) btn.titleLabel?.font = AppFonts.semibold(17) btn.backgroundColor = AppColors.accent @@ -434,9 +434,9 @@ final class RoomPreviewViewController: UIViewController { self.showSuccessAnimation() case .failure(let error): - print("❌ Save error: \(error)") + print(" Save error: \(error)") self.saveButton.isEnabled = true - self.saveButton.setTitle("💾 Save to My Rooms", for: .normal) + self.saveButton.setTitle(" Save to My Rooms", for: .normal) self.showErrorAlert(message: "Failed to save room. Please try again.") } } @@ -546,4 +546,4 @@ extension RoomPreviewViewController: UITextFieldDelegate { textField.resignFirstResponder() return true } -} \ No newline at end of file +} diff --git a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift index d5fdefc..784a098 100644 --- a/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift +++ b/Envision/Screens/MainTabs/Rooms/furniture+room/RoomVisualizeVC.swift @@ -10,6 +10,10 @@ final class RoomVisualizeVC: UIViewController { private var roomModel: ModelEntity? private var displayedModel: ModelEntity? private var placedFurniture: [ModelEntity] = [] + private var isMeasuringMode = false + private var measurementPoints: [SIMD3] = [] + private var measurementLabel: UILabel? + private var measurementLine: ModelEntity? // MARK: - Camera private let cameraAnchor = AnchorEntity() @@ -65,13 +69,205 @@ final class RoomVisualizeVC: UIViewController { // MARK: - Navigation private func setupNavigation() { - navigationItem.rightBarButtonItem = UIBarButtonItem( + let rulerButton = UIBarButtonItem( + image: UIImage(systemName: "ruler"), + style: .plain, + target: self, + action: #selector(rulerTapped) + ) + rulerButton.tintColor = .systemBlue + + let addButton = UIBarButtonItem( image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addFurnitureTapped) ) - navigationItem.rightBarButtonItem?.tintColor = .systemGreen + addButton.tintColor = .systemGreen + + navigationItem.rightBarButtonItems = [addButton, rulerButton] + } + + @objc private func rulerTapped() { + isMeasuringMode.toggle() + + // Update button appearance + if let rulerButton = navigationItem.rightBarButtonItems?.last { + rulerButton.tintColor = isMeasuringMode ? .systemOrange : .systemBlue + rulerButton.image = UIImage(systemName: isMeasuringMode ? "ruler.fill" : "ruler") + } + + if isMeasuringMode { + showMeasurementInstructions() + setupMeasurementTapGesture() + } else { + clearMeasurement() + removeMeasurementTapGesture() + } + } + + private func showMeasurementInstructions() { + let toast = UILabel() + toast.text = "Tap two points to measure distance" + toast.font = .systemFont(ofSize: 15, weight: .medium) + toast.textColor = .white + toast.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.9) + toast.textAlignment = .center + toast.layer.cornerRadius = 12 + toast.clipsToBounds = true + toast.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(toast) + + NSLayoutConstraint.activate([ + toast.centerXAnchor.constraint(equalTo: view.centerXAnchor), + toast.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10), + toast.heightAnchor.constraint(equalToConstant: 40), + toast.widthAnchor.constraint(greaterThanOrEqualToConstant: 250) + ]) + + UIView.animate(withDuration: 0.3, delay: 2.5) { + toast.alpha = 0 + } completion: { _ in + toast.removeFromSuperview() + } + } + + private var measurementTapGesture: UITapGestureRecognizer? + + private func setupMeasurementTapGesture() { + let tap = UITapGestureRecognizer(target: self, action: #selector(handleMeasurementTap(_:))) + arView.addGestureRecognizer(tap) + measurementTapGesture = tap + } + + private func removeMeasurementTapGesture() { + if let gesture = measurementTapGesture { + arView.removeGestureRecognizer(gesture) + measurementTapGesture = nil + } + } + + @objc private func handleMeasurementTap(_ gesture: UITapGestureRecognizer) { + let location = gesture.location(in: arView) + + // Raycast to find 3D point + guard let result = arView.entity(at: location) else { return } + + let worldPosition = result.position(relativeTo: nil) + measurementPoints.append(worldPosition) + + // Add visual marker at tap point + addMeasurementMarker(at: worldPosition) + + if measurementPoints.count == 2 { + calculateAndDisplayDistance() + } else if measurementPoints.count > 2 { + // Reset and start new measurement + clearMeasurement() + measurementPoints.append(worldPosition) + addMeasurementMarker(at: worldPosition) + } + } + + private func addMeasurementMarker(at position: SIMD3) { + let sphere = MeshResource.generateSphere(radius: 0.02) + let material = SimpleMaterial(color: .systemOrange, isMetallic: false) + let marker = ModelEntity(mesh: sphere, materials: [material]) + marker.position = position + marker.name = "measurementMarker" + + if let anchor = arView.scene.anchors.first { + anchor.addChild(marker) + } + } + + private func calculateAndDisplayDistance() { + guard measurementPoints.count >= 2 else { return } + + let point1 = measurementPoints[0] + let point2 = measurementPoints[1] + + // Calculate distance + let distance = simd_distance(point1, point2) + + // Convert to real-world units (assuming model scale) + let realDistance = distance / 0.005 // Adjust based on your model scale + + // Create line between points + drawMeasurementLine(from: point1, to: point2) + + // Display distance + showDistanceLabel(distance: realDistance) + } + + private func drawMeasurementLine(from start: SIMD3, to end: SIMD3) { + // Remove existing line + measurementLine?.removeFromParent() + + let distance = simd_distance(start, end) + let midPoint = (start + end) / 2 + + let mesh = MeshResource.generateBox(size: [0.005, 0.005, distance]) + let material = SimpleMaterial(color: .systemOrange, isMetallic: false) + let line = ModelEntity(mesh: mesh, materials: [material]) + + line.position = midPoint + line.look(at: end, from: midPoint, relativeTo: nil) + line.name = "measurementLine" + + if let anchor = arView.scene.anchors.first { + anchor.addChild(line) + } + measurementLine = line + } + + private func showDistanceLabel(distance: Float) { + measurementLabel?.removeFromSuperview() + + let label = UILabel() + let distanceInMeters = distance + let distanceInCm = distance * 100 + let distanceInFeet = distance * 3.28084 + + if distanceInMeters >= 1 { + label.text = String(format: "📏 %.2f m (%.1f ft)", distanceInMeters, distanceInFeet) + } else { + label.text = String(format: "📏 %.1f cm (%.1f in)", distanceInCm, distanceInCm / 2.54) + } + + label.font = .systemFont(ofSize: 18, weight: .bold) + label.textColor = .white + label.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.95) + label.textAlignment = .center + label.layer.cornerRadius = 12 + label.clipsToBounds = true + label.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(label) + measurementLabel = label + + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: view.centerXAnchor), + label.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), + label.heightAnchor.constraint(equalToConstant: 50), + label.widthAnchor.constraint(greaterThanOrEqualToConstant: 200) + ]) + } + + private func clearMeasurement() { + measurementPoints.removeAll() + measurementLabel?.removeFromSuperview() + measurementLabel = nil + measurementLine?.removeFromParent() + measurementLine = nil + + // Remove all measurement markers + arView.scene.anchors.first?.children.forEach { entity in + if entity.name == "measurementMarker" || entity.name == "measurementLine" { + entity.removeFromParent() + } + } } // MARK: - Gestures @@ -83,9 +279,13 @@ final class RoomVisualizeVC: UIViewController { // MARK: - Loading private func loadRoom() { Task { - let entity = try await Entity.load(contentsOf: roomURL) - await MainActor.run { - setupScene(with: entity) + do { + let entity = try await Entity(contentsOf: roomURL) + await MainActor.run { + setupScene(with: entity) + } + } catch { + print("Failed to load room: \(error)") } } } diff --git a/Envision/Screens/MainTabs/furniture/CreateModel/CreateModelViewController2.swift b/Envision/Screens/MainTabs/furniture/CreateModel/CreateModelViewController2.swift index 6d46328..197c0d0 100644 --- a/Envision/Screens/MainTabs/furniture/CreateModel/CreateModelViewController2.swift +++ b/Envision/Screens/MainTabs/furniture/CreateModel/CreateModelViewController2.swift @@ -59,24 +59,24 @@ class CreateModelViewController2: UIViewController { for try await output in session.outputs { switch output { case .processingComplete: - print("✔ Processing Completed") + print(" Processing Completed") case .requestError(let id, let error): - print("❌ Error in request \(id): \(error.localizedDescription)") + print(" Error in request \(id): \(error.localizedDescription)") case .requestProgress(let id, let progress): let percent = Int(progress * 100) - print("⏳ Progress (\(id)): \(percent)%") + print(" Progress (\(id)): \(percent)%") case .requestComplete(let id, let result): - print("🎉 Request \(id) completed: \(result)") + print(" Request \(id) completed: \(result)") default: break } } } catch { - print("❌ Session failed: \(error)") + print(" Session failed: \(error)") } } @@ -84,7 +84,7 @@ class CreateModelViewController2: UIViewController { try session.process(requests: [request]) } catch { - print("❌ Couldn't create PhotogrammetrySession: \(error)") + print(" Couldn't create PhotogrammetrySession: \(error)") } } diff --git a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift index 576ef17..f070324 100644 --- a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift +++ b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift @@ -169,17 +169,17 @@ final class ObjectCapturePreviewController: UIViewController { // Quality check if photoCount < 20 { - statusLabel.text = "⚠️ Low photo count. For best results, capture 30-50 photos" + statusLabel.text = " Low photo count. For best results, capture 30-50 photos" statusLabel.textColor = .systemOrange } else if photoCount > 100 { - statusLabel.text = "✓ Excellent coverage! Ready for high-quality model" + statusLabel.text = " Excellent coverage! Ready for high-quality model" statusLabel.textColor = .systemGreen } else { - statusLabel.text = "✓ Good photo count. Ready to generate" + statusLabel.text = " Good photo count. Ready to generate" statusLabel.textColor = .systemGreen } } catch { - print("❌ Error loading images: \(error)") + print(" Error loading images: \(error)") } } @@ -310,14 +310,14 @@ final class ObjectCapturePreviewController: UIViewController { // Optimized configuration var config = PhotogrammetrySession.Configuration() config.sampleOrdering = .sequential - config.featureSensitivity = .normal + config.featureSensitivity = .high DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } - print("🔍 Starting photogrammetry with \(self.photoCount) images") - print("📁 Input folder: \(self.imagesFolder.path)") - print("💾 Output: \(outputURL.path)") + print(" Starting photogrammetry with \(self.photoCount) images") + print(" Input folder: \(self.imagesFolder.path)") + print(" Output: \(outputURL.path)") guard let session = try? PhotogrammetrySession( input: self.imagesFolder, @@ -340,10 +340,10 @@ final class ObjectCapturePreviewController: UIViewController { case .processingComplete: - print("✅ Processing complete!") + print(" Processing complete!") case .inputComplete: - print("📥 Input complete") + print(" Input complete") case .requestProgress(let request, let fraction): let percentage = Int(fraction * 100) @@ -367,7 +367,7 @@ final class ObjectCapturePreviewController: UIViewController { case .requestComplete(let request, let result): let elapsed = Date().timeIntervalSince(startTime) - print("✅ Request complete in \(String(format: "%.1f", elapsed))s") + print(" Request complete in \(String(format: "%.1f", elapsed))s") DispatchQueue.main.async { self.progressView.progress = 1.0 @@ -378,31 +378,31 @@ final class ObjectCapturePreviewController: UIViewController { } case .requestError(let request, let error): - print("❌ Request error: \(error)") + print(" Request error: \(error)") DispatchQueue.main.async { self.handleError(message: "Processing failed: \(error.localizedDescription)") } case .processingCancelled: - print("⚠️ Processing cancelled") + print(" Processing cancelled") DispatchQueue.main.async { self.handleError(message: "Processing was cancelled") } case .invalidSample(let id, let reason): - print("⚠️ Invalid sample \(id): \(reason)") + print(" Invalid sample \(id): \(reason)") case .skippedSample(let id): - print("⏭️ Skipped sample: \(id)") + print(" Skipped sample: \(id)") case .automaticDownsampling: - print("📉 Automatic downsampling applied") + print(" Automatic downsampling applied") default: - print("⚠️ Unknown output: \(output)") + print(" Unknown output: \(output)") } } } catch { - print("❌ Task error: \(error)") + print(" Task error: \(error)") DispatchQueue.main.async { self.handleError(message: "An error occurred: \(error.localizedDescription)") } @@ -412,7 +412,7 @@ final class ObjectCapturePreviewController: UIViewController { do { try session.process(requests: [request]) } catch { - print("❌ Process error: \(error)") + print(" Process error: \(error)") DispatchQueue.main.async { self.handleError(message: "Failed to start processing: \(error.localizedDescription)") } @@ -465,7 +465,7 @@ final class ObjectCapturePreviewController: UIViewController { self.generateButton.setTitle("View in My Models", for: .normal) self.generateButton.isEnabled = true - print("✅ Model saved to: \(savedURL.path)") + print(" Model saved to: \(savedURL.path)") // Auto-navigate after 1.5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { diff --git a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift index 93e8f09..c187289 100644 --- a/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift +++ b/Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift @@ -304,7 +304,8 @@ Move the phone slowly around the object // MARK: - Auto Capture private func startAutoCapture() { - captureTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + // Capture every 0.5 seconds for better overlap between photos + captureTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in self.takePhoto() } } @@ -319,7 +320,7 @@ Move the phone slowly around the object let count = images.count if count < 20 { - qualityIndicator.text = "⚠️ Keep capturing" + qualityIndicator.text = " Keep capturing" qualityIndicator.textColor = .systemYellow qualityIndicator.backgroundColor = UIColor.systemYellow.withAlphaComponent(0.2) stopButton.isEnabled = false @@ -332,17 +333,17 @@ Move the phone slowly around the object stopButton.alpha = 1.0 guidanceLabel.text = "Good! Continue for better quality" } else if count < 50 { - qualityIndicator.text = "✓✓ Good coverage" + qualityIndicator.text = " Good coverage" qualityIndicator.textColor = .systemGreen qualityIndicator.backgroundColor = UIColor.systemGreen.withAlphaComponent(0.3) guidanceLabel.text = "Excellent! Almost there..." } else if count < 80 { - qualityIndicator.text = "✓✓✓ Excellent!" + qualityIndicator.text = " Excellent!" qualityIndicator.textColor = .systemTeal qualityIndicator.backgroundColor = UIColor.systemTeal.withAlphaComponent(0.3) guidanceLabel.text = "Perfect coverage! Tap Finish" } else { - qualityIndicator.text = "🏆 Maximum coverage" + qualityIndicator.text = " Maximum coverage" qualityIndicator.textColor = .systemPurple qualityIndicator.backgroundColor = UIColor.systemPurple.withAlphaComponent(0.3) guidanceLabel.text = "Outstanding! Ready to process" @@ -357,7 +358,7 @@ Move the phone slowly around the object // Calculate capture duration if let startTime = captureStartTime { let duration = Date().timeIntervalSince(startTime) - print("📸 Capture completed:") + print(" Capture completed:") print(" • Photos: \(images.count)") print(" • Duration: \(String(format: "%.1f", duration))s") print(" • Average: \(String(format: "%.1f", duration / Double(images.count)))s per photo") diff --git a/Envision/Screens/MainTabs/furniture/ScanFurnitureViewController.swift b/Envision/Screens/MainTabs/furniture/ScanFurnitureViewController.swift index 65597b0..9e89639 100644 --- a/Envision/Screens/MainTabs/furniture/ScanFurnitureViewController.swift +++ b/Envision/Screens/MainTabs/furniture/ScanFurnitureViewController.swift @@ -1089,3 +1089,4 @@ final class FurnitureChipCell: UICollectionViewCell { button.setAttributedTitle(attributedString, for: .normal) } } + diff --git a/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift b/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift index 020571c..31dd7ac 100644 --- a/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift +++ b/Envision/Screens/MainTabs/furniture/roomPlanColor/RoomARWithFurnitureViewController.swift @@ -165,7 +165,7 @@ func replaceEntities(prefix: String, in root: Entity, with modelName: String, sc let originalTransform = entity.transformMatrix(relativeTo: parent) guard let newModel = try? Entity.load(named: modelName) else { - print("❌ Failed to load:", modelName) + print(" Failed to load:", modelName) return } diff --git a/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift b/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift index 2c52004..d17a4e5 100644 --- a/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift +++ b/Envision/Screens/MainTabs/furniture/roomPlanColor/VisualizeRoomViewController.swift @@ -148,7 +148,7 @@ class VisualizeRoomViewController: UIViewController, UIDocumentPickerDelegate { let entity = try Entity.load(contentsOf: url) placeModel(entity) } catch { - print("❌ Failed to load model:", error) + print(" Failed to load model:", error) } } diff --git a/Envision/Screens/MainTabs/profile/ProfileViewController.swift b/Envision/Screens/MainTabs/profile/ProfileViewController.swift index dd4074c..760609d 100644 --- a/Envision/Screens/MainTabs/profile/ProfileViewController.swift +++ b/Envision/Screens/MainTabs/profile/ProfileViewController.swift @@ -4,6 +4,7 @@ // import UIKit +import TipKit class ProfileViewController: UIViewController { @@ -50,6 +51,8 @@ class ProfileViewController: UIViewController { // ("key.fill", "Security", false), ], .about: [ + ("lightbulb.fill", "Tips & Tutorials", false), + ("arrow.counterclockwise", "Restart App Tour", false), ("info.circle.fill", "App Info", false), ("doc.text.fill", "Terms of Service", false), ("shield.lefthalf.filled", "Privacy Policy", false), @@ -268,6 +271,12 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { case (.privacy, "Permissions"): navigationController?.pushViewController(PermissionsViewController(), animated: true) + + case (.about, "Tips & Tutorials"): + navigationController?.pushViewController(TipsLibraryViewController(), animated: true) + + case (.about, "Restart App Tour"): + handleRestartTour() case (.about, "App Info"): navigationController?.pushViewController(AppInfoViewController(), animated: true) @@ -287,4 +296,23 @@ extension ProfileViewController: UITableViewDelegate, UITableViewDataSource { print("Tapped:", item.title) } + + private func handleRestartTour() { + let alert = UIAlertController( + title: "Restart App Tour?", + message: "This will reset all tips and guided tours.", + preferredStyle: .alert + ) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "Restart", style: .default) { _ in + TourManager.shared.resetTour() + + let confirm = UIAlertController(title: "Tour Reset", message: "The app tour will appear again as you navigate.", preferredStyle: .alert) + confirm.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(confirm, animated: true) + }) + + present(alert, animated: true) + } } diff --git a/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift b/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift new file mode 100644 index 0000000..65a331f --- /dev/null +++ b/Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift @@ -0,0 +1,184 @@ +// +// TipsLibraryViewController.swift +// Envision +// + +import UIKit +import SwiftUI +import TipKit + +final class TipsLibraryViewController: UIViewController { + + // MARK: - Data + struct TipItem { + let title: String + let message: String + let icon: String + let color: UIColor + } + + struct TipSection { + let title: String + let items: [TipItem] + } + + private var sections: [TipSection] = [] + + // MARK: - UI + private let tableView: UITableView = { + let tv = UITableView(frame: .zero, style: .insetGrouped) + tv.translatesAutoresizingMaskIntoConstraints = false + tv.register(TipLibraryCell.self, forCellReuseIdentifier: "TipCell") + return tv + }() + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + title = "Tips & Tutorials" + view.backgroundColor = .systemGroupedBackground + + setupData() + setupUI() + } + + private func setupData() { + // Prepare static data matching the real tips + // This allows viewing tips regardless of TipKit state rules + + sections = [ + TipSection(title: "Getting Started", items: [ + TipItem(title: "Welcome to EnVision", message: "Take a tour to learn how to scan rooms and furniture.", icon: "hand.wave.fill", color: .systemBlue), + TipItem(title: "Customize Your Profile", message: "Set your preferences and personalize your experience.", icon: "person.crop.circle", color: .systemOrange) + ]), + + TipSection(title: "Room Scanning", items: [ + TipItem(title: "Scan Your First Room", message: "Use the green camera button to start scanning with LiDAR.", icon: "camera.viewfinder", color: .systemGreen), + TipItem(title: "Scan Slowly", message: "Move your device slowly for the best accuracy.", icon: "tortoise.fill", color: .systemTeal), + TipItem(title: "Import Models", message: "Bring in existing USDZ models from Files.", icon: "square.and.arrow.down", color: .systemIndigo) + ]), + + TipSection(title: "Furniture Capture", items: [ + TipItem(title: "Capture Furniture", message: "Scan objects using 360° photogrammetry.", icon: "camera.metering.center.weighted", color: .systemPurple), + TipItem(title: "Good Lighting", message: "Ensure even lighting and avoid harsh shadows.", icon: "sun.max.fill", color: .systemYellow), + TipItem(title: "360° Coverage", message: "Walk around the entire object to capture all angles.", icon: "arrow.triangle.2.circlepath.camera", color: .systemRed) + ]), + + TipSection(title: "Organization", items: [ + TipItem(title: "Categories", message: "Organize your rooms and furniture with smart categories.", icon: "tag.fill", color: .systemPink), + TipItem(title: "Actions Menu", message: "Select, delete, or share multiple items at once.", icon: "ellipsis.circle", color: .systemGray) + ]) + ] + } + + private func setupUI() { + view.addSubview(tableView) + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor) + ]) + + tableView.dataSource = self + tableView.delegate = self + } +} + +// MARK: - UITableViewDataSource +extension TipsLibraryViewController: UITableViewDataSource, UITableViewDelegate { + + func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].items.count + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section].title + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "TipCell", for: indexPath) as? TipLibraryCell else { + return UITableViewCell() + } + + let item = sections[indexPath.section].items[indexPath.row] + cell.configure(item: item) + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + // Could expand or show detail VC if needed + } +} + +// MARK: - Cell +final class TipLibraryCell: UITableViewCell { + + private let iconContainer = UIView() + private let iconView = UIImageView() + private let titleLabel = UILabel() + private let messageLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + iconContainer.translatesAutoresizingMaskIntoConstraints = false + iconContainer.layer.cornerRadius = 8 + iconView.translatesAutoresizingMaskIntoConstraints = false + iconView.contentMode = .scaleAspectFit + iconView.tintColor = .white + + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.font = .systemFont(ofSize: 16, weight: .semibold) + + messageLabel.translatesAutoresizingMaskIntoConstraints = false + messageLabel.font = .systemFont(ofSize: 14) + messageLabel.textColor = .secondaryLabel + messageLabel.numberOfLines = 0 + + iconContainer.addSubview(iconView) + contentView.addSubview(iconContainer) + contentView.addSubview(titleLabel) + contentView.addSubview(messageLabel) + + NSLayoutConstraint.activate([ + iconContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + iconContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + iconContainer.widthAnchor.constraint(equalToConstant: 32), + iconContainer.heightAnchor.constraint(equalToConstant: 32), + + iconView.centerXAnchor.constraint(equalTo: iconContainer.centerXAnchor), + iconView.centerYAnchor.constraint(equalTo: iconContainer.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: 20), + iconView.heightAnchor.constraint(equalToConstant: 20), + + titleLabel.topAnchor.constraint(equalTo: iconContainer.topAnchor), + titleLabel.leadingAnchor.constraint(equalTo: iconContainer.trailingAnchor, constant: 12), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + + messageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), + messageLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), + messageLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), + messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12) + ]) + } + + func configure(item: TipsLibraryViewController.TipItem) { + titleLabel.text = item.title + messageLabel.text = item.message + iconView.image = UIImage(systemName: item.icon) + iconContainer.backgroundColor = item.color + } +} diff --git a/Envision/Screens/Onboarding/OnboardingController.swift b/Envision/Screens/Onboarding/OnboardingController.swift index 4f1b167..c40a395 100644 --- a/Envision/Screens/Onboarding/OnboardingController.swift +++ b/Envision/Screens/Onboarding/OnboardingController.swift @@ -2,8 +2,6 @@ // OnboardingController.swift // Envisionf2 // -// Created by Abishai on 15/11/25. -// import UIKit diff --git a/Envision/Tips/AppTips.swift b/Envision/Tips/AppTips.swift new file mode 100644 index 0000000..a8ea198 --- /dev/null +++ b/Envision/Tips/AppTips.swift @@ -0,0 +1,680 @@ +// +// AppTips.swift +// Envision +// +// Created for EnVision Tips & Tour System +// Version: 1.0 +// + +import Foundation +import TipKit +import SwiftUI + +// MARK: - Welcome & Onboarding Tips + +@available(iOS 17.0, *) +struct WelcomeTip: Tip { + var title: Text { + Text("Welcome to EnVision! 🎉") + } + + var message: Text? { + Text("Let's take a quick tour to show you how to scan rooms and furniture, then visualize them in AR.") + } + + var image: Image? { + Image(systemName: "hand.wave.fill") + } + + var actions: [Action] { + [ + Action(id: "start-tour", title: "Start Tour"), + Action(id: "skip", title: "Skip for now") + ] + } +} + +// MARK: - My Rooms Tips + +@available(iOS 17.0, *) +struct MyRoomsIntroTip: Tip { + + @Parameter + static var hasRooms: Bool = false + + var title: Text { + Text("📐 Scan Your First Room") + } + + var message: Text? { + Text("Tap the green camera button to start scanning any room using your iPhone's LiDAR sensor. We'll create a precise 3D model!") + } + + var image: Image? { + Image(systemName: "camera.viewfinder") + } + + var actions: [Action] { + [ + Action(id: "scan", title: "Scan Now"), + Action(id: "later", title: "Maybe Later") + ] + } + + var rules: [Rule] { + [ + #Rule(Self.$hasRooms) { $0 == false } + ] + } +} + +@available(iOS 17.0, *) +struct RoomImportTip: Tip { + + @Parameter + static var hasScannedRoom: Bool = false + + var title: Text { + Text("📥 Already Have 3D Models?") + } + + var message: Text? { + Text("Tap the blue import button to bring in existing USDZ room models from your Files app.") + } + + var image: Image? { + Image(systemName: "square.and.arrow.down") + } + + var rules: [Rule] { + [ + #Rule(Self.$hasScannedRoom) { $0 == true } + ] + } +} + +@available(iOS 17.0, *) +struct RoomActionsMenuTip: Tip { + + @Parameter + static var roomCount: Int = 0 + + var title: Text { + Text("⚡ More Options") + } + + var message: Text? { + Text("Tap the three-dot menu for bulk actions like selecting multiple rooms or visualizing furniture in AR.") + } + + var image: Image? { + Image(systemName: "ellipsis.circle") + } + + var rules: [Rule] { + [ + #Rule(Self.$roomCount) { $0 >= 1 } + ] + } +} + +@available(iOS 17.0, *) +struct RoomCategoriesTip: Tip { + + @Parameter + static var roomCount: Int = 0 + + var title: Text { + Text("🏷️ Organize Your Spaces") + } + + var message: Text? { + Text("Use category chips to filter rooms by type. Long-press any room to edit its category and add custom tags.") + } + + var image: Image? { + Image(systemName: "tag.fill") + } + + var rules: [Rule] { + [ + #Rule(Self.$roomCount) { $0 >= 2 } + ] + } +} + +// MARK: - Room Scanning Tips + +@available(iOS 17.0, *) +struct RoomScanningTip: Tip { + var title: Text { + Text("🐢 Scan Slowly for Best Results") + } + + var message: Text? { + Text("Move your device slowly and smoothly around the room. The slower you move, the more accurate your 3D model will be. Aim for good lighting too!") + } + + var image: Image? { + Image(systemName: "tortoise.fill") + } +} + +@available(iOS 17.0, *) +struct RoomCompleteTip: Tip { + var title: Text { + Text("✅ Complete Coverage") + } + + var message: Text? { + Text("Make sure to scan all corners, walls, and furniture. Walk around the entire perimeter at least once. The 'Save' button will appear when enough data is captured.") + } + + var image: Image? { + Image(systemName: "checkmark.circle.fill") + } +} + +@available(iOS 17.0, *) +struct RoomPreviewTip: Tip { + var title: Text { + Text("👀 Review Before Saving") + } + + var message: Text? { + Text("Give your room a descriptive name and select the right category. You can always edit these details later!") + } + + var image: Image? { + Image(systemName: "pencil.circle") + } +} + +// MARK: - My Furniture Tips + +@available(iOS 17.0, *) +struct FurnitureIntroTip: Tip { + + @Parameter + static var hasFurniture: Bool = false + + var title: Text { + Text("🪑 Capture Any Furniture") + } + + var message: Text? { + Text("Tap the green camera to scan furniture in two ways: Automatic capture (walk around) or manual photo import. Let's try it!") + } + + var image: Image? { + Image(systemName: "camera.metering.center.weighted") + } + + var actions: [Action] { + [ + Action(id: "scan", title: "Scan Furniture"), + Action(id: "later", title: "Later") + ] + } + + var rules: [Rule] { + [ + #Rule(Self.$hasFurniture) { $0 == false } + ] + } +} + +@available(iOS 17.0, *) +struct FurnitureCaptureMethodsTip: Tip { + var title: Text { + Text("📸 Two Capture Methods") + } + + var message: Text? { + Text("**Automatic**: Walk around the object while the app auto-captures photos.\n**From Photos**: Import 20+ photos you've already taken.") + } + + var image: Image? { + Image(systemName: "photo.stack") + } +} + +@available(iOS 17.0, *) +struct FurnitureCategoriesTip: Tip { + + @Parameter + static var furnitureCount: Int = 0 + + var title: Text { + Text("📂 Smart Categories") + } + + var message: Text? { + Text("Filter furniture by category: Chairs, Tables, Beds, and more. Categories make it easy to find specific items later!") + } + + var image: Image? { + Image(systemName: "square.grid.2x2") + } + + var rules: [Rule] { + [ + #Rule(Self.$furnitureCount) { $0 >= 2 } + ] + } +} + +// MARK: - Object Capture Tips + +@available(iOS 17.0, *) +struct ObjectCaptureStartTip: Tip { + var title: Text { + Text("🔄 360° Coverage is Key") + } + + var message: Text? { + Text("Walk slowly in a complete circle around the furniture. Capture from multiple heights: low, medium, and high angles. More photos = better quality!") + } + + var image: Image? { + Image(systemName: "arrow.triangle.2.circlepath.camera") + } +} + +@available(iOS 17.0, *) +struct ObjectCapturePhotoCountTip: Tip { + + @Parameter + static var photoCount: Int = 0 + + var title: Text { + Text("📊 Quality Indicator") + } + + var message: Text? { + Text("Watch the photo counter and quality bar at the top. Aim for 40+ photos for excellent results. The color changes from yellow → orange → green as quality improves.") + } + + var image: Image? { + Image(systemName: "chart.bar.fill") + } + + var rules: [Rule] { + [ + #Rule(Self.$photoCount) { $0 < 20 } + ] + } +} + +@available(iOS 17.0, *) +struct ObjectCaptureLightingTip: Tip { + var title: Text { + Text("💡 Good Lighting = Great Models") + } + + var message: Text? { + Text("Scan in well-lit areas with even lighting. Use the flashlight toggle if needed, but natural light works best. Avoid harsh shadows!") + } + + var image: Image? { + Image(systemName: "sun.max.fill") + } +} + +@available(iOS 17.0, *) +struct ObjectCaptureProcessingTip: Tip { + var title: Text { + Text("⚙️ Processing Your Model") + } + + var message: Text? { + Text("Photogrammetry can take 30 seconds to 2 minutes depending on photo count and device performance. Grab a coffee! ☕") + } + + var image: Image? { + Image(systemName: "gearshape.2.fill") + } +} + +// MARK: - AR Visualization Tips + +@available(iOS 17.0, *) +struct ARPlacementTip: Tip { + var title: Text { + Text("🎯 Place in Real Space") + } + + var message: Text? { + Text("Move your device slowly to detect surfaces. Once you see the grid overlay, tap to place your furniture. Pinch to scale, rotate with two fingers!") + } + + var image: Image? { + Image(systemName: "viewfinder") + } +} + +@available(iOS 17.0, *) +struct ARControlsTip: Tip { + var title: Text { + Text("🕹️ Master AR Controls") + } + + var message: Text? { + Text("**Joystick**: Move furniture left/right/forward/back\n**Height Slider**: Adjust vertical position\n**Rotation Slider**: Turn the object\n**+/- Buttons**: Scale up or down") + } + + var image: Image? { + Image(systemName: "gamecontroller.fill") + } +} + +@available(iOS 17.0, *) +struct RoomVisualizationTip: Tip { + + @Parameter + static var hasRoomAndFurniture: Bool = false + + var title: Text { + Text("🏠 Visualize Furniture in Rooms") + } + + var message: Text? { + Text("Go to My Rooms → Menu → 'Visualize furniture' to place your scanned furniture inside scanned rooms. See how it fits before buying!") + } + + var image: Image? { + Image(systemName: "house.and.flag.fill") + } + + var actions: [Action] { + [ + Action(id: "try-now", title: "Try It Now") + ] + } + + var rules: [Rule] { + [ + #Rule(Self.$hasRoomAndFurniture) { $0 == true } + ] + } +} + +// MARK: - Profile & Settings Tips + +@available(iOS 17.0, *) +struct ProfileCustomizationTip: Tip { + var title: Text { + Text("⚙️ Customize Your Experience") + } + + var message: Text? { + Text("Head to your Profile to change themes (Light/Dark), manage notifications, and control privacy settings. Make EnVision truly yours!") + } + + var image: Image? { + Image(systemName: "person.crop.circle.badge.checkmark") + } +} + +@available(iOS 17.0, *) +struct ThemeTip: Tip { + var title: Text { + Text("🌓 Choose Your Theme") + } + + var message: Text? { + Text("Tap 'Appearance' to switch between Light, Dark, or System mode. The app will instantly update with smooth animations.") + } + + var image: Image? { + Image(systemName: "moon.stars.fill") + } +} + +// MARK: - Advanced Features Tips + +@available(iOS 17.0, *) +struct GeometryPlaygroundTip: Tip { + + @Parameter + static var furnitureCount: Int = 0 + + var title: Text { + Text("🎨 Geometry Playground") + } + + var message: Text? { + Text("Advanced users: Try the 'Room Geometry Playground' to visualize and color-code room elements (walls, floors, doors). Great for understanding room structure!") + } + + var image: Image? { + Image(systemName: "cube.transparent") + } + + var rules: [Rule] { + [ + #Rule(Self.$furnitureCount) { $0 >= 3 } + ] + } +} + +@available(iOS 17.0, *) +struct ShareExportTip: Tip { + var title: Text { + Text("📤 Share Your Creations") + } + + var message: Text? { + Text("Long-press any room or furniture model to share it via Messages, AirDrop, or save to Files. Your 3D models are truly yours!") + } + + var image: Image? { + Image(systemName: "square.and.arrow.up") + } +} + +@available(iOS 17.0, *) +struct RulerMeasurementTip: Tip { + var title: Text { + Text("📏 Measure Distances") + } + + var message: Text? { + Text("Tap the ruler icon to measure distances in your 3D room. Tap two points to see the exact distance between them!") + } + + var image: Image? { + Image(systemName: "ruler") + } +} + +// MARK: - Tour Completion Tip + +@available(iOS 17.0, *) +struct TourCompleteTip: Tip { + var title: Text { + Text("🎓 You're All Set!") + } + + var message: Text? { + Text("You've learned the basics of EnVision! Start scanning to build your 3D furniture library. Tips will continue to appear as you explore more features.") + } + + var image: Image? { + Image(systemName: "checkmark.seal.fill") + } + + var actions: [Action] { + [ + Action(id: "finish", title: "Start Using EnVision") + ] + } +} + +// MARK: - Tip Categories for Tips Library + +@available(iOS 17.0, *) +enum TipCategory: String, CaseIterable { + case gettingStarted = "Getting Started" + case roomScanning = "Room Scanning" + case furnitureCapture = "Furniture Capture" + case arVisualization = "AR Visualization" + case advanced = "Advanced Features" + + var icon: String { + switch self { + case .gettingStarted: return "hand.wave.fill" + case .roomScanning: return "house.fill" + case .furnitureCapture: return "chair.fill" + case .arVisualization: return "arkit" + case .advanced: return "sparkles" + } + } + + var color: UIColor { + switch self { + case .gettingStarted: return .systemBlue + case .roomScanning: return .systemGreen + case .furnitureCapture: return .systemOrange + case .arVisualization: return .systemPurple + case .advanced: return .systemPink + } + } +} + +// MARK: - Tips Library Data + +@available(iOS 17.0, *) +struct TipInfo { + let title: String + let message: String + let icon: String + let category: TipCategory +} + +@available(iOS 17.0, *) +struct TipsLibrary { + static let allTips: [TipInfo] = [ + // Getting Started + TipInfo( + title: "Welcome to EnVision", + message: "EnVision helps you scan rooms and furniture, then visualize them in AR to see how they fit in your space.", + icon: "hand.wave.fill", + category: .gettingStarted + ), + TipInfo( + title: "LiDAR Scanning", + message: "Your device's LiDAR sensor creates precise 3D models of rooms. iPhone 12 Pro and newer, or iPad Pro models support this feature.", + icon: "sensor.fill", + category: .gettingStarted + ), + + // Room Scanning + TipInfo( + title: "Scan Your First Room", + message: "Go to My Rooms tab and tap the camera icon. Walk slowly around the room while holding your device steady.", + icon: "camera.viewfinder", + category: .roomScanning + ), + TipInfo( + title: "Scanning Best Practices", + message: "Scan in good lighting, move slowly, and capture all walls and corners. The more complete your scan, the better your 3D model.", + icon: "lightbulb.fill", + category: .roomScanning + ), + TipInfo( + title: "Room Categories", + message: "Organize rooms by category (Living Room, Bedroom, etc.) to easily find them later. Long-press any room to change its category.", + icon: "tag.fill", + category: .roomScanning + ), + TipInfo( + title: "Import USDZ Models", + message: "Already have 3D room models? Tap the import button to bring in USDZ files from your device or cloud storage.", + icon: "square.and.arrow.down", + category: .roomScanning + ), + + // Furniture Capture + TipInfo( + title: "Capture Furniture", + message: "Use the camera in the My Furniture tab to capture any piece of furniture. Walk around the object for best results.", + icon: "camera.metering.center.weighted", + category: .furnitureCapture + ), + TipInfo( + title: "Automatic vs Manual Capture", + message: "Automatic mode captures photos as you walk around. Manual mode lets you import photos you've already taken.", + icon: "photo.stack", + category: .furnitureCapture + ), + TipInfo( + title: "Photo Count Matters", + message: "More photos = better 3D models. Aim for 40+ photos from different angles for optimal quality.", + icon: "number.circle.fill", + category: .furnitureCapture + ), + TipInfo( + title: "Lighting Tips", + message: "Capture in even, natural lighting. Avoid harsh shadows and reflective surfaces for best results.", + icon: "sun.max.fill", + category: .furnitureCapture + ), + + // AR Visualization + TipInfo( + title: "Place Furniture in AR", + message: "View any furniture in AR by tapping it and selecting 'View in AR'. Move your device to find a flat surface, then tap to place.", + icon: "arkit", + category: .arVisualization + ), + TipInfo( + title: "AR Controls", + message: "Use pinch to scale, two fingers to rotate, and drag to move furniture in AR mode.", + icon: "hand.draw.fill", + category: .arVisualization + ), + TipInfo( + title: "Furniture in Rooms", + message: "Place your captured furniture inside scanned rooms to see how it fits before buying!", + icon: "house.and.flag.fill", + category: .arVisualization + ), + TipInfo( + title: "Measure Distances", + message: "Use the ruler tool to measure distances between points in your 3D room model.", + icon: "ruler", + category: .arVisualization + ), + + // Advanced Features + TipInfo( + title: "Geometry Playground", + message: "Advanced visualization mode that color-codes room elements like walls, floors, and doors.", + icon: "cube.transparent", + category: .advanced + ), + TipInfo( + title: "Customize Colors", + message: "In room edit mode, change the colors of walls, floors, and other elements to visualize different designs.", + icon: "paintpalette.fill", + category: .advanced + ), + TipInfo( + title: "Share Your Models", + message: "Export and share your 3D models via AirDrop, Messages, or save to Files for use in other apps.", + icon: "square.and.arrow.up", + category: .advanced + ), + TipInfo( + title: "Theme Options", + message: "Switch between Light, Dark, or System theme in Profile > Appearance.", + icon: "moon.stars.fill", + category: .advanced + ) + ] + + static func tips(for category: TipCategory) -> [TipInfo] { + allTips.filter { $0.category == category } + } +} diff --git a/FURNITURE_SCANNING_GUIDE.md b/FURNITURE_SCANNING_GUIDE.md new file mode 100644 index 0000000..074f0ca --- /dev/null +++ b/FURNITURE_SCANNING_GUIDE.md @@ -0,0 +1,879 @@ +# Furniture Scanning Guide - Technical Documentation & Improvements + +> **A comprehensive guide to the Object Capture implementation in EnVision, with detailed recommendations for generating higher quality 3D models.** + +--- + +## Table of Contents + +1. [Current Implementation Overview](#1-current-implementation-overview) +2. [How Object Capture Works](#2-how-object-capture-works) +3. [Current Code Analysis](#3-current-code-analysis) +4. [Issues with Current Implementation](#4-issues-with-current-implementation) +5. [Recommendations for Better 3D Models](#5-recommendations-for-better-3d-models) +6. [Code Improvements](#6-code-improvements) +7. [Best Practices for Users](#7-best-practices-for-users) +8. [Advanced Features to Add](#8-advanced-features-to-add) +9. [Troubleshooting Common Issues](#9-troubleshooting-common-issues) + +--- + +## 1. Current Implementation Overview + +### File Structure +``` +Object Capture/ +├── ObjectScanViewController.swift # Camera capture UI (411 lines) +├── ObjectCapturePreviewController.swift # Preview & processing (553 lines) +├── ARMeshExporter.swift # Empty/unused +├── ArrowGuideView.swift # Visual guidance +├── FeedbackBubble.swift # User feedback UI +├── InstructionOverlay.swift # Instructions +└── ProgressRingView.swift # Progress indicator +``` + +### Current Flow +``` +┌─────────────────────────┐ +│ ScanFurnitureVC │ +│ │ +│ [Scan ▾] → Automatic │ +└───────────┬─────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ ObjectScanViewController │ +│ │ +│ 1. Show instruction card │ +│ 2. Start camera session │ +│ 3. Auto-capture every 1.0s │ +│ 4. Save JPGs to temp folder │ +│ 5. Track photo count │ +│ │ +│ [Finish Capture] │ +└───────────┬─────────────────────┘ + │ + ▼ +┌─────────────────────────────────────┐ +│ ObjectCapturePreviewController │ +│ │ +│ 1. Display captured photos │ +│ 2. Show quality assessment │ +│ 3. Start PhotogrammetrySession │ +│ 4. Process images → USDZ │ +│ 5. Save via SaveManager │ +│ │ +│ [Generate 3D Model] │ +└─────────────────────────────────────┘ +``` + +--- + +## 2. How Object Capture Works + +### Apple's PhotogrammetrySession + +PhotogrammetrySession uses **Structure from Motion (SfM)** and **Multi-View Stereo (MVS)** algorithms: + +1. **Feature Detection**: Identifies distinctive points in each image +2. **Feature Matching**: Finds corresponding points across images +3. **Camera Pose Estimation**: Determines camera position for each photo +4. **Sparse Point Cloud**: Creates initial 3D points +5. **Dense Reconstruction**: Fills in the mesh +6. **Texture Mapping**: Applies colors from photos + +### Key Factors for Quality + +| Factor | Impact | Current Implementation | +|--------|--------|------------------------| +| **Photo Count** | High | 20-80+ photos ✓ | +| **Photo Overlap** | Critical | Not controlled ⚠️ | +| **Image Quality** | High | Standard JPG ⚠️ | +| **Lighting** | Critical | Basic flash only ⚠️ | +| **Camera Angles** | Critical | Not guided ⚠️ | +| **Object Coverage** | High | No visual feedback ⚠️ | +| **Motion Blur** | High | No detection ❌ | +| **Focus** | High | Auto-focus only ⚠️ | + +--- + +## 3. Current Code Analysis + +### ObjectScanViewController.swift + +#### Camera Setup +```swift +private func setupCamera() { + session.beginConfiguration() + session.sessionPreset = .photo // ⚠️ Could use .high for better quality + + guard let device = AVCaptureDevice.default(for: .video), + let input = try? AVCaptureDeviceInput(device: device) + // ... +} +``` + +**Issues:** +- Uses default video device (wide-angle camera) +- No manual focus control +- No exposure optimization +- No image stabilization settings + +#### Auto Capture Timer +```swift +private func startAutoCapture() { + captureTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + self.takePhoto() + } +} +``` + +**Issues:** +- Fixed 1-second interval regardless of movement +- No motion detection +- No blur detection before capture +- No overlap calculation + +#### Photo Capture +```swift +private func takePhoto() { + let settings = AVCapturePhotoSettings() + photoOutput.capturePhoto(with: settings, delegate: self) +} +``` + +**Issues:** +- Default photo settings (no HEIF, no RAW) +- No flash optimization +- No HDR +- No depth data capture + +### ObjectCapturePreviewController.swift + +#### Photogrammetry Configuration +```swift +var config = PhotogrammetrySession.Configuration() +config.sampleOrdering = .sequential +config.featureSensitivity = .normal // ⚠️ Could be .high +``` + +**Issues:** +- `featureSensitivity = .normal` misses fine details +- No object masking enabled +- No custom bounding box + +#### Model Quality +```swift +let request = PhotogrammetrySession.Request.modelFile(url: outputURL) +// ⚠️ No detail level specified - defaults to .preview +``` + +**Issues:** +- Missing `detail` parameter (defaults to `.preview`) +- Should use `.medium` or `.full` for better quality + +--- + +## 4. Issues with Current Implementation + +### Critical Issues + +| Issue | Impact | Solution | +|-------|--------|----------| +| **No motion blur detection** | Blurry photos ruin reconstruction | Add accelerometer-based capture | +| **Fixed capture interval** | May miss angles or capture duplicates | Use motion-based triggering | +| **Default detail level** | Low-quality output mesh | Specify `.medium` or `.full` | +| **No depth data** | Less accurate geometry | Enable LiDAR if available | +| **No coverage guidance** | Users don't know what angles to capture | Add visual coverage map | + +### Medium Issues + +| Issue | Impact | Solution | +|-------|--------|----------| +| **No HEIF format** | Larger files, less color depth | Enable HEIF capture | +| **No HDR** | Poor handling of shadows/highlights | Enable Smart HDR | +| **No focus lock** | Inconsistent focus across shots | Lock focus on object | +| **No exposure lock** | Brightness varies between photos | Lock exposure | +| **No object masking** | Background included in model | Enable masking | + +### Minor Issues + +| Issue | Impact | Solution | +|-------|--------|----------| +| **No capture sound** | Users unsure if photo taken | Add shutter sound option | +| **No preview of last photo** | Can't verify quality | Show thumbnail | +| **No delete bad photo** | Can't remove blurry shots | Add review mode | + +--- + +## 5. Recommendations for Better 3D Models + +### 5.1 Camera Configuration Improvements + +```swift +private func setupOptimizedCamera() { + session.beginConfiguration() + session.sessionPreset = .photo + + // Prefer LiDAR-enabled camera if available + let discoverySession = AVCaptureDevice.DiscoverySession( + deviceTypes: [.builtInLiDARDepthCamera, .builtInWideAngleCamera], + mediaType: .video, + position: .back + ) + + guard let device = discoverySession.devices.first, + let input = try? AVCaptureDeviceInput(device: device) else { return } + + // Configure device for best quality + try? device.lockForConfiguration() + + // Enable auto-focus with continuous adjustment + if device.isFocusModeSupported(.continuousAutoFocus) { + device.focusMode = .continuousAutoFocus + } + + // Enable auto-exposure with bias + if device.isExposureModeSupported(.continuousAutoExposure) { + device.exposureMode = .continuousAutoExposure + } + + // Enable optical image stabilization + if device.isOpticalStabilizationSupported { + // Applied in photo settings + } + + device.unlockForConfiguration() + + // Add depth output if available + if let depthOutput = AVCaptureDepthDataOutput() { + if session.canAddOutput(depthOutput) { + session.addOutput(depthOutput) + } + } + + session.commitConfiguration() +} +``` + +### 5.2 Smart Capture with Motion Detection + +```swift +import CoreMotion + +class SmartCaptureManager { + private let motionManager = CMMotionManager() + private var lastCaptureAttitude: CMAttitude? + private let minimumRotationDegrees: Double = 5.0 + + func startMotionTracking(onSignificantMovement: @escaping () -> Void) { + guard motionManager.isDeviceMotionAvailable else { return } + + motionManager.deviceMotionUpdateInterval = 0.1 + motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in + guard let motion = motion, let self = self else { return } + + if let lastAttitude = self.lastCaptureAttitude { + let rotation = self.rotationDifference(from: lastAttitude, to: motion.attitude) + + if rotation >= self.minimumRotationDegrees { + onSignificantMovement() + self.lastCaptureAttitude = motion.attitude.copy() as? CMAttitude + } + } else { + self.lastCaptureAttitude = motion.attitude.copy() as? CMAttitude + } + } + } + + private func rotationDifference(from: CMAttitude, to: CMAttitude) -> Double { + let deltaRoll = abs(to.roll - from.roll) * 180 / .pi + let deltaPitch = abs(to.pitch - from.pitch) * 180 / .pi + let deltaYaw = abs(to.yaw - from.yaw) * 180 / .pi + return max(deltaRoll, deltaPitch, deltaYaw) + } + + func isDeviceStable(threshold: Double = 0.5) -> Bool { + guard let motion = motionManager.deviceMotion else { return false } + + let acceleration = motion.userAcceleration + let magnitude = sqrt(pow(acceleration.x, 2) + pow(acceleration.y, 2) + pow(acceleration.z, 2)) + + return magnitude < threshold + } +} +``` + +### 5.3 Enhanced Photo Settings + +```swift +private func takeOptimizedPhoto() { + var settings = AVCapturePhotoSettings() + + // Use HEIF for better quality and smaller size + if photoOutput.availablePhotoCodecTypes.contains(.hevc) { + settings = AVCapturePhotoSettings(format: [ + AVVideoCodecKey: AVVideoCodecType.hevc + ]) + } + + // Enable high resolution + settings.isHighResolutionPhotoEnabled = true + + // Enable Smart HDR if available + if photoOutput.isHighResolutionCaptureEnabled { + settings.isHighResolutionPhotoEnabled = true + } + + // Enable flash in low light + if let device = AVCaptureDevice.default(for: .video) { + if device.hasTorch && isLowLight() { + settings.flashMode = .auto + } + } + + // Enable depth data if available + if photoOutput.isDepthDataDeliverySupported { + settings.isDepthDataDeliveryEnabled = true + } + + // Enable image stabilization + settings.photoQualityPrioritization = .quality + + photoOutput.capturePhoto(with: settings, delegate: self) +} + +private func isLowLight() -> Bool { + guard let device = AVCaptureDevice.default(for: .video) else { return false } + return device.iso > 400 // Arbitrary threshold +} +``` + +### 5.4 Blur Detection Before Capture + +```swift +import Vision + +class BlurDetector { + func detectBlur(in image: UIImage, completion: @escaping (Bool, Double) -> Void) { + guard let cgImage = image.cgImage else { + completion(true, 0) + return + } + + let request = VNGenerateImageFeaturePrintRequest { request, error in + // Use Laplacian variance for blur detection + let laplacianVariance = self.calculateLaplacianVariance(cgImage) + let isBlurry = laplacianVariance < 100 // Threshold + completion(isBlurry, laplacianVariance) + } + + let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) + try? handler.perform([request]) + } + + private func calculateLaplacianVariance(_ image: CGImage) -> Double { + // Simplified blur detection using pixel variance + // In production, use Metal for GPU-accelerated Laplacian + + let context = CIContext() + let ciImage = CIImage(cgImage: image) + + // Apply edge detection filter + guard let filter = CIFilter(name: "CIEdges") else { return 0 } + filter.setValue(ciImage, forKey: kCIInputImageKey) + filter.setValue(1.0, forKey: kCIInputIntensityKey) + + guard let output = filter.outputImage, + let cgOutput = context.createCGImage(output, from: output.extent) else { + return 0 + } + + // Calculate variance of edge image + return calculateVariance(cgOutput) + } + + private func calculateVariance(_ image: CGImage) -> Double { + // Simplified variance calculation + return 150.0 // Placeholder + } +} +``` + +### 5.5 Visual Coverage Guidance + +```swift +class CoverageTracker { + private var capturedAngles: [(pitch: Float, yaw: Float)] = [] + + struct CoverageSegment { + let pitchRange: ClosedRange + let yawRange: ClosedRange + var isCovered: Bool = false + } + + private var segments: [CoverageSegment] = [] + + init() { + // Create 3D grid of segments (spherical coverage) + // Pitch: -60° to +60° (above and below) + // Yaw: 0° to 360° (around object) + + for pitch in stride(from: -60, through: 60, by: 30) { + for yaw in stride(from: 0, through: 330, by: 30) { + segments.append(CoverageSegment( + pitchRange: Float(pitch)...Float(pitch + 30), + yawRange: Float(yaw)...Float(yaw + 30) + )) + } + } + } + + func recordCapture(pitch: Float, yaw: Float) { + capturedAngles.append((pitch, yaw)) + + for i in segments.indices { + if segments[i].pitchRange.contains(pitch) && + segments[i].yawRange.contains(yaw) { + segments[i].isCovered = true + } + } + } + + var coveragePercentage: Float { + let covered = segments.filter { $0.isCovered }.count + return Float(covered) / Float(segments.count) * 100 + } + + var uncoveredDirections: [String] { + var directions: [String] = [] + + let topCovered = segments.filter { $0.pitchRange.lowerBound >= 30 && $0.isCovered }.count + let bottomCovered = segments.filter { $0.pitchRange.upperBound <= -30 && $0.isCovered }.count + + if topCovered < 4 { directions.append("Capture from above") } + if bottomCovered < 4 { directions.append("Capture from below") } + + return directions + } +} +``` + +### 5.6 Improved Photogrammetry Configuration + +```swift +func startOptimizedPhotogrammetry(inputFolder: URL, outputURL: URL) { + var config = PhotogrammetrySession.Configuration() + + // Use highest quality settings + config.sampleOrdering = .sequential + config.featureSensitivity = .high // Capture fine details + config.isObjectMaskingEnabled = true // Remove background + + guard let session = try? PhotogrammetrySession( + input: inputFolder, + configuration: config + ) else { + handleError(message: "Failed to create session") + return + } + + // Request high-quality model + // Options: .preview (fastest), .reduced, .medium, .full, .raw + let request = PhotogrammetrySession.Request.modelFile( + url: outputURL, + detail: .medium // Balance of quality and speed + // Use .full for highest quality (slower) + ) + + // Process with progress tracking + Task { + do { + for try await output in session.outputs { + await handleOutput(output) + } + } catch { + await handleError(error) + } + } + + try? session.process(requests: [request]) +} +``` + +--- + +## 6. Code Improvements + +### 6.1 Updated ObjectScanViewController + +Key changes to implement: + +```swift +// Add these properties +private let motionManager = CMMotionManager() +private var lastCaptureAttitude: CMAttitude? +private var blurDetector = BlurDetector() +private var coverageTracker = CoverageTracker() +private var captureMode: CaptureMode = .motionBased + +enum CaptureMode { + case timerBased // Current: every 1 second + case motionBased // New: capture on movement + case manual // New: tap to capture +} + +// Replace startAutoCapture with smart capture +private func startSmartCapture() { + switch captureMode { + case .timerBased: + startTimerCapture(interval: 0.5) // Faster for more overlap + + case .motionBased: + startMotionBasedCapture() + + case .manual: + // Show manual capture button + showManualCaptureButton() + } +} + +private func startMotionBasedCapture() { + guard motionManager.isDeviceMotionAvailable else { + // Fallback to timer + startTimerCapture(interval: 1.0) + return + } + + motionManager.deviceMotionUpdateInterval = 0.05 + motionManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, _ in + guard let self = self, let motion = motion else { return } + + // Check if device has moved significantly + if self.shouldCaptureBasedOnMotion(motion) { + // Check if device is stable (not moving during shot) + if self.isDeviceStable(motion) { + self.takeOptimizedPhoto() + self.lastCaptureAttitude = motion.attitude.copy() as? CMAttitude + } + } + } +} + +private func shouldCaptureBasedOnMotion(_ motion: CMDeviceMotion) -> Bool { + guard let lastAttitude = lastCaptureAttitude else { + return true // First capture + } + + // Calculate rotation difference + let current = motion.attitude + let deltaRoll = abs(current.roll - lastAttitude.roll) * 180 / .pi + let deltaPitch = abs(current.pitch - lastAttitude.pitch) * 180 / .pi + let deltaYaw = abs(current.yaw - lastAttitude.yaw) * 180 / .pi + + let maxDelta = max(deltaRoll, deltaPitch, deltaYaw) + + return maxDelta >= 5.0 // Capture every 5 degrees of rotation +} + +private func isDeviceStable(_ motion: CMDeviceMotion) -> Bool { + let acc = motion.userAcceleration + let magnitude = sqrt(acc.x*acc.x + acc.y*acc.y + acc.z*acc.z) + return magnitude < 0.3 // Device is relatively still +} +``` + +### 6.2 Updated ObjectCapturePreviewController + +Key changes: + +```swift +@objc private func startProcessing() { + // ... existing setup code ... + + // IMPROVED: Use better configuration + var config = PhotogrammetrySession.Configuration() + config.sampleOrdering = .sequential + config.featureSensitivity = .high // Changed from .normal + config.isObjectMaskingEnabled = true // Added: remove background + + guard let session = try? PhotogrammetrySession( + input: self.imagesFolder, + configuration: config + ) else { + handleError(message: "Failed to create session") + return + } + + // IMPROVED: Specify detail level + let request = PhotogrammetrySession.Request.modelFile( + url: outputURL, + detail: .medium // Added: was using default .preview + ) + + // ... rest of processing ... +} +``` + +--- + +## 7. Best Practices for Users + +### 7.1 Lighting + +| Condition | Quality Impact | Recommendation | +|-----------|----------------|----------------| +| **Bright, even lighting** | ⭐⭐⭐⭐⭐ | Best results | +| **Natural daylight** | ⭐⭐⭐⭐ | Very good | +| **Single light source** | ⭐⭐⭐ | Shadows may cause issues | +| **Low light** | ⭐⭐ | Grainy photos, poor detail | +| **Direct sunlight** | ⭐⭐ | Harsh shadows, overexposure | + +**Recommendations:** +- Use diffused lighting from multiple angles +- Avoid harsh shadows +- Ensure the object is evenly lit +- Use the flashlight for fill light, not as primary + +### 7.2 Object Placement + +``` +GOOD: BAD: + +┌─────────────────┐ ┌─────────────────┐ +│ │ │ ████████████ │ +│ ┌─────┐ │ │ █ Object █ │ +│ │ │ │ │ ████████████ │ +│ │ Obj │ │ │ │ +│ └─────┘ │ │ │ +│ │ └─────────────────┘ +│ Clear space │ Against wall +└─────────────────┘ (can't capture back) +``` + +**Do:** +- Place object in center of open space +- Ensure 360° access around object +- Keep floor/background simple and matte +- Use contrasting background color + +**Don't:** +- Place against walls +- Put on reflective surfaces +- Include other objects in frame +- Use shiny/glass backgrounds + +### 7.3 Capture Technique + +``` +TOP VIEW - Capture Pattern: + + ★ (start) + ↓ + ← ● ● ● → + ● ● + ● OBJ ● + ● ● + ← ● ● ● → + ↑ + ★ (end at different height) + +Walk in circles at 3 different heights: +1. Eye level +2. Above (looking down 30-45°) +3. Below (looking up 30-45°) +``` + +**Photo Overlap:** +``` +Photo 1 Photo 2 Photo 3 +┌──────────────────────────────┐ +│ ████████ │ +│ ████████████████ │ +│ ████████████████ │ +│ ████████████████ │ +│ ████████████ │ +└──────────────────────────────┘ + 60-80% overlap recommended +``` + +### 7.4 Minimum Photo Requirements + +| Object Size | Minimum Photos | Recommended | Maximum Useful | +|-------------|---------------|-------------|----------------| +| Small (<30cm) | 30 | 50-70 | 100 | +| Medium (30-100cm) | 40 | 70-100 | 150 | +| Large (>100cm) | 50 | 100-150 | 200 | + +**Quality Formula:** +``` +Model Quality ≈ (Photo Count × Photo Quality × Coverage) / Processing Detail +``` + +--- + +## 8. Advanced Features to Add + +### 8.1 Real-time Quality Feedback + +```swift +// Add to ObjectScanViewController +struct CaptureQuality { + var sharpness: Float = 0 + var exposure: Float = 0 + var coverage: Float = 0 + + var overall: Float { + (sharpness + exposure + coverage) / 3.0 + } + + var recommendation: String { + if sharpness < 0.5 { return "Hold steadier" } + if exposure < 0.5 { return "Improve lighting" } + if coverage < 0.5 { return "Capture more angles" } + return "Looking good!" + } +} +``` + +### 8.2 AR Preview Before Processing + +```swift +// Add option to preview captured points in AR +func showPointCloudPreview() { + // Use captured images to show approximate 3D structure + // Helps users identify missing coverage areas +} +``` + +### 8.3 Object Bounding Box + +```swift +// Let user define object bounds for better masking +struct ObjectBounds { + var center: SIMD3 + var size: SIMD3 +} + +// Use in configuration +config.isObjectMaskingEnabled = true +// Define custom bounds if object detection fails +``` + +### 8.4 Quality Presets + +```swift +enum CaptureQualityPreset { + case quick // 20-30 photos, .reduced detail + case standard // 40-60 photos, .medium detail + case highQuality // 80-120 photos, .full detail + case maximum // 150+ photos, .raw detail + + var photoCount: ClosedRange { + switch self { + case .quick: return 20...30 + case .standard: return 40...60 + case .highQuality: return 80...120 + case .maximum: return 150...250 + } + } + + var detailLevel: PhotogrammetrySession.Request.Detail { + switch self { + case .quick: return .reduced + case .standard: return .medium + case .highQuality: return .full + case .maximum: return .raw + } + } +} +``` + +### 8.5 Resume Capture Session + +```swift +// Save capture progress for later +func saveProgress() { + let progress = CaptureProgress( + folderURL: tempFolderURL, + photoCount: images.count, + capturedAngles: coverageTracker.capturedAngles, + timestamp: Date() + ) + + try? JSONEncoder().encode(progress) + .write(to: progressFileURL) +} + +func resumeCapture() { + guard let data = try? Data(contentsOf: progressFileURL), + let progress = try? JSONDecoder().decode(CaptureProgress.self, from: data) else { + return + } + + tempFolderURL = progress.folderURL + images = loadExistingImages() + coverageTracker.restore(progress.capturedAngles) +} +``` + +--- + +## 9. Troubleshooting Common Issues + +### 9.1 Poor Model Quality + +| Symptom | Cause | Solution | +|---------|-------|----------| +| Holes in mesh | Missing photo angles | Ensure 360° coverage at multiple heights | +| Blurry textures | Motion blur in photos | Hold device steadier, use stabilization | +| Wrong scale | No reference object | Include object of known size | +| Missing details | Low feature sensitivity | Use `.high` feature sensitivity | +| Background in model | Object masking failed | Use contrasting background | + +### 9.2 Processing Failures + +| Error | Cause | Solution | +|-------|-------|----------| +| "Not enough images" | < 20 photos | Capture at least 30 photos | +| "Failed to find features" | Low-texture object | Add temporary markers | +| "Out of memory" | Too many high-res photos | Reduce photo count or resolution | +| "Processing timeout" | Complex geometry | Use `.reduced` detail first | + +### 9.3 Specific Object Types + +| Object Type | Challenge | Solution | +|-------------|-----------|----------| +| **Shiny/reflective** | Inconsistent reflections | Use polarizer, matte spray | +| **Transparent** | Can't detect surfaces | Not suitable for photogrammetry | +| **Very dark** | Features not visible | Increase lighting significantly | +| **Very thin** | Not enough depth | Capture at extreme angles | +| **Symmetric** | Matching confusion | Add temporary asymmetric markers | +| **Repetitive patterns** | Feature matching errors | Photograph in sections | + +--- + +## Summary: Priority Improvements + +### Immediate (High Impact, Low Effort) +1. ✅ Change `featureSensitivity` to `.high` +2. ✅ Add `detail: .medium` to model request +3. ✅ Enable `isObjectMaskingEnabled = true` +4. ✅ Reduce capture interval to 0.5 seconds + +### Short Term (High Impact, Medium Effort) +1. Add motion-based capture triggering +2. Add blur detection before saving photo +3. Show coverage percentage to user +4. Add quality presets (Quick/Standard/High) + +### Long Term (Medium Impact, High Effort) +1. Implement full coverage guidance system +2. Add AR point cloud preview +3. Support depth data from LiDAR +4. Add resume/pause capture functionality + +--- + +*Last Updated: January 3, 2026* +*EnVision Furniture Scanning Documentation* diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..117e734 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,590 @@ +# EnVision - Improvements & Recommendations + +> **A comprehensive analysis of the EnVision iOS project with actionable improvements, feature suggestions, and technical debt recommendations.** + +--- + +## 📊 Executive Summary + +After a thorough review of the entire codebase, this document outlines: +- **Critical Issues** that need immediate attention +- **Code Quality Improvements** for maintainability +- **Feature Enhancements** for better UX +- **Performance Optimizations** for smoother experience +- **Architecture Improvements** for scalability +- **Security Recommendations** for data protection +- **Accessibility Improvements** for inclusivity +- **Testing Recommendations** for reliability + +--- + +## 🚨 Critical Issues (Priority: HIGH) + +### 1. No Real Backend Integration +**Current State:** Authentication is simulated locally using `UserDefaults`. + +**Issue:** +- User data is only stored locally +- No real authentication/authorization +- No data sync across devices + +**Recommendation:** +``` +- Integrate Firebase Authentication (Email, Google, Apple Sign-In) +- Use Firebase Firestore for user data +- Use Firebase Storage for 3D models and profile images +- Implement proper token-based authentication +``` + +### 2. Missing Error Handling +**Current State:** Many async operations lack proper error handling. + +**Files Affected:** +- `SaveManager.swift` - Some completion handlers ignore errors +- `ObjectScanViewController.swift` - Photo capture errors not handled +- `RoomPlanScannerViewController.swift` - Session errors not displayed + +**Recommendation:** +```swift +// Add comprehensive error handling with user-friendly messages +enum AppError: LocalizedError { + case scanFailed(String) + case saveFailed(String) + case networkError(String) + case authenticationFailed(String) + + var errorDescription: String? { + switch self { + case .scanFailed(let msg): return "Scan failed: \(msg)" + case .saveFailed(let msg): return "Save failed: \(msg)" + case .networkError(let msg): return "Network error: \(msg)" + case .authenticationFailed(let msg): return "Auth failed: \(msg)" + } + } +} +``` + +### 3. Memory Management Issues +**Current State:** Thumbnail caches have no size limits. + +**Files Affected:** +- `MyRoomsViewController.swift` - `thumbnailCache` +- `ScanFurnitureViewController.swift` - `thumbnailCache` + +**Recommendation:** +```swift +// Add cache limits +thumbnailCache.countLimit = 50 +thumbnailCache.totalCostLimit = 50 * 1024 * 1024 // 50MB +``` + +--- + +## 🔧 Code Quality Improvements (Priority: MEDIUM) + +### 1. Create a Unified Networking Layer +**Current State:** No networking layer exists. + +**Recommendation:** Create `NetworkManager.swift` +``` +Services/ +├── NetworkManager.swift # API calls +├── FirebaseService.swift # Firebase integration +├── AuthService.swift # Authentication +└── StorageService.swift # File storage +``` + +### 2. Implement MVVM Architecture +**Current State:** Massive View Controllers with business logic mixed in. + +**Files Affected:** +- `MyRoomsViewController.swift` (652 lines) +- `ScanFurnitureViewController.swift` (1092 lines) +- `ProfileViewController.swift` (291 lines) + +**Recommendation:** +``` +Screens/MainTabs/Rooms/ +├── MyRoomsViewController.swift # View only +├── MyRoomsViewModel.swift # Business logic +├── MyRoomsCoordinator.swift # Navigation +└── Models/ + ├── RoomModel.swift + └── RoomMetadata.swift +``` + +### 3. Remove Code Duplication +**Duplicated Code Found:** + +| Code Pattern | Files | Recommendation | +|--------------|-------|----------------| +| `fileSizeString(for:)` | MyRoomsVC, ScanFurnitureVC | Move to `FileHelper.swift` | +| `fileDateString(for:)` | MyRoomsVC, ScanFurnitureVC | Move to `FileHelper.swift` | +| `showToast(message:)` | Multiple VCs | Move to `UIViewController+Toast.swift` | +| `showLoading/hideLoading` | Multiple VCs | Create `LoadingOverlay` component | +| ChipCell implementations | RoomCell, FurnitureChipCell | Create unified `FilterChipCell` | + +### 4. Use Protocols for Abstraction +**Recommendation:** +```swift +// Create protocols for common patterns +protocol ModelManaging { + func loadModels() + func deleteModel(at url: URL) + func renameModel(at url: URL, to name: String) +} + +protocol ThumbnailGenerating { + func generateThumbnail(for url: URL, completion: @escaping (UIImage?) -> Void) +} + +protocol Searchable { + var searchController: UISearchController { get } + func filterContent(for searchText: String) +} +``` + +### 5. Add Documentation +**Files Missing Documentation:** +- Most view controllers lack class-level documentation +- Public methods lack parameter documentation +- Complex algorithms lack inline comments + +**Recommendation:** +```swift +/// Manages the display and interaction of room models. +/// +/// This view controller provides: +/// - Grid display of scanned room models +/// - Search and filter functionality +/// - Multi-select for batch operations +/// - Context menus for individual actions +/// +/// - Note: Requires iOS 16.0+ for RoomPlan support +final class MyRoomsViewController: UIViewController { + // ... +} +``` + +--- + +## ✨ Feature Enhancements (Priority: MEDIUM) + +### 1. Add Onboarding Improvements +| Feature | Status | Recommendation | +|---------|--------|----------------| +| Skip confirmation | ❌ Missing | Add "Are you sure?" dialog | +| Progress indicator | ❌ Missing | Show "Step 1 of 3" | +| Demo content | ❌ Missing | Add sample room/furniture for new users | +| Tutorial overlay | ❌ Missing | First-time hints for main features | + +### 2. Add Room Features +| Feature | Status | Recommendation | +|---------|--------|----------------| +| Room dimensions display | ❌ Missing | Show width × height × length | +| Floor plan view | ❌ Missing | Add 2D top-down view | +| Room comparison | ❌ Missing | Side-by-side view of 2 rooms | +| Measurement tool | ❌ Missing | Tap-to-measure in AR | +| Export formats | Partial | Add OBJ, FBX, GLB export | +| Room templates | ❌ Missing | Pre-defined room layouts | +| Favorites | ❌ Missing | Star rooms for quick access | +| Recently viewed | ❌ Missing | Track view history | + +### 3. Add Furniture Features +| Feature | Status | Recommendation | +|---------|--------|----------------| +| Furniture dimensions | ❌ Missing | Show size after scan | +| Material detection | ❌ Missing | Identify wood, fabric, metal | +| Color extraction | ❌ Missing | Dominant color from model | +| Price estimation | ❌ Missing | AI-based price range | +| Similar items | ❌ Missing | Find similar furniture online | +| Scan quality score | ❌ Missing | Rate the 3D model quality | +| Before/After | ❌ Missing | Compare room with/without furniture | + +### 4. Add Profile Features +| Feature | Status | Recommendation | +|---------|--------|----------------| +| Account deletion | ❌ Missing | GDPR compliance | +| Data export | ❌ Missing | Export all user data | +| Backup to iCloud | ❌ Missing | Sync across devices | +| Activity log | ❌ Missing | Track scans, imports, etc. | +| Storage usage | ❌ Missing | Show disk space used | +| Subscription tiers | ❌ Missing | Free vs Pro features | + +### 5. Add Social Features +| Feature | Status | Recommendation | +|---------|--------|----------------| +| Share to social | Partial | Add Instagram, TikTok | +| Collaborative viewing | ❌ Missing | SharePlay support | +| Public gallery | ❌ Missing | Browse community rooms | +| Comments/ratings | ❌ Missing | For shared models | + +--- + +## ⚡ Performance Optimizations (Priority: MEDIUM) + +### 1. Lazy Loading for Collections +**Current State:** All thumbnails generated on load. + +**Recommendation:** +```swift +// Implement prefetching +extension MyRoomsViewController: UICollectionViewDataSourcePrefetching { + func collectionView(_ collectionView: UICollectionView, + prefetchItemsAt indexPaths: [IndexPath]) { + for indexPath in indexPaths { + let url = displayFiles[indexPath.item] + generateThumbnail(for: url) { _ in } + } + } + + func collectionView(_ collectionView: UICollectionView, + cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { + // Cancel pending thumbnail requests + } +} +``` + +### 2. Background Processing +**Current State:** Some file operations block main thread. + +**Recommendation:** +```swift +// Use background queues consistently +private let processingQueue = DispatchQueue(label: "com.envision.processing", + qos: .userInitiated, + attributes: .concurrent) + +func processModel(at url: URL) { + processingQueue.async { + // Heavy processing + DispatchQueue.main.async { + // UI updates + } + } +} +``` + +### 3. Image Optimization +**Recommendation:** +- Compress thumbnails before caching +- Use `UIImage.preparingThumbnail(of:)` for efficient resizing +- Implement progressive image loading + +### 4. Model Loading Optimization +**Recommendation:** +```swift +// Preload models in background +func preloadModel(at url: URL) async throws -> Entity { + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.global(qos: .userInitiated).async { + do { + let entity = try Entity.load(contentsOf: url) + continuation.resume(returning: entity) + } catch { + continuation.resume(throwing: error) + } + } + } +} +``` + +--- + +## 🏗 Architecture Improvements (Priority: LOW) + +### 1. Implement Coordinator Pattern +**Recommendation:** +```swift +protocol Coordinator { + var navigationController: UINavigationController { get } + func start() +} + +class RoomsCoordinator: Coordinator { + let navigationController: UINavigationController + + func start() { + let viewModel = MyRoomsViewModel() + let vc = MyRoomsViewController(viewModel: viewModel) + vc.coordinator = self + navigationController.pushViewController(vc, animated: false) + } + + func showRoomDetail(url: URL) { + let vc = RoomViewerViewController(roomURL: url) + navigationController.pushViewController(vc, animated: true) + } +} +``` + +### 2. Dependency Injection +**Current State:** Singletons used everywhere (`UserManager.shared`, `SaveManager.shared`). + +**Recommendation:** +```swift +// Protocol-based injection +protocol UserManaging { + var currentUser: UserModel? { get } + func login(email: String, password: String) async throws -> UserModel +} + +class MyRoomsViewController: UIViewController { + private let userManager: UserManaging + private let saveManager: ModelSaving + + init(userManager: UserManaging = UserManager.shared, + saveManager: ModelSaving = SaveManager.shared) { + self.userManager = userManager + self.saveManager = saveManager + super.init(nibName: nil, bundle: nil) + } +} +``` + +### 3. Create a Design System +**Recommendation:** +``` +DesignSystem/ +├── Colors.swift # AppColors (already exists) +├── Typography.swift # AppFonts (already exists) +├── Spacing.swift # Consistent margins/padding +├── Shadows.swift # Reusable shadow styles +├── Animations.swift # Standard animation curves +└── Components/ + ├── PrimaryButton.swift + ├── SecondaryButton.swift + ├── Card.swift + ├── Badge.swift + └── Toast.swift +``` + +--- + +## 🔒 Security Recommendations (Priority: HIGH) + +### 1. Secure Storage +**Current State:** User data stored in plain `UserDefaults`. + +**Recommendation:** +```swift +// Use Keychain for sensitive data +import Security + +class KeychainManager { + static func save(key: String, data: Data) -> OSStatus { + let query: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecAttrAccount as String: key, + kSecValueData as String: data + ] + return SecItemAdd(query as CFDictionary, nil) + } +} +``` + +### 2. Input Validation +**Current State:** Basic email/password validation exists. + +**Recommendation:** +- Add rate limiting for login attempts +- Implement password strength meter +- Sanitize all user inputs +- Validate file types before import + +### 3. Data Encryption +**Recommendation:** +```swift +// Encrypt sensitive files at rest +import CryptoKit + +func encryptData(_ data: Data, using key: SymmetricKey) throws -> Data { + let sealedBox = try AES.GCM.seal(data, using: key) + return sealedBox.combined! +} +``` + +--- + +## ♿ Accessibility Improvements (Priority: MEDIUM) + +### 1. VoiceOver Support +**Current State:** Limited accessibility labels. + +**Recommendation:** +```swift +// Add comprehensive accessibility +thumbnailView.isAccessibilityElement = true +thumbnailView.accessibilityLabel = "Room thumbnail" +thumbnailView.accessibilityHint = "Double tap to view room details" + +// For cells +cell.accessibilityLabel = "\(roomName), \(categoryName), created \(dateString)" +cell.accessibilityTraits = .button +``` + +### 2. Dynamic Type Support +**Current State:** Fixed font sizes used. + +**Recommendation:** +```swift +// Use scalable fonts +titleLabel.font = UIFont.preferredFont(forTextStyle: .headline) +titleLabel.adjustsFontForContentSizeCategory = true + +sizeLabel.font = UIFont.preferredFont(forTextStyle: .caption1) +sizeLabel.adjustsFontForContentSizeCategory = true +``` + +### 3. Color Contrast +**Recommendation:** +- Ensure all text meets WCAG AA standards (4.5:1 ratio) +- Add high contrast mode support +- Don't rely solely on color to convey information + +### 4. Reduce Motion +**Recommendation:** +```swift +if UIAccessibility.isReduceMotionEnabled { + // Use simpler animations or none + UIView.animate(withDuration: 0) { ... } +} else { + UIView.animate(withDuration: 0.3, + usingSpringWithDamping: 0.8, ...) { ... } +} +``` + +--- + +## 🧪 Testing Recommendations (Priority: HIGH) + +### 1. Unit Tests Needed +``` +EnvisionTests/ +├── Managers/ +│ ├── UserManagerTests.swift +│ ├── SaveManagerTests.swift +│ └── MetadataManagerTests.swift +├── Models/ +│ ├── UserModelTests.swift +│ ├── RoomMetadataTests.swift +│ └── FurnitureMetadataTests.swift +├── ViewModels/ +│ ├── RoomsViewModelTests.swift +│ └── FurnitureViewModelTests.swift +└── Extensions/ + ├── StringValidationTests.swift + └── UIColorHexTests.swift +``` + +### 2. UI Tests Needed +``` +EnvisionUITests/ +├── OnboardingFlowTests.swift +├── LoginFlowTests.swift +├── RoomsTabTests.swift +├── FurnitureTabTests.swift +└── ProfileTabTests.swift +``` + +### 3. Snapshot Tests +**Recommendation:** Use `swift-snapshot-testing` for UI regression testing. + +--- + +## 📁 Files to Add + +| File | Purpose | +|------|---------| +| `Services/NetworkManager.swift` | API networking layer | +| `Services/FirebaseService.swift` | Firebase integration | +| `Helpers/FileHelper.swift` | Common file operations | +| `Helpers/DateHelper.swift` | Date formatting utilities | +| `Components/LoadingOverlay.swift` | Reusable loading view | +| `Components/Toast.swift` | Unified toast component | +| `Components/EmptyStateView.swift` | Reusable empty state | +| `Components/FilterChipCell.swift` | Unified chip cell | +| `Protocols/ModelManaging.swift` | Common model protocols | +| `Errors/AppError.swift` | Unified error types | + +--- + +## 📁 Files to Remove/Refactor + +| File | Issue | Action | +|------|-------|--------| +| `ViewController.swift` | Empty/unused template | Delete | +| `PrimaryButton.swift` + `PrimaryButton1.swift` | Duplicate functionality | Merge into one | +| Large view controllers | Too much responsibility | Split into VM + Coordinator | + +--- + +## 📋 Implementation Priority + +### Phase 1 (Critical - 1-2 weeks) +1. ✅ Fix memory management issues +2. ✅ Add proper error handling +3. ✅ Implement Firebase Authentication +4. ✅ Add unit tests for managers + +### Phase 2 (Important - 2-4 weeks) +1. ✅ Refactor to MVVM +2. ✅ Create unified networking layer +3. ✅ Implement iCloud sync +4. ✅ Add accessibility support + +### Phase 3 (Enhancement - 4-6 weeks) +1. ✅ Add social features +2. ✅ Implement SharePlay +3. ✅ Add room comparison tool +4. ✅ Create subscription system + +### Phase 4 (Polish - Ongoing) +1. ✅ Performance optimizations +2. ✅ UI/UX refinements +3. ✅ Analytics integration +4. ✅ A/B testing infrastructure + +--- + +## 📊 Code Quality Metrics (Current) + +| Metric | Current | Target | +|--------|---------|--------| +| Test Coverage | 0% | 80%+ | +| Documentation | ~20% | 90%+ | +| Accessibility | ~10% | 100% | +| Max VC Lines | 1092 | <300 | +| Duplicate Code | ~15% | <5% | + +--- + +## 🔗 Recommended Libraries + +| Library | Purpose | Notes | +|---------|---------|-------| +| `Firebase` | Backend services | Auth, Firestore, Storage | +| `Kingfisher` | Image caching | Replace manual cache | +| `SnapKit` | Auto Layout DSL | Cleaner constraints | +| `Lottie` | Animations | Enhanced onboarding | +| `SwiftLint` | Code quality | Enforce style guide | +| `Quick/Nimble` | Testing | BDD-style tests | + +--- + +## 📝 Conclusion + +EnVision is a well-structured iOS app with solid AR/3D functionality. The main areas for improvement are: + +1. **Backend Integration** - Move from local storage to Firebase +2. **Architecture** - Adopt MVVM with Coordinators +3. **Testing** - Add comprehensive test coverage +4. **Accessibility** - Full VoiceOver and Dynamic Type support +5. **Performance** - Optimize thumbnail and model loading + +Implementing these improvements will result in a more maintainable, scalable, and user-friendly application. + +--- + +*Last Updated: January 1, 2026* +*Author: GitHub Copilot Analysis* diff --git a/TECHNICAL_DOCUMENTATION.md b/TECHNICAL_DOCUMENTATION.md new file mode 100644 index 0000000..e448d56 --- /dev/null +++ b/TECHNICAL_DOCUMENTATION.md @@ -0,0 +1,1465 @@ +# EnVision - Complete Technical Documentation + +> **A comprehensive technical guide to the EnVision iOS application architecture, workflows, and implementation details.** + +--- + +## Table of Contents + +1. [Project Overview](#1-project-overview) +2. [App Architecture](#2-app-architecture) +3. [App Launch Flow](#3-app-launch-flow) +4. [Tab 1: My Rooms](#4-tab-1-my-rooms) +5. [Tab 2: My Furniture](#5-tab-2-my-furniture) +6. [Tab 3: Profile](#6-tab-3-profile) +7. [Onboarding Flow](#7-onboarding-flow) +8. [Data Models](#8-data-models) +9. [Extensions & Utilities](#9-extensions--utilities) +10. [Components](#10-components) +11. [3D/AR Features](#11-3dar-features) +12. [File Structure Reference](#12-file-structure-reference) + +--- + +## 1. Project Overview + +**EnVision** is an iOS application that leverages Apple's RoomPlan and Object Capture technologies to: +- Scan rooms and create 3D models +- Capture furniture as 3D objects +- Visualize and edit 3D models with custom colors +- Measure dimensions in 3D space +- Manage user profiles and preferences + +### Tech Stack +| Technology | Usage | +|------------|-------| +| **UIKit** | Primary UI framework | +| **RoomPlan** | Room scanning with LiDAR | +| **ARKit** | Augmented reality features | +| **RealityKit** | 3D model rendering | +| **Object Capture** | Photogrammetry for furniture | +| **QuickLook** | 3D model preview | +| **UserDefaults** | Local data persistence | + +### Requirements +- iOS 16.0+ +- Xcode 15.0+ +- iPhone with LiDAR sensor (for scanning features) +- A12 Bionic chip or later + +--- + +## 2. App Architecture + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ AppDelegate │ +│ │ │ +│ ▼ │ +│ SceneDelegate │ +│ │ │ +│ ▼ │ +│ SplashViewController │ +│ │ │ +│ ▼ │ +│ OnboardingController │ +│ │ │ +│ ▼ │ +│ LoginViewController │ +│ │ │ +│ ▼ │ +│ MainTabBarController │ +│ ┌─────────┼─────────┐ │ +│ ▼ ▼ ▼ │ +│ MyRooms MyFurniture Profile │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Design Patterns Used +- **Singleton**: `UserManager`, `SaveManager`, `MetadataManager` +- **Delegation**: Collection views, document pickers, AR sessions +- **Extensions**: Organized helper methods in separate files +- **MVC**: View Controllers with model separation + +--- + +## 3. App Launch Flow + +### 3.1 AppDelegate.swift +**Location**: `Envision/AppDelegate.swift` + +```swift +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_:didFinishLaunchingWithOptions:) -> Bool + func application(_:configurationForConnecting:options:) -> UISceneConfiguration +} +``` + +| Method | Purpose | +|--------|---------| +| `application(_:didFinishLaunchingWithOptions:)` | App initialization, returns `true` | +| `application(_:configurationForConnecting:options:)` | Returns scene configuration for multi-window support | + +### 3.2 SceneDelegate.swift +**Location**: `Envision/SceneDelegate.swift` + +```swift +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_:willConnectTo:options:) // Initial setup + func switchToMainApp() // Navigate to main tabs + func switchToLogin() // Navigate to login +} +``` + +| Method | Purpose | Called When | +|--------|---------|-------------| +| `scene(_:willConnectTo:options:)` | Sets up window with `SplashViewController`, applies saved theme | App launches | +| `switchToMainApp()` | Replaces root with `MainTabBarController` | After successful login | +| `switchToLogin()` | Replaces root with `LoginViewController` | After logout | + +**Theme Handling:** +```swift +// Reads saved theme preference +if let saved = UserDefaults.standard.object(forKey: "selectedTheme") as? Int { + switch saved { + case 0: style = .light + case 1: style = .dark + default: style = .unspecified // System + } +} +``` + +### 3.3 MainTabBarController.swift +**Location**: `Envision/MainTabBarController.swift` + +```swift +final class MainTabBarController: UITabBarController { + override func viewDidLoad() + private func setupTabs() + private func setupLiquidGlassEffect() +} +``` + +**Tab Configuration:** +| Tab | View Controller | Icon (Normal) | Icon (Selected) | +|-----|-----------------|---------------|-----------------| +| My Rooms | `MyRoomsViewController` | `house` | `house.fill` | +| My Furniture | `ScanFurnitureViewController` | `sofa.viewfinder` | `custom.sofafill.viewfinder` | +| Profile | `ProfileViewController` | `person` | `person.fill` | + +**Liquid Glass Effect:** +- Transparent background with blur effect +- Rounded corners (30pt radius) +- Subtle shadow for depth + +--- + +## 4. Tab 1: My Rooms + +### 4.1 Overview +The My Rooms tab displays scanned and imported room models in a grid layout with filtering, search, and management capabilities. + +### 4.2 File Structure +``` +Screens/MainTabs/Rooms/ +├── MyRoomsViewController.swift # Main controller (726 lines) +├── MyRoomsViewController+helpers.swift # Extensions (382 lines) +├── RoomCell.swift # Collection view cell +├── RoomCategory.swift # Category/Type enums +├── RoomModel.swift # Data model +├── MetadataManager.swift # Metadata persistence +├── furniture+room/ # Room viewing/editing +│ ├── RoomViewerViewController.swift +│ ├── RoomVisualizeVC.swift +│ ├── RoomEditVC.swift +│ ├── FurniturePicker.swift +│ ├── FurnitureControlPanel.swift +│ └── OrbitJoystick.swift +└── RoomPlanScan/ + ├── RoomPlanScannerViewController.swift + └── RoomPreviewViewController.swift +``` + +### 4.3 MyRoomsViewController.swift + +#### Properties +```swift +final class MyRoomsViewController: UIViewController { + // MARK: - UI + var collectionView: UICollectionView! + private var loadingOverlay: UIVisualEffectView! + private var activityIndicator: UIActivityIndicatorView! + private var loadingLabel: UILabel! + private let searchController = UISearchController() + private var refreshControl: UIRefreshControl! + var previewURL: URL! + private var emptyStateView: UIView! + + // MARK: - Data + var roomFiles: [URL] = [] + var selectedCategory: RoomCategory? + var selectedRoomType: RoomType? + let thumbnailCache = NSCache() + var isSelectionMode = false +} +``` + +#### Key Methods + +| Method | Line | Purpose | +|--------|------|---------| +| `viewDidLoad()` | ~56 | Initialize UI, clean metadata, load files | +| `setupUI()` | ~63 | Call all setup methods | +| `setupNavigationBar()` | ~74 | Create scan/import buttons, menu | +| `setupSearch()` | ~97 | Configure search controller | +| `setupCollectionView()` | ~106 | Create compositional layout | +| `setupEmptyState()` | ~538 | Create empty state UI | +| `loadRoomFiles()` | ~342 | Load USDZ files from documents | +| `importRoomFiles(_:)` | ~366 | Handle imported files | +| `scanTapped()` | ~229 | Navigate to RoomPlan scanner | +| `chipTapped(_:)` | ~236 | Handle filter chip selection | +| `enableMultipleSelection()` | ~255 | Enter multi-select mode | +| `deleteSelectedRooms()` | ~285 | Batch delete selected | +| `generateThumbnail(for:completion:)` | ~480 | Create/cache thumbnails | +| `fileSizeString(for:)` | ~506 | Format file size | +| `fileDateString(for:)` | ~516 | Format creation date | + +#### UI Layout +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Navigation Bar │ +│ [☰ Menu] [Import ↓] [Scan 📷] │ +├─────────────────────────────────────────────────────────────────┤ +│ 🔍 Search room models... │ +├─────────────────────────────────────────────────────────────────┤ +│ Section 0: Filter Chips (horizontal scroll) │ +│ [All (5)] [Parametric (3)] [Textured (2)] [Living Room] ... │ +├─────────────────────────────────────────────────────────────────┤ +│ Section 1: Room Grid │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ [Thumbnail Image] [Parametric] [Bedroom] │ │ +│ │ │ │ +│ │ Room Name (without extension) │ │ +│ │ 2.4 MB │ │ +│ │ Jan 1, 2026 │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ (repeats for each room...) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### Collection View Layout +```swift +// Compositional layout with 2 sections +private func setupCollectionView() { + let layout = UICollectionViewCompositionalLayout { section, _ in + section == 0 ? self.makeChipsSection() : self.makeRoomsSection() + } +} + +// Chips: Horizontal scroll, estimated width +private func makeChipsSection() -> NSCollectionLayoutSection { + // orthogonalScrollingBehavior = .continuous + // Height: 32pt, Spacing: 8pt +} + +// Rooms: Full width cards, 200pt height +private func makeRoomsSection() -> NSCollectionLayoutSection { + // 1 column on iPhone, 4 columns on iPad + // Height: 200pt +} +``` + +### 4.4 MyRoomsViewController+helpers.swift + +#### Protocol Conformances +```swift +extension MyRoomsViewController: UIDocumentPickerDelegate +extension MyRoomsViewController: UICollectionViewDataSource, UICollectionViewDelegate +extension MyRoomsViewController: UISearchResultsUpdating +extension MyRoomsViewController: QLPreviewControllerDataSource +extension MyRoomsViewController: UICollectionViewDataSourcePrefetching +``` + +#### Key Methods + +| Method | Purpose | +|--------|---------| +| `documentPicker(_:didPickDocumentsAt:)` | Handle imported files | +| `numberOfSections(in:)` | Returns 2 (chips + rooms) | +| `collectionView(_:numberOfItemsInSection:)` | Chips count or filtered files count | +| `collectionView(_:cellForItemAt:)` | Configure ChipCell or RoomCell | +| `collectionView(_:didSelectItemAt:)` | Open room viewer or handle selection | +| `collectionView(_:contextMenuConfigurationForItemAt:)` | Long-press menu | +| `collectionView(_:prefetchItemsAt:)` | Prefetch thumbnails for performance | +| `quickLook(url:)` | Show QuickLook preview | +| `showEditCategoryDialog(for:currentMetadata:)` | Category picker | +| `showEditRoomTypeDialog(for:currentMetadata:)` | Room type picker | +| `showRenameDialog(for:)` | Rename room | +| `shareRoom(url:)` | Share via activity sheet | +| `confirmDelete(url:)` | Delete with confirmation | + +#### Context Menu Actions +```swift +UIMenu(children: [ + UIAction(title: "View in AR", image: "arkit") { self?.quickLook(url: url) }, + UIAction(title: "Edit Category", image: "tag") { ... }, + UIAction(title: "Edit Room Type", image: "cube") { ... }, + UIAction(title: "Rename", image: "pencil") { ... }, + UIAction(title: "Share", image: "square.and.arrow.up") { ... }, + UIAction(title: "Delete", image: "trash", attributes: .destructive) { ... } +]) +``` + +### 4.5 RoomCell.swift + +#### Properties +```swift +final class RoomCell: UICollectionViewCell { + static let reuseID = "RoomCell" + + private let thumbnailView: UIImageView + private let titleLabel: UILabel + private let sizeLabel: UILabel + private let dateLabel: UILabel + private let container: UIView + private let selectionCircle: UIImageView + private let categoryBadge: UIView + private let roomTypeBadge: UIView +} +``` + +#### Configure Method +```swift +func configure( + fileName: String, // Displays without extension + size: String, // e.g., "2.4 MB" + dateText: String, // e.g., "Jan 1, 2026" + thumbnail: UIImage?, + category: RoomCategory?, // Shows badge if set + roomType: RoomType? // Shows badge if set +) +``` + +### 4.6 RoomCategory.swift + +```swift +enum RoomCategory: String, Codable, CaseIterable { + case livingRoom = "Living Room" // 🛋️ Orange + case bedroom = "Bedroom" // 🛏️ Purple + case studyRoom = "Study Room" // 📚 Blue + case office = "Office" // 💼 Green + case other = "Other" // ❓ Gray + + var sfSymbol: String { ... } + var color: UIColor { ... } + var displayName: String { rawValue } +} + +enum RoomType: String, Codable, CaseIterable { + case parametric = "Parametric" // RoomPlan API, Teal + case textured = "Textured" // Object Capture, Pink + + var sfSymbol: String { ... } + var color: UIColor { ... } + var description: String { ... } +} +``` + +### 4.7 MetadataManager.swift + +```swift +class MetadataManager { + static let shared = MetadataManager() + + // Core Methods + func loadMetadata() -> RoomsMetadata + func saveMetadata(_ metadata: RoomsMetadata) + func getMetadata(for filename: String) -> RoomMetadata? + func updateMetadata(for filename: String, metadata: RoomMetadata) + func deleteMetadata(for filename: String) + func renameMetadata(from oldFilename: String, to newFilename: String) + func cleanupOrphanedMetadata() +} +``` + +**Storage Location**: `Documents/roomPlan/rooms_metadata.json` + +### 4.8 Room Scanning Flow + +``` +┌──────────────────────┐ +│ MyRoomsViewController │ +│ │ +│ [Scan Button] │ +└──────────┬───────────┘ + │ scanTapped() + ▼ +┌──────────────────────────────┐ +│ RoomPlanScannerViewController │ +│ │ +│ ┌──────────────────────────┐ │ +│ │ RoomCaptureView │ │ +│ │ (Apple's UI) │ │ +│ └──────────────────────────┘ │ +│ │ +│ [Save Button] │ +└──────────────┬───────────────┘ + │ saveTapped() + ▼ +┌──────────────────────────┐ +│ RoomPreviewViewController │ +│ │ +│ - Preview captured room │ +│ - Enter room name │ +│ - Select category │ +│ - Export as USDZ │ +└──────────────┬───────────┘ + │ Save & Export + ▼ +┌──────────────────────┐ +│ MyRoomsViewController │ +│ │ +│ loadRoomFiles() │ +│ (refreshes grid) │ +└──────────────────────┘ +``` + +### 4.9 Room Viewing Flow + +``` +┌──────────────────────┐ +│ MyRoomsViewController │ +│ │ +│ [Tap on Room Cell] │ +└──────────┬───────────┘ + │ didSelectItemAt + ▼ +┌─────────────────────────────┐ +│ RoomViewerViewController │ +│ │ +│ [Visualize] [Edit] │ ← Segmented Control +│ │ +└──────────┬──────────────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ +┌──────────┐ ┌──────────┐ +│Visualize │ │ Edit │ +│ VC │ │ VC │ +└──────────┘ └──────────┘ +``` + +### 4.10 RoomVisualizeVC.swift + +#### Properties +```swift +final class RoomVisualizeVC: UIViewController { + private let roomURL: URL + private var roomModel: ModelEntity? + private var displayedModel: ModelEntity? + private var placedFurniture: [ModelEntity] = [] + + // Measurement + private var isMeasuringMode = false + private var measurementPoints: [SIMD3] = [] + private var measurementLabel: UILabel? + private var measurementLine: ModelEntity? + + // Camera + private let orbitCamera = PerspectiveCamera() + private var cameraPitch: Float = .pi / 6 + private var cameraYaw: Float = .pi / 4 + private var cameraDistance: Float = 1.5 + + private let arView: ARView // Non-AR mode +} +``` + +#### Key Features +1. **3D Orbit Camera**: Pan and pinch to rotate/zoom +2. **Ruler Tool**: Tap two points to measure distance +3. **Add Furniture**: Place furniture models in the room +4. **Control Panel**: Adjust furniture scale, rotation, position + +#### Measurement Flow +``` +[Tap Ruler Button] → isMeasuringMode = true + │ + ▼ +[Show Instructions Toast] + │ + ▼ +[Tap First Point] → Add orange sphere marker + │ + ▼ +[Tap Second Point] → Add second marker + │ │ + ▼ ▼ +[Draw Line Between] [Calculate Distance] + │ │ + ▼ ▼ +[Show Distance Label: "📏 1.5 m (4.9 ft)"] +``` + +### 4.11 RoomEditVC.swift + +#### Key Features +1. **Color Picker**: Change colors of walls, floors, doors, windows, etc. +2. **Labels Toggle**: Show/hide entity labels +3. **Add Furniture**: Same as Visualize mode +4. **Floating Menu**: Quick access to all editing tools + +#### Color Targets +```swift +enum ColorTarget { + case walls + case doors + case tables + case floors + case windows + case storage + case selected +} +``` + +--- + +## 5. Tab 2: My Furniture + +### 5.1 Overview +The My Furniture tab manages 3D furniture models captured via Object Capture or imported as USDZ files. + +### 5.2 File Structure +``` +Screens/MainTabs/furniture/ +├── ScanFurnitureViewController.swift # Main controller (1092 lines) +├── FurnitureCell.swift # Collection view cell +├── FurnitureCategory.swift # Category enum +├── CreateModel/ +│ ├── CreateModelViewController.swift +│ └── CreateModelViewController2.swift +├── ModelsFromFiles/ +│ ├── USDZCell.swift +│ └── ViewModelsViewController.swift +├── Object Capture/ +│ ├── ObjectScanViewController.swift +│ ├── ObjectCapturePreviewController.swift +│ ├── ARMeshExporter.swift +│ ├── ArrowGuideView.swift +│ ├── FeedbackBubble.swift +│ ├── InstructionOverlay.swift +│ └── ProgressRingView.swift +└── roomPlanColor/ + ├── VisualizeRoomViewController.swift + ├── RoomARWithFurnitureViewController.swift + └── RoomARView 1.swift / 2.swift +``` + +### 5.3 ScanFurnitureViewController.swift + +#### Properties +```swift +final class ScanFurnitureViewController: UIViewController { + // MARK: - UI + private var collectionView: UICollectionView! + private var loadingOverlay: UIVisualEffectView! + private var emptyStateView: UIView! + private let searchController = UISearchController() + private var refreshControl: UIRefreshControl! + + // MARK: - Data + private var furnitureFiles: [URL] = [] + private var filteredFiles: [URL] = [] + private var selectedCategory: FurnitureCategory? = nil + private let thumbnailCache: NSCache = .init() + private var previewURL: URL? +} +``` + +#### Key Methods + +| Method | Line | Purpose | +|--------|------|---------| +| `viewDidLoad()` | ~104 | Initialize UI | +| `setupNavigationBar()` | ~139 | Scan menu, import button | +| `setupCollectionView()` | ~337 | Compositional layout | +| `setupEmptyState()` | ~488 | Empty state UI | +| `loadFurnitureFiles(from:)` | ~544 | Load USDZ from documents | +| `generateThumbnail(for:completion:)` | ~592 | Create/cache thumbnails | +| `automaticCaptureTapped()` | ~618 | Open ObjectScanViewController | +| `createFromPhotosTapped()` | ~623 | Open CreateModelViewController | +| `importUSDZTapped()` | ~628 | Open document picker | +| `getCategoryForURL(_:)` | ~66 | Get/infer category | +| `inferCategory(from:)` | ~75 | Keyword-based inference | +| `chipTapped(at:)` | ~786 | Category filter | +| `showQuickLook(url:)` | ~833 | Open QL preview | +| `renameModel(at:url:)` | ~841 | Edit dialog | +| `deleteModel(at:url:)` | ~936 | Delete confirmation | + +#### UI Layout +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Navigation Bar │ +│ [☰ Menu] [Import ↓] [Scan ▾] │ +├─────────────────────────────────────────────────────────────────┤ +│ 🔍 Search models... │ +├─────────────────────────────────────────────────────────────────┤ +│ Section 0: Category Chips │ +│ [All (8)] [Chairs] [Tables] [Storage] [Beds] [Lighting] ... │ +├─────────────────────────────────────────────────────────────────┤ +│ Section 1: Furniture Grid (2 columns) │ +│ ┌───────────────┐ ┌───────────────┐ │ +│ │ [Thumbnail] │ │ [Thumbnail] │ │ +│ │ │ │ │ │ +│ │ Chair Model │ │ Table Model │ │ +│ │ 1.2 MB │ │ 3.4 MB │ │ +│ │ Jan 1, 2026 │ │ Dec 28, 2025 │ │ +│ └───────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### Scan Menu Options +```swift +let scanMenu = UIMenu(children: [ + UIAction(title: "Automatic Object Capture", + image: "camera.metering.center.weighted") { + self.automaticCaptureTapped() // → ObjectScanViewController + }, + UIAction(title: "Create From Photos", + image: "photo.on.rectangle.angled") { + self.createFromPhotosTapped() // → CreateModelViewController + } +]) +``` + +### 5.4 FurnitureCell.swift + +```swift +final class FurnitureCell: UICollectionViewCell { + static let reuseIdentifier = "FurnitureCell" + + private let container: UIView + private let thumbnailImageView: UIImageView + private let nameLabel: UILabel + private let sizeLabel: UILabel + private let dateLabel: UILabel + private let selectionOverlay: UIView + private let checkmarkImageView: UIImageView + private let placeholderIcon: UIImageView + + func configure(name: String, sizeText: String, dateText: String, thumbnail: UIImage?) +} +``` + +### 5.5 FurnitureCategory.swift + +```swift +enum FurnitureCategory: String, Codable, CaseIterable { + case seating = "Chairs" // 🪑 Blue + case tables = "Tables" // 🪑 Orange + case storage = "Storage" // 🗄️ Purple + case beds = "Beds" // 🛏️ Indigo + case lighting = "Lighting" // 💡 Yellow + case decor = "Decor" // 🖼️ Pink + case kitchen = "Kitchen" // 🍳 Teal + case outdoor = "Outdoor" // 🌳 Green + case office = "Office" // 💻 Brown + case electronics = "Electronics" // 📺 Cyan + case other = "Other" // 📦 Gray + + var sfSymbol: String { ... } + var icon: String { sfSymbol } + var color: UIColor { ... } + var displayName: String { rawValue } +} +``` + +#### Category Inference +```swift +private func inferCategory(from name: String) -> FurnitureCategory { + let lowercased = name.lowercased() + + if lowercased.contains("chair") || lowercased.contains("sofa") { return .seating } + if lowercased.contains("table") || lowercased.contains("desk") { return .tables } + if lowercased.contains("cabinet") || lowercased.contains("shelf") { return .storage } + if lowercased.contains("bed") { return .beds } + if lowercased.contains("lamp") || lowercased.contains("light") { return .lighting } + // ... more patterns + + return .other +} +``` + +### 5.6 Object Capture Flow + +``` +┌────────────────────────────┐ +│ ScanFurnitureViewController │ +│ │ +│ [Scan ▾] → "Automatic" │ +└─────────────┬──────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ ObjectScanViewController │ +│ │ +│ ┌─────────────────────────────┐ │ +│ │ Camera Preview │ │ +│ │ │ │ +│ │ 📸 Auto-capture timer │ │ +│ │ │ │ +│ └─────────────────────────────┘ │ +│ │ +│ Photos: [42] "Keep going..." │ +│ │ +│ [Finish Capture] │ +└─────────────┬───────────────────┘ + │ stopCapture() + ▼ +┌─────────────────────────────────────┐ +│ ObjectCapturePreviewController │ +│ │ +│ Processing photos... │ +│ ████████████░░░░ 75% │ +│ │ +│ → PhotogrammetrySession │ +│ → Generate 3D model │ +│ → Export as USDZ │ +└─────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────┐ +│ ScanFurnitureViewController │ +│ │ +│ loadFurnitureFiles() │ +│ (refreshes grid) │ +└────────────────────────────┘ +``` + +--- + +## 6. Tab 3: Profile + +### 6.1 Overview +The Profile tab manages user settings, preferences, and account information. + +### 6.2 File Structure +``` +Screens/MainTabs/profile/ +├── ProfileViewController.swift +├── ProfileCell.swift +├── EditProfileViewController.swift +└── SubScreens/ + ├── AppearanceViewController.swift + ├── NotificationsViewController.swift + ├── PrivacyControlsViewController.swift + ├── PermissionsViewController.swift + ├── EmailPasswordViewController.swift + ├── AppInfoViewController.swift + ├── TermsViewController.swift + └── PrivacyPolicyViewController.swift +``` + +### 6.3 ProfileViewController.swift + +#### UI Layout +``` +┌─────────────────────────────────────────────┐ +│ Profile │ +├─────────────────────────────────────────────┤ +│ ┌───────────┐ │ +│ │ Avatar │ │ +│ └───────────┘ │ +│ Shaurya │ +│ shaurya@gmail.com │ +│ [Edit Profile] │ +├─────────────────────────────────────────────┤ +│ ACCOUNT │ +│ 👤 My Profile ▶ │ +│ ✉️ Email & Password ▶ │ +├─────────────────────────────────────────────┤ +│ PREFERENCES │ +│ 🎨 Appearance ▶ │ +│ 🔔 Notifications ▶ │ +├─────────────────────────────────────────────┤ +│ PRIVACY & SECURITY │ +│ 🔒 Privacy Controls ▶ │ +│ ✋ Permissions ▶ │ +├─────────────────────────────────────────────┤ +│ ABOUT │ +│ ℹ️ App Info ▶ │ +│ 📄 Terms of Service ▶ │ +│ 🛡️ Privacy Policy ▶ │ +├─────────────────────────────────────────────┤ +│ 🚪 Sign Out (red) │ +├─────────────────────────────────────────────┤ +│ Version 1.0 (1) │ +└─────────────────────────────────────────────┘ +``` + +#### Sections Enum +```swift +private enum Section: Int, CaseIterable { + case account + case preferences + case privacy + case about + case logout +} +``` + +#### Navigation Mapping + +| Section | Item | Destination | +|---------|------|-------------| +| Account | My Profile | `EditProfileViewController` (modal) | +| Account | Email & Password | `EmailPasswordViewController` | +| Preferences | Appearance | `AppearanceViewController` | +| Preferences | Notifications | `NotificationsViewController` | +| Privacy | Privacy Controls | `PrivacyControlsViewController` | +| Privacy | Permissions | `PermissionsViewController` | +| About | App Info | `AppInfoViewController` | +| About | Terms of Service | `TermsViewController` | +| About | Privacy Policy | `PrivacyPolicyViewController` | +| Logout | Sign Out | `handleLogout()` | + +#### Logout Flow +```swift +private func handleLogout() { + // Show confirmation alert + // On confirm: performLogout() +} + +private func performLogout() { + // Clear user data + // Navigate to login via SceneDelegate + if let sceneDelegate = scene.delegate as? SceneDelegate { + sceneDelegate.switchToLogin() + } +} +``` + +--- + +## 7. Onboarding Flow + +### 7.1 File Structure +``` +Screens/Onboarding/ +├── SplashViewController.swift +├── OnboardingController.swift +├── OnboardingPage.swift +├── LoginViewController.swift +├── SignupViewController.swift +├── ForgotPasswordViewController.swift +├── ModernTextField.swift +└── SocialButton.swift +``` + +### 7.2 Complete Flow + +``` +┌──────────────────────┐ +│ SplashViewController │ +│ │ +│ ┌────────────────┐ │ +│ │ EnVision │ │ +│ │ Logo │ │ +│ │ (animated) │ │ +│ └────────────────┘ │ +│ │ +│ "See it in your │ +│ space, before │ +│ you buy it." │ +└──────────┬───────────┘ + │ goNext() after animation + ▼ +┌─────────────────────────────────┐ +│ OnboardingController │ +│ │ +│ ┌───────────────────────────┐ │ +│ │ Page 1: Scan │ │ +│ │ 🔲 "Scan Your Room" │ │ +│ │ Turn your space into │ │ +│ │ a 3D model using AR. │ │ +│ └───────────────────────────┘ │ +│ │ +│ ● ○ ○ │ +│ │ +│ [Skip] [Continue] │ +└─────────────┬───────────────────┘ + │ (swipe or tap Continue) + ▼ +┌─────────────────────────────────┐ +│ Page 2: Capture │ +│ 📷 "Capture Any Furniture" │ +│ Transform real items into │ +│ 3D models. │ +│ │ +│ ○ ● ○ │ +└─────────────┬───────────────────┘ + │ + ▼ +┌─────────────────────────────────┐ +│ Page 3: Visualize │ +│ 🔮 "Visualize with Confidence" │ +│ See how items fit before │ +│ you buy. │ +│ │ +│ ○ ○ ● │ +│ │ +│ [Skip] [Get Started] │ +└─────────────┬───────────────────┘ + │ goToLogin() + ▼ +┌─────────────────────────────────┐ +│ LoginViewController │ +│ │ +│ ┌─────────────────┐ │ +│ │ EnVision │ │ +│ └─────────────────┘ │ +│ │ +│ [Email Field] │ +│ [Password Field] │ +│ │ +│ [Continue Button] │ +│ │ +│ Forgot password? Create Account│ +│ │ +│ [Sign in with Apple] │ +│ [Sign in with Google] │ +└──────────┬──────────────────────┘ + │ + ┌─────┴─────┐ + ▼ ▼ +┌──────────┐ ┌──────────────────┐ +│ Signup │ │ ForgotPassword │ +│VC │ │ VC │ +└────┬─────┘ └──────────────────┘ + │ + ▼ +┌──────────────────────┐ +│ MainTabBarController │ +│ (via SceneDelegate) │ +└──────────────────────┘ +``` + +### 7.3 SplashViewController.swift + +```swift +class SplashViewController: UIViewController { + private let iconView: UIImageView // App logo + private let titleLabel: UILabel // "EnVision" + private let subLabel: UILabel // Tagline + + override func viewDidAppear(_ animated: Bool) { + animateLogo() + } + + private func animateLogo() { + // Spring animation: scale 0.92 → 1.02 → 1.0 + // Duration: 1.2s + // On completion: goNext() + } + + private func goNext() { + // Present OnboardingController with crossDissolve + } +} +``` + +### 7.4 LoginViewController.swift + +#### Key Methods + +| Method | Purpose | +|--------|---------| +| `handleLogin()` | Validate & login via UserManager | +| `goToSignup()` | Push SignupViewController | +| `goToForgotPassword()` | Push ForgotPasswordViewController | +| `showError(_:)` | Display error label | + +#### Login Flow +```swift +@objc private func handleLogin() { + let email = emailField.textField.text ?? "" + let password = passwordField.textField.text ?? "" + + guard !email.isEmpty, !password.isEmpty else { + showError("All fields are required.") + return + } + guard email.isValidEmail else { + showError("Invalid email format.") + return + } + + UserManager.shared.login(email: email, password: password) { result in + switch result { + case .success: + sceneDelegate.switchToMainApp() + case .failure(let error): + self.showError(error.localizedDescription) + } + } +} +``` + +--- + +## 8. Data Models + +### 8.1 RoomMetadata + +```swift +struct RoomMetadata: Codable { + var category: RoomCategory? + var roomType: RoomType? + var createdAt: Date + var dimensions: RoomDimensions? + var tags: [String] + var notes: String? +} + +struct RoomDimensions: Codable { + let width: Double + let height: Double + let length: Double +} + +struct RoomsMetadata: Codable { + var version: String + var rooms: [String: RoomMetadata] // filename -> metadata +} +``` + +### 8.2 FurnitureMetadata + +```swift +struct FurnitureMetadata: Codable { + var category: FurnitureCategory? + var furnitureType: FurnitureType? + var createdAt: Date + var tags: [String] + var notes: String? +} +``` + +### 8.3 UserModel + +```swift +struct UserModel: Codable { + var id: String + var name: String + var email: String + var bio: String? + var profileImagePath: String? + var preferences: UserPreferences? +} + +struct UserPreferences: Codable { + var theme: Int // 0: Light, 1: Dark, 2: System + var notifications: Bool + var haptics: Bool +} +``` + +### 8.4 RoomModel + +```swift +struct RoomModel { + let id: UUID + var name: String + var createdAt: Date + var thumbnail: UIImage? + var sizeDescription: String + var capturedRoom: CapturedRoom // From RoomPlan +} +``` + +--- + +## 9. Extensions & Utilities + +### 9.1 File Structure +``` +Extensions/ +├── Entity+Visit.swift +├── Extensions.swift +├── SaveManager.swift +├── UIColor+Hex.swift +├── UIFont+AppFonts.swift +├── UIViewController+Transition.swift +├── UserManager.swift +└── UserModel.swift +``` + +### 9.2 Entity+Visit.swift + +```swift +extension Entity { + /// Recursively visits all child entities + func visit(_ closure: (Entity) -> Void) { + closure(self) + for child in children { + child.visit(closure) + } + } +} +``` + +### 9.3 Extensions.swift + +```swift +// String validation +extension String { + var isValidEmail: Bool { + // Regex validation + } + + var isStrongPassword: Bool { + // 8+ chars, 1 uppercase, 1 number + } +} + +// UIView helpers +extension UIView { + func applyGradientBackground(colors: [UIColor]) +} +``` + +### 9.4 UIColor+Hex.swift + +```swift +extension UIColor { + convenience init(hex: String) { + // Parse hex string to RGB + } + + func toHex() -> String { + // Convert to hex string + } +} + +struct AppColors { + static let accent = UIColor(hex: "#4A9085") + static let primary = UIColor(hex: "#2C3E50") + static let secondary = UIColor(hex: "#7F8C8D") + static let background = UIColor(hex: "#F5F6FA") + static let error = UIColor(hex: "#E74C3C") + static let success = UIColor(hex: "#27AE60") +} +``` + +### 9.5 UIFont+AppFonts.swift + +```swift +struct AppFonts { + static func regular(_ size: CGFloat) -> UIFont + static func medium(_ size: CGFloat) -> UIFont + static func semibold(_ size: CGFloat) -> UIFont + static func bold(_ size: CGFloat) -> UIFont +} +``` + +### 9.6 SaveManager.swift + +```swift +final class SaveManager { + static let shared = SaveManager() + + enum ModelType: String { + case room = "roomPlan" + case furniture = "furniture" + } + + // Core Methods + func saveModel(from sourceURL: URL, type: ModelType, + customName: String?, completion: @escaping (URL?) -> Void) + func getSavedModels(type: ModelType) -> [URL] + func deleteModel(at url: URL, completion: @escaping (Bool) -> Void) + func getThumbnail(for url: URL, completion: @escaping (UIImage?) -> Void) + func getMetadata(for url: URL) -> ModelMetadata? + func getStorageInfo(type: ModelType) -> (count: Int, totalSize: Int64) +} +``` + +### 9.7 UserManager.swift + +```swift +final class UserManager { + static let shared = UserManager() + + var currentUser: UserModel? + + // Authentication + func login(email: String, password: String, + completion: @escaping (Result) -> Void) + func signup(name: String, email: String, password: String, + completion: @escaping (Result) -> Void) + func logout() + + // Profile + func updateProfile(name: String?, email: String?, bio: String?) + func updatePreferences(_ preferences: UserPreferences) + func saveProfileImage(_ image: UIImage) + func loadProfileImage() -> UIImage? +} +``` + +--- + +## 10. Components + +### 10.1 File Structure +``` +Components/ +├── CustomTextField.swift +├── PrimaryButton.swift +└── PrimaryButton1.swift +``` + +### 10.2 CustomTextField.swift + +```swift +final class CustomTextField: UITextField { + // Styled text field with padding + // Rounded corners, border + // Placeholder styling +} +``` + +### 10.3 PrimaryButton.swift + +```swift +final class PrimaryButton: UIButton { + init(title: String) { + // Green background (#4A9085) + // White text + // Rounded corners + } +} +``` + +### 10.4 PrimaryButton1.swift + +```swift +final class PrimaryButton1: UIButton { + init(title: String) { + // Uses UIButton.Configuration (modern API) + // Filled style + // cornerStyle: .large + } +} +``` + +### 10.5 ModernTextField.swift + +```swift +final class ModernTextField: UIView { + let textField = UITextField() + private let floatingLabel = UILabel() + private let eyeButton = UIButton() + + init(placeholder: String, secure: Bool = false) + + // Features: + // - Floating label animation + // - Show/hide password toggle + // - Focus state styling +} +``` + +### 10.6 SocialButton.swift + +```swift +final class SocialButton: UIButton { + init(title: String, image: UIImage?) { + // Horizontal stack: icon + label + // Light background with border + // Shadow effect + } +} +``` + +--- + +## 11. 3D/AR Features + +### 11.1 RoomPlan Integration + +**RoomPlanScannerViewController.swift** +```swift +final class RoomPlanScannerViewController: UIViewController, + RoomCaptureSessionDelegate { + private let captureSession = RoomCaptureSession() + private lazy var captureView = RoomCaptureView() + private var capturedRoom: CapturedRoom? + + // Delegate methods + func captureSession(_ session: RoomCaptureSession, + didUpdate room: CapturedRoom) + func captureSession(_ session: RoomCaptureSession, + didEndWith room: CapturedRoom, error: Error?) +} +``` + +### 11.2 Object Capture Integration + +**ObjectScanViewController.swift** +```swift +final class ObjectScanViewController: UIViewController { + // Camera + private let session = AVCaptureSession() + private let photoOutput = AVCapturePhotoOutput() + + // Auto-capture timer + private var captureTimer: Timer? + private var images: [URL] = [] + + // Methods + func startCapturing() // Begin auto-capture + func stopCapture() // Finish and process + func capturePhoto() // Single photo capture +} +``` + +**ObjectCapturePreviewController.swift** +```swift +// Uses PhotogrammetrySession to create 3D model +// Processes captured photos +// Exports USDZ file +``` + +### 11.3 3D Visualization + +**RoomVisualizeVC.swift Features:** +- Non-AR 3D view with orbit camera +- Pan gesture: Rotate camera +- Pinch gesture: Zoom in/out +- Ruler tool: Measure distances +- Furniture placement + +**RoomEditVC.swift Features:** +- All visualization features +- Color picker for room elements +- Labels toggle +- Floating action menu + +### 11.4 AR Preview + +Uses `QLPreviewController` for: +- Full AR experience +- Scale, rotate, move models +- Share screenshots +- USDZ native preview + +--- + +## 12. File Structure Reference + +``` +EnVision/ +├── README.md +├── IMPROVEMENTS.md +├── TECHNICAL_DOCUMENTATION.md (this file) +│ +├── Envision/ +│ ├── AppDelegate.swift +│ ├── SceneDelegate.swift +│ ├── MainTabBarController.swift +│ ├── ViewController.swift +│ ├── Info.plist +│ │ +│ ├── 3D_Models/ +│ │ ├── chair.usdz +│ │ ├── hall.usdz +│ │ ├── ios_room.usdz +│ │ ├── ios_room1.usdz +│ │ └── table.usdz +│ │ +│ ├── Assets.xcassets/ +│ │ ├── AppIcon.appiconset/ +│ │ ├── envision.imageset/ +│ │ ├── google_icon.imageset/ +│ │ ├── sofa.viewfinder.symbolset/ +│ │ └── custom.sofafill.viewfinder.symbolset/ +│ │ +│ ├── Base.lproj/ +│ │ └── LaunchScreen.storyboard +│ │ +│ ├── Components/ +│ │ ├── CustomTextField.swift +│ │ ├── PrimaryButton.swift +│ │ └── PrimaryButton1.swift +│ │ +│ ├── Extensions/ +│ │ ├── Entity+Visit.swift +│ │ ├── Extensions.swift +│ │ ├── SaveManager.swift +│ │ ├── UIColor+Hex.swift +│ │ ├── UIFont+AppFonts.swift +│ │ ├── UIViewController+Transition.swift +│ │ ├── UserManager.swift +│ │ └── UserModel.swift +│ │ +│ └── Screens/ +│ ├── MainTabs/ +│ │ ├── furniture/ +│ │ │ ├── FurnitureCategory.swift +│ │ │ ├── FurnitureCell.swift +│ │ │ ├── ScanFurnitureViewController.swift +│ │ │ ├── CreateModel/ +│ │ │ ├── ModelsFromFiles/ +│ │ │ ├── Object Capture/ +│ │ │ └── roomPlanColor/ +│ │ │ +│ │ ├── profile/ +│ │ │ ├── ProfileViewController.swift +│ │ │ ├── ProfileCell.swift +│ │ │ ├── EditProfileViewController.swift +│ │ │ └── SubScreens/ +│ │ │ +│ │ └── Rooms/ +│ │ ├── MyRoomsViewController.swift +│ │ ├── MyRoomsViewController+helpers.swift +│ │ ├── RoomCell.swift +│ │ ├── RoomCategory.swift +│ │ ├── RoomModel.swift +│ │ ├── MetadataManager.swift +│ │ ├── furniture+room/ +│ │ └── RoomPlanScan/ +│ │ +│ └── Onboarding/ +│ ├── SplashViewController.swift +│ ├── OnboardingController.swift +│ ├── OnboardingPage.swift +│ ├── LoginViewController.swift +│ ├── SignupViewController.swift +│ ├── ForgotPasswordViewController.swift +│ ├── ModernTextField.swift +│ └── SocialButton.swift +│ +└── Envision.xcodeproj/ + └── project.pbxproj +``` + +--- + +## Summary + +EnVision is a well-architected iOS app with: + +1. **Clear Separation**: Each feature in its own folder +2. **Singleton Managers**: Centralized data management +3. **Protocol Extensions**: Clean delegate implementations +4. **Modern UI**: Compositional layouts, context menus, SF Symbols +5. **AR/3D Integration**: RoomPlan, Object Capture, RealityKit +6. **Consistent Styling**: Centralized colors and fonts + +--- + +*Last Updated: January 2, 2026* From 30933465fa95d7349d806db5f0fa7e2ddb9bf5ed Mon Sep 17 00:00:00 2001 From: Abhinav Raj Date: Tue, 27 Jan 2026 11:39:41 +0530 Subject: [PATCH 3/3] feat: Add room color persistence and UI improvements ## New Features - RoomColorManager: Singleton for saving/loading room element colors - Persists colors to Documents/RoomColors/{roomName}_colors.json - Supports walls, floors, doors, windows, tables, chairs, storage - Colors persist across app restarts and mode switches - Save button in Edit mode (replaces standalone + button) - Captures thumbnail with current colors - Saves to Documents/RoomThumbnails/{roomName}_thumb.jpg - Shows success alert and navigates to home screen - Add furniture button still available (blue +) - Color picker improvements - Native Cancel and Done buttons in navigation bar - Cancel restores previous colors - Done confirms selection - Thumbnail improvements - MyRoomsViewController loads saved colored thumbnails - Falls back to QuickLook if no saved thumbnail exists ## Bug Fixes - Fixed BillboardComponent crash (EXC_BAD_ACCESS) - Safe cleanup in viewWillDisappear - Async setting of BillboardComponent on main thread - Proper removal before entity cleanup - Fixed PhotogrammetrySession.Request.Detail enum - Changed .preview/.medium/.full to .reduced (iOS 26 SDK) - Updated quality selector UI to reflect available options - Fixed Swift 6 main actor isolation warnings in BackgroundModelProcessor ## New Files - Envision/Managers/RoomColorManager.swift - Envision/Managers/BackgroundModelProcessor.swift - Envision/Tips/TipPresenter.swift - PROJECT_WORKFLOW.md - RECOMMENDED_IMPROVEMENTS.md - AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md - COMPLETE_TECHNICAL_DOCUMENTATION.md - FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md - TIPS_TOUR_IMPROVEMENT_PLAN.md ## Modified Files - RoomEditVC.swift: Save button, color persistence, thumbnail capture - RoomVisualizeVC.swift: Load and apply saved colors - MyRoomsViewController.swift: Load saved thumbnails - VisualizeRoomViewController.swift: BillboardComponent fix, color picker - RoomARWithFurnitureViewController.swift: BillboardComponent fix - ObjectCapturePreviewController.swift: Quality selector fix, cancel button - BackgroundModelProcessor.swift: Main actor isolation fixes --- .DS_Store | Bin 6148 -> 6148 bytes AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md | 1465 ++++++++++++++++ COMPLETE_TECHNICAL_DOCUMENTATION.md | 1029 ++++++++++++ .../UserInterfaceState.xcuserstate | Bin 227169 -> 255765 bytes Envision/.DS_Store | Bin 10244 -> 10244 bytes Envision/AppDelegate.swift | 34 +- Envision/Assets.xcassets/.DS_Store | Bin 6148 -> 8196 bytes Envision/MainTabBarController.swift | 67 +- .../Managers/BackgroundModelProcessor.swift | 434 +++++ Envision/Managers/RoomColorManager.swift | 133 ++ Envision/Managers/TourManager.swift | 26 +- Envision/SceneDelegate.swift | 4 +- Envision/Screens/.DS_Store | Bin 6148 -> 6148 bytes .../Rooms/MyRoomsViewController.swift | 327 ++-- .../Rooms/furniture+room/RoomEditVC.swift | 251 ++- .../furniture+room/RoomVisualizeVC.swift | 37 + .../ObjectCapturePreviewController.swift | 392 +++-- .../ObjectScanViewController.swift | 8 +- .../RoomARWithFurnitureViewController.swift | 9 +- .../VisualizeRoomViewController.swift | 65 +- .../profile/ProfileViewController.swift | 1 - .../TipsLibraryViewController.swift | 2 - Envision/Tips/AppTips.swift | 675 +------- Envision/Tips/TipPresenter.swift | 251 +++ FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md | 1489 +++++++++++++++++ PROJECT_WORKFLOW.md | 302 ++++ RECOMMENDED_IMPROVEMENTS.md | 348 ++++ TIPS_TOUR_IMPROVEMENT_PLAN.md | 1048 ++++++++++++ 28 files changed, 7297 insertions(+), 1100 deletions(-) create mode 100644 AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md create mode 100644 COMPLETE_TECHNICAL_DOCUMENTATION.md create mode 100644 Envision/Managers/BackgroundModelProcessor.swift create mode 100644 Envision/Managers/RoomColorManager.swift create mode 100644 Envision/Tips/TipPresenter.swift create mode 100644 FIREBASE_BACKEND_IMPLEMENTATION_PLAN.md create mode 100644 PROJECT_WORKFLOW.md create mode 100644 RECOMMENDED_IMPROVEMENTS.md create mode 100644 TIPS_TOUR_IMPROVEMENT_PLAN.md diff --git a/.DS_Store b/.DS_Store index aac36d7e420adce94a6ed7619e87f02cef589635..1d2918f23ca6178cf41125ab0b7132a693104df1 100644 GIT binary patch delta 126 zcmZoMXffEZl!zjOzNeRIeo6N(^%RD22Ve$lK4S}3= z!{Frn+yVv=kT`P$D5y2Lj+u+?XW6`Xk+#Ww%w`Ct7Joqol5$Ch&4L^(ESuRm{_+C= DTrDSw delta 126 zcmZoMXffEZl!zKLN=5N0a(%Q#thG1&(7i1tQ7kAh!$ic$0nVsV=KL90m BEfxR( diff --git a/AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md b/AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..6db2b73 --- /dev/null +++ b/AR_MEASUREMENT_TOOL_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1465 @@ +# EnVision - AR Measurement Tool Implementation Plan +*Complete Guide for Adding Measurement Features to Room & Furniture Viewing* + +--- + +## Table of Contents +1. [Overview](#1-overview) +2. [Measurement Types](#2-measurement-types) +3. [Technical Architecture](#3-technical-architecture) +4. [UI/UX Design](#4-uiux-design) +5. [RealityKit Implementation](#5-realitykit-implementation) +6. [ARKit Implementation (Alternative)](#6-arkit-implementation-alternative) +7. [Measurement Precision](#7-measurement-precision) +8. [Code Implementation](#8-code-implementation) +9. [Testing Plan](#9-testing-plan) +10. [Enhancement Ideas](#10-enhancement-ideas) + +--- + +## 1. Overview + +### 1.1 Purpose +Add real-world measurement capabilities to EnVision's 3D viewing experience, allowing users to: +- Measure distances between furniture pieces +- Measure dimensions of individual furniture items +- Measure room dimensions (walls, floor area, ceiling height) +- Verify furniture will fit in specific locations +- Plan room layouts with accurate spacing + +### 1.2 User Stories +1. **"Will this sofa fit?"** - User measures sofa length and compares to wall space +2. **"How much clearance?"** - User measures distance between furniture pieces +3. **"Room dimensions?"** - User measures room walls to verify model accuracy +4. **"Height check"** - User measures furniture height to ensure it fits under shelves +5. **"Floor area"** - User measures available floor space for new furniture + +### 1.3 Where to Add Measurements + +**Existing View Controllers**: +- ✅ `RoomViewerViewController.swift` - View room with placed furniture (PRIORITY) +- ✅ `RoomVisualizeVC.swift` - 3D room visualization +- ⚠️ `MyRoomsViewController.swift` - Could show room dimensions in list view +- ⚠️ `ScanFurnitureViewController.swift` - Could show furniture dimensions + +### 1.4 Key Features +- **Point-to-Point Measurement**: Tap two points, see distance +- **Object Dimension Display**: Show width × length × height for selected furniture +- **Room Dimension Display**: Show room measurements (from RoomPlan metadata) +- **Measurement History**: Keep list of recent measurements +- **Units Toggle**: Metric (meters/cm) ↔ Imperial (feet/inches) +- **Export Measurements**: Share as text or screenshot + +--- + +## 2. Measurement Types + +### 2.1 Point-to-Point Distance +**Use Case**: "How far is this chair from that table?" + +**Visual**: +``` + Start Point ●─────────────● End Point + └─ 2.45 m ─┘ +``` + +**Interaction**: +1. Tap "Measure" button +2. Tap first location +3. Tap second location +4. Distance shown in floating label + +### 2.2 Furniture Dimensions +**Use Case**: "How big is this chair?" + +**Visual**: +``` + ┌─────┐ + │ │ Height: 0.92 m + │ │ + └─────┘ + Width: 0.65 m + Depth: 0.58 m +``` + +**Interaction**: +1. Tap furniture item +2. Show bounding box with dimensions +3. Display width × depth × height + +### 2.3 Room Dimensions +**Use Case**: "What are the exact room measurements?" + +**Visual**: +``` + 5.2 m + ┌───────────┐ + │ │ 4.1 m + │ Room │ + │ │ + └───────────┘ + Height: 2.7 m + Area: 21.3 m² +``` + +**Interaction**: +1. Tap "Room Info" button +2. Show overlay with dimensions from RoomPlan metadata +3. Display floor area calculation + +### 2.4 Multiple Measurements +**Use Case**: "Compare several distances" + +**Visual**: +``` + ●─1.2m─● Table + │ + 0.8m + │ + ●─1.5m─● Chair +``` + +**Interaction**: +1. Create multiple measurements +2. Label each measurement +3. Show measurement list panel +4. Delete individual measurements + +--- + +## 3. Technical Architecture + +### 3.1 Core Components + +```swift +// MARK: - Measurement Data Model +struct Measurement { + let id: UUID + let type: MeasurementType + let startPoint: SIMD3 + let endPoint: SIMD3 + let distance: Float + let label: String? + let timestamp: Date +} + +enum MeasurementType { + case pointToPoint + case objectDimensions + case roomDimensions +} + +// MARK: - Measurement Manager +class MeasurementManager { + var measurements: [Measurement] = [] + var isActive: Bool = false + var currentUnit: MeasurementUnit = .metric + + func addMeasurement(_ measurement: Measurement) + func removeMeasurement(id: UUID) + func clearAllMeasurements() + func formatDistance(_ meters: Float) -> String +} + +// MARK: - Measurement Visualizer (RealityKit) +class MeasurementVisualizer { + func createLine(from start: SIMD3, to end: SIMD3) -> ModelEntity + func createLabel(text: String, at position: SIMD3) -> ModelEntity + func createBoundingBox(for entity: ModelEntity) -> ModelEntity +} +``` + +### 3.2 Flow Diagram + +``` +User taps "Measure" button + ↓ +Measurement mode activated + ↓ +User taps first point (raycast to detect surface/object) + ↓ +Show start point indicator ● + ↓ +User moves finger (show live line + distance) + ↓ +User taps second point + ↓ +Calculate distance (Euclidean) + ↓ +Create permanent line + label (RealityKit entity) + ↓ +Add to measurements list + ↓ +Measurement mode stays active (repeat) or exit +``` + +--- + +## 4. UI/UX Design + +### 4.1 Measurement Controls Panel + +**Location**: Bottom of screen (above furniture picker if present) + +``` +┌────────────────────────────────────┐ +│ [📏 Measure] [📐 Dimensions] [ℹ️ Info] │ +│ │ +│ [Clear All] [m ↔ ft] [Export] │ +└────────────────────────────────────┘ +``` + +**Buttons**: +- **📏 Measure**: Toggle point-to-point mode +- **📐 Dimensions**: Show furniture dimensions +- **ℹ️ Info**: Show room info panel +- **Clear All**: Remove all measurements +- **m ↔ ft**: Toggle units +- **Export**: Share screenshot + text list + +### 4.2 Measurement Display Styles + +**Label Style** (floating above line): +``` +╔═══════════╗ +║ 2.45 m ║ +╚═══════════╝ +``` + +**Properties**: +- Background: Semi-transparent white/dark (adapts to scene) +- Text: Bold, 16pt +- Border: 1pt stroke +- Shadow: Subtle drop shadow for depth + +### 4.3 Visual Elements + +**Point Indicator**: +- Sphere: 0.05m radius +- Color: System blue (bright, visible) +- Material: Unlit (always visible) + +**Measurement Line**: +- Cylinder: 0.01m radius +- Color: System blue +- Material: Unlit +- Dashed style (optional) + +**Bounding Box** (for furniture dimensions): +- Wireframe box around object +- Color: System green +- Line width: 2pt +- Corner spheres for anchors + +### 4.4 Interaction States + +**Idle** (default): +- No measurement mode active +- No visual overlays + +**Measuring** (active): +- First point placed → blue sphere +- Moving finger → live line follows +- Second point placed → line + label created + +**Selected** (tap existing measurement): +- Highlight measurement in yellow +- Show edit/delete buttons +- Allow repositioning endpoints + +--- + +## 5. RealityKit Implementation + +### 5.1 Create MeasurementManager + +**File**: `Envision/Managers/MeasurementManager.swift` + +```swift +import Foundation +import RealityKit + +final class MeasurementManager { + static let shared = MeasurementManager() + + enum MeasurementUnit { + case metric // meters/centimeters + case imperial // feet/inches + } + + private(set) var measurements: [Measurement] = [] + private(set) var isActive: Bool = false + var currentUnit: MeasurementUnit = .metric { + didSet { + // Notify UI to refresh labels + NotificationCenter.default.post(name: .measurementUnitChanged, object: nil) + } + } + + private init() {} + + // MARK: - Measurement CRUD + + func addMeasurement(_ measurement: Measurement) { + measurements.append(measurement) + NotificationCenter.default.post(name: .measurementAdded, object: measurement) + } + + func removeMeasurement(id: UUID) { + measurements.removeAll { $0.id == id } + NotificationCenter.default.post(name: .measurementRemoved, object: id) + } + + func clearAllMeasurements() { + measurements.removeAll() + NotificationCenter.default.post(name: .measurementsCleared, object: nil) + } + + // MARK: - Activation + + func activateMeasurementMode() { + isActive = true + } + + func deactivateMeasurementMode() { + isActive = false + } + + // MARK: - Formatting + + func formatDistance(_ meters: Float) -> String { + switch currentUnit { + case .metric: + if meters < 1.0 { + return String(format: "%.0f cm", meters * 100) + } else { + return String(format: "%.2f m", meters) + } + case .imperial: + let feet = meters * 3.28084 + let totalInches = feet * 12 + let feetPart = Int(totalInches / 12) + let inchesPart = totalInches.truncatingRemainder(dividingBy: 12) + return String(format: "%d' %.1f\"", feetPart, inchesPart) + } + } + + func formatArea(_ squareMeters: Float) -> String { + switch currentUnit { + case .metric: + return String(format: "%.2f m²", squareMeters) + case .imperial: + let sqFeet = squareMeters * 10.7639 + return String(format: "%.2f ft²", sqFeet) + } + } + + // MARK: - Export + + func exportMeasurementsAsText() -> String { + var text = "EnVision Measurements\n" + text += "Date: \(Date().formatted())\n\n" + + for (index, measurement) in measurements.enumerated() { + text += "\(index + 1). " + text += "\(measurement.label ?? "Measurement"): " + text += formatDistance(measurement.distance) + text += "\n" + } + + return text + } +} + +// MARK: - Notification Names +extension Notification.Name { + static let measurementAdded = Notification.Name("measurementAdded") + static let measurementRemoved = Notification.Name("measurementRemoved") + static let measurementsCleared = Notification.Name("measurementsCleared") + static let measurementUnitChanged = Notification.Name("measurementUnitChanged") +} + +// MARK: - Measurement Model +struct Measurement { + let id: UUID + let type: MeasurementType + let startPoint: SIMD3 + let endPoint: SIMD3 + var distance: Float { + return simd_distance(startPoint, endPoint) + } + var label: String? + let timestamp: Date + + init(type: MeasurementType, startPoint: SIMD3, endPoint: SIMD3, label: String? = nil) { + self.id = UUID() + self.type = type + self.startPoint = startPoint + self.endPoint = endPoint + self.label = label + self.timestamp = Date() + } +} + +enum MeasurementType { + case pointToPoint + case objectDimensions + case roomDimensions +} +``` + +### 5.2 Create MeasurementVisualizer + +**File**: `Envision/Managers/MeasurementVisualizer.swift` + +```swift +import Foundation +import RealityKit + +final class MeasurementVisualizer { + + // MARK: - Create Point Indicator + + func createPointIndicator(at position: SIMD3) -> ModelEntity { + let sphere = MeshResource.generateSphere(radius: 0.02) // 2cm sphere + + var material = UnlitMaterial() + material.color = .init(tint: .systemBlue) + + let entity = ModelEntity(mesh: sphere, materials: [material]) + entity.position = position + entity.name = "measurementPoint" + + return entity + } + + // MARK: - Create Line Between Points + + func createLine(from start: SIMD3, to end: SIMD3) -> ModelEntity { + let distance = simd_distance(start, end) + + // Create cylinder as line + let cylinder = MeshResource.generateCylinder(height: distance, radius: 0.005) // 0.5cm thick + + var material = UnlitMaterial() + material.color = .init(tint: .systemBlue) + + let entity = ModelEntity(mesh: cylinder, materials: [material]) + + // Position at midpoint + let midpoint = (start + end) / 2 + entity.position = midpoint + + // Rotate to align with start-end vector + let direction = end - start + let up = SIMD3(0, 1, 0) + + // Calculate rotation to align Y-axis with direction + let rotationAxis = simd_cross(up, simd_normalize(direction)) + let rotationAngle = acos(simd_dot(up, simd_normalize(direction))) + + if simd_length(rotationAxis) > 0.001 { + entity.orientation = simd_quatf(angle: rotationAngle, axis: simd_normalize(rotationAxis)) + } + + entity.name = "measurementLine" + + return entity + } + + // MARK: - Create Text Label + + func createLabel(text: String, at position: SIMD3, billboarding: Bool = true) -> ModelEntity { + // Create text mesh + let textMesh = MeshResource.generateText( + text, + extrusionDepth: 0.001, + font: .systemFont(ofSize: 0.05), // 5cm font size + containerFrame: .zero, + alignment: .center, + lineBreakMode: .byWordWrapping + ) + + var material = UnlitMaterial() + material.color = .init(tint: .white) + + let textEntity = ModelEntity(mesh: textMesh, materials: [material]) + textEntity.position = position + SIMD3(0, 0.1, 0) // Offset above line + + // Add background panel + let panelWidth: Float = 0.3 + let panelHeight: Float = 0.08 + let panel = MeshResource.generatePlane(width: panelWidth, height: panelHeight) + + var panelMaterial = UnlitMaterial() + panelMaterial.color = .init(tint: UIColor.black.withAlphaComponent(0.7)) + + let panelEntity = ModelEntity(mesh: panel, materials: [panelMaterial]) + panelEntity.position = SIMD3(0, 0, -0.002) // Behind text + + // Group text and panel + let labelGroup = ModelEntity() + labelGroup.addChild(panelEntity) + labelGroup.addChild(textEntity) + labelGroup.position = position + labelGroup.name = "measurementLabel" + + // Optional: Make label face camera (billboarding) + if billboarding { + // Will need to update orientation each frame (see below) + } + + return labelGroup + } + + // MARK: - Create Bounding Box (for furniture dimensions) + + func createBoundingBox(for entity: ModelEntity) -> ModelEntity { + let bounds = entity.visualBounds(relativeTo: nil) + let size = bounds.extents + + // Create wireframe box + let boxGroup = ModelEntity() + boxGroup.name = "boundingBox" + + // Create 12 edges of the box + let edges: [(SIMD3, SIMD3)] = [ + // Bottom face + (SIMD3(-size.x/2, -size.y/2, -size.z/2), SIMD3(size.x/2, -size.y/2, -size.z/2)), + (SIMD3(size.x/2, -size.y/2, -size.z/2), SIMD3(size.x/2, -size.y/2, size.z/2)), + (SIMD3(size.x/2, -size.y/2, size.z/2), SIMD3(-size.x/2, -size.y/2, size.z/2)), + (SIMD3(-size.x/2, -size.y/2, size.z/2), SIMD3(-size.x/2, -size.y/2, -size.z/2)), + // Top face + (SIMD3(-size.x/2, size.y/2, -size.z/2), SIMD3(size.x/2, size.y/2, -size.z/2)), + (SIMD3(size.x/2, size.y/2, -size.z/2), SIMD3(size.x/2, size.y/2, size.z/2)), + (SIMD3(size.x/2, size.y/2, size.z/2), SIMD3(-size.x/2, size.y/2, size.z/2)), + (SIMD3(-size.x/2, size.y/2, size.z/2), SIMD3(-size.x/2, size.y/2, -size.z/2)), + // Vertical edges + (SIMD3(-size.x/2, -size.y/2, -size.z/2), SIMD3(-size.x/2, size.y/2, -size.z/2)), + (SIMD3(size.x/2, -size.y/2, -size.z/2), SIMD3(size.x/2, size.y/2, -size.z/2)), + (SIMD3(size.x/2, -size.y/2, size.z/2), SIMD3(size.x/2, size.y/2, size.z/2)), + (SIMD3(-size.x/2, -size.y/2, size.z/2), SIMD3(-size.x/2, size.y/2, size.z/2)) + ] + + for (start, end) in edges { + let line = createLine(from: start, to: end) + line.model?.materials = [createGreenMaterial()] + boxGroup.addChild(line) + } + + boxGroup.position = bounds.center + + return boxGroup + } + + private func createGreenMaterial() -> UnlitMaterial { + var material = UnlitMaterial() + material.color = .init(tint: .systemGreen) + return material + } +} +``` + +### 5.3 Update RoomViewerViewController + +**File**: `Envision/Screens/MainTabs/Rooms/furniture+room/RoomViewerViewController.swift` + +```swift +import UIKit +import RealityKit + +// Add properties +private var measurementManager = MeasurementManager.shared +private var measurementVisualizer = MeasurementVisualizer() +private var measurementMode: MeasurementMode = .inactive +private var firstMeasurementPoint: SIMD3? +private var measurementControlPanel: MeasurementControlPanel! +private var liveMeasurementLine: ModelEntity? +private var measurementEntities: [UUID: [ModelEntity]] = [:] // Track entities per measurement + +enum MeasurementMode { + case inactive + case pointToPoint + case objectDimensions +} + +// MARK: - Setup Measurement UI + +override func viewDidLoad() { + super.viewDidLoad() + // ... existing setup code ... + + setupMeasurementControls() + setupMeasurementGestures() + observeMeasurementNotifications() +} + +private func setupMeasurementControls() { + measurementControlPanel = MeasurementControlPanel() + measurementControlPanel.translatesAutoresizingMaskIntoConstraints = false + measurementControlPanel.delegate = self + view.addSubview(measurementControlPanel) + + NSLayoutConstraint.activate([ + measurementControlPanel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + measurementControlPanel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + measurementControlPanel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -16), + measurementControlPanel.heightAnchor.constraint(equalToConstant: 100) + ]) +} + +// MARK: - Measurement Gestures + +private func setupMeasurementGestures() { + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleMeasurementTap(_:))) + arView.addGestureRecognizer(tapGesture) +} + +@objc private func handleMeasurementTap(_ gesture: UITapGestureRecognizer) { + guard measurementMode != .inactive else { return } + + let location = gesture.location(in: arView) + + switch measurementMode { + case .pointToPoint: + handlePointToPointTap(at: location) + case .objectDimensions: + handleObjectDimensionsTap(at: location) + case .inactive: + break + } +} + +private func handlePointToPointTap(at location: CGPoint) { + // Raycast to find 3D position + guard let raycastResult = arView.raycast(from: location, allowing: .estimatedPlane, alignment: .any).first else { + print("⚠️ No surface found at tap location") + return + } + + let worldPosition = SIMD3( + raycastResult.worldTransform.columns.3.x, + raycastResult.worldTransform.columns.3.y, + raycastResult.worldTransform.columns.3.z + ) + + if firstMeasurementPoint == nil { + // First point + firstMeasurementPoint = worldPosition + + // Create point indicator + let pointEntity = measurementVisualizer.createPointIndicator(at: worldPosition) + arView.scene.addAnchor(pointEntity) + + print("📍 First measurement point placed") + } else { + // Second point - complete measurement + guard let startPoint = firstMeasurementPoint else { return } + + // Create measurement + let measurement = Measurement( + type: .pointToPoint, + startPoint: startPoint, + endPoint: worldPosition, + label: "Distance" + ) + + // Create visual elements + let lineEntity = measurementVisualizer.createLine(from: startPoint, to: worldPosition) + let midpoint = (startPoint + worldPosition) / 2 + let labelText = measurementManager.formatDistance(measurement.distance) + let labelEntity = measurementVisualizer.createLabel(text: labelText, at: midpoint) + let endPointEntity = measurementVisualizer.createPointIndicator(at: worldPosition) + + // Add to scene + arView.scene.addAnchor(lineEntity) + arView.scene.addAnchor(labelEntity) + arView.scene.addAnchor(endPointEntity) + + // Store entities + measurementEntities[measurement.id] = [lineEntity, labelEntity, endPointEntity] + + // Add to manager + measurementManager.addMeasurement(measurement) + + // Reset for next measurement + firstMeasurementPoint = nil + removeLiveMeasurementLine() + + print("✅ Measurement complete: \(labelText)") + } +} + +private func handleObjectDimensionsTap(at location: CGPoint) { + // Cast ray to find furniture entity + guard let entity = arView.entity(at: location) as? ModelEntity else { + print("⚠️ No object found at tap location") + return + } + + // Get bounding box + let boundingBox = measurementVisualizer.createBoundingBox(for: entity) + arView.scene.addAnchor(boundingBox) + + // Get dimensions + let bounds = entity.visualBounds(relativeTo: nil) + let size = bounds.extents + + // Create dimension labels + let width = MeasurementManager.shared.formatDistance(size.x) + let height = MeasurementManager.shared.formatDistance(size.y) + let depth = MeasurementManager.shared.formatDistance(size.z) + + let dimensionText = "W: \(width) × H: \(height) × D: \(depth)" + let labelEntity = measurementVisualizer.createLabel(text: dimensionText, at: bounds.center + SIMD3(0, size.y/2 + 0.1, 0)) + arView.scene.addAnchor(labelEntity) + + print("📐 Object dimensions: \(dimensionText)") +} + +// MARK: - Live Measurement Line (while dragging) + +override func touchesMoved(_ touches: Set, with event: UIEvent?) { + super.touchesMoved(touches, with: event) + + guard measurementMode == .pointToPoint, + let firstPoint = firstMeasurementPoint, + let touch = touches.first else { + return + } + + let location = touch.location(in: arView) + + guard let raycastResult = arView.raycast(from: location, allowing: .estimatedPlane, alignment: .any).first else { + return + } + + let currentPosition = SIMD3( + raycastResult.worldTransform.columns.3.x, + raycastResult.worldTransform.columns.3.y, + raycastResult.worldTransform.columns.3.z + ) + + // Update or create live line + if let liveLine = liveMeasurementLine { + liveLine.removeFromParent() + } + + let lineEntity = measurementVisualizer.createLine(from: firstPoint, to: currentPosition) + arView.scene.addAnchor(lineEntity) + liveMeasurementLine = lineEntity + + // Update live distance label + let distance = simd_distance(firstPoint, currentPosition) + let labelText = measurementManager.formatDistance(distance) + print("📏 Live measurement: \(labelText)") +} + +private func removeLiveMeasurementLine() { + liveMeasurementLine?.removeFromParent() + liveMeasurementLine = nil +} + +// MARK: - Notification Observers + +private func observeMeasurementNotifications() { + NotificationCenter.default.addObserver(self, selector: #selector(handleMeasurementsCleared), name: .measurementsCleared, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleMeasurementRemoved(_:)), name: .measurementRemoved, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleUnitChanged), name: .measurementUnitChanged, object: nil) +} + +@objc private func handleMeasurementsCleared() { + // Remove all measurement entities from scene + for (_, entities) in measurementEntities { + entities.forEach { $0.removeFromParent() } + } + measurementEntities.removeAll() + + // Remove bounding boxes + arView.scene.anchors.forEach { anchor in + if let entity = anchor as? ModelEntity, entity.name == "boundingBox" { + entity.removeFromParent() + } + } + + print("🗑️ All measurements cleared") +} + +@objc private func handleMeasurementRemoved(_ notification: Notification) { + guard let id = notification.object as? UUID else { return } + + // Remove entities for this measurement + measurementEntities[id]?.forEach { $0.removeFromParent() } + measurementEntities.removeValue(forKey: id) + + print("🗑️ Measurement removed: \(id)") +} + +@objc private func handleUnitChanged() { + // Refresh all measurement labels + for (id, entities) in measurementEntities { + guard let measurement = measurementManager.measurements.first(where: { $0.id == id }) else { continue } + + // Find and update label entity + if let labelEntity = entities.first(where: { $0.name == "measurementLabel" }) { + labelEntity.removeFromParent() + + let midpoint = (measurement.startPoint + measurement.endPoint) / 2 + let newLabelText = measurementManager.formatDistance(measurement.distance) + let newLabel = measurementVisualizer.createLabel(text: newLabelText, at: midpoint) + arView.scene.addAnchor(newLabel) + + // Update stored entity + if let index = entities.firstIndex(where: { $0.name == "measurementLabel" }) { + measurementEntities[id]?[index] = newLabel + } + } + } +} +``` + +--- + +## 6. ARKit Implementation (Alternative) + +If not using RealityKit, use ARKit with SceneKit for measurement visualization: + +**File**: `Envision/Managers/ARMeasurementHelper.swift` + +```swift +import ARKit +import SceneKit + +class ARMeasurementHelper { + + func createLineNode(from start: SCNVector3, to end: SCNVector3) -> SCNNode { + let lineGeometry = SCNCylinder(radius: 0.002, height: CGFloat(distance(start, end))) + lineGeometry.firstMaterial?.diffuse.contents = UIColor.systemBlue + + let lineNode = SCNNode(geometry: lineGeometry) + lineNode.position = midpoint(start, end) + + // Rotate to align with direction + lineNode.look(at: end, up: SCNVector3(0, 1, 0), localFront: SCNVector3(0, 1, 0)) + + return lineNode + } + + func createTextNode(text: String, at position: SCNVector3) -> SCNNode { + let textGeometry = SCNText(string: text, extrusionDepth: 1.0) + textGeometry.font = UIFont.systemFont(ofSize: 10) + textGeometry.firstMaterial?.diffuse.contents = UIColor.white + + let textNode = SCNNode(geometry: textGeometry) + textNode.position = position + textNode.scale = SCNVector3(0.005, 0.005, 0.005) + + // Billboard constraint (always face camera) + let constraint = SCNBillboardConstraint() + constraint.freeAxes = [.Y] + textNode.constraints = [constraint] + + return textNode + } + + private func distance(_ a: SCNVector3, _ b: SCNVector3) -> Float { + let dx = a.x - b.x + let dy = a.y - b.y + let dz = a.z - b.z + return sqrt(dx*dx + dy*dy + dz*dz) + } + + private func midpoint(_ a: SCNVector3, _ b: SCNVector3) -> SCNVector3 { + return SCNVector3((a.x + b.x) / 2, (a.y + b.y) / 2, (a.z + b.z) / 2) + } +} +``` + +--- + +## 7. Measurement Precision + +### 7.1 Accuracy Factors + +**LiDAR-based measurements** (iPhone 12 Pro+): +- Typical accuracy: ±1-2cm at 5m distance +- Best case: ±0.5cm at 1m distance + +**Visual SLAM** (non-LiDAR devices): +- Typical accuracy: ±5-10cm +- Depends on lighting, texture, movement + +### 7.2 Improve Accuracy + +```swift +// Use LiDAR when available +func performHighAccuracyRaycast(from point: CGPoint) -> SIMD3? { + // Prefer LiDAR depth data + if ARWorldTrackingConfiguration.supportsSceneReconstruction(.mesh) { + // Use scene reconstruction raycast + if let result = arView.raycast(from: point, allowing: .estimatedPlane, alignment: .any).first { + return SIMD3( + result.worldTransform.columns.3.x, + result.worldTransform.columns.3.y, + result.worldTransform.columns.3.z + ) + } + } + + // Fallback to visual SLAM + if let result = arView.hitTest(point, types: [.featurePoint, .estimatedHorizontalPlane]).first { + return SIMD3( + result.worldTransform.columns.3.x, + result.worldTransform.columns.3.y, + result.worldTransform.columns.3.z + ) + } + + return nil +} +``` + +### 7.3 Confidence Indicators + +```swift +func getMeasurementConfidence(measurement: Measurement) -> String { + let distance = measurement.distance + + if distance < 1.0 { + return "High confidence (±0.5cm)" + } else if distance < 5.0 { + return "Medium confidence (±2cm)" + } else { + return "Low confidence (±5cm)" + } +} +``` + +--- + +## 8. Code Implementation + +### 8.1 Create MeasurementControlPanel (UI) + +**File**: `Envision/Components/MeasurementControlPanel.swift` + +```swift +import UIKit + +protocol MeasurementControlPanelDelegate: AnyObject { + func didTapMeasureButton() + func didTapDimensionsButton() + func didTapInfoButton() + func didTapClearAllButton() + func didTapToggleUnits() + func didTapExportButton() +} + +final class MeasurementControlPanel: UIView { + + weak var delegate: MeasurementControlPanelDelegate? + + // MARK: - UI Elements + + private let stackView = UIStackView() + private let measureButton = UIButton(type: .system) + private let dimensionsButton = UIButton(type: .system) + private let infoButton = UIButton(type: .system) + private let clearAllButton = UIButton(type: .system) + private let toggleUnitsButton = UIButton(type: .system) + private let exportButton = UIButton(type: .system) + + // MARK: - Init + + override init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + backgroundColor = UIColor.systemBackground.withAlphaComponent(0.95) + layer.cornerRadius = 16 + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.1 + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.shadowRadius = 8 + + // Stack view + stackView.axis = .vertical + stackView.spacing = 12 + stackView.distribution = .fillEqually + stackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(stackView) + + // Top row + let topRow = UIStackView() + topRow.axis = .horizontal + topRow.spacing = 8 + topRow.distribution = .fillEqually + + configureButton(measureButton, title: "Measure", icon: "ruler", action: #selector(measureTapped)) + configureButton(dimensionsButton, title: "Dimensions", icon: "square.3.layers.3d", action: #selector(dimensionsTapped)) + configureButton(infoButton, title: "Info", icon: "info.circle", action: #selector(infoTapped)) + + topRow.addArrangedSubview(measureButton) + topRow.addArrangedSubview(dimensionsButton) + topRow.addArrangedSubview(infoButton) + + // Bottom row + let bottomRow = UIStackView() + bottomRow.axis = .horizontal + bottomRow.spacing = 8 + bottomRow.distribution = .fillEqually + + configureButton(clearAllButton, title: "Clear All", icon: "trash", action: #selector(clearAllTapped)) + configureButton(toggleUnitsButton, title: "m ↔ ft", icon: "arrow.left.arrow.right", action: #selector(toggleUnitsTapped)) + configureButton(exportButton, title: "Export", icon: "square.and.arrow.up", action: #selector(exportTapped)) + + bottomRow.addArrangedSubview(clearAllButton) + bottomRow.addArrangedSubview(toggleUnitsButton) + bottomRow.addArrangedSubview(exportButton) + + stackView.addArrangedSubview(topRow) + stackView.addArrangedSubview(bottomRow) + + NSLayoutConstraint.activate([ + stackView.topAnchor.constraint(equalTo: topAnchor, constant: 12), + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -12) + ]) + } + + private func configureButton(_ button: UIButton, title: String, icon: String, action: Selector) { + var config = UIButton.Configuration.filled() + config.baseBackgroundColor = .systemBlue + config.baseForegroundColor = .white + config.cornerStyle = .medium + config.image = UIImage(systemName: icon) + config.imagePlacement = .top + config.imagePadding = 4 + config.title = title + config.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in + var outgoing = incoming + outgoing.font = .systemFont(ofSize: 12, weight: .medium) + return outgoing + } + + button.configuration = config + button.addTarget(self, action: action, for: .touchUpInside) + } + + // MARK: - Actions + + @objc private func measureTapped() { + delegate?.didTapMeasureButton() + highlightButton(measureButton) + } + + @objc private func dimensionsTapped() { + delegate?.didTapDimensionsButton() + highlightButton(dimensionsButton) + } + + @objc private func infoTapped() { + delegate?.didTapInfoButton() + } + + @objc private func clearAllTapped() { + delegate?.didTapClearAllButton() + } + + @objc private func toggleUnitsTapped() { + delegate?.didTapToggleUnits() + + // Update button label + let currentUnit = MeasurementManager.shared.currentUnit + let newTitle = currentUnit == .metric ? "ft ↔ m" : "m ↔ ft" + toggleUnitsButton.configuration?.title = newTitle + } + + @objc private func exportTapped() { + delegate?.didTapExportButton() + } + + private func highlightButton(_ button: UIButton) { + // Visual feedback for active measurement mode + UIView.animate(withDuration: 0.2) { + button.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) + } completion: { _ in + UIView.animate(withDuration: 0.2) { + button.transform = .identity + } + } + } +} +``` + +### 8.2 Implement Delegate in RoomViewerViewController + +```swift +extension RoomViewerViewController: MeasurementControlPanelDelegate { + + func didTapMeasureButton() { + measurementMode = .pointToPoint + measurementManager.activateMeasurementMode() + print("📏 Measurement mode activated") + + // Show instruction overlay + showMeasurementInstruction("Tap two points to measure distance") + } + + func didTapDimensionsButton() { + measurementMode = .objectDimensions + measurementManager.activateMeasurementMode() + print("📐 Object dimensions mode activated") + + showMeasurementInstruction("Tap on furniture to see dimensions") + } + + func didTapInfoButton() { + measurementMode = .inactive + measurementManager.deactivateMeasurementMode() + + // Show room info panel + showRoomInfoPanel() + } + + func didTapClearAllButton() { + let alert = UIAlertController(title: "Clear All Measurements", message: "This will remove all measurement lines and labels.", preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "Clear", style: .destructive) { _ in + MeasurementManager.shared.clearAllMeasurements() + }) + + present(alert, animated: true) + } + + func didTapToggleUnits() { + let currentUnit = MeasurementManager.shared.currentUnit + MeasurementManager.shared.currentUnit = (currentUnit == .metric) ? .imperial : .metric + + print("🔄 Units toggled to: \(MeasurementManager.shared.currentUnit)") + } + + func didTapExportButton() { + let text = MeasurementManager.shared.exportMeasurementsAsText() + + let activityVC = UIActivityViewController(activityItems: [text], applicationActivities: nil) + present(activityVC, animated: true) + } + + private func showMeasurementInstruction(_ text: String) { + // Show temporary toast/banner with instruction + let label = UILabel() + label.text = text + label.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.9) + label.textColor = .white + label.textAlignment = .center + label.font = .systemFont(ofSize: 14, weight: .medium) + label.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(label) + + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + label.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + label.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + label.heightAnchor.constraint(equalToConstant: 44) + ]) + + label.layer.cornerRadius = 8 + label.clipsToBounds = true + label.alpha = 0 + + UIView.animate(withDuration: 0.3) { + label.alpha = 1 + } completion: { _ in + UIView.animate(withDuration: 0.3, delay: 2.0) { + label.alpha = 0 + } completion: { _ in + label.removeFromSuperview() + } + } + } + + private func showRoomInfoPanel() { + // Show panel with room dimensions from metadata + guard let roomMetadata = currentRoomMetadata else { + print("⚠️ No room metadata available") + return + } + + let alert = UIAlertController(title: roomName, message: nil, preferredStyle: .alert) + + var infoText = "" + + if let dimensions = roomMetadata.dimensions { + if let width = dimensions["width"] { + infoText += "Width: \(MeasurementManager.shared.formatDistance(Float(width)))\n" + } + if let length = dimensions["length"] { + infoText += "Length: \(MeasurementManager.shared.formatDistance(Float(length)))\n" + } + if let height = dimensions["height"] { + infoText += "Height: \(MeasurementManager.shared.formatDistance(Float(height)))\n" + } + + // Calculate area + if let width = dimensions["width"], let length = dimensions["length"] { + let area = Float(width * length) + infoText += "Floor Area: \(MeasurementManager.shared.formatArea(area))" + } + } else { + infoText = "No dimensions available for this room." + } + + alert.message = infoText + alert.addAction(UIAlertAction(title: "OK", style: .default)) + + present(alert, animated: true) + } +} +``` + +--- + +## 9. Testing Plan + +### 9.1 Unit Tests + +```swift +import XCTest +@testable import Envision + +class MeasurementTests: XCTestCase { + + func testDistanceCalculation() { + let start = SIMD3(0, 0, 0) + let end = SIMD3(1, 0, 0) + + let measurement = Measurement(type: .pointToPoint, startPoint: start, endPoint: end) + + XCTAssertEqual(measurement.distance, 1.0, accuracy: 0.001) + } + + func testUnitConversion() { + let manager = MeasurementManager.shared + + // Test metric + manager.currentUnit = .metric + XCTAssertEqual(manager.formatDistance(1.5), "1.50 m") + XCTAssertEqual(manager.formatDistance(0.75), "75 cm") + + // Test imperial + manager.currentUnit = .imperial + let result = manager.formatDistance(1.0) // 1m = 3.28 feet = 3' 3.4" + XCTAssertTrue(result.contains("3'")) + } + + func testAreaCalculation() { + let manager = MeasurementManager.shared + + manager.currentUnit = .metric + XCTAssertEqual(manager.formatArea(20.0), "20.00 m²") + + manager.currentUnit = .imperial + let result = manager.formatArea(20.0) // 20 m² ≈ 215 ft² + XCTAssertTrue(result.contains("ft²")) + } +} +``` + +### 9.2 Integration Tests + +**Test Scenarios**: +1. Create point-to-point measurement → Verify line + label appear +2. Toggle units → Verify labels update +3. Clear all → Verify all entities removed +4. Measure object dimensions → Verify bounding box shows +5. Export measurements → Verify text contains all measurements + +### 9.3 Manual Testing Checklist + +- [ ] Place first point → Point indicator appears +- [ ] Drag to second point → Live line follows finger +- [ ] Release → Line + label persist +- [ ] Create multiple measurements → All visible simultaneously +- [ ] Toggle m ↔ ft → Labels update instantly +- [ ] Clear all → All measurements disappear +- [ ] Tap furniture → Bounding box shows +- [ ] Export → Share sheet appears with text +- [ ] Rotate view → Labels remain visible (billboarding works) +- [ ] Switch rooms → Old measurements don't carry over + +--- + +## 10. Enhancement Ideas + +### 10.1 Advanced Features + +**1. Snap to Grid**: +```swift +func snapToGrid(_ position: SIMD3, gridSize: Float = 0.1) -> SIMD3 { + return SIMD3( + round(position.x / gridSize) * gridSize, + round(position.y / gridSize) * gridSize, + round(position.z / gridSize) * gridSize + ) +} +``` + +**2. Angle Measurement**: +```swift +struct AngleMeasurement { + let point1: SIMD3 + let vertex: SIMD3 + let point2: SIMD3 + + var angle: Float { + let v1 = point1 - vertex + let v2 = point2 - vertex + let dot = simd_dot(simd_normalize(v1), simd_normalize(v2)) + return acos(dot) * (180.0 / .pi) // Degrees + } +} +``` + +**3. Area Measurement** (polygon): +```swift +func calculateArea(points: [SIMD3]) -> Float { + // Shoelace formula for polygon area + var area: Float = 0 + for i in 0.. Float { + let bounds = entity.visualBounds(relativeTo: nil) + let size = bounds.extents + return size.x * size.y * size.z +} +``` + +**5. Persistent Measurements** (save to room metadata): +```swift +extension RoomMetadata { + var measurements: [Measurement]? +} + +// Save +roomMetadata.measurements = MeasurementManager.shared.measurements + +// Load +if let savedMeasurements = roomMetadata.measurements { + savedMeasurements.forEach { recreateMeasurementVisuals($0) } +} +``` + +### 10.2 UI Enhancements + +**1. Measurement History Panel**: +``` +┌────────────────────────────┐ +│ Measurements (3) │ +│ ──────────────────────── │ +│ 1. Distance: 2.45 m │ +│ 2. Chair width: 0.65 m │ +│ 3. Table to wall: 1.20 m │ +└────────────────────────────┘ +``` + +**2. Visual Feedback**: +- Haptic feedback on tap (UIImpactFeedbackGenerator) +- Sound effect on measurement complete +- Pulse animation for point indicators + +**3. Accessibility**: +- VoiceOver support for measurements +- High contrast mode for lines/labels +- Dynamic Type for labels + +### 10.3 Performance Optimization + +**1. Entity Pooling**: +```swift +class MeasurementEntityPool { + private var linePool: [ModelEntity] = [] + private var labelPool: [ModelEntity] = [] + + func getLine() -> ModelEntity { + return linePool.popLast() ?? createNewLine() + } + + func returnLine(_ entity: ModelEntity) { + entity.isEnabled = false + linePool.append(entity) + } +} +``` + +**2. LOD (Level of Detail)**: +```swift +func updateMeasurementVisuals(cameraPosition: SIMD3) { + for (id, entities) in measurementEntities { + let distance = simd_distance(cameraPosition, entities[0].position) + + if distance > 10.0 { + // Far away - hide labels, show simplified lines + entities.forEach { $0.isEnabled = false } + } else { + // Close - show full detail + entities.forEach { $0.isEnabled = true } + } + } +} +``` + +--- + +## Appendix: Quick Start Guide + +### For Developers + +**Step 1**: Copy files to project +- `MeasurementManager.swift` → `Envision/Managers/` +- `MeasurementVisualizer.swift` → `Envision/Managers/` +- `MeasurementControlPanel.swift` → `Envision/Components/` + +**Step 2**: Update `RoomViewerViewController.swift` +- Add properties + setup methods +- Implement delegate +- Add gesture handlers + +**Step 3**: Test +- Build and run +- Navigate to room viewer +- Tap "Measure" → Tap two points +- Verify line + label appear + +**Step 4**: Customize +- Adjust colors in visualizer +- Change label font size +- Add more measurement types + +--- + +**Document Version**: 1.0 +**Last Updated**: January 21, 2026 +**Estimated Implementation Time**: 3-5 days +**Priority**: Medium-High + +--- + +*End of AR Measurement Tool Implementation Plan* diff --git a/COMPLETE_TECHNICAL_DOCUMENTATION.md b/COMPLETE_TECHNICAL_DOCUMENTATION.md new file mode 100644 index 0000000..92b9e12 --- /dev/null +++ b/COMPLETE_TECHNICAL_DOCUMENTATION.md @@ -0,0 +1,1029 @@ +# EnVision - Complete Technical Documentation +*Last Updated: January 21, 2026* + +--- + +## Table of Contents +1. [Project Overview](#project-overview) +2. [Architecture](#architecture) +3. [Authentication Flow](#authentication-flow) +4. [Core Features](#core-features) +5. [Data Models](#data-models) +6. [File Structure](#file-structure) +7. [Third-Party Dependencies](#third-party-dependencies) +8. [Backend Design (Firebase)](#backend-design-firebase) +9. [Tips & Tour System (REMOVED)](#tips--tour-system-removed) +10. [Key Improvements Needed](#key-improvements-needed) + +--- + +## 1. Project Overview + +**EnVision** is an iOS 17+ AR/spatial computing app that allows users to: +- Scan rooms using **RoomPlan** (LiDAR) +- Capture furniture using **photogrammetry** (Object Capture) +- View and edit 3D models in AR +- Organize rooms and furniture with categories +- Manage user profiles with preferences + +**Tech Stack:** +- **Language**: Swift (UIKit programmatic, no Storyboards except LaunchScreen) +- **Frameworks**: ARKit, RealityKit, RoomPlan, AVFoundation, QuickLook +- **Persistence**: UserDefaults (currently), FileManager (3D models) +- **Planned Backend**: Firebase (Auth + Firestore + Storage) + +--- + +## 2. Architecture + +### 2.1 App Entry Flow +``` +SceneDelegate (willConnectTo) + ↓ +SplashViewController (logo animation ~2s) + ↓ +OnboardingController (UIPageViewController, 3 pages) + ↓ +LoginViewController + ↓ +MainTabBarController (3 tabs) +``` + +**Note**: Currently no "auto-skip login" logic. Every launch goes through Splash → Onboarding → Login. + +### 2.2 Navigation Structure +``` +MainTabBarController +├── Tab 0: My Rooms (UINavigationController) +│ └── MyRoomsViewController +│ ├── RoomPlanScannerViewController (LiDAR scan) +│ ├── RoomPreviewViewController (save scanned room) +│ ├── RoomViewerViewController (3D viewer + furniture placement) +│ └── RoomEditVC (edit room metadata) +├── Tab 1: My Furniture (UINavigationController) +│ └── ScanFurnitureViewController +│ ├── ObjectScanViewController (auto-capture photogrammetry) +│ ├── ObjectCapturePreviewController (process images) +│ ├── CreateModelViewController (manual photo selection) +│ └── ViewModelsViewController (browse USDZ files) +└── Tab 2: Profile (UINavigationController) + └── ProfileViewController + ├── EditProfileViewController + ├── TipsLibraryViewController (static tips list) + ├── SettingsViewController + └── ThemeViewController +``` + +### 2.3 Key Design Patterns +- **Programmatic UIKit**: No Interface Builder (except LaunchScreen.storyboard) +- **Singleton Managers**: `UserManager`, `SaveManager`, `TourManager`, `MetadataManager` +- **Delegate Pattern**: Used for camera capture, search, collection view +- **File-based Persistence**: USDZ models stored in Documents directory +- **JSON Metadata**: Room/furniture metadata stored as JSON files + +--- + +## 3. Authentication Flow + +### 3.1 Current Implementation (Local Only) + +#### Files Involved: +- `Envision/Screens/Onboarding/LoginViewController.swift` +- `Envision/Screens/Onboarding/SignupViewController.swift` +- `Envision/Screens/Onboarding/ForgotPasswordViewController.swift` +- `Envision/Extensions/UserManager.swift` +- `Envision/Extensions/UserModel.swift` +- `Envision/SceneDelegate.swift` + +#### Login Flow: +1. **UI**: `LoginViewController` + - Email + Password fields (custom `ModernTextField`) + - "Continue" button → `handleLogin()` + - "Create Account" → pushes `SignupViewController` + - "Forgot Password" → pushes `ForgotPasswordViewController` + +2. **Validation**: + - Non-empty fields + - Valid email format (`String.isValidEmail` in `Extensions.swift`) + +3. **Auth Logic** (currently simulated): + ```swift + UserManager.shared.login(email: String, password: String) { success in + if success { + SceneDelegate.shared?.switchToMainApp() + } + } + ``` + +4. **Current Behavior**: + - If a user exists in UserDefaults with matching email → success + - Else creates a new user and logs in (no password verification) + +#### Signup Flow: +1. **UI**: `SignupViewController` + - Name, Email, Password, Confirm Password + - Validates strong password (`String.isStrongPassword`) + +2. **Auth Logic**: + ```swift + UserManager.shared.signup(name:email:password:) { success in + if success { + SceneDelegate.shared?.switchToMainApp() + } + } + ``` + +3. **Current Behavior**: + - Creates `UserModel` with provided data + - Stores in UserDefaults as JSON (`"currentUser"` key) + - Switches to `MainTabBarController` + +#### Forgot Password Flow: +1. **UI**: `ForgotPasswordViewController` + - Email field only + - "Send Reset Link" button → `handleReset()` + +2. **Current Behavior**: + - Validates email + - Shows alert "Check your email" + - **No actual backend call** (TODO comment in code) + +### 3.2 Session Management + +**Current**: +- `UserManager.shared.currentUser` stored in `UserDefaults` +- `UserManager.shared.isLoggedIn` checks if `currentUser != nil` +- No auto-login on app launch + +**Logout**: +- `UserManager.shared.logout()` clears UserDefaults +- `SceneDelegate.shared?.switchToLogin()` resets root to login + +--- + +## 4. Core Features + +### 4.1 Room Scanning (RoomPlan + LiDAR) + +#### Files: +- `Envision/Screens/MainTabs/Rooms/MyRoomsViewController.swift` (main library) +- `Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPlanScannerViewController.swift` (LiDAR scan) +- `Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPreviewViewController.swift` (save scan) +- `Envision/Screens/MainTabs/Rooms/RoomModel.swift` (data model) +- `Envision/Screens/MainTabs/Rooms/MetadataManager.swift` (JSON persistence) + +#### Flow: +1. **Scan Initiation**: `MyRoomsViewController` → "+" button → `RoomPlanScannerViewController` +2. **LiDAR Capture**: User walks around room, RoomPlan builds 3D mesh +3. **Preview**: `RoomPreviewViewController` shows captured room +4. **Save**: + - USDZ file saved to `Documents/roomPlan/{UUID}.usdz` + - Metadata saved to `Documents/roomPlan/rooms_metadata.json` + - Thumbnail generated using QuickLook + +#### Data Model: +```swift +struct RoomModel { + let id: UUID + let name: String + var category: RoomCategory + let createdAt: Date + let usdzFilename: String + var thumbnailPath: String? +} + +struct RoomMetadata { + var name: String + var category: String + var createdAt: String + var dimensions: [String: Double]? + var notes: String? +} +``` + +#### Categories: +- Living Room, Bedroom, Kitchen, Bathroom, Office, Dining Room, Garage, Outdoor, Other + +### 4.2 Furniture Capture (Object Capture / Photogrammetry) + +#### Files: +- `Envision/Screens/MainTabs/furniture/ScanFurnitureViewController.swift` (library) +- `Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift` (auto-capture) +- `Envision/Screens/MainTabs/furniture/Object Capture/ObjectCapturePreviewController.swift` (process) +- `Envision/Screens/MainTabs/furniture/CreateModel/CreateModelViewController.swift` (manual photos) +- `Envision/Screens/MainTabs/furniture/FurnitureCategory.swift` + +#### Flow: +1. **Scan Initiation**: `ScanFurnitureViewController` → Scan menu +2. **Options**: + - **Automatic Object Capture**: `ObjectScanViewController` + - Auto-captures photos every 0.5s while user walks around object + - Shows counter, quality indicator, guidance + - Minimum 20 photos (recommended 50+) + - **Create From Photos**: `CreateModelViewController` + - User manually selects 20-100 photos from library + - Validates photo count and quality + +3. **Processing**: `ObjectCapturePreviewController` + - Uses `PhotogrammetrySession` (iOS 17+) + - Generates USDZ model + - Saves to `Documents/furniture/{UUID}.usdz` + +4. **Storage**: + - USDZ files in `Documents/furniture/` + - Thumbnails cached in-memory + +#### Categories: +- Seating, Tables, Storage, Beds, Lighting, Decor, Kitchen, Outdoor, Office, Electronics, Other + +### 4.3 3D Viewing & AR Placement + +#### Files: +- `Envision/Screens/MainTabs/Rooms/furniture+room/RoomViewerViewController.swift` +- `Envision/Screens/MainTabs/Rooms/furniture+room/FurniturePicker.swift` +- `Envision/Screens/MainTabs/Rooms/furniture+room/FurnitureControlPanel.swift` +- `Envision/Screens/MainTabs/Rooms/furniture+room/OrbitJoystick.swift` + +#### Features: +- **RealityKit-based 3D viewer** +- **Furniture placement** via drag-and-drop +- **Transform controls**: Move, Rotate, Scale +- **Orbit camera** with joystick +- **Save/Load** placed furniture positions +- **AR Preview** (launches QuickLook AR) + +### 4.4 Profile & Settings + +#### Files: +- `Envision/Screens/MainTabs/profile/ProfileViewController.swift` +- `Envision/Screens/MainTabs/profile/EditProfileViewController.swift` +- `Envision/Screens/MainTabs/profile/SubScreens/SettingsViewController.swift` +- `Envision/Screens/MainTabs/profile/SubScreens/ThemeViewController.swift` +- `Envision/Screens/MainTabs/profile/SubScreens/TipsLibraryViewController.swift` + +#### Features: +- **Edit Profile**: Name, Email, Bio, Profile Picture +- **Preferences**: Notifications, Scan Reminders, New Features +- **Theme**: Light, Dark, System +- **Tips & Tutorials**: Static list of all tips (not affected by TipKit removal) +- **App Tour Reset**: Resets tour state in `TourManager` +- **Logout**: Clears session and returns to login + +--- + +## 5. Data Models + +### 5.1 User Model +```swift +struct UserModel: Codable { + let id: String + var name: String + var email: String + var bio: String? + var profileImagePath: String? + let createdAt: Date + var preferences: UserPreferences +} + +struct UserPreferences: Codable { + var notificationsEnabled: Bool = true + var scanReminders: Bool = true + var newFeatureAlerts: Bool = true + var theme: Int = 0 // 0: System, 1: Light, 2: Dark +} +``` + +**Persistence**: UserDefaults (`"currentUser"` key, JSON-encoded) + +### 5.2 Room Model +```swift +struct RoomModel { + let id: UUID + let name: String + var category: RoomCategory + let createdAt: Date + let usdzFilename: String + var thumbnailPath: String? +} + +struct RoomMetadata: Codable { + var name: String + var category: String + var createdAt: String + var dimensions: [String: Double]? + var notes: String? +} + +struct RoomsMetadata: Codable { + let version: String + var rooms: [String: RoomMetadata] // Key = filename +} +``` + +**Persistence**: +- USDZ files: `Documents/roomPlan/{filename}.usdz` +- Metadata: `Documents/roomPlan/rooms_metadata.json` +- Thumbnails: `Documents/roomPlan/thumbnails/{filename}.jpg` + +### 5.3 Furniture Model +```swift +// No explicit struct - just file URLs +// Category inference from filename or UserDefaults + +enum FurnitureCategory: String, CaseIterable { + case seating, tables, storage, beds, lighting, + decor, kitchen, outdoor, office, electronics, other + + var icon: String { /* SF Symbol */ } + var color: UIColor { /* Category color */ } +} +``` + +**Persistence**: +- USDZ files: `Documents/furniture/{filename}.usdz` +- Category: `UserDefaults` (`"furniture_category_{filename}"`) +- Thumbnails: In-memory cache (`NSCache`) + +--- + +## 6. File Structure + +### 6.1 Project Organization +``` +EnVision/ +├── Envision/ +│ ├── AppDelegate.swift # App lifecycle, theme setup +│ ├── SceneDelegate.swift # Window/scene management, root switching +│ ├── MainTabBarController.swift # 3-tab container +│ ├── Info.plist # App config (camera, photo library, ARKit) +│ │ +│ ├── Assets.xcassets/ # Images, icons, SF Symbols +│ ├── Base.lproj/ +│ │ └── LaunchScreen.storyboard # Only storyboard in project +│ │ +│ ├── 3D_Models/ # Sample USDZ files +│ │ ├── chair.usdz +│ │ ├── table.usdz +│ │ ├── hall.usdz +│ │ └── ios_room*.usdz +│ │ +│ ├── Components/ # Reusable UI components +│ │ ├── CustomTextField.swift +│ │ ├── PrimaryButton.swift +│ │ └── ModernTextField.swift +│ │ +│ ├── Extensions/ # Utilities & managers +│ │ ├── Extensions.swift # String validation, Date formatting +│ │ ├── UIColor+Hex.swift # Hex color support +│ │ ├── UIFont+AppFonts.swift # Custom fonts (if any) +│ │ ├── UIViewController+Transition.swift +│ │ ├── Entity+Visit.swift # RealityKit entity helpers +│ │ ├── UserManager.swift # Auth & user state +│ │ ├── UserModel.swift # User data model +│ │ └── SaveManager.swift # File I/O helpers +│ │ +│ ├── Managers/ +│ │ └── TourManager.swift # App tour state (deprecated) +│ │ +│ ├── Tips/ # TipKit (REMOVED, placeholder only) +│ │ ├── AppTips.swift # Empty placeholder +│ │ └── TipPresenter.swift # Empty placeholder +│ │ +│ └── Screens/ +│ ├── Onboarding/ # Login, Signup, Forgot Password +│ │ ├── SplashViewController.swift +│ │ ├── OnboardingController.swift +│ │ ├── OnboardingPage.swift +│ │ ├── LoginViewController.swift +│ │ ├── SignupViewController.swift +│ │ ├── ForgotPasswordViewController.swift +│ │ ├── ModernTextField.swift +│ │ └── SocialButton.swift +│ │ +│ └── MainTabs/ +│ ├── Rooms/ # Room scanning & management +│ │ ├── MyRoomsViewController.swift +│ │ ├── MyRoomsViewController+helpers.swift +│ │ ├── RoomModel.swift +│ │ ├── RoomCategory.swift +│ │ ├── RoomCell.swift +│ │ ├── MetadataManager.swift +│ │ ├── RoomPlanScan/ +│ │ │ ├── RoomPlanScannerViewController.swift +│ │ │ └── RoomPreviewViewController.swift +│ │ └── furniture+room/ +│ │ ├── RoomViewerViewController.swift +│ │ ├── RoomEditVC.swift +│ │ ├── RoomVisualizeVC.swift +│ │ ├── FurniturePicker.swift +│ │ ├── FurnitureControlPanel.swift +│ │ └── OrbitJoystick.swift +│ │ +│ ├── furniture/ # Furniture capture & library +│ │ ├── ScanFurnitureViewController.swift +│ │ ├── FurnitureCategory.swift +│ │ ├── FurnitureCell.swift +│ │ ├── Object Capture/ +│ │ │ ├── ObjectScanViewController.swift +│ │ │ ├── ObjectCapturePreviewController.swift +│ │ │ ├── ARMeshExporter.swift +│ │ │ ├── InstructionOverlay.swift +│ │ │ ├── FeedbackBubble.swift +│ │ │ └── ArrowGuideView.swift +│ │ ├── CreateModel/ +│ │ │ ├── CreateModelViewController.swift +│ │ │ └── CreateModelViewController2.swift +│ │ ├── ModelsFromFiles/ +│ │ │ ├── ViewModelsViewController.swift +│ │ │ └── USDZCell.swift +│ │ └── roomPlanColor/ +│ │ └── (Color customization - not fully implemented) +│ │ +│ └── profile/ # User profile & settings +│ ├── ProfileViewController.swift +│ ├── ProfileCell.swift +│ ├── EditProfileViewController.swift +│ └── SubScreens/ +│ ├── SettingsViewController.swift +│ ├── ThemeViewController.swift +│ ├── TipsLibraryViewController.swift +│ ├── AboutViewController.swift +│ ├── PrivacyViewController.swift +│ └── SupportViewController.swift +│ +└── Envision.xcodeproj/ +``` + +### 6.2 Documents Directory Structure (Runtime) +``` +Documents/ +├── roomPlan/ +│ ├── {uuid}.usdz # Room USDZ files +│ ├── rooms_metadata.json # All room metadata +│ └── thumbnails/ +│ └── {uuid}.jpg +│ +└── furniture/ + └── {uuid}.usdz # Furniture USDZ files +``` + +--- + +## 7. Third-Party Dependencies + +**Current**: None (uses only Apple frameworks) + +**Planned** (for Firebase backend): +- Firebase SDK (via Swift Package Manager) + - FirebaseAuth + - FirebaseFirestore + - FirebaseStorage + +--- + +## 8. Backend Design (Firebase) + +### 8.1 Recommended Firebase Products + +1. **Firebase Authentication** + - Email/Password authentication + - Password reset emails + - (Future) Sign in with Apple, Google + +2. **Cloud Firestore** + - User profiles + - Room metadata + - Furniture metadata + - Shared collections + +3. **Firebase Storage** + - Profile pictures + - Room USDZ files (optional, can stay local) + - Furniture USDZ files (optional) + - Thumbnails + +### 8.2 Firestore Data Model + +#### Collection: `users/{uid}` +```json +{ + "name": "John Doe", + "email": "john@example.com", + "bio": "AR enthusiast", + "profileImageURL": "gs://bucket/users/{uid}/profile.jpg", + "createdAt": "2026-01-21T10:00:00Z", + "preferences": { + "notificationsEnabled": true, + "scanReminders": true, + "newFeatureAlerts": true, + "theme": 0 + } +} +``` + +#### Collection: `users/{uid}/rooms/{roomId}` +```json +{ + "id": "uuid", + "name": "Living Room", + "category": "living", + "createdAt": "2026-01-21T10:30:00Z", + "usdzURL": "gs://bucket/users/{uid}/rooms/{roomId}.usdz", + "thumbnailURL": "gs://bucket/users/{uid}/rooms/{roomId}_thumb.jpg", + "dimensions": { + "width": 5.2, + "length": 4.8, + "height": 2.7 + }, + "notes": "Main living area" +} +``` + +#### Collection: `users/{uid}/furniture/{furnitureId}` +```json +{ + "id": "uuid", + "name": "Modern Chair", + "category": "seating", + "createdAt": "2026-01-21T11:00:00Z", + "usdzURL": "gs://bucket/users/{uid}/furniture/{furnitureId}.usdz", + "thumbnailURL": "gs://bucket/users/{uid}/furniture/{furnitureId}_thumb.jpg" +} +``` + +### 8.3 Implementation Plan + +#### Phase 1: Authentication +1. Add Firebase SDK via SPM +2. Configure `FirebaseApp` in `AppDelegate` +3. Replace `UserManager.login/signup` with Firebase Auth calls: + ```swift + Auth.auth().signIn(withEmail:password:) { result, error in + // Fetch user doc from Firestore + } + + Auth.auth().createUser(withEmail:password:) { result, error in + // Create user doc in Firestore + } + + Auth.auth().sendPasswordReset(withEmail:) { error in + // Show success alert + } + ``` +4. Add auto-login in `SplashViewController`: + ```swift + if Auth.auth().currentUser != nil { + switchToMainApp() + } else { + goToOnboarding() + } + ``` + +#### Phase 2: User Profile Sync +1. Create Firestore helper: `FirestoreManager.swift` +2. On login/signup: fetch/create user doc +3. On profile edit: update Firestore + local cache +4. Keep local cache for offline access + +#### Phase 3: Room & Furniture Sync +1. Upload USDZ to Firebase Storage (optional, bandwidth consideration) +2. Store metadata in Firestore +3. Sync on app launch / manual refresh +4. Show sync indicator in UI + +#### Phase 4: Security Rules +```javascript +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /users/{userId} { + allow read, write: if request.auth.uid == userId; + + match /rooms/{roomId} { + allow read, write: if request.auth.uid == userId; + } + + match /furniture/{furnitureId} { + allow read, write: if request.auth.uid == userId; + } + } + } +} +``` + +### 8.4 Migration Strategy +- Keep existing file-based storage as primary +- Add Firebase as sync/backup layer +- Implement "Export/Import" feature for USDZ files +- Show cloud sync status in UI + +--- + +## 9. Tips & Tour System (REMOVED) + +### 9.1 Previous Implementation (Now Removed) + +**What was removed**: +- `import TipKit` from all files +- `Tips.configure()` in `AppDelegate` +- `Tips.resetDatastore()` in `TourManager` +- All `TipView(...)` hosting in view controllers +- `AppTips.swift` definitions (25 tips) +- `TipPresenter.swift` SwiftUI hosting controller + +**Reason for removal**: +- SwiftUI `TipView` hosting in UIKit was causing layout issues (vertical text, overlapping UI, non-responsive buttons) +- TipKit requires iOS 17+ and was adding complexity without stable behavior + +### 9.2 What Still Exists (Unchanged) + +**Profile → Tips & Tutorials**: +- `TipsLibraryViewController.swift` remains fully functional +- Shows static list of all tips (hardcoded) +- User can browse tips anytime regardless of app state +- No TipKit dependency + +**TourManager**: +- `TourManager.swift` remains for tour state tracking +- Stores `hasCompletedTour`, `currentTourStep` in UserDefaults +- `resetTour()` clears tour state (no longer calls TipKit) +- Used by "Restart App Tour" button in Profile + +### 9.3 Future Recommendation: Pure UIKit Tips + +If tips need to be re-added, implement as **pure UIKit** (no SwiftUI): + +1. **Custom UIView subclass**: `TipBubbleView` + - Arrow pointer (CAShapeLayer) + - Title + message labels + - Action buttons (primary + dismiss) + - Auto-layout constraints + +2. **Presentation**: + - Add as subview to target view controller + - Position relative to anchor view (e.g., below nav bar, above button) + - Animate in/out with spring animations + +3. **State Management**: + - Keep `TourManager` for progression tracking + - Store "seen tips" in UserDefaults + - Rules-based showing (e.g., "show after first room scan") + +4. **Benefits**: + - Full control over layout + - No SwiftUI hosting issues + - Responsive touch handling + - Native UIKit feel + +--- + +## 10. Key Improvements Needed + +### Priority 1: Tips & Tour System (CRITICAL) + +**Problem**: Tips feature was removed due to instability. App has no onboarding guidance. + +**Recommendation**: Implement pure UIKit tips system (see section 9.3) + +**Implementation Checklist**: +- [ ] Create `TipBubbleView.swift` (UIView subclass) + - Arrow pointer with CAShapeLayer + - Title, message, primary button, dismiss button + - Constraint-based layout +- [ ] Create `TipCoordinator.swift` + - Manages tip lifecycle (show, dismiss, track) + - Rules engine (conditions for showing) + - Integrates with TourManager +- [ ] Define tip content (same as old AppTips.swift): + - Welcome tip (on first launch) + - My Rooms tips (scan, import, actions menu) + - Furniture tips (capture, quality) + - Profile tips (settings, customization) +- [ ] Add tip anchors to view controllers: + - `MyRoomsViewController`: below nav bar, above collection view + - `ScanFurnitureViewController`: below scan button + - `ProfileViewController`: above settings row +- [ ] Wire progression: + - Step 1: Welcome → My Rooms + - Step 2: First room scan → Furniture + - Step 3: First furniture capture → Profile + - Step 4: Complete tour +- [ ] Add "Skip Tour" option (respects user choice) +- [ ] Test on iPhone 14 Pro / 15 Pro (different screen sizes) +- [ ] Ensure tips don't block critical UI elements + +**Estimated Effort**: 2-3 days + +--- + +### Priority 2: Firebase Backend Integration + +**Problem**: Auth & data currently local-only (UserDefaults, FileManager). No sync, no multi-device. + +**Recommendation**: Implement Firebase (see section 8) + +**Implementation Checklist**: +- [ ] Add Firebase SDK via SPM +- [ ] Configure Firebase project (console.firebase.google.com) +- [ ] Add `GoogleService-Info.plist` to Xcode +- [ ] Configure `FirebaseApp` in `AppDelegate.application(_:didFinishLaunchingWithOptions:)` +- [ ] Rewrite `UserManager.login/signup/reset` with Firebase Auth +- [ ] Add auto-login in `SplashViewController` +- [ ] Create `FirestoreManager.swift` for Firestore operations +- [ ] Sync user profile on login/edit +- [ ] (Optional) Upload USDZ to Firebase Storage +- [ ] Add offline caching (Firestore has built-in cache) +- [ ] Implement security rules +- [ ] Add sync indicator UI (cloud icon in nav bar) +- [ ] Handle network errors gracefully + +**Estimated Effort**: 4-5 days + +--- + +### Priority 3: Auto-Login & Session Persistence + +**Problem**: User must log in every time app launches (no persistent session check). + +**Recommendation**: Check Firebase Auth state in `SplashViewController` + +**Implementation**: +```swift +// In SplashViewController.goNext() +if let user = Auth.auth().currentUser { + // User already logged in + SceneDelegate.shared?.switchToMainApp() +} else { + // Show onboarding + let onboarding = OnboardingController() + present(onboarding, animated: true) +} +``` + +**Implementation Checklist**: +- [ ] Add Firebase Auth state check in `SplashViewController` +- [ ] Keep onboarding for first-time users only +- [ ] Add "Show Onboarding Again" option in Settings +- [ ] Test logout → re-login flow +- [ ] Test app termination → relaunch (session should persist) + +**Estimated Effort**: 1 day + +--- + +### Priority 4: Error Handling & Loading States + +**Problem**: No consistent error handling. No loading indicators during async operations. + +**Recommendation**: Add error alerts + loading overlays + +**Implementation Checklist**: +- [ ] Create `ErrorAlertHelper.swift` (standard error alert factory) +- [ ] Create `LoadingOverlay.swift` (reusable loading view) +- [ ] Add error handling to: + - Login/Signup (network errors, invalid credentials) + - Room scanning (RoomPlan failure, no LiDAR) + - Furniture capture (insufficient photos, processing failure) + - File operations (disk full, permission denied) +- [ ] Add loading states to: + - Login/Signup (show spinner during auth) + - Room/Furniture processing (show progress %) + - File uploads (Firebase Storage) +- [ ] Add retry logic for network failures +- [ ] Log errors to console (or Firebase Crashlytics) + +**Estimated Effort**: 2 days + +--- + +### Priority 5: Thumbnail Generation Optimization + +**Problem**: Thumbnails generated synchronously on main thread (blocks UI). + +**Recommendation**: Move to background queue + cache + +**Implementation Checklist**: +- [ ] Use `QLThumbnailGenerator` with `.background` queue +- [ ] Cache thumbnails in Documents (persistent, not in-memory only) +- [ ] Show placeholder image while generating +- [ ] Regenerate thumbnails if USDZ changes +- [ ] Add "Clear Thumbnail Cache" option in Settings + +**Estimated Effort**: 1 day + +--- + +### Priority 6: Search & Filter UX + +**Problem**: Search is basic text matching. No advanced filters (date, size, category combination). + +**Recommendation**: Add filter chips + sort options + +**Implementation Checklist**: +- [ ] Add sort menu (Name A-Z, Date Created, Recently Modified) +- [ ] Add multi-select category filter (not just single) +- [ ] Add date range filter (Last 7 days, Last 30 days, Custom) +- [ ] Add size filter for rooms (Small, Medium, Large) +- [ ] Persist filter/sort state in UserDefaults +- [ ] Show "Clear Filters" button when active + +**Estimated Effort**: 2 days + +--- + +### Priority 7: AR Placement Improvements + +**Problem**: Furniture placement in `RoomViewerViewController` is basic. No snap-to-grid, no collision detection. + +**Recommendation**: Add placement helpers + +**Implementation Checklist**: +- [ ] Add snap-to-grid (0.1m increments) +- [ ] Add collision detection (furniture can't overlap) +- [ ] Add "Align to Wall" button (snap to nearest room wall) +- [ ] Add measurement tool (show distance between furniture) +- [ ] Add undo/redo for transforms +- [ ] Add "Reset Position" button (return to original placement) +- [ ] Save/load furniture transforms in room metadata + +**Estimated Effort**: 3 days + +--- + +### Priority 8: Accessibility + +**Problem**: No VoiceOver support, no Dynamic Type support. + +**Recommendation**: Add accessibility labels + scale fonts + +**Implementation Checklist**: +- [ ] Add `.accessibilityLabel` to all interactive elements +- [ ] Add `.accessibilityHint` for non-obvious actions +- [ ] Support Dynamic Type (use `.preferredFont(forTextStyle:)`) +- [ ] Test with VoiceOver enabled +- [ ] Add high contrast mode support +- [ ] Add reduce motion support (disable fancy animations) +- [ ] Test with Accessibility Inspector + +**Estimated Effort**: 2 days + +--- + +### Priority 9: Localization + +**Problem**: All strings hardcoded in English. + +**Recommendation**: Extract strings to `Localizable.strings` + +**Implementation Checklist**: +- [ ] Create `Localizable.strings` (English) +- [ ] Replace all hardcoded strings with `NSLocalizedString` +- [ ] Add Spanish localization (es.lproj) +- [ ] Add French localization (fr.lproj) +- [ ] Test language switching +- [ ] Localize Info.plist strings (camera/photo permissions) + +**Estimated Effort**: 3 days + +--- + +### Priority 10: Unit & UI Tests + +**Problem**: No tests. No CI/CD. + +**Recommendation**: Add XCTest suite + +**Implementation Checklist**: +- [ ] Create `EnvisionTests` target +- [ ] Add unit tests for: + - `UserManager` (login/signup/logout) + - `MetadataManager` (load/save) + - `RoomModel` (init, category inference) + - String extensions (email/password validation) +- [ ] Create `EnvisionUITests` target +- [ ] Add UI tests for: + - Login flow (happy path + error cases) + - Signup flow + - Room scan flow (mock LiDAR) + - Furniture capture flow (mock camera) +- [ ] Set up GitHub Actions CI (run tests on PR) + +**Estimated Effort**: 4 days + +--- + +## Summary of Improvements (Prioritized) + +| Priority | Feature | Effort | Impact | Status | +|----------|---------|--------|--------|--------| +| **1** | **Tips & Tour System (UIKit)** | 2-3 days | **Critical** | ⚠️ **REMOVED** | +| **2** | **Firebase Backend** | 4-5 days | High | ❌ Not started | +| **3** | **Auto-Login** | 1 day | High | ❌ Not started | +| **4** | **Error Handling** | 2 days | High | ⚠️ Partial | +| **5** | **Thumbnail Optimization** | 1 day | Medium | ⚠️ Partial | +| **6** | **Search & Filter UX** | 2 days | Medium | ⚠️ Basic only | +| **7** | **AR Placement Helpers** | 3 days | Medium | ❌ Not started | +| **8** | **Accessibility** | 2 days | Medium | ❌ Not started | +| **9** | **Localization** | 3 days | Low | ❌ Not started | +| **10** | **Unit & UI Tests** | 4 days | Low | ❌ Not started | + +**Total Estimated Effort**: ~24-27 days + +--- + +## Appendix: Key Code Snippets + +### A1: UserManager.login (Current - Local Only) +```swift +// Envision/Extensions/UserManager.swift +func login(email: String, password: String, completion: @escaping (Bool) -> Void) { + // Simulated login + if let user = currentUser, user.email == email { + completion(true) + } else { + // Create new user (no password verification) + let newUser = UserModel( + id: UUID().uuidString, + name: email.components(separatedBy: "@").first ?? "User", + email: email, + createdAt: Date(), + preferences: UserPreferences() + ) + currentUser = newUser + completion(true) + } +} +``` + +### A2: SceneDelegate.switchToMainApp +```swift +// Envision/SceneDelegate.swift +func switchToMainApp() { + let mainVC = MainTabBarController() + + window?.rootViewController = mainVC + window?.makeKeyAndVisible() + + UIView.transition(with: window!, duration: 0.4, options: .transitionCrossDissolve) { + // Smooth fade transition + } +} +``` + +### A3: RoomPlanScanner (LiDAR) +```swift +// Envision/Screens/MainTabs/Rooms/RoomPlanScan/RoomPlanScannerViewController.swift +import RoomPlan + +class RoomPlanScannerViewController: UIViewController, RoomCaptureViewDelegate { + private var captureView: RoomCaptureView! + + override func viewDidLoad() { + super.viewDidLoad() + captureView = RoomCaptureView(frame: view.bounds) + captureView.captureSession.run(configuration: .init()) + captureView.delegate = self + view.addSubview(captureView) + } + + func captureView(_ view: RoomCaptureView, didEndWith data: CapturedRoom) { + // Convert to USDZ + let url = exportToUSDZ(data) + // Save and show preview + } +} +``` + +### A4: ObjectScanViewController (Photogrammetry) +```swift +// Envision/Screens/MainTabs/furniture/Object Capture/ObjectScanViewController.swift +class ObjectScanViewController: UIViewController { + private let session = AVCaptureSession() + private let photoOutput = AVCapturePhotoOutput() + private var images: [URL] = [] + + private func startAutoCapture() { + captureTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { _ in + self.takePhoto() + } + } + + @objc private func stopCapture() { + captureTimer?.invalidate() + // Process images with PhotogrammetrySession + let preview = ObjectCapturePreviewController(imagesFolder: tempFolderURL) + navigationController?.pushViewController(preview, animated: true) + } +} +``` + +--- + +**Document Version**: 1.0 +**Last Updated**: January 21, 2026 +**Author**: GitHub Copilot +**Project**: EnVision iOS App + +--- + +*End of Technical Documentation* diff --git a/Envision.xcodeproj/project.xcworkspace/xcuserdata/user86.xcuserdatad/UserInterfaceState.xcuserstate b/Envision.xcodeproj/project.xcworkspace/xcuserdata/user86.xcuserdatad/UserInterfaceState.xcuserstate index ca2916e0956b77ca7e435984704ae99c7e25fdf8..9a0919a4752aaaf265cdc4dba9470a77d25875ff 100644 GIT binary patch literal 255765 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 literal 227169 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