game-maker / suite (push) Successful in 9m5s
Four new rows on sprite, text, collider and container (block TRANSFORM): scale, rotation in degrees clockwise, mirrorX, mirrorY. As properties they get override/reset/apply, loading of old projects, write-back after play and the Properties node for free; camera and behavior have none. The hierarchy composes T*R*S per level (composeAlongChain replaces sumAlongChain), so children rotate and scale with their parent. Reparenting and grouping keep the look (carriedTransform). Drawing goes through placeNode, the gizmo is a polygon, cull and objectRect use the AABB, drags are computed in the parent's system. Colliders are oriented boxes: SAT for box/box, box-circle in the box frame; exitDistance replaces axisSeparation/axisCorrection, moveAndSlide moves along the object's own axes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
152 lines
5.6 KiB
TypeScript
152 lines
5.6 KiB
TypeScript
/**
|
||
* Where an object is in the world: its position, rotation, scale and mirror applied inside its
|
||
* parent's, all the way up (`composeAlongChain`). The model side reads the transform rows off the
|
||
* asset and the object's overrides; the running world reads them off the entity's props.
|
||
*/
|
||
import type { Asset } from '../model/Asset';
|
||
import { findObject, type ObjectTree } from '../model/hierarchy';
|
||
import {
|
||
apply,
|
||
decompose,
|
||
determinant,
|
||
IDENTITY,
|
||
invert,
|
||
linearOf,
|
||
type Mat,
|
||
multiply,
|
||
sameLinear,
|
||
scaleOf,
|
||
angleOf,
|
||
transformMatrix,
|
||
} from '../model/matrix';
|
||
import { findAsset, type Project } from '../model/Project';
|
||
import type { Position, PropertyBag, Vec2 } from '../model/Properties';
|
||
import type { SceneObject } from '../model/SceneObject';
|
||
import { type ChainTransform, composeAlongChain } from '../model/walk';
|
||
import { hasTransformProps, TRANSFORM_PROPS } from './PropertySchema';
|
||
|
||
export interface LocalTransform {
|
||
rotation: number;
|
||
scale: number;
|
||
mirrorX: boolean;
|
||
mirrorY: boolean;
|
||
}
|
||
|
||
export const NEUTRAL: LocalTransform = { rotation: 0, scale: 1, mirrorX: false, mirrorY: false };
|
||
|
||
export function transformOf(props: PropertyBag): LocalTransform {
|
||
return {
|
||
rotation: finiteOr(props.rotation, 0),
|
||
scale: finiteOr(props.scale, 1),
|
||
mirrorX: props.mirrorX === true,
|
||
mirrorY: props.mirrorY === true,
|
||
};
|
||
}
|
||
|
||
function finiteOr(value: unknown, fallback: number): number {
|
||
const n = Number(value);
|
||
return Number.isFinite(n) ? n : fallback;
|
||
}
|
||
|
||
/** The object's own rows without resolving the whole bag: an override, else the asset's value. */
|
||
export function transformOfObject(asset: Asset, object: SceneObject): LocalTransform {
|
||
if (!hasTransformProps(asset.type)) return NEUTRAL;
|
||
const bag: PropertyBag = {};
|
||
for (const { key } of TRANSFORM_PROPS) {
|
||
const value = object.overrides[key] ?? asset.props[key];
|
||
if (value !== undefined) bag[key] = value;
|
||
}
|
||
return transformOf(bag);
|
||
}
|
||
|
||
export function localMatrixOf(position: Vec2, t: LocalTransform): Mat {
|
||
return transformMatrix(position.x, position.y, t.rotation, t.scale, t.mirrorX, t.mirrorY);
|
||
}
|
||
|
||
function localOf(project: Project, object: SceneObject, withEditorOffset: boolean): ChainTransform {
|
||
const asset = findAsset(project, object.assetId);
|
||
const t = asset ? transformOfObject(asset, object) : NEUTRAL;
|
||
const offset = withEditorOffset ? object.editorOffset : { x: 0, y: 0 };
|
||
const at = { x: object.position.x + offset.x, y: object.position.y + offset.y };
|
||
return { m: localMatrixOf(at, t), z: object.position.z };
|
||
}
|
||
|
||
function chain(project: Project, tree: ObjectTree, object: SceneObject, withEditorOffset: boolean) {
|
||
return composeAlongChain(
|
||
object,
|
||
(o) => (o.parentId ? findObject(tree, o.parentId) : undefined),
|
||
(o) => localOf(project, o, withEditorOffset),
|
||
);
|
||
}
|
||
|
||
export function worldTransform(project: Project, tree: ObjectTree, object: SceneObject): ChainTransform {
|
||
return chain(project, tree, object, false);
|
||
}
|
||
|
||
/**
|
||
* The world transform plus the editor offsets along the chain. A view aid only – the game,
|
||
* collisions and the graph all use `worldTransform`.
|
||
*/
|
||
export function editorTransform(project: Project, tree: ObjectTree, object: SceneObject): ChainTransform {
|
||
return chain(project, tree, object, true);
|
||
}
|
||
|
||
export function positionOf({ m, z }: ChainTransform): Position {
|
||
return { x: m.tx, y: m.ty, z };
|
||
}
|
||
|
||
export function worldPosition(project: Project, tree: ObjectTree, object: SceneObject): Position {
|
||
return positionOf(worldTransform(project, tree, object));
|
||
}
|
||
|
||
export function editorPosition(project: Project, tree: ObjectTree, object: SceneObject): Position {
|
||
return positionOf(editorTransform(project, tree, object));
|
||
}
|
||
|
||
/** The world transform of a parent ('' = top level, the identity). */
|
||
export function parentTransform(project: Project, tree: ObjectTree, parentId: string): ChainTransform {
|
||
const parent = parentId ? findObject(tree, parentId) : undefined;
|
||
return parent ? worldTransform(project, tree, parent) : { m: IDENTITY, z: 0 };
|
||
}
|
||
|
||
/** The local position that puts an object at `world` under the given parent ('' = top level). */
|
||
export function localPositionFor(
|
||
project: Project,
|
||
tree: ObjectTree,
|
||
parentId: string,
|
||
world: Position,
|
||
): Position {
|
||
const parent = parentTransform(project, tree, parentId);
|
||
const local = apply(invert(parent.m), world);
|
||
return { x: local.x, y: local.y, z: world.z - parent.z };
|
||
}
|
||
|
||
/**
|
||
* The rows that keep an object looking the same after the linear part above it changed from
|
||
* `before` to `after` (re-parenting, grouping). Nothing changes when nothing has to: an object
|
||
* moved between two upright parents keeps its own rows, mirror flags included.
|
||
*/
|
||
export function carriedTransform(own: LocalTransform, before: Mat, after: Mat): LocalTransform {
|
||
const carry = multiply(invert(linearOf(after)), linearOf(before));
|
||
if (sameLinear(carry, IDENTITY)) return own;
|
||
if (determinant(carry) > 0)
|
||
return {
|
||
...own,
|
||
rotation: own.rotation + (angleOf(carry) * 180) / Math.PI,
|
||
scale: own.scale * scaleOf(carry),
|
||
};
|
||
return { ...decompose(multiply(carry, localMatrixOf({ x: 0, y: 0 }, own))), mirrorY: false };
|
||
}
|
||
|
||
/** Writes the carried rows as overrides of a stored object; a type without the rows is left alone. */
|
||
export function carryTransform(asset: Asset, object: SceneObject, before: Mat, after: Mat): void {
|
||
if (!hasTransformProps(asset.type)) return;
|
||
const own = transformOfObject(asset, object);
|
||
const carried = carriedTransform(own, before, after);
|
||
if (carried === own) return;
|
||
object.overrides.scale = carried.scale;
|
||
object.overrides.rotation = carried.rotation;
|
||
object.overrides.mirrorX = carried.mirrorX;
|
||
object.overrides.mirrorY = carried.mirrorY;
|
||
}
|