refactor(three-sixty-viewer): migrate the iDevice to TypeScript - #39
Open
erseco wants to merge 10 commits into
Open
refactor(three-sixty-viewer): migrate the iDevice to TypeScript#39erseco wants to merge 10 commits into
erseco wants to merge 10 commits into
Conversation
Selectively apply the generic TypeScript-iDevice build infrastructure from upstream PR exelearning#2147 (commits 085d7f9 and f29923e), without any Interactive Video code: - scripts/build-idevices.ts + scripts/build-idevices.spec.ts: centralized, convention-based builder (src/edition|export/index.ts -> classic IIFE bundles, per-iDevice strict tsconfig typecheck, --only/--watch/--typecheck, optional build.config.json manifest for deviations). - Slide migrates onto the central builder via build.config.json; the bespoke scripts/build-slide-editor.ts is removed and package.json gains typecheck:idevices / bundle:idevices / bundle:idevices:watch, wired into build:all before resource packaging. - vitest.config.mts discovers TypeScript-iDevice specs and coverage by convention (public/files/perm/idevices/base/*/src/**) instead of naming a single iDevice. - build-resource-bundles.js keeps .map files out of resource ZIPs. - Generic E2E helpers from the upstream infra commit (downloadViaFileMenu, extractZipToTempDir) and the e2e.yml artifact-list comment. - Docs: ADR-0006 (TypeScript iDevices build convention), doc/development/idevices-typescript.md, records/mkdocs/AGENTS/skill rows. ADR-0001..0005 IDs stay reserved for the upstream Interactive Video PR. Co-Authored-By: Claude <noreply@anthropic.com>
Single shared implementation of everything the edition and export bundles used to duplicate: - src/shared/types.ts: versioned document model (v1/v2), discriminated hotspot-action union with an explicit UnsupportedHotspotAction that keeps a future action's original type and payload, HydrationResult. - schema.ts: hydrateDocument() as the only entry point for unknown persisted input (object or JSON string) — ok / unsupported-version / invalid, never throws, never mutates; serializeDocument() writes unsupported actions back in their original wire form (lossless round-trip). - migration.ts: v1 single-image → v2 one-scene tour, mirroring the legacy detection exactly. - normalization.ts: pure, idempotent v2 normalization (scenes, initial view, behaviour, clamped ranges, enum fallbacks, start-scene resolution). - hotspot-actions.ts: per-action normalization/serialization, explicit validation rules, scene-reference lookup and deterministic repair. - geometry.ts: yaw/pitch ↔ direction, NDC mapping, letterbox-aware flat-image rect and percent conversion (clicks outside the contained image are ignored). - ids.ts: legacy-format ID generation with injectable entropy; deterministic sequential generator for tests. - urls.ts: safe link-scheme allowlist and provider embed-URL mapping. - html.ts: escaping and label truncation. 91 colocated Vitest specs cover migration fixtures, idempotence, mutation- freedom, clamping, unsupported-action round-trips and future-version refusal. Co-Authored-By: Claude <noreply@anthropic.com>
Replace the hand-written export/three-sixty-viewer.js (now a generated, gitignored IIFE built from src/export/index.ts) with typed, modular code: - src/viewer/: browser layer shared with the editor preview — structural three.js types (the vendored library stays a page global; no @types/three, no bundled copy), panorama renderer owning every WebGL resource it creates, flat-image renderer with letterbox-aware positioning, hotspot overlay, nav/fullscreen/drag-capture controls, render-loop + disposer-bag lifecycle helpers, and asset:// resolution behind a boundary. - src/export/: per-node instances in a WeakMap-backed registry instead of one mutable global list. Each instance owns its scene controller, renderers, hotspot layer, controls, animation frame, resize observer and modal; destroy() releases all of them and re-rendering a node disposes its predecessor. Multiple viewers per page stay fully independent. - Accessible content modal (role=dialog, focus trap, Escape, focus restoration), safe link opening (scheme allowlist + noopener), explicit handling of unsupported future hotspot actions, WebGL/three.js/no-image fallbacks, and prefers-reduced-motion disabling autorotation. - renderView/renderBehaviour/init keep the exact exe_export.js contract and window.$threesixtyviewer is assigned explicitly by the entry point. Documents with version > 2 render an accessible notice instead of being guessed at. 84 colocated Vitest specs (viewer + export) run against an injectable THREE mock and manual frame scheduler. Co-Authored-By: Claude <noreply@anthropic.com>
…otspot authoring Replace the 1900-line $exeDevice singleton (edition/three-sixty-viewer.js, now a generated, gitignored IIFE built from src/edition/index.ts) with a thin device bridge over an explicit editor instance: - state.ts: the single typed source of truth. Scene/hotspot mutations, start-scene resolution, action-type switching, per-hotspot validation and save-time validation all live here, DOM-free and headlessly tested. - device.ts implements the eXeLearning contract (init/save/destroy). Re-initializing destroys the previous editor completely; documents from a NEWER schema version show an explanation and save() passes the original payload through untouched instead of rewriting it. save() before init or on validation failure returns false. - Modular UI: form.ts (pure HTML, every legacy control id preserved), scene-list, scene-editor, hotspot-list + action-specific hotspot-editor, asset-picker boundary (file manager / FileReader fallback), three-loader (lazy vendored three.js for the preview) and preview.ts reusing the SAME src/viewer renderers as the export runtime — the mirrored normalization and geometry copies are gone. - Hotspot authoring: explicit placement mode with aria-pressed state, Escape cancel, an aria-live status region and a visible hint; clicks land via the shared raycast/letterbox geometry and clicks on letterbox bars are ignored instead of snapping to an edge; placed hotspots are selected and revealed. List-based creation stays. Inline validation renders next to the affected field (goToScene targets, safe link schemes, image/video sources) and deleting a referenced scene now asks for confirmation, states how many hotspots are affected and repairs them deterministically. - Unsupported future hotspot actions render read-only with their original type visible and survive editing sessions byte-for-byte. 89 colocated Vitest specs cover the editing model, form contracts, placement, drag, preview lifecycle and the device bridge (repeated init, v1 open+save, future-version passthrough). Co-Authored-By: Claude <noreply@anthropic.com>
- src/test/bundle-contract.spec.ts evaluates the ACTUAL generated IIFEs in the test window: classic-script shape (no imports/require/chunks), the window.$exeDevice and window.$threesixtyviewer contracts, safe evaluation without THREE loaded, re-evaluation reassigning a fresh editor (the workarea reloads the script per edit), and a real v1-open → v2-save round-trip through the compiled bundle. - scripts/build-idevices.spec.ts now asserts the repository state includes three-sixty-viewer via the src/ convention (edition + export entries, strict tsconfig) next to Slide's manifest. - .github/workflows/e2e.yml ships the two generated (gitignored) bundles in the build-public artifact so fresh E2E runners don't 404 on them. - Playwright spec: the two tests that reached into removed internals (normalizeData/_openLink) now exercise the public bundle contracts; new scenarios cover second-scene + start-scene persistence, goToScene hotspot editing + persistence, placement mode (aria-pressed, Escape cancel, click placement on a flat preview via the file-input fallback, letterbox clicks ignored) and the scene-deletion confirmation with deterministic repair. Repeated reopen-after-reload boilerplate extracted into reopenForEdit(). - Fix surfaced by E2E: the behaviour controls fieldset (autorotate + speed, zoom, fullscreen, labels, nav arrows) was left unwired in the TypeScript editor; wireBehaviourFields() restores it with a colocated spec. bun x playwright test --project=chromium …/three-sixty-viewer.spec.ts: 14/14 passing. Co-Authored-By: Claude <noreply@anthropic.com>
…002) SDD-0002 records what is specific to the 360° Viewer on top of the generic ADR-0006 convention and doc/development/idevices-typescript.md: the src/ layout (shared/viewer/edition/export), the public v1 and v2 formats and the v1 → v2 migration, explicit future-version and unsupported-action handling, the single shared normalization used by both bundles, the external three.js/ OrbitControls contract, per-instance lifecycle and cleanup, the hotspot placement architecture, and the unit / bundle-contract / E2E test strategy. SDD-0001 stays reserved for the upstream Interactive Video PR. Co-Authored-By: Claude <noreply@anthropic.com>
… points from v8 The two src/*/index.ts entry points only assign window globals and are exercised as compiled IIFEs by the bundle-contract specs — v8 cannot see them through imports, so they are excluded from coverage by convention. New editor specs cover the asset-picking callbacks (file manager + FileReader fallback), hotspot media picking, row selection, live scene-list title updates and the wired behaviour controls. Co-Authored-By: Claude <noreply@anthropic.com>
Bundle ReportBundle size has no change ✅ |
… Video Rewrite the edition surface to follow the Interactive Video patterns: accordion hotspot list (one expanded editor at a time), type badges with text labels, add bar, transient Saved feedback, inline delete confirmation, section counts, and polished scene rows. Update unit and E2E coverage for the collapsed-by-default list after reload.
Resolve conflicts with upstream main: - bun.lock: keep main's dependency versions (pdfjs-dist 6.2.108 and other bumps) - doc/architecture/adr/records.md: keep ADR-0001/ADR-0042 from main and ADR-0006 from this branch - package.json: auto-merged (iDevice build scripts + main dep/test updates)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Full TypeScript refactor of the 360° Viewer iDevice
(
public/files/perm/idevices/base/three-sixty-viewer/):moves into a typed, modular
src/tree (shared/,viewer/,edition/,export/,test/), compiled by the centralized TypeScript-iDevice buildconvention selectively reused from upstream PR
exelearning/exelearning#2147
(ADR-0006).
edition/three-sixty-viewer.jsandexport/three-sixty-viewer.jsbundles (+ source maps) are not committed— they are gitignored and rebuilt by
bun run bundle:idevices/build:all, and shipped to E2E CI runners via the build artifact.carried is replaced by one shared implementation; the persisted format
is unchanged and the v1 → v2 migration remains fully supported.
Architecture
Both window globals are assigned explicitly by the entry points (the
workarea re-evaluates the edition script per edit session); nothing relies on
the bundler's
globalName. Every runtime instance and every editor owns aLIFO disposer bag — animation frames, WebGL resources (textures, geometry,
material, renderer), OrbitControls, observers, listeners and modal handlers
are all released on
destroy(), and re-rendering a node disposes itspredecessor first. Multiple viewers on one page share no state.
prefers-reduced-motiondisables autorotation in both the runtime and thepreview.
Build integration
Selectively applied from upstream PR exelearning#2147 (commits
085d7f9+f29923e),infrastructure only:
scripts/build-idevices.ts+ spec — convention-based builder(
src/edition|export/index.ts→ classic IIFEedition|export/<name>.js,per-iDevice strict
tsconfig.jsontypecheck,--only/--watch/--typecheck,optional
build.config.jsonmanifest).build.config.json(
scripts/build-slide-editor.tsremoved);package.jsongainstypecheck:idevices/bundle:idevices/bundle:idevices:watch, wiredinto
build:allbefore resource packaging sopublic/bundles/idevices.zipalways contains fresh bundles (
.mapfiles are excluded from resource ZIPs).vitest.config.mtsdiscovers TypeScript-iDevice specs and coverage byconvention (
public/files/perm/idevices/base/*/src/**), not by naming asingle iDevice.
doc/development/idevices-typescript.md(adapted to thisrepo's state); ADR-0001…0005 / SDD-0001 IDs stay reserved for the
upstream Interactive Video PR so the two histories can merge without
renumbering.
schema/importer/workarea/SCORM changes,
bun.lockchurn and IV-specificADRs/SDD.
.github/workflows/e2e.ymlships the two generated bundles in thebuild-public artifact — a fresh CI checkout serves them without 404s.
Schema compatibility
version: 2,ideviceId,startSceneId,scenes[](id, title, src, alt, description,equirectangular/flatprojection, initial yaw/pitch/fov, hotspots with yaw/pitch and x/y percent,
labels, icons),
behaviour(autorotate, zoom, fullscreen, nav controls,render quality, labels + position, brightness/contrast/saturation). All
ranges, defaults and enum fallbacks match the legacy normalization.
exact legacy detection; nothing is lost (verified by fixtures and by an E2E
test driving the real bundle's
init→save).version > 2is never rewritten:hydrateDocument()returnsunsupported-versionwith the original payload intact; the editor shows anexplanation and
save()passes the payload through byte-for-byte; theruntime renders an accessible notice.
text: theybecome an explicit
UnsupportedHotspotActionin memory and serialize backto their original wire form (lossless round-trip, covered by tests).
Hotspot authoring improvements
aria-pressed, shows a hint, announces via anaria-liveregion, cancelswith Escape, and ends after one placement. Equirectangular clicks are
unprojected to yaw/pitch; flat clicks map to percentages of the contained
image rectangle and clicks on letterbox bars are ignored instead of
snapping to an edge. Numeric fields remain for precise adjustment;
list-based creation is untouched.
exist and differ from the current scene; links accept only safe schemes;
image/video need a source) with text + icon, never colour alone.
goToScene hotspots point at the scene; accepting clears their targets
deterministically and flags them for review — no silent dangling
references.
handles; dragging handles updates the row; save announces
Saved.in thestatus region. Unsafe link URLs block saving (
save()→false).Accessibility
Keyboard-operable authoring controls with visible focus; labelled icon
buttons;
aria-livestatus region; Escape closes the runtime modal andcancels placement; modal focus trap + focus restoration to the trigger;
hotspots in exported content are real buttons operable via Enter/Space;
accessible fallbacks when WebGL/three.js/the image are unavailable;
reduced-motion support; no colour-only meaning.
Tests
src/(shared schema/migration/normalization/geometry/actions, viewer, export runtime, editor,
placement, bundle contracts) + 8 builder specs (
bun test scripts/build-idevices.spec.ts). The old eval-based*.test.jspairs arereplaced without losing behavioural coverage.
shape, both window globals and APIs, safe evaluation without
THREE,re-evaluation contract, and a v1-open → v2-save round-trip.
persistence, flat projection, link hotspots, second scene + start scene
persistence, goToScene editing, placement mode (aria-pressed/Escape/click
placement/letterbox ignore), reference-aware scene deletion, bundle
contracts, v1 → v2 through the real bundle. Repeated reopen-after-reload
boilerplate got a shared helper.
shared/99.6 %/100 %,viewer/95.4 %/95.9 %,
export/96.1 %/97.5 %,edition/92.8 %/96.1 %. The twobundle entry points are excluded by convention (exercised as compiled IIFEs
by the bundle-contract specs — v8 cannot instrument them through imports).
Verification
All executed on this branch:
make fix— clean (2 pre-existing warnings untouched).bun test scripts/build-idevices.spec.ts— 8/8.bun run typecheck:idevicesandbun run bundle:idevices— green; slide +three-sixty-viewer build through the one convention.
bun x vitest run public/files/perm/idevices/base/three-sixty-viewer/src—284/284.
make test-frontend(full Vitest suite) — 13 823 tests, 0 failures.make test-integration— 721/721.make test-unit— frontend green; backend has pre-existing environmentalfailures unrelated to this PR (verified identical on the unmodified
mainbase commit):src/routes/api/v1/assets.spec.ts(SQLite migration002 fails in the dev container),
src/cli/commands/elp-convert.spec.ts("/root must not be writable" — the container runs as root) and one
order-dependent
src/routes/api/convert.spec.tscase that passes inisolation.
bun x playwright test --project=chromium …/three-sixty-viewer.spec.ts—14/14. The COMPLETE chromium E2E suite could not finish in the
development container (three attempts each crashed the container under
load — resource exhaustion unrelated to this change); it relies on the CI
run of this PR, which now also receives the generated bundles as build
artifacts.
bun scripts/build-idevices.ts --only three-sixty-viewer --typecheckregenerates them;
git statusstays clean (bundles and maps ignored) andpublic/bundles/idevices.zipcontains the bundle but no.mapfiles.Known limitations
unit tests against a structural THREE mock plus manual E2E; no automated
pixel-level WebGL assertions (deliberate — they are unreliable in CI).
behaviour, kept intentionally to swap the renderer + per-mode fields).
branch.
Follow-ups
the
interactive-videobuild entries merge in without conflicts with thisbranch's ADR-0006/SDD-0002 usage.
circleand per-hotspot colourthemes would slot into
shared/types.ts+hotspot-renderer.tswithout aschema bump.
doc/development/idevices-typescript.md§Testing suggests adding newiDevice spec globs; with the convention-based Vitest config this is now
automatic and the guide could drop that step.
Docs: adds
SDD-0002(360° Viewer TypeScript architecture), reuses ADR-0006and the TypeScript-iDevices guide from upstream.
Generated by Claude Code