Z-order via position.z and reset-to-default for asset properties
- Position is now { x, y, z }; higher z draws on top, ties keep hierarchy order
- Viewport sorts the objects layer deterministically (syncDrawOrder)
- Inspector shows a Z field; asset properties that differ from the schema
default are marked bold with a reset button (ResetAssetPropertyCommand)
- isModified() helper, unit tests, E2E checks and seed updated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
aaa8154adf
commit
d4b1ffa051
18 files changed
+232
-63
No files matched your search
@@ -127,6 +127,30 @@ export class SetAssetPropertyCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Puts an asset property back to its schema default (the asset-level "clear override"). */
|
||||
export class ResetAssetPropertyCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: PropertyValue | undefined;
|
||||
|
||||
constructor(private readonly assetId: string, private readonly key: string) {
|
||||
this.label = `Reset ${key}`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const asset = requireAsset(project, this.assetId);
|
||||
const def = schemaFor(asset.type).find((d) => d.key === this.key);
|
||||
if (!def) throw new CommandError(`Property "${this.key}" is not defined for ${asset.type}`);
|
||||
this.previous = asset.props[this.key];
|
||||
asset.props[this.key] = def.default;
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
const asset = requireAsset(project, this.assetId);
|
||||
if (this.previous === undefined) delete asset.props[this.key];
|
||||
else asset.props[this.key] = this.previous;
|
||||
}
|
||||
}
|
||||
|
||||
export class SetActiveSceneCommand implements Command {
|
||||
readonly label = 'Switch scene';
|
||||
private previous = '';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isPlaceableType } from '../model/AssetTypes';
|
||||
import type { PropertyValue, Vec2 } from '../model/Properties';
|
||||
import type { Position, PropertyValue, Vec2 } from '../model/Properties';
|
||||
import { findScene, findSceneObject, type Project } from '../model/Project';
|
||||
import { createSceneObject, type SceneObject } from '../model/SceneObject';
|
||||
import { coerceValue, schemaFor } from '../properties/PropertySchema';
|
||||
@@ -24,7 +24,7 @@ export class PlaceAssetCommand implements Command {
|
||||
readonly label = 'Place asset';
|
||||
readonly objectId = newId();
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly assetId: string, private readonly position: Vec2) {}
|
||||
constructor(private readonly sceneId: string, private readonly assetId: string, private readonly position: Vec2 | Position) {}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = requireScene(project, this.sceneId);
|
||||
@@ -60,15 +60,16 @@ export class RemoveSceneObjectCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the full position (x, y and draw order z) of a scene object. */
|
||||
export class MoveSceneObjectCommand implements Command {
|
||||
readonly label = 'Move object';
|
||||
private previous: Vec2 = { x: 0, y: 0 };
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
|
||||
constructor(
|
||||
private readonly sceneId: string,
|
||||
private readonly objectId: string,
|
||||
private readonly position: Vec2,
|
||||
previous?: Vec2,
|
||||
private readonly position: Position,
|
||||
previous?: Position,
|
||||
) {
|
||||
if (previous) this.previous = { ...previous };
|
||||
}
|
||||
@@ -76,7 +77,7 @@ export class MoveSceneObjectCommand implements Command {
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
this.previous = { ...object.position };
|
||||
object.position = { x: this.position.x, y: this.position.y };
|
||||
object.position = { x: this.position.x, y: this.position.y, z: this.position.z };
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
|
||||
@@ -4,7 +4,21 @@ export type PropertyValue = number | string | boolean;
|
||||
/** Sparse map of property key → value (asset properties or scene-object overrides). */
|
||||
export type PropertyBag = Record<string, PropertyValue>;
|
||||
|
||||
/** A point in world or screen space. */
|
||||
export interface Vec2 {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a scene object sits: `x`/`y` in world units, `z` is the draw order. Higher `z` is
|
||||
* drawn on top; objects with equal `z` keep their hierarchy order.
|
||||
*/
|
||||
export interface Position extends Vec2 {
|
||||
z: number;
|
||||
}
|
||||
|
||||
/** Stable draw order: ascending `z`, ties keep the input (hierarchy) order. */
|
||||
export function sortByZ<T extends { z: number }>(items: readonly T[]): T[] {
|
||||
return [...items].sort((a, b) => a.z - b.z);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PropertyBag, Vec2 } from './Properties';
|
||||
import type { Position, PropertyBag, Vec2 } from './Properties';
|
||||
|
||||
/**
|
||||
* An asset placed inside a scene. It links to the asset by id, owns a position
|
||||
@@ -10,10 +10,12 @@ export interface SceneObject {
|
||||
assetId: string;
|
||||
/** Optional display name; falls back to the asset name when empty. */
|
||||
name: string;
|
||||
position: Vec2;
|
||||
/** World position plus draw order (`z`). */
|
||||
position: Position;
|
||||
overrides: PropertyBag;
|
||||
}
|
||||
|
||||
export function createSceneObject(id: string, assetId: string, position: Vec2): SceneObject {
|
||||
return { id, assetId, name: '', position: { x: position.x, y: position.y }, overrides: {} };
|
||||
/** Creates a scene object; `z` defaults to 0 when the caller only has a 2D point. */
|
||||
export function createSceneObject(id: string, assetId: string, position: Vec2 | Position): SceneObject {
|
||||
return { id, assetId, name: '', position: { x: position.x, y: position.y, z: 'z' in position ? position.z : 0 }, overrides: {} };
|
||||
}
|
||||
@@ -33,6 +33,17 @@ export function isOverridden(object: SceneObject, key: string): boolean {
|
||||
return object.overrides[key] !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the asset's own value for `key` differs from the schema default – the asset-level
|
||||
* counterpart of an override. The inspector highlights such values and offers a reset.
|
||||
*/
|
||||
export function isModified(asset: Asset, key: string): boolean {
|
||||
const def = schemaFor(asset.type).find((d) => d.key === key);
|
||||
if (!def) return false;
|
||||
const own = asset.props[key];
|
||||
return own !== undefined && own !== def.default;
|
||||
}
|
||||
|
||||
/** Value the object would have without its override for `key` (the asset's value). */
|
||||
export function inheritedValue(asset: Asset, key: string): PropertyValue | undefined {
|
||||
return resolveAssetProps(asset)[key];
|
||||
|
||||
@@ -67,7 +67,7 @@ function normalizeObjects(raw: unknown, types: Map<string, AssetType>): SceneObj
|
||||
id: o.id,
|
||||
assetId: o.assetId,
|
||||
name: typeof o.name === 'string' ? o.name : '',
|
||||
position: { x: toNumber(p.x), y: toNumber(p.y) },
|
||||
position: { x: toNumber(p.x), y: toNumber(p.y), z: toNumber(p.z) },
|
||||
overrides: normalizeBag(o.overrides, type, true),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user