Refactor round 2: fold duplicated command and inspector code

- applyCommands sit on ObjectEditCommand with a revert hook instead of
  five copies of the same preamble and undo
- actionIf replaces six identical Reset/Apply menu blocks, addNameField
  the three name rows, assetLink the two asset links
- requireStructuralObject replaces ten two-line preambles and drops the
  duplicate holder lookup
- __gm.objectsOf had no caller; the SMB remnant in e2e.sh is gone

Proven: check.sh 797 tests, full e2e 633 checks, smokes 24/19/20 against
the same bundle.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Fable 5.1 committed 2026-09-12 16:33:24 +02:00
1 parent 6471671f7d
commit ce42d16657
13 files changed
+139 -223

No files matched your search

+2 -3
View File
@@ -16,9 +16,8 @@ LOGDIR=$DIR/logs
running() { systemctl is-active --quiet "$UNIT.service"; }
rel() { echo "${1#"$DIR"/}"; }
# Newest by name, not by mtime: the timestamped names sort chronologically and a share-mounted
# worktree can hand out surprising mtimes. The glob demands that shape, so a hand-dropped file
# in logs/ cannot outsort the newest run.
# Newest by name, not by mtime: the timestamped names sort chronologically, and the glob demands
# that shape, so a hand-dropped file in logs/ cannot outsort the newest run.
latest_log() { ls -1 "$LOGDIR"/e2e-[0-9]*.log 2>/dev/null | tail -1; }
start() {
+45 -74
View File
@@ -4,53 +4,48 @@
* instance's deviation again, which is all it takes for the value to follow the template – the
* content of a placed container is resolved from it, never copied (see `model/instance.ts`).
*/
import {
captureObject,
type InstanceTarget,
instanceTargetOf,
type ObjectMemo,
restoreObject,
} from '../model/instance';
import type { Asset } from '../model/Asset';
import type { InstanceTarget, ObjectRef } from '../model/instance';
import { type Axis, type Position, type PropertyValue, setBagValue } from '../model/Properties';
import type { Project } from '../model/Project';
import { resolveSceneObjectProps } from '../properties/resolve';
import { type Command, CommandError } from './Command';
import { requireAsset, requireHolder, requireObjectRef, requirePropDef } from './require';
import { CommandError } from './Command';
import { requireAsset, requirePropDef } from './require';
import { ObjectEditCommand } from './sceneObjectCommands';
const ALL_AXES: readonly Axis[] = ['x', 'y', 'z'];
function requireTarget(project: Project, holderId: string, objectId: string): InstanceTarget {
const target = instanceTargetOf(project, requireHolder(project, holderId), objectId);
if (!target) throw new CommandError('That object is not part of a placed container');
return target;
function requireTarget(ref: ObjectRef): InstanceTarget {
if (!ref.target) throw new CommandError('That object is not part of a placed container');
return ref.target;
}
/**
* Writes the object's value for `key` into its asset and drops the override. For content of a
* placed container the template's override goes too, so every instance follows the asset again.
*/
export class ApplyOverrideToAssetCommand implements Command {
export class ApplyOverrideToAssetCommand extends ObjectEditCommand {
readonly label: string;
private asset: Asset | undefined;
private previousAssetValue: PropertyValue | undefined;
private previousTemplateValue: PropertyValue | undefined;
private memo: ObjectMemo | undefined;
private template: InstanceTarget | undefined;
private previousTemplateValue: PropertyValue | undefined;
constructor(
private readonly holderId: string,
private readonly objectId: string,
holderId: string,
objectId: string,
private readonly key: string,
) {
super(holderId, objectId);
this.label = `Apply ${key} to asset`;
}
execute(project: Project): void {
const ref = requireObjectRef(project, this.holderId, this.objectId);
protected override apply(ref: ObjectRef, project: Project): void {
const asset = requireAsset(project, ref.object.assetId);
requirePropDef(asset, this.key);
const value = resolveSceneObjectProps(asset, ref.object)[this.key];
this.asset = asset;
this.previousAssetValue = asset.props[this.key];
this.memo = captureObject(ref);
asset.props[this.key] = value!;
ref.setOverride(this.key, undefined);
this.template = ref.target;
@@ -59,126 +54,102 @@ export class ApplyOverrideToAssetCommand implements Command {
delete this.template.template.overrides[this.key];
}
undo(project: Project): void {
const asset = requireAsset(
project,
requireObjectRef(project, this.holderId, this.objectId).object.assetId,
);
protected override revert(): void {
if (this.template) setBagValue(this.template.template.overrides, this.key, this.previousTemplateValue);
if (this.memo) restoreObject(this.memo);
setBagValue(asset.props, this.key, this.previousAssetValue);
if (this.asset) setBagValue(this.asset.props, this.key, this.previousAssetValue);
}
}
export class ApplyOverrideToContainerCommand implements Command {
export class ApplyOverrideToContainerCommand extends ObjectEditCommand {
readonly label: string;
private previous: PropertyValue | undefined;
private memo: ObjectMemo | undefined;
private target: InstanceTarget | undefined;
constructor(
private readonly holderId: string,
private readonly objectId: string,
holderId: string,
objectId: string,
private readonly key: string,
) {
super(holderId, objectId);
this.label = `Apply ${key} to container`;
}
execute(project: Project): void {
const target = requireTarget(project, this.holderId, this.objectId);
const ref = requireObjectRef(project, this.holderId, this.objectId);
protected override apply(ref: ObjectRef): void {
const target = requireTarget(ref);
this.target = target;
this.memo = captureObject(ref);
this.previous = target.template.overrides[this.key];
setBagValue(target.template.overrides, this.key, ref.object.overrides[this.key]);
// The deviation is gone the moment the template carries it, so the edit must go with it.
ref.setOverride(this.key, target.template.overrides[this.key]);
}
undo(): void {
protected override revert(): void {
if (this.target) setBagValue(this.target.template.overrides, this.key, this.previous);
if (this.memo) restoreObject(this.memo);
}
}
export class RevertOverrideToTemplateCommand implements Command {
export class RevertOverrideToTemplateCommand extends ObjectEditCommand {
readonly label: string;
private memo: ObjectMemo | undefined;
constructor(
private readonly holderId: string,
private readonly objectId: string,
holderId: string,
objectId: string,
private readonly key: string,
) {
super(holderId, objectId);
this.label = `Revert ${key}`;
}
execute(project: Project): void {
const target = requireTarget(project, this.holderId, this.objectId);
const ref = requireObjectRef(project, this.holderId, this.objectId);
this.memo = captureObject(ref);
ref.setOverride(this.key, target.base.overrides[this.key]);
}
undo(): void {
if (this.memo) restoreObject(this.memo);
protected override apply(ref: ObjectRef): void {
ref.setOverride(this.key, requireTarget(ref).base.overrides[this.key]);
}
}
/** Writes the object's local position – one axis, or all – into its container template. */
export class ApplyPositionToContainerCommand implements Command {
export class ApplyPositionToContainerCommand extends ObjectEditCommand {
readonly label: string;
private previous: Position = { x: 0, y: 0, z: 0 };
private memo: ObjectMemo | undefined;
private target: InstanceTarget | undefined;
constructor(
private readonly holderId: string,
private readonly objectId: string,
holderId: string,
objectId: string,
private readonly axis?: Axis,
) {
super(holderId, objectId);
this.label = axis ? `Apply ${axis} to container` : 'Apply position to container';
}
execute(project: Project): void {
const target = requireTarget(project, this.holderId, this.objectId);
const ref = requireObjectRef(project, this.holderId, this.objectId);
protected override apply(ref: ObjectRef): void {
const target = requireTarget(ref);
this.target = target;
this.memo = captureObject(ref);
this.previous = { ...target.template.position };
for (const axis of this.axis ? [this.axis] : ALL_AXES)
target.template.position[axis] = ref.object.position[axis];
ref.setPosition(ref.object.position);
}
undo(): void {
protected override revert(): void {
if (this.target) this.target.template.position = { ...this.previous };
if (this.memo) restoreObject(this.memo);
}
}
export class RevertPositionToTemplateCommand implements Command {
export class RevertPositionToTemplateCommand extends ObjectEditCommand {
readonly label: string;
private memo: ObjectMemo | undefined;
constructor(
private readonly holderId: string,
private readonly objectId: string,
holderId: string,
objectId: string,
private readonly axis?: Axis,
) {
super(holderId, objectId);
this.label = axis ? `Revert ${axis}` : 'Revert position';
}
execute(project: Project): void {
const target = requireTarget(project, this.holderId, this.objectId);
const ref = requireObjectRef(project, this.holderId, this.objectId);
this.memo = captureObject(ref);
protected override apply(ref: ObjectRef): void {
const base = requireTarget(ref).base;
const position = { ...ref.object.position };
for (const axis of this.axis ? [this.axis] : ALL_AXES) position[axis] = target.base.position[axis];
for (const axis of this.axis ? [this.axis] : ALL_AXES) position[axis] = base.position[axis];
ref.setPosition(position);
}
undo(): void {
if (this.memo) restoreObject(this.memo);
}
}
+2 -3
View File
@@ -7,7 +7,7 @@ import { removeById } from '../util/list';
import { buildAsset } from './assetCommands';
import { type Command, CommandError } from './Command';
import { nextAssetName } from './naming';
import { requireAsset, requireObject, requireHolder, requireStructural } from './require';
import { requireAsset, requireHolder, requireStructuralObject } from './require';
/**
* Creates a collider asset sized like the object (sprites: width × height; anything else: schema
@@ -25,8 +25,7 @@ export class AddColliderCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.targetId);
const target = requireObject(project, this.holderId, this.targetId);
const target = requireStructuralObject(project, holder, this.targetId);
const targetAsset = requireAsset(project, target.assetId);
if (targetAsset.type === 'collider') throw new CommandError('A collider cannot have a collider');
if (isLiveInstance(project, holder, target))
+3 -5
View File
@@ -17,7 +17,7 @@ import { removeById, type Removed, restoreAt } from '../util/list';
import { buildAsset } from './assetCommands';
import { type Command, CommandError } from './Command';
import { nextAssetName } from './naming';
import { requireObject, requireHolder, requireStructural } from './require';
import { requireHolder, requireStructuralObject } from './require';
/**
* Turns the selected objects and their descendants into a container asset placed in their stead, the
@@ -39,8 +39,7 @@ export class CreateContainerCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
const ids = [...new Set(this.objectIds)];
for (const id of ids) requireStructural(project, holder, id);
const selected = ids.map((id) => requireObject(project, this.holderId, id));
const selected = ids.map((id) => requireStructuralObject(project, holder, id));
if (selected.length === 0) throw new CommandError('Select at least one object to create a container');
const worlds = selected.map((o) => worldPosition(holder, o));
@@ -126,8 +125,7 @@ export class UnpackContainerCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
const object = requireStructuralObject(project, holder, this.objectId);
if (!isLiveInstance(project, holder, object))
throw new CommandError(`${object.name || 'That object'} is not a placed container`);
const created = materializeInstance(project, holder, object);
+8
View File
@@ -50,6 +50,14 @@ export function requireStructural(project: Project, holder: ObjectTree, objectId
throw new CommandError(`${name} comes from a container template; open the container to change it`);
}
/** A stored object the caller may restructure: template content is rejected before the lookup. */
export function requireStructuralObject(project: Project, holder: ObjectTree, objectId: string): SceneObject {
requireStructural(project, holder, objectId);
const object = findObject(holder, objectId);
if (!object) throw new CommandError(`Scene object ${objectId} not found`);
return object;
}
export function requireBehavior(project: Project, assetId: string): BehaviorAsset {
const asset = findAssetOf(project, assetId, 'behavior');
if (!asset) throw new CommandError(`Behavior ${assetId} not found`);
+13 -20
View File
@@ -20,15 +20,15 @@ import {
requireObjectRef,
requirePropDef,
requireHolder,
requireStructural,
requireStructuralObject,
} from './require';
/**
* An edit to one placed object. The base captures the object before the edit and puts it back on
* undo, so a subclass only says what changes. `applyCommands` deliberately does not use this: its
* commands carry extra undo state whose restore order matters.
* undo, so a subclass only says what changes – and, if it wrote somewhere else too, how to take
* that back (`revert`).
*/
abstract class ObjectEditCommand implements Command {
export abstract class ObjectEditCommand implements Command {
abstract readonly label: string;
private memo: ObjectMemo | undefined;
@@ -39,6 +39,8 @@ abstract class ObjectEditCommand implements Command {
protected abstract apply(ref: ObjectRef, project: Project): void;
protected revert(): void {}
execute(project: Project): void {
const ref = requireObjectRef(project, this.holderId, this.objectId);
this.memo = captureObject(ref);
@@ -46,6 +48,7 @@ abstract class ObjectEditCommand implements Command {
}
undo(): void {
this.revert();
if (this.memo) restoreObject(this.memo);
}
}
@@ -71,10 +74,7 @@ export class PlaceAssetCommand implements Command {
const asset = requireAsset(project, this.assetId);
const rejection = placementRejection(project, holder, asset);
if (rejection) throw new CommandError(rejection);
if (this.parentId) {
requireObject(project, this.holderId, this.parentId);
requireStructural(project, holder, this.parentId);
}
if (this.parentId) requireStructuralObject(project, holder, this.parentId);
const local = localPositionFor(holder, this.parentId, toPosition(this.position));
holder.objects.push(createSceneObject(this.objectId, this.assetId, local, this.parentId));
}
@@ -99,10 +99,7 @@ export class RemoveSceneObjectsCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
for (const id of this.objectIds) {
requireStructural(project, holder, id);
requireObject(project, this.holderId, id);
}
for (const id of this.objectIds) requireStructuralObject(project, holder, id);
this.removed = removeSubtrees(holder, this.objectIds);
}
@@ -226,14 +223,12 @@ export class ReorderObjectCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
const object = requireStructuralObject(project, holder, this.objectId);
if (this.beforeId === this.objectId)
throw new CommandError('An object cannot be moved in front of itself');
let at = holder.objects.length;
if (this.beforeId) {
requireStructural(project, holder, this.beforeId);
const before = requireObject(project, this.holderId, this.beforeId);
const before = requireStructuralObject(project, holder, this.beforeId);
if (before.parentId !== object.parentId)
throw new CommandError('An object can only be reordered among its own siblings');
at = holder.objects.indexOf(before);
@@ -271,12 +266,10 @@ export class SetParentCommand implements Command {
execute(project: Project): void {
const holder = requireHolder(project, this.holderId);
requireStructural(project, holder, this.objectId);
const object = requireObject(project, this.holderId, this.objectId);
const object = requireStructuralObject(project, holder, this.objectId);
if (this.parentId) {
if (this.parentId === this.objectId) throw new CommandError('An object cannot be its own parent');
requireStructural(project, holder, this.parentId);
if (isLiveInstance(project, holder, requireObject(project, this.holderId, this.parentId)))
if (isLiveInstance(project, holder, requireStructuralObject(project, holder, this.parentId)))
throw new CommandError('A placed container shows its template; unpack it to put objects inside');
if (isSelfOrAncestor(holder, this.objectId, this.parentId))
throw new CommandError('An object cannot be parented to one of its own children');
-1
View File
@@ -168,7 +168,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
};
(window as unknown as { __gm: unknown }).__gm = {
state,
objectsOf,
sceneObjects: () => (state.activeScene ? objectsOf(state.activeScene.id) : []),
locate,
locateAll,
+7 -14
View File
@@ -19,10 +19,10 @@ import { type StructFieldKind, VARIABLE_KINDS } from '../../core/properties/kind
import { type PropertyDef, schemaForAsset, type StructFieldDef } from '../../core/properties/PropertySchema';
import { markedAssetSchema } from '../../core/properties/deviation';
import { isModified, resolveAssetProps } from '../../core/properties/resolve';
import { Dropdown, HStack, Label, Menu, TextField, Theme } from '../../ui';
import { Dropdown, HStack, Label, Menu, Theme } from '../../ui';
import { aspectActions, sizeCommand } from './aspectAction';
import { createFieldRow, type FieldHandlers, labelledRow } from './fieldFactory';
import { addNote, addTitle, type InspectorHost } from './host';
import { actionIf, createFieldRow, type FieldHandlers } from './fieldFactory';
import { addNameField, addNote, addTitle, type InspectorHost } from './host';
import { buildVariable, kindMenu, type VariableEditing } from './variableRows';
import { listOpener, previewingHandlers, structFields } from './previewHandlers';
import { assetValue } from './valueSpecs';
@@ -49,15 +49,12 @@ export function buildAssetSection(host: InspectorHost, assetId: string): void {
const asset = findAsset(host.state.project, assetId);
if (!asset) return;
addTitle(host, `${asset.type.toUpperCase()} ASSET`);
host.nameField = new TextField(asset.name, {
addNameField(host, asset.name, {
onChange: (name) => {
if (name.trim()) host.state.execute(new RenameAssetCommand(assetId, name.trim()));
else host.nameField?.setValue(asset.name);
},
});
const { row } = labelledRow('Name');
row.add(host.nameField, 1);
host.body.add(row);
buildProps(host, asset, markedAssetSchema(asset));
if (asset.type === 'behavior') buildVariables(host, asset);
@@ -100,13 +97,9 @@ function buildProps(host: InspectorHost, asset: Asset, schema: readonly Property
),
reset: () => {
const target = findAsset(host.state.project, asset.id);
if (!target || !isModified(target, def.key)) return [];
return [
{
label: 'Reset to default',
onSelect: () => host.state.execute(new ResetAssetPropertyCommand(asset.id, def.key)),
},
];
return actionIf(target && isModified(target, def.key), 'Reset to default', () =>
host.state.execute(new ResetAssetPropertyCommand(asset.id, def.key)),
);
},
},
);
+3
View File
@@ -88,6 +88,9 @@ export interface FieldAction {
*/
export type FieldActions = () => FieldAction[];
export const actionIf = (offered: unknown, label: string, onSelect: () => void): FieldAction[] =>
offered ? [{ label, onSelect }] : [];
/** One trailing button of a row: its glyph, and what it currently offers. */
export interface FieldActionGroup {
glyph: string;
+4 -7
View File
@@ -6,9 +6,9 @@ import {
import type { TextureAsset, TextureFrame } from '../../core/model/Asset';
import { findAssetOf, type Project } from '../../core/model/Project';
import { frameRect, type FrameRect, normalizeFrameRect, setFrameRect } from '../../core/model/texture';
import { Button, TextField } from '../../ui';
import { createFieldRow, type FieldHandlers, labelledRow } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host';
import { Button } from '../../ui';
import { createFieldRow, type FieldHandlers } from './fieldFactory';
import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host';
import { previewingHandlers } from './previewHandlers';
const RECT_COMPONENTS = [
@@ -30,7 +30,7 @@ export function buildFrameSection(host: InspectorHost, assetId: string, frameId:
if (!asset || !frame) return;
addTitle(host, 'TEXTURE FRAME');
host.nameField = new TextField(frame.name, {
addNameField(host, frame.name, {
onChange: (name) => {
// The command rejects a blank name but cannot put the old text back into the field,
// so an empty one is not sent at all.
@@ -38,9 +38,6 @@ export function buildFrameSection(host: InspectorHost, assetId: string, frameId:
else host.nameField?.setValue(frame.name);
},
});
const { row } = labelledRow('Name');
row.add(host.nameField, 1);
host.body.add(row);
addNote(host, `Frame of the texture "${asset.name}".`);
addTitle(host, 'RECTANGLE (IMAGE PIXELS)');
+9 -2
View File
@@ -1,6 +1,6 @@
import { Label, type TextField, type VStack } from '../../ui';
import { Label, TextField, type TextFieldOptions, type VStack } from '../../ui';
import type { EditorState } from '../state';
import type { FieldContext, FieldRow } from './fieldFactory';
import { type FieldContext, type FieldRow, labelledRow } from './fieldFactory';
/** What the inspector sections build into: the panel body and the registry of rows. */
export interface InspectorHost extends FieldContext {
@@ -27,3 +27,10 @@ export function addNote(
export function addGap(host: InspectorHost): void {
host.body.add(new Label(''));
}
export function addNameField(host: InspectorHost, value: string, options: TextFieldOptions): void {
host.nameField = new TextField(value, options);
const { row } = labelledRow('Name');
row.add(host.nameField, 1);
host.body.add(row);
}
+42 -93
View File
@@ -37,12 +37,12 @@ import {
deviatesFromTemplate,
resolveSceneObjectProps,
} from '../../core/properties/resolve';
import { Button, TextField } from '../../ui';
import { Button } from '../../ui';
import { createContainer, removeObjects } from '../actions';
import { aspectActions, sizeCommand } from './aspectAction';
import { defSignature } from './assetSection';
import { createFieldRow, type FieldRow, labelledRow, type RowActions } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host';
import { actionIf, createFieldRow, type FieldRow, labelledRow, type RowActions } from './fieldFactory';
import { addGap, addNameField, addNote, addTitle, type InspectorHost } from './host';
import type { ListSlot } from '../state';
import { parentRow } from './parentRow';
import { listOpener, previewingHandlers, structFields } from './previewHandlers';
@@ -115,38 +115,14 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
const target = targetOf(project, holderId, objectId);
addTitle(host, `${asset.type.toUpperCase()} OBJECT`);
host.nameField = new TextField(object.name, {
addNameField(host, object.name, {
placeholder: asset.name,
onChange: (name) => host.state.execute(new RenameSceneObjectCommand(holderId, objectId, name.trim())),
});
const nameRow = labelledRow('Name');
nameRow.row.add(host.nameField, 1);
host.body.add(nameRow.row);
const assetRow = labelledRow('Asset');
assetRow.row.add(
new Button(asset.name, {
variant: 'ghost',
onClick: () => host.state.select({ kind: 'asset', assetId: asset.id }),
}),
1,
);
host.body.add(assetRow.row);
assetLink(host, 'Asset', asset);
host.body.add(parentRow(host, view, object, target !== undefined));
// Content of a placed container: values compare against the template, not just the asset.
if (target) {
const linkRow = labelledRow('Part of');
linkRow.row.add(
new Button(target.asset.name, {
variant: 'ghost',
onClick: () => host.state.select({ kind: 'asset', assetId: target.asset.id }),
}),
1,
);
host.body.add(linkRow.row);
}
if (target) assetLink(host, 'Part of', target.asset);
// The master switch, right where it is looked for: above everything type-specific.
const resolved = resolveSceneObjectProps(asset, object);
@@ -168,25 +144,13 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
write: (ref, value) => ref.setPosition(value),
command: (v) => new MoveSceneObjectCommand(holderId, objectId, v),
reset: () =>
moved()
? [
{
label: 'Reset to container',
onSelect: () =>
host.state.execute(new RevertPositionToTemplateCommand(holderId, objectId, axis)),
},
]
: [],
actionIf(moved(), 'Reset to container', () =>
host.state.execute(new RevertPositionToTemplateCommand(holderId, objectId, axis)),
),
apply: () =>
moved()
? [
{
label: 'Apply to container',
onSelect: () =>
host.state.execute(new ApplyPositionToContainerCommand(holderId, objectId, axis)),
},
]
: [],
actionIf(moved(), 'Apply to container', () =>
host.state.execute(new ApplyPositionToContainerCommand(holderId, objectId, axis)),
),
});
}
@@ -203,16 +167,12 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
// No apply: the offset never leaves the placed object.
reset: () => {
const current = currentRef(host.state, holderId, objectId)?.object;
if (!current || current.editorOffset[axis] === 0) return [];
return [
{
label: 'Reset to 0',
onSelect: () =>
host.state.execute(
new SetEditorOffsetCommand(holderId, objectId, { ...current.editorOffset, [axis]: 0 }),
),
},
];
if (!current) return [];
return actionIf(current.editorOffset[axis] !== 0, 'Reset to 0', () =>
host.state.execute(
new SetEditorOffsetCommand(holderId, objectId, { ...current.editorOffset, [axis]: 0 }),
),
);
},
});
}
@@ -232,6 +192,18 @@ export function buildObjectSection(host: InspectorHost, holderId: string, object
if (asset.type === 'container') contentSection(host, holderId, object, asset.objects.length);
}
function assetLink(host: InspectorHost, label: string, asset: Asset): void {
const { row } = labelledRow(label);
row.add(
new Button(asset.name, {
variant: 'ghost',
onClick: () => host.state.select({ kind: 'asset', assetId: asset.id }),
}),
1,
);
host.body.add(row);
}
/**
* What a placed container holds. While it follows its template the content cannot be changed here –
* unpacking writes it into the scene as ordinary objects, which cuts the link for good.
@@ -318,46 +290,23 @@ function propertyRow(
reset: () => {
const { template, asset } = bases();
return [
...(template
? [
{
label: 'Reset to container',
onSelect: () =>
host.state.execute(new RevertOverrideToTemplateCommand(holderId, objectId, def.key)),
},
]
: []),
...(asset
? [
{
label: 'Reset to asset',
onSelect: () => host.state.execute(new ClearOverrideCommand(holderId, objectId, def.key)),
},
]
: []),
...actionIf(template, 'Reset to container', () =>
host.state.execute(new RevertOverrideToTemplateCommand(holderId, objectId, def.key)),
),
...actionIf(asset, 'Reset to asset', () =>
host.state.execute(new ClearOverrideCommand(holderId, objectId, def.key)),
),
];
},
apply: () => {
const { template, applyToAsset } = bases();
return [
...(template
? [
{
label: 'Apply to container',
onSelect: () =>
host.state.execute(new ApplyOverrideToContainerCommand(holderId, objectId, def.key)),
},
]
: []),
...(applyToAsset
? [
{
label: 'Apply to asset',
onSelect: () =>
host.state.execute(new ApplyOverrideToAssetCommand(holderId, objectId, def.key)),
},
]
: []),
...actionIf(template, 'Apply to container', () =>
host.state.execute(new ApplyOverrideToContainerCommand(holderId, objectId, def.key)),
),
...actionIf(applyToAsset, 'Apply to asset', () =>
host.state.execute(new ApplyOverrideToAssetCommand(holderId, objectId, def.key)),
),
];
},
};
+1 -1
View File
@@ -16,7 +16,7 @@ export { type DropSpot, ListView, type ListItem, type PickModifiers } from './wi
export { Popup, type PopupOptions } from './widgets/Popup';
export { Menu, type MenuItem, type MenuOptions } from './widgets/Menu';
export { Dialog, type DialogButton } from './widgets/Dialog';
export { TextField } from './widgets/TextField';
export { TextField, type TextFieldOptions } from './widgets/TextField';
export { NumberField } from './widgets/NumberField';
export { ColorField } from './widgets/ColorField';
export { ColorPicker } from './widgets/ColorPicker';