Skip to content

Commit 44a0685

Browse files
feat(stdlib): PixiJS 8.x Container transform-and-event surface (bindings #1) (#502)
## Summary Extends the Tier-1 PixiJS binding (#446 row #1) with the 11 most-load-bearing accessors + on/off pointer-event registration. This is the **largest single chunk** of idaptik's `src/bindings/Pixi.res` surface still not bound — 215 `src/app/*.res` files depend on Container transforms and FederatedPointerEvent handlers, so this PR is a forward unblocker for the ReScript→AffineScript migration. ## What lands `stdlib/Pixi.affine` (+46 lines): 11 new `extern fn`s. | Surface | Externs | |---|---| | Container transforms | `pixiContainerSetScale`, `SetPivot`, `SetRotation`, `SetAlpha`, `SetZIndex`, `SetSortableChildren`, `SetEventMode`, `SetCursor` | | FederatedPointerEvent registration | `pixiContainerOn`, `pixiContainerOff` | | Sprite | `pixiSpriteSetAnchor` | `lib/codegen_deno.ml` (+22 lines): 11 `__as_*` prelude helpers + 11 entries in the existing `deno_builtins` dispatch block, matching the existing wasmCall/motion/pixi pattern. `tests/codegen-deno/pixi_smoke.{affine,harness.mjs}` (+80 lines combined): new `smokeAccessorsFlow` exercises every new extern via the existing harness pattern. `MockContainer` grows `scale`/`pivot` Point mocks + handler `Map` + the new fields; asserts handler identity is preserved across `on(...)` → `off(...)`. `docs/bindings-roadmap.adoc` row #1 status note expanded — Container 8.x transform-and-event surface promoted from "deferred" to "landed"; remaining deferred items (typed `FederatedPointerEvent` accessors, `parent` read accessor with Option-null handling, Point/Rectangle/Circle helper types, sprite atlases, filters, hitArea) listed explicitly. ## Design notes **Why `pub extern fn pixiContainerSetEventMode(c, mode: String)` rather than a sum type?** Pixi 8's `eventMode` values are open strings (`"static"` / `"dynamic"` / `"passive"` / `"none"` / `"auto"`). A sum type would either freeze the set or require codegen-tagged-variant lowering that doesn't yet exist on the Deno-ESM backend (deferred to json.affine v0.3, mirroring the `WasmValue` decision in #467). Matches the existing pattern in `stdlib/PixiUI.affine` for `slider.orientation`. **Why `handler: Json`?** The `FederatedPointerEvent` reaches the handler as a JS object; AffineScript-side code uses existing `Json` accessors to read `e.global.x`, `e.target`, etc. A typed `FederatedPointerEvent` extern type with dedicated accessor `extern fn`s is the natural follow-up — captured in the roadmap row as a deferred item, not in this PR's scope. The Json handler avoids forcing every caller through a typed surface they may not want. **Anchor is on Sprite, not Container.** Pixi 8 keeps the same split as 7.x — `Container` has no `anchor`. The binding mirrors that with `pixiSpriteSetAnchor` rather than putting it on `pixiContainerSetAnchor`. ## Test plan - [x] `dune build bin/main.exe` — clean (only the expected parser warnings) - [x] `dune runtest` — 354 tests pass - [x] `tools/run_codegen_deno_tests.sh` — all 17 harnesses including extended `pixi_smoke.harness.mjs` OK - [ ] CI build job - [ ] CI `tools/run_codegen_deno_tests.sh` job - [ ] CI governance + Hypatia (known baselines per repo CLAUDE.md may be red — those are not from this PR) ## Refs - Umbrella: #446 (Tier 1 — idaptik blockers) - Tier-1 sub-issue: #450 - Row updated: `docs/bindings-roadmap.adoc` row #1 - Prior PixiJS-related PRs for context: #429 (restart on Deno-ESM), #435 (@pixi/ui MVP), #436 (motion ●), #437 (@pixi/sound) - Compile-time pattern doc: `docs/specs/zig-ffi-patterns.adoc` (PR #474 — non-conflicting siblings) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent a5e0fb1 commit 44a0685

5 files changed

Lines changed: 146 additions & 6 deletions

File tree

docs/bindings-roadmap.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ no further significant ReScript → AffineScript work is tractable.
4949

5050
|1
5151
|*PixiJS core* (Application, Container, Sprite, Graphics, Text, Ticker, FederatedEvent)
52-
|`◑` partial (Application / Container / Sprite / Graphics / Text / Texture / Ticker landed; FederatedEvent + extensive accessor coverage deferred)
52+
|`◑` partial (Application / Container / Sprite / Graphics / Text / Texture / Ticker landed; Container 8.x transform-and-event surface — scale / pivot / rotation / alpha / zIndex / sortableChildren / eventMode / cursor + on/off pointer-event registration + Sprite anchor — landed; opaque `FederatedPointerEvent` Json accessors + `parent` read accessor + Point/Rectangle/Circle helper types + sprite atlases + filters + hitArea deferred)
5353
|`stdlib/Pixi.affine` (eventual home: `affinescript-pixijs` as a separate repo per the SNIFs/typed-wasm precedent)
54-
|idaptik `src/bindings/Pixi.res`; all 215 `src/app/*.res` files depend on this. Restarted 2026-05-28 — the prior `affinescript-pixijs/` directory used the obsolete `.as` extension + AGPL-3.0-or-later headers + a Zig→C→WASM-import architecture incompatible with the Deno-ESM emitter; it was deleted and the surface rebuilt in `stdlib/Pixi.affine` matching the wasmCall / motion pattern. Test fixture: `tests/codegen-deno/pixi_smoke.{affine,harness.mjs}`. Follow-ups: extensive Container accessors (anchor, scale, pivot, parent, zIndex, eventMode, filters, hitArea); FederatedEvent + on/off pointer-event surface; Point/Rectangle/Circle helper types; sprite atlases.
54+
|idaptik `src/bindings/Pixi.res`; all 215 `src/app/*.res` files depend on this. Restarted 2026-05-28 — the prior `affinescript-pixijs/` directory used the obsolete `.as` extension + AGPL-3.0-or-later headers + a Zig→C→WASM-import architecture incompatible with the Deno-ESM emitter; it was deleted and the surface rebuilt in `stdlib/Pixi.affine` matching the wasmCall / motion pattern. Container accessor + on/off expansion landed 2026-05-31. Test fixture: `tests/codegen-deno/pixi_smoke.{affine,harness.mjs}` exercises every shipped extern, including `smokeAccessorsFlow` over the 11-extern 8.x transform-and-event addition. Follow-ups: typed `FederatedPointerEvent` accessors (read global/client x,y; target; stopPropagation); `parent` read accessor with Option-null handling; Point/Rectangle/Circle helper types; sprite atlases; filters; hitArea.
5555

5656
|2
5757
|*@pixi/sound* (`Sound.from`, play, stop, volume, setVolume, loop)

lib/codegen_deno.ml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,20 @@ const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container();
275275
const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; };
276276
const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; };
277277
const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; };
278+
const __as_pixiContainerSetScale = (c, x, y) => { c.scale.set(x, y); return 0; };
279+
const __as_pixiContainerSetPivot = (c, x, y) => { c.pivot.set(x, y); return 0; };
280+
const __as_pixiContainerSetRotation = (c, rad) => { c.rotation = rad; return 0; };
281+
const __as_pixiContainerSetAlpha = (c, a) => { c.alpha = a; return 0; };
282+
const __as_pixiContainerSetZIndex = (c, z) => { c.zIndex = z; return 0; };
283+
const __as_pixiContainerSetSortableChildren = (c, v) => { c.sortableChildren = v; return 0; };
284+
const __as_pixiContainerSetEventMode = (c, mode) => { c.eventMode = mode; return 0; };
285+
const __as_pixiContainerSetCursor = (c, cursor) => { c.cursor = cursor; return 0; };
278286
const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; };
287+
const __as_pixiContainerOn = (c, event, handler) => { c.on(event, handler); return 0; };
288+
const __as_pixiContainerOff = (c, event, handler) => { c.off(event, handler); return 0; };
279289
const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; };
280290
const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t);
291+
const __as_pixiSpriteSetAnchor = (s, x, y) => { s.anchor.set(x, y); return 0; };
281292
// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/
282293
// Text actual Container subclasses, so the JS object is the same.
283294
const __as_pixiSpriteAsContainer = (s) => s;
@@ -516,9 +527,20 @@ let () =
516527
b "pixiContainerAddChild" (fun a -> Printf.sprintf "__as_pixiContainerAddChild(%s, %s)" (arg 0 a) (arg 1 a));
517528
b "pixiContainerRemoveChild" (fun a -> Printf.sprintf "__as_pixiContainerRemoveChild(%s, %s)" (arg 0 a) (arg 1 a));
518529
b "pixiContainerSetPosition" (fun a -> Printf.sprintf "__as_pixiContainerSetPosition(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
530+
b "pixiContainerSetScale" (fun a -> Printf.sprintf "__as_pixiContainerSetScale(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
531+
b "pixiContainerSetPivot" (fun a -> Printf.sprintf "__as_pixiContainerSetPivot(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
532+
b "pixiContainerSetRotation" (fun a -> Printf.sprintf "__as_pixiContainerSetRotation(%s, %s)" (arg 0 a) (arg 1 a));
533+
b "pixiContainerSetAlpha" (fun a -> Printf.sprintf "__as_pixiContainerSetAlpha(%s, %s)" (arg 0 a) (arg 1 a));
534+
b "pixiContainerSetZIndex" (fun a -> Printf.sprintf "__as_pixiContainerSetZIndex(%s, %s)" (arg 0 a) (arg 1 a));
535+
b "pixiContainerSetSortableChildren" (fun a -> Printf.sprintf "__as_pixiContainerSetSortableChildren(%s, %s)" (arg 0 a) (arg 1 a));
536+
b "pixiContainerSetEventMode" (fun a -> Printf.sprintf "__as_pixiContainerSetEventMode(%s, %s)" (arg 0 a) (arg 1 a));
537+
b "pixiContainerSetCursor" (fun a -> Printf.sprintf "__as_pixiContainerSetCursor(%s, %s)" (arg 0 a) (arg 1 a));
519538
b "pixiContainerSetVisible" (fun a -> Printf.sprintf "__as_pixiContainerSetVisible(%s, %s)" (arg 0 a) (arg 1 a));
539+
b "pixiContainerOn" (fun a -> Printf.sprintf "__as_pixiContainerOn(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
540+
b "pixiContainerOff" (fun a -> Printf.sprintf "__as_pixiContainerOff(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
520541
b "pixiContainerDestroy" (fun a -> Printf.sprintf "__as_pixiContainerDestroy(%s)" (arg 0 a));
521542
b "pixiSpriteFrom" (fun a -> Printf.sprintf "__as_pixiSpriteFrom(%s)" (arg 0 a));
543+
b "pixiSpriteSetAnchor" (fun a -> Printf.sprintf "__as_pixiSpriteSetAnchor(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
522544
b "pixiSpriteAsContainer" (fun a -> Printf.sprintf "__as_pixiSpriteAsContainer(%s)" (arg 0 a));
523545
b "pixiTextureFromUrl" (fun a -> Printf.sprintf "__as_pixiTextureFromUrl(%s)" (arg 0 a));
524546
b "pixiGraphicsNew" (fun _ -> "__as_pixiGraphicsNew()");

stdlib/Pixi.affine

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,50 @@ pub extern fn pixiContainerRemoveChild(parent: Container, child: Container) -> I
8383
/// Set `c.x` and `c.y` in one call. Returns 0.
8484
pub extern fn pixiContainerSetPosition(c: Container, x: Float, y: Float) -> Int;
8585

86+
/// `c.scale.set(x, y)` — uniform-or-per-axis scaling. Returns 0.
87+
pub extern fn pixiContainerSetScale(c: Container, x: Float, y: Float) -> Int;
88+
89+
/// `c.pivot.set(x, y)` — origin for rotation and scaling. Returns 0.
90+
pub extern fn pixiContainerSetPivot(c: Container, x: Float, y: Float) -> Int;
91+
92+
/// `c.rotation = rad` — rotation in radians. Returns 0.
93+
pub extern fn pixiContainerSetRotation(c: Container, rad: Float) -> Int;
94+
95+
/// `c.alpha = a` — opacity (0.0–1.0). Returns 0.
96+
pub extern fn pixiContainerSetAlpha(c: Container, a: Float) -> Int;
97+
98+
/// `c.zIndex = z` — draw-order index; only honoured when the parent
99+
/// has `sortableChildren = true`. Returns 0.
100+
pub extern fn pixiContainerSetZIndex(c: Container, z: Int) -> Int;
101+
102+
/// `c.sortableChildren = v` — enables zIndex sort of children on this
103+
/// container. Returns 0.
104+
pub extern fn pixiContainerSetSortableChildren(c: Container, v: Bool) -> Int;
105+
106+
/// `c.eventMode = mode` — pointer-event participation. PIXI 8 accepts
107+
/// `"static"`, `"dynamic"`, `"passive"`, `"none"`, `"auto"`. Returns 0.
108+
pub extern fn pixiContainerSetEventMode(c: Container, mode: String) -> Int;
109+
110+
/// `c.cursor = cursor` — CSS cursor name displayed while pointer is
111+
/// over this container (`"pointer"`, `"grab"`, …). Returns 0.
112+
pub extern fn pixiContainerSetCursor(c: Container, cursor: String) -> Int;
113+
86114
/// Set `c.visible`. Returns 0.
87115
pub extern fn pixiContainerSetVisible(c: Container, v: Bool) -> Int;
88116

117+
/// `c.on(event, handler)` — register a `FederatedPointerEvent`
118+
/// handler. `event` is a PIXI 8 event string (`"pointerdown"`,
119+
/// `"pointerup"`, `"pointertap"`, `"pointerover"`, `"pointerout"`,
120+
/// `"pointermove"`, `"pointercancel"`, `"globalpointermove"`).
121+
/// `handler` is an opaque JS function (`(event: Json) => void`) — the
122+
/// `FederatedPointerEvent` arg is exposed as `Json` and read with
123+
/// existing Json accessors. Returns 0.
124+
pub extern fn pixiContainerOn(c: Container, event: String, handler: Json) -> Int;
125+
126+
/// `c.off(event, handler)` — deregister a previously-registered
127+
/// handler. Same `event`/`handler` shape as `pixiContainerOn`. Returns 0.
128+
pub extern fn pixiContainerOff(c: Container, event: String, handler: Json) -> Int;
129+
89130
/// `c.destroy()` — recursively free this container and its children.
90131
/// Returns 0.
91132
pub extern fn pixiContainerDestroy(c: Container) -> Int;
@@ -95,6 +136,11 @@ pub extern fn pixiContainerDestroy(c: Container) -> Int;
95136
/// `new PIXI.Sprite(texture)`.
96137
pub extern fn pixiSpriteFrom(texture: Texture) -> Sprite;
97138

139+
/// `s.anchor.set(x, y)` — anchor point on the sprite for positioning
140+
/// and rotation. `0.0, 0.0` is top-left; `0.5, 0.5` centres the sprite
141+
/// on its `x, y`. Returns 0.
142+
pub extern fn pixiSpriteSetAnchor(s: Sprite, x: Float, y: Float) -> Int;
143+
98144
/// Upcast a Sprite to its Container superclass. Zero-cost identity
99145
/// lowering — the underlying JS value is the same object.
100146
pub extern fn pixiSpriteAsContainer(s: Sprite) -> Container;

tests/codegen-deno/pixi_smoke.affine

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// extern shape (constructor, mutator, async init, upcast).
77

88
use Deno::{Json};
9-
use Pixi::{Application, Container, Sprite, Graphics, Text, Texture, pixiAppInit, pixiAppStage, pixiContainerNew, pixiContainerAddChild, pixiContainerSetPosition, pixiContainerSetVisible, pixiSpriteFrom, pixiSpriteAsContainer, pixiTextureFromUrl, pixiGraphicsNew, pixiGraphicsRect, pixiGraphicsFill, pixiGraphicsAsContainer};
9+
use Pixi::{Application, Container, Sprite, Graphics, Text, Texture, pixiAppInit, pixiAppStage, pixiContainerNew, pixiContainerAddChild, pixiContainerSetPosition, pixiContainerSetVisible, pixiContainerSetScale, pixiContainerSetPivot, pixiContainerSetRotation, pixiContainerSetAlpha, pixiContainerSetZIndex, pixiContainerSetSortableChildren, pixiContainerSetEventMode, pixiContainerSetCursor, pixiContainerOn, pixiContainerOff, pixiSpriteFrom, pixiSpriteSetAnchor, pixiSpriteAsContainer, pixiTextureFromUrl, pixiGraphicsNew, pixiGraphicsRect, pixiGraphicsFill, pixiGraphicsAsContainer};
1010

1111
pub fn smokeInit(options: Json) -> Application / { Async } = pixiAppInit(options);
1212

@@ -27,3 +27,27 @@ pub fn smokeGraphicsFlow(app: Application, color: Int) -> Int {
2727
pixiContainerAddChild(pixiAppStage(app), pixiGraphicsAsContainer(g));
2828
0
2929
}
30+
31+
/// Exercises every Container accessor + Sprite anchor + on/off
32+
/// FederatedEvent registration shape — the PixiJS 8 transform-and-event
33+
/// surface idaptik's 215 src/app files lean on.
34+
pub fn smokeAccessorsFlow(app: Application, url: String, onDown: Json, onMove: Json) -> Int {
35+
let stage = pixiAppStage(app);
36+
let texture = pixiTextureFromUrl(url);
37+
let sprite = pixiSpriteFrom(texture);
38+
let spriteC = pixiSpriteAsContainer(sprite);
39+
pixiSpriteSetAnchor(sprite, 0.5, 0.5);
40+
pixiContainerSetScale(spriteC, 2.0, 3.0);
41+
pixiContainerSetPivot(spriteC, 10.0, 20.0);
42+
pixiContainerSetRotation(spriteC, 1.5708);
43+
pixiContainerSetAlpha(spriteC, 0.75);
44+
pixiContainerSetZIndex(spriteC, 7);
45+
pixiContainerSetSortableChildren(stage, true);
46+
pixiContainerSetEventMode(spriteC, "static");
47+
pixiContainerSetCursor(spriteC, "pointer");
48+
pixiContainerOn(spriteC, "pointerdown", onDown);
49+
pixiContainerOn(spriteC, "pointermove", onMove);
50+
pixiContainerAddChild(stage, spriteC);
51+
pixiContainerOff(spriteC, "pointermove", onMove);
52+
0
53+
}

tests/codegen-deno/pixi_smoke.harness.mjs

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,49 @@ const initCalls = [];
1111
const textureUrls = [];
1212
const operationsLog = [];
1313

14+
class MockPoint {
15+
constructor() { this.x = 0; this.y = 0; }
16+
set(x, y) { this.x = x; this.y = y; }
17+
}
18+
1419
class MockContainer {
1520
constructor() {
16-
this.x = 0; this.y = 0; this.visible = true;
21+
this.x = 0; this.y = 0;
22+
this.scale = new MockPoint();
23+
this.pivot = new MockPoint();
24+
this.rotation = 0;
25+
this.alpha = 1;
26+
this.zIndex = 0;
27+
this.sortableChildren = false;
28+
this.eventMode = "auto";
29+
this.cursor = "";
30+
this.visible = true;
1731
this.children = [];
32+
this.handlers = new Map();
1833
}
1934
addChild(c) { this.children.push(c); operationsLog.push("addChild"); }
2035
removeChild(c) {
2136
this.children = this.children.filter((ch) => ch !== c);
2237
operationsLog.push("removeChild");
2338
}
39+
on(event, handler) {
40+
if (!this.handlers.has(event)) this.handlers.set(event, []);
41+
this.handlers.get(event).push(handler);
42+
}
43+
off(event, handler) {
44+
const list = this.handlers.get(event);
45+
if (!list) return;
46+
this.handlers.set(event, list.filter((h) => h !== handler));
47+
}
2448
destroy() { operationsLog.push("destroy"); }
2549
}
2650

2751
class MockSprite extends MockContainer {
28-
constructor(texture) { super(); this.texture = texture; }
52+
constructor(texture) {
53+
super();
54+
this.texture = texture;
55+
this.anchor = new MockPoint();
56+
}
2957
}
3058

3159
class MockGraphics extends MockContainer {
@@ -60,7 +88,7 @@ globalThis.__as_pixi = {
6088
},
6189
};
6290

63-
const { smokeInit, smokeSpriteFlow, smokeGraphicsFlow } = await import("./pixi_smoke.deno.js");
91+
const { smokeInit, smokeSpriteFlow, smokeGraphicsFlow, smokeAccessorsFlow } = await import("./pixi_smoke.deno.js");
6492

6593
// Async init returns an Application after `await app.init(options)`
6694
const app = await smokeInit({ width: 800, height: 600, backgroundColor: 0x1099bb });
@@ -83,4 +111,24 @@ const graphics = app.stage.children[1];
83111
assert.deepEqual(graphics.paths, [{ kind: "rect", x: 0, y: 0, w: 50, h: 50 }], "rect path recorded");
84112
assert.deepEqual(graphics.fills, [{ color: 0xff0000 }], "fill recorded with color");
85113

114+
// Accessors flow: every Container 8.x setter + Sprite anchor + on/off
115+
const onDown = (e) => { onDown.lastEvent = e; };
116+
const onMove = (e) => { onMove.lastEvent = e; };
117+
assert.equal(smokeAccessorsFlow(app, "/assets/btn.png", onDown, onMove), 0, "smokeAccessorsFlow returns 0");
118+
assert.equal(textureUrls.length, 2, "second Texture.from recorded");
119+
assert.equal(app.stage.children.length, 3, "accessor sprite added as third child");
120+
const aSprite = app.stage.children[2];
121+
assert.equal(aSprite.anchor.x, 0.5, "sprite.anchor.x"); assert.equal(aSprite.anchor.y, 0.5, "sprite.anchor.y");
122+
assert.equal(aSprite.scale.x, 2.0, "container.scale.x"); assert.equal(aSprite.scale.y, 3.0, "container.scale.y");
123+
assert.equal(aSprite.pivot.x, 10.0, "container.pivot.x"); assert.equal(aSprite.pivot.y, 20.0, "container.pivot.y");
124+
assert.equal(aSprite.rotation, 1.5708, "container.rotation");
125+
assert.equal(aSprite.alpha, 0.75, "container.alpha");
126+
assert.equal(aSprite.zIndex, 7, "container.zIndex");
127+
assert.equal(app.stage.sortableChildren, true, "stage.sortableChildren set");
128+
assert.equal(aSprite.eventMode, "static", "container.eventMode");
129+
assert.equal(aSprite.cursor, "pointer", "container.cursor");
130+
assert.equal(aSprite.handlers.get("pointerdown").length, 1, "pointerdown handler registered");
131+
assert.equal(aSprite.handlers.get("pointermove").length, 0, "pointermove handler off after off()");
132+
assert.equal(aSprite.handlers.get("pointerdown")[0], onDown, "pointerdown handler identity preserved");
133+
86134
console.log("pixi_smoke.harness.mjs OK");

0 commit comments

Comments
 (0)