Canvas-only 2D game editor/runtime built on PixiJS v8 + Vite + TypeScript. - core: asset model (Scene/Camera/Sprite), scene objects with sparse overrides, property schema + resolve, command stack (undo/redo), serialization and project store interface (16 unit tests) - ui: custom in-canvas widget toolkit (panels, fields, color picker, list/scroll, popup) with a hidden <input> keyboard bridge - editor: asset/hierarchy panels, schema-driven inspector, toolbar - viewport: box rendering, camera frame, selection gizmo, drag/zoom/pan - runtime: world/entity instantiation for play mode, input, camera follow - api: PHP JSON project store with revision counter (409 on conflict) - tools: CDP-based e2e smoke test Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
88 lines
2.8 KiB
TypeScript
88 lines
2.8 KiB
TypeScript
import type { PropertyValue } from '../../core/model/Properties';
|
||
import type { PropertyDef } from '../../core/properties/PropertySchema';
|
||
import { Button, Checkbox, ColorField, HStack, Label, NumberField, TextField, Theme, type Widget } from '../../ui';
|
||
|
||
export interface FieldHandlers {
|
||
/** Live value while dragging/typing – apply as a preview, no undo entry. */
|
||
onInput: (value: PropertyValue) => void;
|
||
/** Final value – commit as a command. */
|
||
onChange: (value: PropertyValue) => void;
|
||
}
|
||
|
||
export interface FieldRow {
|
||
row: Widget;
|
||
/** Push a model value into the widget (no events fired). */
|
||
setValue(value: PropertyValue): void;
|
||
/** Highlight the row as overridden (scene objects) and show the reset button. */
|
||
setOverridden(overridden: boolean): void;
|
||
}
|
||
|
||
/**
|
||
* Maps one PropertySchema entry to a labelled row containing the matching widget.
|
||
* The inspector is entirely generated through this function: adding a new property
|
||
* kind means adding a case here, adding a property means adding a schema line.
|
||
*/
|
||
export function createFieldRow(def: PropertyDef, value: PropertyValue, handlers: FieldHandlers, onReset?: () => void): FieldRow {
|
||
const row = new HStack({ gap: Theme.gap, align: 'center' });
|
||
row.minHeight = Theme.rowHeight;
|
||
const label = new Label(def.label, { style: 'muted' });
|
||
label.minWidth = 64;
|
||
row.add(label);
|
||
|
||
let setValue: (v: PropertyValue) => void;
|
||
|
||
switch (def.kind) {
|
||
case 'number': {
|
||
const field = new NumberField(Number(value), {
|
||
min: def.min,
|
||
max: def.max,
|
||
step: def.step ?? 1,
|
||
onInput: handlers.onInput,
|
||
onChange: handlers.onChange,
|
||
});
|
||
row.add(field, 1);
|
||
setValue = (v) => field.setNumber(Number(v));
|
||
break;
|
||
}
|
||
case 'text': {
|
||
const field = new TextField(String(value), { onInput: handlers.onInput, onChange: handlers.onChange });
|
||
row.add(field, 1);
|
||
setValue = (v) => field.setValue(String(v));
|
||
break;
|
||
}
|
||
case 'color': {
|
||
const field = new ColorField(Number(value), { onInput: handlers.onInput, onChange: handlers.onChange });
|
||
row.add(field, 1);
|
||
setValue = (v) => field.setValue(Number(v));
|
||
break;
|
||
}
|
||
case 'boolean': {
|
||
const field = new Checkbox(Boolean(value));
|
||
field.onChange = handlers.onChange;
|
||
row.add(field);
|
||
row.add(new Label(''), 1);
|
||
setValue = (v) => field.setChecked(Boolean(v));
|
||
break;
|
||
}
|
||
}
|
||
|
||
let reset: Button | null = null;
|
||
if (onReset) {
|
||
reset = new Button('↺', { variant: 'ghost', width: Theme.controlHeight, onClick: onReset });
|
||
reset.visible = false;
|
||
row.add(reset);
|
||
}
|
||
|
||
return {
|
||
row,
|
||
setValue,
|
||
setOverridden(overridden) {
|
||
label.setStyle(overridden ? 'bodyBold' : 'muted');
|
||
if (reset && reset.visible !== overridden) {
|
||
reset.visible = overridden;
|
||
row.relayout();
|
||
}
|
||
},
|
||
};
|
||
}
|