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

+4 -11
View File
@@ -2,18 +2,13 @@ import type { TextureAsset } from '../../core/model/Asset';
import { DEFAULT_SLICE, type SliceOptions, sliceFrames } from '../../core/model/texture';
import { Button, Checkbox, HStack, Label, NumberField, Popup, Spacer, Theme, type Widget } from '../../ui';
/** Width of the label column, matching the inspector's so the two read as one language. */
/** Matches the inspector's label column. */
const LABEL_WIDTH = 76;
const FIELD_WIDTH = 76;
/**
* Cuts a sheet into a grid. Asks for the number of columns and rows rather than a cell size:
* everyone knows how many tiles their sheet has, and deriving the cell size from the picture
* cannot disagree with it.
*
* The dialog states, live, how many frames the current numbers would produce and how big each
* one is. That readout is the whole point – it turns "did I mean 8×4 or 4×8" from something
* you find out by undoing into something you see before committing.
* Cuts a sheet into a grid, asked for as columns and rows rather than a cell size: the counts are
* what the user knows, and a cell size derived from the picture cannot disagree with them.
*/
export class SliceDialog extends Popup {
private readonly options: SliceOptions = { ...DEFAULT_SLICE };
@@ -60,7 +55,6 @@ export class SliceDialog extends Popup {
this.refreshSummary();
}
/** Two numeric fields on one row – the grid's parameters come in x/y pairs. */
private pair(
labelA: string,
keyA: keyof SliceOptions,
@@ -90,7 +84,7 @@ export class SliceDialog extends Popup {
const field = new NumberField(this.options[key], {
min,
step: 1,
// Counts of cells and pixel offsets; the value is rounded on change, so 0 hides nothing.
// Counts and pixel offsets, rounded on change, so 0 decimals hides nothing.
decimals: 0,
onChange: (value) => {
this.options[key] = Math.round(Number(value));
@@ -101,7 +95,6 @@ export class SliceDialog extends Popup {
return field;
}
/** Live readout of what the current numbers would do, including the refusal case. */
private refreshSummary(): void {
const rects = sliceFrames(this.asset.width, this.asset.height, this.options);
const first = rects[0];
+13 -41
View File
@@ -43,7 +43,6 @@ const MIN_LABEL_SIZE = 28;
const LABEL_INSET = 3;
const LABEL_PAD = 2;
/** A drag of an existing frame: which one, from what, and by which handle. */
interface FrameDrag {
frameId: string;
handle: FrameHandle;
@@ -52,7 +51,7 @@ interface FrameDrag {
moved: boolean;
}
/** A new frame being spanned on empty space (image pixels; nothing is in the model yet). */
/** A new frame being spanned, in image pixels; nothing is in the model yet. */
interface NewFrame {
start: Vec2;
current: Vec2;
@@ -60,15 +59,8 @@ interface NewFrame {
}
/**
* The Texture tab's canvas: one texture's picture with its frames drawn on top, where frames
* are created by dragging on empty space and moved or resized by their handles.
*
* Everything but the picture is drawn in widget-local pixels rather than in a zoomed
* container: outlines, handles and names have to keep a constant size on screen, which a
* scaled container would defeat. The picture itself is a Sprite whose scale is the zoom, so
* the GPU does the sampling – and `scaleMode` follows the texture's own Filter property, so a
* pixel-art sheet is inspected as crisp pixels rather than as the mush a smoothed preview
* would show.
* The Texture tab's canvas. Everything but the picture is drawn in widget-local pixels rather
* than in a zoomed container: outlines, handles and names must keep a constant size on screen.
*/
export class TextureCanvas extends Widget {
private readonly bg = new Graphics();
@@ -127,7 +119,7 @@ export class TextureCanvas extends Widget {
return { w: Math.max(this.minWidth, 200), h: Math.max(this.minHeight, 200) };
}
/** Which texture is shown. Switching resets the view, since the next picture is a new subject. */
/** Switching resets the view, since the next picture is a new subject. */
show(assetId: string): void {
if (this.assetId === assetId) return;
this.assetId = assetId;
@@ -139,17 +131,12 @@ export class TextureCanvas extends Widget {
return this.assetId;
}
/** Refits the picture into the widget – the "reset view" the toolbar offers. */
fit(): void {
this.needsFit = true;
this.redraw();
}
/**
* Where an image pixel currently sits on screen. Only the check scripts need this: dragging
* a frame is done in image coordinates, so a test that cannot convert them can drag
* somewhere but cannot aim at a known part of the picture.
*/
/** Where an image pixel sits on screen; only the check scripts need it, to aim drags. */
imageToScreen(x: number, y: number): Vec2 {
const local = imageToLocal(this.view, x, y);
const origin = this.getGlobalPosition();
@@ -176,9 +163,8 @@ export class TextureCanvas extends Widget {
private onPointerDown(e: FederatedPointerEvent): void {
const asset = this.asset();
// Nothing is cut from a picture whose size is unknown: `normalizeFrameRect` has no bounds
// to hold such a frame inside, so it would keep a rectangle that lies off the sheet for
// good. An unmeasured texture is a failed upload, and the hint says so.
// Without a known picture size `normalizeFrameRect` has no bounds, and would keep a frame
// that lies off the sheet.
if (!asset || asset.width <= 0 || asset.height <= 0) return;
const local = this.toLocal(e.global);
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
@@ -191,8 +177,7 @@ export class TextureCanvas extends Widget {
const point = localToImage(this.view, local.x, local.y);
const tolerance = HANDLE_SIZE / this.view.zoom;
// The selected frame is tested first and by its handles: it is the one being worked on,
// and its handles must stay reachable even where a frame drawn later overlaps them.
// The selected frame is tested first, so its handles stay reachable under a frame drawn later.
const selected = this.frame(this.selectedFrameId());
const onSelected = selected ? handleAt(selected, point, tolerance) : null;
if (selected && onSelected) {
@@ -309,7 +294,6 @@ export class TextureCanvas extends Widget {
e.preventDefault();
}
/** Announces what a press would do before it happens (resize arrows, move, crosshair). */
private updateCursor(local: Vec2): void {
const asset = this.asset();
if (!asset) {
@@ -345,7 +329,6 @@ export class TextureCanvas extends Widget {
if (asset && texture) {
this.image.texture = texture;
this.image.position.set(this.view.panX, this.view.panY);
// The sprite is drawn at the picture's own pixel size, so the zoom is the whole scale.
this.image.width = asset.width * this.view.zoom;
this.image.height = asset.height * this.view.zoom;
}
@@ -365,11 +348,7 @@ export class TextureCanvas extends Widget {
});
}
/**
* The checkerboard behind the picture. It is clipped to the picture rather than filling the
* widget: its job is to make transparent pixels of *this image* visible, and a board that
* ran past the edges would hide where the image actually ends.
*/
/** Clipped to the picture rather than filling the widget, which would hide where it ends. */
private drawChecker(asset: TextureAsset | null, hasPicture: boolean): void {
this.checker.clear();
if (!asset || !hasPicture || asset.width <= 0) return;
@@ -377,8 +356,8 @@ export class TextureCanvas extends Widget {
const w = asset.width * this.view.zoom;
const h = asset.height * this.view.zoom;
this.checker.rect(origin.x, origin.y, w, h).fill(CHECKER_DARK);
// Only the squares that land inside the widget are worth emitting; a 4000 px sheet at a
// high zoom would otherwise cost tens of thousands of rectangles per redraw.
// Only the squares inside the widget: a 4000 px sheet zoomed in would otherwise cost tens of
// thousands of rectangles per redraw.
const from = (start: number, limit: number) => Math.max(0, Math.floor((limit - start) / CHECKER_SIZE));
const x0 = from(origin.x, 0);
const y0 = from(origin.y, 0);
@@ -467,15 +446,8 @@ export class TextureCanvas extends Widget {
const x = at.x + LABEL_INSET;
const y = at.y + LABEL_INSET;
label.position.set(x + LABEL_PAD, y + LABEL_PAD);
/*
* A dark plate behind the name, not just a colour choice for the text.
*
* The name is drawn *on the user's picture*, which may be any colour – the grey used at
* first vanished on the light quadrants of a test sheet, and no fixed colour can be safe on
* an arbitrary image. The plate makes the background known, so light text on it always
* reads. Same reasoning as the scene grid, which is drawn white at a low alpha rather than
* in a theme colour so that it survives whatever the scene puts behind it.
*/
// A dark plate, not just a text colour: the name is drawn on the user's picture, where no
// fixed colour is legible against every background.
this.overlay
.roundRect(x, y, label.width + LABEL_PAD * 2, label.height + LABEL_PAD * 2, 2)
.fill({ color: 0x000000, alpha: selected ? 0.75 : 0.55 });
+8 -10
View File
@@ -11,13 +11,8 @@ import { TextureCanvas } from './TextureCanvas';
const INFO_WIDTH = 190;
/**
* The Texture tab: a picker for which texture is being worked on, the tools that act on the
* whole sheet, and the canvas the frames are drawn on.
*
* It follows the selection the way the graph editor does – selecting a texture asset (or one
* of its frames) shows it here – but it does not *force* the tab, and it keeps showing the
* last texture when the selection moves elsewhere. Otherwise clicking a sprite to check
* whether it wears the right frame would empty the tab you are working in.
* The Texture tab. It follows the selection, but keeps showing the last texture when the
* selection moves elsewhere – clicking a sprite must not empty the tab being worked in.
*/
export class TextureEditor extends VStack {
private readonly canvas: TextureCanvas;
@@ -40,8 +35,7 @@ export class TextureEditor extends VStack {
header.add(new Button('Fit', { onClick: () => this.canvas.fit() }));
header.add(new Spacer(), 1);
this.info = new Label('', { style: 'small', align: 'right' });
// Reserved rather than measured: the label is built empty, and a stack sizes it once from
// the text it had at the time – so without a floor the readout is clipped from then on.
// Reserved, not measured: a stack sizes the label once, from the empty text it was built with.
this.info.minWidth = INFO_WIDTH;
header.add(this.info);
@@ -63,7 +57,11 @@ export class TextureEditor extends VStack {
return this.canvas.currentAssetId;
}
/** Shows a texture explicitly (opening the tab for a freshly imported image). */
/** For the check scripts: the picker's width must not follow the name it happens to show. */
get pickerWidth(): number {
return this.picker.w;
}
show(assetId: string): void {
this.canvas.show(assetId);
this.refresh();
+6 -20
View File
@@ -1,8 +1,4 @@
/**
* How the Texture tab maps image pixels onto the widget, and what a drag on a frame means.
* Pure geometry – no Pixi, no editor state – so the fiddly part of the editor (which handle
* did I grab, what does dragging it do) is unit-testable instead of only clickable.
*/
/** Geometry for the Texture tab: no Pixi and no editor state, so it stays unit-testable. */
import type { FrameRect } from '../../core/model/texture';
import type { Vec2 } from '../../core/model/Properties';
@@ -27,7 +23,6 @@ export function localToImage(view: SheetView, x: number, y: number): Vec2 {
return { x: (x - view.panX) / view.zoom, y: (y - view.panY) / view.zoom };
}
/** The view that centres the whole picture in a `w`×`h` widget. */
export function fitView(imageWidth: number, imageHeight: number, w: number, h: number): SheetView {
if (imageWidth <= 0 || imageHeight <= 0 || w <= 0 || h <= 0) return { panX: 0, panY: 0, zoom: 1 };
const zoom = clampZoom(Math.min(w / imageWidth, h / imageHeight) * FIT_MARGIN || 1);
@@ -50,10 +45,7 @@ export function zoomAbout(view: SheetView, factor: number, localX: number, local
return after;
}
/**
* Which part of a frame a point grabs: a corner or edge handle (`hx`/`hy` in −1/0/1, naming
* the edge each axis is pinned to), `'move'` anywhere else inside it, or null outside.
*/
/** `hx`/`hy` are −1/0/1, naming the edge each axis is pinned to; `'move'` is anywhere inside. */
export type FrameHandle = { hx: -1 | 0 | 1; hy: -1 | 0 | 1 } | 'move';
/** The nine anchor points of a rectangle; (0, 0) is its centre and is not a handle. */
@@ -81,12 +73,8 @@ export function frameHandles(): readonly { hx: -1 | 0 | 1; hy: -1 | 0 | 1 }[] {
}
/**
* What a press at `point` (image pixels) does to `rect`. `tolerance` is the grab radius in
* image pixels – the caller converts a constant screen distance, so handles stay equally easy
* to hit at every zoom.
*
* Corners are tested before edges so that at a small frame, where all handles overlap, the
* corner wins: a corner resizes both axes and is the one worth reaching for.
* What a press at `point` does to `rect`; `tolerance` is the grab radius in image pixels.
* Corners are tested before edges, so the corner wins on a frame small enough for both to overlap.
*/
export function handleAt(rect: FrameRect, point: Vec2, tolerance: number): FrameHandle | null {
for (const handle of HANDLES) {
@@ -99,9 +87,8 @@ export function handleAt(rect: FrameRect, point: Vec2, tolerance: number): Frame
}
/**
* The rectangle a drag produces: `handle` decides which edges follow the pointer. The result
* may be inverted or stick out of the image – squaring it up is `normalizeFrameRect`'s job,
* so that a drag and a typed-in value are corrected by exactly one piece of code.
* The rectangle a drag produces. It may come out inverted or outside the image: squaring it up
* is `normalizeFrameRect`'s job, shared with typed-in values.
*/
export function dragFrame(start: FrameRect, handle: FrameHandle, dx: number, dy: number): FrameRect {
if (handle === 'move') return { ...start, x: start.x + dx, y: start.y + dy };
@@ -126,7 +113,6 @@ export function rectBetween(a: Vec2, b: Vec2): FrameRect {
return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), w: Math.abs(b.x - a.x), h: Math.abs(b.y - a.y) };
}
/** The mouse cursor a handle asks for, so a resize edge announces itself before it is grabbed. */
export function handleCursor(handle: FrameHandle): string {
if (handle === 'move') return 'move';
if (handle.hx === 0) return 'ns-resize';