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
2 changes: 1 addition & 1 deletion .github/workflows/ci-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ jobs:
# first thing a visitor sees and the last thing a build log mentions.
run: |
test -f out/index.html
test -f out/home-page.html
test -f out/pipeline.html
test -f out/wasm/versions.json
test -f out/wasm/hdrgen.wasm

Expand Down
2 changes: 1 addition & 1 deletion DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Serve `out/` as static files. The only requirements are the `.wasm` MIME type
above and clean-URL handling, which almost every static host does.

For a quick local check, note that Python's `http.server` does **not** map
`/home-page` to `home-page.html`, so use the `.html` paths or a server that
`/pipeline` to `pipeline.html`, so use the `.html` paths or a server that
does:

```sh
Expand Down
16 changes: 8 additions & 8 deletions PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ Four tabs (`src/app/navigation.tsx`), identical in both hosts:

| Tab | Route | Purpose |
|---|---|---|
| Image Generator | `/home-page` | Configure and run the LDR → HDR calibration pipeline |
| Settings | `/settings-page` | Output folder, and the versions of everything bundled |
| Image Generator | `/pipeline` | Configure and run the LDR → HDR calibration pipeline |
| Settings | `/settings` | Output folder, and the versions of everything bundled |
| Runs | `/runs` | History of previous pipeline runs and their outcomes |
| Image Viewer | `/image-viewer`, `/image-viewer/view` | Load and inspect a generated `.hdr` file |
| Image Viewer | `/viewer`, `/viewer/view` | Load and inspect a generated `.hdr` file |

`/` redirects to `/home-page`, so the site root resolves in a browser.
`/` redirects to `/pipeline`, so the site root resolves in a browser.

## 3. Feature: Image Generator (Home Page)

`src/app/home-page/page.tsx`
`src/app/pipeline/page.tsx`

- **Image set input** — drag-and-drop or file-picker selection of an LDR bracket (JPEG, TIFF, or camera raw). Multiple named image sets can be staged; each set is validated to contain at least 2 images, and every staged set is run. On the desktop a set is a directory; in a browser, `webkitdirectory` reports a relative path, so nested folders still become separate sets and a plain multi-file selection becomes one.
- **Camera response function** — upload of a `.rsp` file describing the camera's tone response, required for JPEG-derived input.
Expand All @@ -48,7 +48,7 @@ Four tabs (`src/app/navigation.tsx`), identical in both hosts:

## 4. Feature: Calibration Pipeline

`src/lib/pipeline/*`, driven from `src/app/home-page/run-wasm-pipeline.ts`
`src/lib/pipeline/*`, driven from `src/app/pipeline/run-wasm-pipeline.ts`

The pipeline is TypeScript orchestrating WebAssembly. It runs **in a Web Worker**, not on the page: Emscripten's `callMain` is synchronous and blocks its thread for the whole of a tool, so an inline pipeline froze the tab for the length of an hdrgen merge. The worker reads no files itself — the page stages the bytes and transfers them in, because only the page knows how to reach a file (Tauri's filesystem on the desktop, the virtual filesystem in a browser), and keeping that out of the worker is what lets one worker serve both hosts.

Expand Down Expand Up @@ -79,7 +79,7 @@ The two former Rust commands have TypeScript equivalents: raw conversion rides o

## 5. Feature: Image Viewer

`src/app/image-viewer/*`
`src/app/viewer/*`

- **File intake** — drag-and-drop or file picker for a single `.hdr` file (extension-validated); state is passed to the viewer route via a serialized URL query string (`viewer-url.ts`).
- **Rendering** — a `three.js` (WebGL) canvas renders the HDR pixel data as a texture, with pan/zoom (`react-zoom-pan-pinch`).
Expand All @@ -96,7 +96,7 @@ The viewer works on every platform with no additional software. It requires WebG

## 6. Feature: Settings

`src/app/settings-page/page.tsx`
`src/app/settings/page.tsx`

- Output folder, on the desktop. It is hidden in a browser, because a browser downloads and the browser chooses where — an output path there would be a control that does nothing (`canWriteToChosenDirectory()`).
- There are no tool paths to configure: every tool ships with the app.
Expand Down
4 changes: 2 additions & 2 deletions __tests__/build-pipeline-params.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import type { pipelineConfig } from "../src/app/home-page/(pipeline-configuration)/config-provider";
import { buildPipelineParams } from "../src/app/home-page/build-pipeline-params";
import type { pipelineConfig } from "../src/app/pipeline/(pipeline-configuration)/config-provider";
import { buildPipelineParams } from "../src/app/pipeline/build-pipeline-params";

const settings = {
dcrawEmuPath: "/tools/dcraw",
Expand Down
4 changes: 2 additions & 2 deletions __tests__/calibration-files.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import type { pipelineConfig } from "@/app/home-page/(pipeline-configuration)/config-provider";
import { unsuppliedCalibrationFiles } from "@/app/home-page/calibration-files";
import type { pipelineConfig } from "@/app/pipeline/(pipeline-configuration)/config-provider";
import { unsuppliedCalibrationFiles } from "@/app/pipeline/calibration-files";

/** The form's own defaults: every calibration field starts empty. */
function makeConfig(overrides: Partial<pipelineConfig> = {}): pipelineConfig {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/hdr-metadata-priority.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import { HdrMetadataDetails } from "../src/app/image-viewer/view/illuminance-details";
import { HdrMetadataDetails } from "../src/app/viewer/view/illuminance-details";

const KEY_LABEL_REGEX = /^(CAPDATE|COMPUTED_VERTICAL_ILLUMINANCE|FORMAT|VIEW)$/;

Expand Down
2 changes: 1 addition & 1 deletion __tests__/image-matrix-lock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jest.mock("@tauri-apps/plugin-fs", () => ({
stat: () => Promise.resolve({ isDirectory: false, isFile: true, size: 1024 }),
}));

import { SelectedImageProvider } from "../src/app/home-page/selected-image-context";
import { SelectedImageProvider } from "../src/app/pipeline/selected-image-context";
import {
ImageMatrixInput,
type ImageSetIssue,
Expand Down
2 changes: 1 addition & 1 deletion __tests__/image-matrix-selection-clear.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jest.mock("@tauri-apps/plugin-fs", () => ({
import {
SelectedImageProvider,
useSelectedImage,
} from "../src/app/home-page/selected-image-context";
} from "../src/app/pipeline/selected-image-context";
import { ImageMatrixInput } from "../src/components/ui/image-matrix-input";
import type { ImageSet } from "../src/components/ui/image-set-preview";
import { TooltipProvider } from "../src/components/ui/tooltip";
Expand Down
2 changes: 1 addition & 1 deletion __tests__/lens-mask-editor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ declare const jest: typeof import("@jest/globals").jest;

const DIALOG_TITLE = /Configure lens mask/;

import { LensMaskEditor } from "../src/app/home-page/lens-mask-editor";
import { LensMaskEditor } from "../src/app/pipeline/lens-mask-editor";

describe("LensMaskEditor", () => {
it("edits the same motion values as the inline preview", async () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/lens-mask-fit.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@jest/globals";
import { describeMaskOverflow } from "@/app/home-page/lens-mask-fit";
import { describeMaskOverflow } from "@/app/pipeline/lens-mask-fit";

// The dimensions dcraw_emu produces for the example Canon 5D Mark III CR2s,
// and therefore the dimensions of the merged picture crop is handed.
Expand Down
2 changes: 1 addition & 1 deletion __tests__/lens-mask-placement.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jest.mock("@/lib/generic-image-metadata", () => {
return { useGenericImageMetadata: () => metadata };
});

import { ScaledCircularMaskSelection } from "../src/app/home-page/fs-circular-mas-selection";
import { ScaledCircularMaskSelection } from "../src/app/pipeline/fs-circular-mas-selection";

describe("lens mask default placement", () => {
it("centres the mask and starts the radius at a quarter of the image height", async () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/lens-mask-ring.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock("@/lib/generic-image-metadata", () => {

declare const jest: typeof import("@jest/globals").jest;

import { LensMaskEditor } from "../src/app/home-page/lens-mask-editor";
import { LensMaskEditor } from "../src/app/pipeline/lens-mask-editor";

const IMAGE_WIDTH = 5616;
const IMAGE_HEIGHT = 3744;
Expand Down
4 changes: 2 additions & 2 deletions __tests__/luminance-aggregates.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, expect, it } from "@jest/globals";
import type { FalsecolorLuminanceMatrix } from "@/app/image-viewer/view/falsecolor-luminance-webgpu";
import type { FalsecolorLuminanceMatrix } from "@/app/viewer/view/falsecolor-luminance-webgpu";
import {
computeLuminanceSummary,
inferFisheyeMask,
} from "@/app/image-viewer/view/luminance-aggregates";
} from "@/app/viewer/view/luminance-aggregates";

const CORNER_LUMINANCE = 1000;
const INSIDE_LUMINANCE = 10;
Expand Down
4 changes: 2 additions & 2 deletions __tests__/page.test.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import Home from "../src/app/home-page/page";
import { SelectedImageProvider } from "../src/app/home-page/selected-image-context";
import Home from "../src/app/pipeline/page";
import { SelectedImageProvider } from "../src/app/pipeline/selected-image-context";
import { PipelineStatusProvider } from "../src/app/pipeline-status-context";
import { TooltipProvider } from "../src/components/ui/tooltip";

Expand Down
4 changes: 2 additions & 2 deletions __tests__/pending-confirmation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import {
type RunConfirmation,
RunConfirmDialog,
} from "../src/app/home-page/run-confirm-dialog";
import { usePendingConfirmation } from "../src/app/home-page/use-pending-confirmation";
} from "../src/app/pipeline/run-confirm-dialog";
import { usePendingConfirmation } from "../src/app/pipeline/use-pending-confirmation";

const GENERATE_ANYWAY = /Generate anyway/i;
const GO_BACK = /Go back/i;
Expand Down
4 changes: 2 additions & 2 deletions __tests__/preflight.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "@jest/globals";
import type { pipelineConfig } from "@/app/home-page/(pipeline-configuration)/config-provider";
import { describeRunBlocker } from "@/app/home-page/preflight";
import type { pipelineConfig } from "@/app/pipeline/(pipeline-configuration)/config-provider";
import { describeRunBlocker } from "@/app/pipeline/preflight";

/** The example Canon 5D Mark III CR2s, as dcraw_emu renders them. */
const CR2: [number, number] = [5796, 3870];
Expand Down
2 changes: 1 addition & 1 deletion __tests__/preset-file-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jest.mock("../src/lib/storage/kv", () => ({

declare const jest: typeof import("@jest/globals").jest;

import type { pipelineConfig } from "../src/app/home-page/(pipeline-configuration)/config-provider";
import type { pipelineConfig } from "../src/app/pipeline/(pipeline-configuration)/config-provider";
import { type PresetSourceIo, savePreset } from "../src/lib/presets";

const io: PresetSourceIo = {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/presets.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "@jest/globals";
import type { pipelineConfig } from "../src/app/home-page/(pipeline-configuration)/config-provider";
import type { pipelineConfig } from "../src/app/pipeline/(pipeline-configuration)/config-provider";
import { presetFields, sha256Hex } from "../src/lib/presets";

describe("sha256Hex", () => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/run-batch.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, jest } from "@jest/globals";
import { describeBatchSummary, runBatch } from "../src/app/home-page/run-batch";
import { describeBatchSummary, runBatch } from "../src/app/pipeline/run-batch";
import type { ImageSet } from "../src/components/ui/image-set-preview";

function set(name: string): ImageSet {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/run-confirm-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
describeRunConfirmation,
type RunConfirmation,
RunConfirmDialog,
} from "../src/app/home-page/run-confirm-dialog";
} from "../src/app/pipeline/run-confirm-dialog";

const TITLE = /Not all calibration files have been uploaded/i;
const GO_BACK = /Go back/i;
Expand Down
2 changes: 1 addition & 1 deletion __tests__/run-console.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ declare const jest: typeof import("@jest/globals").jest;

const SET_LINE = /Set 1 of 3/;

import { RunConsole } from "../src/app/home-page/run-console";
import { RunConsole } from "../src/app/pipeline/run-console";

describe("RunConsole", () => {
it("shows every log entry, not just the newest", () => {
Expand Down
6 changes: 3 additions & 3 deletions __tests__/selection-details.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, expect, it } from "@jest/globals";
import { render, screen } from "@testing-library/react";
import { ImageSelectionProvider } from "../src/app/image-viewer/view/image-selection-context";
import type { LuminanceSummary } from "../src/app/image-viewer/view/luminance-aggregates";
import { SelectionDetails } from "../src/app/image-viewer/view/selection-details";
import { ImageSelectionProvider } from "../src/app/viewer/view/image-selection-context";
import type { LuminanceSummary } from "../src/app/viewer/view/luminance-aggregates";
import { SelectionDetails } from "../src/app/viewer/view/selection-details";
import { TooltipProvider } from "../src/components/ui/tooltip";

const MASK_NOTE = /outside the lens circle/i;
Expand Down
6 changes: 3 additions & 3 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
// This is an HDRI (High Dynamic Range Imaging) tool: bitwise
// operators are the correct, idiomatic tool for WebGPU buffer-usage
// flag masks (falsecolor-luminance-webgpu.ts), Radiance RLE binary
// scanline decoding (image-viewer/view/page.tsx), and byte-size
// scanline decoding (viewer/view/page.tsx), and byte-size
// shift constants (generic-image-metadata.ts, tiff-worker.ts).
// Every current use is a real bitmask/binary-format operation, not
// a typo for a logical operator, so this rule is disabled globally
Expand Down Expand Up @@ -121,7 +121,7 @@
// "(tiff-image)"). This is a Biome bug, not a project preference —
// exclude them entirely so `ultracite check`/`fix` don't crash.
"includes": [
"src/app/home-page/fs-circular-mas-selection.tsx",
"src/app/pipeline/fs-circular-mas-selection.tsx",
"src/components/ui/(image)/(tiff-image)/tiff-image.tsx"
],
"linter": { "enabled": false },
Expand Down Expand Up @@ -206,7 +206,7 @@
// plugins" when ultracite's unsafe fix rewrote these imports. Keep
// the bare specifier so the browser bundle keeps working.
"includes": [
"src/app/image-viewer/page.tsx",
"src/app/viewer/page.tsx",
"src/components/ui/image-matrix-input.tsx",
"src/components/ui/image-set-preview.tsx",
"src/components/ui/(image)/generic-image.tsx",
Expand Down
10 changes: 5 additions & 5 deletions e2e-tests/test/specs/app.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ function getPipelineFailureMessage(outputDir: string): string | null {
describe("LumiLab", () => {
it("opens to the home page", async () => {
await browser.waitUntil(
async () => (await browser.getUrl()).endsWith("/home-page"),
async () => (await browser.getUrl()).endsWith("/pipeline"),
{
timeout: 10_000,
timeoutMsg: "expected the app to load to the home page",
Expand Down Expand Up @@ -354,7 +354,7 @@ describe("LumiLab", () => {
await setPersistedSettings({ outputPath: tempOutputDirectory });
await browser.refresh();
await browser.waitUntil(
async () => (await browser.getUrl()).endsWith("/home-page"),
async () => (await browser.getUrl()).endsWith("/pipeline"),
{
timeout: 10_000,
timeoutMsg: "expected the app to return to the home page after refresh",
Expand Down Expand Up @@ -472,9 +472,9 @@ describe("LumiLab", () => {
// on macOS and Linux, so the literal Windows origin that used to be here
// navigated nowhere on the other two.
const homeUrl = await browser.getUrl();
await browser.url(homeUrl.replace(/\/home-page.*$/, "/image-viewer"));
await browser.url(homeUrl.replace(/\/pipeline.*$/, "/viewer"));
await browser.waitUntil(
async () => (await browser.getUrl()).endsWith("/image-viewer"),
async () => (await browser.getUrl()).endsWith("/viewer"),
{
timeout: 10_000,
timeoutMsg: "expected the app to navigate to the image viewer page",
Expand All @@ -488,7 +488,7 @@ describe("LumiLab", () => {
await dispatchDrop("image-viewer-input", [hdrFilePath]);

await browser.waitUntil(
async () => (await browser.getUrl()).includes("/image-viewer/view"),
async () => (await browser.getUrl()).includes("/viewer/view"),
{
timeout: 10_000,
timeoutMsg: "expected the dropped HDR file to open in the image viewer",
Expand Down
2 changes: 1 addition & 1 deletion e2e-web/perf.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default defineConfig({
command: `npx serve ../out -l ${PORT}`,
reuseExistingServer: true,
timeout: 60_000,
url: `http://127.0.0.1:${PORT}/home-page`,
url: `http://127.0.0.1:${PORT}/pipeline`,
},
}
: {}),
Expand Down
4 changes: 2 additions & 2 deletions e2e-web/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ export default defineConfig({
webServer: {
// `next start` cannot serve this build: `next.config.js` sets
// `output: "export"`, so there is no server to start. `serve` maps
// `/home-page` to `home-page.html`, which Python's `http.server` notably
// `/pipeline` to `pipeline.html`, which Python's `http.server` notably
// does not -- see DEPLOYMENT.md.
command: `npx serve ../out -l ${PORT}`,
reuseExistingServer: !process.env.CI,
timeout: 60_000,
url: `http://127.0.0.1:${PORT}/home-page`,
url: `http://127.0.0.1:${PORT}/pipeline`,
},
workers: 1,
});
19 changes: 7 additions & 12 deletions e2e-web/tests/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test("the site root lands on the generator", async ({ page }) => {
// A static export with no `index.html` 404s at `/`, which is the first
// thing a visitor sees and the last thing a build log mentions.
await page.goto("/");
await expect(page).toHaveURL(/\/home-page/);
await expect(page).toHaveURL(/\/pipeline/);
await expect(page.getByRole("heading", { name: "LumiLab" })).toBeVisible();
});

Expand All @@ -22,12 +22,7 @@ test("every tab is reachable by URL, not only by clicking", async ({
// Deep links matter more here than on the desktop: a browser user can
// bookmark, refresh, or be sent one, and a static export only serves a
// route it actually emitted a file for.
for (const route of [
"/home-page",
"/settings-page",
"/runs",
"/image-viewer",
]) {
for (const route of ["/pipeline", "/settings", "/runs", "/viewer"]) {
await page.goto(route);
await expect(page).toHaveURL(new RegExp(route));
await expect(page.locator("nav")).toBeVisible();
Expand All @@ -37,7 +32,7 @@ test("every tab is reachable by URL, not only by clicking", async ({
test("the logo and title sit flush left, and the controls flush right", async ({
page,
}) => {
await page.goto("/home-page");
await page.goto("/pipeline");

const header = page.locator("#logo");
const title = page.getByRole("heading", { name: "LumiLab" });
Expand Down Expand Up @@ -86,7 +81,7 @@ test("the logo and title sit flush left, and the controls flush right", async ({
test("the settings page reports the bundled tool versions", async ({
page,
}) => {
await page.goto("/settings-page");
await page.goto("/settings");

// `public/wasm/versions.json` is generated at build time and is easy to
// leave out of an export. Without it the page renders, so nothing fails --
Expand All @@ -103,7 +98,7 @@ test("settings scrolls to its last card", async ({ page }) => {
// Regression: the action bar was `fixed bottom-0` and sat on top of the end
// of the page, so the final card could not be reached at any window size.
await page.setViewportSize({ height: 600, width: 1280 });
await page.goto("/settings-page");
await page.goto("/settings");

const reachedBottom = await page.evaluate(() => {
const scroller = Array.from(document.querySelectorAll("*")).find((el) => {
Expand All @@ -127,7 +122,7 @@ test("settings scrolls to its last card", async ({ page }) => {
});

test("dark mode can be chosen and survives a reload", async ({ page }) => {
await page.goto("/home-page");
await page.goto("/pipeline");

const isDark = () =>
page.evaluate(() => document.documentElement.classList.contains("dark"));
Expand All @@ -148,6 +143,6 @@ test("dark mode can be chosen and survives a reload", async ({ page }) => {
await page.reload();
await expect.poll(isDark).toBe(true);

await page.goto("/settings-page");
await page.goto("/settings");
await expect.poll(isDark).toBe(true);
});
Loading
Loading