Skip to content

feat(stdlib): PixiJS 8.x Container transform-and-event surface (bindings #1) - #502

Merged
hyperpolymath merged 1 commit into
mainfrom
claude/pixi-container-accessors
May 31, 2026
Merged

feat(stdlib): PixiJS 8.x Container transform-and-event surface (bindings #1)#502
hyperpolymath merged 1 commit into
mainfrom
claude/pixi-container-accessors

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

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 fns.

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 fns 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

  • dune build bin/main.exe — clean (only the expected parser warnings)
  • dune runtest — 354 tests pass
  • 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

🤖 Generated with Claude Code

…ngs #1 expansion)

Extends the Tier-1 PixiJS binding with the 11 most-load-bearing
Container 8.x accessors + Sprite anchor + FederatedPointerEvent
on/off registration — the largest single chunk of idaptik's
src/bindings/Pixi.res surface still not bound.

stdlib/Pixi.affine (+46 lines):
  - Container: pixiContainerSetScale / SetPivot / SetRotation /
    SetAlpha / SetZIndex / SetSortableChildren / SetEventMode /
    SetCursor (8 transform/visual accessors)
  - Container: pixiContainerOn / pixiContainerOff (2 pointer-event
    registration externs taking the handler as opaque Json — the
    FederatedPointerEvent reaches the handler as Json which AS
    code reads via existing Json accessors; typed accessor surface
    deferred to follow-up)
  - Sprite: pixiSpriteSetAnchor (the anchor lives on Sprite, not
    Container — Pixi 8 keeps the same split)

lib/codegen_deno.ml: 11 __as_* prelude helpers + 11 dispatch table
entries, mirroring the existing wasmCall/motion/pixi pattern.

tests/codegen-deno/pixi_smoke.{affine,harness.mjs}: new
smokeAccessorsFlow exercises every new extern; harness's
MockContainer grows scale/pivot Point mocks + handler Map +
sortableChildren/eventMode/cursor/zIndex/alpha/rotation fields;
asserts handler identity preserved across on() + off().

docs/bindings-roadmap.adoc row #1 status note updated to reflect
the new surface + remaining deferred items (typed
FederatedPointerEvent accessors, parent read accessor with
Option-null handling, Point/Rectangle/Circle helper types, sprite
atlases, filters, hitArea).

Refs #446 — Tier 1 #1 expansion.
@hyperpolymath
hyperpolymath force-pushed the claude/pixi-container-accessors branch from 15f68ce to a464aa2 Compare May 31, 2026 07:33
@hyperpolymath
hyperpolymath merged commit 44a0685 into main May 31, 2026
15 of 24 checks passed
@hyperpolymath
hyperpolymath deleted the claude/pixi-container-accessors branch May 31, 2026 07:33
hyperpolymath added a commit that referenced this pull request May 31, 2026
) (#506)

## Summary

Ships the first concrete surface for Tier-1 #9 of #446 — **IPC /
structuredClone** for host↔guest message passing. This is the binding
`idaptik-ums` Gossamer IPC for level I/O depends on, and the pattern any
embedded-engine binding in the estate needs.

## What lands

`stdlib/Ipc.affine` (+105 lines, new module): 2 extern types + 9 extern
fns covering MessageChannel construction, port handoff, post +
onmessage, start/close lifecycle, generic `targetPostMessage`, and
`structuredCloneValue`.

| Surface | Externs |
|---|---|
| Channel | `MessageChannel` / `MessagePort` opaque types;
`messageChannelNew` / `messageChannelPort1` / `messageChannelPort2` |
| Port | `messagePortPostMessage` / `messagePortOnMessage` /
`messagePortStart` / `messagePortClose` |
| Cross-context | `targetPostMessage` (Worker / iframe.contentWindow /
self-from-worker) |
| Deep-clone | `structuredCloneValue` |

`lib/codegen_deno.ml` (+23 lines): 9 `__as_*` prelude helpers + 9
dispatch entries adjacent to the pixiSound block. No consumer init —
`MessageChannel` / `MessagePort` / `structuredClone` are standard
web-platform globals.

`tests/codegen-deno/ipc_smoke.{affine,harness.mjs}` (+98 lines
combined): port-pair postMessage round-trip with handler-identity
preservation, standalone close lifecycle, target-post stub, and
structuredClone deep-copy with reference-distinctness assertions across
nested arrays + objects.

`docs/bindings-roadmap.adoc` row #9 promoted `○ → ◑`; deferred items
listed.

## Design notes

**Why is `MessagePort` an opaque `extern type` instead of a
record/tagged-union?** Same reason `WasmExports` and `WasmValue` (#467)
are opaque — the Deno-ESM backend doesn't yet have tagged-variant
codegen (deferred to json.affine v0.3), and a real `MessagePort` carries
internal worker-thread state that isn't usefully observable from
AffineScript anyway. The handler observes the `MessageEvent` as opaque
`Json` and reads `event.data` via the existing Json accessors. A typed
`MessageEvent` extern-type with dedicated accessor externs is the
natural follow-up axis, captured in the roadmap deferred-items list.

**Why is the host responsible for closing the ports?** Inline `close()`
calls in `smokeChannelFlow` would race the microtask-async delivery —
MessagePort drops queued messages on close. The fixture surfaces this as
a documented authoring pattern (it lives in a top-of-file comment) so
anyone writing IPC code with this binding doesn't get bitten.

**Why `setTimeout(50)` in the harness instead of
`setImmediate`/microtask flush?** Empirically verified — Node 20's
`worker_threads`-backed `MessageChannel` batches delivery beyond a
single setImmediate tick. The comment in the harness records the test
that surfaced it (a standalone `node` repl reproducer with the same
shape).

**Why no `transfer` list yet?** Owner directive at #455 (Option-B
kickoff scope) — ship the generic surface, layer typed-and-richer
variants as follow-ups once usage patterns crystallise. Same pattern as
`WasmValue` — opaque tagged scalar first, typed wrappers next.

## 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 18 harnesses including the
new `ipc_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 — not from this PR)

## Refs

- Umbrella: #446 (Tier 1 — idaptik blockers)
- Tier-1 sub-issue: #450
- Row updated: `docs/bindings-roadmap.adoc` row #9
- Pattern siblings: #467 (`wasm_export_call` Option-B), #474 (Zig-FFI
patterns doc), #502 (PixiJS 8.x Container)
- Adjacent: Tier-3 #25 Web Workers — `Worker` constructors would consume
this surface

🤖 Generated with [Claude Code](https://claude.com/claude-code)
hyperpolymath added a commit that referenced this pull request May 31, 2026
…#510)

## Summary

Ships the Canvas 2D half of Tier-1 #8 of #446. This is the surface
`idaptik-ums` (App.res: 1178 LoC of DOM + canvas) and every non-Pixi UI
in the estate has been waiting for; WebGL / WebGL2 / WebGPU stays at `○`
until a consumer surfaces a concrete need.

## What lands

`stdlib/Canvas.affine` (+170 lines, new module): 1 extern type (`Ctx2D`)
+ 26 extern fns covering the full idaptik-ums-relevant Canvas 2D
surface:

| Surface | Externs |
|---|---|
| Context acquisition | `canvasGetContext2D` |
| Styles | `canvasFillStyle` / `StrokeStyle` / `LineWidth` /
`GlobalAlpha` |
| Rectangles | `canvasFillRect` / `StrokeRect` / `ClearRect` |
| Paths | `canvasBeginPath` / `ClosePath` / `MoveTo` / `LineTo` / `Arc`
/ `Fill` / `Stroke` |
| Transform stack | `canvasSave` / `Restore` / `Translate` / `Rotate` /
`Scale` |
| Text | `canvasFont` / `TextAlign` / `TextBaseline` / `FillText` /
`StrokeText` / `MeasureText` |
| Images | `canvasDrawImage` / `canvasDrawImageScaled` |

`lib/codegen_deno.ml` (+58 lines): 26 `__as_canvas*` prelude helpers +
26 dispatch entries adjacent to the Ipc block.

`tests/codegen-deno/canvas_smoke.{affine,harness.mjs}` (+160 lines
combined): 5 distinct smoke functions exercise every shipped extern. The
`MockCtx2D` records every method call as a typed-op tuple so the test
asserts call order + arguments, not just side-effects. The
`canvasMeasureText` round-trip exercises the `Json`-return shape (the
`TextMetrics`-shaped object).

`docs/bindings-roadmap.adoc` row #8 status promoted `○ → ◑`; deferred
items captured (WebGL, gradients/patterns, ImageData, curve primitives,
compositing/clip).

## Design notes

**Why is `HTMLCanvasElement` not its own extern type?** The
canvas-creation entry point is host-dependent — browser
`document.createElement("canvas")` vs idaptik's pre-existing DOM tree vs
jsdom-under-Deno. Treating the canvas as opaque `Json` defers the typed
wrapper to the natural follow-up once `affinescript-dom` lands runtime
support (currently blocked on the wasm-codegen `for-in` / `while` gap,
issue #255).

**Why fix `arc` counter-clockwise to `false`?** The 6-arg shape (`ccw`
boolean) would force every call site to pass a literal `false`. The
5-arg shape covers the overwhelming majority of consumers; a typed
wrapper with the `ccw` flag is a follow-up if a consumer surfaces the
need.

**Why two separate `drawImage` externs instead of one with optional
args?** AffineScript doesn't have JS-style variadic functions; making
`w` and `h` `Option<Float>` would force every natural-size call to
thread `None`s. The two-extern split keeps each call site simple.

**Why open-string `eventMode` / `textAlign` / `textBaseline`?** Same
rationale as the PixiJS expansion (#502) — Pixi 8's `eventMode` and
Canvas's text-alignment values are open enumerations, and a sum-type
binding would either freeze the set or require tagged-variant codegen
that doesn't exist on the Deno-ESM backend yet (deferred to json.affine
v0.3). Consistency with existing patterns.

## Test plan

- [x] `dune build bin/main.exe` — clean (only the expected parser
warnings)
- [x] `dune runtest --force` — 356 tests pass (was 354 pre-PR; +2 from
new Canvas + Ipc modules' AOT smoke)
- [x] `tools/run_codegen_deno_tests.sh` — all 19 harnesses including the
new `canvas_smoke.harness.mjs` OK
- [ ] CI build job
- [ ] CI `tools/run_codegen_deno_tests.sh` job
- [ ] CI governance + Hypatia (6 baselines per repo CLAUDE.md may be red
— pre-existing on main, not regressions)

## Refs

- Umbrella: #446 (Tier 1 — idaptik blockers)
- Tier-1 sub-issue: #450
- Row updated: `docs/bindings-roadmap.adoc` row #8
- Pattern siblings shipped this session: #502 (PixiJS Container
expansion), #506 (Ipc.affine)
- Adjacent / future: `affinescript-dom` (gated on #255), WebGL/WebGPU
(Tier 3 axis, not surfaced as need yet)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@github-actions

Copy link
Copy Markdown

🔍 Hypatia Security Scan

Findings: 83 issues detected

Severity Count
🔴 Critical 2
🟠 High 13
🟡 Medium 68

⚠️ Action Required: Critical security issues found!

View findings
[
  {
    "reason": "Action perpolymath/standards/.github/workflows/governance-reusable.yml@main\n needs attention",
    "type": "unpinned_action",
    "file": "governance.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action ons/checkout@v6\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Action land/setup-deno@v2\n    needs attention",
    "type": "unpinned_action",
    "file": "publish-jsr.yml",
    "action": "pin_sha",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in affine-vscode-publish.yml",
    "type": "missing_timeout_minutes",
    "file": "affine-vscode-publish.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in casket-pages.yml",
    "type": "missing_timeout_minutes",
    "file": "casket-pages.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  },
  {
    "reason": "Issue in ci.yml",
    "type": "missing_timeout_minutes",
    "file": "ci.yml",
    "action": "flag",
    "rule_module": "workflow_audit",
    "severity": "medium"
  }
]

Powered by Hypatia Neurosymbolic CI/CD Intelligence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant