Fit sprite size to the texture aspect ratio

Add ↔ next to Width and ↕ next to Height in the inspector. Each sets its
field so the sprite matches the ratio of its assigned texture or frame,
reading the frame's own rect rather than the sheet's.

The buttons reuse the existing FieldActions convention, so they simply
do not appear when no texture is assigned, when the sprite is already
proportional, or on assets whose schema has no texture property.

Comparison is a cross product with a relative tolerance: 3135 of the
40000 size pairs up to 200x200 do not survive a round trip through a
division, so an exact check would leave the button standing after its
own click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-08 16:27:38 +02:00
1 parent 4d84231b9f
commit 0b34bdd990
9 files changed
+412 -65

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import type { PropertyBag } from '../model/Properties';
import type { Project } from '../model/Project';
import { resolveTexture } from '../model/texture';
export type SizeAxis = 'width' | 'height';
export interface PixelSize {
w: number;
h: number;
}
/**
* Pixel size of the picture a sprite wears: its frame's rectangle, else the whole image.
* Null while that is not known – an upload still running has no measured size yet, and a
* ratio taken from 0 × 0 would be invented.
*/
export function texturePixelSize(project: Project, props: PropertyBag): PixelSize | null {
const ref = resolveTexture(project, props);
if (!ref) return null;
const w = ref.rect ? ref.rect.w : ref.sourceWidth;
const h = ref.rect ? ref.rect.h : ref.sourceHeight;
return w > 0 && h > 0 ? { w, h } : null;
}
/**
* What `axis` has to become for the sprite to carry the picture's proportions, leaving the
* other side where it is. Null when the other side is not a usable number.
*/
export function fitAxisToAspect(size: PixelSize, props: PropertyBag, axis: SizeAxis): number | null {
const other = Number(props[axis === 'width' ? 'height' : 'width']);
if (!Number.isFinite(other) || other <= 0) return null;
const fitted = axis === 'width' ? (other * size.w) / size.h : (other * size.h) / size.w;
return Number.isFinite(fitted) && fitted > 0 ? fitted : null;
}
/**
* Whether the sprite already has the picture's proportions. Compared as a cross product with a
* relative tolerance, because fitting one side and then asking about the other must not answer
* "no" over a rounding error: 3135 of the 40 000 sizes up to 200 × 200 fail an exact test after
* their own fit (1 × 11 at height 100 gives 9.090909090909092, whose cross product is 100.000…1).
*/
export function matchesAspect(size: PixelSize, props: PropertyBag): boolean {
const w = Number(props.width);
const h = Number(props.height);
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return false;
return Math.abs(w * size.h - h * size.w) <= 1e-9 * w * size.h;
}