transform: std/transform 2D: Transform2, GlobalTransform2 and propagation (step 3a of #89) #779
Labels
No labels
loop
queue/failed
queue/queued
ready
status/parked
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
torbscript/language#779
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Step 3 of 3 of #89 (ECS slice 1), first sub-step. The specification is ECS.md section 7 ("Hierarchy"), section 11 ("The package cut",
std/transform) and the owner's decision in section 15 q7 (GlobalTransform2is its own component, not a field ofTransform2). Steps 1 and 2 (#453, #763) are merged:std/ecshasColumn,Store,query/pairs,ParentanddepthOrder.std/linearexists onmain(Vector2,Angle,Matrix2,Matrix3, generic over the scalar).The whole of step 3 (2D and 3D) is more than one PR, so it is split into two issues, filed just in time: this one is 3a, the 2D half; 3b (
Transform3,GlobalTransform3,toMatrix4, 3D propagation) is filed when 3a has landed.Goal
A new package
std/transformwithTransform2andGlobalTransform2as separate components over the scalar, their arithmetic, and the one propagation system overdepthOrder, generic over the stores it reads and writes.Depends on
#453 (merged),
std/linearonmain. Independent of #763 (merged). No seed impact: no syntax, no native, no driver flag, no std name the compiler looks up by string (a new package, nothing renamed).Requirements
std/transform(std/transform/project.trbwith runtime dependenciesstd/linearandstd/ecs;std/ecsitself stays free ofstd/linear, ECS.md section 11) shall providepublic type Transform2<Scalar: Real = Float>withtranslation: Vector2<Scalar>,rotation: Angle<Scalar>,scale: Vector2<Scalar>,Equals, Show, andstatic identity(zero translation, zero rotation, scale one).Transform2shall providecomposed(with: Self): Self(the child transform written in this transform's space, soparent.composed(with: child)is the child's transform in the parent's parent space),inverse(): Self?(Nonewhen a scale component is zero),interpolated(toward: Self, by: Scalar): Self(translation and scale linear, rotation by the shortestAngledifference) andtoMatrix3(): Matrix3<Scalar>(affine, composed fromMatrix2.rotation,Matrix2.scalingand the translation).composedmust agree withtoMatrix3multiplication (tested on non-uniform scale with a rotation, which is the case a decomposed form cannot always represent: see the decision below).public type GlobalTransform2<Scalar: Real = Float>as its own component (q7) holding the world-spaceMatrix3<Scalar>(matrix), withEquals, Show,identity,transformedPoint(point)andtransformedDirection(direction)delegating to the matrix.query:public fn propagate<Space, Place, Parents, Scalar>(...)taking theStore<Transform2<Scalar>>to read, theStore<GlobalTransform2<Scalar>>to write, and theColumn<Parent>plus the entities order fromdepthOrder, so a program's own group supplies the stores. For each entity with aTransform2indepthOrder, itsGlobalTransform2is the parent's global matrix times its owntoMatrix3()(an entity without aParent, or whose parent has no global, uses its own matrix). One pass is enough because every parent precedes its children. The exact signature is the implementer's, within these semantics; noWorldtype is shipped (ECS.md q3).Float(the default) andFixed: the generics are bounded byRealasstd/linearis, and a test runs the propagation overFixedand shows the same global as an exact hand computation.# Examplesblock; the package is pure value arithmetic and does not import anything butstd/core,std/number,std/linear,std/ecs.Decisions (by the loop, refinement)
QuaternionandMatrix4; the first PR proves the package, the generics and the propagation form once, the second is mechanical. Each lands as one PR with two Conventional Commits.GlobalTransform2holds aMatrix3, not a decomposedTransform2. A parent with non-uniform scale and a rotated child produces a shear that a translation/rotation/scale triple cannot represent, so a decomposed global would be wrong exactly where it matters (Unity and Bevy hold a matrix for the same reason).Transform2stays decomposed because that is what an author sets and what interpolates.Transform2.composedis for the cases a triple can hold and is documented as exact for uniform parent scale; the global path always goes through matrices. Both are tested against each other.std/ecs: ECS.md section 11 puts it between slices 1 and 2 as its own package that owns the arithmetic whilestd/ecsowns the order.ComponentKey("std/transform/Transform2")strings for the access sets, as step 1 and 2 do;componentKey<T>()is #90.Acceptance criteria
std/transformexists withTransform2,GlobalTransform2andpropagateas above;torb check std/transformandtorb format --checkare clean;tools/check-commits.shclean.std/transform/tests/: identity is neutral forcomposedon both sides;inversecomposed with the transform gives identity within a tolerance, andNonefor a zero scale;interpolatedat 0, 1 and 0.5 (including a rotation across the pi boundary takes the short way);toMatrix3of a translation-only, rotation-only and scale-only transform equals the expected matrix.std/ecs/tests/groups.trbdoes): a three-level chain (root, child, grandchild) given in reverse order yields the expected globals in one pass; a root with non-uniform scale and a rotated child gives the matrix product, not a decomposed approximation; an entity withoutParenthas global equal to its own matrix; a parent cycle panics throughdepthOrder(existing message).Fixedequals the hand-computed values exactly.tests/conformance/transform-propagate.trbsteps a small hierarchy through aSchedule(fromstd/ecs) for several frames with a system that rotates the root and a propagation system, printing the globals each frame; tier B runs it in the VM and natively and both must givetransform-propagate.expectedbyte for byte; the.allocationsbudget is taken from the first forge CI run; a leak run is clean.docs/standard-library/transform.md(new page, with the pattern of a propagation system registered in a schedule as the program's own code),skills/regenerated (torb docs skill docs skills,--checkis a tier A gate); ECS.md section 11/13 notesstd/transform2D landed with #89 and this issue; #89 step 3 line updated by the PR text (Refs #89).Seed impact
None (see "Depends on").
Size and model
One PR, two Conventional Commits:
feat(std/transform): Transform2, GlobalTransform2 and propagation(package, tests, conformance program) anddocs(std/transform): standard library page, skills and the design record. Implementer: sonnet, medium (std/package, no compiler change).Part of #89 (
Refs #89, notCloses).std/ecswith oneStoreper group type, andstd/transform(ECS slice 1) #89std/ecswith oneStoreper group type, andstd/transform(ECS slice 1) #89[loop:torbscript] claim implement by session:loop-torbscript-20261114 until 2026-11-14T10:10Z
std/ecswith oneStoreper group type, andstd/transform(ECS slice 1) #89[loop:torbscript] implement: PR #780 opened (head
e234aeab); std/transform 2D (Transform2, GlobalTransform2, propagate) with 26 tests, two conformance programs, docs and skills. Native leg and .allocations for transform-propagate come from the first CI run; review requested from argus.