docs: add constitution, spec, plan, and data model for InteriorScan MVP
- Constitution: self-hosted first, FPS-grade immersion, mobile-first capture, API-first, open formats - Spec: 6 user stories (upload & process, share & embed, dashboard, API, measurements, billing) - Plan: 6 phases, 50 tasks, Phase 1-3 = MVP core - Data model: Drizzle schema with projects, photos, walkthroughs, processing_jobs, subscriptions
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
# Feature Specification: InteriorScan MVP
|
||||
**Feature Branch**: `feat/001-interiorscan-mvp`
|
||||
**Status**: Draft — Awaiting Approval
|
||||
|
||||
## Vision Statement
|
||||
|
||||
**InteriorScan turns apartment photos into a navigable 3D space** — like a Doom map of a real home. Real estate agents photograph a property, upload to InteriorScan, and share a link where potential buyers walk through the space in first-person. This filters out properties that don't fit, saving both agents and buyers from unnecessary physical visits.
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 — Upload & Process (Priority: P1) 🎯 MVP Core
|
||||
|
||||
As a real estate agent, I upload 30-100 photos of a property and InteriorScan reconstructs the interior as a 3D walkthrough, so I can share a link with potential buyers who navigate it like a first-person game.
|
||||
|
||||
**Why this priority**: Without reconstruction, nothing else matters. This is the core value proposition.
|
||||
|
||||
**Independent Test**: Upload photos → receive a shareable walkthrough URL. A visitor opens URL → navigates space in first-person.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** I am logged in, **When** I create a new project, upload 40+ JPEG photos of an interior, and submit for processing, **Then** the system accepts the upload, shows a processing status (queued → processing → complete), and within 60 minutes produces a 3D walkthrough.
|
||||
2. **Given** a walkthrough has completed processing, **When** I open the shareable link in a desktop browser, **Then** I see the reconstructed 3D space with WASD + mouse controls, collision detection, and texture-mapped walls/floors/ceilings.
|
||||
3. **Given** a walkthrough has completed processing, **When** I open the shareable link on a mobile browser, **Then** I see the reconstructed 3D space with tap-to-walk navigation and gyroscope look-around option.
|
||||
|
||||
### User Story 2 — Share & Embed (Priority: P1) 🎯 MVP Distribution
|
||||
|
||||
As a real estate agent, I can share a walkthrough link or embed the viewer on any listing page, so potential buyers encounter the 3D tour wherever they find the property.
|
||||
|
||||
**Why this priority**: The walkthrough is useless without distribution. Sharing and embedding are how real estate works.
|
||||
|
||||
**Independent Test**: Agent shares link → opens in new tab. Agent copies embed code → pastes on WordPress/listing → viewer renders.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** a completed walkthrough, **When** I click "Share", **Then** I get a copyable link and an oEmbed-embeddable iframe snippet.
|
||||
2. **Given** an embed snippet, **When** I paste it into a website, **Then** the InteriorScan viewer renders inline with all navigation controls functional.
|
||||
3. **Given** a shared link, **When** an unauthenticated visitor opens it, **Then** they can navigate the full walkthrough without signing up (public access).
|
||||
|
||||
### User Story 3 — Dashboard & Projects (Priority: P2)
|
||||
|
||||
As a real estate agent, I manage all my properties from a dashboard, so I can track processing status, edit metadata, and organize my portfolio.
|
||||
|
||||
**Why this priority**: Essential for retention but not for first-value. A single walkthrough proves the concept.
|
||||
|
||||
**Independent Test**: Agent creates 3 projects → sees them in dashboard → edits name/address → deletes one.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** I am logged in, **When** I visit the dashboard, **Then** I see a grid of my projects with thumbnail, name, address, processing status, and date.
|
||||
2. **Given** a project exists, **When** I edit its name or address, **Then** the changes persist and appear in the walkthrough metadata.
|
||||
3. **Given** I have 3 projects, **When** I delete one, **Then** it is removed from the dashboard and its data is scheduled for cleanup within 24h.
|
||||
|
||||
### User Story 4 — API Access (Priority: P2)
|
||||
|
||||
As a developer integrating InteriorScan, I can upload photos and retrieve walkthroughs via REST API, so I can embed reconstruction into my own platform.
|
||||
|
||||
**Why this priority**: API-first is a constitution principle, but the web UI validates the pipeline first. API follows proven flow.
|
||||
|
||||
**Independent Test**: `POST /api/v1/projects` with photos → `GET /api/v1/projects/:id` returns status → `GET /api/v1/projects/:id/walkthrough` returns viewer URL.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** I have an API key, **When** I POST photos to `/api/v1/projects`, **Then** I receive a project ID and a `Location` header for status polling.
|
||||
2. **Given** a project is processing, **When** I GET `/api/v1/projects/:id`, **Then** I receive `{status: "processing", progress: 45, estimatedCompletion: "..."}`.
|
||||
3. **Given** a project is complete, **When** I GET `/api/v1/projects/:id/walkthrough`, **Then** I receive the viewer URL and embed snippet.
|
||||
|
||||
### User Story 5 — Measurements & Floor Plans (Priority: P3)
|
||||
|
||||
As a buyer, I can see room dimensions overlaid on the walkthrough and download a floor plan, so I can evaluate if furniture fits without visiting.
|
||||
|
||||
**Why this priority**: Differentiates from pure 360° tours but requires accurate reconstruction. Comes after core pipeline is proven.
|
||||
|
||||
**Independent Test**: Open walkthrough → toggle measurements → see room dimensions in meters.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** a walkthrough with LiDAR data, **When** I toggle "Show Measurements", **Then** room dimensions appear as overlays on walls and floor (±5% accuracy).
|
||||
2. **Given** a completed walkthrough, **When** I click "Download Floor Plan", **Then** I receive a PDF/DXF floor plan with labeled rooms and dimensions.
|
||||
|
||||
### User Story 6 — Subscription & Billing (Priority: P3)
|
||||
|
||||
As an agent, I subscribe to a plan (Free/Pro/Enterprise) that determines how many walkthroughs I can create per month, so I only pay for what I use.
|
||||
|
||||
**Why this priority**: Revenue model, but not needed to validate the core pipeline.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
1. **Given** I am on the Free plan, **When** I attempt to create my 4th walkthrough, **Then** I see an upgrade prompt with Pro plan pricing.
|
||||
2. **Given** I upgrade to Pro, **When** payment is confirmed via Stripe, **Then** my walkthrough limit increases to 25/month and the badge updates.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **Fewer than 20 photos**: System rejects with guidance on minimum coverage
|
||||
- **Corrupt/unreadable photos**: System skips corrupt files, warns user, continues with valid ones
|
||||
- **Processing failure**: System retries once, then marks as "failed" with error details and refund option
|
||||
- **Very large spaces (>200 photos)**: System queues with estimated time, processes in chunks
|
||||
- **Mobile browser without WebGL**: Fallback to 360° photo carousel (progressive enhancement)
|
||||
- **Duplicate photos**: System deduplicates by perceptual hash before processing
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
- **FR-001**: System MUST accept 20-500 JPEG/PNG photos per project via web upload or API
|
||||
- **FR-002**: System MUST reconstruct 3D mesh from photos using COLMAP/OpenMVS pipeline
|
||||
- **FR-003**: System MUST provide first-person navigation (WASD + mouse) in desktop browsers
|
||||
- **FR-004**: System MUST provide tap-to-walk navigation in mobile browsers
|
||||
- **FR-005**: System MUST generate shareable public links for completed walkthroughs
|
||||
- **FR-006**: System MUST provide oEmbed-embeddable iframe code
|
||||
- **FR-007**: System MUST show processing status: queued → processing → complete (with progress %)
|
||||
- **FR-008**: System MUST handle processing failures gracefully with retry + notification
|
||||
- **FR-009**: System MUST export walkthrough data in open formats (OBJ minimum)
|
||||
- **FR-010**: System MUST work on iPhone, Android, desktop Chrome, Firefox, Safari
|
||||
|
||||
### Non-Functional Requirements
|
||||
- **NFR-001**: Walkthrough viewer MUST load initial mesh in <5 seconds on 4G
|
||||
- **NFR-002**: Navigation MUST run at ≥30 FPS on mid-range 2024 devices
|
||||
- **NFR-003**: Processing MUST complete within 60 minutes for 100 photos
|
||||
- **NFR-004**: System MUST handle 10 concurrent processing jobs
|
||||
- **NFR-005**: Walkthrough links MUST work without authentication (public)
|
||||
- **NFR-006**: API MUST follow OpenAPI 3.1 spec with Zod-validated schemas
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-001**: First tester uploads 40 photos → walks through reconstructed kitchen in <10 min total
|
||||
- **SC-002**: Unauthenticated visitor opens shareable link → navigates fluently at 30+ FPS
|
||||
- **SC-003**: Processing pipeline produces visual quality comparable to Matterport's basic tier
|
||||
- **SC-004**: 3 of monyi's tester friends complete a walkthrough creation within 1 hour of onboarding
|
||||
@@ -0,0 +1,207 @@
|
||||
# InteriorScan — Data Model
|
||||
|
||||
## Entity Relationship
|
||||
|
||||
```
|
||||
users ──< team_members >── teams ──< projects ──< photos
|
||||
│ │
|
||||
│ └── walkthroughs
|
||||
│
|
||||
└── subscriptions
|
||||
|
||||
projects ──< processing_jobs
|
||||
```
|
||||
|
||||
## Drizzle Schema Definition
|
||||
|
||||
```typescript
|
||||
// packages/db/src/schema.ts
|
||||
|
||||
import { pgTable, text, timestamp, integer, jsonb, boolean, pgEnum } from 'drizzle-orm/pg-core';
|
||||
import { cuid2 } from './custom-types'; // branded cuid2 ID type
|
||||
|
||||
// ── Enums ──────────────────────────────────────────
|
||||
|
||||
export const projectStatusEnum = pgEnum('project_status', [
|
||||
'draft', // created, no photos yet
|
||||
'uploading', // photos being uploaded
|
||||
'queued', // queued for processing
|
||||
'processing', // COLMAP/OpenMVS pipeline running
|
||||
'complete', // walkthrough ready
|
||||
'failed', // processing error
|
||||
]);
|
||||
|
||||
export const processingStageEnum = pgEnum('processing_stage', [
|
||||
'feature_extraction', // SIFT/ORB features
|
||||
'feature_matching', // Correspondence search
|
||||
'sfm', // Structure from Motion → sparse cloud
|
||||
'mvs', // Multi-View Stereo → dense cloud
|
||||
'meshing', // Mesh generation
|
||||
'texturing', // UV mapping + texturing
|
||||
'tiling', // LOD + Draco compression
|
||||
'complete',
|
||||
]);
|
||||
|
||||
export const subscriptionPlanEnum = pgEnum('subscription_plan', [
|
||||
'free',
|
||||
'pro',
|
||||
'enterprise',
|
||||
]);
|
||||
|
||||
export const teamRoleEnum = pgEnum('team_role', [
|
||||
'owner',
|
||||
'admin',
|
||||
'member',
|
||||
'viewer',
|
||||
]);
|
||||
|
||||
// ── Users (better-auth managed) ─────────────────────
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
email: text('email').notNull().unique(),
|
||||
name: text('name').notNull(),
|
||||
emailVerified: boolean('email_verified').default(false),
|
||||
image: text('image'), // avatar URL
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Teams & Membership ──────────────────────────────
|
||||
|
||||
export const teams = pgTable('teams', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
slug: text('slug').notNull().unique(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const teamMembers = pgTable('team_members', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||
userId: cuid2('user_id').references(() => users.id).notNull(),
|
||||
role: teamRoleEnum('role').default('member').notNull(),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Projects ────────────────────────────────────────
|
||||
|
||||
export const projects = pgTable('projects', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||
name: text('name').notNull(),
|
||||
address: text('address'), // property address
|
||||
description: text('description'),
|
||||
status: projectStatusEnum('status').default('draft').notNull(),
|
||||
photoCount: integer('photo_count').default(0),
|
||||
captureDevice: text('capture_device'), // 'iphone-lidar', 'android', 'theta-z1', 'unknown'
|
||||
processingProgress: integer('processing_progress').default(0), // 0-100
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Photos ──────────────────────────────────────────
|
||||
|
||||
export const photos = pgTable('photos', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
projectId: cuid2('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
|
||||
originalKey: text('original_key').notNull(), // S3/storage key for original
|
||||
thumbnailKey: text('thumbnail_key'), // processed thumbnail key
|
||||
perceptualHash: text('perceptual_hash'), // for deduplication
|
||||
width: integer('width'),
|
||||
height: integer('height'),
|
||||
fileSizeBytes: integer('file_size_bytes'),
|
||||
exifData: jsonb('exif_data'), // GPS, camera model, focal length
|
||||
isDuplicate: boolean('is_duplicate').default(false),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Walkthroughs ────────────────────────────────────
|
||||
|
||||
export const walkthroughs = pgTable('walkthroughs', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
projectId: cuid2('project_id').references(() => projects.id).notNull(),
|
||||
meshKey: text('mesh_key').notNull(), // tiled mesh storage key
|
||||
textureKey: text('texture_key').notNull(), // texture atlas storage key
|
||||
navmeshKey: text('navmesh_key'), // navmesh for click-to-walk
|
||||
pointCloudKey: text('point_cloud_key'), // E57/LAS point cloud (optional)
|
||||
thumbnailKey: text('thumbnail_key'), // preview image
|
||||
bounds: jsonb('bounds'), // { min: [x,y,z], max: [x,y,z] }
|
||||
stats: jsonb('stats'), // { vertices: N, faces: N, rooms: N }
|
||||
isPublic: boolean('is_public').default(true),
|
||||
slug: text('slug').notNull().unique(), // for shareable URLs
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Processing Jobs ─────────────────────────────────
|
||||
|
||||
export const processingJobs = pgTable('processing_jobs', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
projectId: cuid2('project_id').references(() => projects.id).notNull(),
|
||||
stage: processingStageEnum('stage').default('feature_extraction').notNull(),
|
||||
progress: integer('progress').default(0), // 0-100 per stage
|
||||
error: text('error'), // error message if failed
|
||||
startedAt: timestamp('started_at'),
|
||||
completedAt: timestamp('completed_at'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Subscriptions ───────────────────────────────────
|
||||
|
||||
export const subscriptions = pgTable('subscriptions', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||
plan: subscriptionPlanEnum('plan').default('free').notNull(),
|
||||
stripeCustomerId: text('stripe_customer_id'),
|
||||
stripePriceId: text('stripe_price_id'),
|
||||
stripeSubscriptionId: text('stripe_subscription_id'),
|
||||
monthlyQuota: integer('monthly_quota').default(3), // Free=3, Pro=25, Enterprise=unlimited
|
||||
monthlyUsage: integer('monthly_usage').default(0),
|
||||
currentPeriodStart: timestamp('current_period_start'),
|
||||
currentPeriodEnd: timestamp('current_period_end'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// ── Sessions (better-auth managed) ──────────────────
|
||||
|
||||
export const sessions = pgTable('sessions', {
|
||||
id: cuid2('id').primaryKey(),
|
||||
userId: cuid2('user_id').references(() => users.id).notNull(),
|
||||
token: text('token').notNull().unique(),
|
||||
expiresAt: timestamp('expires_at').notNull(),
|
||||
ipAddress: text('ip_address'),
|
||||
userAgent: text('user_agent'),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
});
|
||||
```
|
||||
|
||||
## Indexes
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_projects_team_id ON projects(team_id);
|
||||
CREATE INDEX idx_projects_status ON projects(status);
|
||||
CREATE INDEX idx_photos_project_id ON photos(project_id);
|
||||
CREATE INDEX idx_photos_perceptual_hash ON photos(perceptual_hash);
|
||||
CREATE INDEX idx_walkthroughs_project_id ON walkthroughs(project_id);
|
||||
CREATE INDEX idx_walkthroughs_slug ON walkthroughs(slug);
|
||||
CREATE INDEX idx_processing_jobs_project_id ON processing_jobs(project_id);
|
||||
CREATE INDEX idx_team_members_user_id ON team_members(user_id);
|
||||
CREATE INDEX idx_team_members_team_id ON team_members(team_id);
|
||||
CREATE INDEX idx_subscriptions_team_id ON subscriptions(team_id);
|
||||
```
|
||||
|
||||
## Constraints
|
||||
|
||||
| Rule | Level | Implementation |
|
||||
|------|-------|---------------|
|
||||
| User email unique | DB | `users.email UNIQUE` |
|
||||
| Team slug unique | DB | `teams.slug UNIQUE` |
|
||||
| Walkthrough slug unique | DB | `walkthroughs.slug UNIQUE` |
|
||||
| Project belongs to team | DB | `projects.team_id FK → teams.id` |
|
||||
| Photo belongs to project | DB | `photos.project_id FK → projects.id CASCADE` |
|
||||
| Max 500 photos per project | App | Validate in Zod schema + upload endpoint |
|
||||
| Monthly quota enforcement | App | Middleware checks `subscriptions.monthlyUsage < monthlyQuota` |
|
||||
| Photo deduplication | App | Perceptual hash comparison before insert |
|
||||
@@ -0,0 +1,131 @@
|
||||
# InteriorScan MVP — Implementation Plan
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Build an interior photogrammetry SaaS that turns apartment photos into navigable 3D walkthroughs (Doom-map style).
|
||||
|
||||
**Architecture:** Turborepo monorepo (tpl-next-saas template). Next.js 16 App Router for web UI + dashboard. GPU worker (Node.js) orchestrates COLMAP/OpenMVS processing pipeline. three.js FPS viewer for walkthrough navigation. PostgreSQL + Drizzle for data. Valkey for queues/sessions. Stripe for billing.
|
||||
|
||||
**Tech Stack:** Next.js 16 · Drizzle ORM · PostgreSQL · Valkey · three.js · COLMAP · OpenMVS · Stripe · Biome · Vitest
|
||||
|
||||
---
|
||||
|
||||
## Constitution Check
|
||||
|
||||
| Principle | Status | Notes |
|
||||
|---|---|---|
|
||||
| I. Self-Hosted First | ✅ PASS | All infra self-hosted except Stripe |
|
||||
| II. Progressive Experience | ✅ PASS | WebGL → 360° fallback, mobile tap-to-walk |
|
||||
| III. FPS-Grade Immersion | ✅ PASS | WASD + mouse primary, tap-to-walk secondary |
|
||||
| IV. Photo-First Capture | ✅ PASS | Mobile photos as baseline, LiDAR enhancement |
|
||||
| V. API-First Architecture | ✅ PASS | API endpoints defined before UI |
|
||||
| VI. TypeScript Strict | ✅ PASS | strict + noUncheckedIndexedAccess |
|
||||
| VII. Test-First | ⚠ CONDITIONAL | TDD for business logic; 3D viewer tested via Playwright E2E |
|
||||
| VIII. Open Formats | ✅ PASS | OBJ export in MVP, E57/DXF/IFC in P4+ |
|
||||
|
||||
⚠ VII conditional: 3D rendering correctness is validated via E2E visual regression, not unit tests. Business logic (upload, processing, auth) follows strict TDD.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
See `data-model.md` (separate document — same directory).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Monorepo Scaffold + Core Infra (Shared)
|
||||
|
||||
- [ ] T001 Create monorepo from tpl-next-saas template
|
||||
- [ ] T002 Configure Drizzle schema with InteriorScan tables (projects, photos, walkthroughs, processing_jobs, users, teams, subscriptions)
|
||||
- [ ] T003 Run migration: `pnpm db:migrate`
|
||||
- [ ] T004 [P] Add seed data: 1 user, 1 team, 3 project statuses
|
||||
- [ ] T005 [P] Set up Valkey connection for job queue
|
||||
- [ ] T006 [P] Configure Docker Compose (postgres + valkey + web)
|
||||
|
||||
**Checkpoint**: Migration green, `pnpm dev` starts without errors, health endpoint returns 200.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Photo Upload & Processing Pipeline (P1) 🎯 MVP Core
|
||||
|
||||
### 2A: Upload API
|
||||
|
||||
- [ ] T007 Write failing test: POST /api/v1/projects with photos returns 201 + project ID
|
||||
- [ ] T008 Implement project creation endpoint with Zod validation
|
||||
- [ ] T009 Write failing test: Upload 3 JPEGs → stored in object storage, DB has photo records
|
||||
- [ ] T010 Implement photo upload with deduplication (perceptual hash)
|
||||
- [ ] T011 Write failing test: GET /api/v1/projects/:id returns status + progress
|
||||
- [ ] T012 Implement project status endpoint
|
||||
|
||||
**Checkpoint**: Upload 40 photos via API → receive project ID → poll status → see "queued".
|
||||
|
||||
### 2B: Processing Worker
|
||||
|
||||
- [ ] T013 Set up `apps/processor/` — Node.js worker with BullMQ/Valkey job queue
|
||||
- [ ] T014 Write failing test: Job queued → worker picks up → runs COLMAP SfM → produces sparse point cloud
|
||||
- [ ] T015 Implement COLMAP SfM step (feature extraction + matching + sparse reconstruction)
|
||||
- [ ] T016 Write failing test: SfM output → OpenMVS dense reconstruction → mesh file produced
|
||||
- [ ] T017 Implement OpenMVS dense reconstruction step (MVS + meshing + texturing)
|
||||
- [ ] T018 Write failing test: Mesh + textures → tiled output for three.js streaming
|
||||
- [ ] T019 Implement mesh processing + tiling step (LOD generation, Draco compression, texture atlas)
|
||||
- [ ] T020 Implement progress reporting (Valkey pub/sub → web UI)
|
||||
- [ ] T021 Write failing test: Processing failure → job marked failed → user notified
|
||||
- [ ] T022 Implement error handling + retry logic
|
||||
|
||||
**Checkpoint**: Upload 40 photos → processing completes → produces OBJ mesh + tiled three.js output. Total time <60 min for 100 photos on GPU.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 3D Viewer — FPS Walkthrough (P1) 🎯 MVP Core
|
||||
|
||||
- [ ] T023 Create `packages/viewer/` — three.js + Vite library package
|
||||
- [ ] T024 Implement FPS camera controller (WASD + mouse look, collision detection, gravity)
|
||||
- [ ] T025 Implement tap-to-walk controller (mobile: tap destination → navmesh pathfinding)
|
||||
- [ ] T026 Implement mesh loader (tiled LODeD mesh streaming, Draco decompression)
|
||||
- [ ] T027 Implement texture loading (progressive: low-res first, high-res on demand)
|
||||
- [ ] T028 Implement mini-map overlay (bird's eye view with player position)
|
||||
- [ ] T029 Implement measurement overlay toggle (room dimensions on walls/floor)
|
||||
- [ ] T030 Implement WebGL fallback detection → 360° photo carousel mode
|
||||
- [ ] T031 Implement touch controls (gyroscope look, pinch-zoom, two-finger walk)
|
||||
- [ ] T032 Write Playwright E2E test: load walkthrough → verify 3D renders → verify WASD movement → verify tap-to-walk
|
||||
|
||||
**Checkpoint**: Open walkthrough link → walk through reconstructed space at 30+ FPS. WASD works. Click-to-walk works. Mini-map shows position.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Web UI — Dashboard & Sharing (P1 + P2)
|
||||
|
||||
- [ ] T033 Create project creation flow (multi-step: name/address → photo upload → processing)
|
||||
- [ ] T034 Create processing status page (progress bar, ETA, stage indicator)
|
||||
- [ ] T035 Create walkthrough view page (embedded viewer + share controls)
|
||||
- [ ] T036 Implement share link generation (public, no auth required)
|
||||
- [ ] T037 Implement oEmbed embed code generator
|
||||
- [ ] T038 Create dashboard grid (project cards with thumbnail, status, date)
|
||||
- [ ] T039 Create project settings (edit name, address, delete)
|
||||
|
||||
**Checkpoint**: Agent creates project → uploads photos → sees processing progress → walkthrough appears → shares link → visitor navigates without login.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Auth, Teams & Billing (P3)
|
||||
|
||||
- [ ] T040 Set up better-auth with email + Google OAuth
|
||||
- [ ] T041 Implement RBAC (owner/admin/member/viewer per team)
|
||||
- [ ] T042 Create subscription tiers (Free: 3/month, Pro: 25/month, Enterprise: unlimited)
|
||||
- [ ] T043 Implement Stripe checkout + webhook
|
||||
- [ ] T044 Implement usage tracking + quota enforcement middleware
|
||||
|
||||
**Checkpoint**: User signs up → creates 3 projects on Free → gets upgrade prompt → pays via Stripe → quota increases.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Launch
|
||||
|
||||
- [ ] T045 Landing page with demo walkthrough embedded
|
||||
- [ ] T046 Pricing page with 3 tiers
|
||||
- [ ] T047 Email notifications (processing complete, processing failed, quota warning)
|
||||
- [ ] T048 Mobile-responsive dashboard with bottom nav
|
||||
- [ ] T049 Accessibility audit (WCAG 2.2 AA): keyboard nav, screen reader, contrast
|
||||
- [ ] T050 Performance audit (Core Web Vitals, first mesh load <5s on 4G)
|
||||
|
||||
**Checkpoint**: Real tester (monyi's friends) create walkthrough from phone photos in <1h total. Lighthouse 90+.
|
||||
Reference in New Issue
Block a user