Keep behavior variables private to their own graph (#257)
A behavior often needs working state such as a timer or a counter. Until now every variable showed up on each placed object and in the Properties menu, so the inspector filled up with values nobody should touch. A private variable works like any other inside its graph but is not offered anywhere outside. - Each behavior variable gets a lock switch next to Keep. It starts off, so existing projects stay as they are (`privateVariables`). - A placed object shows no private variable, neither in the inspector nor while the game runs. If all are private, it says so. - Switching a variable to private drops its overrides on every placed object in one undo step. Switching back does not restore them. - A running game does not write a private value back as an override, and a project change during the run does not reset it. - `+ Property` no longer offers private variables. A port that names one by hand still reads and writes it without a warning. - The default value of a private variable is read-only in the asset inspector. Its shape (name, fields, type, delete) stays editable. - Keep is now a pin icon instead of a word. With the lock added, the header of a list variable had no room left and the lock covered `+ Item`. Not done: the animation track picker still offers private variables. Tests: ./check.sh -> 186 files, 2711 tests, CHECK EXIT 0. e2e 1,19,20,21,56 -> ALL PASSED, 119 checks. New module 56 has 7 checks with a real mouse. Counter-checks fail as expected: unit without the write-back filter, the sync guard, dropping overrides or loading the flag. Browser with all variables on the object, an unfiltered menu, or no value lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf6565d108
commit
e8b3ac0a3b
16 files changed
+455
-49
No files matched your search
@@ -5,7 +5,13 @@ import {
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
} from '../model/Properties';
|
||||
import { isKeptVariable, setKeptVariable, type VariableOwnerAsset } from '../model/Asset';
|
||||
import {
|
||||
isKeptVariable,
|
||||
isPrivateVariable,
|
||||
setKeptVariable,
|
||||
setPrivateVariable,
|
||||
type VariableOwnerAsset,
|
||||
} from '../model/Asset';
|
||||
import { overrideSites } from '../model/instance';
|
||||
import type { Project } from '../model/Project';
|
||||
import type { VariableKind } from '../properties/kinds';
|
||||
@@ -93,7 +99,12 @@ function restoreOverrides(
|
||||
/** Removes a variable together with its default and every instance override. */
|
||||
export class RemoveVariableCommand implements Command {
|
||||
readonly label: string;
|
||||
private removed?: { def: Removed<PropertyDef>; value: PropertyValue | undefined; kept: boolean };
|
||||
private removed?: {
|
||||
def: Removed<PropertyDef>;
|
||||
value: PropertyValue | undefined;
|
||||
kept: boolean;
|
||||
hidden: boolean;
|
||||
};
|
||||
private overrides: TakenOverride[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -108,9 +119,13 @@ export class RemoveVariableCommand implements Command {
|
||||
const def = removeFirst(asset.variables, (v) => v.key === this.key);
|
||||
if (!def) throw new CommandError(`Variable "${this.key}" not found`);
|
||||
const kept = asset.type === 'behavior' && isKeptVariable(asset, this.key);
|
||||
this.removed = { def, value: asset.props[this.key], kept };
|
||||
const hidden = isPrivateVariable(asset, this.key);
|
||||
this.removed = { def, value: asset.props[this.key], kept, hidden };
|
||||
delete asset.props[this.key];
|
||||
if (asset.type === 'behavior') setKeptVariable(asset, this.key, false);
|
||||
if (asset.type === 'behavior') {
|
||||
setKeptVariable(asset, this.key, false);
|
||||
setPrivateVariable(asset, this.key, false);
|
||||
}
|
||||
this.overrides = takeOverrides(project, this.assetId, this.key);
|
||||
}
|
||||
|
||||
@@ -119,7 +134,10 @@ export class RemoveVariableCommand implements Command {
|
||||
const asset = requireVariableOwner(project, this.assetId);
|
||||
restoreAt(asset.variables, [this.removed.def]);
|
||||
if (this.removed.value !== undefined) asset.props[this.key] = this.removed.value;
|
||||
if (this.removed.kept && asset.type === 'behavior') setKeptVariable(asset, this.key, true);
|
||||
if (asset.type === 'behavior') {
|
||||
if (this.removed.kept) setKeptVariable(asset, this.key, true);
|
||||
if (this.removed.hidden) setPrivateVariable(asset, this.key, true);
|
||||
}
|
||||
restoreOverrides(project, this.assetId, this.key, this.overrides);
|
||||
}
|
||||
}
|
||||
@@ -143,9 +161,12 @@ export class RenameVariableCommand extends RenameCommand {
|
||||
if (asset.variables.some((v) => v.key === to)) throw new CommandError(`Variable "${to}" already exists`);
|
||||
def.key = to;
|
||||
def.label = to;
|
||||
// Renamed in place, so the switch keeps its spot in the list.
|
||||
if (asset.type === 'behavior' && asset.keptVariables)
|
||||
asset.keptVariables = asset.keptVariables.map((k) => (k === from ? to : k));
|
||||
// Renamed in place, so the switches keep their spot in the list.
|
||||
if (asset.type === 'behavior') {
|
||||
if (asset.keptVariables) asset.keptVariables = asset.keptVariables.map((k) => (k === from ? to : k));
|
||||
if (asset.privateVariables)
|
||||
asset.privateVariables = asset.privateVariables.map((k) => (k === from ? to : k));
|
||||
}
|
||||
if (asset.props[from] !== undefined) {
|
||||
asset.props[to] = asset.props[from]!;
|
||||
delete asset.props[from];
|
||||
@@ -185,6 +206,38 @@ export class SetVariableKeptCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a behavior's variable is seen outside its own graph (#257). Going private drops the
|
||||
* overrides of every placed object: nobody would see them any more, yet they would still act.
|
||||
*/
|
||||
export class SetVariablePrivateCommand implements Command {
|
||||
readonly label: string;
|
||||
private previous = false;
|
||||
private overrides: TakenOverride[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly assetId: string,
|
||||
private readonly key: string,
|
||||
private readonly hidden: boolean,
|
||||
) {
|
||||
this.label = hidden ? `Make ${key} private` : `Make ${key} public`;
|
||||
}
|
||||
|
||||
execute(project: Project): void {
|
||||
const asset = requireBehavior(project, this.assetId);
|
||||
if (!asset.variables.some((v) => v.key === this.key))
|
||||
throw new CommandError(`Variable "${this.key}" not found`);
|
||||
this.previous = isPrivateVariable(asset, this.key);
|
||||
setPrivateVariable(asset, this.key, this.hidden);
|
||||
this.overrides = this.hidden ? takeOverrides(project, this.assetId, this.key) : [];
|
||||
}
|
||||
|
||||
undo(project: Project): void {
|
||||
setPrivateVariable(requireBehavior(project, this.assetId), this.key, this.previous);
|
||||
restoreOverrides(project, this.assetId, this.key, this.overrides);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A struct's fields are no properties of anything placed, so the reserved property names are free
|
||||
* there – as they are for a struct field.
|
||||
|
||||
+27
-3
@@ -37,14 +37,20 @@ export interface SceneAsset extends AssetBase {
|
||||
* `keptVariables` names the variables a `Run behavior` call hands on to the next call from the same
|
||||
* node on the same object (#229); the others start at their default every call. Absent keeps none,
|
||||
* so a project from before the switch runs as it did, and a new variable starts out not kept.
|
||||
*
|
||||
* `privateVariables` names the variables only the behavior's own graph sees (#257): a placed object
|
||||
* neither shows nor overrides them, and a `Properties` node does not offer them. Absent is none.
|
||||
*/
|
||||
export interface BehaviorAsset extends AssetBase {
|
||||
type: 'behavior';
|
||||
variables: PropertyDef[];
|
||||
keptVariables?: string[];
|
||||
privateVariables?: string[];
|
||||
graph: Graph;
|
||||
}
|
||||
|
||||
type VariableSwitch = 'keptVariables' | 'privateVariables';
|
||||
|
||||
/** Whether a call of this behavior hands `key` on to the next call (#229). */
|
||||
export function isKeptVariable(asset: BehaviorAsset, key: string): boolean {
|
||||
return asset.keptVariables?.includes(key) === true;
|
||||
@@ -52,10 +58,28 @@ export function isKeptVariable(asset: BehaviorAsset, key: string): boolean {
|
||||
|
||||
/** Writes the switch for one variable; an empty list is dropped rather than stored. */
|
||||
export function setKeptVariable(asset: BehaviorAsset, key: string, on: boolean): void {
|
||||
const rest = (asset.keptVariables ?? []).filter((k) => k !== key);
|
||||
setSwitch(asset, 'keptVariables', key, on);
|
||||
}
|
||||
|
||||
/** Whether `key` is hidden from everything outside the behavior's own graph (#257). */
|
||||
export function isPrivateVariable(asset: Asset, key: string): boolean {
|
||||
return asset.type === 'behavior' && asset.privateVariables?.includes(key) === true;
|
||||
}
|
||||
|
||||
export function setPrivateVariable(asset: BehaviorAsset, key: string, on: boolean): void {
|
||||
setSwitch(asset, 'privateVariables', key, on);
|
||||
}
|
||||
|
||||
/** The variables a placed object shows and overrides: all but the private ones. */
|
||||
export function publicVariables(asset: BehaviorAsset): PropertyDef[] {
|
||||
return asset.variables.filter((def) => !isPrivateVariable(asset, def.key));
|
||||
}
|
||||
|
||||
function setSwitch(asset: BehaviorAsset, list: VariableSwitch, key: string, on: boolean): void {
|
||||
const rest = (asset[list] ?? []).filter((k) => k !== key);
|
||||
const next = on ? [...rest, key] : rest;
|
||||
if (next.length > 0) asset.keptVariables = next;
|
||||
else delete asset.keptVariables;
|
||||
if (next.length > 0) asset[list] = next;
|
||||
else delete asset[list];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -184,9 +184,14 @@ export function deserializeProject(raw: unknown, fallbackId = ''): Project {
|
||||
graph: normalizeGraph(a.graph),
|
||||
};
|
||||
// Only names that are declared, each once, in declaration order.
|
||||
const kept = new Set(Array.isArray(a.keptVariables) ? a.keptVariables : []);
|
||||
const keptVariables = declared.map((v) => v.key).filter((key) => kept.has(key));
|
||||
const declaredOf = (list: unknown): string[] => {
|
||||
const named = new Set(Array.isArray(list) ? list : []);
|
||||
return declared.map((v) => v.key).filter((key) => named.has(key));
|
||||
};
|
||||
const keptVariables = declaredOf(a.keptVariables);
|
||||
if (keptVariables.length > 0) behavior.keptVariables = keptVariables;
|
||||
const privateVariables = declaredOf(a.privateVariables);
|
||||
if (privateVariables.length > 0) behavior.privateVariables = privateVariables;
|
||||
assets.push(behavior);
|
||||
} else if (type === 'variables') {
|
||||
const shared: VariablesAsset = { id, type, name, props, variables: variables.get(id) ?? [] };
|
||||
|
||||
Reference in new issue
Block a user