Mark overrides in gold instead of bold, and up the hierarchy
An inspector label whose value deviates from its base was drawn bold, which is easy to miss next to the regular labels and says nothing in the hierarchy, where a folded subtree could hide a change entirely. The colour now lives as a theme token (`override`, #f2c84b) used by both the inspector labels and the hierarchy rows. Whether a row deviates is decided by `src/core/properties/deviation.ts`: `objectDeviates` compares an object against what it follows -- the container template for content, the asset otherwise -- and `deviationMarks` carries the mark up to every ancestor, so a folded subtree still shows that something inside was changed. `deviationMarks` takes the stored holder, not the resolved one: in the resolved view container content looks like an ordinary object and `instanceTargetOf` would compare against the asset, marking every instance. Covered by 3 unit tests and 5 e2e checks, including one that strips an object's own overrides to prove the row stays gold for a deviating child and restores them byte for byte. Full suite remote on the RTX 4070: 275 checks, all passed.
This commit is contained in:
1 parent
37e1450fca
commit
b87fcdfbad
13 files changed
+197
-14
No files matched your search
@@ -19,9 +19,9 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
|
||||
| **Selection** | One or more scene objects. Click selects, `Ctrl`-click adds/removes, dragging on empty scene space draws a rubber band (objects whose position lies inside are selected). Dragging a selected object moves the whole selection; `Delete` removes it; the inspector offers **Create container** and **Remove** for several objects. |
|
||||
| **Behavior** | Logic as an asset: a **node graph** plus the **variables** it declares. Placed like anything else (a diamond symbol in the scene); its parent is the **owner** the graph acts on. |
|
||||
| **Collider** | A shape (box or circle) for collision detection, placed as a **child** of the object it belongs to (its *body*). Drawn as a green outline in edit mode only. |
|
||||
| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector marks them bold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. |
|
||||
| **Default** | Every property has a schema default. Asset values that differ from it are marked bold in the inspector and can be reset (↺) the same way an override can. |
|
||||
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is always the **template**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Bold marks the same thing the buttons offer. |
|
||||
| **Override** | A per-object value that replaces the asset's value for one property. Only changed keys are stored; the inspector paints their label gold and offers a reset (↺) and an **apply** (↑) that writes the value into the asset instead. Both appear only where the value really differs from its base – an override carrying the asset's own value is none. |
|
||||
| **Default** | Every property has a schema default. Asset values that differ from it are marked gold in the inspector and can be reset (↺) the same way an override can. |
|
||||
| **Instance** | An object resolved from a placed container's template. Its **values** can be edited – position, name, property overrides – and only what actually deviates is stored, as a sparse `instanceEdits` entry on the container object keyed by the template path. Everything else keeps following the template. Its **structure** cannot: it can not be removed, re-parented or given a collider, and nothing of the scene can be parented into it. ↺ ▾ resets to the nearest base, which inside a container is always the **template**; ↑ ▾ offers **Apply to container** (write the value into the template, where every other instance still at the old value picks it up) and/or **Apply to asset** (which drops the template's override as well). With one base left the menu collapses to a plain ↺ / ↑, with none the buttons disappear. Gold marks the same thing the buttons offer. |
|
||||
| **Unpack** | Writes a placed container's content into the scene as ordinary objects (hierarchy context menu, or the inspector's *Content* section). The ids survive it, so the selection does; from then on the structure is editable and the objects no longer follow the template. |
|
||||
| **Edit mode** | The Scene tab draws the project model; panels, grid, gizmos and drag-to-move are active. |
|
||||
| **Play mode** | The editor switches to a **runtime copy** of the project (same ids). A `World` is instantiated from its active scene (one entity per scene object, resolved properties as mutable state) and shown in the **Game** tab. Systems tick it and mirror their changes into the copy; every panel stays open and edits the copy, which the world picks up immediately. **Stop discards world and copy** – the saved model is never touched. |
|
||||
@@ -37,7 +37,7 @@ everything that happens is defined by behavior graphs.
|
||||
Scene tab shows the live positions (symbols, gizmos and dragging included), the hierarchy and
|
||||
inspector show and edit the running objects, the graph can be changed, undo/redo work on the
|
||||
play session's own history. All of it happens in the runtime copy: values a behavior changes
|
||||
appear as overrides (bold, ↺ resets to the asset value), objects a graph destroys vanish from
|
||||
appear as overrides (gold, ↺ resets to the asset value), objects a graph destroys vanish from
|
||||
the lists, objects a graph spawns (`Spawn` node) appear in them – selectable, editable, still
|
||||
only in the runtime copy –, and Esc/Stop brings back exactly what was saved. Autosave pauses
|
||||
while playing.
|
||||
@@ -128,6 +128,10 @@ an outline – orders the object among *that* row's siblings rather than putting
|
||||
re-parents it on the way when the gap belongs to another parent. Since equal `z` falls back to
|
||||
this order, that is also how two objects at the same depth are put in front of each other. A drag
|
||||
that lands where it started changes nothing and costs no undo step.
|
||||
A row's **name turns gold** when the object deviates from what it follows – its container
|
||||
template, or its asset – and also when anything below it does, so a folded subtree still shows
|
||||
that something inside was changed. It is the same gold the inspector paints an overridden
|
||||
label in.
|
||||
Rows with children **fold**: click the chevron (or press ←/→, or double-click the row) to open
|
||||
and close a subtree, right-click → **Expand all** / **Collapse all** for the whole scene. A
|
||||
scene opens fully collapsed – which rows are open is view state of the panel, never stored in
|
||||
@@ -404,7 +408,7 @@ npm run format # Prettier over src, tests and tools
|
||||
npm run check # typecheck + lint + format:check + tests, what CI would run
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
export GM_REMOTE=user@host # where the browser runs; see below
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 270 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 275 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs # 24 checks: the example game, played end to end
|
||||
```
|
||||
|
||||
@@ -420,7 +424,7 @@ all, so the app never renders.)
|
||||
over SSH and forwards its CDP port back to `127.0.0.1`, so the harness itself is unchanged. The
|
||||
checks load the public base URL, so the remote side needs only a browser and internet: no repo, no
|
||||
node. Setup is an SSH key (`GM_REMOTE_KEY`, default `~/.ssh/gm-win.key`). Measured against a Windows
|
||||
desktop with an RTX 4070: **270 checks in 128 s and 24 smoke checks in 16 s, with this machine idle**
|
||||
desktop with an RTX 4070: **the full suite in ~130 s and 24 smoke checks in 16 s, with this machine idle**
|
||||
(0.4 s of CPU for the whole smoke run, load average 0.04). **That is how the suite is meant to be
|
||||
run here.** A local run works and costs 124 s, but it pins the development box – set `GM_REMOTE` and
|
||||
leave it set; without a reachable host, do not fall back to local, fix the host.
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ancestorIdsOf, type ObjectTree } from '../model/hierarchy';
|
||||
import { instanceTargetOf, isEmptyEdit, resolveObjects } from '../model/instance';
|
||||
import { findAsset, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { schemaForAsset } from './PropertySchema';
|
||||
import { deviatesFromAsset } from './resolve';
|
||||
|
||||
/**
|
||||
* Whether a placed object deviates from whatever it follows: the container template it was
|
||||
* resolved from, or its asset. `holder` must be the stored holder – what an object follows is
|
||||
* decided by where it lives, and in a resolved view everything looks stored.
|
||||
*/
|
||||
export function objectDeviates(project: Project, holder: ObjectTree, object: SceneObject): boolean {
|
||||
const target = instanceTargetOf(project, holder, object.id);
|
||||
if (target) {
|
||||
// Instance edits are pruned to what really deviates, so their presence is the answer.
|
||||
const edit = target.instance.instanceEdits[target.path];
|
||||
return edit !== undefined && !isEmptyEdit(edit);
|
||||
}
|
||||
if (object.editorOffset.x !== 0 || object.editorOffset.y !== 0) return true;
|
||||
const asset = findAsset(project, object.assetId);
|
||||
return (
|
||||
asset !== undefined && schemaForAsset(asset).some((def) => deviatesFromAsset(object, asset, def.key))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids the hierarchy marks: everything deviating itself, plus every ancestor of one. The ancestors
|
||||
* matter because subtrees fold – without them a collapsed row would hide that something below it
|
||||
* was changed.
|
||||
*/
|
||||
export function deviationMarks(project: Project, holder: ObjectTree): Set<string> {
|
||||
const view: ObjectTree = { objects: resolveObjects(project, holder) };
|
||||
const marks = new Set<string>();
|
||||
for (const object of view.objects) {
|
||||
if (!objectDeviates(project, holder, object)) continue;
|
||||
marks.add(object.id);
|
||||
for (const ancestorId of ancestorIdsOf(view, object.id)) marks.add(ancestorId);
|
||||
}
|
||||
return marks;
|
||||
}
|
||||
+17
-1
@@ -1,4 +1,4 @@
|
||||
import { type Application, type Container, Rectangle, Text } from 'pixi.js';
|
||||
import { type Application, Color, type ColorSource, type Container, Rectangle, Text } from 'pixi.js';
|
||||
import { resolveObjects } from '../core/model/instance';
|
||||
import { findObjectHolder } from '../core/model/Project';
|
||||
import { Button, ui, Widget } from '../ui';
|
||||
@@ -34,6 +34,20 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
const p = node.getGlobalPosition();
|
||||
return { x: p.x + node.width / 2, y: p.y + node.height / 2 };
|
||||
};
|
||||
/** Fill colour of a visible text, as `#rrggbb` – what tells an override label from a plain one. */
|
||||
const textColor = (text: string): string | null => {
|
||||
let found: string | null = null;
|
||||
const walk = (node: Container): void => {
|
||||
if (found !== null || !node.visible) return;
|
||||
if (node instanceof Text && node.text === text) {
|
||||
found = new Color(node.style.fill as ColorSource).toHex();
|
||||
return;
|
||||
}
|
||||
for (const child of node.children) walk(child);
|
||||
};
|
||||
walk(app.stage);
|
||||
return found;
|
||||
};
|
||||
/**
|
||||
* The visible text of the overlay layer, in drawing order – everything an open popup shows.
|
||||
* Menus hide filtered-out entries, so this is also what a search box currently leaves.
|
||||
@@ -118,6 +132,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
objectsOf,
|
||||
sceneObjects: () => (state.activeScene ? objectsOf(state.activeScene.id) : []),
|
||||
locate,
|
||||
textColor,
|
||||
overlayTexts,
|
||||
overlayRect,
|
||||
textOverflows,
|
||||
@@ -159,6 +174,7 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
/** Rows the hierarchy shows – collapsed subtrees are not in here. */
|
||||
hierarchyOrder: () => hierarchy.rowOrder(),
|
||||
hierarchyChevron: (id: string) => hierarchy.chevronPosition(id),
|
||||
hierarchyRowColor: (id: string) => hierarchy.rowColor(id),
|
||||
hierarchyExpandAll: () => hierarchy.expandAll(),
|
||||
hierarchyCollapseAll: () => hierarchy.collapseAll(),
|
||||
/** The colour actually on screen at a point, as `#rrggbb` – what was drawn, not what the model says. */
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface FieldRow {
|
||||
/** Push a model value into the widget (no events fired). */
|
||||
setValue(value: PropertyValue): void;
|
||||
/**
|
||||
* Highlight the row as deviating from its base value (an override on scene objects, a
|
||||
* Colour the label as deviating from its base value (an override on scene objects, a
|
||||
* non-default value on assets) and re-evaluate which actions currently apply.
|
||||
*/
|
||||
setModified(modified: boolean): void;
|
||||
@@ -154,7 +154,7 @@ export function createFieldRow(
|
||||
row,
|
||||
setValue: field.setValue,
|
||||
setModified(modified) {
|
||||
label.setStyle(modified ? 'bodyBold' : 'muted');
|
||||
label.setStyle(modified ? 'override' : 'muted');
|
||||
refreshActions();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -474,7 +474,7 @@ function parentRow(host: InspectorHost, holder: ObjectHolder, object: SceneObjec
|
||||
}
|
||||
|
||||
/**
|
||||
* Bold marks exactly what the ↺/↑ buttons offer: for content of a placed container a deviation
|
||||
* Gold marks exactly what the ↺/↑ buttons offer: for content of a placed container a deviation
|
||||
* from the template (position per axis, overrides), for any other object one from the asset. The
|
||||
* editor offset has one base only – no offset at all.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import { hasColorSwatch } from '../../core/model/AssetTypes';
|
||||
import { ancestorIdsOf, childIdsOf, findObject, visibleTreeOrder } from '../../core/model/hierarchy';
|
||||
import { isLiveInstance, isTemplateContent, resolvedHolder } from '../../core/model/instance';
|
||||
import { findAsset, type ObjectHolder } from '../../core/model/Project';
|
||||
import { deviationMarks } from '../../core/properties/deviation';
|
||||
import { resolveSceneObjectProps } from '../../core/properties/resolve';
|
||||
import { type DropSpot, ListView, Menu, Panel, ScrollContainer } from '../../ui';
|
||||
import { addCollider, createContainer, removeObjects } from '../actions';
|
||||
@@ -18,6 +19,9 @@ import { type EditorState, objectSelection } from '../state';
|
||||
* siblings, or onto the empty space below the rows to move it to the top level. The object keeps
|
||||
* its world position every time.
|
||||
*
|
||||
* A row's name turns gold when the object deviates from what it follows – its container template
|
||||
* or its asset – or when anything below it does, so a folded subtree still shows it.
|
||||
*
|
||||
* Rows with children fold: the chevron (or ←/→, or a double-click on the row) toggles them.
|
||||
* Everything starts collapsed – which ids are open is view state kept here, never part of the
|
||||
* project, and selecting an object always unfolds the way down to it.
|
||||
@@ -128,6 +132,11 @@ export class HierarchyPanel extends Panel {
|
||||
return holder ? visibleTreeOrder(holder, this.expanded).map((e) => e.object.id) : [];
|
||||
}
|
||||
|
||||
/** Label colour of a row as `#rrggbb` – gold means it or something below it deviates (test hook). */
|
||||
rowColor(id: string): string | null {
|
||||
return this.list.rowColor(id);
|
||||
}
|
||||
|
||||
/** Global centre of a row's chevron, so tests can click the real hit area (test hook). */
|
||||
chevronPosition(id: string): { x: number; y: number } | null {
|
||||
return this.list.chevronGlobal(id);
|
||||
@@ -226,6 +235,10 @@ export class HierarchyPanel extends Panel {
|
||||
this.shownHolderId = holder?.id ?? '';
|
||||
const kind = holder?.type === 'container' ? 'Container' : 'Scene';
|
||||
this.setTitle(holder ? `${kind}: ${holder.name}` : 'Scene objects');
|
||||
// From the stored holder, not the resolved one: what an object follows depends on where it
|
||||
// lives, and in a resolved view container content looks stored.
|
||||
const source = this.source();
|
||||
const marks = source ? deviationMarks(this.state.project, source) : new Set<string>();
|
||||
this.list.setItems(
|
||||
(holder ? visibleTreeOrder(holder, this.expanded) : []).map(({ object, depth, hasChildren }) => {
|
||||
const asset = findAsset(this.state.project, object.assetId);
|
||||
@@ -237,6 +250,7 @@ export class HierarchyPanel extends Panel {
|
||||
depth,
|
||||
swatch: asset && hasColorSwatch(asset.type) ? Number(props.color ?? 0) : undefined,
|
||||
expanded: hasChildren ? this.expanded.has(object.id) : undefined,
|
||||
overridden: marks.has(object.id),
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -41,6 +41,8 @@ export const Theme = {
|
||||
trigger: 0xf2c84b,
|
||||
container: 0x3fb8c9,
|
||||
marquee: 0x4c8dff,
|
||||
/** A value deviating from what it follows: an override on a placed object, a non-default on an asset. */
|
||||
override: 0xf2c84b,
|
||||
/** Node graph: canvas, node chrome, per-category title colors, port colors, live highlight. */
|
||||
graphBackground: 0x15171b,
|
||||
node: 0x262a32,
|
||||
@@ -67,6 +69,7 @@ export type TextStyleToken =
|
||||
| 'body'
|
||||
| 'bodyBold'
|
||||
| 'muted'
|
||||
| 'override'
|
||||
| 'small'
|
||||
| 'title'
|
||||
| 'mono'
|
||||
@@ -89,6 +92,9 @@ export function textStyle(token: TextStyleToken = 'body'): TextStyle {
|
||||
case 'muted':
|
||||
style = new TextStyle({ ...base, fill: Theme.colors.textMuted });
|
||||
break;
|
||||
case 'override':
|
||||
style = new TextStyle({ ...base, fill: Theme.colors.override });
|
||||
break;
|
||||
case 'small':
|
||||
style = new TextStyle({ ...base, fontSize: Theme.smallFontSize, fill: Theme.colors.textMuted });
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Graphics, Text } from 'pixi.js';
|
||||
import { Color, type ColorSource, Graphics, Text } from 'pixi.js';
|
||||
import { drawControlBox } from '../draw';
|
||||
import { centreY } from '../math';
|
||||
import { Theme, textStyle } from '../Theme';
|
||||
@@ -25,6 +25,7 @@ export class ListRow extends Widget {
|
||||
super();
|
||||
this.cursor = 'pointer';
|
||||
this.minHeight = height;
|
||||
if (item.overridden) this.labelText.style = textStyle('override');
|
||||
this.addChild(this.bg, this.labelText, this.hint);
|
||||
this.trackHover();
|
||||
}
|
||||
@@ -37,6 +38,11 @@ export class ListRow extends Widget {
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
/** Fill of the label as `#rrggbb` – tells a marked row from a plain one (test hook). */
|
||||
get labelColor(): string {
|
||||
return new Color(this.labelText.style.fill as ColorSource).toHex();
|
||||
}
|
||||
|
||||
/** Left edge of the chevron slot (or of the content itself in a flat list). */
|
||||
private get indent(): number {
|
||||
return Theme.padding + (this.item.depth ?? 0) * Theme.treeIndent;
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface ListItem {
|
||||
swatch?: number;
|
||||
/** Whether the subtree is shown; such rows get a chevron. Left undefined for leaves. */
|
||||
expanded?: boolean;
|
||||
/** Draws the label in the override colour – the row, or something inside it, deviates. */
|
||||
overridden?: boolean;
|
||||
}
|
||||
|
||||
/** Pixels of travel before a press turns into a drag. */
|
||||
@@ -129,6 +131,11 @@ export class ListView extends Widget {
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
/** Label colour of a row as `#rrggbb`; null when no such row is shown (test hook). */
|
||||
rowColor(id: string): string | null {
|
||||
return this.rows.find((r) => r.item.id === id)?.labelColor ?? null;
|
||||
}
|
||||
|
||||
/** Global centre of a row's chevron; null when the row has none. */
|
||||
chevronGlobal(id: string): { x: number; y: number } | null {
|
||||
return this.rows.find((r) => r.item.id === id)?.chevronGlobal() ?? null;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateContainerCommand } from '../../src/core/commands/containerCommands';
|
||||
import {
|
||||
ClearOverrideCommand,
|
||||
SetOverrideCommand,
|
||||
SetParentCommand,
|
||||
} from '../../src/core/commands/sceneObjectCommands';
|
||||
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';
|
||||
|
||||
describe('deviation marks', () => {
|
||||
it('marks an object only once an override really deviates', () => {
|
||||
const f = makeSpriteScene();
|
||||
const id = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const deviates = () => objectDeviates(f.project, f.sceneOf(), f.object(id));
|
||||
expect(deviates()).toBe(false);
|
||||
// The asset's own value carried as an override is no deviation.
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'width', 100));
|
||||
expect(deviates()).toBe(false);
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, id, 'width', 250));
|
||||
expect(deviates()).toBe(true);
|
||||
f.stack.execute(new ClearOverrideCommand(f.sceneId, id, 'width'));
|
||||
expect(deviates()).toBe(false);
|
||||
});
|
||||
|
||||
it('marks the ancestors of a deviating object, so a folded row still shows it', () => {
|
||||
const f = makeSpriteScene();
|
||||
const parent = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const child = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
f.stack.execute(new SetParentCommand(f.sceneId, child, parent));
|
||||
expect(deviationMarks(f.project, f.sceneOf()).size).toBe(0);
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, child, 'width', 250));
|
||||
expect([...deviationMarks(f.project, f.sceneOf())].sort()).toEqual([child, parent].sort());
|
||||
});
|
||||
|
||||
it('marks container content against its template, and the instance above it', () => {
|
||||
const f = makeSpriteScene();
|
||||
const a = place(f, f.spriteId, { x: 0, y: 0 });
|
||||
const create = new CreateContainerCommand(f.sceneId, [a]);
|
||||
f.stack.execute(create);
|
||||
const template = (findAsset(f.project, create.assetId) as ContainerAsset).objects[0]!.id;
|
||||
const content = instanceChildId(create.objectId, template);
|
||||
expect(deviationMarks(f.project, f.sceneOf()).size).toBe(0);
|
||||
f.stack.execute(new SetOverrideCommand(f.sceneId, content, 'width', 250));
|
||||
expect([...deviationMarks(f.project, f.sceneOf())].sort()).toEqual([content, create.objectId].sort());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
override darstellung bei scene objects die werte von assets überschreiben:
|
||||
- momentan wird es fett dargestellt => stattdessen das label in einer gelb/goldener schriftfarbe darstellen
|
||||
- auch in der hierarchie der scene/container bitte den namen gelb/gold darstellen, falls es irgendein child/property gibt dass dirty ist
|
||||
|
||||
---
|
||||
|
||||
**Umgesetzt (2026-09-09), beide Punkte.**
|
||||
|
||||
- Inspector: das Label einer abweichenden Property ist jetzt gold (`#f2c84b`) statt fett.
|
||||
Die Farbe steht als Theme-Token `override` an einer Stelle.
|
||||
- Hierarchie: der Name wird gold, wenn das Objekt selbst abweicht **oder etwas darunter** —
|
||||
eine zugeklappte Zeile soll nicht verbergen, dass im Teilbaum etwas geaendert wurde.
|
||||
|
||||
Die Logik dazu sitzt in `src/core/properties/deviation.ts` (rein, getestet), nicht im Panel:
|
||||
`objectDeviates` vergleicht gegen das, dem ein Objekt folgt — bei Container-Inhalt gegen das
|
||||
**Template**, sonst gegen das **Asset** —, `deviationMarks` traegt die Marke auf alle Vorfahren
|
||||
hoch. Wichtig dabei: `deviationMarks` bekommt den **gespeicherten** Holder, nicht den
|
||||
aufgeloesten; in der aufgeloesten Sicht sieht Container-Inhalt wie ein normales Objekt aus und
|
||||
jede Instanz waere faelschlich gold.
|
||||
@@ -1,3 +0,0 @@
|
||||
override darstellung bei scene objects die werte von assets überschreiben:
|
||||
- momentan wird es fett dargestellt => stattdessen das label in einer gelb/goldener schriftfarbe darstellen
|
||||
- auch in der hierarchie der scene/container bitte den namen gelb/gold darstellen, falls es irgendein child/property gibt dass dirty ist
|
||||
@@ -14,6 +14,7 @@ export async function inspector(t) {
|
||||
drag,
|
||||
press,
|
||||
typeText,
|
||||
mutate,
|
||||
undo,
|
||||
redo,
|
||||
state,
|
||||
@@ -33,6 +34,28 @@ export async function inspector(t) {
|
||||
);
|
||||
await shot('02-object-selected');
|
||||
|
||||
// 1b. Deviations are shown in gold – on the inspector label and on the hierarchy row. The blue
|
||||
// crate overrides width and color, the camera and the red crate's subtree nothing.
|
||||
const GOLD = '#f2c84b';
|
||||
const PLAIN = '#e6e8ee';
|
||||
const rowColor = (id) => json(`__gm.hierarchyRowColor('${id}')`);
|
||||
check('an overridden object is marked gold in the hierarchy', (await rowColor('o2')) === GOLD);
|
||||
check('an untouched object is not', (await rowColor('o3')) === PLAIN, await rowColor('o3'));
|
||||
check('nor is a parent whose subtree is untouched', (await rowColor('o1')) === PLAIN);
|
||||
check(
|
||||
'the overridden field is gold, an inherited one is not',
|
||||
(await json(`__gm.textColor('Width')`)) === GOLD &&
|
||||
(await json(`__gm.textColor('Height')`)) === '#8a90a0',
|
||||
JSON.stringify(await json(`[__gm.textColor('Width'), __gm.textColor('Height')]`)),
|
||||
);
|
||||
// The mark also travels up: with the crate's own overrides taken away the row must stay gold
|
||||
// for the sake of its collider child, which keeps one. Put back right after, byte for byte.
|
||||
await mutate(`__gm.state.activeScene.objects.find(o => o.id === 'o2').overrides = {}`);
|
||||
check('a parent carries the mark of a deviating child', (await rowColor('o2')) === GOLD);
|
||||
await mutate(
|
||||
`__gm.state.activeScene.objects.find(o => o.id === 'o2').overrides = { width: 140, color: 0x4488ff }`,
|
||||
);
|
||||
|
||||
// 2. Rename via inspector: the name field sits right of the "Name" label.
|
||||
const nameLabel = await locate('Name');
|
||||
await click(nameLabel.x + 160, nameLabel.y);
|
||||
|
||||
Reference in new issue
Block a user