Apply/revert buttons for overrides, container instances and child positions
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0b78382bcc
commit
3379232e64
7 files changed
+544
-37
No files matched your search
@@ -0,0 +1,196 @@
|
||||
import { instancesOf, type TemplateLink, templateLinkOf } from '../model/container';
|
||||
import type { Position, PropertyValue } from '../model/Properties';
|
||||
import { findScene, type Project } from '../model/Project';
|
||||
import type { SceneObject } from '../model/SceneObject';
|
||||
import { resolveSceneObjectProps } from '../properties/resolve';
|
||||
import { schemaForAsset } from '../properties/PropertySchema';
|
||||
import { type Command, CommandError } from './Command';
|
||||
import { requireAsset } from './assetCommands';
|
||||
import { requireObject } from './sceneObjectCommands';
|
||||
|
||||
/**
|
||||
* "Apply" is the opposite direction of an override: a value the user changed on a placed
|
||||
* object is written back to where it came from – the asset, or (for objects that are part
|
||||
* of a placed container) the container's template. The rule for instances follows the usual
|
||||
* prefab semantics: copies that were still at the template's value follow the change,
|
||||
* copies the user modified on their own keep their value.
|
||||
*/
|
||||
|
||||
type Axis = 'x' | 'y' | 'z';
|
||||
|
||||
function requireLink(project: Project, sceneId: string, object: SceneObject): TemplateLink {
|
||||
const scene = findScene(project, sceneId);
|
||||
const link = scene && templateLinkOf(project, scene, object);
|
||||
if (!link) throw new CommandError(`${object.name || object.id} is not part of a placed container`);
|
||||
return link;
|
||||
}
|
||||
|
||||
function setOverride(object: SceneObject, key: string, value: PropertyValue | undefined): void {
|
||||
if (value === undefined) delete object.overrides[key];
|
||||
else object.overrides[key] = value;
|
||||
}
|
||||
|
||||
/** Remembered override values of several objects, for undo. */
|
||||
interface OverrideMemo {
|
||||
object: SceneObject;
|
||||
previous: PropertyValue | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the template's override for `key` and moves every instance that still had the old
|
||||
* template value along with it. Returns what to restore on undo.
|
||||
*/
|
||||
function updateTemplateOverride(project: Project, link: TemplateLink, key: string, next: PropertyValue | undefined): OverrideMemo[] {
|
||||
const previous = link.template.overrides[key];
|
||||
const memos: OverrideMemo[] = [{ object: link.template, previous }];
|
||||
for (const { object } of instancesOf(project, link.template.id)) {
|
||||
if (object.overrides[key] !== previous) continue;
|
||||
memos.push({ object, previous: object.overrides[key] });
|
||||
setOverride(object, key, next);
|
||||
}
|
||||
setOverride(link.template, key, next);
|
||||
return memos;
|
||||
}
|
||||
|
||||
function restoreOverrides(memos: OverrideMemo[], key: string): void {
|
||||
for (const memo of [...memos].reverse()) setOverride(memo.object, key, memo.previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the object's value for `key` into its asset and drops the override – the object
|
||||
* follows the asset again, as does everything else without an override of its own. For a
|
||||
* container instance the template's override is dropped too (and instances that shared it).
|
||||
*/
|
||||
export class ApplyOverrideToAssetCommand implements Command {
|
||||
readonly label: string;
|
||||
private previousAssetValue: PropertyValue | undefined;
|
||||
private previousOverride: PropertyValue | undefined;
|
||||
private templateMemos: OverrideMemo[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
this.label = `Apply ${key} to asset`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const scene = findScene(project, this.sceneId);
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const asset = requireAsset(project, object.assetId);
|
||||
if (!schemaForAsset(asset).some((d) => d.key === this.key)) throw new CommandError(`Property "${this.key}" is not defined for ${asset.type}`);
|
||||
const value = resolveSceneObjectProps(asset, object)[this.key];
|
||||
this.previousAssetValue = asset.props[this.key];
|
||||
this.previousOverride = object.overrides[this.key];
|
||||
asset.props[this.key] = value!;
|
||||
delete object.overrides[this.key];
|
||||
const link = scene && templateLinkOf(project, scene, object);
|
||||
this.templateMemos = link ? updateTemplateOverride(project, link, this.key, undefined) : [];
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const asset = requireAsset(project, object.assetId);
|
||||
restoreOverrides(this.templateMemos, this.key);
|
||||
setOverride(object, this.key, this.previousOverride);
|
||||
if (this.previousAssetValue === undefined) delete asset.props[this.key];
|
||||
else asset.props[this.key] = this.previousAssetValue;
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes the object's override for `key` into its container template (instances at the old value follow). */
|
||||
export class ApplyOverrideToContainerCommand implements Command {
|
||||
readonly label: string;
|
||||
private memos: OverrideMemo[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
this.label = `Apply ${key} to container`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const link = requireLink(project, this.sceneId, object);
|
||||
this.memos = updateTemplateOverride(project, link, this.key, object.overrides[this.key]);
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
restoreOverrides(this.memos, this.key);
|
||||
}
|
||||
}
|
||||
|
||||
/** Puts the object's override for `key` back to what its container template says. */
|
||||
export class RevertOverrideToTemplateCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: PropertyValue | undefined;
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly key: string) {
|
||||
this.label = `Revert ${key}`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const link = requireLink(project, this.sceneId, object);
|
||||
this.previous = object.overrides[this.key];
|
||||
setOverride(object, this.key, link.template.overrides[this.key]);
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
setOverride(requireObject(project, this.sceneId, this.objectId), this.key, this.previous);
|
||||
}
|
||||
}
|
||||
|
||||
/** Remembered positions of several objects, for undo. */
|
||||
interface PositionMemo {
|
||||
object: SceneObject;
|
||||
previous: Position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the object's local position (one axis, or all) into its container template.
|
||||
* Instances whose axis still matched the template follow; the others keep their own value.
|
||||
*/
|
||||
export class ApplyPositionToContainerCommand implements Command {
|
||||
readonly label: string;
|
||||
private memos: PositionMemo[] = [];
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly axis?: Axis) {
|
||||
this.label = axis ? `Apply ${axis} to container` : 'Apply position to container';
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const link = requireLink(project, this.sceneId, object);
|
||||
const axes: Axis[] = this.axis ? [this.axis] : ['x', 'y', 'z'];
|
||||
const before = { ...link.template.position };
|
||||
this.memos = [{ object: link.template, previous: before }];
|
||||
for (const instance of instancesOf(project, link.template.id)) {
|
||||
const changed = axes.filter((a) => instance.object.position[a] === before[a]);
|
||||
if (changed.length === 0) continue;
|
||||
this.memos.push({ object: instance.object, previous: { ...instance.object.position } });
|
||||
for (const a of changed) instance.object.position[a] = object.position[a];
|
||||
}
|
||||
for (const a of axes) link.template.position[a] = object.position[a];
|
||||
}
|
||||
|
||||
undo(): void {
|
||||
for (const memo of [...this.memos].reverse()) memo.object.position = { ...memo.previous };
|
||||
}
|
||||
}
|
||||
|
||||
/** Puts the object's local position (one axis, or all) back to its container template. */
|
||||
export class RevertPositionToTemplateCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous: Position = { x: 0, y: 0, z: 0 };
|
||||
|
||||
constructor(private readonly sceneId: string, private readonly objectId: string, private readonly axis?: Axis) {
|
||||
this.label = axis ? `Revert ${axis}` : 'Revert position';
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const object = requireObject(project, this.sceneId, this.objectId);
|
||||
const link = requireLink(project, this.sceneId, object);
|
||||
this.previous = { ...object.position };
|
||||
for (const a of this.axis ? [this.axis] : (['x', 'y', 'z'] as Axis[])) object.position[a] = link.template.position[a];
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
requireObject(project, this.sceneId, this.objectId).position = { ...this.previous };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user