Consolidate duplicated editor code, trim comments, refresh README
Split GraphCanvas (1076 -> 831 lines) into EdgeLayer and graphMenus; the port and marker menus were the same list and are now one function. Value specs of the inspector and the list tab share valueSpecs.ts, and the remaining copies (compound commands, type glyphs, typed asset lookup, name rule, play/editor world setup, marquee) are merged. Four copies had already drifted apart; merging them fixes: - "Disconnect N links" in the port menu is one undo step, not N - "+ Add collider" is hidden on a placed container instance, as in the hierarchy menu, instead of always failing - a bool property coerced from the text "false" becomes false - a port command on an unknown node type reports instead of crashing Comment-only edits leave the bundle byte-identical. README corrected in 35 places (step limit, jump-run clock, suite duration, 409 body, ...). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9212e61c4c
commit
8f92a06c84
96 files changed
+999
-1261
No files matched your search
@@ -9,6 +9,8 @@ import {
|
||||
type PropertyValue,
|
||||
type StructValue,
|
||||
textOf,
|
||||
booleanOf,
|
||||
colorOf,
|
||||
} from '../model/Properties';
|
||||
|
||||
/** The inspector is generated from these entries, so a new property is one line in a schema below. */
|
||||
@@ -81,12 +83,7 @@ export interface PrimitiveFieldDef {
|
||||
kind: FieldKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list held by a struct field. Its items are primitives, and that asymmetry is the whole point:
|
||||
* a list may hold structs and a struct may hold lists, so only forbidding one of the two here
|
||||
* keeps the storable shapes finite. `list → struct → primitive` and `struct → list → primitive`
|
||||
* are the two deepest, which is exactly what `asStoredValue` lets through.
|
||||
*/
|
||||
/** A list held by a struct field; its items are primitives only, which keeps storable shapes finite. */
|
||||
export interface ListFieldDef {
|
||||
key: string;
|
||||
kind: 'list';
|
||||
@@ -116,6 +113,16 @@ export function isCommonProp(key: string): boolean {
|
||||
return COMMON_KEYS.has(key);
|
||||
}
|
||||
|
||||
/** A field lives inside its struct, so the reserved property keys do not apply to it. */
|
||||
export function isValidFieldName(name: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
||||
}
|
||||
|
||||
/** Identifier-like, and not a common property key – a variable "enabled" would shadow the switch. */
|
||||
export function isValidVariableName(name: string): boolean {
|
||||
return isValidFieldName(name) && !isCommonProp(name);
|
||||
}
|
||||
|
||||
/** Position is deliberately absent: it belongs to scene objects, not to asset types. */
|
||||
const ASSET_SCHEMAS: Record<AssetType, PropertySchema> = {
|
||||
scene: [{ key: 'backgroundColor', label: 'Background', kind: 'color', default: 0x2a2d34 }],
|
||||
@@ -262,10 +269,6 @@ const EMPTY_FIELD: Record<FieldKind, number | string | boolean> = {
|
||||
color: 0,
|
||||
};
|
||||
|
||||
export function emptyField(kind: FieldKind): number | string | boolean {
|
||||
return EMPTY_FIELD[kind];
|
||||
}
|
||||
|
||||
export function emptyFieldValue(field: StructFieldDef): FieldValue {
|
||||
return field.kind === 'list' ? [] : EMPTY_FIELD[field.kind];
|
||||
}
|
||||
@@ -283,7 +286,6 @@ export function structOf(fields: readonly StructFieldDef[], from: PropertyValue
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Clamps / coerces a raw value so it satisfies the definition. */
|
||||
export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyValue {
|
||||
switch (def.kind) {
|
||||
case 'number': {
|
||||
@@ -293,12 +295,10 @@ export function coerceValue(def: PropertyDef, value: PropertyValue): PropertyVal
|
||||
if (def.max !== undefined) n = Math.min(def.max, n);
|
||||
return n;
|
||||
}
|
||||
case 'color': {
|
||||
const n = typeof value === 'number' ? value : parseInt(textOf(value).replace('#', ''), 16);
|
||||
return Number.isFinite(n) ? Math.max(0, Math.min(0xffffff, Math.floor(n))) : def.default;
|
||||
}
|
||||
case 'color':
|
||||
return colorOf(value, def.default);
|
||||
case 'boolean':
|
||||
return Boolean(value);
|
||||
return booleanOf(value);
|
||||
case 'text':
|
||||
case 'key':
|
||||
case 'asset':
|
||||
@@ -333,16 +333,8 @@ export function coerceFieldValue(value: PropertyValue, field: StructFieldDef): F
|
||||
/** The primitive half of `coerceListItem`, split off because a struct field is never a struct. */
|
||||
export function coerceField(value: PropertyValue, items: FieldKind): number | string | boolean {
|
||||
if (items === 'text') return textOf(value);
|
||||
if (items === 'boolean') {
|
||||
if (typeof value === 'boolean') return value;
|
||||
// A list is typed as text, so the spelling has to count: Boolean('false') would be true.
|
||||
const s = textOf(value).trim().toLowerCase();
|
||||
return !(s === '' || s === 'false' || s === '0' || s === 'no');
|
||||
}
|
||||
if (items === 'color') {
|
||||
const c = typeof value === 'number' ? value : parseInt(textOf(value).replace('#', ''), 16);
|
||||
return Number.isFinite(c) ? Math.max(0, Math.min(0xffffff, Math.floor(c))) : 0;
|
||||
}
|
||||
if (items === 'boolean') return typeof value === 'boolean' ? value : booleanOf(textOf(value));
|
||||
if (items === 'color') return colorOf(value, 0);
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
@@ -50,10 +50,8 @@ export function matchesPixelSize(size: PixelSize, props: PropertyBag): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the sprite already has the picture's proportions. Compared as a cross product with a
|
||||
* relative tolerance, because fitting one side and then asking about the other must not answer
|
||||
* "no" over a rounding error: 3135 of the 40 000 sizes up to 200 × 200 fail an exact test after
|
||||
* their own fit (1 × 11 at height 100 gives 9.090909090909092, whose cross product is 100.000…1).
|
||||
* Cross product with a relative tolerance, because 3135 of the 40 000 sizes up to 200 × 200 fail an
|
||||
* exact test right after their own fit (rounding).
|
||||
*/
|
||||
export function matchesAspect(size: PixelSize, props: PropertyBag): boolean {
|
||||
const w = Number(props.width);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
/**
|
||||
* The `runInEditor` switch of a behavior (see `ASSET_SCHEMAS.behavior`). A behavior with it on
|
||||
* keeps running while the project is *not* playing, and then it edits the saved model: what it
|
||||
* moves or writes is a real change that is saved and is not on the undo stack. That is what makes
|
||||
* the switch useful (a behavior can build or arrange content) and why it is off by default.
|
||||
*/
|
||||
/** A behavior's `runInEditor` switch: its writes are real, saved and not undoable, hence off by default. */
|
||||
import type { ObjectTree } from '../model/hierarchy';
|
||||
import { resolveObjects } from '../model/instance';
|
||||
import type { PropertyBag } from '../model/Properties';
|
||||
|
||||
@@ -26,10 +26,6 @@ export function resolveSceneObjectProps(asset: Asset, object: SceneObject): Prop
|
||||
return bag;
|
||||
}
|
||||
|
||||
export function isOverridden(object: SceneObject, key: string): boolean {
|
||||
return object.overrides[key] !== undefined;
|
||||
}
|
||||
|
||||
/** Whether the asset's own value differs from the schema default – the asset-level override. */
|
||||
export function isModified(asset: Asset, key: string): boolean {
|
||||
const def = propDef(asset, key);
|
||||
|
||||
Reference in new issue
Block a user