Measure a deviation against the base, not the innermost template
An instance edit was dropped as soon as its value matched the template object. With nested containers the template is not what the value falls back to: the outer container holds an edit for the object below it, and that one wins. If the inner template happened to carry the same value the user typed, the edit was thrown away and the value jumped to whatever the container in between says. Reported against the Jump-and-Run project: a coin narrowed to 8 inside "level", then set back to the sprite's own 24 in the scene, snapped back to 8. Reproduced through the inspector before touching anything. InstanceTarget now offers `base`: the template with the edits of every container in between woven in. Everything that judges deviation uses it - setOverride/setName/setPosition, both Revert commands, and the gold marks in the inspector. Writing still goes into the real template, which is where "apply to container" belongs. `base` is built on every read. Apply writes into the template and then asks whether the edit still deviates, so a base captured beforehand would answer for the template as it was - the e2e suite caught exactly that. Also in here, because it corrects the commit before it: placing an asset by double-clicking its row is gone, as asked for in asset-doppelklick.md. Dragging is the only way to place now. That took AssetPanel.place, the PlacementTarget interface, Editor.placementTarget and the two worldCenter() methods that existed only to feed it. A double-click on a scene asset still makes it the active scene.
This commit is contained in:
1 parent
75d377af39
commit
d7c8d3e579
15 files changed
+179
-76
No files matched your search
@@ -21,7 +21,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
|
|||||||
| **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. |
|
| **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 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. |
|
| **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. |
|
| **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. |
|
| **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 the **template as the containers in between leave it**; ↑ ▾ 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. |
|
| **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. |
|
| **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. |
|
| **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. |
|
||||||
@@ -120,7 +120,8 @@ be refused, which today means a container that would end up inside itself (`plac
|
|||||||
`src/core/model/placement.ts` – the same function the command runs, so the preview cannot promise
|
`src/core/model/placement.ts` – the same function the command runs, so the preview cannot promise
|
||||||
a placement the drop then denies). Picking a container up selects it, but the Container tab does
|
a placement the drop then denies). Picking a container up selects it, but the Container tab does
|
||||||
*not* follow that while a drag is in flight: the target must not swap out from under the pointer.
|
*not* follow that while a drag is in flight: the target must not swap out from under the pointer.
|
||||||
A double-click on the row still places at the middle of the visible view.
|
Dragging is the only way to place: a double-click on a scene makes it the active one, on anything
|
||||||
|
else it does nothing.
|
||||||
|
|
||||||
**Editor symbols are always on top.** The viewport draws scene content (sprites) in an
|
**Editor symbols are always on top.** The viewport draws scene content (sprites) in an
|
||||||
`objects` layer and editor symbols (camera icon, labels) in an `overlay` layer above it, so a
|
`objects` layer and editor symbols (camera icon, labels) in an `overlay` layer above it, so a
|
||||||
@@ -319,8 +320,12 @@ still refused – that cycle would resolve forever.
|
|||||||
So **editing a template changes every instance**, structure included: add, delete or re-parent a
|
So **editing a template changes every instance**, structure included: add, delete or re-parent a
|
||||||
template object and every placed container shows it immediately. What an instance may deviate in
|
template object and every placed container shows it immediately. What an instance may deviate in
|
||||||
is values, and only per slot: an override key, one position axis, the name. Those live in
|
is values, and only per slot: an override key, one position axis, the name. Those live in
|
||||||
`instanceEdits` on the container object and are dropped again the moment the value matches the
|
`instanceEdits` on the container object and are dropped again the moment the value matches its
|
||||||
template, which is what keeps "untouched follows, touched keeps" exact without any bookkeeping.
|
base, which is what keeps "untouched follows, touched keeps" exact without any bookkeeping. That
|
||||||
|
base is the template **with the edits of every container in between woven in** (`InstanceTarget.base`):
|
||||||
|
inside nested containers the innermost template is not what a value falls back to, so comparing
|
||||||
|
against it would drop an edit that does deviate – and the value would jump to the value of the
|
||||||
|
container in between.
|
||||||
|
|
||||||
The other direction is therefore just as small: **Apply to container** writes the value into the
|
The other direction is therefore just as small: **Apply to container** writes the value into the
|
||||||
template and drops the edit, **Reset to container** drops the edit alone.
|
template and drops the edit, **Reset to container** drops the edit alone.
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export class RevertOverrideToTemplateCommand implements Command {
|
|||||||
const target = requireTarget(project, this.holderId, this.objectId);
|
const target = requireTarget(project, this.holderId, this.objectId);
|
||||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||||
this.memo = captureObject(ref);
|
this.memo = captureObject(ref);
|
||||||
ref.setOverride(this.key, target.template.overrides[this.key]);
|
ref.setOverride(this.key, target.base.overrides[this.key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
undo(): void {
|
undo(): void {
|
||||||
@@ -174,7 +174,7 @@ export class RevertPositionToTemplateCommand implements Command {
|
|||||||
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
const ref = requireObjectRef(project, this.holderId, this.objectId);
|
||||||
this.memo = captureObject(ref);
|
this.memo = captureObject(ref);
|
||||||
const position = { ...ref.object.position };
|
const position = { ...ref.object.position };
|
||||||
for (const axis of this.axis ? [this.axis] : ALL_AXES) position[axis] = target.template.position[axis];
|
for (const axis of this.axis ? [this.axis] : ALL_AXES) position[axis] = target.base.position[axis];
|
||||||
ref.setPosition(position);
|
ref.setPosition(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,15 @@ export interface InstanceTarget {
|
|||||||
/** The container asset that declares `template` – the one to open to change it for good. */
|
/** The container asset that declares `template` – the one to open to change it for good. */
|
||||||
asset: ContainerAsset;
|
asset: ContainerAsset;
|
||||||
template: SceneObject;
|
template: SceneObject;
|
||||||
|
/**
|
||||||
|
* What the object would show without this instance's edit: the template with the edits of every
|
||||||
|
* container in between already woven in. Only this is a base a value can fall back to, so it –
|
||||||
|
* not `template` – decides whether an edit deviates. The two differ once containers nest.
|
||||||
|
*
|
||||||
|
* Built fresh on every read, because "apply" writes into `template` and then asks whether the
|
||||||
|
* edit still deviates – a base captured beforehand would answer for the old template.
|
||||||
|
*/
|
||||||
|
readonly base: SceneObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Where a derived id points: the instance holding the edits, and the template object behind it. */
|
/** Where a derived id points: the instance holding the edits, and the template object behind it. */
|
||||||
@@ -215,16 +224,35 @@ export function instanceTargetOf(
|
|||||||
const split = splitInstanceId(objectId);
|
const split = splitInstanceId(objectId);
|
||||||
const instance = split && holder.objects.find((o) => o.id === split.instanceId);
|
const instance = split && holder.objects.find((o) => o.id === split.instanceId);
|
||||||
if (!split || !instance) return undefined;
|
if (!split || !instance) return undefined;
|
||||||
|
const segments = split.path.split(PATH_SEP);
|
||||||
let asset = containerAssetOf(project, instance);
|
let asset = containerAssetOf(project, instance);
|
||||||
let template: SceneObject | undefined;
|
let template: SceneObject | undefined;
|
||||||
for (const segment of split.path.split(PATH_SEP)) {
|
// Each container on the way holds edits for what lies below it, keyed relative to itself.
|
||||||
|
const between: { holder: SceneObject; key: string }[] = [];
|
||||||
|
for (const [index, segment] of segments.entries()) {
|
||||||
const next = template ? containerAssetOf(project, template) : asset;
|
const next = template ? containerAssetOf(project, template) : asset;
|
||||||
if (!next) return undefined;
|
if (!next) return undefined;
|
||||||
asset = next;
|
asset = next;
|
||||||
template = next.objects.find((t) => t.id === segment);
|
template = next.objects.find((t) => t.id === segment);
|
||||||
if (!template) return undefined;
|
if (!template) return undefined;
|
||||||
|
if (index < segments.length - 1)
|
||||||
|
between.push({ holder: template, key: segments.slice(index + 1).join(PATH_SEP) });
|
||||||
}
|
}
|
||||||
return asset && template ? { instance, path: split.path, asset, template } : undefined;
|
if (!asset || !template) return undefined;
|
||||||
|
const leaf = template;
|
||||||
|
return {
|
||||||
|
instance,
|
||||||
|
path: split.path,
|
||||||
|
asset,
|
||||||
|
template: leaf,
|
||||||
|
get base() {
|
||||||
|
let edit: InstanceEdit | undefined;
|
||||||
|
// Outermost wins, as in `expand`: it sits closest to the instance being edited.
|
||||||
|
for (let i = between.length - 1; i >= 0; i--)
|
||||||
|
edit = mergeEdits(edit, between[i]!.holder.instanceEdits[between[i]!.key]);
|
||||||
|
return resolveChild(leaf, edit, leaf.id, leaf.parentId);
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- writing -----------------------------------------------------------------------------------
|
// --- writing -----------------------------------------------------------------------------------
|
||||||
@@ -267,13 +295,13 @@ function instanceRef(target: InstanceTarget, object: SceneObject): ObjectRef {
|
|||||||
target,
|
target,
|
||||||
setName: (name) =>
|
setName: (name) =>
|
||||||
write((edit) => {
|
write((edit) => {
|
||||||
if (name === target.template.name) delete edit.name;
|
if (name === target.base.name) delete edit.name;
|
||||||
else edit.name = name;
|
else edit.name = name;
|
||||||
}),
|
}),
|
||||||
setPosition: (position) =>
|
setPosition: (position) =>
|
||||||
write((edit) => {
|
write((edit) => {
|
||||||
for (const axis of AXES) {
|
for (const axis of AXES) {
|
||||||
if (position[axis] === target.template.position[axis]) delete edit.position[axis];
|
if (position[axis] === target.base.position[axis]) delete edit.position[axis];
|
||||||
else edit.position[axis] = position[axis];
|
else edit.position[axis] = position[axis];
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -284,7 +312,7 @@ function instanceRef(target: InstanceTarget, object: SceneObject): ObjectRef {
|
|||||||
}),
|
}),
|
||||||
setOverride: (key, value) =>
|
setOverride: (key, value) =>
|
||||||
write((edit) => {
|
write((edit) => {
|
||||||
if (value === target.template.overrides[key]) delete edit.overrides[key];
|
if (value === target.base.overrides[key]) delete edit.overrides[key];
|
||||||
else setBagValue(edit.overrides, key, value);
|
else setBagValue(edit.overrides, key, value);
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,6 +53,6 @@ export function deviatesFromAsset(object: SceneObject, asset: Asset, key: string
|
|||||||
* Compared as overrides, not as effective values: both sides resolve against the same asset, so
|
* Compared as overrides, not as effective values: both sides resolve against the same asset, so
|
||||||
* "neither overrides" means equal even when the asset value changed since.
|
* "neither overrides" means equal even when the asset value changed since.
|
||||||
*/
|
*/
|
||||||
export function deviatesFromTemplate(object: SceneObject, template: SceneObject, key: string): boolean {
|
export function deviatesFromTemplate(object: SceneObject, base: SceneObject, key: string): boolean {
|
||||||
return object.overrides[key] !== template.overrides[key];
|
return object.overrides[key] !== base.overrides[key];
|
||||||
}
|
}
|
||||||
+2
-12
@@ -9,7 +9,7 @@ import { attachAutosave } from './autosave';
|
|||||||
import { ContainerEditor } from './container/ContainerEditor';
|
import { ContainerEditor } from './container/ContainerEditor';
|
||||||
import { GraphEditor } from './graph/GraphEditor';
|
import { GraphEditor } from './graph/GraphEditor';
|
||||||
import { Inspector } from './inspector/Inspector';
|
import { Inspector } from './inspector/Inspector';
|
||||||
import { AssetPanel, type PlacementTarget } from './panels/AssetPanel';
|
import { AssetPanel } from './panels/AssetPanel';
|
||||||
import { HierarchyPanel } from './panels/HierarchyPanel';
|
import { HierarchyPanel } from './panels/HierarchyPanel';
|
||||||
import { Toolbar } from './panels/Toolbar';
|
import { Toolbar } from './panels/Toolbar';
|
||||||
import { type PlayHost, PlaySession, type WorldSnapshot } from './PlaySession';
|
import { type PlayHost, PlaySession, type WorldSnapshot } from './PlaySession';
|
||||||
@@ -92,7 +92,7 @@ export class Editor implements PlayHost {
|
|||||||
|
|
||||||
this.workspace = new HStack();
|
this.workspace = new HStack();
|
||||||
this.leftColumn = new VStack({ gap: 1 });
|
this.leftColumn = new VStack({ gap: 1 });
|
||||||
this.assets = new AssetPanel(state, () => this.placementTarget(), textures);
|
this.assets = new AssetPanel(state, textures);
|
||||||
this.leftColumn.add(this.assets, 1);
|
this.leftColumn.add(this.assets, 1);
|
||||||
installTextureImport(app.canvas, { state, textures });
|
installTextureImport(app.canvas, { state, textures });
|
||||||
this.leftColumn.add(this.hierarchy, 1);
|
this.leftColumn.add(this.hierarchy, 1);
|
||||||
@@ -185,16 +185,6 @@ export class Editor implements PlayHost {
|
|||||||
ui().focus.validate();
|
ui().focus.validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Where the Assets panel places: into the container being edited, otherwise into the scene. */
|
|
||||||
private placementTarget(): PlacementTarget | undefined {
|
|
||||||
if (this.tab === 'container') {
|
|
||||||
const holder = findObjectHolder(this.state.project, this.containerEditor.currentAssetId);
|
|
||||||
return holder ? { holder, at: this.containerEditor.worldCenter() } : undefined;
|
|
||||||
}
|
|
||||||
const scene = this.state.activeScene;
|
|
||||||
return scene ? { holder: scene, at: this.viewport.worldCenter() } : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
boot(): Promise<void> {
|
boot(): Promise<void> {
|
||||||
return this.projects.boot();
|
return this.projects.boot();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Graphics } from 'pixi.js';
|
import { Graphics } from 'pixi.js';
|
||||||
import type { ContainerAsset } from '../../core/model/Asset';
|
import type { ContainerAsset } from '../../core/model/Asset';
|
||||||
import type { Vec2 } from '../../core/model/Properties';
|
|
||||||
import { findAsset, listContainers } from '../../core/model/Project';
|
import { findAsset, listContainers } from '../../core/model/Project';
|
||||||
import {
|
import {
|
||||||
activeDrag,
|
activeDrag,
|
||||||
@@ -79,11 +78,6 @@ export class ContainerEditor extends VStack {
|
|||||||
return this.canvas.dropInfo();
|
return this.canvas.dropInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Middle of the view – where a double-click in the Assets panel places into this template. */
|
|
||||||
worldCenter(): Vec2 {
|
|
||||||
return this.canvas.worldCenter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Where a template object is drawn, in global screen coordinates – for the check scripts. */
|
/** Where a template object is drawn, in global screen coordinates – for the check scripts. */
|
||||||
objectScreen(objectId: string): { x: number; y: number } | null {
|
objectScreen(objectId: string): { x: number; y: number } | null {
|
||||||
return this.canvas.objectScreen(objectId);
|
return this.canvas.objectScreen(objectId);
|
||||||
|
|||||||
@@ -487,12 +487,12 @@ function markObjectRows(
|
|||||||
for (const axis of POSITION_AXES)
|
for (const axis of POSITION_AXES)
|
||||||
host.fields
|
host.fields
|
||||||
.get(axis)
|
.get(axis)
|
||||||
?.setModified(Boolean(target) && object.position[axis] !== target!.template.position[axis]);
|
?.setModified(Boolean(target) && object.position[axis] !== target!.base.position[axis]);
|
||||||
for (const axis of OFFSET_AXES)
|
for (const axis of OFFSET_AXES)
|
||||||
host.fields.get(offsetKey(axis))?.setModified(object.editorOffset[axis] !== 0);
|
host.fields.get(offsetKey(axis))?.setModified(object.editorOffset[axis] !== 0);
|
||||||
for (const def of schemaForAsset(asset)) {
|
for (const def of schemaForAsset(asset)) {
|
||||||
const deviates = target
|
const deviates = target
|
||||||
? deviatesFromTemplate(object, target.template, def.key)
|
? deviatesFromTemplate(object, target.base, def.key)
|
||||||
: deviatesFromAsset(object, asset, def.key);
|
: deviatesFromAsset(object, asset, def.key);
|
||||||
host.fields.get(def.key)?.setModified(deviates);
|
host.fields.get(def.key)?.setModified(deviates);
|
||||||
}
|
}
|
||||||
@@ -516,7 +516,7 @@ function deviations(
|
|||||||
const target = targetOf(state.project, holderId, objectId);
|
const target = targetOf(state.project, holderId, objectId);
|
||||||
const fromAsset = deviatesFromAsset(object, asset, key);
|
const fromAsset = deviatesFromAsset(object, asset, key);
|
||||||
return {
|
return {
|
||||||
template: Boolean(target) && deviatesFromTemplate(object, target!.template, key),
|
template: Boolean(target) && deviatesFromTemplate(object, target!.base, key),
|
||||||
asset: !target && fromAsset,
|
asset: !target && fromAsset,
|
||||||
applyToAsset: fromAsset,
|
applyToAsset: fromAsset,
|
||||||
};
|
};
|
||||||
@@ -531,7 +531,7 @@ function positionMoved(
|
|||||||
): boolean {
|
): boolean {
|
||||||
const target = targetOf(state.project, holderId, objectId);
|
const target = targetOf(state.project, holderId, objectId);
|
||||||
const object = currentRef(state, holderId, objectId)?.object;
|
const object = currentRef(state, holderId, objectId)?.object;
|
||||||
return Boolean(target && object) && object!.position[axis] !== target!.template.position[axis];
|
return Boolean(target && object) && object!.position[axis] !== target!.base.position[axis];
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentRef(
|
function currentRef(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { CreateAssetCommand, SetActiveSceneCommand } from '../../core/commands/assetCommands';
|
import { CreateAssetCommand, SetActiveSceneCommand } from '../../core/commands/assetCommands';
|
||||||
import { nextAssetName } from '../../core/commands/naming';
|
import { nextAssetName } from '../../core/commands/naming';
|
||||||
import { PlaceAssetCommand } from '../../core/commands/sceneObjectCommands';
|
|
||||||
import {
|
import {
|
||||||
ASSET_TYPE_LABELS,
|
ASSET_TYPE_LABELS,
|
||||||
ASSET_TYPES,
|
ASSET_TYPES,
|
||||||
@@ -8,31 +7,20 @@ import {
|
|||||||
hasColorSwatch,
|
hasColorSwatch,
|
||||||
isPlaceableType,
|
isPlaceableType,
|
||||||
} from '../../core/model/AssetTypes';
|
} from '../../core/model/AssetTypes';
|
||||||
import type { Vec2 } from '../../core/model/Properties';
|
import { findAsset } from '../../core/model/Project';
|
||||||
import { findAsset, type ObjectHolder } from '../../core/model/Project';
|
|
||||||
import type { TextureStore } from '../../core/store/TextureStore';
|
import type { TextureStore } from '../../core/store/TextureStore';
|
||||||
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui';
|
import { Button, ListView, Menu, type MenuItem, Panel, ScrollContainer, startDrag } from '../../ui';
|
||||||
import { confirmDeleteAsset } from '../actions';
|
import { confirmDeleteAsset } from '../actions';
|
||||||
import { type EditorState, objectSelection } from '../state';
|
import type { EditorState } from '../state';
|
||||||
import { pickAndImport } from '../textureImport';
|
import { pickAndImport } from '../textureImport';
|
||||||
|
|
||||||
/**
|
/** Lists all assets of the project; creates and deletes them. Placing is a drag into a view. */
|
||||||
* Where a placed asset lands. The tab in front decides, so it appears where the user is
|
|
||||||
* looking: the Scene tab places into the active scene, the Container tab into the template.
|
|
||||||
*/
|
|
||||||
export interface PlacementTarget {
|
|
||||||
holder: ObjectHolder;
|
|
||||||
at: Vec2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Lists all assets of the project; creates, places and deletes them. */
|
|
||||||
export class AssetPanel extends Panel {
|
export class AssetPanel extends Panel {
|
||||||
private readonly list = new ListView();
|
private readonly list = new ListView();
|
||||||
private readonly scroll = new ScrollContainer();
|
private readonly scroll = new ScrollContainer();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly state: EditorState,
|
private readonly state: EditorState,
|
||||||
private readonly target: () => PlacementTarget | undefined,
|
|
||||||
/** Where imported images go. Without it the Texture entry has nothing to upload to. */
|
/** Where imported images go. Without it the Texture entry has nothing to upload to. */
|
||||||
private readonly textures: TextureStore | null = null,
|
private readonly textures: TextureStore | null = null,
|
||||||
) {
|
) {
|
||||||
@@ -99,12 +87,10 @@ export class AssetPanel extends Panel {
|
|||||||
pickAndImport({ state: this.state, textures: this.textures });
|
pickAndImport({ state: this.state, textures: this.textures });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Double-click / Enter: scenes become active, everything else is placed. */
|
/** Double-click / Enter on a scene makes it the active one. Placing is a drag, not a click. */
|
||||||
private activate(id: string): void {
|
private activate(id: string): void {
|
||||||
const asset = findAsset(this.state.project, id);
|
const asset = findAsset(this.state.project, id);
|
||||||
if (!asset) return;
|
if (asset?.type === 'scene') this.state.execute(new SetActiveSceneCommand(id));
|
||||||
if (asset.type === 'scene') this.state.execute(new SetActiveSceneCommand(id));
|
|
||||||
else this.place(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Where an asset's row sits on screen, so a check can pick it up by id (test hook). */
|
/** Where an asset's row sits on screen, so a check can pick it up by id (test hook). */
|
||||||
@@ -119,17 +105,6 @@ export class AssetPanel extends Panel {
|
|||||||
startDrag({ kind: 'asset', id: assetId }, asset.name);
|
startDrag({ kind: 'asset', id: assetId }, asset.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private place(assetId: string): void {
|
|
||||||
const target = this.target();
|
|
||||||
if (!target) {
|
|
||||||
this.state.setNotice('Nothing to place into – create a scene, or open a container first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { holder, at } = target;
|
|
||||||
const command = new PlaceAssetCommand(holder.id, assetId, at);
|
|
||||||
if (this.state.execute(command)) this.state.select(objectSelection(holder.id, [command.objectId]));
|
|
||||||
}
|
|
||||||
|
|
||||||
private openContextMenu(id: string, x: number, y: number): Menu | undefined {
|
private openContextMenu(id: string, x: number, y: number): Menu | undefined {
|
||||||
const asset = findAsset(this.state.project, id);
|
const asset = findAsset(this.state.project, id);
|
||||||
if (!asset) return undefined;
|
if (!asset) return undefined;
|
||||||
|
|||||||
@@ -116,11 +116,6 @@ export class SceneViewport extends WorldViewport {
|
|||||||
return p ? { assetId: p.assetId, x: p.at.x, y: p.at.y, rejection: p.rejection } : null;
|
return p ? { assetId: p.assetId, x: p.at.x, y: p.at.y, rejection: p.rejection } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** World coordinates at the centre of the view. */
|
|
||||||
worldCenter(): Vec2 {
|
|
||||||
return { x: Math.round(this.view.x), y: Math.round(this.view.y) };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frames everything the holder contains. The origin is always included: it is where a container
|
* Frames everything the holder contains. The origin is always included: it is where a container
|
||||||
* is grabbed when placed, so a template drawn far off to one side would otherwise hide it.
|
* is grabbed when placed, so a template drawn far off to one side would otherwise hide it.
|
||||||
|
|||||||
@@ -107,12 +107,15 @@ describe('position apply / revert', () => {
|
|||||||
|
|
||||||
describe('override apply / revert', () => {
|
describe('override apply / revert', () => {
|
||||||
it('applies an override to the container template and to instances that had no own value', () => {
|
it('applies an override to the container template and to instances that had no own value', () => {
|
||||||
const { stack, sceneId, a, a2, object, templateOfA } = setup();
|
const { stack, sceneId, a, a2, object, templateOfA, sceneOf, instanceId } = setup();
|
||||||
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
stack.execute(new SetOverrideCommand(sceneId, a, 'width', 77));
|
||||||
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
stack.execute(new ApplyOverrideToContainerCommand(sceneId, a, 'width'));
|
||||||
expect(templateOfA().overrides.width).toBe(77);
|
expect(templateOfA().overrides.width).toBe(77);
|
||||||
expect(object(a2).overrides.width).toBe(77);
|
expect(object(a2).overrides.width).toBe(77);
|
||||||
expect(object(a).overrides.width).toBe(77);
|
expect(object(a).overrides.width).toBe(77);
|
||||||
|
// The template carries it now, so nothing is left to deviate – the edit has to be gone, not
|
||||||
|
// merely equal. It is dropped by comparing against a base read *after* the write.
|
||||||
|
expect(sceneOf().objects.find((o) => o.id === instanceId)!.instanceEdits).toEqual({});
|
||||||
stack.undo();
|
stack.undo();
|
||||||
expect(templateOfA().overrides.width).toBeUndefined();
|
expect(templateOfA().overrides.width).toBeUndefined();
|
||||||
expect(object(a2).overrides.width).toBeUndefined();
|
expect(object(a2).overrides.width).toBeUndefined();
|
||||||
@@ -173,3 +176,57 @@ describe('override apply / revert', () => {
|
|||||||
expect(object(a2).overrides.height).toBe(9);
|
expect(object(a2).overrides.height).toBe(9);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container inside a container: the value a scene edit falls back to is the template *plus* the
|
||||||
|
* edits of every container in between – not the innermost template alone.
|
||||||
|
*/
|
||||||
|
function nested() {
|
||||||
|
const f = makeSpriteScene();
|
||||||
|
const inner = new CreateContainerCommand(f.sceneId, [place(f, f.spriteId, { x: 0, y: 0 })]);
|
||||||
|
f.stack.execute(inner);
|
||||||
|
const innerAsset = () => findAsset(f.project, inner.assetId) as ContainerAsset;
|
||||||
|
const leaf = innerAsset().objects[0]!.id;
|
||||||
|
// The sprite is 100 wide anyway: an override carrying the asset's own value, the way putting a
|
||||||
|
// value back by hand in a container editor leaves one behind.
|
||||||
|
f.stack.execute(new SetOverrideCommand(inner.assetId, leaf, 'width', 100));
|
||||||
|
|
||||||
|
const outer = new CreateContainerCommand(f.sceneId, [inner.objectId]);
|
||||||
|
f.stack.execute(outer);
|
||||||
|
const outerAsset = () => findAsset(f.project, outer.assetId) as ContainerAsset;
|
||||||
|
const middle = outerAsset().objects[0]!.id;
|
||||||
|
// Inside the outer container, that one sprite is narrowed to 8.
|
||||||
|
f.stack.execute(new SetOverrideCommand(outer.assetId, instanceChildId(middle, leaf), 'width', 8));
|
||||||
|
|
||||||
|
return { ...f, leaf, deep: instanceChildId(outer.objectId, `${middle}/${leaf}`) };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('a base two levels up', () => {
|
||||||
|
it('keeps a scene value that equals the inner template but not the container in between', () => {
|
||||||
|
const { stack, sceneId, deep, object } = nested();
|
||||||
|
expect(object(deep).overrides.width).toBe(8);
|
||||||
|
stack.execute(new SetOverrideCommand(sceneId, deep, 'width', 10));
|
||||||
|
expect(object(deep).overrides.width).toBe(10);
|
||||||
|
// 100 is what the innermost template says – but the container in between says 8, so this is
|
||||||
|
// a deviation and has to be kept. Comparing against the template alone dropped it.
|
||||||
|
stack.execute(new SetOverrideCommand(sceneId, deep, 'width', 100));
|
||||||
|
expect(object(deep).overrides.width).toBe(100);
|
||||||
|
stack.undo();
|
||||||
|
expect(object(deep).overrides.width).toBe(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets to the value the container in between gives, not to the inner template', () => {
|
||||||
|
const { stack, sceneId, deep, object } = nested();
|
||||||
|
stack.execute(new SetOverrideCommand(sceneId, deep, 'width', 100));
|
||||||
|
stack.execute(new RevertOverrideToTemplateCommand(sceneId, deep, 'width'));
|
||||||
|
expect(object(deep).overrides.width).toBe(8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops an edit that matches the base, so the value follows again', () => {
|
||||||
|
const { stack, sceneId, deep, object, sceneOf } = nested();
|
||||||
|
stack.execute(new SetOverrideCommand(sceneId, deep, 'width', 8));
|
||||||
|
const instance = sceneOf().objects.find((o) => o.id === deep.split('~')[0])!;
|
||||||
|
expect(instance.instanceEdits).toEqual({});
|
||||||
|
expect(object(deep).overrides.width).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
Angeblich kann man Assets per Doppelklick in die Scene/Container platzieren. Das funktioniert aber nicht.
|
||||||
|
Falls es irgendwie implementiert ist gehört es sowieso raus => Das Doppelklick Feature brauchen wir nicht!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Umgesetzt (2026-09-09).** Raus, wie gewuenscht.
|
||||||
|
|
||||||
|
Es *war* implementiert: `ListView` meldete einen Doppelklick (und Enter) als `onActivate`, und
|
||||||
|
`AssetPanel.activate` platzierte damit in die Mitte der sichtbaren Ansicht — der Weg, den ich in
|
||||||
|
`asset-drag-and-drop.md` noch als "bleibt bestehen" beschrieben hatte. Warum er bei dir nichts
|
||||||
|
tat, habe ich nicht weiter verfolgt: das Feature faellt ohnehin weg.
|
||||||
|
|
||||||
|
Entfernt sind damit auch die Teile, die nur dafuer existierten: `AssetPanel.place`, das
|
||||||
|
`PlacementTarget`-Interface samt `Editor.placementTarget`, und die beiden `worldCenter()`-Methoden
|
||||||
|
in `ContainerEditor` und `SceneViewport`, die sonst tot im Baum stehengeblieben waeren.
|
||||||
|
|
||||||
|
**Geblieben:** Doppelklick (oder Enter) auf ein **Szenen**-Asset setzt es weiterhin als aktive
|
||||||
|
Szene — das ist kein Platzieren, sondern derselbe Befehl wie im Kontextmenue.
|
||||||
@@ -5,7 +5,8 @@ zukünftig soll es per drag and drop passieren
|
|||||||
|
|
||||||
**Umgesetzt (2026-09-09).** Drag and Drop ersetzt den Menueintrag vollstaendig — auf deine
|
**Umgesetzt (2026-09-09).** Drag and Drop ersetzt den Menueintrag vollstaendig — auf deine
|
||||||
Ansage hin ist "Place in scene"/"Place in container" aus dem Kontextmenue verschwunden.
|
Ansage hin ist "Place in scene"/"Place in container" aus dem Kontextmenue verschwunden.
|
||||||
Doppelklick auf die Zeile platziert weiterhin in die Mitte der sichtbaren Ansicht.
|
Doppelklick auf die Zeile platzierte zunaechst weiterhin in die Mitte der Ansicht — auf deine Notiz
|
||||||
|
`asset-doppelklick.md` hin ist auch das entfernt.
|
||||||
|
|
||||||
- **Ziehen:** die Zeile im Assets-Panel wird nach 4 px Weg zum Drag; ein Ghost mit dem
|
- **Ziehen:** die Zeile im Assets-Panel wird nach 4 px Weg zum Drag; ein Ghost mit dem
|
||||||
Asset-Namen haengt am Cursor (`src/ui/drag.ts`). Die `ListView` erkennt die Geste nur und
|
Asset-Namen haengt am Cursor (`src/ui/drag.ts`). Die `ListView` erkennt die Geste nur und
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
folgende fehlverhalten, beispiel jump and run projekt:
|
||||||
|
- wenn ich IM container asset "level" von einem einzigen coin sprite die breite ändere von 24 => 8
|
||||||
|
- wenn ich dann in der level scene bin und auf das geänderte coin sprite gehe und von 8 => 10 ändere funktioniert es noch
|
||||||
|
- sobald ich aber auf den original standard wert 24 ändere (oder auf den reset button klicke) dann springt es zurück auf 8 (ist korrekt bei reset, aber wenn ich explizit die ursprünglichen 24 haben will, geht das nicht, weil dieser wert nicht mehr als overwrite gilt)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Umgesetzt (2026-09-09).** Reproduziert im Browser am echten Jump-and-Run-Stand: nach dem Tippen
|
||||||
|
von 24 stand `{"ov":8}` da, genau wie gemeldet. Deine Deutung war richtig — der Wert galt nicht
|
||||||
|
mehr als Override und wurde weggeworfen.
|
||||||
|
|
||||||
|
**Ursache.** Ein Instanz-Edit wurde geloescht, sobald der Wert dem Template-Objekt gleicht
|
||||||
|
(`instanceRef.setOverride`). Bei **verschachtelten** Containern ist das Template aber nicht die
|
||||||
|
Basis: `level` haelt auf seinem `c1`-Objekt einen Edit fuer das darunterliegende `coin-body`
|
||||||
|
(die 8), und der gewinnt gegen das Template im `coin`-Asset. Traegt dieses Template zufaellig
|
||||||
|
schon die 24 — was jede Ruecknahme auf den Asset-Wert in einem Container-Editor hinterlaesst,
|
||||||
|
weil ein gespeicherter Override nie geloescht wird — dann galten 24 als "gleich dem Template",
|
||||||
|
der Edit fiel weg und der Wert landete auf der Zwischenebene: **8**.
|
||||||
|
|
||||||
|
**Fix.** `InstanceTarget` hat jetzt `base`: das Template mit den Edits **jedes Containers
|
||||||
|
dazwischen** eingewoben. Nur das ist ein Wert, auf den etwas zurueckfallen kann. Alles, was eine
|
||||||
|
Abweichung beurteilt, benutzt es: `setOverride`/`setName`/`setPosition`, die beiden
|
||||||
|
`Revert…ToTemplate`-Kommandos und die Gold-Markierung im Inspector. Das **Schreiben** von
|
||||||
|
"Apply to container" geht weiterhin ins echte Template — dort gehoert der Wert hin.
|
||||||
|
|
||||||
|
Bei einer einzigen Ebene sind `base` und `template` identisch, das Verhalten aendert sich dort
|
||||||
|
nicht. Reset landet weiterhin auf 8 (deine Erwartung), 24 bleibt jetzt als Override stehen.
|
||||||
|
|
||||||
|
Drei Unit-Tests in `tests/core/apply.test.ts` halten es fest; zwei davon schlagen ohne den Fix
|
||||||
|
fehl (gegengeprueft).
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Der E2E-Check `pixel-art filtering keeps the border between two quadrants hard` faellt
|
||||||
|
**sporadisch** um — heute (2026-09-09) zweimal in vier Laeufen, jeweils ohne dass eine Aenderung
|
||||||
|
Texturen beruehrt haette.
|
||||||
|
|
||||||
|
- gruen: `["#e63c3c","#e63c3c","#3cc85a","#3cc85a","#3cc85a"]` — harte Kante, `nearest` greift
|
||||||
|
- rot: `["#c65641","#a67047","#868a4c","#66a552","#3cc85a"]` — weicher Verlauf, also `linear`
|
||||||
|
|
||||||
|
Der Folge-Check (`switching the filter back to smooth …`) meldet im Fehlerfall fuer **beide**
|
||||||
|
Filter denselben Verlauf, ist aber trotzdem gruen — er vergleicht nicht gegen den Erwartungswert.
|
||||||
|
Das heisst: wenn der Filterwechsel haengt, faellt genau ein Check, und der zweite deckt es zu.
|
||||||
|
|
||||||
|
Zu klaeren: laedt der Test die Textur zu frueh aus (Timing), oder wird `scaleMode` beim ersten
|
||||||
|
Setzen manchmal nicht auf die schon hochgeladene GPU-Textur durchgereicht (dann ist es ein echter
|
||||||
|
Produktfehler und kein Testproblem)?
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
folgende fehlverhalten, beispiel jump and run projekt:
|
|
||||||
- wenn ich IM container asset "level" von einem einzigen coin sprite die breite ändere von 24 => 8
|
|
||||||
- wenn ich dann in der level scene bin und auf das geänderte coin sprite gehe und von 8 => 10 ändere funktioniert es noch
|
|
||||||
- sobald ich aber auf den original standard wert 24 ändere (oder auf den reset button klicke) dann springt es zurück auf 8 (ist korrekt bei reset, aber wenn ich explizit die ursprünglichen 24 haben will, geht das nicht, weil dieser wert nicht mehr als overwrite gilt)
|
|
||||||
Reference in new issue
Block a user