Split the object inspector, and sweep out the section banners

objectSection.ts had grown to 559 lines around three separable parts: the
queries, the vector rows and the parent row. Four object commands repeated the
same capture/restore preamble – applyCommands stays out, its five carry extra
undo state whose restore order matters.

58 dashed banner comments are gone. The nine labels in nodes/index.ts stay:
they are the only structure a flat 90-entry array has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rootandClaude Opus 5 committed 2026-09-10 07:14:45 +02:00
1 parent 11981b40d0
commit 3b3f374e10
11 files changed
+258 -244

No files matched your search

+8 -157
View File
@@ -5,7 +5,6 @@ import {
RevertOverrideToTemplateCommand,
RevertPositionToTemplateCommand,
} from '../../core/commands/applyCommands';
import type { Command } from '../../core/commands/Command';
import { UnpackContainerCommand } from '../../core/commands/containerCommands';
import {
ClearOverrideCommand,
@@ -13,21 +12,18 @@ import {
RenameSceneObjectCommand,
SetEditorOffsetCommand,
SetOverrideCommand,
SetParentCommand,
} from '../../core/commands/sceneObjectCommands';
import type { Asset } from '../../core/model/Asset';
import { findObject, isSelfOrAncestor, treeOrder } from '../../core/model/hierarchy';
import { findObject } from '../../core/model/hierarchy';
import {
type InstanceTarget,
instanceTargetOf,
isLiveInstance,
isTemplateContent,
objectRef,
type ObjectRef,
resolvedHolder,
} from '../../core/model/instance';
import type { Axis, PropertyBag, PropertyValue } from '../../core/model/Properties';
import { findAsset, findObjectHolder, type ObjectHolder, type Project } from '../../core/model/Project';
import type { PropertyValue } from '../../core/model/Properties';
import { findAsset, findObjectHolder, type Project } from '../../core/model/Project';
import type { SceneObject } from '../../core/model/SceneObject';
import {
COMMON_PROPS,
@@ -41,28 +37,15 @@ import {
deviatesFromTemplate,
resolveSceneObjectProps,
} from '../../core/properties/resolve';
import { Button, Dropdown, Label, Menu, type MenuItem, TextField } from '../../ui';
import { Button, TextField } from '../../ui';
import { addCollider, createContainer, removeObjects } from '../actions';
import { aspectActions, sizeCommand } from './aspectAction';
import { createFieldRow, type FieldActions, type FieldRow, labelledRow } from './fieldFactory';
import { createFieldRow, type FieldRow, labelledRow } from './fieldFactory';
import { addGap, addNote, addTitle, type InspectorHost } from './host';
import { parentRow } from './parentRow';
import { previewingHandlers } from './previewHandlers';
const POSITION_AXES: readonly Axis[] = ['x', 'y', 'z'];
const OFFSET_AXES = ['x', 'y'] as const;
const offsetKey = (axis: 'x' | 'y') => `editorOffset${axis.toUpperCase()}`;
/** The holder with the content of placed containers resolved in – what the inspector addresses. */
function viewOf(project: Project, holderId: string): ObjectHolder | undefined {
const holder = findObjectHolder(project, holderId);
return holder && resolvedHolder(project, holder);
}
function targetOf(project: Project, holderId: string, objectId: string): InstanceTarget | undefined {
const holder = findObjectHolder(project, holderId);
return holder && instanceTargetOf(project, holder, objectId);
}
import { currentProps, currentRef, deviations, positionMoved, targetOf, viewOf } from './objectQueries';
import { componentRow, OFFSET_AXES, offsetKey, POSITION_AXES } from './vectorRows';
/** Structure-defining facts of an object selection: parent, template link and schema. */
export function objectSignature(
@@ -124,7 +107,6 @@ export function buildObjectsSection(host: InspectorHost, holderId: string, objec
);
}
/** One object: name, asset, parent, container link, the switch, position, editor offset, properties, collider button. */
export function buildObjectSection(host: InspectorHost, holderId: string, objectId: string): void {
const { project } = host.state;
const view = viewOf(project, holderId);
@@ -311,48 +293,6 @@ export function refreshObjectSection(host: InspectorHost, holderId: string, obje
return true;
}
interface ComponentSpec<V extends object> {
key: string;
label: string;
axis: keyof V & string;
read: (ref: ObjectRef) => V;
write: (ref: ObjectRef, value: V) => void;
command: (value: V) => Command;
reset?: FieldActions;
apply?: FieldActions;
}
/**
* A number row editing one component of a vector on the object (position, editor offset).
* The row edits one axis, the command carries the whole vector: the other components come
* from the target after the preview was taken back, so they are the pre-preview ones.
*/
function componentRow<V extends object>(
host: InspectorHost,
holderId: string,
objectId: string,
value: number,
spec: ComponentSpec<V>,
): void {
const withAxis = (vector: V, next: PropertyValue): V => ({ ...vector, [spec.axis]: Number(next) });
const row = createFieldRow(
host,
{ key: spec.key, label: spec.label, kind: 'number', default: 0, step: 1 },
value,
previewingHandlers<ObjectRef, V>(host, {
target: () => currentRef(host.state, holderId, objectId),
// Copied: the snapshot outlives the scrub, the vector in the model does not.
read: (ref) => ({ ...spec.read(ref) }),
write: spec.write,
toModel: (next, ref) => withAxis(spec.read(ref), next),
commit: spec.command,
}),
{ reset: spec.reset, apply: spec.apply },
);
host.fields.set(spec.key, row);
host.body.add(row.row);
}
/**
* One editable property of a placed object: writes an override, offers ↺ (back to the asset
* or, inside a container, to the template) and ↑ (apply upwards).
@@ -444,43 +384,6 @@ function propertyRow(
return row;
}
/**
* "Parent: <name> ▾" – picks another object of the same scene or container, or none. The object's own
* subtree is excluded (that would be a cycle). For behaviors the parent is the owner. Content of a
* placed container only shows its parent: where it hangs is the template's business.
*/
function parentRow(host: InspectorHost, holder: ObjectHolder, object: SceneObject, fixed: boolean) {
const { row } = labelledRow('Parent');
const nameOf = (o: SceneObject) => o.name || findAsset(host.state.project, o.assetId)?.name || '?';
const parent = findObject(holder, object.parentId);
const name = parent ? nameOf(parent) : 'None';
if (fixed) {
row.add(new Label(name, { style: 'muted' }), 1);
return row;
}
const button = new Dropdown(name);
button.onClick = () => {
// Resolved anew on every open: `holder` is a derived copy of the scene, so one captured when
// the row was built lists the objects of that moment, not the ones there now.
const current = viewOf(host.state.project, holder.id) ?? holder;
const items: MenuItem[] = [
{ label: 'None', onSelect: () => host.state.execute(new SetParentCommand(holder.id, object.id, '')) },
];
for (const { object: candidate, depth } of treeOrder(current)) {
if (isSelfOrAncestor(current, object.id, candidate.id)) continue;
const label = `${' '.repeat(depth)}${nameOf(candidate)}`;
items.push({
label: candidate.id === object.parentId ? `● ${label}` : label,
onSelect: () => host.state.execute(new SetParentCommand(holder.id, object.id, candidate.id)),
});
}
// Searchable: this lists every object of the scene or container, which grows with the project.
new Menu(items, { searchable: true }).openBelow(button);
};
row.add(button, 1);
return row;
}
/**
* 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
@@ -505,55 +408,3 @@ function markObjectRows(
host.fields.get(def.key)?.setModified(deviates);
}
}
/**
* What the ↺/↑ menus may offer, each only when it actually differs. Reset goes to the nearest base
* only: dropping a deviation inside a container lands on the template, never past it. Evaluated on
* demand so a menu never acts on a stale snapshot.
*/
function deviations(
state: InspectorHost['state'],
holderId: string,
objectId: string,
key: string,
): { template: boolean; asset: boolean; applyToAsset: boolean } {
const view = viewOf(state.project, holderId);
const object = view && findObject(view, objectId);
const asset = object && findAsset(state.project, object.assetId);
if (!view || !object || !asset) return { template: false, asset: false, applyToAsset: false };
const target = targetOf(state.project, holderId, objectId);
const fromAsset = deviatesFromAsset(object, asset, key);
return {
template: Boolean(target) && deviatesFromTemplate(object, target!.base, key),
asset: !target && fromAsset,
applyToAsset: fromAsset,
};
}
/** Whether the object sits somewhere else on `axis` than its container template says. */
function positionMoved(
state: InspectorHost['state'],
holderId: string,
objectId: string,
axis: Axis,
): boolean {
const target = targetOf(state.project, holderId, objectId);
const object = currentRef(state, holderId, objectId)?.object;
return Boolean(target && object) && object!.position[axis] !== target!.base.position[axis];
}
function currentRef(
state: InspectorHost['state'],
holderId: string,
objectId: string,
): ObjectRef | undefined {
const holder = findObjectHolder(state.project, holderId);
return holder && objectRef(state.project, holder, objectId);
}
/** The values the object shows right now, asset defaults and overrides merged. */
function currentProps(state: InspectorHost['state'], holderId: string, objectId: string): PropertyBag | null {
const object = currentRef(state, holderId, objectId)?.object;
const asset = object && findAsset(state.project, object.assetId);
return object && asset ? resolveSceneObjectProps(asset, object) : null;
}