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

+16 -56
View File
@@ -1,8 +1,3 @@
/**
* Getting a picture from the user's machine into the project: pick, drop or paste a file,
* shrink it if it is too big to send, upload the bytes, add a texture asset. The three ways in
* share every step after the file – so a dropped and a picked image behave identically.
*/
import { CreateTextureCommand } from '../core/commands/assetCommands';
import { nextAssetName } from '../core/commands/naming';
import type { TextureAsset } from '../core/model/Asset';
@@ -12,16 +7,10 @@ import type { EditorState } from './state';
/** What the picker offers and a drop accepts; the server rejects everything else anyway. */
export const IMAGE_ACCEPT = 'image/png,image/jpeg,image/webp,image/gif';
/**
* Longest side a stored texture may have. A 4000×3000 phone photo has no business being a
* sprite texture: it costs 48 MB of video memory to draw something a fraction of that size.
*/
/** Longest side a stored texture may have: a 4000×3000 photo costs 48 MB of video memory. */
export const MAX_TEXTURE_SIDE = 2048;
/**
* Byte budget for one upload. The API would take 4 MB, but nginx cuts every request off at
* 1 MB and answers with its own HTML page, so nginx is the wall we have to stay under.
*/
/** Byte budget for one upload: nginx cuts every request off at 1 MB, below the API's own 4 MB. */
export const MAX_UPLOAD_BYTES = 950 * 1024;
/** Tried in order when an image has to be re-encoded; the first result that fits wins. */
@@ -30,9 +19,8 @@ const WEBP_QUALITIES = [0.9, 0.75, 0.6] as const;
export type ImagePlan = { kind: 'keep' } | { kind: 'reencode'; width: number; height: number };
/**
* How to get an image within both limits. Re-encoding is a last resort – it would turn crisp
* pixel art into mush – so anything that is already small enough in both senses is sent byte
* for byte as it lies on disk. `bytes` is the encoded file size, not the pixel count.
* Re-encoding would turn crisp pixel art into mush, so an image within both limits is kept byte
* for byte. `bytes` is the encoded file size, not the pixel count.
*/
export function planImageFit(width: number, height: number, bytes: number): ImagePlan {
const scale = Math.min(1, MAX_TEXTURE_SIDE / Math.max(width, height));
@@ -44,11 +32,7 @@ export function planImageFit(width: number, height: number, bytes: number): Imag
};
}
/**
* Encodes at decreasing quality and stops at the first result within `MAX_UPLOAD_BYTES`. When
* none fits, the last (lowest quality) attempt comes back, so the caller can report how far
* off it was. `encode` is a parameter to keep the ladder testable without a canvas.
*/
/** Encodes at decreasing quality; when none fits the budget, the last attempt comes back anyway. */
export async function encodeWithinBudget(encode: (quality: number) => Promise<Blob>): Promise<Blob> {
let result = await encode(WEBP_QUALITIES[0]);
for (const quality of WEBP_QUALITIES.slice(1)) {
@@ -66,11 +50,7 @@ export interface PreparedImage {
height: number;
}
/**
* The bytes to upload for one picked file: the file itself whenever it already fits (see
* `planImageFit`), otherwise a downscaled WebP. Rejects when the file is not a decodable image
* or stays too large even at the lowest quality.
*/
/** The bytes to upload: the file itself when it already fits, otherwise a downscaled WebP. */
export async function prepareImage(file: File): Promise<PreparedImage> {
let bitmap: ImageBitmap;
try {
@@ -81,7 +61,6 @@ export async function prepareImage(file: File): Promise<PreparedImage> {
try {
const plan = planImageFit(bitmap.width, bitmap.height, file.size);
if (plan.kind === 'keep') {
// Untouched on purpose: re-encoding costs quality and would break pixel art.
return {
blob: file,
type: file.type || 'application/octet-stream',
@@ -89,8 +68,7 @@ export async function prepareImage(file: File): Promise<PreparedImage> {
height: bitmap.height,
};
}
// Only the first frame of an animated GIF survives this, which is all the renderer ever
// draws – a texture is one still picture.
// Only the first frame of an animated GIF survives this.
const canvas = new OffscreenCanvas(plan.width, plan.height);
const context = canvas.getContext('2d');
if (!context) throw new Error('This browser cannot resize images.');
@@ -122,10 +100,8 @@ export interface ImportOutcome {
}
/**
* Imports the files one after another – prepare, upload, create the asset – selects the last
* one that worked and sums the result up as a notice. Sequential on purpose: each asset is
* named against the project as it stands *after* the previous one. A file that fails is
* reported and the remaining ones are still imported.
* Imports the files sequentially, so each asset is named against the project as the previous one
* left it. A file that fails is reported in the notice; the remaining ones are still imported.
*/
export async function importImages(
files: readonly File[],
@@ -147,11 +123,8 @@ async function importImage(file: File, deps: TextureImportDeps): Promise<ImportO
const image = await prepareImage(file);
const uploaded = await deps.textures.upload(image.blob, image.type);
// The server stores by content hash, so importing a picture the project already has gives
// back the file it already references. Making a second asset for it would be clutter with a
// trap in it: two assets on one file can disagree about `filter`, while the scale mode
// belongs to the single shared source. Reuse instead – and say so, rather than silently
// doing nothing visible.
// The server stores by content hash, so a re-imported picture comes back as the file the
// project already references; two assets on one file would disagree about `filter`.
const existing = deps.state.project.assets.find(
(asset): asset is TextureAsset => asset.type === 'texture' && asset.file === uploaded.file,
);
@@ -169,11 +142,7 @@ async function importImage(file: File, deps: TextureImportDeps): Promise<ImportO
}
}
/**
* One line for the whole batch: how many arrived, how many were already there, and why the
* first casualty did not. Reuse gets its own words – "Imported 1 image" would be a lie about a
* picture the project already had, and the user would look for a new asset that never appeared.
*/
/** One line for the whole batch; reuse is worded apart, since "Imported 1 image" would be a lie. */
export function importNotice(outcomes: readonly ImportOutcome[]): string {
const added = outcomes.filter((outcome) => outcome.assetId !== null && !outcome.reused).length;
const reused = outcomes.filter((outcome) => outcome.reused).length;
@@ -195,7 +164,6 @@ export function importNotice(outcomes: readonly ImportOutcome[]): string {
return `Imported ${added + reused} of ${outcomes.length}.${known} ${problem}`;
}
/** "player_ship.png" → "player_ship"; a pasted screenshot arrives as "image.png". */
function assetNameFor(file: File): string {
return file.name.replace(/\.[^.]+$/, '').trim() || 'Texture';
}
@@ -206,10 +174,7 @@ function formatBytes(bytes: number): string {
// --- ways in ---------------------------------------------------------------------------------
/**
* Opens the browser's file dialog and resolves with what the user chose – empty when the
* dialog was cancelled. The `<input>` lives only for this one call.
*/
/** Opens the browser's file dialog; resolves empty when the dialog was cancelled. */
export function pickImageFiles(): Promise<File[]> {
return new Promise((resolve) => {
const input = document.createElement('input');
@@ -223,13 +188,12 @@ export function pickImageFiles(): Promise<File[]> {
resolve(files);
};
input.addEventListener('change', () => finish([...(input.files ?? [])]));
// Without `cancel` a dismissed dialog would leave both the element and this promise behind.
// Without `cancel` a dismissed dialog leaves both the element and this promise behind.
input.addEventListener('cancel', () => finish([]));
input.click();
});
}
/** The `+` menu's Texture entry: pick files and import them, reporting anything that goes wrong. */
export function pickAndImport(deps: TextureImportDeps): void {
void pickImageFiles()
.then((files) => importImages(files, deps))
@@ -237,12 +201,8 @@ export function pickAndImport(deps: TextureImportDeps): void {
}
/**
* Wires the two ways of importing an image that need no menu: dropping files onto `target`
* (the Pixi canvas) and pasting one from the clipboard. Returns the function that removes both
* again.
*
* Paste is bound to the window because the canvas cannot hold focus, and is ignored while a
* DOM text field has it (the `KeyboardBridge` input): Ctrl+V belongs to the text being edited.
* Wires drop-onto-`target` and paste, and returns the remover for both. Paste is bound to the
* window because the canvas cannot hold focus, and skipped while a DOM text field has it.
*/
export function installTextureImport(target: HTMLElement, deps: TextureImportDeps): () => void {
const onDragOver = (event: DragEvent) => {