Offer the pictures the server already holds when adding a texture
"+ → Texture" opened the machine's file dialog, so a picture that was uploaded once but lost its asset – an undone import, a deleted asset – was unreachable from the editor while still sitting on the server. Nothing collects those files, by design. It now opens a grid of every picture the server holds, with thumbnails: pick one and "Use image" makes a texture asset on that file, "Upload…" is the way in for bytes the server does not have yet and lands in the same import as a drop. A tile captioned with an asset name is one this project already wears – picking it selects that asset instead of making a second one, since two assets on one file would fight over `filter`. - `GET ?r=textures` now reports `width`/`height`; a texture asset holds the pixel size, and reading it off the GPU texture would tie the asset's correctness to load timing. - `TextureLibrary.load(file)` loads without touching `scaleMode` – a thumbnail must not restage the filter of a picture the scene is drawing with. - The grid geometry lives in `texture/imageGrid.ts` as pure functions; inside the widget no test could reach it. - The part after an upload is now one function shared by the drop import and the picker, so "one file = one asset" is stated in a single place. 12 new E2E checks; the strongest one undoes an import, leaving the file on the server with no asset naming it, and picks exactly that file back out of the dialog.
This commit is contained in:
1 parent
2f4dc843d7
commit
88ec367ab0
16 files changed
+774
-56
No files matched your search
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Geometry of the image picker's tile grid: how many tiles fit a width, where each one sits, and
|
||||
* which one a point hits. Pure, so the arithmetic is testable without a canvas.
|
||||
*/
|
||||
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
/** Narrowest a tile may get before a column is dropped, and widest it may grow. */
|
||||
export const TILE_MIN = 92;
|
||||
export const TILE_MAX = 132;
|
||||
export const TILE_GAP = 8;
|
||||
/** Strip under the picture holding the caption. */
|
||||
export const CAPTION_HEIGHT = 16;
|
||||
|
||||
export interface GridMetrics {
|
||||
columns: number;
|
||||
/** Side of a tile's picture area; a tile is this plus the caption strip. */
|
||||
tile: number;
|
||||
rows: number;
|
||||
/** Total height of the grid, which is what the scroll container has to give it. */
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function gridMetrics(width: number, count: number, gap = TILE_GAP): GridMetrics {
|
||||
const columns = Math.max(1, Math.floor((width + gap) / (TILE_MIN + gap)));
|
||||
const spread = (width - gap * (columns - 1)) / columns;
|
||||
const tile = Math.max(TILE_MIN, Math.min(TILE_MAX, Math.floor(spread)));
|
||||
const rows = Math.ceil(count / columns);
|
||||
const height = rows * (tile + CAPTION_HEIGHT) + Math.max(0, rows - 1) * gap;
|
||||
return { columns, tile, rows, height };
|
||||
}
|
||||
|
||||
export function tileRect(index: number, metrics: GridMetrics, gap = TILE_GAP): Rect {
|
||||
const column = index % metrics.columns;
|
||||
const row = Math.floor(index / metrics.columns);
|
||||
const h = metrics.tile + CAPTION_HEIGHT;
|
||||
return { x: column * (metrics.tile + gap), y: row * (h + gap), w: metrics.tile, h };
|
||||
}
|
||||
|
||||
/** Index of the tile under a point, or -1 for the gaps between them and everything past the last. */
|
||||
export function tileAt(x: number, y: number, count: number, metrics: GridMetrics, gap = TILE_GAP): number {
|
||||
if (x < 0 || y < 0) return -1;
|
||||
const h = metrics.tile + CAPTION_HEIGHT;
|
||||
const column = Math.floor(x / (metrics.tile + gap));
|
||||
const row = Math.floor(y / (h + gap));
|
||||
if (column >= metrics.columns) return -1;
|
||||
if (x - column * (metrics.tile + gap) > metrics.tile) return -1;
|
||||
if (y - row * (h + gap) > h) return -1;
|
||||
const index = row * metrics.columns + column;
|
||||
return index < count ? index : -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* The picture centred in the box, scaled to fit either way. Small pictures are blown up: an icon
|
||||
* shown at its own 16 px would be unrecognisable next to a full-size sheet.
|
||||
*/
|
||||
export function containRect(imageW: number, imageH: number, boxW: number, boxH: number): Rect {
|
||||
if (imageW <= 0 || imageH <= 0) return { x: 0, y: 0, w: 0, h: 0 };
|
||||
const scale = Math.min(boxW / imageW, boxH / imageH);
|
||||
const w = Math.max(1, Math.round(imageW * scale));
|
||||
const h = Math.max(1, Math.round(imageH * scale));
|
||||
return { x: Math.round((boxW - w) / 2), y: Math.round((boxH - h) / 2), w, h };
|
||||
}
|
||||
Reference in new issue
Block a user