@moku-labs/game 0.15.2 → 0.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3753,7 +3753,7 @@ function featureProblems(flows, features) {
3753
3753
  * sceneIdOf("board"); // undefined: a scene is an object with an id
3754
3754
  * ```
3755
3755
  */
3756
- function sceneIdOf(value) {
3756
+ function sceneIdOf$1(value) {
3757
3757
  if (typeof value !== "object" || value === null || !("id" in value)) return void 0;
3758
3758
  return typeof value.id === "string" ? value.id : void 0;
3759
3759
  }
@@ -3772,7 +3772,7 @@ function declaredScenes(features) {
3772
3772
  for (const list of lists) {
3773
3773
  if (!Array.isArray(list)) continue;
3774
3774
  for (const scene of list) {
3775
- const id = sceneIdOf(scene);
3775
+ const id = sceneIdOf$1(scene);
3776
3776
  if (id !== void 0) ids.add(id);
3777
3777
  }
3778
3778
  }
@@ -8374,6 +8374,32 @@ const NO_ROWS = {
8374
8374
  next: () => DONE
8375
8375
  };
8376
8376
  /**
8377
+ * Tells whether a system's query may match, before its run: every term has a store and none of them
8378
+ * is empty, which is the smallest store among the terms not being empty. A store met for the first
8379
+ * time is kept in the entry, so later checks only read sizes. An empty query may always match. An
8380
+ * entity that carries one term and not the others still passes: the walk finds nothing, as before.
8381
+ *
8382
+ * @param stores - Every component store of the world, by name.
8383
+ * @param entry - The registered system.
8384
+ * @returns False when a term has no entity: the system is skipped.
8385
+ */
8386
+ function mayMatch(stores, entry) {
8387
+ const names = entry.plan.names;
8388
+ const kept = entry.stores;
8389
+ const count = names.length;
8390
+ for (let index = 0; index < count; index += 1) {
8391
+ let store = kept[index];
8392
+ if (store === void 0) {
8393
+ const name = names[index];
8394
+ store = name === void 0 ? void 0 : stores.get(name);
8395
+ if (store === void 0) return false;
8396
+ kept[index] = store;
8397
+ }
8398
+ if (store.size === 0) return false;
8399
+ }
8400
+ return true;
8401
+ }
8402
+ /**
8377
8403
  * The rows the runner hands a system's `run`: its walk, reset; none for an empty query. A system
8378
8404
  * entry made without a walk gets one here, once.
8379
8405
  *
@@ -8467,6 +8493,113 @@ function rememberPlan(ctx, key, terms) {
8467
8493
  return plan;
8468
8494
  }
8469
8495
  //#endregion
8496
+ //#region src/plugins/world/ecs/scenes.ts
8497
+ /**
8498
+ * Creates the set of declared scene ids. Its own function: a collection is built outside the
8499
+ * exported declarations.
8500
+ *
8501
+ * @returns An empty set.
8502
+ */
8503
+ function emptyIds() {
8504
+ return /* @__PURE__ */ new Set();
8505
+ }
8506
+ /**
8507
+ * Reads the id of one entry of a feature's `scenes` list, without trusting the entry.
8508
+ *
8509
+ * @param entry - What the feature put in the list.
8510
+ * @returns The id, or `undefined` when the entry carries none.
8511
+ * @example
8512
+ * ```ts
8513
+ * sceneIdOf({ id: "board", layers: {} }); // "board"
8514
+ * ```
8515
+ */
8516
+ function sceneIdOf(entry) {
8517
+ if (typeof entry !== "object" || entry === null) return void 0;
8518
+ const id = entry.id;
8519
+ return typeof id === "string" ? id : void 0;
8520
+ }
8521
+ /**
8522
+ * Records the scene ids every feature declares, so the systems registered from now on have their
8523
+ * `scenes` checked. Called once at start, before the features' systems register.
8524
+ *
8525
+ * @param scene - The scene state.
8526
+ * @param features - The registered features.
8527
+ */
8528
+ function declareScenes(scene, features) {
8529
+ const declared = emptyIds();
8530
+ for (const feature of features) {
8531
+ const list = feature.description.scenes;
8532
+ if (!Array.isArray(list)) continue;
8533
+ for (const entry of list) {
8534
+ const id = sceneIdOf(entry);
8535
+ if (id !== void 0) declared.add(id);
8536
+ }
8537
+ }
8538
+ scene.declared = declared;
8539
+ }
8540
+ /**
8541
+ * Throws for a name in a system's `scenes` that no feature declares as a scene: such a system
8542
+ * would never run. Nothing is checked before start, when the declared scenes are not known yet.
8543
+ * The caller guards it for dev builds.
8544
+ *
8545
+ * @param scene - The scene state.
8546
+ * @param definition - The system being registered.
8547
+ * @throws {Error} When a name in `scenes` is not a declared scene.
8548
+ */
8549
+ function checkScenes(scene, definition) {
8550
+ const declared = scene.declared;
8551
+ if (declared === void 0 || definition.scenes === void 0) return;
8552
+ for (const name of definition.scenes) {
8553
+ if (declared.has(name)) continue;
8554
+ const known = [...declared].join(", ") || "none";
8555
+ throw new Error(`[game] System "${definition.name}" runs on the scene "${name}", which no feature declares.\n Name a scene a feature lists in its scenes (declared: ${known}).`);
8556
+ }
8557
+ }
8558
+ /**
8559
+ * Mounts a scene for the systems: the scene mounted before starts leaving. The same scene again
8560
+ * changes nothing.
8561
+ *
8562
+ * @param scene - The scene state.
8563
+ * @param id - The id of the scene now mounted.
8564
+ */
8565
+ function mountScene(scene, id) {
8566
+ const before = scene.mounted;
8567
+ if (before === id) return;
8568
+ if (before !== void 0 && !scene.leaving.includes(before)) scene.leaving.push(before);
8569
+ scene.mounted = id;
8570
+ }
8571
+ /**
8572
+ * Ends the leave of every leaving scene once nothing carries `Exiting`: the elements of the JSX
8573
+ * views of the scenes before played their exit. Called at the start of a phase while a scene leaves.
8574
+ *
8575
+ * @param state - The ecs branch of the plugin state.
8576
+ */
8577
+ function endLeaving(state) {
8578
+ const exiting = state.stores.get(Exiting.componentName);
8579
+ if (exiting !== void 0 && exiting.size > 0) return;
8580
+ const leaving = state.scene.leaving;
8581
+ while (leaving.length > 0) leaving.pop();
8582
+ }
8583
+ /**
8584
+ * Tells whether one of a system's scenes is mounted or leaving.
8585
+ *
8586
+ * @param scene - The scene state.
8587
+ * @param names - The system's `scenes`.
8588
+ * @returns True when the system may run.
8589
+ */
8590
+ function onScene(scene, names) {
8591
+ const { mounted, leaving } = scene;
8592
+ const count = names.length;
8593
+ const left = leaving.length;
8594
+ for (let index = 0; index < count; index += 1) {
8595
+ const name = names[index];
8596
+ if (name === void 0) continue;
8597
+ if (name === mounted) return true;
8598
+ for (let at = 0; at < left; at += 1) if (leaving[at] === name) return true;
8599
+ }
8600
+ return false;
8601
+ }
8602
+ //#endregion
8470
8603
  //#region src/plugins/world/ecs/schema.ts
8471
8604
  const notJson = null;
8472
8605
  /**
@@ -8635,13 +8768,27 @@ function frameSnapshot(ctx, time) {
8635
8768
  return snapshot;
8636
8769
  }
8637
8770
  /**
8638
- * Registers a system at the end of its phase list and returns the remover. Its query is resolved
8639
- * here, once: a later change to the definition's `query` array is not seen.
8771
+ * An empty slot for each term of a plan: the stores its check keeps, filled with `push`, so the
8772
+ * list is a packed array of its exact length from the start.
8773
+ *
8774
+ * @param count - The number of terms.
8775
+ * @returns The slots, all `undefined`.
8776
+ */
8777
+ function storeSlots(count) {
8778
+ const slots = [];
8779
+ for (let index = 0; index < count; index += 1) slots.push(void 0);
8780
+ return slots;
8781
+ }
8782
+ /**
8783
+ * Registers a system at the end of its phase list and returns the remover. Its query and its
8784
+ * `scenes` are read here, once: a later change to the definition is not seen. A dev build checks
8785
+ * the `scenes` against the scenes the features declare, once the world started.
8640
8786
  *
8641
8787
  * @param ctx - Domain context of the world plugin.
8642
8788
  * @param definition - The system.
8643
8789
  * @returns The remover; calling it twice is a no-op.
8644
- * @throws {Error} When a system of that name is already registered.
8790
+ * @throws {Error} When a system of that name is already registered, and in a dev build for a
8791
+ * name in `scenes` that no feature declares.
8645
8792
  */
8646
8793
  function registerSystem(ctx, definition) {
8647
8794
  const state = ctx.state.ecs;
@@ -8651,11 +8798,15 @@ function registerSystem(ctx, definition) {
8651
8798
  "layout",
8652
8799
  "sync"
8653
8800
  ]) if (state.systems[phase].some((entry) => entry.definition.name === definition.name)) throw new Error(`[game] System "${definition.name}" is registered twice.\n Give the second system another name, or remove the first one.`);
8801
+ if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) checkScenes(state.scene, definition);
8654
8802
  const plan = planOf(ctx, definition.query);
8655
8803
  const entry = {
8656
8804
  definition,
8657
8805
  frame: ctx.deps.time.snapshot().frame,
8658
8806
  plan,
8807
+ stores: storeSlots(plan.names.length),
8808
+ scenes: definition.scenes === void 0 ? void 0 : [...definition.scenes],
8809
+ skipped: false,
8659
8810
  walk: plan.names.length === 0 ? void 0 : new SystemWalk(ctx, plan)
8660
8811
  };
8661
8812
  const list = state.systems[definition.phase];
@@ -8682,7 +8833,8 @@ function createPhaseContext(state, world, snapshot, time) {
8682
8833
  world,
8683
8834
  res: (resourceType) => resourceValue(state, resourceType),
8684
8835
  snapshot,
8685
- time
8836
+ time,
8837
+ resumed: false
8686
8838
  };
8687
8839
  state.phaseContext = context;
8688
8840
  return context;
@@ -8707,6 +8859,27 @@ function phaseContextOf(ctx, world, time) {
8707
8859
  return known;
8708
8860
  }
8709
8861
  /**
8862
+ * Tells whether a system runs in this phase: one of its `scenes` is mounted or leaving, and no
8863
+ * term of its query has an empty store. A skipped system remembers it; one that runs is told in
8864
+ * `resumed` whether its last chance was skipped. No walk, no mark and no clock read happen for a
8865
+ * skipped system: the check comes before all three.
8866
+ *
8867
+ * @param state - The ecs branch of the plugin state.
8868
+ * @param entry - The system.
8869
+ * @param context - What its `run` receives; `resumed` is written here.
8870
+ * @returns True when the system runs.
8871
+ */
8872
+ function wakes(state, entry, context) {
8873
+ const scenes = entry.scenes;
8874
+ if (scenes !== void 0 && !onScene(state.scene, scenes) || !mayMatch(state.stores, entry)) {
8875
+ entry.skipped = true;
8876
+ return false;
8877
+ }
8878
+ context.resumed = entry.skipped;
8879
+ entry.skipped = false;
8880
+ return true;
8881
+ }
8882
+ /**
8710
8883
  * Runs one system, reporting a throw: the try lives here, so the loop of a phase holds none.
8711
8884
  *
8712
8885
  * @param ctx - Domain context of the world plugin.
@@ -8726,7 +8899,9 @@ function guardedRun(ctx, entry, context, phase) {
8726
8899
  }
8727
8900
  /**
8728
8901
  * Runs the systems of one phase and flushes the commands they queued. A throwing system is
8729
- * reported and skipped for this frame; the frame goes on.
8902
+ * reported and skipped for this frame; the frame goes on. A system whose query has no entity, or
8903
+ * none of whose `scenes` is mounted or leaving, is skipped in its slot. While a scene leaves, the
8904
+ * phase first ends the leave when nothing carries `Exiting` any more.
8730
8905
  *
8731
8906
  * @param ctx - Domain context of the world plugin.
8732
8907
  * @param world - The ecs API a system's `run` receives.
@@ -8736,21 +8911,24 @@ function guardedRun(ctx, entry, context, phase) {
8736
8911
  function runPhase(ctx, world, phase, time) {
8737
8912
  const state = ctx.state.ecs;
8738
8913
  if (phase !== ALWAYS && effectiveMode(ctx) !== "live") return;
8914
+ if (state.scene.leaving.length > 0) endLeaving(state);
8739
8915
  const context = phaseContextOf(ctx, world, time);
8740
8916
  const entries = state.systems[phase];
8741
8917
  state.running = phase;
8742
8918
  if (state.timing === void 0) for (let index = 0; index < entries.length; index += 1) {
8743
8919
  const entry = entries[index];
8744
- if (entry !== void 0 && entry.frame < time.frame) guardedRun(ctx, entry, context, phase);
8920
+ if (entry === void 0 || entry.frame >= time.frame) continue;
8921
+ if (wakes(state, entry, context)) guardedRun(ctx, entry, context, phase);
8745
8922
  }
8746
8923
  else runTimed(ctx, state.timing, entries, context, phase, time);
8747
8924
  state.running = void 0;
8748
8925
  flushCommands(ctx);
8749
8926
  }
8750
8927
  /**
8751
- * Runs the systems of one phase as `runPhase` does, same order, same error isolation, and adds
8752
- * each run's time to its row of the timing table: two clock reads per run, an indexed loop, no
8753
- * closure. The rows were zeroed at the start of the frame, so after the frame they hold its time.
8928
+ * Runs the systems of one phase as `runPhase` does, same order, same error isolation, same skip,
8929
+ * and adds each run's time to its row of the timing table: two clock reads per run, an indexed
8930
+ * loop, no closure. The rows were zeroed at the start of the frame, so after the frame they hold
8931
+ * its time; a skipped system's row stays 0.
8754
8932
  *
8755
8933
  * @param ctx - Domain context of the world plugin.
8756
8934
  * @param timing - The timing table of the agent build.
@@ -8760,11 +8938,13 @@ function runPhase(ctx, world, phase, time) {
8760
8938
  * @param time - The frame's `Time`.
8761
8939
  */
8762
8940
  function runTimed(ctx, timing, entries, context, phase, time) {
8941
+ const state = ctx.state.ecs;
8763
8942
  const count = entries.length;
8764
8943
  for (let index = 0; index < count; index += 1) {
8765
8944
  const entry = entries[index];
8766
8945
  if (entry === void 0 || entry.frame >= time.frame) continue;
8767
- const row = rowOf(ctx.state.ecs, entry.definition.name);
8946
+ if (!wakes(state, entry, context)) continue;
8947
+ const row = rowOf(state, entry.definition.name);
8768
8948
  const started = timing.clock();
8769
8949
  guardedRun(ctx, entry, context, phase);
8770
8950
  timing.ms[row] = (timing.ms[row] ?? 0) + timing.clock() - started;
@@ -8774,7 +8954,8 @@ function runTimed(ctx, timing, entries, context, phase, time) {
8774
8954
  //#region src/plugins/world/ecs/api.ts
8775
8955
  /**
8776
8956
  * Empties the world in place: entities, components, resources, systems, hooks and the frame
8777
- * history. The state object keeps its identity, so every view on it stays valid.
8957
+ * history. The mounted scene stays: `scenes` still shows it. The state object keeps its identity,
8958
+ * so every view on it stays valid.
8778
8959
  *
8779
8960
  * @param ctx - Domain context of the world plugin.
8780
8961
  */
@@ -8896,6 +9077,9 @@ function createEcsApi(ctx) {
8896
9077
  setMode: (mode) => {
8897
9078
  state.mode = mode;
8898
9079
  },
9080
+ setScene: (scene) => {
9081
+ mountScene(state.scene, scene);
9082
+ },
8899
9083
  typeOf: (name) => asComponent$2(state.types.get(name)),
8900
9084
  snapshot: () => snapshotWorld(state, effectiveMode(ctx)),
8901
9085
  diff: (from, to) => diffHistory(state.history, from, to),
@@ -9033,14 +9217,17 @@ function registerEach(ctx, feature, key, accepts, register) {
9033
9217
  }
9034
9218
  /**
9035
9219
  * Registers what every feature brought, in feature order: components first, so their schema is
9036
- * known, then systems, then projections.
9220
+ * known, then systems, then projections. The scene ids of every feature are read first, so a dev
9221
+ * build checks the `scenes` of each system as it registers.
9037
9222
  *
9038
9223
  * @param ctx - Domain context of the world plugin.
9039
9224
  * @param ecs - The ecs module.
9040
9225
  * @param projection - The projection module.
9041
9226
  */
9042
9227
  function registerFeatures(ctx, ecs, projection) {
9043
- for (const feature of ctx.deps.flow.features.all()) {
9228
+ const features = ctx.deps.flow.features.all();
9229
+ declareScenes(ctx.state.ecs.scene, features);
9230
+ for (const feature of features) {
9044
9231
  registerEach(ctx, feature, "components", isComponentEntry, (entry) => registerType(ctx, entry));
9045
9232
  registerEach(ctx, feature, "systems", isSystemEntry, (entry) => ecs.system(entry));
9046
9233
  registerEach(ctx, feature, "projections", isProjectionEntry, (entry) => projection.register(entry));
@@ -9086,7 +9273,8 @@ function connectWorld(ctx) {
9086
9273
  }
9087
9274
  /**
9088
9275
  * Empties the world in `onStop`: the frame callbacks and the hint listener are removed, then
9089
- * every track, view, queued view, entity and resource is dropped and the frame history is reset.
9276
+ * every track, view, queued view, entity and resource is dropped, and the scenes and the frame
9277
+ * history are reset.
9090
9278
  * No `onRemoved` fires, because `renderer` stopped earlier and destroyed its own objects.
9091
9279
  *
9092
9280
  * @param state - The plugin state, the only thing a teardown context carries.
@@ -9137,6 +9325,9 @@ function clearWorld(state) {
9137
9325
  state.ecs.mode = "live";
9138
9326
  state.ecs.timing = void 0;
9139
9327
  state.ecs.timingSlots.clear();
9328
+ state.ecs.scene.mounted = void 0;
9329
+ state.ecs.scene.leaving.length = 0;
9330
+ state.ecs.scene.declared = void 0;
9140
9331
  resetHistory(state.ecs);
9141
9332
  }
9142
9333
  //#endregion
@@ -11334,6 +11525,7 @@ function exposeEcs(ecs, projection) {
11334
11525
  ecs.setMode(mode);
11335
11526
  if (ecs.mode() === "fast") projection.flushAll();
11336
11527
  },
11528
+ setScene: ecs.setScene,
11337
11529
  snapshot: ecs.snapshot,
11338
11530
  diff: (from, to) => withKeys(ecs.diff(from, to), ecs, projection),
11339
11531
  schema: ecs.schema
@@ -11432,6 +11624,19 @@ function emptyMap$10() {
11432
11624
  return /* @__PURE__ */ new Map();
11433
11625
  }
11434
11626
  /**
11627
+ * Creates the scene state of a world no scene was mounted in: nothing mounted, nothing leaving, the
11628
+ * declared scenes unknown until start.
11629
+ *
11630
+ * @returns An empty scene state.
11631
+ */
11632
+ function emptyScene() {
11633
+ return {
11634
+ mounted: void 0,
11635
+ leaving: [],
11636
+ declared: void 0
11637
+ };
11638
+ }
11639
+ /**
11435
11640
  * Creates the initial ecs state: no entity, no type, no system, no resource and no frame history:
11436
11641
  * a dev build starts one on its first frame. Every collection is its own object, so two apps in
11437
11642
  * one process never share a world.
@@ -11473,7 +11678,8 @@ function createEcsState() {
11473
11678
  walkReported: false,
11474
11679
  queryPlans: emptyMap$10(),
11475
11680
  walkLists: [],
11476
- despawns: 0
11681
+ despawns: 0,
11682
+ scene: emptyScene()
11477
11683
  };
11478
11684
  }
11479
11685
  //#endregion
@@ -11585,7 +11791,7 @@ const worldPlugin = /*#__PURE__*/ createPlugin$1("world", {
11585
11791
  */
11586
11792
  function slotsPadding(slots) {
11587
11793
  let pad = 0;
11588
- for (const slot of slots) pad += slot.filter.padding;
11794
+ for (let index = 0; index < slots.length; index += 1) pad += slots[index]?.filter.padding ?? 0;
11589
11795
  return pad;
11590
11796
  }
11591
11797
  /**
@@ -11745,8 +11951,9 @@ function checkMissedMarks(ctx, state, stage, views, shown) {
11745
11951
  //#endregion
11746
11952
  //#region src/plugins/renderer/demand/shown.ts
11747
11953
  /**
11748
- * The paddings of every filter a node holds, enabled or not: a superset of what any of them could spill onto the
11749
- * screen. Pixi answers `undefined` before the first write and `null` after a clear.
11954
+ * The paddings of every filter a node holds: what they could spill onto the screen. Pixi holds the enabled filters of
11955
+ * a view (E3), and a filter switched on reaches Pixi before the decision judges the marks again. Pixi answers
11956
+ * `undefined` before the first write and `null` after a clear.
11750
11957
  *
11751
11958
  * @param node - A display object on the chain of a view.
11752
11959
  * @returns The sum of the paddings; 0 without a filter.
@@ -11755,7 +11962,7 @@ function paddingOf$2(node) {
11755
11962
  const filters = node.filters;
11756
11963
  if (filters === null || filters === void 0) return 0;
11757
11964
  let pad = 0;
11758
- for (const filter of filters) pad += filter.padding;
11965
+ for (let index = 0; index < filters.length; index += 1) pad += filters[index]?.padding ?? 0;
11759
11966
  return pad;
11760
11967
  }
11761
11968
  /** What `chainPadding` answers for a chain that is not drawn. */
@@ -11777,6 +11984,15 @@ function noAlpha() {
11777
11984
  return false;
11778
11985
  }
11779
11986
  /**
11987
+ * The reader without the engine's records: Pixi's own `alpha`.
11988
+ *
11989
+ * @param node - A node on the chain of a view.
11990
+ * @returns True at alpha 0 or below.
11991
+ */
11992
+ function alphaZero(node) {
11993
+ return node.alpha <= 0;
11994
+ }
11995
+ /**
11780
11996
  * Walks from a node up to the stage: every node on the way is visible, renderable (or a kept top its quad draws) and
11781
11997
  * not fully transparent, and the chain reaches the stage. The paddings of the filters met on the way are summed; no
11782
11998
  * object is made.
@@ -11784,14 +12000,15 @@ function noAlpha() {
11784
12000
  * @param from - The first node of the walk.
11785
12001
  * @param stage - The stage of the application.
11786
12002
  * @param through - The kept reader: the padding of a kept top drawn through its quad, or `HIDDEN`.
12003
+ * @param transparent - Whether a node is drawn at alpha 0.
11787
12004
  * @param stop - Where the walk notes a visible node at alpha 0 it stopped at; left as it is for any other stop.
11788
12005
  * @returns The sum of the paddings of the chain, or `HIDDEN` when the chain is not drawn.
11789
12006
  */
11790
- function chainPadding(from, stage, through, stop) {
12007
+ function chainPadding(from, stage, through, transparent, stop) {
11791
12008
  let pad = 0;
11792
12009
  for (let node = from; node !== stage; node = node.parent) {
11793
12010
  if (node === null || !node.visible) return HIDDEN;
11794
- if (node.alpha <= 0) {
12011
+ if (transparent(node)) {
11795
12012
  if (stop !== void 0) stop.node = node;
11796
12013
  return HIDDEN;
11797
12014
  }
@@ -11861,15 +12078,16 @@ function alphaStop(node, top, alphaHidden) {
11861
12078
  * @param entity - The entity.
11862
12079
  * @param through - The kept reader: the padding of a kept top drawn through its quad, or `HIDDEN`; none by default.
11863
12080
  * @param alphaHidden - Sync's reader of the `"alpha"` reason; none by default.
12081
+ * @param transparent - Whether a node is drawn at alpha 0; Pixi's `alpha` by default.
11864
12082
  * @returns Where the walk stopped.
11865
12083
  */
11866
- function unseenBy(state, host, views, entity, through = noneThrough, alphaHidden = noAlpha) {
12084
+ function unseenBy(state, host, views, entity, through = noneThrough, alphaHidden = noAlpha, transparent = alphaZero) {
11867
12085
  const view = views.get(entity);
11868
12086
  const stage = host.stage();
11869
12087
  if (view === void 0 || stage === void 0) return "other";
11870
12088
  const top = view.wrapper ?? view.object;
11871
12089
  state.stop.node = void 0;
11872
- const pad = chainPadding(top, stage, through, state.stop);
12090
+ const pad = chainPadding(top, stage, through, transparent, state.stop);
11873
12091
  if (pad === HIDDEN) return alphaStop(state.stop.node, top, alphaHidden);
11874
12092
  if (view.wrapper !== void 0 || top.children.length > 0) return "shown";
11875
12093
  return onScreen(state, host, top, pad) ? "shown" : "off-screen";
@@ -11882,10 +12100,11 @@ function unseenBy(state, host, views, entity, through = noneThrough, alphaHidden
11882
12100
  * @param views - The views by entity.
11883
12101
  * @param entity - The marked entity.
11884
12102
  * @param through - The kept reader: the padding of a kept top drawn through its quad, or `HIDDEN`; none by default.
12103
+ * @param transparent - Whether a node is drawn at alpha 0; Pixi's `alpha` by default.
11885
12104
  * @returns True when the view is drawn.
11886
12105
  */
11887
- function isShown$2(state, host, views, entity, through = noneThrough) {
11888
- return unseenBy(state, host, views, entity, through) === "shown";
12106
+ function isShown$2(state, host, views, entity, through = noneThrough, transparent = alphaZero) {
12107
+ return unseenBy(state, host, views, entity, through, noAlpha, transparent) === "shown";
11889
12108
  }
11890
12109
  /**
11891
12110
  * The ancestor rule of loose marks: the walk of `isShown` from the parent of the view's top node, without bounds. A
@@ -11896,13 +12115,14 @@ function isShown$2(state, host, views, entity, through = noneThrough) {
11896
12115
  * @param views - The views by entity.
11897
12116
  * @param entity - The entity of a loose mark.
11898
12117
  * @param through - The kept reader: the padding of a kept top drawn through its quad, or `HIDDEN`; none by default.
12118
+ * @param transparent - Whether a node is drawn at alpha 0; Pixi's `alpha` by default.
11899
12119
  * @returns True when the ancestors of the view draw it.
11900
12120
  */
11901
- function ancestorsShown(host, views, entity, through = noneThrough) {
12121
+ function ancestorsShown(host, views, entity, through = noneThrough, transparent = alphaZero) {
11902
12122
  const view = views.get(entity);
11903
12123
  const stage = host.stage();
11904
12124
  if (view === void 0 || stage === void 0) return false;
11905
- return chainPadding((view.wrapper ?? view.object).parent, stage, through) !== HIDDEN;
12125
+ return chainPadding((view.wrapper ?? view.object).parent, stage, through, transparent) !== HIDDEN;
11906
12126
  }
11907
12127
  //#endregion
11908
12128
  //#region src/plugins/renderer/pixels.ts
@@ -12025,6 +12245,15 @@ function firstWhole(wholes) {
12025
12245
  return wholes.size === 0 ? void 0 : wholes.values().next().value;
12026
12246
  }
12027
12247
  /**
12248
+ * Whether a node is drawn at alpha 0, read from Pixi: the reader of a demand module built without sync's records.
12249
+ *
12250
+ * @param node - A node on the chain of a view.
12251
+ * @returns True at alpha 0 or below.
12252
+ */
12253
+ function pixiTransparent(node) {
12254
+ return node.alpha <= 0;
12255
+ }
12256
+ /**
12028
12257
  * Tells whether one entity of a mark list passes a test, walked by index: no iterator.
12029
12258
  *
12030
12259
  * @param entities - The marks.
@@ -12067,11 +12296,12 @@ function clearMarks(state) {
12067
12296
  * @param deps.host - The host module.
12068
12297
  * @param deps.views - The views by entity: `ctx.state.sync.views`, whose identity sync keeps.
12069
12298
  * @param deps.hiddenBy - Sync's `hiddenBy`: whether one reason hides a view (spec 26 R1.7.1).
12299
+ * @param deps.transparent - Sync's reader of a node drawn at alpha 0 from its records (E4); Pixi's `alpha` without it.
12070
12300
  * @returns The demand module.
12071
12301
  */
12072
12302
  function createDemandApi(ctx, deps) {
12073
12303
  const state = ctx.state.demand;
12074
- const { host, views, hiddenBy } = deps;
12304
+ const { host, views, hiddenBy, transparent = pixiTransparent } = deps;
12075
12305
  /**
12076
12306
  * Whether the render phase decides this frame: `onDemand` is on, or the pixel oracle runs in shadow.
12077
12307
  *
@@ -12084,7 +12314,7 @@ function createDemandApi(ctx, deps) {
12084
12314
  * @param entity - The entity.
12085
12315
  * @returns True when its view is drawn as the frame stands.
12086
12316
  */
12087
- const shown = (entity) => isShown$2(state, host, views, entity, through);
12317
+ const shown = (entity) => isShown$2(state, host, views, entity, through, transparent);
12088
12318
  /**
12089
12319
  * Whether a node is the top or the visual of a view hidden by `"alpha"` (spec 26 R1.7.1), read from sync's state.
12090
12320
  *
@@ -12110,7 +12340,7 @@ function createDemandApi(ctx, deps) {
12110
12340
  * @param entity - The entity of a loose mark.
12111
12341
  * @returns True when it counts.
12112
12342
  */
12113
- const looseShown = (entity) => ancestorsShown(host, views, entity, through);
12343
+ const looseShown = (entity) => ancestorsShown(host, views, entity, through, transparent);
12114
12344
  /**
12115
12345
  * Notes an entity in the record since the last drawn frame, in a dev or an agent build.
12116
12346
  *
@@ -12226,7 +12456,7 @@ function createDemandApi(ctx, deps) {
12226
12456
  settle,
12227
12457
  frame: () => state.frame,
12228
12458
  shown,
12229
- unseenBy: (entity) => unseenBy(state, host, views, entity, through, alphaHidden),
12459
+ unseenBy: (entity) => unseenBy(state, host, views, entity, through, alphaHidden, transparent),
12230
12460
  marked: () => state.marked,
12231
12461
  loose: () => state.loose,
12232
12462
  visual: () => state.visual,
@@ -12589,281 +12819,486 @@ function watchPixelRatio(ctx) {
12589
12819
  state.cleanups.push(() => query?.removeEventListener("change", follow));
12590
12820
  }
12591
12821
  //#endregion
12592
- //#region src/plugins/renderer/host/sdf-text.ts
12822
+ //#region src/plugins/renderer/host/sdf-batcher.ts
12823
+ /** The key of the glyph record on the proxy graphics of every bitmap text the renderer's pipe lays out. */
12824
+ const SDF = Symbol("moku-sdf-glyphs");
12825
+ /** The name the renderer's batcher of distance-field glyphs registers under, and the element's `batcherName`. */
12826
+ const SDF_BATCHER = "moku-sdf";
12827
+ /** The name of Pixi's default batcher, which an element goes back to. */
12828
+ const DEFAULT_BATCHER = "default";
12829
+ /** Floats of one vertex: position 2, uv 2, colour 1 (unorm8x4), texture id and rounding 1 (uint16x2), distance 1. */
12830
+ const VERTEX_SIZE = 7;
12831
+ /** Bytes of one vertex. */
12832
+ const STRIDE = 28;
12833
+ /** The name of the renderer's batched SDF program, on both backends. */
12834
+ const PROGRAM_NAME = "moku-sdf-batch";
12835
+ /**
12836
+ * The renderer's piece of the high shader: the glyph's edge width from the vertex, at the scale of the render group,
12837
+ * and the coverage with the alpha applied once. The template's `outColor * vColor` applies the colour and every alpha.
12838
+ */
12839
+ const SDF_BIT = {
12840
+ name: "moku-sdf-batch-bit",
12841
+ vertex: {
12842
+ header: `
12843
+ @in aSdfDistance: f32;
12844
+ @out @interpolate(flat) vSdfDistance: f32;
12845
+ `,
12846
+ main: `
12847
+ let sdfWorld = globalUniforms.uWorldTransformMatrix;
12848
+ vSdfDistance = aSdfDistance * (length(sdfWorld[0].xy) + length(sdfWorld[1].xy)) * 0.5;
12849
+ `
12850
+ },
12851
+ fragment: {
12852
+ header: `
12853
+ @in @interpolate(flat) vSdfDistance: f32;
12854
+ `,
12855
+ main: `
12856
+ outColor = vec4<f32>(calculateMSDFAlpha(outColor, vec4<f32>(vColor.rgb / max(vColor.a, 1e-4), 1.0), vSdfDistance));
12857
+ `
12858
+ }
12859
+ };
12593
12860
  /**
12594
- * The fragment step of the renderer's WGSL SDF shader, in place of Pixi's
12595
- * `calculateMSDFAlpha(outColor, vColor, …)`. The coverage is the glyph's alone and its gamma
12596
- * leans on the true luma of the colour; the template's `outColor * vColor` applies the alpha.
12597
- */
12598
- const SINGLE_ALPHA_STEP = `
12599
- outColor = vec4<f32>(calculateMSDFAlpha(outColor, vec4<f32>(vColor.rgb / max(vColor.a, 1e-4), 1.0), localUniforms.uDistance));
12600
- `;
12601
- /** The same fragment step in GLSL, for the WebGL program. */
12602
- const SINGLE_ALPHA_STEP_GL = `
12603
- outColor = vec4(calculateMSDFAlpha(outColor, vec4(vColor.rgb / max(vColor.a, 1e-4), 1.0), uDistance));
12604
- `;
12605
- /** The name of the renderer's local-uniform piece, apart from Pixi's in its shader cache. */
12606
- const TEXT_BIT_NAME = "local-uniform-msdf-single-alpha-bit";
12607
- /** The name of the renderer's SDF program, on both backends. */
12608
- const PROGRAM_NAME = "sdf-shader-single-alpha";
12861
+ * The same piece in GLSL, for the WebGL program. Pixi compiles its batch programs as GLSL ES 1.00 (its `in` and `out`
12862
+ * are macros), where `flat` is a reserved word: the distance is a plain varying, the same at the three vertices of a
12863
+ * glyph's triangle, as Pixi's own texture id is.
12864
+ */
12865
+ const SDF_BIT_GL = {
12866
+ name: "moku-sdf-batch-bit",
12867
+ vertex: {
12868
+ header: `
12869
+ in float aSdfDistance;
12870
+ out float vSdfDistance;
12871
+ `,
12872
+ main: `
12873
+ vSdfDistance = aSdfDistance * (length(uWorldTransformMatrix[0].xy) + length(uWorldTransformMatrix[1].xy)) * 0.5;
12874
+ `
12875
+ },
12876
+ fragment: {
12877
+ header: `
12878
+ in float vSdfDistance;
12879
+ `,
12880
+ main: `
12881
+ outColor = vec4(calculateMSDFAlpha(outColor, vec4(vColor.rgb / max(vColor.a, 1e-4), 1.0), vSdfDistance));
12882
+ `
12883
+ }
12884
+ };
12609
12885
  /**
12610
- * The scale of a transform as Pixi's `_updateDistanceField` measures it: the mean length of its two axes. Pure.
12886
+ * The edge width one vertex of an element carries: Pixi's distance at the element's scale in its render group, times
12887
+ * the resolution. A text's group transform is what Pixi scales by; the render group's own scale is the shader's.
12611
12888
  *
12612
- * @param linear - The linear part of a transform.
12613
- * @returns The scale.
12614
- * @example
12615
- * ```ts
12616
- * scaleOf({ a: 0.25, b: 0, c: 0, d: 0.25 }); // 0.25
12617
- * ```
12889
+ * @param element - The element.
12890
+ * @param a - `a` of its group transform.
12891
+ * @param b - `b` of its group transform.
12892
+ * @param c - `c` of its group transform.
12893
+ * @param d - `d` of its group transform.
12894
+ * @param density - Device pixels per CSS pixel.
12895
+ * @returns The distance, device px at the render group's scale 1; 0 for an element without glyphs.
12618
12896
  */
12619
- function scaleOf$1(linear) {
12620
- return (Math.hypot(linear.a, linear.b) + Math.hypot(linear.c, linear.d)) / 2;
12897
+ function distanceOf(element, a, b, c, d, density) {
12898
+ const glyphs = element.renderable?.[SDF];
12899
+ if (glyphs === void 0) return 0;
12900
+ return glyphs.perScale * (Math.sqrt(a * a + b * b) + Math.sqrt(c * c + d * d)) * density / 2;
12621
12901
  }
12622
12902
  /**
12623
- * The local uniforms of one text, in the order of the WGSL struct, at Pixi's starting values. The
12624
- * pipe writes `uDistance`, the graphics adaptor the rest, before every draw.
12903
+ * Keeps one shader per batch texture limit: made the first time a limit is asked for, shared by every batcher after.
12904
+ * Its own function, because lint rule L5 refuses a collection built inside an exported declaration.
12625
12905
  *
12626
- * Pixi writes `uDistance` in CSS pixels (`AbstractBitmapTextPipe._updateDistanceField`: the scale
12627
- * of the text's group transform times the font's range over its scale), while the shader measures
12628
- * the glyph edge in device pixels, so at a resolution above 1 the edge is smeared over that many
12629
- * more pixels. The group's `uDistance` is therefore an accessor: Pixi's write keeps the CSS value,
12630
- * every read answers it times `density.resolution`, and Pixi reads the uniform by property each
12631
- * time it uploads it, on both backends. The resolution is read at the draw, so a change of
12632
- * resolution needs nothing more.
12906
+ * @param make - Makes the shader of one texture limit.
12907
+ * @returns The shader for a texture limit.
12908
+ */
12909
+ function cachedBy(make) {
12910
+ const shaders = /* @__PURE__ */ new Map();
12911
+ return (maxTextures) => {
12912
+ const known = shaders.get(maxTextures);
12913
+ if (known !== void 0) return known;
12914
+ const shader = make(maxTextures);
12915
+ shaders.set(maxTextures, shader);
12916
+ return shader;
12917
+ };
12918
+ }
12919
+ /**
12920
+ * The shaders of the batcher: Pixi's five SDF bits on both backends with the local uniforms replaced by the
12921
+ * renderer's piece, and Pixi's batch sampler slots for WebGL, as Pixi's own `DefaultShader` carries them.
12633
12922
  *
12634
- * The scale is the text's world scale, read at the draw as well. Pixi's group transform is the
12635
- * text against its render group, which leaves out the group's own scale: a kept picture renders its
12636
- * view as a render group of its own, so its labels would draw with other edges than live. And Pixi
12637
- * writes the distance only when the group's instructions are built, so a label whose view scaled
12638
- * since would keep the edge of the old scale. A write of Pixi made while the pipe names the text
12639
- * keeps the distance per unit of the group scale it was made with; a read answers that times the
12640
- * text's world scale. A write without a text, or at a group scale of 0, is answered as Pixi wrote it.
12923
+ * @param pixi - The loaded Pixi module.
12924
+ * @returns The shader for a texture limit.
12925
+ */
12926
+ function sdfShaders(pixi) {
12927
+ return cachedBy((maxTextures) => new pixi.Shader({
12928
+ gpuProgram: pixi.compileHighShaderGpuProgram({
12929
+ name: PROGRAM_NAME,
12930
+ bits: [
12931
+ pixi.colorBit,
12932
+ pixi.generateTextureBatchBit(maxTextures),
12933
+ SDF_BIT,
12934
+ pixi.mSDFBit,
12935
+ pixi.roundPixelsBit
12936
+ ]
12937
+ }),
12938
+ glProgram: pixi.compileHighShaderGlProgram({
12939
+ name: PROGRAM_NAME,
12940
+ bits: [
12941
+ pixi.colorBitGl,
12942
+ pixi.generateTextureBatchBitGl(maxTextures),
12943
+ SDF_BIT_GL,
12944
+ pixi.mSDFBitGl,
12945
+ pixi.roundPixelsBitGl
12946
+ ]
12947
+ }),
12948
+ resources: { batchSamplers: pixi.getBatchSamplersUniformGroup(maxTextures) }
12949
+ }));
12950
+ }
12951
+ /**
12952
+ * The geometry of one batcher: Pixi's batch geometry with the distance after the texture id, one buffer of 7 floats a
12953
+ * vertex.
12641
12954
  *
12642
12955
  * @param pixi - The loaded Pixi module.
12643
- * @param density - What the device pixels per CSS pixel are read from at each draw: the renderer.
12644
- * @param density.resolution - The renderer's resolution, read at each upload.
12645
- * @param writing - The text whose distance Pixi writes now, held by the pipe.
12646
- * @returns A new uniform group.
12647
- */
12648
- function createLocalUniforms(pixi, density, writing) {
12649
- const group = new pixi.UniformGroup({
12650
- uColor: {
12651
- value: new Float32Array([
12652
- 1,
12653
- 1,
12654
- 1,
12655
- 1
12656
- ]),
12657
- type: "vec4<f32>"
12658
- },
12659
- uTransformMatrix: {
12660
- value: new pixi.Matrix(),
12661
- type: "mat3x3<f32>"
12662
- },
12663
- uDistance: {
12664
- value: 4,
12665
- type: "f32"
12666
- },
12667
- uRound: {
12668
- value: 0,
12669
- type: "f32"
12670
- }
12956
+ * @returns A new geometry.
12957
+ */
12958
+ function sdfGeometry(pixi) {
12959
+ const buffer = new pixi.Buffer({
12960
+ data: /* @__PURE__ */ new Float32Array(1),
12961
+ label: "attribute-sdf-batch-buffer",
12962
+ usage: pixi.BufferUsage.VERTEX | pixi.BufferUsage.COPY_DST,
12963
+ shrinkToFit: false
12671
12964
  });
12672
- let css = 4;
12673
- let text;
12674
- let perScale = 0;
12675
- Object.defineProperty(group.uniforms, "uDistance", {
12676
- enumerable: true,
12677
- get: () => {
12678
- return (text !== void 0 && perScale !== 0 && text !== void 0 ? perScale * scaleOf$1(text.worldTransform) : css) * density.resolution;
12679
- },
12680
- set: (value) => {
12681
- const scale = writing.text === void 0 ? 0 : scaleOf$1(writing.text.groupTransform);
12682
- css = value;
12683
- text = writing.text;
12684
- perScale = scale > 0 ? value / scale : 0;
12685
- }
12965
+ const indexBuffer = new pixi.Buffer({
12966
+ data: /* @__PURE__ */ new Uint32Array(1),
12967
+ label: "index-sdf-batch-buffer",
12968
+ usage: pixi.BufferUsage.INDEX | pixi.BufferUsage.COPY_DST,
12969
+ shrinkToFit: false
12970
+ });
12971
+ return new pixi.Geometry({
12972
+ attributes: {
12973
+ aPosition: {
12974
+ buffer,
12975
+ format: "float32x2",
12976
+ stride: STRIDE,
12977
+ offset: 0
12978
+ },
12979
+ aUV: {
12980
+ buffer,
12981
+ format: "float32x2",
12982
+ stride: STRIDE,
12983
+ offset: 8
12984
+ },
12985
+ aColor: {
12986
+ buffer,
12987
+ format: "unorm8x4",
12988
+ stride: STRIDE,
12989
+ offset: 16
12990
+ },
12991
+ aTextureIdAndRound: {
12992
+ buffer,
12993
+ format: "uint16x2",
12994
+ stride: STRIDE,
12995
+ offset: 20
12996
+ },
12997
+ aSdfDistance: {
12998
+ buffer,
12999
+ format: "float32",
13000
+ stride: STRIDE,
13001
+ offset: 24
13002
+ }
13003
+ },
13004
+ indexBuffer
12686
13005
  });
12687
- return group;
12688
13006
  }
12689
13007
  /**
12690
- * The shader a text already draws with on one renderer: the custom shader of its graphics context.
13008
+ * Builds the batcher of distance-field glyphs from the module object, at call time, because Pixi's `Batcher` arrives
13009
+ * with the lazily loaded module. Its packing is Pixi's `DefaultBatcher`'s loop with the distance written after the
13010
+ * texture id; the distances are packed at the resolution the batcher pipe last drew it at (`packAt`).
12691
13011
  *
12692
- * @param text - The text.
12693
- * @param renderer - The renderer's uid, the key of the text's GPU data.
12694
- * @returns The shader, or `undefined` before the text was laid out on that renderer.
12695
- */
12696
- function shaderOf(text, renderer) {
12697
- return text._gpuData[renderer]?.context.customShader ?? void 0;
13012
+ * @param pixi - The loaded Pixi module.
13013
+ * @returns The batcher class, with the extension Pixi's registry files it under.
13014
+ */
13015
+ function createSdfBatcher(pixi) {
13016
+ const shaderFor = sdfShaders(pixi);
13017
+ /** Pixi's batcher, with a vertex that carries the edge width of its glyph. */
13018
+ class SdfBatcher extends pixi.Batcher {
13019
+ static extension = {
13020
+ type: [pixi.ExtensionType.Batcher],
13021
+ name: SDF_BATCHER
13022
+ };
13023
+ name = SDF_BATCHER;
13024
+ geometry = sdfGeometry(pixi);
13025
+ shader = shaderFor(this.maxTextures);
13026
+ /** Device pixels per CSS pixel the distances are packed at; a double from the start, so writes box nothing. */
13027
+ density = NaN;
13028
+ vertexSize = VERTEX_SIZE;
13029
+ /**
13030
+ * Packs the vertices of a mesh element: Pixi's default vertex and the distance.
13031
+ *
13032
+ * @param element - The element.
13033
+ * @param float32View - The attribute buffer as floats.
13034
+ * @param uint32View - The attribute buffer as unsigned integers.
13035
+ * @param index - The first float of the element.
13036
+ * @param textureId - The element's slot in its batch's textures.
13037
+ */
13038
+ packAttributes(element, float32View, uint32View, index, textureId) {
13039
+ const textureIdAndRound = textureId << 16 | element.roundPixels & 65535;
13040
+ const { a, b, c, d, tx, ty } = element.transform;
13041
+ const distance = distanceOf(element, a, b, c, d, this.density);
13042
+ const positions = element.positions;
13043
+ const uvs = element.uvs;
13044
+ const argb = element.color;
13045
+ const end = element.attributeOffset + element.attributeSize;
13046
+ let at = index;
13047
+ for (let vertex = element.attributeOffset; vertex < end; vertex += 1) {
13048
+ const x = positions[vertex * 2] ?? 0;
13049
+ const y = positions[vertex * 2 + 1] ?? 0;
13050
+ float32View[at] = a * x + c * y + tx;
13051
+ float32View[at + 1] = d * y + b * x + ty;
13052
+ float32View[at + 2] = uvs[vertex * 2] ?? 0;
13053
+ float32View[at + 3] = uvs[vertex * 2 + 1] ?? 0;
13054
+ uint32View[at + 4] = argb;
13055
+ uint32View[at + 5] = textureIdAndRound;
13056
+ float32View[at + 6] = distance;
13057
+ at += VERTEX_SIZE;
13058
+ }
13059
+ }
13060
+ /**
13061
+ * Packs the four vertices of a quad element: Pixi's default quad and the distance.
13062
+ *
13063
+ * @param element - The element.
13064
+ * @param float32View - The attribute buffer as floats.
13065
+ * @param uint32View - The attribute buffer as unsigned integers.
13066
+ * @param index - The first float of the element.
13067
+ * @param textureId - The element's slot in its batch's textures.
13068
+ */
13069
+ packQuadAttributes(element, float32View, uint32View, index, textureId) {
13070
+ const { a, b, c, d, tx, ty } = element.transform;
13071
+ const { minX, minY, maxX, maxY } = element.bounds;
13072
+ const uvs = element.texture.uvs;
13073
+ const argb = element.color;
13074
+ const textureIdAndRound = textureId << 16 | element.roundPixels & 65535;
13075
+ const distance = distanceOf(element, a, b, c, d, this.density);
13076
+ float32View[index] = a * minX + c * minY + tx;
13077
+ float32View[index + 1] = d * minY + b * minX + ty;
13078
+ float32View[index + 2] = uvs.x0;
13079
+ float32View[index + 3] = uvs.y0;
13080
+ uint32View[index + 4] = argb;
13081
+ uint32View[index + 5] = textureIdAndRound;
13082
+ float32View[index + 6] = distance;
13083
+ float32View[index + 7] = a * maxX + c * minY + tx;
13084
+ float32View[index + 8] = d * minY + b * maxX + ty;
13085
+ float32View[index + 9] = uvs.x1;
13086
+ float32View[index + 10] = uvs.y1;
13087
+ uint32View[index + 11] = argb;
13088
+ uint32View[index + 12] = textureIdAndRound;
13089
+ float32View[index + 13] = distance;
13090
+ float32View[index + 14] = a * maxX + c * maxY + tx;
13091
+ float32View[index + 15] = d * maxY + b * maxX + ty;
13092
+ float32View[index + 16] = uvs.x2;
13093
+ float32View[index + 17] = uvs.y2;
13094
+ uint32View[index + 18] = argb;
13095
+ uint32View[index + 19] = textureIdAndRound;
13096
+ float32View[index + 20] = distance;
13097
+ float32View[index + 21] = a * minX + c * maxY + tx;
13098
+ float32View[index + 22] = d * maxY + b * minX + ty;
13099
+ float32View[index + 23] = uvs.x3;
13100
+ float32View[index + 24] = uvs.y3;
13101
+ uint32View[index + 25] = argb;
13102
+ uint32View[index + 26] = textureIdAndRound;
13103
+ float32View[index + 27] = distance;
13104
+ }
13105
+ /**
13106
+ * Packs every element of the drawn batches again at a resolution and hands the buffer to the GPU.
13107
+ *
13108
+ * @param density - Device pixels per CSS pixel.
13109
+ */
13110
+ packAt(density) {
13111
+ this.density = density;
13112
+ for (let batch = 0; batch < this.batchIndex; batch += 1) {
13113
+ const elements = this.batches[batch]?.elements;
13114
+ if (elements === void 0) continue;
13115
+ for (let at = 0; at < elements.length; at += 1) {
13116
+ const element = elements[at];
13117
+ if (element !== void 0) this.updateElement(element);
13118
+ }
13119
+ }
13120
+ this.dirty = false;
13121
+ this.geometry.buffers[0]?.update(this.attributeSize * 4);
13122
+ }
13123
+ }
13124
+ return SdfBatcher;
12698
13125
  }
12699
13126
  /**
12700
- * Keeps one compiled program per batch texture limit: compiled the first time a limit is asked
12701
- * for, shared by every text after. Its own function, because lint rule L5 refuses a collection
12702
- * built inside an exported declaration.
13127
+ * Tells whether a batch's batcher is the renderer's batcher of distance-field glyphs.
12703
13128
  *
12704
- * @param compile - Compiles the program of one texture limit.
12705
- * @returns The program for a texture limit.
13129
+ * @param batcher - The batcher of a batch.
13130
+ * @returns True for the SDF batcher.
12706
13131
  */
12707
- function cachedBy(compile) {
12708
- const programs = /* @__PURE__ */ new Map();
12709
- return (maxTextures) => {
12710
- const known = programs.get(maxTextures);
12711
- if (known !== void 0) return known;
12712
- const program = compile(maxTextures);
12713
- programs.set(maxTextures, program);
12714
- return program;
12715
- };
13132
+ function isSdfBatcher(batcher) {
13133
+ return batcher.name === SDF_BATCHER;
12716
13134
  }
12717
13135
  /**
12718
- * The compile step of the renderer's WGSL SDF program: Pixi's `SdfShader` bits with the fragment
12719
- * step of the local-uniform piece replaced.
13136
+ * Builds the renderer's batcher pipe from the module object, at call time. It keeps the base's `static extension`
13137
+ * object, so the registry files it under the same types and name, and Pixi's batcher registry, which `getBatcher`
13138
+ * reads. `addToBatch` names the batcher of an element before Pixi adds it: the glyphs of a distance-field text go to
13139
+ * `SDF_BATCHER`; a pooled element that carried that name before, now another graphics' or a plain font's, goes back to
13140
+ * Pixi's default. `execute` packs the distances of a batcher again when the renderer's resolution changed since it
13141
+ * last drew, before Pixi binds its geometry. The monitor's counting pipe of a dev build extends this pipe.
12720
13142
  *
12721
13143
  * @param pixi - The loaded Pixi module.
12722
- * @returns The WebGPU program for a texture limit.
13144
+ * @returns The pipe class.
12723
13145
  */
12724
- function gpuProgramCache(pixi) {
12725
- const base = pixi.localUniformMSDFBit;
12726
- const textBit = {
12727
- ...base,
12728
- name: TEXT_BIT_NAME,
12729
- fragment: {
12730
- ...base.fragment,
12731
- main: SINGLE_ALPHA_STEP
12732
- }
12733
- };
12734
- return cachedBy((maxTextures) => pixi.compileHighShaderGpuProgram({
12735
- name: PROGRAM_NAME,
12736
- bits: [
12737
- pixi.colorBit,
12738
- pixi.generateTextureBatchBit(maxTextures),
12739
- textBit,
12740
- pixi.mSDFBit,
12741
- pixi.roundPixelsBit
12742
- ]
12743
- }));
13146
+ function createSdfBatcherPipe(pixi) {
13147
+ /** Pixi's batcher pipe, which routes the glyphs of distance-field text to the renderer's batcher. */
13148
+ class SdfRoutingBatcherPipe extends pixi.BatcherPipe {
13149
+ static extension = pixi.BatcherPipe.extension;
13150
+ /**
13151
+ * Names the element's batcher, then adds it as Pixi does.
13152
+ *
13153
+ * @param element - The element.
13154
+ * @param instructionSet - The instructions of its render group.
13155
+ */
13156
+ addToBatch(element, instructionSet) {
13157
+ routeElement(element);
13158
+ super.addToBatch(element, instructionSet);
13159
+ }
13160
+ /**
13161
+ * Draws a batch as Pixi does; the first batch of an SDF batcher's run first packs the distances again when the
13162
+ * resolution moved.
13163
+ *
13164
+ * @param batch - The batch.
13165
+ */
13166
+ execute(batch) {
13167
+ const batcher = batch.batcher;
13168
+ if (batch.action === "startBatch" && isSdfBatcher(batcher)) {
13169
+ const resolution = this.renderer.resolution;
13170
+ if (batcher.density !== resolution) batcher.packAt(resolution);
13171
+ }
13172
+ super.execute(batch);
13173
+ }
13174
+ }
13175
+ return SdfRoutingBatcherPipe;
12744
13176
  }
12745
13177
  /**
12746
- * The compile step of the renderer's GLSL SDF program: Pixi's `SdfShader` GL bits with the same
12747
- * fragment step replaced.
13178
+ * Names the batcher of one element: `SDF_BATCHER` for a glyph of a distance-field text, Pixi's default again for an
13179
+ * element that carried that name from an earlier owner, as it was otherwise.
12748
13180
  *
12749
- * @param pixi - The loaded Pixi module.
12750
- * @returns The WebGL program for a texture limit.
13181
+ * @param element - The element.
12751
13182
  */
12752
- function glProgramCache(pixi) {
12753
- const base = pixi.localUniformMSDFBitGl;
12754
- const textBit = {
12755
- ...base,
12756
- name: TEXT_BIT_NAME,
12757
- fragment: {
12758
- ...base.fragment,
12759
- main: SINGLE_ALPHA_STEP_GL
12760
- }
12761
- };
12762
- return cachedBy((maxTextures) => pixi.compileHighShaderGlProgram({
12763
- name: PROGRAM_NAME,
12764
- bits: [
12765
- pixi.colorBitGl,
12766
- pixi.generateTextureBatchBitGl(maxTextures),
12767
- textBit,
12768
- pixi.mSDFBitGl,
12769
- pixi.roundPixelsBitGl
12770
- ]
12771
- }));
13183
+ function routeElement(element) {
13184
+ const glyphs = element.renderable?.[SDF];
13185
+ if (glyphs !== void 0 && glyphs.perScale > 0) element.batcherName = SDF_BATCHER;
13186
+ else if (element.batcherName === "moku-sdf") element.batcherName = DEFAULT_BATCHER;
12772
13187
  }
13188
+ //#endregion
13189
+ //#region src/plugins/renderer/host/sdf-text.ts
13190
+ /** The suffix of a bitmap font's key in Pixi's cache, the one `_updateDistanceField` reads. */
13191
+ const CACHE_SUFFIX$1 = "-bitmap";
12773
13192
  /**
12774
- * Builds the renderer's bitmap text pipe from the module object. It is made at call time, because
12775
- * Pixi's pipe arrives with the lazily loaded module. It keeps the base's `static extension` object
12776
- * as it is, so the registry files it under the same types and name. Every text gets a shader of its
12777
- * own once, at its first layout, over the one shared program of its backend, and keeps it when it is laid out again
12778
- * (Pixi asks for one at every layout); a WebGL shader also gets
12779
- * Pixi's batch sampler slots, as Pixi's own `SdfShader` does. Its `uDistance` answers Pixi's CSS
12780
- * distance at the text's world scale times the renderer's resolution: the pipe names the text
12781
- * while Pixi's `addRenderable` and `updateRenderable`, the two that write the distance, run.
13193
+ * Measures the edge of a text's glyphs again when its font family or size changed since: Pixi's distance at scale 1
13194
+ * from the font in Pixi's cache, 0 for a font without a distance field or not in the cache.
13195
+ *
13196
+ * @param pixi - The loaded Pixi module.
13197
+ * @param glyphs - The text's glyph record.
13198
+ * @param style - The text's style.
13199
+ */
13200
+ function measureGlyphs(pixi, glyphs, style) {
13201
+ const family = style.fontFamily;
13202
+ const size = style.fontSize;
13203
+ if (family === glyphs.family && size === glyphs.size) return;
13204
+ const key = `${String(family)}${CACHE_SUFFIX$1}`;
13205
+ const font = pixi.Cache.has(key) ? pixi.Cache.get(key) : void 0;
13206
+ const field = font?.distanceField;
13207
+ glyphs.family = family;
13208
+ glyphs.size = size;
13209
+ glyphs.perScale = font === void 0 || field === void 0 || field.type === "none" ? 0 : field.range * (1 / (font.baseRenderedFontSize / size));
13210
+ }
13211
+ /**
13212
+ * Builds the renderer's bitmap text pipe from the module object, at call time, because Pixi's pipe arrives with the
13213
+ * lazily loaded module. It keeps the base's `static extension` object, so the registry files it under the same types
13214
+ * and name. Every text's proxy graphics gets a batchable context and its glyph record when Pixi makes it; the record is
13215
+ * brought up to date before Pixi adds or updates the text, so the batcher packs the edge of the text's font.
12782
13216
  *
12783
13217
  * @param pixi - The loaded Pixi module.
12784
13218
  * @returns The pipe class.
12785
13219
  */
12786
13220
  function createSdfTextPipe(pixi) {
12787
- const gpuProgramFor = gpuProgramCache(pixi);
12788
- const glProgramFor = glProgramCache(pixi);
12789
- /** Pixi's bitmap text pipe, with an SDF shader that applies the alpha once. */
12790
- class SingleAlphaBitmapTextPipe extends pixi.BitmapTextPipe {
13221
+ /** Pixi's bitmap text pipe, with distance-field text drawn by the renderer's batcher. */
13222
+ class SdfTextPipe extends pixi.BitmapTextPipe {
12791
13223
  static extension = pixi.BitmapTextPipe.extension;
12792
- /** The text whose distance Pixi writes now, read by the uniforms of every text of this pipe. */
12793
- writing = { text: void 0 };
12794
13224
  /**
12795
- * The text Pixi lays out now: `getSdfShader` hands its own shader back, read at the call, so the text keeps it and no
12796
- * shader is made. Pixi asks `getSdfShader()` at every layout of a text (twice for a new one) and keeps the answer
12797
- * only for a context without a shader: a new shader each time was garbage (about 22 KB a text update), and on WebGL
12798
- * it listened to the shared batch-sampler group for good, a leak.
13225
+ * Makes a text's proxy graphics as Pixi does, batchable at any length, with the edge of its glyphs.
13226
+ *
13227
+ * @param text - The text.
13228
+ * @returns The proxy.
12799
13229
  */
12800
- laying;
13230
+ initGpuText(text) {
13231
+ const proxy = super.initGpuText(text);
13232
+ const glyphs = {
13233
+ perScale: NaN,
13234
+ family: void 0,
13235
+ size: NaN
13236
+ };
13237
+ proxy.context.batchMode = "batch";
13238
+ proxy[SDF] = glyphs;
13239
+ measureGlyphs(pixi, glyphs, text._style);
13240
+ return proxy;
13241
+ }
12801
13242
  /**
12802
- * Adds a text to the instructions as Pixi does, naming it while Pixi writes its distance and lays it out.
13243
+ * Adds a text to the instructions as Pixi does, its glyph edge measured first.
12803
13244
  *
12804
13245
  * @param text - The text.
12805
13246
  * @param instructionSet - The instructions of its render group.
12806
13247
  */
12807
13248
  addRenderable(text, instructionSet) {
12808
- this.writing.text = text;
12809
- this.laying = text;
13249
+ this.measure(text);
12810
13250
  super.addRenderable(text, instructionSet);
12811
- this.laying = void 0;
12812
- this.writing.text = void 0;
12813
13251
  }
12814
13252
  /**
12815
- * Updates a text as Pixi does, naming it while Pixi writes its distance.
13253
+ * Updates a text as Pixi does, its glyph edge measured first.
12816
13254
  *
12817
13255
  * @param text - The text.
12818
13256
  */
12819
13257
  updateRenderable(text) {
12820
- this.writing.text = text;
13258
+ this.measure(text);
12821
13259
  super.updateRenderable(text);
12822
- this.writing.text = void 0;
12823
13260
  }
12824
13261
  /**
12825
- * The shader of one distance-field text, asked for each time Pixi lays the text out: the text's own when it has one,
12826
- * else a new one.
13262
+ * No shader: Pixi then leaves the text's context without one, and batchable.
12827
13263
  *
12828
- * @returns The text's shader, or a new shader with its own local uniforms on the program of the backend.
13264
+ * @returns `null`.
12829
13265
  */
12830
13266
  getSdfShader() {
12831
- const own = this.laying === void 0 ? void 0 : shaderOf(this.laying, this._renderer.uid);
12832
- if (own !== void 0) return own;
12833
- const maxTextures = this._renderer.limits.maxBatchableTextures;
12834
- const density = this._renderer;
12835
- if (this._renderer.name === "webgpu") return new pixi.Shader({
12836
- gpuProgram: gpuProgramFor(maxTextures),
12837
- resources: { localUniforms: createLocalUniforms(pixi, density, this.writing) }
12838
- });
12839
- return new pixi.Shader({
12840
- glProgram: glProgramFor(maxTextures),
12841
- resources: {
12842
- localUniforms: createLocalUniforms(pixi, density, this.writing),
12843
- batchSamplers: pixi.getBatchSamplersUniformGroup(maxTextures)
12844
- }
12845
- });
13267
+ return null;
13268
+ }
13269
+ /**
13270
+ * Measures the glyph edge of a text Pixi made a proxy for already; a new text's proxy measures its own.
13271
+ *
13272
+ * @param text - The text.
13273
+ */
13274
+ measure(text) {
13275
+ const proxy = text._gpuData[this._renderer.uid];
13276
+ if (proxy !== void 0) measureGlyphs(pixi, proxy[SDF], text._style);
12846
13277
  }
12847
13278
  }
12848
- return SingleAlphaBitmapTextPipe;
13279
+ return SdfTextPipe;
12849
13280
  }
12850
13281
  /**
12851
- * Swaps Pixi's bitmap text pipe for the renderer's in the module's extension registry: `remove`
12852
- * frees the name on both backends, `add` puts the subclass in under it. Every application made
12853
- * afterwards draws its distance-field text with it. The registry is global to the module object,
12854
- * so the remover matters: it swaps Pixi's own pipe back.
13282
+ * Installs the three pieces of batched distance-field text in the module's extension registry: the batcher under its
13283
+ * own name, and the bitmap text pipe and the batch pipe under Pixi's, on both backends. Every application made
13284
+ * afterwards draws distance-field text with them. Every class is built before the registry is touched, so a module
13285
+ * without the pieces throws with the registry as it was. The registry is global to the module object, so the remover
13286
+ * matters: it swaps Pixi's own pipes back and leaves the batcher, which a pooled element may still name. A counting batch pipe of the monitor, which extends the routing pipe, is
13287
+ * installed after this one, in its place.
12855
13288
  *
12856
13289
  * @param pixi - The loaded Pixi module.
12857
13290
  * @returns The remover.
12858
- * @throws {TypeError} When the module has no `BitmapTextPipe` to subclass.
12859
- */
12860
- function installSdfTextPipe(pixi) {
12861
- const pipe = createSdfTextPipe(pixi);
12862
- pixi.extensions.remove(pixi.BitmapTextPipe);
12863
- pixi.extensions.add(pipe);
13291
+ * @throws {TypeError} When the module has no `Batcher`, `BitmapTextPipe` or `BatcherPipe` to subclass.
13292
+ */
13293
+ function installSdfText(pixi) {
13294
+ const batcher = createSdfBatcher(pixi);
13295
+ const textPipe = createSdfTextPipe(pixi);
13296
+ const batchPipe = createSdfBatcherPipe(pixi);
13297
+ pixi.extensions.remove(pixi.BitmapTextPipe, pixi.BatcherPipe);
13298
+ pixi.extensions.add(batcher, textPipe, batchPipe);
12864
13299
  return () => {
12865
- pixi.extensions.remove(pipe);
12866
- pixi.extensions.add(pixi.BitmapTextPipe);
13300
+ pixi.extensions.remove(textPipe, batchPipe);
13301
+ pixi.extensions.add(pixi.BitmapTextPipe, pixi.BatcherPipe);
12867
13302
  };
12868
13303
  }
12869
13304
  //#endregion
@@ -13000,26 +13435,27 @@ function installCounting$1(ctx, pixi, deps) {
13000
13435
  } else installPasses(ctx, pixi, deps.installPasses);
13001
13436
  }
13002
13437
  /**
13003
- * Swaps Pixi's bitmap text pipe for the renderer's, whose SDF shader applies the alpha of
13004
- * distance-field text once. Like the draw counter it runs before `new Application()`, because Pixi
13005
- * builds its pipes inside `init`, and once per host state: a restore reuses the installed pipe.
13006
- * Every build installs it. A Pixi without the pieces draws on with its own pipe, with a warning.
13438
+ * Installs the renderer's batched distance-field text: its batcher, its bitmap text pipe and its batch pipe, which draw
13439
+ * consecutive labels in one draw with the alpha applied once. It runs before `new Application()`, because Pixi builds
13440
+ * its pipes inside `init`, and before the draw counter, whose batch pipe extends the routing one and takes its place;
13441
+ * once per host state: a restore reuses the installed pieces. Every build installs it. A Pixi without the pieces draws
13442
+ * on with its own pipes, with a warning.
13007
13443
  *
13008
13444
  * @param ctx - Domain context of the renderer plugin.
13009
13445
  * @param pixi - The module the application is built from.
13010
13446
  */
13011
- function installSdfText(ctx, pixi) {
13447
+ function installTextBatch(ctx, pixi) {
13012
13448
  const state = ctx.state.host;
13013
13449
  if (state.uninstallSdfText !== void 0) return;
13014
13450
  try {
13015
- state.uninstallSdfText = installSdfTextPipe(pixi);
13451
+ state.uninstallSdfText = installSdfText(pixi);
13016
13452
  } catch (error) {
13017
13453
  ctx.log.warn("renderer: distance-field text applies its alpha twice", { error });
13018
13454
  }
13019
13455
  }
13020
13456
  /**
13021
13457
  * Swaps Pixi's render-group pipe for the renderer's, which binds a filter's input again after a group cached as a
13022
- * texture was refreshed inside the filter (F10). Like the bitmap text pipe it runs before `new Application()`, once per
13458
+ * texture was refreshed inside the filter (F10). Like the batched text it runs before `new Application()`, once per
13023
13459
  * host state, in every build. A Pixi without the pieces draws on with its own pipe, with a warning.
13024
13460
  *
13025
13461
  * @param ctx - Domain context of the renderer plugin.
@@ -13053,8 +13489,8 @@ async function createApplication(ctx, mount, deps) {
13053
13489
  const pixi = loaded ?? await ctx.config.loadPixi();
13054
13490
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && loaded === void 0) startupMark("moku:pixi-end");
13055
13491
  state.pixi = pixi;
13492
+ installTextBatch(ctx, pixi);
13056
13493
  installCounting$1(ctx, pixi, deps);
13057
- installSdfText(ctx, pixi);
13058
13494
  installRenderGroup(ctx, pixi);
13059
13495
  state.resolution.dpr = devicePixels();
13060
13496
  state.resolution.cap = ctx.config.maxResolution;
@@ -13099,7 +13535,7 @@ function destroyApplication(state) {
13099
13535
  /**
13100
13536
  * Undoes everything `init` created. Works on the state alone, because `onStop` has no context.
13101
13537
  * `ready` drops first, so a device that goes away during the teardown is ignored. The draw
13102
- * counter, the bitmap text pipe and the render-group pipe swap Pixi's own classes back, so the next application on
13538
+ * counter, the batched text and the render-group pipe swap Pixi's own classes back, so the next application on
13103
13539
  * the page starts from them. The pool trim starts over and its listeners go; Pixi's pool itself is left.
13104
13540
  *
13105
13541
  * @param state - The host branch of the plugin state.
@@ -14018,8 +14454,9 @@ function endLoan(inputs) {
14018
14454
  * @returns The slot and its inputs, or `undefined`.
14019
14455
  */
14020
14456
  function firstInputs(slots) {
14021
- for (const slot of slots) {
14022
- if (!slot.filter.enabled) continue;
14457
+ for (let index = 0; index < slots.length; index += 1) {
14458
+ const slot = slots[index];
14459
+ if (slot === void 0 || !slot.filter.enabled) continue;
14023
14460
  return slot.inputs === void 0 ? void 0 : {
14024
14461
  slot,
14025
14462
  inputs: slot.inputs
@@ -14256,7 +14693,11 @@ function renderStage3(kctx, pixi, matrices, record, now) {
14256
14693
  key[KEY.s3Tx] = now.tx;
14257
14694
  key[KEY.s3Ty] = now.ty;
14258
14695
  let passes = 1;
14259
- for (const slot of record.slots) if (slot.filter.enabled) passes += slot.passes;
14696
+ const slots = record.slots;
14697
+ for (let index = 0; index < slots.length; index += 1) {
14698
+ const slot = slots[index];
14699
+ if (slot?.filter.enabled === true) passes += slot.passes;
14700
+ }
14260
14701
  kctx.state.flushPasses += passes;
14261
14702
  return true;
14262
14703
  }
@@ -14662,7 +15103,8 @@ function createKeptState(config) {
14662
15103
  warnedBudget: false,
14663
15104
  verifySkipped: 0,
14664
15105
  bypassed: false,
14665
- flushPasses: 0
15106
+ flushPasses: 0,
15107
+ calm: false
14666
15108
  };
14667
15109
  }
14668
15110
  /**
@@ -14798,11 +15240,12 @@ function readStack(slots, resolution, into) {
14798
15240
  let enabled = 0;
14799
15241
  let rs = Number.POSITIVE_INFINITY;
14800
15242
  let padding = 0;
14801
- for (const slot of slots) {
14802
- if (!slot.filter.enabled) continue;
15243
+ for (let index = 0; index < slots.length; index += 1) {
15244
+ const filter = slots[index]?.filter;
15245
+ if (filter === void 0 || !filter.enabled) continue;
14803
15246
  enabled += 1;
14804
- rs = Math.min(rs, resolutionOf(slot.filter, resolution));
14805
- padding += slot.filter.padding;
15247
+ rs = Math.min(rs, resolutionOf(filter, resolution));
15248
+ padding += filter.padding;
14806
15249
  }
14807
15250
  into.enabled = enabled;
14808
15251
  into.rs = enabled === 0 ? resolution : rs;
@@ -14834,8 +15277,9 @@ function holdsFilters(node) {
14834
15277
  * @returns True when they do.
14835
15278
  */
14836
15279
  function plainFilters(slots) {
14837
- for (const slot of slots) {
14838
- const { filter } = slot;
15280
+ for (let index = 0; index < slots.length; index += 1) {
15281
+ const filter = slots[index]?.filter;
15282
+ if (filter === void 0) continue;
14839
15283
  if (filter.enabled && (filter.blendRequired || !plainBlend(filter.blendMode))) return false;
14840
15284
  }
14841
15285
  return true;
@@ -15126,16 +15570,18 @@ function neededBytes(record, now, box) {
15126
15570
  return bytes;
15127
15571
  }
15128
15572
  /**
15129
- * Hands the filters of a live record from its top to its stand-in, before stage 1 renders the top without them.
15573
+ * Hands the filters of a live record from its top to its stand-in, before stage 1 renders the top without them: the
15574
+ * enabled ones, as `sync` hangs them (E3).
15130
15575
  *
15131
15576
  * @param kctx - The kept context.
15132
15577
  * @param pixi - The module that draws.
15133
15578
  * @param record - The record.
15579
+ * @param view - Its view.
15134
15580
  */
15135
- function handFilters(kctx, pixi, record) {
15581
+ function handFilters(kctx, pixi, record, view) {
15136
15582
  const standIn = standInOf(pixi, record);
15137
15583
  kctx.sync.clearFilters(record.top);
15138
- standIn.filters = filtersOf$1(kctx.ctx.state.sync, record.entity).map((slot) => slot.filter);
15584
+ kctx.sync.applyFilters(view, filtersOf$1(kctx.ctx.state.sync, record.entity), noMark$1, standIn);
15139
15585
  }
15140
15586
  /**
15141
15587
  * Writes the take keys of a record: the linear part, the stack resolution and the translation of this frame, and the
@@ -15240,7 +15686,7 @@ function isEmptyBounds(bounds) {
15240
15686
  */
15241
15687
  function renderStages(kctx, pixi, matrices, record, view, now) {
15242
15688
  const live = record.mode === "live";
15243
- if (live) handFilters(kctx, pixi, record);
15689
+ if (live) handFilters(kctx, pixi, record, view);
15244
15690
  const first = firstInputs(record.slots);
15245
15691
  let ok = renderStage1(kctx, pixi, matrices, record);
15246
15692
  if (ok && first !== void 0) renderStage2(kctx, pixi, matrices, record, {
@@ -15538,6 +15984,23 @@ function drawingChanged(kctx, record, before) {
15538
15984
  return before.mode !== record.mode || before.passes !== kctx.state.flushPasses || !Object.is(before.qtx, record.key[KEY.qtx]) || !Object.is(before.qty, record.key[KEY.qty]) || before.shown !== quadShown(record);
15539
15985
  }
15540
15986
  /**
15987
+ * The frame of a kept record in a calm frame (no mark, no whole-frame reason) that owes nothing: no dirty flag, no
15988
+ * settle retake, no churn, not bypassed. Its step would find every key as it stands and its quad where it is, so it is
15989
+ * counted as drawn from its picture and left alone (E5).
15990
+ *
15991
+ * @param kctx - The kept context.
15992
+ * @param record - The record.
15993
+ * @returns True when the record was drawn as it stands; false when it is to be stepped.
15994
+ */
15995
+ function stepCalm(kctx, record) {
15996
+ const { state } = kctx;
15997
+ if (!state.calm || state.bypassed || record.mode !== "kept" || record.dirtyS1 || record.dirtyS2 || record.dirtyS3 || record.settle || record.churn !== 0) return false;
15998
+ record.lastDrawn = state.frame;
15999
+ state.drawnPictures += 1;
16000
+ record.quiet += 1;
16001
+ return true;
16002
+ }
16003
+ /**
15541
16004
  * One drawn frame of a record, inside the flush: eligibility, the keys of this frame, the policy of its life, the quad
15542
16005
  * of a kept one; the dirty flags are cleared.
15543
16006
  *
@@ -15639,7 +16102,7 @@ function bypassRecord(kctx, record, view, live) {
15639
16102
  return;
15640
16103
  }
15641
16104
  kctx.sync.clearFilters(record.top);
15642
- if (record.standIn !== void 0) record.standIn.filters = filters.map((slot) => slot.filter);
16105
+ if (record.standIn !== void 0) kctx.sync.applyFilters(view, filters, noMark$1, record.standIn);
15643
16106
  kctx.sync.swapHidden(kctx.hidden, view, "kept", true);
15644
16107
  if (record.quad !== void 0) record.quad.visible = record.top.visible && !kctx.sync.hiddenBy(view, "alpha") && !kctx.sync.hiddenBy(view, "cover");
15645
16108
  }
@@ -15849,7 +16312,7 @@ function stepRecords(kctx, pixi, matrices) {
15849
16312
  continue;
15850
16313
  }
15851
16314
  const view = kctx.ctx.state.sync.views.get(record.entity);
15852
- if (view !== void 0) stepRecord(kctx, pixi, matrices, record, view);
16315
+ if (view !== void 0 && !stepCalm(kctx, record)) stepRecord(kctx, pixi, matrices, record, view);
15853
16316
  }
15854
16317
  }
15855
16318
  /**
@@ -15902,9 +16365,10 @@ function createKeptApi(ctx, deps) {
15902
16365
  const bypass = (on) => {
15903
16366
  if (state.bypassed === on) return;
15904
16367
  state.bypassed = on;
15905
- for (const record of state.records) {
15906
- const view = viewOf(record);
15907
- if (record.mode === "kept" && view !== void 0) bypassRecord(kctx, record, view, on);
16368
+ for (let index = 0; index < state.records.length; index += 1) {
16369
+ const record = state.records[index];
16370
+ const view = record === void 0 ? void 0 : viewOf(record);
16371
+ if (record?.mode === "kept" && view !== void 0) bypassRecord(kctx, record, view, on);
15908
16372
  }
15909
16373
  };
15910
16374
  /**
@@ -15913,7 +16377,9 @@ function createKeptApi(ctx, deps) {
15913
16377
  */
15914
16378
  const sortRecords = () => {
15915
16379
  let changed = state.unsorted;
15916
- for (const record of state.records) {
16380
+ for (let index = 0; index < state.records.length; index += 1) {
16381
+ const record = state.records[index];
16382
+ if (record === void 0) continue;
15917
16383
  const depth = depthOf(record.top);
15918
16384
  if (depth !== record.depth) {
15919
16385
  record.depth = depth;
@@ -15982,6 +16448,7 @@ function createKeptApi(ctx, deps) {
15982
16448
  if (wholes.has("debug") || wholes.has("restore")) for (const record of state.records) record.dirtyS1 = true;
15983
16449
  const marked = deps.demand.marked();
15984
16450
  const loose = deps.demand.loose();
16451
+ state.calm = wholes.size === 0 && marked.size === 0 && loose.size === 0;
15985
16452
  for (let index = 0; index < marked.size; index += 1) collectOne(marked.at(index));
15986
16453
  for (let index = 0; index < loose.size; index += 1) collectOne(loose.at(index));
15987
16454
  },
@@ -15995,6 +16462,7 @@ function createKeptApi(ctx, deps) {
15995
16462
  if (state.on) {
15996
16463
  sortRecords();
15997
16464
  stepRecords(kctx, pixi, matrices);
16465
+ state.calm = false;
15998
16466
  } else for (const record of state.records) if (record.mode === "kept") live(record);
15999
16467
  flushRanges(kctx);
16000
16468
  },
@@ -16379,7 +16847,7 @@ const ALL_SECTIONS = Object.freeze({
16379
16847
  * @param next - The switches asked for; `undefined` for none.
16380
16848
  * @returns The sections, frozen; `base` itself when nothing differs.
16381
16849
  */
16382
- function switched(base, next) {
16850
+ function switched$1(base, next) {
16383
16851
  if (next === void 0) return base;
16384
16852
  const sections = { ...base };
16385
16853
  let changed = false;
@@ -18211,7 +18679,7 @@ function createSinks(draws) {
18211
18679
  */
18212
18680
  function createPerfState(config, sinks) {
18213
18681
  const events = createEventRing();
18214
- const sections = switched(ALL_SECTIONS, config.sections);
18682
+ const sections = switched$1(ALL_SECTIONS, config.sections);
18215
18683
  sinks.sections = bitsOf(sections);
18216
18684
  sinks.sectionsNext = sinks.sections;
18217
18685
  return {
@@ -20959,7 +21427,7 @@ function stopPerf(perf, sinks) {
20959
21427
  */
20960
21428
  function switchSections(pc, next) {
20961
21429
  const { perf, sinks } = pc;
20962
- const sections = switched(perf.sections, next);
21430
+ const sections = switched$1(perf.sections, next);
20963
21431
  if (sections === perf.sections) return sections;
20964
21432
  perf.sections = sections;
20965
21433
  sinks.sectionsNext = bitsOf(sections);
@@ -20980,7 +21448,7 @@ const READ_ONLY_SECTIONS = "[game] monitor.sections switches the sections only o
20980
21448
  */
20981
21449
  function refuseSections(pc, next) {
20982
21450
  const { sections } = pc.perf;
20983
- if (switched(sections, next) === sections) return sections;
21451
+ if (switched$1(sections, next) === sections) return sections;
20984
21452
  throw Object.assign(/* @__PURE__ */ new Error(READ_ONLY_SECTIONS), { reason: "read-only" });
20985
21453
  }
20986
21454
  //#endregion
@@ -21545,6 +22013,23 @@ function hiddenBy(view, reason) {
21545
22013
  return (view[maskOf(view, reason)] & BITS[reason]) !== 0;
21546
22014
  }
21547
22015
  /**
22016
+ * Whether a node on the chain of a view is drawn at alpha 0, read from the engine's records (E4): Pixi's `alpha` getter
22017
+ * hands its double back boxed at every read of a walk of `shown`. The top of a built-in visual, its wrapper or its
22018
+ * object, draws at alpha 0 exactly when the `"alpha"` reason hides it, and so does an adapter's own object. A node of no
22019
+ * view on a chain is the engine's (a clip container, a layer, the root), never faded. A game's object and the wrapper
22020
+ * of an adapter carry an alpha the engine does not hold: Pixi's is read.
22021
+ *
22022
+ * @param state - The sync state.
22023
+ * @param node - A node on the chain of a view, below the stage.
22024
+ * @returns True at alpha 0.
22025
+ */
22026
+ function transparentNode(state, node) {
22027
+ const view = state.viewOfTop.get(node);
22028
+ if (view === void 0) return false;
22029
+ if (view.kind === "Display" || view.kind === "adapter" && node === view.wrapper) return node.alpha <= 0;
22030
+ return hiddenBy(view, "alpha");
22031
+ }
22032
+ /**
21548
22033
  * Tells whether a node is hidden, and by these reasons alone: spec 23's eligibility of a picture whose ancestor is in
21549
22034
  * an overlay's range (`"freeze"`) or is itself a kept top drawn through its quad (`"kept"`).
21550
22035
  *
@@ -21678,6 +22163,8 @@ function clearAdapters(state) {
21678
22163
  }
21679
22164
  //#endregion
21680
22165
  //#region src/plugins/renderer/sync/filters.ts
22166
+ /** What a display object without filters holds: one shared list, plain, so a still frame's compare makes none. */
22167
+ const NO_FILTERS = [];
21681
22168
  /**
21682
22169
  * The filters a display object holds now. Pixi answers `undefined` before the first write and
21683
22170
  * `null` after a clear, which its getter type does not say.
@@ -21686,10 +22173,11 @@ function clearAdapters(state) {
21686
22173
  * @returns The filters, empty when there are none.
21687
22174
  */
21688
22175
  function heldFilters(target) {
21689
- return target.filters ?? [];
22176
+ return target.filters ?? NO_FILTERS;
21690
22177
  }
21691
22178
  /**
21692
- * Tells whether a display object holds exactly these filter instances, in this order.
22179
+ * Tells whether a display object holds exactly the enabled filters of these slots, in their order (E3): Pixi is handed
22180
+ * no disabled filter, which draws nothing but costs a filter push and pop, two batch breaks, every frame.
21693
22181
  *
21694
22182
  * @param target - The display object.
21695
22183
  * @param slots - The slots to compare with.
@@ -21697,7 +22185,37 @@ function heldFilters(target) {
21697
22185
  */
21698
22186
  function holds(target, slots) {
21699
22187
  const held = heldFilters(target);
21700
- return held.length === slots.length && slots.every((slot, index) => held[index] === slot.filter);
22188
+ let at = 0;
22189
+ for (let index = 0; index < slots.length; index += 1) {
22190
+ const filter = slots[index]?.filter;
22191
+ if (filter === void 0 || !filter.enabled) continue;
22192
+ if (held[at] !== filter) return false;
22193
+ at += 1;
22194
+ }
22195
+ return at === held.length;
22196
+ }
22197
+ /**
22198
+ * The enabled filters of slots, in their order: what Pixi is handed.
22199
+ *
22200
+ * @param slots - The slots.
22201
+ * @returns A new list, or `undefined` when no filter is enabled.
22202
+ */
22203
+ function enabledFilters(slots) {
22204
+ let enabled;
22205
+ for (const slot of slots) if (slot.filter.enabled) {
22206
+ enabled ??= [];
22207
+ enabled.push(slot.filter);
22208
+ }
22209
+ return enabled;
22210
+ }
22211
+ /**
22212
+ * Writes the enabled filters of slots onto a display object, or clears them when none is enabled.
22213
+ *
22214
+ * @param target - The display object.
22215
+ * @param slots - The slots.
22216
+ */
22217
+ function writeFilters(target, slots) {
22218
+ target.filters = enabledFilters(slots) ?? null;
21701
22219
  }
21702
22220
  /**
21703
22221
  * Takes every filter off a display object. Nothing is written when it holds none: Pixi copies the
@@ -21711,8 +22229,10 @@ function clearFilters(target) {
21711
22229
  /**
21712
22230
  * Hangs the filters on a view: on its wrapper when it has one, so they cover the entity's whole
21713
22231
  * subtree, otherwise on the object; on the stand-in of its kept picture while it has one
21714
- * (`kept.filterTarget`, spec 23 P9.1). Written only when the instances differ from what the target
21715
- * holds, and `before` runs once just before that write: the caller marks the entity there.
22232
+ * (`kept.filterTarget`, spec 23 P9.1). Only the enabled ones: a disabled filter draws nothing, and Pixi would push and
22233
+ * pop it around the view every frame (E3); a view whose filters are all disabled holds none. Written only when they
22234
+ * differ from what the target holds, and `before` runs once just before that write: the caller marks the entity there.
22235
+ * A filter switched on or off later is written by `applySwitched` in the frame it was switched.
21716
22236
  *
21717
22237
  * @param view - The view.
21718
22238
  * @param slots - The filters in draw order; empty clears them.
@@ -21722,7 +22242,7 @@ function clearFilters(target) {
21722
22242
  function applyFilters(view, slots, before, target = view.wrapper ?? view.object) {
21723
22243
  if (holds(target, slots)) return;
21724
22244
  before();
21725
- target.filters = slots.length === 0 ? null : slots.map((slot) => slot.filter);
22245
+ writeFilters(target, slots);
21726
22246
  }
21727
22247
  /**
21728
22248
  * The filters kept for an entity.
@@ -21815,6 +22335,45 @@ function moveFilters(state, entity, view, before, kept) {
21815
22335
  clearFilters(view.object);
21816
22336
  applyFilters(view, slots, before);
21817
22337
  }
22338
+ /** What the walk of the switched filters reads, set before the walk: one at a time, no closure per frame. */
22339
+ const switched = {
22340
+ sync: void 0,
22341
+ kept: void 0,
22342
+ wrote: false
22343
+ };
22344
+ /**
22345
+ * Writes the enabled filters of one view again when one of them was switched on or off since they were handed to Pixi.
22346
+ *
22347
+ * @param slots - The filters of the view.
22348
+ * @param entity - The entity.
22349
+ */
22350
+ function applySwitchedOne(slots, entity) {
22351
+ const { sync, kept } = switched;
22352
+ const view = sync?.views.get(entity);
22353
+ if (view === void 0 || kept === void 0) return;
22354
+ const target = kept.filterTarget(view);
22355
+ if (holds(target, slots)) return;
22356
+ writeFilters(target, slots);
22357
+ switched.wrote = true;
22358
+ }
22359
+ /**
22360
+ * Hands Pixi the filters of every view whose filters were switched on or off since they were written (E3), before the
22361
+ * frame is drawn, so a switch is drawn in the frame it was made. No mark: the writer of a switch tells the renderer
22362
+ * with `invalidate` first. A write moves `passesVersion` on: the paddings the walks of `shown` read changed (E4).
22363
+ *
22364
+ * @param state - The sync branch of the plugin state.
22365
+ * @param kept - The kept module: where each view's filters hang.
22366
+ */
22367
+ function applySwitched(state, kept) {
22368
+ if (state.filters.size === 0) return;
22369
+ switched.sync = state;
22370
+ switched.kept = kept;
22371
+ switched.wrote = false;
22372
+ state.filters.forEach(applySwitchedOne);
22373
+ if (switched.wrote) state.passesVersion += 1;
22374
+ switched.sync = void 0;
22375
+ switched.kept = void 0;
22376
+ }
21818
22377
  /**
21819
22378
  * Forgets the filters and the filter area of an entity whose view left, only when the entity itself is gone. A live
21820
22379
  * entity keeps them for its next view, in this frame, as a `ui` element that swaps its shape for a
@@ -25057,6 +25616,7 @@ function applyHud(sctx, entity, view, factor) {
25057
25616
  if (view.hud === factor) return;
25058
25617
  markView(sctx, entity);
25059
25618
  view.hud = factor;
25619
+ sctx.ctx.state.sync.passesVersion += 1;
25060
25620
  if (ownsAlpha(view)) {
25061
25621
  applyAlpha(sctx, entity, view, componentAlpha(sctx, entity, view));
25062
25622
  return;
@@ -25812,6 +26372,7 @@ function dropView(sctx, entity) {
25812
26372
  state.views.delete(entity);
25813
26373
  if (state.filters.has(entity) || state.areas.has(entity)) state.viewlessEntries = true;
25814
26374
  state.orphans.delete(entity);
26375
+ state.held.delete(entity);
25815
26376
  state.entityOf.delete(view.object);
25816
26377
  state.viewOfTop.delete(view.wrapper ?? view.object);
25817
26378
  untrackKey(state, entity, view.textureKey);
@@ -26034,9 +26595,11 @@ function renderFrame(ctx, modules) {
26034
26595
  if (modules.viewport.applyPending()) {
26035
26596
  const root = modules.sync.root();
26036
26597
  modules.monitor.changed("resize");
26598
+ ctx.state.sync.passesVersion += 1;
26037
26599
  if (root !== void 0) modules.viewport.apply(root);
26038
26600
  modules.demand.whole("resize");
26039
26601
  }
26602
+ modules.sync.stepFilters();
26040
26603
  modules.sync.stepOverlays();
26041
26604
  modules.sync.drainOverlayEvents(modules.monitor);
26042
26605
  modules.kept.collect();
@@ -26232,19 +26795,22 @@ function countUpload(sinks, source) {
26232
26795
  /**
26233
26796
  * Builds the seven counting subclasses from the module object. They are made at call time,
26234
26797
  * because the base classes arrive with the lazily loaded module. Each keeps the base's `static
26235
- * extension` object as it is, so the registry files it under the same type and name.
26798
+ * extension` object as it is, so the registry files it under the same type and name. The batch pipe extends the one
26799
+ * it is given: the routing pipe of the renderer's batched text, which the plugin root injects, so the counting pipe
26800
+ * that takes its place routes the same.
26236
26801
  *
26237
26802
  * @param pixi - The loaded Pixi module.
26238
26803
  * @param sinks - What every instance counts into.
26804
+ * @param batcherPipe - The batch pipe the counting one extends; Pixi's own when none is given.
26239
26805
  * @returns The seven counting classes (BatcherPipe among them).
26240
26806
  */
26241
- function createCountingClasses(pixi, sinks) {
26807
+ function createCountingClasses(pixi, sinks, batcherPipe = pixi.BatcherPipe) {
26242
26808
  const counter = sinks.draws;
26243
26809
  /**
26244
26810
  * Counts every rebuild of a render group's instruction list: Pixi's render group system calls `buildStart` once per
26245
26811
  * rebuild and nowhere else. It inherits the static batcher registry, so batchers resolve as before.
26246
26812
  */
26247
- class CountingBatcherPipe extends pixi.BatcherPipe {
26813
+ class CountingBatcherPipe extends batcherPipe {
26248
26814
  static extension = pixi.BatcherPipe.extension;
26249
26815
  /**
26250
26816
  * Counts the rebuild, in the frame's counter only inside a frame, then begins it.
@@ -26451,10 +27017,11 @@ function createCountingClasses(pixi, sinks) {
26451
27017
  *
26452
27018
  * @param pixi - The loaded Pixi module.
26453
27019
  * @param sinks - The sinks of the renderer that installs it.
27020
+ * @param batcherPipe - The batch pipe the counting one extends: the routing pipe of the batched text.
26454
27021
  * @returns The remover.
26455
27022
  */
26456
- function installCounting(pixi, sinks) {
26457
- const counting = createCountingClasses(pixi, sinks);
27023
+ function installCounting(pixi, sinks, batcherPipe = pixi.BatcherPipe) {
27024
+ const counting = createCountingClasses(pixi, sinks, batcherPipe);
26458
27025
  const bases = [
26459
27026
  pixi.BatcherPipe,
26460
27027
  pixi.GpuBatchAdaptor,
@@ -27796,8 +28363,9 @@ function emptyEntitySet$1() {
27796
28363
  return /* @__PURE__ */ new Set();
27797
28364
  }
27798
28365
  /**
27799
- * Where the walk of `shown` stopped for an entity's view, walked once between two sync passes: an entity with several
27800
- * endless loops asks once a frame (spec 26 R1.7.3). Nothing is allocated once the map has grown.
28366
+ * Where the walk of `shown` stopped for an entity's view, walked again only when `passesVersion` moved since (E4): an
28367
+ * entity with several endless loops is walked once, and the loops of a still board are answered from their records
28368
+ * (spec 26 R1.7.3). A record is made at an entity's first ask and written in place after.
27801
28369
  *
27802
28370
  * @param sctx - Domain context of the sync module.
27803
28371
  * @param entity - The entity a loop is about to write.
@@ -27807,9 +28375,16 @@ function unseenOf(sctx, entity) {
27807
28375
  const state = sctx.ctx.state.sync;
27808
28376
  if (state.root === void 0 || !state.views.has(entity)) return "other";
27809
28377
  const known = state.held.get(entity);
27810
- if (known !== void 0) return known;
28378
+ if (known !== void 0 && known.at === state.passesVersion) return known.answer;
27811
28379
  const answer = sctx.deps.demand.unseenBy(entity);
27812
- state.held.set(entity, answer);
28380
+ if (known === void 0) state.held.set(entity, {
28381
+ answer,
28382
+ at: state.passesVersion
28383
+ });
28384
+ else {
28385
+ known.answer = answer;
28386
+ known.at = state.passesVersion;
28387
+ }
27813
28388
  return answer;
27814
28389
  }
27815
28390
  /**
@@ -28200,7 +28775,6 @@ function createSyncApi(ctx, deps) {
28200
28775
  * view. A still frame (nothing changed, nothing waits) returns before any work and makes nothing.
28201
28776
  */
28202
28777
  const pass = () => {
28203
- if (state.held.size > 0) state.held.clear();
28204
28778
  if (still()) return;
28205
28779
  state.passesVersion += 1;
28206
28780
  if (state.removed.size > 0) {
@@ -28347,6 +28921,7 @@ function createSyncApi(ctx, deps) {
28347
28921
  layerContainer: (name) => layerContainer(state, name),
28348
28922
  layerOf: (entity) => layerOf(sctx, entity),
28349
28923
  stepOverlays: () => stepOverlays(sctx),
28924
+ stepFilters: () => applySwitched(state, deps.kept),
28350
28925
  drainOverlayEvents: (monitor) => drainOverlayEvents(sctx, monitor),
28351
28926
  warmStep: () => warmStep(sctx),
28352
28927
  onWarm: (fn) => {
@@ -28814,14 +29389,14 @@ function createViewportApi(ctx, deps) {
28814
29389
  /**
28815
29390
  * The counting `host` installs before `new Application()`: the seven counting classes (BatcherPipe among them) of
28816
29391
  * `monitor` in a dev build and in a debug build, bound to the sinks in the monitor branch of the
28817
- * state, none in production. A production `define` folds the dev or agent guard, so the counting
28818
- * module leaves the bundle.
29392
+ * state, none in production; the counting batch pipe extends the routing pipe of the batched text. A production
29393
+ * `define` folds the dev or agent guard, so the counting module leaves the bundle.
28819
29394
  *
28820
29395
  * @param ctx - Domain context of the renderer plugin.
28821
29396
  * @returns The bound installer, or `undefined` without the dev flag and the agent flag.
28822
29397
  */
28823
29398
  function countingOf(ctx) {
28824
- return typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? (pixi) => installCounting(pixi, ctx.state.monitor.sinks) : void 0;
29399
+ return typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? (pixi) => installCounting(pixi, ctx.state.monitor.sinks, createSdfBatcherPipe(pixi)) : void 0;
28825
29400
  }
28826
29401
  /**
28827
29402
  * The writers of `sync` the kept cache draws through: the one owner of `renderable`, the one writer of a view's
@@ -28861,7 +29436,8 @@ function createModules$1(ctx) {
28861
29436
  const demand = createDemandApi(ctx, {
28862
29437
  host,
28863
29438
  views: ctx.state.sync.views,
28864
- hiddenBy
29439
+ hiddenBy,
29440
+ transparent: (node) => transparentNode(ctx.state.sync, node)
28865
29441
  });
28866
29442
  const built = {};
28867
29443
  const kept = createKeptApi(ctx, {
@@ -30994,7 +31570,8 @@ function same$1(stored, next) {
30994
31570
  * every frame all the same.
30995
31571
  *
30996
31572
  * A frame that moves every field writes the track's own patch, filled in place. A frame that
30997
- * leaves a field alone builds the patch of the fields it moves, so no other field is written.
31573
+ * leaves a field alone writes the track's patch of the fields it moves, kept for the next such
31574
+ * frame and built again only when the moved fields change, so no other field is written.
30998
31575
  *
30999
31576
  * @param actx - Domain context of the anim plugin.
31000
31577
  * @param track - The track to write, its normalised time in `clock[1]`.
@@ -31009,7 +31586,7 @@ function writeFields(actx, track, final) {
31009
31586
  }
31010
31587
  beginTrack(actx, track, stored);
31011
31588
  const owned = track.muted();
31012
- const { names, live, moved, values, seen, patch } = track;
31589
+ const { names, live, moved, values, seen } = track;
31013
31590
  let fields = 0;
31014
31591
  let count = 0;
31015
31592
  for (let slot = 0; slot < names.length; slot++) {
@@ -31029,27 +31606,57 @@ function writeFields(actx, track, final) {
31029
31606
  continue;
31030
31607
  }
31031
31608
  seen[slot] = next;
31032
- patch[field] = next;
31033
31609
  moved[slot] = 1;
31034
31610
  count++;
31035
31611
  }
31036
31612
  if (count === 0) return;
31037
- ecs.set(track.entity, track.component, count === fields ? patch : movedPatch(track));
31613
+ const written = count === fields ? track.patch : partialPatch(track);
31614
+ fillPatch(track, written);
31615
+ ecs.set(track.entity, track.component, written);
31038
31616
  }
31039
31617
  /**
31040
- * The patch of a frame that moved only some of the fields of a track: those fields, at the values
31041
- * the frame put into the track's patch.
31618
+ * Puts the values of the moved slots into a patch of the track: each moved field gets what the
31619
+ * frame left in `values` for it.
31042
31620
  *
31043
31621
  * @param track - The track that writes.
31044
- * @returns A new patch of the moved fields only.
31622
+ * @param written - Its full patch, or its patch of the moved fields.
31045
31623
  */
31046
- function movedPatch(track) {
31047
- const written = {};
31624
+ function fillPatch(track, written) {
31625
+ const { names, moved, values } = track;
31626
+ for (let slot = 0; slot < names.length; slot++) {
31627
+ const field = names[slot];
31628
+ if (moved[slot] === 1 && field !== void 0) written[field] = values[slot] ?? 0;
31629
+ }
31630
+ }
31631
+ /**
31632
+ * The patch of a frame that moved only some of the fields of a track: the one the track keeps,
31633
+ * when the frame moved the same fields as the frame it was built for, otherwise a new one.
31634
+ *
31635
+ * @param track - The track that writes, its `moved` flags set by this frame.
31636
+ * @returns The patch of exactly the moved fields; their values are filled after.
31637
+ */
31638
+ function partialPatch(track) {
31639
+ const { moved, partialMoved, partial } = track;
31640
+ if (partial === void 0) return keepPartial(track);
31641
+ for (let slot = 0; slot < moved.length; slot++) if (moved[slot] !== partialMoved[slot]) return keepPartial(track);
31642
+ return partial;
31643
+ }
31644
+ /**
31645
+ * Builds the patch of the fields the frame moved and keeps it on the track, with the `moved`
31646
+ * flags it was built for. Runs only when the moved fields change.
31647
+ *
31648
+ * @param track - The track that writes.
31649
+ * @returns The new patch, its values still to fill.
31650
+ */
31651
+ function keepPartial(track) {
31652
+ const built = {};
31048
31653
  for (let slot = 0; slot < track.names.length; slot++) {
31049
31654
  const field = track.names[slot];
31050
- if (track.moved[slot] === 1 && field !== void 0) written[field] = track.patch[field];
31655
+ if (track.moved[slot] === 1 && field !== void 0) built[field] = 0;
31051
31656
  }
31052
- return written;
31657
+ track.partialMoved.set(track.moved);
31658
+ track.partial = built;
31659
+ return built;
31053
31660
  }
31054
31661
  /**
31055
31662
  * Releases the field bookkeeping of a track that is ending: the fields it owned, its own offsets
@@ -31289,7 +31896,9 @@ function startTrack$1(actx, entity, component, to, options, muted, driven = fals
31289
31896
  masked: noMaskedFields(),
31290
31897
  keep,
31291
31898
  reveals: revealsOf(component, target),
31292
- patch: { ...target }
31899
+ patch: { ...target },
31900
+ partial: void 0,
31901
+ partialMoved: new Uint8Array(names.length)
31293
31902
  };
31294
31903
  actx.state.nextId += 1;
31295
31904
  if (track.ended) return track;
@@ -35124,8 +35733,8 @@ function overTargetOf(ctx, node, run) {
35124
35733
  }
35125
35734
  /**
35126
35735
  * Builds the scene in one synchronous block: the layers, then the projections of the old scene
35127
- * out, then the ones of the new scene in, then the list the scene names to `assets`, then the
35128
- * frame loop out of its idle cap, and only then the event.
35736
+ * out, then the ones of the new scene in, the new scene to the world's systems, then the list the
35737
+ * scene names to `assets`, then the frame loop out of its idle cap, and only then the event.
35129
35738
  *
35130
35739
  * @param ctx - Domain context of the plugin.
35131
35740
  * @param scene - The scene whose bundles are there.
@@ -35140,6 +35749,7 @@ function apply(ctx, scene, named) {
35140
35749
  if (previous !== void 0) projection.unmount(previous.projections);
35141
35750
  projection.mount(scene.projections, state.owner);
35142
35751
  state.current = scene.id;
35752
+ ctx.deps.world.ecs.setScene(scene.id);
35143
35753
  state.named = named;
35144
35754
  ctx.deps.assets.sceneBundles(named);
35145
35755
  ctx.deps.time.wake();
@@ -41588,8 +42198,9 @@ function countParticles(ectx) {
41588
42198
  state.over.particles = above;
41589
42199
  }
41590
42200
  /**
41591
- * Builds the `animate` system of the particles. It returns at once while the renderer does not
41592
- * draw. Time is the frame's `delta` only, so `time.step(dt)` drives every test.
42201
+ * Builds the `animate` system of the emitters. It runs while an entity carries `Emitter` (the world
42202
+ * skips a system whose query has no entity) and returns at once while the renderer does not draw.
42203
+ * Time is the frame's `delta` only, so `time.step(dt)` drives every test.
41593
42204
  *
41594
42205
  * @param ectx - Domain context of the effects plugin.
41595
42206
  * @returns The system `world.ecs.system` takes.
@@ -41605,6 +42216,26 @@ function createParticleSystem(ectx) {
41605
42216
  if (pixi === void 0) return;
41606
42217
  const reducedMotion = ectx.deps.anim.reducedMotion();
41607
42218
  for (const [entity, emitter] of rows) runEmitter(ectx, pixi, entity, emitter, context.time, reducedMotion);
42219
+ }
42220
+ });
42221
+ }
42222
+ /**
42223
+ * Builds the `animate` system of the orphans, registered right after the emitters: the particles
42224
+ * of an emitter whose host left fly on after the last `Emitter` is gone, so this system has no
42225
+ * query and runs every frame. It returns at once while the renderer does not draw, then steps the
42226
+ * orphans and keeps the particle budget.
42227
+ *
42228
+ * @param ectx - Domain context of the effects plugin.
42229
+ * @returns The system `world.ecs.system` takes.
42230
+ */
42231
+ function createOrphanSystem(ectx) {
42232
+ return system({
42233
+ name: "effects:orphans",
42234
+ phase: "animate",
42235
+ query: [],
42236
+ run: (_rows, context) => {
42237
+ const host = ectx.deps.renderer.host;
42238
+ if (!host.ready() || host.pixi() === void 0) return;
41608
42239
  runOrphans(ectx, context.time);
41609
42240
  countParticles(ectx);
41610
42241
  }
@@ -41932,7 +42563,7 @@ function warnScreenReads(ectx, entries) {
41932
42563
  });
41933
42564
  }
41934
42565
  /**
41935
- * Opens the world side: the two systems, the hook that retires an emitter's instance, and the two
42566
+ * Opens the world side: the three systems, the hook that retires an emitter's instance, and the two
41936
42567
  * hooks per filter kind; then records the filter components that are on entities already.
41937
42568
  *
41938
42569
  * @param ectx - Domain context of the effects plugin.
@@ -41940,7 +42571,7 @@ function warnScreenReads(ectx, entries) {
41940
42571
  */
41941
42572
  function openWorld(ectx, kinds) {
41942
42573
  const ecs = ectx.deps.world.ecs;
41943
- ectx.state.removers.push(ecs.system(createParticleSystem(ectx)), ecs.system(createFilterSystem(ectx)), ecs.onRemoved(Emitter, (entity) => forgetEmitter(ectx, entity)), ...kinds.map((kind) => ecs.onRemoved(kind.component, (entity) => forgetFilter(ectx, kind, entity))), ...kinds.map((kind) => ecs.onAdded(kind.component, (entity) => trackFilter(ectx, kind, entity))));
42574
+ ectx.state.removers.push(ecs.system(createParticleSystem(ectx)), ecs.system(createOrphanSystem(ectx)), ecs.system(createFilterSystem(ectx)), ecs.onRemoved(Emitter, (entity) => forgetEmitter(ectx, entity)), ...kinds.map((kind) => ecs.onRemoved(kind.component, (entity) => forgetFilter(ectx, kind, entity))), ...kinds.map((kind) => ecs.onAdded(kind.component, (entity) => trackFilter(ectx, kind, entity))));
41944
42575
  for (const kind of kinds) for (const [entity] of ecs.query(kind.component)) trackFilter(ectx, kind, entity);
41945
42576
  }
41946
42577
  /**
@@ -57466,6 +58097,7 @@ function featurePluginOf(name, description, children) {
57466
58097
  */
57467
58098
  function defineFeature(name, description) {
57468
58099
  if (takenNames().has(name)) throw new Error(`[game] Feature name "${name}" is reserved or already a plugin.\n Choose another feature name.`);
58100
+ assertNoSystemsRun(name, description);
57469
58101
  const children = Object.freeze([...description.features ?? []]);
57470
58102
  assertFeatures(name, children);
57471
58103
  const logicOnly = Object.assign(featurePluginOf(name, logicKeysOf(description), children), { features: Object.freeze(children.map((child) => child.logicOnly)) });
@@ -57475,6 +58107,18 @@ function defineFeature(name, description) {
57475
58107
  });
57476
58108
  }
57477
58109
  /**
58110
+ * Throws for a feature that still writes `systemsRun`, removed after 0.15.3: the type refuses it,
58111
+ * this catches a game in JavaScript or a factory that passes the key through.
58112
+ *
58113
+ * @param name - Feature name.
58114
+ * @param description - What the feature brings.
58115
+ * @throws {Error} When the description carries `systemsRun`.
58116
+ */
58117
+ function assertNoSystemsRun(name, description) {
58118
+ if (!("systemsRun" in description)) return;
58119
+ throw new Error(`[game] Feature "${name}" sets systemsRun, which the engine no longer has.\n Delete it: a system whose query has no entity is not run (world README, "When a system runs").`);
58120
+ }
58121
+ /**
57478
58122
  * Tells whether an entry of a feature tree carries its `features` list, as every plugin of
57479
58123
  * `defineFeature` does. A plain plugin, a cast object or a hole in the array does not.
57480
58124
  *
@@ -57947,6 +58591,168 @@ function defineEffect(kind, options = {}) {
57947
58591
  });
57948
58592
  }
57949
58593
  //#endregion
58594
+ //#region src/plugins/anim/tween/bezier.ts
58595
+ /**
58596
+ * @file anim/tween — the CSS timing function `cubic-bezier(x1, y1, x2, y2)` as an easing. The
58597
+ * curve is solved once, into a table of its progress at 512 even steps of time; a frame reads two
58598
+ * cells and draws a line between them. Where a line strays from the curve (the curve leaves an end
58599
+ * vertically), that step is solved again at each call, inside the bracket the table keeps. Either
58600
+ * way a call allocates nothing.
58601
+ */
58602
+ /**
58603
+ * How many even steps of time the table splits 0…1 into. With 512, a line strays at most 1e-5
58604
+ * from any curve that keeps a finite slope (the CSS keywords, an overshoot both ways), so only a
58605
+ * curve that leaves an end vertically solves any step at a call; 8 KB per curve.
58606
+ */
58607
+ const STEPS = 512;
58608
+ /**
58609
+ * How far the line of a step may stray from the curve at the step's middle before that step is
58610
+ * solved at each call instead: a fifth of the 1e-4 the factory promises, so the rest of the step,
58611
+ * where the line may stray a little more, stays inside the promise too (2e-5 measured).
58612
+ */
58613
+ const STRAY = 2e-5;
58614
+ /** Halvings of a solve: 2^-48 of the bracket, far below what a double of progress shows. */
58615
+ const HALVINGS = 48;
58616
+ /**
58617
+ * One coordinate of a cubic curve from 0 to 1 with the two inner control values. Pure.
58618
+ *
58619
+ * @param along - The curve parameter, 0 to 1.
58620
+ * @param first - The first control value.
58621
+ * @param second - The second control value.
58622
+ * @returns The coordinate at `along`.
58623
+ * @example
58624
+ * ```ts
58625
+ * coordinate(0.5, 1 / 3, 2 / 3); // 0.5: the middle of a straight line
58626
+ * ```
58627
+ */
58628
+ function coordinate(along, first, second) {
58629
+ const back = 1 - along;
58630
+ return 3 * back * back * along * first + 3 * back * along * along * second + along * along * along;
58631
+ }
58632
+ /**
58633
+ * The curve parameter whose x is `time`, found by halving a bracket of parameters. The x of the
58634
+ * curve never falls while x1 and x2 lie in 0…1, so halving always finds it. Pure.
58635
+ *
58636
+ * @param time - The x to reach.
58637
+ * @param x1 - The x of the first control point.
58638
+ * @param x2 - The x of the second control point.
58639
+ * @param low - A parameter whose x is at most `time`.
58640
+ * @param high - A parameter whose x is at least `time`.
58641
+ * @returns The parameter.
58642
+ * @example
58643
+ * ```ts
58644
+ * parameterAt(0.25, 0, 1, 0, 1); // 0.25: on a straight line the parameter is the time
58645
+ * ```
58646
+ */
58647
+ function parameterAt(time, x1, x2, low, high) {
58648
+ let from = low;
58649
+ let to = high;
58650
+ for (let step = 0; step < HALVINGS; step++) {
58651
+ const middle = (from + to) / 2;
58652
+ if (coordinate(middle, x1, x2) < time) from = middle;
58653
+ else to = middle;
58654
+ }
58655
+ return (from + to) / 2;
58656
+ }
58657
+ /**
58658
+ * Solves a curve into its table: the parameter and the progress at every even step of time, and
58659
+ * the steps whose line strays from the curve at their middle.
58660
+ *
58661
+ * @param points - `[x1, y1, x2, y2]`, x1 and x2 in 0…1.
58662
+ * @returns The table.
58663
+ */
58664
+ function solveTable(points) {
58665
+ const [x1 = 0, y1 = 0, x2 = 0, y2 = 0] = points;
58666
+ const params = /* @__PURE__ */ new Float64Array(513);
58667
+ const progress = /* @__PURE__ */ new Float64Array(513);
58668
+ const solved = new Uint8Array(STEPS);
58669
+ for (let index = 1; index < STEPS; index++) {
58670
+ params[index] = parameterAt(index / STEPS, x1, x2, 0, 1);
58671
+ progress[index] = coordinate(params[index] ?? 0, y1, y2);
58672
+ }
58673
+ params[STEPS] = 1;
58674
+ progress[STEPS] = 1;
58675
+ for (let index = 0; index < STEPS; index++) {
58676
+ const middle = parameterAt((index + .5) / STEPS, x1, x2, params[index] ?? 0, params[index + 1] ?? 1);
58677
+ const line = ((progress[index] ?? 0) + (progress[index + 1] ?? 0)) / 2;
58678
+ if (Math.abs(coordinate(middle, y1, y2) - line) > STRAY) solved[index] = 1;
58679
+ }
58680
+ return {
58681
+ points,
58682
+ params,
58683
+ progress,
58684
+ solved
58685
+ };
58686
+ }
58687
+ /**
58688
+ * The cell a solved step passes its time in and its progress out: a double handed to or returned
58689
+ * from a call V8 does not inline is boxed, a cell is not.
58690
+ */
58691
+ const SOLVED = /* @__PURE__ */ new Float64Array(1);
58692
+ /**
58693
+ * The progress of a curve at one time: exactly 0 up to the start and 1 from the end on, a line
58694
+ * between two cells of the table in between, or the curve itself on a step whose line strays.
58695
+ *
58696
+ * @param table - The curve's table.
58697
+ * @param time - The time.
58698
+ * @returns The progress.
58699
+ */
58700
+ function progressAt(table, time) {
58701
+ if (time <= 0) return 0;
58702
+ if (time >= 1) return 1;
58703
+ const at = time * STEPS;
58704
+ const index = Math.floor(at);
58705
+ if (table.solved[index] === 1) {
58706
+ SOLVED[0] = time;
58707
+ solveStep(table, index);
58708
+ return SOLVED[0] ?? 0;
58709
+ }
58710
+ const low = table.progress[index] ?? 0;
58711
+ return low + ((table.progress[index + 1] ?? 1) - low) * (at - index);
58712
+ }
58713
+ /**
58714
+ * The progress on a step whose line strays from the curve: the curve solved inside the bracket of
58715
+ * parameters the table keeps for that step. It reads the time from `SOLVED[0]` and leaves the
58716
+ * progress there. Kept apart so the common call stays small.
58717
+ *
58718
+ * @param table - The curve's table.
58719
+ * @param index - The step.
58720
+ */
58721
+ function solveStep(table, index) {
58722
+ const points = table.points;
58723
+ const along = parameterAt(SOLVED[0] ?? 0, points[0] ?? 0, points[2] ?? 0, table.params[index] ?? 0, table.params[index + 1] ?? 1);
58724
+ SOLVED[0] = coordinate(along, points[1] ?? 0, points[3] ?? 0);
58725
+ }
58726
+ /**
58727
+ * The CSS timing function `cubic-bezier(x1, y1, x2, y2)` as an easing a tween, a keyframe segment
58728
+ * or a motion takes for `ease`. The curve is solved once, here, into a table; a frame reads it, so
58729
+ * an eased time costs a few reads and allocates nothing, and the progress stays within 1e-4 of the
58730
+ * exact curve. It is exactly 0 up to the start and exactly 1 from the end on, and may leave 0…1 on
58731
+ * the way (an overshoot). Make each curve once, at module level, and share it.
58732
+ *
58733
+ * @param x1 - The x of the first control point, 0 to 1.
58734
+ * @param y1 - The y of the first control point; below 0 or above 1 overshoots.
58735
+ * @param x2 - The x of the second control point, 0 to 1.
58736
+ * @param y2 - The y of the second control point; below 0 or above 1 overshoots.
58737
+ * @returns The curve: normalised time in, eased progress out.
58738
+ * @throws {Error} When x1 or x2 lies outside 0…1: the curve would be no function of time.
58739
+ * @example
58740
+ * ```ts
58741
+ * // The design's arrival curve, made once and named by every tween that arrives.
58742
+ * const arrive = cubicBezier(0.22, 0.61, 0.36, 1);
58743
+ * arrive(0.5); // 0.873 (to 1e-4)
58744
+ * const drop = defineAnimation("card.drop", {
58745
+ * slots: { card: type<Target>() },
58746
+ * build: ({ card }) => sequence(tween(card, Transform, { y: 40 }, { ms: 300, ease: arrive }))
58747
+ * });
58748
+ * ```
58749
+ */
58750
+ function cubicBezier(x1, y1, x2, y2) {
58751
+ if (!(x1 >= 0 && x1 <= 1 && x2 >= 0 && x2 <= 1)) throw new Error(`[game] cubicBezier needs x1 and x2 between 0 and 1, got ${x1} and ${x2}.\n The x of a control point is a time; y1 and y2 may leave 0…1 for an overshoot.`);
58752
+ const table = solveTable(Float64Array.of(x1, y1, x2, y2));
58753
+ return (t) => progressAt(table, t);
58754
+ }
58755
+ //#endregion
57950
58756
  //#region src/index.ts
57951
58757
  /**
57952
58758
  * The `@moku-labs/game` package — 2D puzzle game engine on Moku Core.
@@ -58155,4 +58961,4 @@ const screen = [
58155
58961
  uiPlugin
58156
58962
  ];
58157
58963
  //#endregion
58158
- export { parallel as $, SHADER_WHATS as $t, types_exports$12 as A, Blur as At, DebugTrigger as B, assetsPlugin as Bt, types_exports$5 as C, Traced as Ct, types_exports$9 as D, effectsPlugin as Dt, types_exports$8 as E, messageDuration as Et, types_exports$17 as F, Outline as Ft, textPlugin as G, rendererPlugin as Gt, popup as H, AnimPlayer as Ht, uiPlugin as I, defineFilter as It, defineAnimation as J, MONITOR_SECTIONS as Jt, platformPlugin as K, PASS_KINDS as Kt, resolve as L, debugPlugin as Lt, types_exports$14 as M, Displacement as Mt, types_exports$15 as N, Glow as Nt, types_exports$10 as O, Emitter as Ot, types_exports$16 as P, Noise as Pt, mark as Q, PROBLEM_RULES as Qt, defineMotion as R, audioPlugin as Rt, types_exports$4 as S, Traceable as St, types_exports$7 as T, messageArgument as Tt, defineTokens as U, Animation as Ut, LocalWrite as V, animPlugin as Vt, defineStyle as W, Frames as Wt, frames as X, PERF_GAP_BUCKETS as Xt, external as Y, PERF_CAUSES as Yt, haptic as Z, PERF_LOG_VERSION as Zt, load as _, Pressable as _t, defineEffect as a, slot as an, spawned as at, types_exports$2 as b, Tappable as bt, schedule as c, modelPlugin as cn, use as ct, tr as d, timePlugin as dn, Draggable as dt, DEMAND_REASONS as en, play$1 as et, defineFeature as f, clockPlugin as fn, DropTarget as ft, defineBundles as g, PointerOver as gt, music as h, Pointer as ht, screen as i, exit as in, spawn as it, types_exports$13 as j, ColorMatrix as jt, types_exports$11 as k, Alpha as kt, projection as l, SaveUnreadableError as ln, wait as lt, defineEmitter as m, Hovered as mt, createPlugin as n, flowPlugin as nn, set as nt, guide as o, to as on, stagger as ot, flattenFeatures as p, Held as pt, HAPTIC_KINDS as q, frameAt as qt, defineGame as r, refuseStart as rn, sfx as rt, hint as s, type as sn, tween as st, createApp as t, worldPlugin as tn, sequence as tt, defineScene as u, lifecyclePlugin as un, inputPlugin as ut, types_exports as v, Pressed as vt, types_exports$6 as w, i18nPlugin as wt, types_exports$3 as x, Touchable as xt, types_exports$1 as y, Swipeable as yt, Box as z, scenesPlugin as zt };
58964
+ export { mark as $, PROBLEM_RULES as $t, types_exports$11 as A, Alpha as At, Box as B, scenesPlugin as Bt, types_exports$4 as C, Traceable as Ct, types_exports$8 as D, messageDuration as Dt, types_exports$7 as E, messageArgument as Et, types_exports$16 as F, Noise as Ft, defineStyle as G, Frames as Gt, LocalWrite as H, animPlugin as Ht, types_exports$17 as I, Outline as It, HAPTIC_KINDS as J, frameAt as Jt, textPlugin as K, rendererPlugin as Kt, uiPlugin as L, defineFilter as Lt, types_exports$13 as M, ColorMatrix as Mt, types_exports$14 as N, Displacement as Nt, types_exports$9 as O, effectsPlugin as Ot, types_exports$15 as P, Glow as Pt, haptic as Q, PERF_LOG_VERSION as Qt, resolve as R, debugPlugin as Rt, types_exports$3 as S, Touchable as St, types_exports$6 as T, i18nPlugin as Tt, popup as U, AnimPlayer as Ut, DebugTrigger as V, assetsPlugin as Vt, defineTokens as W, Animation as Wt, external as X, PERF_CAUSES as Xt, defineAnimation as Y, MONITOR_SECTIONS as Yt, frames as Z, PERF_GAP_BUCKETS as Zt, defineBundles as _, PointerOver as _t, cubicBezier as a, exit as an, spawn as at, types_exports$1 as b, Swipeable as bt, hint as c, type as cn, tween as ct, defineScene as d, lifecyclePlugin as dn, inputPlugin as dt, SHADER_WHATS as en, parallel as et, tr as f, timePlugin as fn, Draggable as ft, music as g, Pointer as gt, defineEmitter as h, Hovered as ht, screen as i, refuseStart as in, sfx as it, types_exports$12 as j, Blur as jt, types_exports$10 as k, Emitter as kt, schedule as l, modelPlugin as ln, use as lt, flattenFeatures as m, Held as mt, createPlugin as n, worldPlugin as nn, sequence as nt, defineEffect as o, slot as on, spawned as ot, defineFeature as p, clockPlugin as pn, DropTarget as pt, platformPlugin as q, PASS_KINDS as qt, defineGame as r, flowPlugin as rn, set as rt, guide as s, to as sn, stagger as st, createApp as t, DEMAND_REASONS as tn, play$1 as tt, projection as u, SaveUnreadableError as un, wait as ut, load as v, Pressable as vt, types_exports$5 as w, Traced as wt, types_exports$2 as x, Tappable as xt, types_exports as y, Pressed as yt, defineMotion as z, audioPlugin as zt };