<TourBuilder> — visual tour.json authoring tool
Upload a floorplan image, click it to place rooms, upload a pool of clips/stills, wire up doorways between rooms, and export a real, validated
Tourmanifest — the exact shape<VideoTour>consumes. Plug-and-play, importable into any app, publicly showcased at/components/tour-builder.
Why this exists
Every Tour manifest in this repo so far (public/showcase-samples/demo-house/tour.json)
was hand-written JSON. The operator asked for "an admin tool for building the video
walkthroughs (upload a group of videos and a way to put in a floorplan and move between
them or room to room 360s etc)" — this is that tool: a real room-graph editor instead of
hand-authoring the manifest.
Scope boundary (read before extending this component)
Same discipline as <FileUpload>/<FileList>/<ProcessingStatus> — see CLAUDE.md's
"Scope boundary" section. This is an in-browser editor, not a real asset pipeline:
- Uploaded files (floorplan + media pool) are used for live preview only, via
session-scoped
URL.createObjectURLblob URLs. Closing the tab loses the in-progress edit — there is no persistence, no auto-save, no backend of any kind. - The exported manifest references each file by its original filename
(
"kitchen-spin.mp4"), which is a placeholder the author replaces with wherever they actually host that file once it's uploaded somewhere real. This tool cannot know or guess that eventual URL — same reasoning as<FileList>'s "already-resolved URLs" contract, just from the authoring side instead of the display side. - No fetch, no storage SDK, no auth, no real upload path. Real upload/hosting/
persistence (e.g. a real "save this tour to R2 + Supabase" admin flow) belongs to the
separate
personal-droneplatform, never here.
The editing model
- Floorplan. One image, uploaded via
<FileUpload multiple={false} accept="image/*">. Clicking anywhere on the rendered image computes a[x%, y%]position (from the click's offset within the image'sgetBoundingClientRect()) and creates a newTourRoomthere — the sameposfield<FloorPlanMap>already renders inside<VideoTour>itself, so a tour authored here drops straight into the existing minimap rendering with no conversion. - Media pool. A second
<FileUpload accept="video/*,image/*">target collects every video/image the author has ready. Every uploaded filename populates a shared<datalist>, offered as autocomplete on every still/spin/doorway-clip field — so an author can either pick an already-uploaded file's name or just type a filename they'll upload later, without the tool forcing a rigid dropdown-only choice. - Rooms. Each placed room gets a card: label, still (required — matches
TourRoom.still's "Always provide one" contract), spin (optional), and a doorway editor. - Doorways. Add a directed edge to any other existing room, optionally with a
transition-clip filename (blank → the existing wipe fallback, exactly like
TourEdge.clip: null). Deleting a room also strips any other room's doorway that pointed at it — an author can't be left with a manifest containing a dangling edge just because they deleted the wrong room card. - Export. Real-time JSON preview (
lib/tour-types.ts's exactTourshape), a<CopyButton>, and a real file download (Blob+ a synthetic<a download>click — the same "real, working download" bar<FileList>and<MinecraftExport>already hold). The download button is disabled while real validation issues remain unresolved.
Validation — tour-builder-utils.ts's validateTour()
Pure function, structural (not just "is this field non-empty"):
- Slug/title required.
- At least one room.
startRoommust reference a real room id in the manifest.- Every room needs a label and a still.
- Every doorway's
tomust reference a real room id — this is the check that actually matters at runtime: a dangling edge here wouldn't fail to export, it would fail inside<VideoTour>the first time a visitor navigated through it (an edge to nowhere).
Validated live on every edit; issues render as a list next to the export controls, and block the download button (but never the copy button or the live JSON preview — an author can still inspect/copy a work-in-progress manifest).
What this deliberately does NOT do (yet)
- No drag-to-reposition — rooms are placed by click, not draggable afterward. A reasonable v2 addition, not built here.
- No automatic still-frame capture from an uploaded spin/clip video — the author
supplies a still filename directly; extracting a real thumbnail frame client-side is real,
separate work (a
<video>+<canvas>capture, or ideally reusing the sameffmpegframe-extraction step/pipeline's docs already describe for the nadir-grid pipeline). - No real save/load beyond the JSON export/
initialTourprop — no draft persistence across page reloads (would needlocalStorageat minimum, real storage for anything more), out of scope for a stateless framework component.
Acceptance criteria
- Given a blank builder, when rendered, then real validation issues are shown and the download button is disabled.
- Given an uploaded floorplan, when clicked, then a new room is placed at the correct
[x%, y%]and set as the tour's start room if none was set yet. - Given two valid rooms, when a doorway is added between them, then the exported JSON
contains a real
TourEdgewith the entered clip filename (ornullfor a wipe). - Given a room with an inbound doorway from another room, when that room is deleted, then the other room's doorway to it is also removed (no dangling edge left behind).
- Given uploaded media files, when added, then their filenames populate the shared datalist offered on every still/spin/clip field.
- Given
npm testandnpm run build, when run after this story, then both pass cleanly.