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:
1 parent
6471671f7d
commit
ce42d16657
13 files changed
+139
-223
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)),
|
||||
),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user