Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .cursor/rules/ls-foundry-core.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ packages/ # publishable + internal packages

| Scope | Use | Examples |
|-------|-----|----------|
| `@jeffgo10/*` | **Published** to GitHub Packages (`npm.pkg.github.com`) | `shared-types`, `react-canvas-designer`, `canvas-upscaler`, `three-d-label-customizer`, `helpers`, `gl-viewer` (v1.0.0; consumed by **LiteShadeMedia**), `panorama-viewer` (v0.1.2; consumed by **Vantage**) |
| `@jeffgo10/*` | **Published** to GitHub Packages (`npm.pkg.github.com`) | `shared-types`, `react-canvas-designer`, `canvas-upscaler`, `three-d-label-customizer`, `helpers`, `gl-viewer` (v1.0.0; consumed by **LiteShadeMedia**), `panorama-viewer` (v0.2.0; consumed by **Vantage**) |
| `@ls-foundry/*` | **Private** monorepo-only | `docs`, `tsconfig`, `ui`, `utils`, `maps` |

Publish scope must match GitHub owner (`@jeffgo10`). Bump versions in `packages/*/package.json` before merge so CI can publish.
Expand Down Expand Up @@ -58,7 +58,7 @@ Always bump `shared-types` first when changing layout/DPI APIs; keep dependent p
| `@jeffgo10/history` | 1.0.0 | `packages/history/` — generic undo/redo snapshot stacks |
| `@jeffgo10/three-d-label-customizer` | 0.1.0 | `docs/three-d-label-customizer/` |
| `@jeffgo10/gl-viewer` | 1.0.0 | `packages/gl-viewer/` (consumer: **LiteShadeMedia**) |
| `@jeffgo10/panorama-viewer` | 0.1.2 | `docs/panorama-viewer/` (consumer: **Vantage**) |
| `@jeffgo10/panorama-viewer` | 0.2.0 | `docs/panorama-viewer/` (consumer: **Vantage**) |

`three-d-label-customizer` is a **standalone** product mockup package. Obsidian: `LS Foundry/Notes — three-d-label-customizer`.

Expand Down
4 changes: 2 additions & 2 deletions apps/docs/src/app/panorama/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ function PanoramaPage() {
<p className="mt-3 max-w-xl text-sm leading-relaxed text-white/50">
Demo for{" "}
<code className="text-white/70">@jeffgo10/panorama-viewer</code>.
Upload an equirectangular 360 image, place navigation / info / label
hotspots in edit mode, and inspect package-owned content UI.
Upload an equirectangular 360 image, place or drag navigation / info /
label hotspots in edit mode, and inspect package-owned content UI.
</p>
<div className="mt-4 flex flex-wrap gap-4 text-xs tracking-[0.15em] text-white/40">
<Link
Expand Down
19 changes: 18 additions & 1 deletion apps/docs/src/components/PanoramaViewerSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,8 @@ function PanoramaViewerSection() {
/>
</label>
<p className="sm:col-span-3 text-xs text-white/40">
Click the sphere to place a hotspot of the selected kind.
Click the sphere to place a hotspot. Drag an existing hotspot to
reposition it on the 360 sphere.
</p>
</div>
) : null}
Expand All @@ -214,11 +215,27 @@ function PanoramaViewerSection() {
markers={markers}
mode={mode}
fitParent
orientationControl={mode === "view"}
className="h-full min-h-0 w-full"
onSphereClick={handleSphereClick}
onMarkerMove={(marker, pos) => {
setMarkers((prev) =>
prev.map((m) =>
m.id === marker.id
? { ...m, yaw: pos.yaw, pitch: pos.pitch }
: m,
),
);
setLastEvent(
`Moved ${marker.kind} to yaw ${pos.yaw.toFixed(1)}, pitch ${pos.pitch.toFixed(1)}`,
);
}}
onMarkerClick={(m) => {
setLastEvent(`Clicked ${m.kind} “${m.title ?? m.id}”`);
}}
onOrientationChange={(active) => {
setLastEvent(active ? "Gyro on" : "Gyro off");
}}
/>
</div>

Expand Down
7 changes: 3 additions & 4 deletions docs/panorama-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Platform-agnostic — no Supabase, R2, Next routes, or billing. Primary consumer
| Item | Value |
|------|-------|
| npm | `@jeffgo10/panorama-viewer` |
| Version | `0.1.2` |
| Version | `0.2.0` |
| Source | `packages/panorama-viewer/` |

## Browser demo
Expand All @@ -38,8 +38,7 @@ See [packages/panorama-viewer/README.md](../../packages/panorama-viewer/README.m

Issue/fix log: [engineering-notes.md](./engineering-notes.md)

## Follow-ups (not in 0.1.0)
## Follow-ups (not in 0.2.0)

- Drag-to-reposition markers in edit mode
- Gyroscope / VR
- Multi-scene tour graph (stays in the consumer app)
- Host “Enable motion” chrome (RoamVantage / Vantage) — package only exposes control + events
16 changes: 13 additions & 3 deletions docs/panorama-viewer/engineering-notes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Panorama viewer — engineering notes

Running log for `@jeffgo10/panorama-viewer` (**v0.1.2**; first publish v0.1.0).
Running log for `@jeffgo10/panorama-viewer` (**v0.2.0**; first publish v0.1.0).

Standalone ls-foundry package — not StickPak, not LiteShadeMedia / gl-viewer. Demo: `pnpm run dev --filter=@ls-foundry/docs` → `/panorama`. Obsidian: `LS Foundry/Notes — panorama-viewer`.

Expand All @@ -9,23 +9,33 @@ Standalone ls-foundry package — not StickPak, not LiteShadeMedia / gl-viewer.
| Item | Value |
|------|-------|
| npm | `@jeffgo10/panorama-viewer` |
| Version | `0.1.2` |
| Version | `0.2.0` |
| Source | `packages/panorama-viewer/` |
| Engine | Pannellum `^2.5.7` (npm assets, not CDN) |

## Architecture (v0.1.0)

1. **`PanoramaViewer`** — client-only React component; loads `pannellum/build/pannellum.js`, destroys on unmount, remounts when `imageUrl` or marker geometry changes.
1. **`PanoramaViewer`** — client-only React component; loads `pannellum/build/pannellum.js`, destroys on unmount, remounts when `imageUrl` or marker **structure** changes.
2. **`markersToHotSpots`** — pure mapper from `PanoramaMarker[]` to Pannellum `hotSpots` with `createTooltipFunc` for package-owned content DOM.
3. **Styles** — `dist/styles.css` bundles Pannellum CSS + pin/content classes; consumers import `@jeffgo10/panorama-viewer/styles.css`.
4. **Edit mode** — sphere click → `mouseEventToCoords` → `onSphereClick({ yaw, pitch })`. Drag-pan is ignored via pointer movement threshold.

## Architecture (v0.2.0)

5. **Drag-to-reposition** — in `mode="edit"` with `onMarkerMove`, pointer-down on `[data-marker-id]` starts a drag; move remaps via `mouseEventToCoords` + `removeHotSpot`/`addHotSpot`; pointer-up fires `onMarkerMove(marker, { yaw, pitch })`.
6. **Structural vs position keys** — remount only on `markersStructuralKey` (id/kind/title/pin/content/data). Yaw/pitch sync via `markersPositionsKey` without remounting the panorama.
7. **View preserve** — camera yaw/pitch/hfov saved across structural remounts when `imageUrl` is unchanged.
8. **Device orientation** — `orientationControl` (view mode only) exposes Pannellum gyro / tilt-to-look. Props: `orientationOnByDefault`, `onOrientationChange`. Handle: `startOrientation` / `stopOrientation` / `isOrientationAvailable` / `isOrientationActive`. Built-in orientation button hidden unless `.ls-pv-orientation` is on the shell. iOS needs a user-gesture `startOrientation()` for permission.

## Issues and fixes

| Symptom | Cause | Fix |
|---------|-------|-----|
| Edit mode places marker while drag-panning | `click` fires after drag | Track pointer movement; skip `onSphereClick` when movement exceeds threshold |
| Viewer flashes then collapses to 0 height | Pannellum `.pnlm-container { height: 100% }` with no sized parent | Outer shell with default `min(70vh, 520px)`; inner canvas absolute fill; docs use fixed-height wrapper + `fitParent` |
| Dragging a marker also pans / opens info | Hotspot click + sphere pan share the gesture | Edit drag uses pointer capture; suppress click after move; grab cursor in `.ls-pv-shell-edit` |
| Remount on every yaw tweak resets camera | Full geometry key remounted viewer | Split structural vs position keys; preserve view on remount |
| Gyro unavailable / no API on wrapper | Props/handle only covered markers | Expose `orientationControl` + imperative start/stop; document iOS permission |
| (initial) | Vantage CDN scaffold | Publish reusable package with bundled pannellum assets |

## HTML content trust boundary
Expand Down
25 changes: 25 additions & 0 deletions packages/panorama-viewer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Changelog

## 0.2.0

### Added

- **Device orientation (gyro / tilt-to-look)** for `mode="view"`:
- Props: `orientationControl`, `orientationOnByDefault`, `onOrientationChange`
- Handle: `startOrientation()`, `stopOrientation()`, `isOrientationAvailable()`, `isOrientationActive()`
- Helpers: `deviceOrientationPermissionRequired()`, `isOrientationControlEnabled()`
- **Edit-mode marker drag** via `onMarkerMove` (yaw/pitch sync without remount)

### Notes for consumers (RoamVantage / Vantage)

1. Bump to `@jeffgo10/panorama-viewer@0.2.0` (e.g. `/update-ls-foundry-packages` in roamvantage).
2. On public tour viewers (`/t/[slug]`, `/t/demo`), set `orientationControl` (and optionally wire an “Enable motion” button that calls `ref.startOrientation()` on iOS).
3. Keep `orientationControl` off in edit / designer mode.

## 0.1.2

Prior published pin used by RoamVantage Phase 1.

## 0.1.0

Initial package publish (Pannellum wrapper + markers).
58 changes: 56 additions & 2 deletions packages/panorama-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,13 @@ const markers: PanoramaMarker[] = [
| `fitParent` | `boolean?` | Skip default height; parent must size the shell (`h-full`) |
| `initialYaw` / `initialPitch` / `initialHfov` | `number?` | Initial camera (degrees) |
| `markers` | `PanoramaMarker[]?` | Hotspots |
| `mode` | `"view" \| "edit"` | Edit enables sphere click |
| `mode` | `"view" \| "edit"` | Edit enables sphere click + marker drag |
| `orientationControl` | `boolean?` | Enable Pannellum gyro / tilt-to-look (`mode="view"` only). Default `false` |
| `orientationOnByDefault` | `boolean?` | Start with gyro on when supported. Default `false`. Prefer a user-gesture `startOrientation()` on iOS |
| `onOrientationChange` | `(active: boolean) => void` | Gyro became active / inactive |
| `onSphereClick` | `(pos) => void` | Edit: yaw/pitch of click |
| `onMarkerClick` | `(marker) => void` | Marker click |
| `onMarkerMove` | `(marker, pos) => void` | Edit: drag finished — update `markers` |

### Marker kinds

Expand All @@ -125,9 +129,51 @@ type PanoramaViewerHandle = {
getView: () => { yaw: number; pitch: number; hfov: number } | null;
lookAt: (yaw: number, pitch: number, hfov?: number) => void;
destroy: () => void;
startOrientation: () => void;
stopOrientation: () => void;
isOrientationAvailable: () => boolean;
isOrientationActive: () => boolean;
};
```

## Device orientation (gyro / tilt-to-look)

Opt-in for **view mode** only (ignored in edit). Drag / pinch still work alongside gyro (Pannellum’s model).

```tsx
const ref = useRef<PanoramaViewerHandle>(null);

<PanoramaViewer
ref={ref}
imageUrl={url}
mode="view"
orientationControl
// Prefer false on iOS — call startOrientation() from a button tap instead
orientationOnByDefault={false}
onOrientationChange={(active) => setGyroOn(active)}
/>

<button
type="button"
onClick={() => ref.current?.startOrientation()}
disabled={!ref.current?.isOrientationAvailable()}
>
Enable motion
</button>
```

### iOS Safari

- Requires **HTTPS**.
- iOS 13+ prompts via `DeviceOrientationEvent.requestPermission()` — must run from a **user gesture**. Pannellum’s `startOrientation()` (and our handle method) perform that request; auto-start via `orientationOnByDefault` often fails with `autoLoad: true`.
- If permission is denied or the device has no IMU, methods no-op and `isOrientationAvailable()` / Pannellum’s support check stay false / inactive.
- Helper: `deviceOrientationPermissionRequired()` — `true` when the host should show an “Enable motion” button before calling `startOrientation()`.

### Manual check

- **iOS Safari:** tap Enable motion → allow → tilt phone; drag still pans.
- **Android Chrome:** `orientationControl` + optional `orientationOnByDefault`, or the built-in Pannellum orientation button when the shell has `ls-pv-orientation`.

## Edit mode

```tsx
Expand All @@ -141,10 +187,18 @@ type PanoramaViewerHandle = {
{ id: crypto.randomUUID(), yaw, pitch, kind: "info", content: { type: "text", body: "New" } },
]);
}}
onMarkerMove={(marker, { yaw, pitch }) => {
setMarkers((prev) =>
prev.map((m) => (m.id === marker.id ? { ...m, yaw, pitch } : m)),
);
}}
/>
```

Sphere clicks place markers; **drag-to-pan does not** (movement above a small pixel threshold is ignored). Drag-to-reposition existing markers is a follow-up (not in 0.1.x).
- **Place:** sphere click → `onSphereClick({ yaw, pitch })` (drag-to-pan is ignored via a small pixel threshold).
- **Reposition:** drag an existing hotspot → live preview on the sphere → `onMarkerMove(marker, { yaw, pitch })` on pointer up. Provide `onMarkerMove` to enable drag; the parent must update `markers` to persist.

Yaw/pitch-only updates do **not** remount the viewer (structural changes still do). Camera pose is preserved across structural remounts when the image URL is unchanged.

## Demo

Expand Down
12 changes: 12 additions & 0 deletions packages/panorama-viewer/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ module.exports = {
lines: 90,
statements: 90,
},
"./src/marker-drag.ts": {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
"./src/orientation.ts": {
branches: 85,
functions: 90,
lines: 90,
statements: 90,
},
"./src/PanoramaViewer.tsx": {
branches: 40,
functions: 50,
Expand Down
4 changes: 2 additions & 2 deletions packages/panorama-viewer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jeffgo10/panorama-viewer",
"version": "0.1.2",
"version": "0.2.0",
"description": "React 360 panorama viewer (Pannellum) with navigation, info, and label hotspots",
"license": "MIT",
"repository": {
Expand All @@ -21,7 +21,7 @@
},
"./styles.css": "./dist/styles.css"
},
"files": ["dist", "README.md"],
"files": ["dist", "README.md", "CHANGELOG.md"],
"scripts": {
"build": "tsup && node ./scripts/bundle-styles.mjs",
"dev": "tsup --watch",
Expand Down
Loading
Loading