Files
feuerwerk/tests/core/resolve.test.ts
T
rootandClaude Opus 5 5a6af74e44 Add textures for sprites
Images are uploaded to a content-addressed blob endpoint and stored as
data/textures/<sha256>.<ext>; the project only keeps the file name. The
sprite points at a texture asset through an ordinary property of the new
kind `asset`, so instance overrides, "Apply to container", "Reset to
asset", undo and `Set property` work without any extra code. The new kind
also replaces the ad-hoc asset picker the graph nodes faked as an enum.

Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V.
Images wider than 2048px are downscaled client-side and re-encoded to
WebP if needed, because nginx caps uploads at 1MB; an image that already
fits is uploaded byte for byte so pixel art is never resampled.

The sprite colour default changes from red to white, since colour tints
the texture multiplicatively and a tint of white shows an image as it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:43:08 +02:00

130 lines
4.8 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.
import { describe, expect, it } from 'vitest';
import { buildAsset } from '../../src/core/commands/assetCommands';
import { createSceneObject } from '../../src/core/model/SceneObject';
import { coerceValue, defaultsFor, schemaFor } from '../../src/core/properties/PropertySchema';
import {
deviatesFromAsset,
deviatesFromTemplate,
inheritedValue,
isModified,
isOverridden,
resolveAssetProps,
resolveSceneObjectProps,
} from '../../src/core/properties/resolve';
describe('PropertySchema', () => {
it('has no position on any asset type', () => {
for (const type of ['scene', 'camera', 'sprite'] as const) {
const keys = schemaFor(type).map((d) => d.key);
expect(keys).not.toContain('x');
expect(keys).not.toContain('y');
expect(keys).not.toContain('position');
}
});
it('provides defaults for every key', () => {
expect(defaultsFor('sprite')).toEqual({
enabled: true,
width: 100,
height: 100,
color: 0xffffff,
texture: '',
});
});
it('coerces values against the definition', () => {
const width = schemaFor('sprite').find((d) => d.key === 'width')!;
expect(coerceValue(width, -5)).toBe(1);
expect(coerceValue(width, '42')).toBe(42);
expect(coerceValue(width, 'abc')).toBe(100);
const color = schemaFor('sprite').find((d) => d.key === 'color')!;
expect(coerceValue(color, '#ff0000')).toBe(0xff0000);
expect(coerceValue(color, 0x1ffffff)).toBe(0xffffff);
});
});
describe('resolve', () => {
it('fills asset props with defaults and ignores unknown keys', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props = { width: 50, bogus: 1 };
expect(resolveAssetProps(sprite)).toEqual({
enabled: true,
width: 50,
height: 100,
color: 0xffffff,
texture: '',
});
});
it('applies sparse overrides on top of asset values', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.color = 0x00ff00;
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(resolveSceneObjectProps(sprite, object)).toEqual({
enabled: true,
width: 100,
height: 100,
color: 0x00ff00,
texture: '',
});
object.overrides.color = 0x0000ff;
expect(resolveSceneObjectProps(sprite, object).color).toBe(0x0000ff);
expect(isOverridden(object, 'color')).toBe(true);
expect(isOverridden(object, 'width')).toBe(false);
expect(inheritedValue(sprite, 'color')).toBe(0x00ff00);
});
it('detects asset values that differ from the schema default', () => {
const sprite = buildAsset('sprite', 'Box');
expect(isModified(sprite, 'width')).toBe(false); // explicit default counts as unmodified
sprite.props.width = 250;
expect(isModified(sprite, 'width')).toBe(true);
delete sprite.props.height; // missing → resolved from default → unmodified
expect(isModified(sprite, 'height')).toBe(false);
expect(isModified(sprite, 'bogus')).toBe(false);
});
it('ignores an override that carries the asset value – nothing to reset or apply', () => {
const sprite = buildAsset('sprite', 'Box');
sprite.props.width = 250;
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false); // no override at all
object.overrides.width = 250; // same value as the asset → not a deviation
expect(isOverridden(object, 'width')).toBe(true);
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false);
object.overrides.width = 300;
expect(deviatesFromAsset(object, sprite, 'width')).toBe(true);
// The asset moving to the object's value settles the difference again.
sprite.props.width = 300;
expect(deviatesFromAsset(object, sprite, 'width')).toBe(false);
});
it('compares an instance against its template as overrides, not as values', () => {
const sprite = buildAsset('sprite', 'Box');
const template = createSceneObject('t1', sprite.id, { x: 0, y: 0 });
const object = createSceneObject('o1', sprite.id, { x: 0, y: 0 });
expect(deviatesFromTemplate(object, template, 'width')).toBe(false); // neither overrides
template.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
object.overrides.width = 140;
expect(deviatesFromTemplate(object, template, 'width')).toBe(false);
object.overrides.width = 77;
expect(deviatesFromTemplate(object, template, 'width')).toBe(true);
});
it('keeps position on the scene object only', () => {
const sprite = buildAsset('sprite', 'Box');
const object = createSceneObject('o1', sprite.id, { x: 10, y: 20 });
expect(object.position).toEqual({ x: 10, y: 20, z: 0 });
expect('position' in sprite).toBe(false);
expect(resolveSceneObjectProps(sprite, object)).not.toHaveProperty('x');
});
});