diff --git a/affinescript-pixijs/ffi/zig/src/main.zig b/affinescript-pixijs/ffi/zig/src/main.zig deleted file mode 100644 index 4998f50b..00000000 --- a/affinescript-pixijs/ffi/zig/src/main.zig +++ /dev/null @@ -1,27 +0,0 @@ -/// AffineScript PixiJS Connector FFI (Zig Implementation) -/// (c) 2026 hyperpolymath -/// SPDX-License-Identifier: AGPL-3.0-or-later - -const std = @import("std"); - -/// External JS functions imported into the WASM environment -extern fn js_pixi_init(w: u32, h: u32, bg: u32) u64; -extern fn js_pixi_create_sprite(texture: u64) u64; -extern fn js_pixi_get_stage(app: u64) u64; -extern fn js_pixi_add_child(parent: u64, child: u64) void; - -export fn as_pixi_init(w: u32, h: u32, bg: u32) u64 { - return js_pixi_init(w, h, bg); -} - -export fn as_pixi_create_sprite(texture: u64) u64 { - return js_pixi_create_sprite(texture); -} - -export fn as_pixi_get_stage(app: u64) u64 { - return js_pixi_get_stage(app); -} - -export fn as_pixi_add_child(parent: u64, child: u64) void { - js_pixi_add_child(parent, child); -} diff --git a/affinescript-pixijs/package.json b/affinescript-pixijs/package.json deleted file mode 100644 index c04d710e..00000000 --- a/affinescript-pixijs/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@affinescript/pixijs", - "version": "0.1.0", - "description": "AffineScript connector for PixiJS — high-performance 2D graphics with typed WASM.", - "type": "module", - "main": "./src/pixi.as", - "exports": { - ".": "./src/pixi.as" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "keywords": [ - "affinescript", - "pixijs", - "wasm", - "typed-wasm", - "graphics" - ], - "author": "hyperpolymath", - "license": "AGPL-3.0-or-later", - "dependencies": { - "pixi.js": "^8.0.0" - }, - "devDependencies": { - "affinescript-vite": "file:../../../../affinescript-vite", - "vite": "^8.0.5" - } -} diff --git a/affinescript-pixijs/src/abi/Pixi.idr b/affinescript-pixijs/src/abi/Pixi.idr deleted file mode 100644 index 4a8f4fbc..00000000 --- a/affinescript-pixijs/src/abi/Pixi.idr +++ /dev/null @@ -1,61 +0,0 @@ -||| AffineScript PixiJS Connector ABI -||| (c) 2026 hyperpolymath -||| SPDX-License-Identifier: AGPL-3.0-or-later - -module AffineScript.Pixi.ABI - -import Data.Maybe - -%default total - --------------------------------------------------------------------------------- --- Opaque Handles --------------------------------------------------------------------------------- - -||| Opaque handle to a PixiJS object (Application, Sprite, etc.) -public export -record Handle where - constructor MkHandle - ptr : Bits64 - --------------------------------------------------------------------------------- --- Foreign Function Declarations (Zig FFI Layer) --------------------------------------------------------------------------------- - -||| Initialize PixiJS Application -export -%foreign "C:as_pixi_init, libaspixi" -prim__init : Bits32 -> Bits32 -> Bits32 -> PrimIO Bits64 - -||| Create a sprite from a texture -export -%foreign "C:as_pixi_create_sprite, libaspixi" -prim__createSprite : Bits64 -> PrimIO Bits64 - -||| Get the stage from the application -export -%foreign "C:as_pixi_get_stage, libaspixi" -prim__getStage : Bits64 -> PrimIO Bits64 - -||| Add a child to a container -export -%foreign "C:as_pixi_add_child, libaspixi" -prim__addChild : Bits64 -> Bits64 -> PrimIO () - --------------------------------------------------------------------------------- --- Safe High-Level Wrappers --------------------------------------------------------------------------------- - -||| Initialize application safely -export -init : (width : Bits32) -> (height : Bits32) -> (bgColor : Bits32) -> IO Handle -init w h bg = do - ptr <- primIO (prim__init w h bg) - pure (MkHandle ptr) - -||| Get the stage handle -export -getStage : Handle -> IO Handle -getStage (MkHandle app) = do - ptr <- primIO (prim__getStage app) - pure (MkHandle ptr) diff --git a/affinescript-pixijs/src/pixi.as b/affinescript-pixijs/src/pixi.as deleted file mode 100644 index 1a5b5fa4..00000000 --- a/affinescript-pixijs/src/pixi.as +++ /dev/null @@ -1,36 +0,0 @@ -/** - * AffineScript PixiJS Connector - * (c) 2026 hyperpolymath - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -// Externs for PixiJS types and functions -// These will be linked at runtime via Typed WASM imports - -extern type Application; -extern type Sprite; -extern type Texture; -extern type Container; -extern type Graphics; - -extern fn createApplication(options: { width: Int, height: Int, backgroundColor: Int }) -> Application; -extern fn getStage(app: Application) -> Container; -extern fn addChild(container: Container, child: Sprite) -> Void; -extern fn fromImage(url: String) -> Texture; -extern fn createSprite(texture: Texture) -> Sprite; - -// High-level wrapper for cleaner usage -pub fn init_pixi(width: Int, height: Int) -> Application { - createApplication({ - width: width, - height: height, - backgroundColor: 0x1099bb - }) -} - -pub fn add_sprite(app: Application, url: String) -> Sprite { - let texture = fromImage(url); - let sprite = createSprite(texture); - addChild(getStage(app), sprite); - sprite -} diff --git a/docs/bindings-roadmap.adoc b/docs/bindings-roadmap.adoc index 5dd7e65f..578f9240 100644 --- a/docs/bindings-roadmap.adoc +++ b/docs/bindings-roadmap.adoc @@ -49,9 +49,9 @@ no further significant ReScript → AffineScript work is tractable. |1 |*PixiJS core* (Application, Container, Sprite, Graphics, Text, Ticker, FederatedEvent) -|`◐` scaffold -|`affinescript-pixijs` -|idaptik `src/bindings/Pixi.res`; all 215 `src/app/*.res` files depend on this. Currently 1 `.as` stub + 1 Idris2 ABI + 1 Zig FFI; needs full PixiJS 8.x surface. +|`◑` partial (Application / Container / Sprite / Graphics / Text / Texture / Ticker landed; FederatedEvent + extensive accessor coverage deferred) +|`stdlib/Pixi.affine` (eventual home: `affinescript-pixijs` as a separate repo per the SNIFs/typed-wasm precedent) +|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. |2 |*@pixi/sound* (`Sound.from`, play, stop, volume, setVolume, loop) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index c85b92da..6d47f252 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -173,6 +173,41 @@ const __as_motionCancel = (controls) => { if (controls && typeof controls.cancel === "function") controls.cancel(); return 0; }; +// ---- pixi.js (bindings #1): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi (the PIXI namespace +// from `import * as PIXI from "pixi.js"`). Tests set it in the harness +// before importing the generated module. +const __as_pixiAppInit = async (options) => { + const app = new globalThis.__as_pixi.Application(); + await app.init(options); + return app; +}; +const __as_pixiAppCanvas = (app) => app.canvas; +const __as_pixiAppStage = (app) => app.stage; +const __as_pixiAppTicker = (app) => app.ticker; +const __as_pixiAppDestroy = (app) => { app.destroy(); return 0; }; +const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container(); +const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; }; +const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; }; +const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; }; +const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; }; +const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; }; +const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t); +// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/ +// Text actual Container subclasses, so the JS object is the same. +const __as_pixiSpriteAsContainer = (s) => s; +const __as_pixiTextureFromUrl = (url) => globalThis.__as_pixi.Texture.from(url); +const __as_pixiGraphicsNew = () => new globalThis.__as_pixi.Graphics(); +const __as_pixiGraphicsRect = (g, x, y, w, h) => { g.rect(x, y, w, h); return 0; }; +const __as_pixiGraphicsFill = (g, color) => { g.fill({ color }); return 0; }; +const __as_pixiGraphicsClear = (g) => { g.clear(); return 0; }; +const __as_pixiGraphicsAsContainer = (g) => g; +const __as_pixiTextNew = (options) => new globalThis.__as_pixi.Text(options); +const __as_pixiTextSetText = (t, content) => { t.text = content; return 0; }; +const __as_pixiTextAsContainer = (t) => t; +const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; +const __as_pixiTickerStart = (t) => { t.start(); return 0; }; +const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; // `++` is overloaded (string concat / array concat); `a + b` would // stringify arrays. Dispatch on shape so stdlib/string.affine's // `result ++ [x]` and `a ++ b` are both correct. @@ -322,6 +357,32 @@ let () = b "motionAnimate" (fun a -> Printf.sprintf "__as_motionAnimate(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); b "motionAwait" (fun a -> Printf.sprintf "(await __as_motionAwait(%s))" (arg 0 a)); b "motionCancel" (fun a -> Printf.sprintf "__as_motionCancel(%s)" (arg 0 a)); + (* ---- pixi.js (bindings #1) ---- *) + b "pixiAppInit" (fun a -> Printf.sprintf "(await __as_pixiAppInit(%s))" (arg 0 a)); + b "pixiAppCanvas" (fun a -> Printf.sprintf "__as_pixiAppCanvas(%s)" (arg 0 a)); + b "pixiAppStage" (fun a -> Printf.sprintf "__as_pixiAppStage(%s)" (arg 0 a)); + b "pixiAppTicker" (fun a -> Printf.sprintf "__as_pixiAppTicker(%s)" (arg 0 a)); + b "pixiAppDestroy" (fun a -> Printf.sprintf "__as_pixiAppDestroy(%s)" (arg 0 a)); + b "pixiContainerNew" (fun _ -> "__as_pixiContainerNew()"); + b "pixiContainerAddChild" (fun a -> Printf.sprintf "__as_pixiContainerAddChild(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiContainerRemoveChild" (fun a -> Printf.sprintf "__as_pixiContainerRemoveChild(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiContainerSetPosition" (fun a -> Printf.sprintf "__as_pixiContainerSetPosition(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "pixiContainerSetVisible" (fun a -> Printf.sprintf "__as_pixiContainerSetVisible(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiContainerDestroy" (fun a -> Printf.sprintf "__as_pixiContainerDestroy(%s)" (arg 0 a)); + b "pixiSpriteFrom" (fun a -> Printf.sprintf "__as_pixiSpriteFrom(%s)" (arg 0 a)); + b "pixiSpriteAsContainer" (fun a -> Printf.sprintf "__as_pixiSpriteAsContainer(%s)" (arg 0 a)); + b "pixiTextureFromUrl" (fun a -> Printf.sprintf "__as_pixiTextureFromUrl(%s)" (arg 0 a)); + b "pixiGraphicsNew" (fun _ -> "__as_pixiGraphicsNew()"); + b "pixiGraphicsRect" (fun a -> Printf.sprintf "__as_pixiGraphicsRect(%s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a)); + b "pixiGraphicsFill" (fun a -> Printf.sprintf "__as_pixiGraphicsFill(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiGraphicsClear" (fun a -> Printf.sprintf "__as_pixiGraphicsClear(%s)" (arg 0 a)); + b "pixiGraphicsAsContainer" (fun a -> Printf.sprintf "__as_pixiGraphicsAsContainer(%s)" (arg 0 a)); + b "pixiTextNew" (fun a -> Printf.sprintf "__as_pixiTextNew(%s)" (arg 0 a)); + b "pixiTextSetText" (fun a -> Printf.sprintf "__as_pixiTextSetText(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiTextAsContainer" (fun a -> Printf.sprintf "__as_pixiTextAsContainer(%s)" (arg 0 a)); + b "pixiTickerAdd" (fun a -> Printf.sprintf "__as_pixiTickerAdd(%s, %s)" (arg 0 a) (arg 1 a)); + b "pixiTickerStart" (fun a -> Printf.sprintf "__as_pixiTickerStart(%s)" (arg 0 a)); + b "pixiTickerStop" (fun a -> Printf.sprintf "__as_pixiTickerStop(%s)" (arg 0 a)); (* Generic JS array push helper (returns the array, fluent). *) b "arrayPush" (fun a -> Printf.sprintf "(%s.push(%s), %s)" (arg 0 a) (arg 1 a) (arg 0 a)); (* ---- honest string/number primitives underpinning the diff --git a/stdlib/Pixi.affine b/stdlib/Pixi.affine new file mode 100644 index 00000000..1bd35a58 --- /dev/null +++ b/stdlib/Pixi.affine @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Pixi.affine — bindings for the `pixi.js` v8.x npm library (bindings +// #1 in docs/bindings-roadmap.adoc). +// +// Initial typed surface covering Application init, Container hierarchy, +// Sprite + Texture, Graphics + Text — the minimum needed to start +// replacing idaptik's src/bindings/Pixi.res (the largest binding +// surface in idaptik's 215-file src/app/ tree). +// +// Targets the Deno-ESM backend. Consumer (or its host wrapper) sets +// `globalThis.__as_pixi` to the PIXI namespace at module-init time: +// +// import * as PIXI from "pixi.js"; +// globalThis.__as_pixi = PIXI; +// +// Tests mock the same shape — see `tests/codegen-deno/pixi_smoke.*`. +// +// History: replaces the broken `affinescript-pixijs/` scaffold that +// used the obsolete `.as` extension, AGPL-3.0-or-later headers, and a +// Zig→C→WASM-import architecture incompatible with the Deno-ESM +// emitter. Restart, not revival. The eventual separate-repo home +// (`hyperpolymath/affinescript-pixijs`) is a follow-up — the binding +// is operational from here today. +// +// PixiJS 8.x type hierarchy: Sprite, Graphics, and Text are all +// subclasses of Container. AffineScript has no subtype polymorphism, +// so the binding exposes explicit upcast functions +// (`pixiSpriteAsContainer`, etc.) — these are zero-cost identity +// lowerings; the underlying JS value is the same object. + +module Pixi; + +use Deno::{Json}; + +// ── Opaque host types ────────────────────────────────────────────── +// +// Each maps to its same-named PIXI class. They're treated opaquely at +// the AS boundary; field access goes through extern fns. + +pub extern type Application; +pub extern type Container; +pub extern type Sprite; +pub extern type Graphics; +pub extern type Text; +pub extern type Texture; +pub extern type Ticker; + +// ── Application ──────────────────────────────────────────────────── + +/// `new PIXI.Application(); await app.init(options)`. +/// `options` is a JSON object: `{ width, height, backgroundColor, ... }`. +/// PIXI 8 split construction from initialisation, so this is async. +pub extern fn pixiAppInit(options: Json) -> Application / { Async }; + +/// `app.canvas` — the underlying `HTMLCanvasElement` (opaque Json). +pub extern fn pixiAppCanvas(app: Application) -> Json; + +/// `app.stage` — the root `Container`. Children added here are +/// rendered on the next ticker frame. +pub extern fn pixiAppStage(app: Application) -> Container; + +/// `app.ticker` — the render-loop driver. +pub extern fn pixiAppTicker(app: Application) -> Ticker; + +/// `app.destroy()` — tear down rendering + free resources. Returns 0. +pub extern fn pixiAppDestroy(app: Application) -> Int; + +// ── Container ────────────────────────────────────────────────────── + +/// `new PIXI.Container()`. +pub extern fn pixiContainerNew() -> Container; + +/// `parent.addChild(child)`. Returns 0 (the JS-side return value is +/// discarded — call the child accessor on the parent for the typed +/// handle if needed). +pub extern fn pixiContainerAddChild(parent: Container, child: Container) -> Int; + +/// `parent.removeChild(child)`. Returns 0. +pub extern fn pixiContainerRemoveChild(parent: Container, child: Container) -> Int; + +/// Set `c.x` and `c.y` in one call. Returns 0. +pub extern fn pixiContainerSetPosition(c: Container, x: Float, y: Float) -> Int; + +/// Set `c.visible`. Returns 0. +pub extern fn pixiContainerSetVisible(c: Container, v: Bool) -> Int; + +/// `c.destroy()` — recursively free this container and its children. +/// Returns 0. +pub extern fn pixiContainerDestroy(c: Container) -> Int; + +// ── Sprite ───────────────────────────────────────────────────────── + +/// `new PIXI.Sprite(texture)`. +pub extern fn pixiSpriteFrom(texture: Texture) -> Sprite; + +/// Upcast a Sprite to its Container superclass. Zero-cost identity +/// lowering — the underlying JS value is the same object. +pub extern fn pixiSpriteAsContainer(s: Sprite) -> Container; + +// ── Texture ──────────────────────────────────────────────────────── + +/// `PIXI.Texture.from(url)` — async load of an image URL. PIXI 8 +/// returns a synchronous handle whose internal load completes lazily +/// at the first draw. +pub extern fn pixiTextureFromUrl(url: String) -> Texture; + +// ── Graphics ─────────────────────────────────────────────────────── + +/// `new PIXI.Graphics()`. +pub extern fn pixiGraphicsNew() -> Graphics; + +/// `g.rect(x, y, w, h)` — record a rectangle path. Returns 0. +pub extern fn pixiGraphicsRect(g: Graphics, x: Float, y: Float, w: Float, h: Float) -> Int; + +/// `g.fill({ color })` — paint the recorded path. Returns 0. +pub extern fn pixiGraphicsFill(g: Graphics, color: Int) -> Int; + +/// `g.clear()` — drop all recorded paths. Returns 0. +pub extern fn pixiGraphicsClear(g: Graphics) -> Int; + +/// Upcast Graphics → Container. Zero-cost identity lowering. +pub extern fn pixiGraphicsAsContainer(g: Graphics) -> Container; + +// ── Text ─────────────────────────────────────────────────────────── + +/// `new PIXI.Text({ text, style })`. `options` is a JSON object +/// matching the PIXI 8 TextOptions shape. +pub extern fn pixiTextNew(options: Json) -> Text; + +/// `t.text = content`. Returns 0. +pub extern fn pixiTextSetText(t: Text, content: String) -> Int; + +/// Upcast Text → Container. Zero-cost identity lowering. +pub extern fn pixiTextAsContainer(t: Text) -> Container; + +// ── Ticker ───────────────────────────────────────────────────────── + +/// `ticker.add(callback)`. `callback` is an opaque JS function (PIXI's +/// ticker passes a `delta: Ticker` arg the callback can ignore). +/// Returns 0. +pub extern fn pixiTickerAdd(t: Ticker, callback: Json) -> Int; + +/// `ticker.start()`. Returns 0. +pub extern fn pixiTickerStart(t: Ticker) -> Int; + +/// `ticker.stop()`. Returns 0. +pub extern fn pixiTickerStop(t: Ticker) -> Int; diff --git a/tests/codegen-deno/pixi_smoke.affine b/tests/codegen-deno/pixi_smoke.affine new file mode 100644 index 00000000..23a9d718 --- /dev/null +++ b/tests/codegen-deno/pixi_smoke.affine @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MPL-2.0 +// bindings #1 — PixiJS smoke test. +// +// The harness mocks globalThis.__as_pixi before importing the +// generated module; this fixture exercises the round-trip of each +// extern shape (constructor, mutator, async init, upcast). + +use Deno::{Json}; +use Pixi::{Application, Container, Sprite, Graphics, Text, Texture, pixiAppInit, pixiAppStage, pixiContainerNew, pixiContainerAddChild, pixiContainerSetPosition, pixiContainerSetVisible, pixiSpriteFrom, pixiSpriteAsContainer, pixiTextureFromUrl, pixiGraphicsNew, pixiGraphicsRect, pixiGraphicsFill, pixiGraphicsAsContainer}; + +pub fn smokeInit(options: Json) -> Application / { Async } = pixiAppInit(options); + +pub fn smokeSpriteFlow(app: Application, url: String) -> Int { + let texture = pixiTextureFromUrl(url); + let sprite = pixiSpriteFrom(texture); + let stage = pixiAppStage(app); + pixiContainerAddChild(stage, pixiSpriteAsContainer(sprite)); + pixiContainerSetPosition(pixiSpriteAsContainer(sprite), 100.0, 200.0); + pixiContainerSetVisible(pixiSpriteAsContainer(sprite), true); + 0 +} + +pub fn smokeGraphicsFlow(app: Application, color: Int) -> Int { + let g = pixiGraphicsNew(); + pixiGraphicsRect(g, 0.0, 0.0, 50.0, 50.0); + pixiGraphicsFill(g, color); + pixiContainerAddChild(pixiAppStage(app), pixiGraphicsAsContainer(g)); + 0 +} diff --git a/tests/codegen-deno/pixi_smoke.harness.mjs b/tests/codegen-deno/pixi_smoke.harness.mjs new file mode 100644 index 00000000..5c46c53b --- /dev/null +++ b/tests/codegen-deno/pixi_smoke.harness.mjs @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: MPL-2.0 +// bindings #1 — Node ESM harness for the PixiJS binding. +// +// Installs a globalThis.__as_pixi mock covering Application, +// Container, Sprite, Graphics, Texture; drives the smoke functions; +// asserts every side-effect was observed on the mock objects. + +import assert from "node:assert/strict"; + +const initCalls = []; +const textureUrls = []; +const operationsLog = []; + +class MockContainer { + constructor() { + this.x = 0; this.y = 0; this.visible = true; + this.children = []; + } + addChild(c) { this.children.push(c); operationsLog.push("addChild"); } + removeChild(c) { + this.children = this.children.filter((ch) => ch !== c); + operationsLog.push("removeChild"); + } + destroy() { operationsLog.push("destroy"); } +} + +class MockSprite extends MockContainer { + constructor(texture) { super(); this.texture = texture; } +} + +class MockGraphics extends MockContainer { + constructor() { super(); this.paths = []; this.fills = []; } + rect(x, y, w, h) { this.paths.push({ kind: "rect", x, y, w, h }); } + fill(opts) { this.fills.push(opts); } + clear() { this.paths = []; this.fills = []; } +} + +class MockText extends MockContainer { + constructor(opts) { super(); this.text = opts.text ?? ""; this.style = opts.style; } +} + +class MockApplication { + constructor() { + this.stage = new MockContainer(); + this.canvas = { tagName: "CANVAS" }; + this.ticker = { add() {}, start() {}, stop() {} }; + } + async init(options) { initCalls.push(options); } + destroy() { operationsLog.push("appDestroy"); } +} + +globalThis.__as_pixi = { + Application: MockApplication, + Container: MockContainer, + Sprite: MockSprite, + Graphics: MockGraphics, + Text: MockText, + Texture: { + from(url) { textureUrls.push(url); return { __mockTexture: true, url }; }, + }, +}; + +const { smokeInit, smokeSpriteFlow, smokeGraphicsFlow } = await import("./pixi_smoke.deno.js"); + +// Async init returns an Application after `await app.init(options)` +const app = await smokeInit({ width: 800, height: 600, backgroundColor: 0x1099bb }); +assert.equal(initCalls.length, 1, "app.init called once"); +assert.deepEqual(initCalls[0], { width: 800, height: 600, backgroundColor: 0x1099bb }, "init options reach host"); + +// Sprite flow: load texture → make sprite → upcast → addChild → position + visibility +assert.equal(smokeSpriteFlow(app, "/assets/player.png"), 0, "smokeSpriteFlow returns 0"); +assert.deepEqual(textureUrls, ["/assets/player.png"], "Texture.from received the url"); +assert.equal(app.stage.children.length, 1, "1 child added after sprite flow"); +const sprite = app.stage.children[0]; +assert.equal(sprite.x, 100, "sprite.x set"); +assert.equal(sprite.y, 200, "sprite.y set"); +assert.equal(sprite.visible, true, "sprite.visible set"); + +// Graphics flow: new → rect → fill → upcast → addChild +assert.equal(smokeGraphicsFlow(app, 0xff0000), 0, "smokeGraphicsFlow returns 0"); +assert.equal(app.stage.children.length, 2, "graphics added as second child"); +const graphics = app.stage.children[1]; +assert.deepEqual(graphics.paths, [{ kind: "rect", x: 0, y: 0, w: 50, h: 50 }], "rect path recorded"); +assert.deepEqual(graphics.fills, [{ color: 0xff0000 }], "fill recorded with color"); + +console.log("pixi_smoke.harness.mjs OK");