Files
feuerwerk/src/core/model/generated.ts
T
adminandClaude Opus 5.5 056a622524
game-maker / gate (push) Successful in 2m57s
game-maker / suite (push) Successful in 8m55s
Refactoring round 6: shared sheet canvas, fewer copies (#85)
Recurring cleanup round. Behaviour is unchanged. Duplicated code in
src went from 27 clones with 299 lines to 6 clones with 56 lines
(jscpd, the two identical OFL licence texts not counted).

- SheetCanvas is the new base of the texture, pixel art and vector art
  canvases. It holds background, mask, hint, panning, wheel zoom, fit,
  view save and restore and the screen mapping. Pixel art keeps its
  whole-pixel panning and fixed zoom steps.
- mixColor in src/core/util/color.ts replaces three copies in
  animation, gradient and particles. It clamps t like the particle one.
- fingerprint now lives once in generated.ts, next to the revision
  contract from #185.
- MoveCommand next to RenameCommand is the base of the three reorder
  commands for tile pictures, tilemap layers and vector shapes.
- The two raycast nodes share inputs, settings and filter via rayOf.
- canvasRect() lives once in AssetTabEditor.
- Removed unused code: findPixelArt, isDrawableShape,
  GeneratedPictures.has and SnapToggle.snapping. Tests now check the
  same things through equivalent paths.
- 38 values used only in their own file lost their export.
- Three comments that told history now describe the current state.
  The bundle stayed md5-identical for those edits.
- README lists vector art, pixel art and palette, and links the two
  wiki pages.

Findings:
- PaintTilesCommand.changes is meant to prevent empty undo steps but
  has not been called since #128. The rectangle tool therefore records
  a no-op undo step when it paints the same tile over itself. Left as
  is, because fixing it changes behaviour.
- No test covered middle-click panning in any sheet tab before. 13f
  and 48a now check it.
- The default values of the ray inputs were untested. Length 1000
  could become 10 without a red test. tests/core/raycast.test.ts now
  covers it.
- CI measured 1308 editor checks at d7c0b7c, not the 1307 counted for
  #164. The sum is now 1310 (1308 + 2), computed.

Tests:
- ./check.sh: 144 files, 2143 tests, exit 0 (before 2142).
- e2e 1-16,37,39-43,47,48 fresh: ALL PASSED, 729 checks.
- e2e 1-13,47,48 with the new checks: ALL PASSED, 518 checks.
- Counter-checks, each restored via cp and cmp: halved panning fails
  both new pan checks; unclamped mix fails 1 test, no blue channel 3;
  coarser hash 3; broken reorder undo 2; triggers setting forced on 2;
  bent ray length 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 06:57:43 +02:00

91 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Pictures that are drawn rather than uploaded – vector art (#129), the pixel editor (#130).
*
* A picture is named by `TextureRef.file` everywhere: the sprite, the tile, the tilemap mesh, the
* particle source and the picker all pass that string around. A generated picture therefore gets a
* key of its own, `gen:<assetId>@<revision>`, which no uploaded file can look like (those are
* `<sha256>.<ext>`, see `isTextureFile`). Whoever draws it is asked for it under that key.
*
* The revision is the other half of the contract: it changes whenever the drawing changes, so a
* changed picture is a new key. Nothing has to ask "is this still valid" – the library rasterises
* the key it is given and lets the old one go. A counter and a fingerprint of the content both
* work; a fingerprint cannot be forgotten in a command, which is why the token is a string.
*/
import type { Asset } from './Asset';
import type { Project } from './Project';
const PREFIX = 'gen:';
/** Anything but `@`, which separates the two halves, and nothing empty. */
const TOKEN = /^[^@]+$/;
/** What an asset that draws its own picture tells the picture path. */
export interface GeneratedPicture {
/** Canvas size in pixels; the sprite takes its size from it, as it does from an image. */
width: number;
height: number;
/** Changes with the drawing – a counter or a fingerprint of the content. */
revision: string | number;
}
/**
* Reads the canvas of one asset type; null when this asset draws nothing (yet). The project is there
* for a drawing that shows another asset – a vector shape filled with a texture (#186).
*/
export type PictureSource = (asset: Asset, project?: Project) => GeneratedPicture | null;
/**
* Keyed by asset type as a plain string, not by `AssetType`: a plugin may bring an asset type of
* its own (#126), and the picture path has no reason to know the closed list.
*/
const sources = new Map<string, PictureSource>();
/** Registers the reader for an asset type; the returned function takes it back out again. */
export function registerPictureSource(type: string, read: PictureSource): () => void {
sources.set(type, read);
return () => {
if (sources.get(type) === read) sources.delete(type);
};
}
export function generatesPictures(type: string): boolean {
return sources.has(type);
}
/** The canvas this asset draws, or null when its type draws none. */
export function generatedPictureOf(asset: Asset, project?: Project): GeneratedPicture | null {
const picture = sources.get(asset.type)?.(asset, project) ?? null;
if (!picture) return null;
return picture.width > 0 && picture.height > 0 ? picture : null;
}
/** FNV-1a over the text, in base 36: short, stable across sessions, and free of `@`. */
export function fingerprint(text: string): string {
let hash = 0x811c9dc5;
for (let i = 0; i < text.length; i++) {
hash ^= text.charCodeAt(i);
hash = Math.imul(hash, 0x01000193) >>> 0;
}
return hash.toString(36);
}
export function generatedKey(assetId: string, revision: string | number): string {
return `${PREFIX}${assetId}@${revision}`;
}
export function isGeneratedFile(file: string): boolean {
return parseGeneratedKey(file) !== null;
}
/** The two halves of a key, or null for anything that is not one – an uploaded file included. */
export function parseGeneratedKey(file: string): { assetId: string; revision: string } | null {
if (!file.startsWith(PREFIX)) return null;
const rest = file.slice(PREFIX.length);
const at = rest.lastIndexOf('@');
if (at <= 0) return null;
const assetId = rest.slice(0, at);
const revision = rest.slice(at + 1);
if (!TOKEN.test(assetId) || !TOKEN.test(revision)) return null;
return { assetId, revision };
}