Trim needless comments and fix the picker width

Remove comments that only restate the code, keeping the section banners
and the notes that carry a reason, a measured fact or an external
contract.

ButtonOptions.width claimed to be a fixed width but only set minWidth,
so the Graph and Texture pickers grew with the name they showed. It is
now what it promises, with a separate minWidth for the buttons that have
two forms. Narrow buttons shrink their padding, otherwise a 24px icon
button had 8px left for its glyph and truncate dropped it entirely.

__gm.labelsDropped() guards that class: the overflow guard only saw text
that was too wide, never a label truncated to nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 15:52:05 +02:00
1 parent f16801aef3
commit 4d84231b9f
115 files changed
+707 -1564

No files matched your search

+15 -60
View File
@@ -2,18 +2,13 @@ import type { TextureAsset, TextureFrame } from './Asset';
import type { PropertyBag } from './Properties';
import { findAsset, type Project } from './Project';
/**
* A stored image file name: `{sha256}.{png|jpg|webp|gif}`. Mirrors `TextureStore::isValidName`
* on the server – the name is derived from the bytes there, so anything else in a document
* was hand-written and must not be turned into a request.
*/
/** Mirrors `TextureStore::isValidName` on the server, where the name is derived from the bytes. */
const FILE_PATTERN = /^[a-f0-9]{64}\.(png|jpg|webp|gif)$/;
export function isTextureFile(name: string): boolean {
return FILE_PATTERN.test(name);
}
/** A rectangle in image pixels: the part of a texture a frame cuts out. */
export interface FrameRect {
x: number;
y: number;
@@ -21,40 +16,29 @@ export interface FrameRect {
h: number;
}
/** Smallest frame that is still a picture rather than a slip of the mouse. */
export const MIN_FRAME_SIZE = 1;
/**
* A picture ready to be drawn: which file, whether to smooth it when scaled, and which part
* of it. `rect` is null for the whole image – not a rectangle covering everything, because
* the renderer can then skip the texture matrix entirely (Pixi fits an unmatrixed fill to the
* shape by itself) and because "the whole picture" survives the image being re-uploaded at a
* different size.
* `rect` is null for the whole image rather than a rectangle covering it: the renderer can then
* skip the texture matrix (Pixi fits an unmatrixed fill to the shape) and the meaning survives
* the image being re-uploaded at a different size.
*/
export interface TextureRef {
file: string;
/** False for pixel art, where interpolation is the difference between crisp and mush. */
smooth: boolean;
/** Sub-rectangle in image pixels, or null for the whole image. */
rect: FrameRect | null;
/** Size of the whole image in pixels – the reference frame `rect` is expressed in. */
sourceWidth: number;
sourceHeight: number;
}
/** A texture asset together with one of its frames. */
export interface FrameRef {
asset: TextureAsset;
frame: TextureFrame;
}
/**
* The frame with that id, wherever in the project it lives.
*
* Frame ids come from the same generator as asset ids and are unique across the document,
* which is what lets a sprite's `texture` property hold either kind: an asset id means the
* whole picture, a frame id means one cut-out of it. One property, one picker, and every
* sprite written before frames existed keeps meaning exactly what it meant.
* Frame ids share the generator with asset ids and are unique across the document, which lets a
* sprite's `texture` property hold either: an asset id means the whole picture, a frame id a cut-out.
*/
export function findFrame(project: Project, frameId: string): FrameRef | null {
if (!frameId) return null;
@@ -71,12 +55,6 @@ export function textureRefIds(asset: TextureAsset): string[] {
return [asset.id, ...asset.frames.map((f) => f.id)];
}
/**
* The picture a bag of properties points at, or null when it points at nothing usable –
* no reference, a dangling one (the texture or the frame was deleted), or one whose upload
* never finished. Callers draw their plain colour in that case, which is what a sprite
* looked like before textures existed.
*/
export function resolveTexture(project: Project, props: PropertyBag): TextureRef | null {
const id = String(props.texture ?? '');
if (!id) return null;
@@ -98,12 +76,8 @@ function refFor(asset: TextureAsset, frame: TextureFrame | null): TextureRef | n
}
/**
* A rectangle rounded to whole pixels and forced inside the image.
*
* Frames are pixel coordinates into a bitmap, so a fractional one has no meaning – it would
* sample half of a neighbouring pixel along every edge, which on a sprite sheet is the
* classic seam of foreign colour. Dragging produces fractions constantly (the view is
* zoomed), so this is applied on the way into the model rather than trusted from callers.
* Rounds to whole pixels and forces the rectangle inside the image: a fractional frame samples
* half of a neighbouring pixel along every edge, the classic sprite-sheet seam.
*/
export function normalizeFrameRect(rect: FrameRect, imageWidth: number, imageHeight: number): FrameRect {
// An image of unknown size (upload still running) cannot constrain anything; only round.
@@ -121,14 +95,13 @@ function clampInt(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, n));
}
/** How a sheet is cut into a regular grid – the shape of nearly every sprite sheet. */
export interface SliceOptions {
columns: number;
rows: number;
/** Pixels skipped at the left/top edge of the sheet before the first cell. */
offsetX: number;
offsetY: number;
/** Pixels between neighbouring cells (sheets exported with padding). */
/** Pixels between neighbouring cells. */
spacingX: number;
spacingY: number;
}
@@ -143,20 +116,13 @@ export const DEFAULT_SLICE: SliceOptions = {
};
/**
* The rectangles a grid cuts a sheet into, in reading order (left to right, top to bottom) –
* which is the order sprite sheets are authored in and therefore the order an animation will
* want them.
*
* Cell size is derived from the image rather than asked for: everyone knows how many tiles
* their sheet has, almost nobody knows the pixel size off-hand, and deriving it cannot
* disagree with the picture. A cell that would reach past the image is dropped instead of
* clipped – a clipped one looks like a frame and is not one.
* The rectangles a grid cuts a sheet into, in reading order. Cell size is derived from the image;
* a cell that would reach past the edge is dropped rather than clipped.
*/
export function sliceFrames(imageWidth: number, imageHeight: number, options: SliceOptions): FrameRect[] {
const columns = Math.floor(options.columns);
const rows = Math.floor(options.rows);
// NaN has to be rejected explicitly: every comparison against it is false, so a NaN column
// count would pass `columns < 1` and only produce nothing by accident of the loop condition.
// NaN passes `columns < 1` (every comparison against it is false), so reject it explicitly.
if (!Number.isFinite(columns) || !Number.isFinite(rows)) return [];
if (!Object.values(options).every((value) => Number.isFinite(value))) return [];
if (columns < 1 || rows < 1 || imageWidth <= 0 || imageHeight <= 0) return [];
@@ -175,7 +141,6 @@ export function sliceFrames(imageWidth: number, imageHeight: number, options: Sl
return rects;
}
/** The rectangle part of a frame, and the way to write one back – the only glue between the two. */
export function frameRect(frame: TextureFrame): FrameRect {
return { x: frame.x, y: frame.y, w: frame.w, h: frame.h };
}
@@ -187,14 +152,7 @@ export function setFrameRect(frame: TextureFrame, rect: FrameRect): void {
frame.h = rect.h;
}
/**
* A frame name that is free within its texture: `base`, else "base 2", "base 3" … – the same
* shape `nextAssetName` produces, because a name the user reads should not be numbered by two
* conventions in one document.
*
* Names matter more here than they look: they are what the sprite's picker shows, so two
* frames called "idle" would be one choice repeated rather than two.
*/
/** A name free within the texture: `base`, else "base 2", "base 3" … – the shape `nextAssetName` produces. */
export function uniqueFrameName(asset: TextureAsset, base: string, ignoreId = ''): string {
const taken = takenFrameNames(asset, ignoreId);
const stem = base || 'Frame';
@@ -202,11 +160,8 @@ export function uniqueFrameName(asset: TextureAsset, base: string, ignoreId = ''
}
/**
* `count` free names numbered from 1 – what slicing a sheet wants, where the number is the
* cell's position and "Frame 1" next to "Frame 2" is the whole point.
*
* Numbers already taken are skipped rather than suffixed again: re-slicing a sheet that keeps
* its old frames must not produce "Frame 2 1".
* `count` free names numbered from 1. Taken numbers are skipped rather than suffixed again, so
* re-slicing a sheet that keeps its old frames cannot produce "Frame 2 1".
*/
export function nextFrameNames(asset: TextureAsset, count: number, base = 'Frame'): string[] {
const taken = takenFrameNames(asset);