diff --git a/README.md b/README.md index 0ba7befd..a869f2b2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Live: · Widget playground: isModified(asset, def.key)); +} /** * Whether a placed object deviates from whatever it follows: the container template it was diff --git a/src/editor/debugHook.ts b/src/editor/debugHook.ts index ee098a40..6602a60b 100644 --- a/src/editor/debugHook.ts +++ b/src/editor/debugHook.ts @@ -143,6 +143,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E dropPreview: () => viewport.dropInfo(), /** An asset row by id – two assets can share a name with the container picker's label. */ assetRow: (id: string) => assets.rowGlobal(id), + assetRowColor: (id: string) => assets.rowColor(id), gameInfo: () => gameView.debugInfo(), layerOf: (id: string) => viewport.layerOf(id), drawOrder: () => viewport.drawOrder(), diff --git a/src/editor/inspector/assetSection.ts b/src/editor/inspector/assetSection.ts index b4e4f738..3709c307 100644 --- a/src/editor/inspector/assetSection.ts +++ b/src/editor/inspector/assetSection.ts @@ -12,12 +12,12 @@ import { import type { Asset, BehaviorAsset } from '../../core/model/Asset'; import { findAsset, type Project } from '../../core/model/Project'; import { - COMMON_PROPS, type PropertyDef, schemaForAsset, VARIABLE_KINDS, type VariableKind, } from '../../core/properties/PropertySchema'; +import { markedAssetSchema } from '../../core/properties/deviation'; import { isModified, resolveAssetProps } from '../../core/properties/resolve'; import { Button, Dropdown, HStack, Label, Menu, Spacer, TextField, Theme } from '../../ui'; import { aspectActions, sizeCommand } from './aspectAction'; @@ -63,12 +63,8 @@ export function buildAssetSection(host: InspectorHost, assetId: string): void { row.add(host.nameField, 1); host.body.add(row); - if (asset.type === 'behavior') { - buildProps(host, asset, COMMON_PROPS); - buildVariables(host, asset); - } else { - buildProps(host, asset, schemaForAsset(asset)); - } + buildProps(host, asset, markedAssetSchema(asset)); + if (asset.type === 'behavior') buildVariables(host, asset); } /** Pushes the current asset values into the rows. False when the asset is gone (rebuild). */ diff --git a/src/editor/panels/AssetPanel.ts b/src/editor/panels/AssetPanel.ts index 1f36eb2d..339b339e 100644 --- a/src/editor/panels/AssetPanel.ts +++ b/src/editor/panels/AssetPanel.ts @@ -8,13 +8,19 @@ import { isPlaceableType, } from '../../core/model/AssetTypes'; import { findAsset } from '../../core/model/Project'; +import { assetDeviates } from '../../core/properties/deviation'; import type { TextureStore } from '../../core/store/TextureStore'; import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui'; import { confirmDeleteAsset } from '../actions'; import type { EditorState } from '../state'; import { pickAndImport } from '../textureImport'; -/** Lists all assets of the project; creates and deletes them. Placing is a drag into a view. */ +/** + * Lists all assets of the project; creates and deletes them. Placing is a drag into a view. + * + * A row's name turns gold when the asset carries a property the inspector marks – the same rule, + * so the list cannot promise a deviation the inspector then fails to show. + */ export class AssetPanel extends Panel { private readonly list = new ListView(); private readonly scroll = new ScrollContainer(); @@ -59,6 +65,7 @@ export class AssetPanel extends Panel { label: asset.name, hint: asset.type === 'scene' && asset.id === active ? 'scene ●' : asset.type, swatch: hasColorSwatch(asset.type) ? Number(asset.props.color ?? 0) : undefined, + overridden: assetDeviates(asset), })), ); this.syncSelection(); @@ -98,6 +105,11 @@ export class AssetPanel extends Panel { return this.list.rowGlobal(assetId); } + /** Label colour of a row as `#rrggbb` – gold means the asset deviates from the defaults (test hook). */ + rowColor(assetId: string): string | null { + return this.list.rowColor(assetId); + } + /** Hands the asset to the viewport under the pointer; a scene has nowhere to be placed into. */ private startPlacementDrag(assetId: string): void { const asset = findAsset(this.state.project, assetId); diff --git a/tests/core/deviation.test.ts b/tests/core/deviation.test.ts index 2fa87b67..0147dad6 100644 --- a/tests/core/deviation.test.ts +++ b/tests/core/deviation.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from 'vitest'; +import { + CreateAssetCommand, + ResetAssetPropertyCommand, + SetAssetPropertyCommand, +} from '../../src/core/commands/assetCommands'; import { CreateContainerCommand } from '../../src/core/commands/containerCommands'; +import { AddVariableCommand } from '../../src/core/commands/variableCommands'; import { ClearOverrideCommand, SetOverrideCommand, @@ -8,8 +14,8 @@ import { import { instanceChildId } from '../../src/core/model/instance'; import { findAsset } from '../../src/core/model/Project'; import type { ContainerAsset } from '../../src/core/model/Asset'; -import { deviationMarks, objectDeviates } from '../../src/core/properties/deviation'; -import { makeSpriteScene, place } from '../helpers'; +import { assetDeviates, deviationMarks, objectDeviates } from '../../src/core/properties/deviation'; +import { makeProject, makeSpriteScene, place } from '../helpers'; describe('deviation marks', () => { it('marks an object only once an override really deviates', () => { @@ -48,3 +54,37 @@ describe('deviation marks', () => { expect([...deviationMarks(f.project, f.sceneOf())].sort()).toEqual([content, create.objectId].sort()); }); }); + +describe('asset deviation', () => { + const makeAsset = (type: 'sprite' | 'behavior', name: string) => { + const f = makeProject(); + const create = new CreateAssetCommand(type, name); + f.stack.execute(create); + const asset = () => findAsset(f.project, create.asset.id)!; + expect(asset().type).toBe(type); + return { ...f, asset, assetId: create.asset.id }; + }; + + it('marks an asset only once a property really differs from its default', () => { + const f = makeAsset('sprite', 'Box'); + expect(assetDeviates(f.asset())).toBe(false); + // Written, but the default value – the inspector shows no gold, so neither does the list. + f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'width', 100)); + expect(f.asset().props.width).toBe(100); + expect(assetDeviates(f.asset())).toBe(false); + f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'width', 250)); + expect(assetDeviates(f.asset())).toBe(true); + f.stack.execute(new ResetAssetPropertyCommand(f.assetId, 'width')); + expect(assetDeviates(f.asset())).toBe(false); + }); + + it('leaves a behavior unmarked for its variables, which the inspector does not mark either', () => { + const f = makeAsset('behavior', 'Mover'); + f.stack.execute(new AddVariableCommand(f.assetId, 'speed', 'number')); + f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'speed', 42)); + expect(f.asset().props.speed).toBe(42); + expect(assetDeviates(f.asset())).toBe(false); + f.stack.execute(new SetAssetPropertyCommand(f.assetId, 'enabled', false)); + expect(assetDeviates(f.asset())).toBe(true); + }); +}); diff --git a/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md b/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md new file mode 100644 index 00000000..dbff8f01 --- /dev/null +++ b/todo-notes/done/overwrite-textfarbe-auch-bei-assets.md @@ -0,0 +1,37 @@ +die textfarbe beim asset soll auch in diesem gold dargestellt werden, falls ein property des assets einen override hat. +ist somit relativ simpel: im prinzip genau das checken was im inspector erscheint wenn man das asset auswählt => gibt es wo einen override (gold) dann auch den assetname links im panel gold darstellen. + +--- + +**Umgesetzt (2026-09-09).** Der Name einer Asset-Zeile im Assets-Panel ist gold, solange das +Asset irgendeine Property traegt, die der Inspector gold malen wuerde — dieselbe Farbe und +dasselbe Kriterium wie in der Hierarchie. + +Die Regel steht als reine Funktion in `src/core/properties/deviation.ts`, neben dem schon +vorhandenen `objectDeviates`: + +- `markedAssetSchema(asset)` — die Zeilen, die der Asset-Inspector als "modified" markiert. +- `assetDeviates(asset)` — ob davon eine von ihrem Schema-Default abweicht (`isModified`). + +**Panel und Inspector benutzen dieselbe Liste**, statt die Bedingung zweimal zu formulieren: +`buildAssetSection` baut seine Property-Zeilen jetzt aus `markedAssetSchema(asset)` (das ersetzt +die Fallunterscheidung Behavior/Rest, die dort vorher stand). Damit kann die Liste keine +Abweichung behaupten, die der Inspector danach nicht zeigt. + +**Ein Sonderfall, der eine Entscheidung brauchte: die Variablen eines Behaviors.** Sie sind Teil +von `schemaForAsset`, aber der Inspector markiert sie bewusst nie (`setModified` ist dort ein +No-op) — ihr Default ist auf der Variablen selbst deklariert, der Asset-Wert *ist* dieser +Default. Ein Behavior mit `speed = 200` ist deshalb nicht gold. Sonst waere im E2E-Fixture der +"Mover" gold gewesen, ohne dass der Inspector irgendwo etwas markiert haette — genau der +Widerspruch, den die Notiz ausschliesst. + +**Belegt** (E2E-Modul 04, sechs neue Checks ueber den Test-Hook `__gm.assetRowColor`, der die +tatsaechliche Label-Farbe der Zeile liest): + +- Crate (Hoehe 60 statt 100, eigene Farbe) → `#f2c84b`, Main camera (alles auf Default) → `#e6e8ee` +- Mover (Variable `speed` = 200 gegen deklarierten Default 0) → `#e6e8ee`, wie der Inspector +- ein ↺ zurueckgesetzt → bleibt gold, weil die zweite Abweichung noch steht +- die zweite auch zurueckgesetzt → wird schlicht, **ohne Reload**; Undo bringt die Marke zurueck + +Screenshot `15-asset-reset`: im Panel sind "Level 1" (Hintergrundfarbe), "Crate" und +"Crate collider" gold, "Main camera", "Mover", "Spawner" und das frisch angelegte "Sprite" weiss. diff --git a/todo-notes/overwrite-textfarbe-auch-bei-assets.md b/todo-notes/overwrite-textfarbe-auch-bei-assets.md deleted file mode 100644 index 5ec01b89..00000000 --- a/todo-notes/overwrite-textfarbe-auch-bei-assets.md +++ /dev/null @@ -1 +0,0 @@ -die textfarbe beim asset soll auch in diesem gold dargestellt werden, falls ein property des assets einen override hat diff --git a/todo-notes/unreferenced-image-files.md b/todo-notes/unreferenced-image-files.md new file mode 100644 index 00000000..42fdfe22 --- /dev/null +++ b/todo-notes/unreferenced-image-files.md @@ -0,0 +1,14 @@ +Zitat aus der readme: "Unreferenced image files are never collected." + +Ich würde sagen dass soll vorerst auch so bleiben. + +Was passt: + drag and drop file in den editor => textur wird hochgeladen und passende asset wird erstellt => das soll so bleiben, das passt + +Was wir auch haben aber ändern: + klickt man auf das `+` => `Texture`, dann öffnet sich der File System Browser. + +Anstelle des file system browser => ein dialog mit allen images die am server existieren. + - auswählen => ok, dann wird es wiederverwendet => dialog schließt sich => texture asset mit ausgewählten image wird erstellt. + - oder hochladen button => file system browser => OK => dialog schließt sich => texture asset mit hochgeladenen image wird erstellt (im prinzip das was wir jetzt auch haben) + \ No newline at end of file diff --git a/tools/e2e/04-assets.mjs b/tools/e2e/04-assets.mjs index dbab9e63..b69bc050 100644 --- a/tools/e2e/04-assets.mjs +++ b/tools/e2e/04-assets.mjs @@ -143,4 +143,42 @@ export async function assets(t) { (await evaluate(`__gm.locate('↺')`))?.y === heightReset.y, JSON.stringify(await evaluate(`__gm.locate('↺')`)), ); + + // 12b. The same gold in the asset list: a row is marked while the asset carries anything the + // inspector marks. The Crate deviates twice, so it takes two resets to turn it plain. + const GOLD = '#f2c84b'; + const PLAIN = '#e6e8ee'; + const assetColor = (id) => json(`__gm.assetRowColor('${id}')`); + check('a deviating asset is marked gold in the list', (await assetColor('crate')) === GOLD); + check('an asset at its defaults is not', (await assetColor('cam')) === PLAIN, await assetColor('cam')); + // The Mover's `speed` is 200 against a declared variable default of 0. The inspector marks no + // variable, so neither may the list – the two must say the same thing. + check( + 'a behavior is not marked for a variable value, which the inspector does not mark either', + (await assetColor('mover')) === PLAIN, + await assetColor('mover'), + ); + await click(heightReset.x, heightReset.y); + await sleep(150); + check( + 'the mark stays while the second deviation is left', + (await assetColor('crate')) === GOLD, + await assetColor('crate'), + ); + const colorOnly = await evaluate(`__gm.locate('↺')`); + await click(colorOnly.x, colorOnly.y); + await sleep(150); + check( + 'resetting the last deviation clears the mark, without a reload', + (await assetColor('crate')) === PLAIN, + `${await assetColor('crate')} props=${JSON.stringify((await asset('crate')).props)}`, + ); + await shot('15b-asset-marks'); + await undo(); + await undo(); + check( + 'undo brings the mark back', + (await assetColor('crate')) === GOLD, + `${await assetColor('crate')} props=${JSON.stringify((await asset('crate')).props)}`, + ); }