@moku-labs/game 0.15.18 → 0.15.22

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.
Files changed (37) hide show
  1. package/dist/app/page.d.mts +1 -1
  2. package/dist/app/page.mjs +1 -1
  3. package/dist/app/system.d.mts +3 -3
  4. package/dist/app.d.mts +3 -3
  5. package/dist/app.mjs +1 -1
  6. package/dist/assets.d.mts +1 -1
  7. package/dist/{capture-D7XvoHdH.mjs → capture-PTJXDpxK.mjs} +117 -16
  8. package/dist/{commands-CQt3jo0G.d.mts → commands-BvNp27KS.d.mts} +5 -5
  9. package/dist/control.d.mts +4 -4
  10. package/dist/control.mjs +1 -1
  11. package/dist/{define-BAuGN0UM.d.mts → define-DAVDb7dr.d.mts} +2 -2
  12. package/dist/{headless-uUUH54Ij.d.mts → headless-BCwsb1wR.d.mts} +1 -1
  13. package/dist/{headless-BA6ffzqD.mjs → headless-Bxxl_mQX.mjs} +1 -1
  14. package/dist/{index-ChqvcZwY.d.mts → index-CKXNW8_t.d.mts} +26 -4
  15. package/dist/index.d.mts +5 -5
  16. package/dist/index.mjs +3 -3
  17. package/dist/inspect.d.mts +6 -6
  18. package/dist/inspect.mjs +1 -1
  19. package/dist/jsx-dev-runtime.d.mts +1 -1
  20. package/dist/jsx-runtime.d.mts +1 -1
  21. package/dist/{run-BqYmcNo7.mjs → run-DRcc2SKF.mjs} +2 -2
  22. package/dist/{runtime-BNP4wbDk.d.mts → runtime-Dt7VdhqR.d.mts} +1 -1
  23. package/dist/{sources-BYKXYPhd.mjs → sources-Dpc4NXvG.mjs} +1 -1
  24. package/dist/{src-B6WrtDtL.mjs → src-BcfQy5vB.mjs} +1302 -280
  25. package/dist/testing.d.mts +4 -4
  26. package/dist/testing.mjs +2 -2
  27. package/dist/{types-C4igewxk.d.mts → types-B02c7d6g.d.mts} +21 -5
  28. package/dist/{types-bwefoJES.d.mts → types-BFGyVykz.d.mts} +213 -19
  29. package/dist/{types-DMsvVZZT.d.mts → types-C8JK9u1Y.d.mts} +2 -2
  30. package/dist/{types-BP99ij9v.d.mts → types-COWBfy2L.d.mts} +8 -5
  31. package/dist/{types-DGvpNGxm.d.mts → types-YxQts-fd.d.mts} +4 -4
  32. package/dist/{visual-BdapVgBW.d.mts → visual-Dv67ITPu.d.mts} +6 -6
  33. package/dist/{visual-DNM5oFYx.mjs → visual-kEC9nb-o.mjs} +20 -2
  34. package/dist/visual.d.mts +1 -1
  35. package/dist/visual.mjs +2 -2
  36. package/llms.txt +2 -2
  37. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
2
  import { r as systemSource, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { a as finishDrafts, i as enableDraftPatches, n as deepFreeze$1, o as openDrafts, r as dropDrafts } from "./drafts-BawpSaVp.mjs";
4
- import { d as graphHash, f as isRestCheckpoint, h as pushEntry, i as servePaused, l as findNode, m as compact, n as captureFrame, o as collectFlows, p as slotNames, r as serveCaptures, s as describeGraph, t as cancelCaptures, u as framePath } from "./capture-D7XvoHdH.mjs";
5
- import { $ as component, A as localPoseOf, B as OverlayLevel, C as builtInStyles, D as textFor, E as readStyle, F as rootPoseOf, G as arcDefaults, H as Shape, I as Display, J as Exiting, K as componentsFor, L as HudPart, M as poseUnder, N as rootPointOf, O as applyPose, P as rootPoseInto, Q as Tree, R as NineSlice, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, _ as uiFor, a as visualOf$1, b as Countdown, c as Covered, d as LocalWrite, f as Scroll, h as UiCounters, i as samePose, it as tag, j as parentOf$1, k as isLeaving, l as DebugTrigger, m as UI_OWNER, n as drawnRect, nt as system, o as visualRectOf, p as ScrollState, r as fitScaleOf, rt as systemsFor, s as Box, t as CONTENT$1, tt as resource, u as Escapable, x as Text, z as Overlay } from "./visual-DNM5oFYx.mjs";
4
+ import { _ as pushEntry, c as collectFlows, d as findNode, f as framePath, g as compact, h as slotNames, i as servePaused, l as describeGraph, m as isRestCheckpoint, n as captureFrame, o as epochOf, p as graphHash, r as serveCaptures, s as pageNow$1, t as cancelCaptures } from "./capture-PTJXDpxK.mjs";
5
+ import { $ as component, A as localPoseOf, B as OverlayLevel, C as builtInStyles, D as textFor, E as readStyle, F as rootPoseOf, G as arcDefaults, H as Shape, I as Display, J as Exiting, K as componentsFor, L as HudPart, M as poseUnder, N as rootPointOf, O as applyPose, P as rootPoseInto, Q as Tree, R as NineSlice, U as Sprite, V as Parent, W as Transform, X as Now, Y as Layer, Z as Order, _ as uiFor, a as visualOf$1, b as Countdown, c as Covered, d as LocalWrite, f as Scroll, h as UiCounters, i as samePose, it as tag, j as parentOf$1, k as isLeaving, l as DebugTrigger, m as UI_OWNER, n as drawnRect, nt as system, o as visualRectOf, p as ScrollState, r as fitScaleOf, rt as systemsFor, s as Box, t as CONTENT$1, tt as resource, u as Escapable, x as Text, z as Overlay } from "./visual-kEC9nb-o.mjs";
6
6
  import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { n as isComponentDefinition } from "./component-DGg5DqKK.mjs";
8
8
  import { n as flatten } from "./flatten-nUX9PpGo.mjs";
@@ -252,7 +252,7 @@ function createMeasureState() {
252
252
  *
253
253
  * @returns The page clock, ms.
254
254
  */
255
- function pageNow$1() {
255
+ function pageNow() {
256
256
  return globalThis.performance.now();
257
257
  }
258
258
  /**
@@ -277,7 +277,7 @@ function timePhase(ctx, record, callbacks, phase, start) {
277
277
  error
278
278
  });
279
279
  }
280
- const end = pageNow$1();
280
+ const end = pageNow();
281
281
  record.phaseMs[phase] = end - start;
282
282
  return end;
283
283
  }
@@ -294,7 +294,7 @@ function timePhase(ctx, record, callbacks, phase, start) {
294
294
  function runMeasuredFrame(ctx, measure, at, stepped) {
295
295
  const { record } = measure;
296
296
  const phases = capturePhases(ctx);
297
- let mark = pageNow$1();
297
+ let mark = pageNow();
298
298
  record.startMs = stepped ? 0 : mark - at;
299
299
  mark = timePhase(ctx, record, phases[0], 0, mark);
300
300
  mark = timePhase(ctx, record, phases[1], 1, mark);
@@ -324,7 +324,7 @@ function runMeasuredFrame(ctx, measure, at, stepped) {
324
324
  } catch (error) {
325
325
  ctx.log.error("time: measure listener failed", { error });
326
326
  }
327
- measure.listenersMs = pageNow$1() - mark;
327
+ measure.listenersMs = pageNow() - mark;
328
328
  measure.lastAt = at;
329
329
  measure.skipped = 0;
330
330
  }
@@ -977,7 +977,7 @@ function createRateState(config) {
977
977
  * @returns The page clock, ms.
978
978
  */
979
979
  function nowOf() {
980
- return typeof globalThis.performance === "undefined" ? 0 : pageNow$1();
980
+ return typeof globalThis.performance === "undefined" ? 0 : pageNow();
981
981
  }
982
982
  /**
983
983
  * Empties both windows: evidence of one rate never judges another, and a resume is a new situation.
@@ -1650,12 +1650,12 @@ function runFrameWork(ctx, at) {
1650
1650
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) {
1651
1651
  const { measure } = ctx.state;
1652
1652
  if (measure !== void 0 && measure.listeners.length > 0) {
1653
- runMeasuredFrame(ctx, measure, at ?? pageNow$1(), at === void 0);
1653
+ runMeasuredFrame(ctx, measure, at ?? pageNow(), at === void 0);
1654
1654
  return measure.record.workMs;
1655
1655
  }
1656
1656
  }
1657
1657
  runPhases(ctx);
1658
- return at !== void 0 && ctx.state.rate.governor !== void 0 ? pageNow$1() - at : 0;
1658
+ return at !== void 0 && ctx.state.rate.governor !== void 0 ? pageNow() - at : 0;
1659
1659
  }
1660
1660
  /**
1661
1661
  * Runs exactly one frame: advances `Time` by the scaled delta, then calls the six phases.
@@ -1849,7 +1849,7 @@ function createTimeApi(ctx) {
1849
1849
  }
1850
1850
  return noMeasure;
1851
1851
  },
1852
- agentClock: () => typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? pageNow$1 : void 0,
1852
+ agentClock: () => typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? pageNow : void 0,
1853
1853
  chargeAgent: (ms) => {
1854
1854
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__) {
1855
1855
  const { measure } = ctx.state;
@@ -3050,7 +3050,7 @@ async function effectValue(state, log, descriptor, signal) {
3050
3050
  * @param work - The promise of the running effect.
3051
3051
  * @returns A promise that settles in the next `signals` phase after `work` settled.
3052
3052
  */
3053
- function enqueue$1(state, work) {
3053
+ function enqueue$2(state, work) {
3054
3054
  return new Promise((resolve, reject) => {
3055
3055
  const pending = {
3056
3056
  ready: false,
@@ -3191,7 +3191,7 @@ function createFxApi(ctx, deps) {
3191
3191
  const shown = descriptor.kind === "guide" ? beginGuide(state, signal) : signal;
3192
3192
  const work = effectValue(state, ctx.log, descriptor, shown);
3193
3193
  if (state.mode === "fast" || !ctx.deps.time.isRunning()) return work;
3194
- return enqueue$1(state, work);
3194
+ return enqueue$2(state, work);
3195
3195
  },
3196
3196
  buffer: (item) => {
3197
3197
  if (state.mode === "fast") return;
@@ -5507,7 +5507,7 @@ function startDeadline(timeoutMs) {
5507
5507
  * @param running - The promise of `run()`.
5508
5508
  * @returns `true` when the loop settled, `false` when the deadline won and the loop still runs.
5509
5509
  */
5510
- async function settle$2(ctx, running) {
5510
+ async function settle$3(ctx, running) {
5511
5511
  const quiet = running.then(noop, noop);
5512
5512
  const deadline = startDeadline(ctx.config.settleTimeoutMs);
5513
5513
  try {
@@ -5536,7 +5536,7 @@ async function stopRunner(ctx) {
5536
5536
  stop.abort("stop");
5537
5537
  state.abort = stop;
5538
5538
  }
5539
- const settled = running === void 0 || await settle$2(ctx, running);
5539
+ const settled = running === void 0 || await settle$3(ctx, running);
5540
5540
  state.running = void 0;
5541
5541
  ctx.state.gate.held = void 0;
5542
5542
  if (settled) state.abort = void 0;
@@ -14939,6 +14939,11 @@ function createHostApi(ctx, deps) {
14939
14939
  });
14940
14940
  return true;
14941
14941
  },
14942
+ initSource: (source) => {
14943
+ const app = state.app;
14944
+ if (!state.ready || app === void 0) return;
14945
+ app.renderer.texture.initSource(source);
14946
+ },
14942
14947
  clearsCanvas: () => state.ready && state.app?.renderer.background.clearBeforeRender === true,
14943
14948
  setResolution: (value, reason) => askResolution(state, value, reason),
14944
14949
  overrideResolution: (value) => {
@@ -15058,7 +15063,10 @@ const KEY = {
15058
15063
  qc: 25,
15059
15064
  qd: 26,
15060
15065
  qtx: 27,
15061
- qty: 28
15066
+ qty: 28,
15067
+ /** The canvas size at the take, CSS px: a picture cut to it is taken again when it changes (F53). */
15068
+ sw: 29,
15069
+ sh: 30
15062
15070
  };
15063
15071
  /** A whole number of texels within this much (P2.5). */
15064
15072
  const ON_GRID = 1 / 64;
@@ -15123,6 +15131,27 @@ function gridSpan(start, end, resolution) {
15123
15131
  };
15124
15132
  }
15125
15133
  /**
15134
+ * A span on the canvas cut to the canvas and the padding around it (F53). Pixi's live filter pass cuts its target to
15135
+ * the viewport and pads it after, so it draws what lies within the padding of the canvas too: a glow just off the
15136
+ * screen still reaches in. A span wholly outside comes back empty: its end is not after its start.
15137
+ *
15138
+ * @param start - The span's start on the canvas, CSS px.
15139
+ * @param end - Its end.
15140
+ * @param size - The canvas size on that axis, CSS px.
15141
+ * @param padding - The padding of the filters, CSS px.
15142
+ * @returns The part of the span kept.
15143
+ * @example
15144
+ * ```ts
15145
+ * cutSpan(-40, 120, 100, 10); // { start: -10, end: 110 }
15146
+ * ```
15147
+ */
15148
+ function cutSpan(start, end, size, padding) {
15149
+ return {
15150
+ start: Math.max(start, -padding),
15151
+ end: Math.min(end, size + padding)
15152
+ };
15153
+ }
15154
+ /**
15126
15155
  * The padding of a stack on the grid of its resolution: what stage 3 adds on each side of the subtree picture, so the
15127
15156
  * picture's bounds stay on that grid inside the output texture.
15128
15157
  *
@@ -15925,6 +15954,123 @@ function flushRanges(kctx) {
15925
15954
  }
15926
15955
  }
15927
15956
  //#endregion
15957
+ //#region src/plugins/renderer/kept/drawn-bounds.ts
15958
+ /** Every flag of a node's display status: visible, renderable, not culled (Pixi's `localDisplayStatus`). */
15959
+ const SHOWN = 7;
15960
+ /** The effects of a node that holds none. */
15961
+ const NO_EFFECTS = [];
15962
+ /** The matrix of each depth of the walk, from the top's space to the node's. Grown once, then reused. */
15963
+ const MATRICES = [];
15964
+ /** The bounds a masked node's subtree is gathered in, one per depth. Grown once, then reused. */
15965
+ const MASKED = [];
15966
+ /** The bounds the last walk answered, made once. */
15967
+ const RESULT = [];
15968
+ /**
15969
+ * The matrix of a depth, made the first time the walk reaches it.
15970
+ *
15971
+ * @param pixi - The module that draws.
15972
+ * @param depth - The depth under the top.
15973
+ * @returns The matrix.
15974
+ */
15975
+ function matrixAt(pixi, depth) {
15976
+ const known = MATRICES[depth];
15977
+ if (known !== void 0) return known;
15978
+ const made = new pixi.Matrix();
15979
+ MATRICES[depth] = made;
15980
+ return made;
15981
+ }
15982
+ /**
15983
+ * The bounds a masked node of a depth gathers its subtree in, made the first time the walk needs it.
15984
+ *
15985
+ * @param pixi - The module that draws.
15986
+ * @param depth - The depth under the top.
15987
+ * @returns The bounds, cleared.
15988
+ */
15989
+ function maskedAt(pixi, depth) {
15990
+ const known = MASKED[depth];
15991
+ if (known !== void 0) return known.clear();
15992
+ const made = new pixi.Bounds();
15993
+ MASKED[depth] = made;
15994
+ return made;
15995
+ }
15996
+ /**
15997
+ * Tells whether Pixi skips a node and its subtree: hidden, not renderable, culled, or not measured (a mask).
15998
+ *
15999
+ * @param node - A node under the top.
16000
+ * @returns True when it adds nothing.
16001
+ */
16002
+ function skipped(node) {
16003
+ return node.localDisplayStatus !== SHOWN || !node.measurable;
16004
+ }
16005
+ /**
16006
+ * Adds what a node draws itself and what its children draw to the bounds, under the node's matrix.
16007
+ *
16008
+ * @param pixi - The module that draws.
16009
+ * @param node - The node, shown.
16010
+ * @param into - The bounds so far.
16011
+ * @param depth - Its depth under the top.
16012
+ * @param top - The top.
16013
+ */
16014
+ function addContent(pixi, node, into, depth, top) {
16015
+ const matrix = matrixAt(pixi, depth);
16016
+ if (node.boundsArea) {
16017
+ into.addRect(node.boundsArea, matrix);
16018
+ return;
16019
+ }
16020
+ if (node.renderPipeId !== void 0) {
16021
+ const own = node.bounds;
16022
+ into.addFrame(own.minX, own.minY, own.maxX, own.maxY, matrix);
16023
+ }
16024
+ const children = node.children;
16025
+ for (let index = 0; index < children.length; index += 1) {
16026
+ const child = children[index];
16027
+ if (child !== void 0) addDrawn$1(pixi, child, into, depth + 1, top);
16028
+ }
16029
+ }
16030
+ /**
16031
+ * Adds what one node and its subtree draw to the bounds, in the top's space. The top itself is never skipped: it is
16032
+ * the root of the picture's render, drawn whatever its own flags say. A masked node gathers its subtree apart, then
16033
+ * the mask cuts it.
16034
+ *
16035
+ * @param pixi - The module that draws.
16036
+ * @param node - The node.
16037
+ * @param into - The bounds so far.
16038
+ * @param depth - Its depth under the top, 0 for the top.
16039
+ * @param top - The top: the space of the bounds, and the root a mask is measured from.
16040
+ */
16041
+ function addDrawn$1(pixi, node, into, depth, top) {
16042
+ if (depth > 0 && skipped(node)) return;
16043
+ const matrix = matrixAt(pixi, depth);
16044
+ if (depth === 0) matrix.identity();
16045
+ else {
16046
+ node.updateLocalTransform();
16047
+ matrix.appendFrom(node.localTransform, matrixAt(pixi, depth - 1));
16048
+ }
16049
+ const effects = node.effects ?? NO_EFFECTS;
16050
+ if (effects.length === 0) {
16051
+ addContent(pixi, node, into, depth, top);
16052
+ return;
16053
+ }
16054
+ const masked = maskedAt(pixi, depth);
16055
+ addContent(pixi, node, masked, depth, top);
16056
+ for (const effect of effects) effect.addLocalBounds?.(masked, top);
16057
+ if (masked.isValid) into.addFrame(masked.minX, masked.minY, masked.maxX, masked.maxY);
16058
+ }
16059
+ /**
16060
+ * The bounds of what a subtree draws, in its top's own space: no node Pixi skips, masks applied.
16061
+ *
16062
+ * @param pixi - The module that draws.
16063
+ * @param top - The top of the subtree.
16064
+ * @returns The bounds, one object the next walk writes again; empty (`isValid` false) when nothing is drawn.
16065
+ */
16066
+ function drawnBounds(pixi, top) {
16067
+ const into = RESULT[0] ?? new pixi.Bounds();
16068
+ RESULT[0] = into;
16069
+ into.clear();
16070
+ addDrawn$1(pixi, top, into, 0, top);
16071
+ return into;
16072
+ }
16073
+ //#endregion
15928
16074
  //#region src/plugins/renderer/kept/state.ts
15929
16075
  /**
15930
16076
  * @file renderer/kept — the state of the kept-picture cache, and its stop from state alone.
@@ -16081,6 +16227,7 @@ const NOW = {
16081
16227
  enabled: 0,
16082
16228
  rs: 1,
16083
16229
  P: 0,
16230
+ alpha: NaN,
16084
16231
  r: 1,
16085
16232
  a: 1,
16086
16233
  b: 0,
@@ -16109,27 +16256,42 @@ function resolutionOf$1(filter, resolution) {
16109
16256
  return own === "inherit" ? resolution : own;
16110
16257
  }
16111
16258
  /**
16112
- * Reads what the enabled filters of a stack draw with: how many, the smallest resolution, and the padding sum Pixi
16113
- * pads by (whole pixels).
16259
+ * Reads what the enabled filters of a stack draw with: how many, the smallest resolution, the padding sum Pixi
16260
+ * pads by (whole pixels), and the alpha they multiply by while every one is an `AlphaFilter`.
16114
16261
  *
16115
16262
  * @param slots - The record's slots.
16116
16263
  * @param resolution - The renderer resolution.
16264
+ * @param alphaFilter - Pixi's `AlphaFilter` class.
16117
16265
  * @param into - Where the numbers go.
16118
16266
  */
16119
- function readStack(slots, resolution, into) {
16267
+ function readStack(slots, resolution, alphaFilter, into) {
16120
16268
  let enabled = 0;
16121
16269
  let rs = Number.POSITIVE_INFINITY;
16122
16270
  let padding = 0;
16271
+ let alpha = 1;
16123
16272
  for (let index = 0; index < slots.length; index += 1) {
16124
16273
  const filter = slots[index]?.filter;
16125
16274
  if (filter === void 0 || !filter.enabled) continue;
16126
16275
  enabled += 1;
16127
16276
  rs = Math.min(rs, resolutionOf$1(filter, resolution));
16128
16277
  padding += filter.padding;
16278
+ alpha *= filter instanceof alphaFilter ? filter.alpha : NaN;
16129
16279
  }
16130
16280
  into.enabled = enabled;
16131
16281
  into.rs = enabled === 0 ? resolution : rs;
16132
16282
  into.P = Math.trunc(padding);
16283
+ into.alpha = alpha;
16284
+ }
16285
+ /**
16286
+ * Tells whether a stack draws as its subtree picture at an alpha (F54): every enabled filter an `AlphaFilter`, no
16287
+ * padding, at the renderer resolution. The quad then draws the picture itself at that alpha, as Pixi's alpha pass
16288
+ * draws its input: no stage 3, no pass when the alpha changes.
16289
+ *
16290
+ * @param now - This frame's numbers.
16291
+ * @returns True when it does.
16292
+ */
16293
+ function drawsDirect(now) {
16294
+ return !Number.isNaN(now.alpha) && now.P === 0 && now.rs === now.r;
16133
16295
  }
16134
16296
  /**
16135
16297
  * Tells whether a blend mode draws over what is under it as a picture does.
@@ -16283,7 +16445,7 @@ function holdsDisplay(kctx, top) {
16283
16445
  */
16284
16446
  function createRecord$1(kctx, pixi, entity, view, slots, category) {
16285
16447
  const top = view.wrapper ?? view.object;
16286
- const key = (/* @__PURE__ */ new Float64Array(29)).fill(NaN);
16448
+ const key = (/* @__PURE__ */ new Float64Array(31)).fill(NaN);
16287
16449
  const record = {
16288
16450
  entity,
16289
16451
  top,
@@ -16316,6 +16478,8 @@ function createRecord$1(kctx, pixi, entity, view, slots, category) {
16316
16478
  bytes: 0,
16317
16479
  settle: false,
16318
16480
  refused: 0,
16481
+ cut: false,
16482
+ direct: false,
16319
16483
  displays: void 0
16320
16484
  };
16321
16485
  const { state } = kctx;
@@ -16441,7 +16605,8 @@ function linearDiffers(key, now, base, rsAt) {
16441
16605
  */
16442
16606
  function neededBytes(record, now, box) {
16443
16607
  const { w1, h1, Ps } = box;
16444
- let bytes = textureBytes(w1, h1, now.rs) + textureBytes(w1 + 2 * Ps, h1 + 2 * Ps, now.r);
16608
+ let bytes = textureBytes(w1, h1, now.rs);
16609
+ if (!drawsDirect(now)) bytes += textureBytes(w1 + 2 * Ps, h1 + 2 * Ps, now.r);
16445
16610
  const first = firstInputs(record.slots);
16446
16611
  if (first !== void 0) for (let index = 0; index < first.inputs.count; index += 1) {
16447
16612
  const scale = first.inputs.scaleOf(index) ?? 0;
@@ -16508,14 +16673,22 @@ function writeTakeKeys(record, now, box) {
16508
16673
  function takePicture(kctx, pixi, matrices, record, view, now) {
16509
16674
  const { top } = record;
16510
16675
  if (!kctx.ctx.config.onDemand && holdsDisplay(kctx, top)) return false;
16511
- const local = clippedToArea(top.getLocalBounds(), top.filterArea);
16676
+ const drawn = drawnBounds(pixi, top);
16677
+ if (!drawn.isValid) return false;
16678
+ const local = clippedToArea(drawn, top.filterArea);
16512
16679
  if (isEmptyBounds(local)) return false;
16513
16680
  const box = linearBounds(now.a, now.b, now.c, now.d, local);
16514
- const x = gridSpan(now.tx + box.x0, now.tx + box.x1, now.rs);
16515
- const y = gridSpan(now.ty + box.y0, now.ty + box.y1, now.rs);
16681
+ const screen = kctx.host.screen();
16682
+ const width = screen?.width ?? Number.POSITIVE_INFINITY;
16683
+ const height = screen?.height ?? Number.POSITIVE_INFINITY;
16684
+ const Ps = paddingOnGrid(now.P, now.rs);
16685
+ const across = cutSpan(now.tx + box.x0, now.tx + box.x1, width, Ps);
16686
+ const down = cutSpan(now.ty + box.y0, now.ty + box.y1, height, Ps);
16687
+ if (isEmptySpan(across) || isEmptySpan(down)) return false;
16688
+ const x = gridSpan(across.start, across.end, now.rs);
16689
+ const y = gridSpan(down.start, down.end, now.rs);
16516
16690
  const w1 = x.texels / now.rs;
16517
16691
  const h1 = y.texels / now.rs;
16518
- const Ps = paddingOnGrid(now.P, now.rs);
16519
16692
  if (!fitsTexture(w1, h1, now.rs) || !fitsTexture(w1 + 2 * Ps, h1 + 2 * Ps, now.r)) return false;
16520
16693
  const needed = neededBytes(record, now, {
16521
16694
  w1,
@@ -16528,6 +16701,11 @@ function takePicture(kctx, pixi, matrices, record, view, now) {
16528
16701
  return false;
16529
16702
  }
16530
16703
  record.refused = 0;
16704
+ const cutAcross = across.start > now.tx + box.x0 || across.end < now.tx + box.x1;
16705
+ const cutDown = down.start > now.ty + box.y0 || down.end < now.ty + box.y1;
16706
+ record.cut = cutAcross || cutDown;
16707
+ record.key[KEY.sw] = width;
16708
+ record.key[KEY.sh] = height;
16531
16709
  writeTakeKeys(record, now, {
16532
16710
  x,
16533
16711
  y,
@@ -16540,6 +16718,21 @@ function takePicture(kctx, pixi, matrices, record, view, now) {
16540
16718
  return ok;
16541
16719
  }
16542
16720
  /**
16721
+ * Tells whether a span holds nothing: its end is not after its start. Pure.
16722
+ *
16723
+ * @param span - The span.
16724
+ * @param span.start - Its start.
16725
+ * @param span.end - Its end.
16726
+ * @returns True when it is empty.
16727
+ * @example
16728
+ * ```ts
16729
+ * isEmptySpan({ start: -10, end: -10 }); // true
16730
+ * ```
16731
+ */
16732
+ function isEmptySpan(span) {
16733
+ return span.end <= span.start;
16734
+ }
16735
+ /**
16543
16736
  * The local bounds of a top cut to its filter area, both in the top's own units: its filters draw nothing outside
16544
16737
  * the area. With no area the bounds are returned as they are. Pure.
16545
16738
  *
@@ -16591,6 +16784,13 @@ function renderStages(kctx, pixi, matrices, record, view, now) {
16591
16784
  if (live) handFilters(kctx, pixi, record, view);
16592
16785
  const first = firstInputs(record.slots);
16593
16786
  let ok = renderStage1(kctx, pixi, matrices, record);
16787
+ if (drawsDirect(now)) {
16788
+ goDirect(kctx, record);
16789
+ account(kctx, record);
16790
+ if (!ok && live) putFiltersBack(kctx, record, view);
16791
+ return ok;
16792
+ }
16793
+ record.direct = false;
16594
16794
  if (ok && first !== void 0) renderStage2(kctx, pixi, matrices, record, {
16595
16795
  inputs: first.inputs,
16596
16796
  resolution: now.r,
@@ -16599,13 +16799,36 @@ function renderStages(kctx, pixi, matrices, record, view, now) {
16599
16799
  else if (record.s2Of !== void 0) releaseInputs(kctx, record);
16600
16800
  ok = ok && renderStage3(kctx, pixi, matrices, record, now);
16601
16801
  account(kctx, record);
16602
- if (!ok && live) {
16603
- if (record.standIn !== void 0) record.standIn.filters = null;
16604
- kctx.sync.applyFilters(view, filtersOf$1(kctx.ctx.state.sync, record.entity), noMark$1, record.top);
16605
- }
16802
+ if (!ok && live) putFiltersBack(kctx, record, view);
16606
16803
  return ok;
16607
16804
  }
16608
16805
  /**
16806
+ * Puts the filters of a live record whose take failed back on its top: it draws live again.
16807
+ *
16808
+ * @param kctx - The kept context.
16809
+ * @param record - The record.
16810
+ * @param view - Its view.
16811
+ */
16812
+ function putFiltersBack(kctx, record, view) {
16813
+ if (record.standIn !== void 0) record.standIn.filters = null;
16814
+ kctx.sync.applyFilters(view, filtersOf$1(kctx.ctx.state.sync, record.entity), noMark$1, record.top);
16815
+ }
16816
+ /**
16817
+ * Lets a record draw its subtree picture itself (F54): the pre-pass results and the filter output go, and the quad
16818
+ * shows the picture at the stack's alpha from now on.
16819
+ *
16820
+ * @param kctx - The kept context.
16821
+ * @param record - The record.
16822
+ */
16823
+ function goDirect(kctx, record) {
16824
+ if (record.s2Of !== void 0) releaseInputs(kctx, record);
16825
+ if (record.s3 !== void 0) {
16826
+ dropTexture(kctx, record.s3);
16827
+ record.s3 = void 0;
16828
+ }
16829
+ record.direct = true;
16830
+ }
16831
+ /**
16609
16832
  * Ends the loan of stage 2 when the first filter with inputs left the stack: the results are destroyed.
16610
16833
  *
16611
16834
  * @param kctx - The kept context.
@@ -16641,7 +16864,8 @@ function account(kctx, record) {
16641
16864
  function placeQuad(sync, pixi, matrices, record, view) {
16642
16865
  const { top, key } = record;
16643
16866
  const parent = top.parent;
16644
- const output = record.s3 ?? pixi.Texture.EMPTY;
16867
+ const output = (record.direct ? record.s1 : record.s3) ?? pixi.Texture.EMPTY;
16868
+ const alpha = record.direct ? NOW.alpha : 1;
16645
16869
  if (record.quad === void 0 || record.quad.destroyed) {
16646
16870
  record.quad = new pixi.Sprite(output);
16647
16871
  record.quad.label = `kept:${labelOf$3(top)}`;
@@ -16655,8 +16879,9 @@ function placeQuad(sync, pixi, matrices, record, view) {
16655
16879
  }
16656
16880
  if (quad.zIndex !== top.zIndex) quad.zIndex = top.zIndex;
16657
16881
  if (quad.blendMode !== top.blendMode) quad.blendMode = top.blendMode;
16882
+ if (quad.alpha !== alpha) quad.alpha = alpha;
16658
16883
  const inverse = parent.getGlobalTransform(matrices.parent, false).invert();
16659
- const Ps = key[KEY.Ps] ?? 0;
16884
+ const Ps = record.direct ? 0 : key[KEY.Ps] ?? 0;
16660
16885
  const x = (key[KEY.X0] ?? 0) - Ps + NOW.tx - (key[KEY.tx] ?? 0);
16661
16886
  const y = (key[KEY.Y0] ?? 0) - Ps + NOW.ty - (key[KEY.ty] ?? 0);
16662
16887
  const { local } = matrices;
@@ -16800,6 +17025,17 @@ function noteMove(kctx, record) {
16800
17025
  if (record.settle) kctx.demand.request();
16801
17026
  }
16802
17027
  /**
17028
+ * Tells whether the canvas has another size than at the take.
17029
+ *
17030
+ * @param kctx - The kept context.
17031
+ * @param key - The record's key.
17032
+ * @returns True when it does.
17033
+ */
17034
+ function canvasChanged(kctx, key) {
17035
+ const screen = kctx.host.screen();
17036
+ return screen !== void 0 && (screen.width !== key[KEY.sw] || screen.height !== key[KEY.sh]);
17037
+ }
17038
+ /**
16803
17039
  * A kept record's frame (P4.3): a stage-1 change retakes (three in a row go live and calm); otherwise a new chain
16804
17040
  * version redoes stages 2 and 3, a filter value, the resolution, the padding or (placed) a move stage 3; at rest after
16805
17041
  * an off-grid move one settle retake is owed.
@@ -16816,8 +17052,12 @@ function stepKept$1(kctx, pixi, matrices, record, view) {
16816
17052
  churn(kctx, pixi, matrices, record, view);
16817
17053
  return;
16818
17054
  }
16819
- record.churn = 0;
16820
17055
  const moved = NOW.tx !== key[KEY.lastTx] || NOW.ty !== key[KEY.lastTy];
17056
+ if (record.cut && (moved || canvasChanged(kctx, key))) {
17057
+ churn(kctx, pixi, matrices, record, view);
17058
+ return;
17059
+ }
17060
+ record.churn = 0;
16821
17061
  if (!moved && record.settle) {
16822
17062
  retakeOrGoLive(kctx, pixi, matrices, record, view);
16823
17063
  return;
@@ -16835,6 +17075,12 @@ function stepKept$1(kctx, pixi, matrices, record, view) {
16835
17075
  */
16836
17076
  function refresh$1(kctx, pixi, matrices, record) {
16837
17077
  const { key } = record;
17078
+ if (drawsDirect(NOW)) {
17079
+ if (record.direct) return;
17080
+ goDirect(kctx, record);
17081
+ account(kctx, record);
17082
+ return;
17083
+ }
16838
17084
  const inputs = firstInputs(record.slots)?.inputs;
16839
17085
  const stageTwoOwed = inputs !== void 0 && (record.dirtyS2 || inputs !== record.s2Of || inputs.version() !== record.s2InputsVersion || record.s1Version !== record.s2PictureVersion);
16840
17086
  const inputsLeft = inputs === void 0 && record.s2Of !== void 0;
@@ -16846,6 +17092,7 @@ function refresh$1(kctx, pixi, matrices, record) {
16846
17092
  padding: NOW.P
16847
17093
  });
16848
17094
  if (!stageThreeOwed) return;
17095
+ record.direct = false;
16849
17096
  renderStage3(kctx, pixi, matrices, record, NOW);
16850
17097
  account(kctx, record);
16851
17098
  if (!stageTwoOwed) kctx.state.refreshesOpen += 1;
@@ -16915,7 +17162,7 @@ function stepCalm(kctx, record) {
16915
17162
  function stepRecord(kctx, pixi, matrices, record, view) {
16916
17163
  noteDrawing(kctx, record, BEFORE);
16917
17164
  NOW.r = kctx.host.resolution();
16918
- readStack(record.slots, NOW.r, NOW);
17165
+ readStack(record.slots, NOW.r, pixi.AlphaFilter, NOW);
16919
17166
  if (eligible(kctx, record, view, NOW)) stepEligible(kctx, pixi, matrices, record, view);
16920
17167
  else stepIneligible(kctx, record, view);
16921
17168
  if (drawingChanged(kctx, record, BEFORE)) dirtyAbove(kctx, record);
@@ -17581,31 +17828,6 @@ function stopDemand(state) {
17581
17828
  state.verify.reference = void 0;
17582
17829
  }
17583
17830
  //#endregion
17584
- //#region src/plugins/renderer/monitor/clock.ts
17585
- /**
17586
- * @file renderer/monitor — the one renderer file that reads the page clock; lint L3 ignores it (eslint `ignores`). It
17587
- * measures the page in agent builds: the agent phase, a line's `t`, an event's time between frames. Never a game's
17588
- * state, whose time is `clock`.
17589
- */
17590
- /**
17591
- * Reads `globalThis.performance.now()` at call time, so a test stubs it.
17592
- *
17593
- * @returns Page-clock milliseconds.
17594
- */
17595
- function pageNow() {
17596
- return globalThis.performance.now();
17597
- }
17598
- /**
17599
- * Turns page-clock milliseconds into page epoch milliseconds, `performance.timeOrigin + ms`, rounded to 0.01 ms: the
17600
- * `t` of every log line.
17601
- *
17602
- * @param pageMs - Page-clock milliseconds.
17603
- * @returns Epoch milliseconds.
17604
- */
17605
- function epochOf(pageMs) {
17606
- return Math.round((globalThis.performance.timeOrigin + pageMs) * 100) / 100;
17607
- }
17608
- //#endregion
17609
17831
  //#region src/plugins/renderer/reasons.ts
17610
17832
  /**
17611
17833
  * @file renderer plugin — the reasons a frame is drawn, in ring order: the monitor stores a reason by its index. A
@@ -19265,9 +19487,9 @@ function wrapDevice(pc, device) {
19265
19487
  * @returns The pipeline.
19266
19488
  */
19267
19489
  value: function countedPipeline(descriptor) {
19268
- const started = pageNow();
19490
+ const started = pageNow$1();
19269
19491
  const made = createRenderPipeline.call(this, descriptor);
19270
- recordCompile(sinks, pc.perf.frame, descriptor.label ?? null, pageNow() - started);
19492
+ recordCompile(sinks, pc.perf.frame, descriptor.label ?? null, pageNow$1() - started);
19271
19493
  return made;
19272
19494
  }
19273
19495
  });
@@ -19282,9 +19504,9 @@ function wrapDevice(pc, device) {
19282
19504
  * @returns The module.
19283
19505
  */
19284
19506
  value: function countedModule(descriptor) {
19285
- const started = pageNow();
19507
+ const started = pageNow$1();
19286
19508
  const made = createShaderModule.call(this, descriptor);
19287
- recordModule(sinks, pc.perf.frame, descriptor.label ?? null, pageNow() - started);
19509
+ recordModule(sinks, pc.perf.frame, descriptor.label ?? null, pageNow$1() - started);
19288
19510
  return made;
19289
19511
  }
19290
19512
  });
@@ -19312,9 +19534,9 @@ function wrapGl(pc, gl) {
19312
19534
  * @param program - The program.
19313
19535
  */
19314
19536
  value: function countedLink(program) {
19315
- const started = pageNow();
19537
+ const started = pageNow$1();
19316
19538
  linkProgram.call(this, program);
19317
- recordCompile(sinks, pc.perf.frame, null, pageNow() - started);
19539
+ recordCompile(sinks, pc.perf.frame, null, pageNow$1() - started);
19318
19540
  }
19319
19541
  });
19320
19542
  return () => {
@@ -19980,7 +20202,7 @@ function headerLine(pc, thermal) {
19980
20202
  return {
19981
20203
  v: 1,
19982
20204
  kind: "header",
19983
- t: epochOf(pageNow()),
20205
+ t: epochOf(pageNow$1()),
19984
20206
  build: buildId(),
19985
20207
  timeOrigin: globalThis.performance.timeOrigin,
19986
20208
  engine: typeof __MOKU_GAME_ENGINE__ === "string" ? __MOKU_GAME_ENGINE__ : null,
@@ -20500,7 +20722,7 @@ function eventLine(name, detail, frame) {
20500
20722
  return {
20501
20723
  v: 1,
20502
20724
  kind: "event",
20503
- t: epochOf(pageNow()),
20725
+ t: epochOf(pageNow$1()),
20504
20726
  build: buildId(),
20505
20727
  frame,
20506
20728
  name,
@@ -20518,7 +20740,7 @@ function markLine(label, auto) {
20518
20740
  return {
20519
20741
  v: 1,
20520
20742
  kind: "mark",
20521
- t: epochOf(pageNow()),
20743
+ t: epochOf(pageNow$1()),
20522
20744
  build: buildId(),
20523
20745
  label,
20524
20746
  auto
@@ -20535,7 +20757,7 @@ function thermalLine(reading) {
20535
20757
  return {
20536
20758
  v: 1,
20537
20759
  kind: "thermal",
20538
- t: epochOf(pageNow()),
20760
+ t: epochOf(pageNow$1()),
20539
20761
  build: buildId(),
20540
20762
  status: reading.status,
20541
20763
  headroom: reading.headroom,
@@ -20677,7 +20899,7 @@ function buildStartup(pc) {
20677
20899
  return {
20678
20900
  v: 1,
20679
20901
  kind: "startup",
20680
- t: epochOf(pageNow()),
20902
+ t: epochOf(pageNow$1()),
20681
20903
  build: buildId(),
20682
20904
  htmlMs: responseEnd === void 0 ? null : roundTo(responseEnd, 2),
20683
20905
  script,
@@ -21104,7 +21326,7 @@ function writeKept(pc, slot) {
21104
21326
  * @param section - The `COST` index the time goes to.
21105
21327
  */
21106
21328
  function lap(perf, section) {
21107
- const now = pageNow();
21329
+ const now = pageNow$1();
21108
21330
  perf.sectionsMs[section] = cellF64(perf.sectionsMs, section) + now - cellF64(perf.staged, 2);
21109
21331
  perf.staged[2] = now;
21110
21332
  }
@@ -21496,7 +21718,7 @@ function commitFrame(pc, measure) {
21496
21718
  const sections = sinks.sections;
21497
21719
  const slot = ring.written % ring.capacity;
21498
21720
  const previous = ring.size === 0 ? -1 : ring.newest;
21499
- perf.staged[2] = pageNow();
21721
+ perf.staged[2] = pageNow$1();
21500
21722
  stageBudget(perf, measure);
21501
21723
  writeMeasure(pc, slot, measure);
21502
21724
  writeKept(pc, slot);
@@ -22116,7 +22338,7 @@ function notePauses(pc, reasons, paused, resumed) {
22116
22338
  const detail = { reasons: [...reasons] };
22117
22339
  if (paused) {
22118
22340
  writeEvent(pc, "pause", detail);
22119
- pc.perf.staged[1] = pageNow();
22341
+ pc.perf.staged[1] = pageNow$1();
22120
22342
  flushPerf(pc);
22121
22343
  } else if (resumed) {
22122
22344
  writeEvent(pc, "resume", detail);
@@ -22138,7 +22360,7 @@ function noteChange(perf, change) {
22138
22360
  * @param perf - The perf state.
22139
22361
  */
22140
22362
  function beginAgent(perf) {
22141
- if (Number.isNaN(perf.agentStart)) perf.agentStart = pageNow();
22363
+ if (Number.isNaN(perf.agentStart)) perf.agentStart = pageNow$1();
22142
22364
  }
22143
22365
  /**
22144
22366
  * Closes the open agent span: its time goes into the agent phase of the frame interval it falls in, and to
@@ -22149,7 +22371,7 @@ function beginAgent(perf) {
22149
22371
  function endAgent(pc) {
22150
22372
  const { perf } = pc;
22151
22373
  if (Number.isNaN(perf.agentStart)) return;
22152
- const span = pageNow() - perf.agentStart;
22374
+ const span = pageNow$1() - perf.agentStart;
22153
22375
  perf.agentStart = NaN;
22154
22376
  perf.agentMs += span;
22155
22377
  pc.ctx.deps.time.chargeAgent(span);
@@ -22192,7 +22414,7 @@ function attachFirst(pc) {
22192
22414
  writeEvent(pc, "device-restored", { kind: host.kind() });
22193
22415
  });
22194
22416
  host.onTrim((reason, freedMb) => {
22195
- perf.problems.trimAt = pageNow();
22417
+ perf.problems.trimAt = pageNow$1();
22196
22418
  writeEvent(pc, "pool-trim", {
22197
22419
  reason,
22198
22420
  freedMb
@@ -22534,6 +22756,17 @@ function createMonitorApi(ctx, deps) {
22534
22756
  }
22535
22757
  if (pc === void 0) sinks.inFrame = false;
22536
22758
  };
22759
+ /**
22760
+ * The agent span: `beginAgent` and `endAgent`, also the span the captures are taken in.
22761
+ */
22762
+ const agentSpan = {
22763
+ begin: () => {
22764
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) beginAgent(pc.perf);
22765
+ },
22766
+ end: () => {
22767
+ if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) endAgent(pc);
22768
+ }
22769
+ };
22537
22770
  return {
22538
22771
  stats: () => {
22539
22772
  const usage = deps.host.textures();
@@ -22587,12 +22820,8 @@ function createMonitorApi(ctx, deps) {
22587
22820
  event: (name, detail = NO_DETAIL) => {
22588
22821
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) writeEvent(pc, name, detail);
22589
22822
  },
22590
- beginAgent: () => {
22591
- if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) beginAgent(pc.perf);
22592
- },
22593
- endAgent: () => {
22594
- if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) endAgent(pc);
22595
- },
22823
+ beginAgent: agentSpan.begin,
22824
+ endAgent: agentSpan.end,
22596
22825
  sections: (next) => {
22597
22826
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) {
22598
22827
  if (next === void 0) return pc.perf.sections;
@@ -22626,7 +22855,7 @@ function createMonitorApi(ctx, deps) {
22626
22855
  state.lastDrawn = drawn;
22627
22856
  notePasses(drawn);
22628
22857
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) pc.perf.drawn = drawn;
22629
- if (drawn) serveCaptures(mctx);
22858
+ if (drawn) serveCaptures(mctx, agentSpan);
22630
22859
  },
22631
22860
  waiting: () => state.captures.length > 0,
22632
22861
  changed: (change) => {
@@ -22647,7 +22876,7 @@ function createMonitorApi(ctx, deps) {
22647
22876
  },
22648
22877
  flush: () => {
22649
22878
  if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && pc !== void 0) {
22650
- pc.perf.staged[1] = pageNow();
22879
+ pc.perf.staged[1] = pageNow$1();
22651
22880
  flushPerf(pc);
22652
22881
  }
22653
22882
  },
@@ -23728,7 +23957,7 @@ function variantsOf(sctx) {
23728
23957
  * @param subject - The subject.
23729
23958
  * @param variants - Its variants.
23730
23959
  */
23731
- function enqueue(warm, subject, variants) {
23960
+ function enqueue$1(warm, subject, variants) {
23732
23961
  for (const variant of variants) warm.queue.push({
23733
23962
  subject,
23734
23963
  variant
@@ -23770,7 +23999,7 @@ function abandon(warm) {
23770
23999
  *
23771
24000
  * @param sctx - Domain context of the sync module.
23772
24001
  */
23773
- function startWarm$1(sctx) {
24002
+ function startWarm$2(sctx) {
23774
24003
  const state = sctx.ctx.state.sync;
23775
24004
  const { warm } = state;
23776
24005
  dropObjects(warm);
@@ -23781,12 +24010,12 @@ function startWarm$1(sctx) {
23781
24010
  warm.failed = false;
23782
24011
  if (!canWarm(sctx)) return;
23783
24012
  const variants = variantsOf(sctx);
23784
- for (const subject of CONTENT) enqueue(warm, subject, variants);
24013
+ for (const subject of CONTENT) enqueue$1(warm, subject, variants);
23785
24014
  const [font] = state.fonts.keys();
23786
24015
  if (font !== void 0) {
23787
24016
  warm.textFont = font;
23788
24017
  warm.textQueued = true;
23789
- enqueue(warm, "text", variants);
24018
+ enqueue$1(warm, "text", variants);
23790
24019
  }
23791
24020
  for (const filter of warm.filters) {
23792
24021
  const entry = warm.waiting.get(filter);
@@ -23795,7 +24024,7 @@ function startWarm$1(sctx) {
23795
24024
  resolve: []
23796
24025
  });
23797
24026
  else entry.left = variants.length;
23798
- enqueue(warm, filter, variants);
24027
+ enqueue$1(warm, filter, variants);
23799
24028
  }
23800
24029
  sctx.deps.demand.request();
23801
24030
  }
@@ -23843,7 +24072,7 @@ function warmFilter(sctx, filter) {
23843
24072
  entry.left = variants.length;
23844
24073
  warm.filters.add(filter);
23845
24074
  warm.waiting.set(filter, entry);
23846
- enqueue(warm, filter, variants);
24075
+ enqueue$1(warm, filter, variants);
23847
24076
  sctx.deps.demand.request();
23848
24077
  }
23849
24078
  return new Promise((resolve) => {
@@ -24153,6 +24382,419 @@ function clearFonts(state) {
24153
24382
  state.fontCache = void 0;
24154
24383
  }
24155
24384
  //#endregion
24385
+ //#region src/plugins/renderer/sync/outline.ts
24386
+ /** Above every child of the wrapper, whatever `Order` a child carries. */
24387
+ const OUTLINE_Z = Number.MAX_SAFE_INTEGER;
24388
+ const CYAN = 65535;
24389
+ const RED = 16711680;
24390
+ /**
24391
+ * Writes the slice widths a texture asks for, in the units it is drawn in: its default borders divided
24392
+ * by the resolution of its source. A texture without borders gets 0 on every side, never Pixi's 10. Written in place,
24393
+ * so a nine-slice write allocates nothing.
24394
+ *
24395
+ * @param texture - The texture a nine-slice draws, or `undefined` while nothing can draw.
24396
+ * @param out - The widths to write.
24397
+ * @returns `out`.
24398
+ */
24399
+ function bordersInto(texture, out) {
24400
+ const borders = texture?.defaultBorders;
24401
+ if (texture === void 0 || borders === void 0) {
24402
+ out.left = 0;
24403
+ out.top = 0;
24404
+ out.right = 0;
24405
+ out.bottom = 0;
24406
+ return out;
24407
+ }
24408
+ const resolution = texture.source.resolution > 0 ? texture.source.resolution : 1;
24409
+ out.left = borders.left / resolution;
24410
+ out.top = borders.top / resolution;
24411
+ out.right = borders.right / resolution;
24412
+ out.bottom = borders.bottom / resolution;
24413
+ return out;
24414
+ }
24415
+ /**
24416
+ * The factor Pixi shrinks the corners of a nine-slice by when two of them would overlap.
24417
+ *
24418
+ * @param size - The drawn size.
24419
+ * @param size.width - Drawn width.
24420
+ * @param size.height - Drawn height.
24421
+ * @param borders - The slice widths.
24422
+ * @returns 1 when the corners fit, less when they overlap.
24423
+ * @example
24424
+ * ```ts
24425
+ * sliceScale({ width: 100, height: 200 }, { left: 72, top: 76, right: 72, bottom: 76 }); // 0.6944
24426
+ * ```
24427
+ */
24428
+ function sliceScale(size, borders) {
24429
+ const across = borders.left + borders.right;
24430
+ const down = borders.top + borders.bottom;
24431
+ const scaleX = across > 0 && size.width < across ? size.width / across : 1;
24432
+ const scaleY = down > 0 && size.height < down ? size.height / down : 1;
24433
+ return Math.min(scaleX, scaleY);
24434
+ }
24435
+ /**
24436
+ * Strokes the bounds of a nine-slice and the four lines Pixi cuts it at, into the wrapper of its
24437
+ * view: cyan, red when the corners overlap or the texture is missing. The graphics is kept on the
24438
+ * view and redrawn in place.
24439
+ *
24440
+ * @param pixi - The Pixi module the renderer loaded.
24441
+ * @param wrapper - The wrapper of the view, which carries its pose.
24442
+ * @param view - The view of the nine-slice.
24443
+ * @param shape - Size, borders and whether the texture is missing.
24444
+ */
24445
+ function drawOutline(pixi, wrapper, view, shape) {
24446
+ const outline = view.outline ?? new pixi.Graphics();
24447
+ const { width, height, borders } = shape;
24448
+ const scale = sliceScale(shape, borders);
24449
+ const left = borders.left * scale;
24450
+ const right = width - borders.right * scale;
24451
+ const top = borders.top * scale;
24452
+ const bottom = height - borders.bottom * scale;
24453
+ outline.label = `outline#${shape.entity}`;
24454
+ outline.zIndex = OUTLINE_Z;
24455
+ outline.clear();
24456
+ outline.rect(0, 0, width, height).moveTo(left, 0).lineTo(left, height).moveTo(right, 0).lineTo(right, height).moveTo(0, top).lineTo(width, top).moveTo(0, bottom).lineTo(width, bottom);
24457
+ outline.stroke({
24458
+ color: shape.missing || scale < 1 ? RED : CYAN,
24459
+ width: 1,
24460
+ pixelLine: true
24461
+ });
24462
+ if (outline.parent !== wrapper) wrapper.addChild(outline);
24463
+ view.outline = outline;
24464
+ }
24465
+ //#endregion
24466
+ //#region src/plugins/renderer/sync/hollow.ts
24467
+ /** The vertices across and down: the four cut lines of a nine-slice on each axis. */
24468
+ const SIDE = 4;
24469
+ /** The cells of the 3 × 3 slicing on each axis. */
24470
+ const CELLS = 3;
24471
+ /** The cell left out, row by row: the centre, (1, 1). */
24472
+ const CENTRE = 4;
24473
+ /** Indices of one quad: two triangles. */
24474
+ const QUAD = 6;
24475
+ /**
24476
+ * The index buffer of the rim: the two triangles of every cell of the 3 × 3 slicing but the centre, in Pixi's
24477
+ * `PlaneGeometry` winding. Static: written once per geometry.
24478
+ *
24479
+ * @returns 48 indices into the 16 vertices.
24480
+ * @example
24481
+ * ```ts
24482
+ * rimIndices().length; // 48
24483
+ * ```
24484
+ */
24485
+ function rimIndices() {
24486
+ const indices = /* @__PURE__ */ new Uint32Array(48);
24487
+ let at = 0;
24488
+ for (let cell = 0; cell < 9; cell += 1) {
24489
+ if (cell === CENTRE) continue;
24490
+ const topLeft = Math.floor(cell / CELLS) * SIDE + cell % CELLS;
24491
+ const bottomLeft = topLeft + SIDE;
24492
+ indices.set([
24493
+ topLeft,
24494
+ topLeft + 1,
24495
+ bottomLeft,
24496
+ topLeft + 1,
24497
+ bottomLeft + 1,
24498
+ bottomLeft
24499
+ ], at);
24500
+ at += QUAD;
24501
+ }
24502
+ return indices;
24503
+ }
24504
+ /**
24505
+ * Writes one row of the grid: its four vertices, one per cut line across, at the same `y`.
24506
+ *
24507
+ * @param target - The 32 numbers of the grid: x and y of each vertex, row by row.
24508
+ * @param row - The row, 0 to 3.
24509
+ * @param y - The row's line down.
24510
+ * @param across - The cut lines across.
24511
+ */
24512
+ function writeRow(target, row, y, across) {
24513
+ const at = row * SIDE * 2;
24514
+ target[at] = across.start;
24515
+ target[at + 1] = y;
24516
+ target[at + 2] = across.near;
24517
+ target[at + 3] = y;
24518
+ target[at + 4] = across.far;
24519
+ target[at + 5] = y;
24520
+ target[at + 6] = across.end;
24521
+ target[at + 7] = y;
24522
+ }
24523
+ /**
24524
+ * Writes the 4 × 4 grid of the cut lines in place: Pixi's vertex order, row by row.
24525
+ *
24526
+ * @param target - The 32 numbers of the grid.
24527
+ * @param across - The cut lines across.
24528
+ * @param down - The cut lines down.
24529
+ */
24530
+ function writeGrid(target, across, down) {
24531
+ writeRow(target, 0, down.start, across);
24532
+ writeRow(target, 1, down.near, across);
24533
+ writeRow(target, 2, down.far, across);
24534
+ writeRow(target, 3, down.end, across);
24535
+ }
24536
+ /**
24537
+ * Makes the class of the rim-only nine-slice over one loaded Pixi module.
24538
+ *
24539
+ * @param pixi - The Pixi module the renderer loaded.
24540
+ * @returns The class.
24541
+ */
24542
+ function createHollowClass(pixi) {
24543
+ /** A nine-slice that draws its rim alone: Pixi's mesh over the 16 vertices of the slicing and its eight border cells. */
24544
+ class HollowNineSlice extends pixi.Mesh {
24545
+ /** Tells the rim-only object from Pixi's `NineSliceSprite`. */
24546
+ hollow = true;
24547
+ /** The slice widths, in the units the panel is drawn in. */
24548
+ borders = {
24549
+ left: 0,
24550
+ top: 0,
24551
+ right: 0,
24552
+ bottom: 0
24553
+ };
24554
+ /** The cut lines across, scratch of the next write. */
24555
+ across = {
24556
+ start: 0,
24557
+ near: 0,
24558
+ far: 0,
24559
+ end: 0
24560
+ };
24561
+ /** The cut lines down, scratch of the next write. */
24562
+ down = {
24563
+ start: 0,
24564
+ near: 0,
24565
+ far: 0,
24566
+ end: 0
24567
+ };
24568
+ /** The panel's width the positions are laid out for. */
24569
+ boxWidth = 0;
24570
+ /** The panel's height the positions are laid out for. */
24571
+ boxHeight = 0;
24572
+ /** False while Pixi's constructor writes the first texture: the geometry does not exist yet. */
24573
+ built = false;
24574
+ /**
24575
+ * Builds the mesh over a geometry of its own, 0 × 0 with no border, its uvs cut for the texture.
24576
+ *
24577
+ * @param texture - The texture it draws first.
24578
+ */
24579
+ constructor(texture) {
24580
+ super({
24581
+ geometry: new pixi.MeshGeometry({
24582
+ positions: /* @__PURE__ */ new Float32Array(32),
24583
+ uvs: /* @__PURE__ */ new Float32Array(32),
24584
+ indices: rimIndices()
24585
+ }),
24586
+ texture
24587
+ });
24588
+ this.built = true;
24589
+ this.layoutUvs();
24590
+ }
24591
+ /**
24592
+ * The width of the left border column.
24593
+ *
24594
+ * @returns Units.
24595
+ */
24596
+ get leftWidth() {
24597
+ return this.borders.left;
24598
+ }
24599
+ /**
24600
+ * Sets the width of the left border column: the positions and the uvs are written again.
24601
+ *
24602
+ * @param value - Units.
24603
+ */
24604
+ set leftWidth(value) {
24605
+ if (this.borders.left === value) return;
24606
+ this.borders.left = value;
24607
+ this.layoutAll();
24608
+ }
24609
+ /**
24610
+ * The height of the top border row.
24611
+ *
24612
+ * @returns Units.
24613
+ */
24614
+ get topHeight() {
24615
+ return this.borders.top;
24616
+ }
24617
+ /**
24618
+ * Sets the height of the top border row: the positions and the uvs are written again.
24619
+ *
24620
+ * @param value - Units.
24621
+ */
24622
+ set topHeight(value) {
24623
+ if (this.borders.top === value) return;
24624
+ this.borders.top = value;
24625
+ this.layoutAll();
24626
+ }
24627
+ /**
24628
+ * The width of the right border column.
24629
+ *
24630
+ * @returns Units.
24631
+ */
24632
+ get rightWidth() {
24633
+ return this.borders.right;
24634
+ }
24635
+ /**
24636
+ * Sets the width of the right border column: the positions and the uvs are written again.
24637
+ *
24638
+ * @param value - Units.
24639
+ */
24640
+ set rightWidth(value) {
24641
+ if (this.borders.right === value) return;
24642
+ this.borders.right = value;
24643
+ this.layoutAll();
24644
+ }
24645
+ /**
24646
+ * The height of the bottom border row.
24647
+ *
24648
+ * @returns Units.
24649
+ */
24650
+ get bottomHeight() {
24651
+ return this.borders.bottom;
24652
+ }
24653
+ /**
24654
+ * Sets the height of the bottom border row: the positions and the uvs are written again.
24655
+ *
24656
+ * @param value - Units.
24657
+ */
24658
+ set bottomHeight(value) {
24659
+ if (this.borders.bottom === value) return;
24660
+ this.borders.bottom = value;
24661
+ this.layoutAll();
24662
+ }
24663
+ /**
24664
+ * The width of the panel, as on a `NineSliceSprite`: its box, never a scale.
24665
+ *
24666
+ * @returns Units.
24667
+ */
24668
+ get width() {
24669
+ return this.boxWidth;
24670
+ }
24671
+ /**
24672
+ * Sets the width of the panel: the positions are written again.
24673
+ *
24674
+ * @param value - Units.
24675
+ */
24676
+ set width(value) {
24677
+ if (this.boxWidth === value) return;
24678
+ this.boxWidth = value;
24679
+ this.layoutPositions();
24680
+ }
24681
+ /**
24682
+ * The height of the panel, as on a `NineSliceSprite`: its box, never a scale.
24683
+ *
24684
+ * @returns Units.
24685
+ */
24686
+ get height() {
24687
+ return this.boxHeight;
24688
+ }
24689
+ /**
24690
+ * Sets the height of the panel: the positions are written again.
24691
+ *
24692
+ * @param value - Units.
24693
+ */
24694
+ set height(value) {
24695
+ if (this.boxHeight === value) return;
24696
+ this.boxHeight = value;
24697
+ this.layoutPositions();
24698
+ }
24699
+ /**
24700
+ * The texture the rim is cut from.
24701
+ *
24702
+ * @returns The texture.
24703
+ */
24704
+ get texture() {
24705
+ return super.texture;
24706
+ }
24707
+ /**
24708
+ * Sets the texture as Pixi's mesh does; the uvs are cut for its frame.
24709
+ *
24710
+ * @param value - The texture.
24711
+ */
24712
+ set texture(value) {
24713
+ super.texture = value;
24714
+ if (this.built) this.layoutUvs();
24715
+ }
24716
+ /**
24717
+ * Frees the mesh as Pixi does, and its own geometry with it.
24718
+ *
24719
+ * @param options - Pixi's destroy options; a texture is freed only when they ask.
24720
+ */
24721
+ destroy(options) {
24722
+ if (this.destroyed) return;
24723
+ const geometry = this.geometry;
24724
+ super.destroy(options);
24725
+ geometry.destroy(true);
24726
+ }
24727
+ /** Writes the positions and the uvs again: a border changed. */
24728
+ layoutAll() {
24729
+ this.layoutPositions();
24730
+ this.layoutUvs();
24731
+ }
24732
+ /**
24733
+ * Writes the 16 positions in place: the cut lines at the borders, all four shrunk by one factor when two corners
24734
+ * would overlap, as Pixi's `NineSliceGeometry` shrinks them.
24735
+ */
24736
+ layoutPositions() {
24737
+ const across = this.across;
24738
+ const down = this.down;
24739
+ const borders = this.borders;
24740
+ const scale = sliceScale(this, borders);
24741
+ across.start = 0;
24742
+ across.near = borders.left * scale;
24743
+ across.far = this.boxWidth - borders.right * scale;
24744
+ across.end = this.boxWidth;
24745
+ down.start = 0;
24746
+ down.near = borders.top * scale;
24747
+ down.far = this.boxHeight - borders.bottom * scale;
24748
+ down.end = this.boxHeight;
24749
+ writeGrid(this.geometry.positions, across, down);
24750
+ this.geometry.getBuffer("aPosition").update();
24751
+ }
24752
+ /**
24753
+ * Writes the 16 uvs in place: the texture's frame (its trim, when it has one) cut at the unscaled borders, as
24754
+ * Pixi's `NineSliceGeometry` cuts it. Pixi maps them into an atlas page when it batches the mesh.
24755
+ */
24756
+ layoutUvs() {
24757
+ const across = this.across;
24758
+ const down = this.down;
24759
+ const borders = this.borders;
24760
+ const texture = this.texture;
24761
+ const width = texture.width;
24762
+ const height = texture.height;
24763
+ const trim = texture.trim;
24764
+ const x = trim?.x ?? 0;
24765
+ const y = trim?.y ?? 0;
24766
+ const u0 = x / width;
24767
+ const v0 = y / height;
24768
+ const u1 = (x + (trim?.width ?? width)) / width;
24769
+ const v1 = (y + (trim?.height ?? height)) / height;
24770
+ const texelU = 1 / width;
24771
+ const texelV = 1 / height;
24772
+ across.start = u0;
24773
+ across.near = u0 + texelU * borders.left;
24774
+ across.far = u1 - texelU * borders.right;
24775
+ across.end = u1;
24776
+ down.start = v0;
24777
+ down.near = v0 + texelV * borders.top;
24778
+ down.far = v1 - texelV * borders.bottom;
24779
+ down.end = v1;
24780
+ writeGrid(this.geometry.uvs, across, down);
24781
+ this.geometry.getBuffer("aUV").update();
24782
+ }
24783
+ }
24784
+ return HollowNineSlice;
24785
+ }
24786
+ /**
24787
+ * Builds a rim-only nine-slice of Pixi's white texture, its class made at the first one over the loaded module.
24788
+ *
24789
+ * @param state - The sync branch of the plugin state, which keeps the class.
24790
+ * @param pixi - The Pixi module the renderer loaded.
24791
+ * @returns The object.
24792
+ */
24793
+ function makeHollow(state, pixi) {
24794
+ state.hollowClass ??= createHollowClass(pixi);
24795
+ return new state.hollowClass(pixi.Texture.WHITE);
24796
+ }
24797
+ //#endregion
24156
24798
  //#region src/plugins/renderer/sync/labels.ts
24157
24799
  /**
24158
24800
  * Builds the label of one view.
@@ -24189,15 +24831,25 @@ function isGraphics(object) {
24189
24831
  return "roundRect" in object;
24190
24832
  }
24191
24833
  /**
24192
- * Tells whether a display object is a nine-slice sprite.
24834
+ * Tells whether a display object draws a nine-slice: Pixi's `NineSliceSprite`, or the engine's rim-only mesh of a
24835
+ * `hollow` one. Both carry the slice widths.
24193
24836
  *
24194
24837
  * @param object - The display object.
24195
- * @returns True for a nine-slice sprite.
24838
+ * @returns True for either nine-slice object.
24196
24839
  */
24197
24840
  function isNineSlice(object) {
24198
24841
  return "leftWidth" in object;
24199
24842
  }
24200
24843
  /**
24844
+ * Tells whether a display object is the engine's rim-only nine-slice (F59).
24845
+ *
24846
+ * @param object - The display object.
24847
+ * @returns True for the hollow mesh, false for Pixi's `NineSliceSprite` and every other object.
24848
+ */
24849
+ function isHollow(object) {
24850
+ return "hollow" in object;
24851
+ }
24852
+ /**
24201
24853
  * Takes a display object out of the Pixi tree. Pixi containers are not DOM nodes, so the DOM
24202
24854
  * helper the linter suggests does not exist on them.
24203
24855
  *
@@ -24248,18 +24900,20 @@ function dropOutline(view) {
24248
24900
  view.outline = void 0;
24249
24901
  }
24250
24902
  /**
24251
- * The pool an object belongs to: its class and the texture it draws.
24903
+ * The pool an object belongs to: its class and the texture it draws. A rim-only nine-slice has a pool of its own, so
24904
+ * a filled panel never gets a hollow object back, nor the other way round.
24252
24905
  *
24253
24906
  * @param kind - Which component gave the entity its display object.
24254
24907
  * @param textureKey - The asset key.
24908
+ * @param hollow - True for the rim-only object of a `NineSlice` with `hollow: true`.
24255
24909
  * @returns The pool key.
24256
24910
  * @example
24257
24911
  * ```ts
24258
- * poolKeyOf("Sprite", "board.cell"); // "Sprite:board.cell"
24912
+ * poolKeyOf("NineSlice", "glass.frost", true); // "NineSlice.hollow:glass.frost"
24259
24913
  * ```
24260
24914
  */
24261
- function poolKeyOf(kind, textureKey) {
24262
- return `${kind}:${textureKey}`;
24915
+ function poolKeyOf(kind, textureKey, hollow) {
24916
+ return hollow ? `${kind}.hollow:${textureKey}` : `${kind}:${textureKey}`;
24263
24917
  }
24264
24918
  /**
24265
24919
  * Brings the object of a view that left back to the state a fresh one has: detached, drawn, opaque, unmoved, white.
@@ -24269,9 +24923,17 @@ function poolKeyOf(kind, textureKey) {
24269
24923
  * @param view - The view whose object goes back to its pool; its wrapper is gone already.
24270
24924
  */
24271
24925
  function resetObject(view) {
24272
- const object = view.object;
24273
- detach$1(object);
24926
+ detach$1(view.object);
24274
24927
  clearHidden(view);
24928
+ resetNode(view.object);
24929
+ }
24930
+ /**
24931
+ * Brings a detached object back to a fresh one's pose and look: drawn, opaque, unmoved, white for a sprite and a
24932
+ * graphics.
24933
+ *
24934
+ * @param object - The object, out of the tree.
24935
+ */
24936
+ function resetNode(object) {
24275
24937
  object.visible = true;
24276
24938
  object.alpha = 1;
24277
24939
  object.rotation = 0;
@@ -24321,7 +24983,6 @@ function oldestPool(state) {
24321
24983
  * @param view - The view that left.
24322
24984
  */
24323
24985
  function release$1(sctx, view) {
24324
- const state = sctx.ctx.state.sync;
24325
24986
  dropMask(view);
24326
24987
  dropOutline(view);
24327
24988
  clearFilters(view.object);
@@ -24344,15 +25005,50 @@ function release$1(sctx, view) {
24344
25005
  view.display?.adapter.destroy(view.object);
24345
25006
  return;
24346
25007
  }
24347
- const groups = sctx.ctx.state.kept.madeGroups;
24348
- if (groups.has(view.object)) {
24349
- view.object.disableRenderGroup();
24350
- groups.delete(view.object);
24351
- }
25008
+ dropGroup(sctx, view.object);
24352
25009
  resetObject(view);
24353
- const pool = state.pools.get(view.poolKey) ?? emptyPool();
24354
- pool.push(view.object);
24355
- state.pools.set(view.poolKey, pool);
25010
+ store$1(sctx, view.object, view.poolKey);
25011
+ }
25012
+ /**
25013
+ * Puts an object a view no longer draws with back into a pool, while the view goes on with another one: the
25014
+ * nine-slice whose `hollow` flipped (F59). Reset as `release` resets a view's object; its view keeps its hidden
25015
+ * reasons, which hang on the view's wrapper.
25016
+ *
25017
+ * @param sctx - Domain context of the sync module.
25018
+ * @param object - The object, still in the view's wrapper.
25019
+ * @param poolKey - The pool it belongs to.
25020
+ */
25021
+ function releaseObject(sctx, object, poolKey) {
25022
+ dropGroup(sctx, object);
25023
+ detach$1(object);
25024
+ resetNode(object);
25025
+ store$1(sctx, object, poolKey);
25026
+ }
25027
+ /**
25028
+ * Takes away the render group the kept cache made of an object (spec 23 P9.3), before it is pooled.
25029
+ *
25030
+ * @param sctx - Domain context of the sync module.
25031
+ * @param object - The object.
25032
+ */
25033
+ function dropGroup(sctx, object) {
25034
+ const groups = sctx.ctx.state.kept.madeGroups;
25035
+ if (!groups.has(object)) return;
25036
+ object.disableRenderGroup();
25037
+ groups.delete(object);
25038
+ }
25039
+ /**
25040
+ * Pushes a reset object into its pool and evicts the one that waited longest while the pools hold more than
25041
+ * `poolLimit`.
25042
+ *
25043
+ * @param sctx - Domain context of the sync module.
25044
+ * @param object - The reset object.
25045
+ * @param poolKey - Its pool.
25046
+ */
25047
+ function store$1(sctx, object, poolKey) {
25048
+ const state = sctx.ctx.state.sync;
25049
+ const pool = state.pools.get(poolKey) ?? emptyPool();
25050
+ pool.push(object);
25051
+ state.pools.set(poolKey, pool);
24356
25052
  state.pooled += 1;
24357
25053
  while (state.pooled > sctx.ctx.config.poolLimit) {
24358
25054
  const oldest = oldestPool(state);
@@ -24363,21 +25059,32 @@ function release$1(sctx, view) {
24363
25059
  }
24364
25060
  /**
24365
25061
  * Destroys the pooled objects that draw one of these keys, so a bundle that left frees its GPU
24366
- * memory instead of waiting for a reuse that resolves nothing.
25062
+ * memory instead of waiting for a reuse that resolves nothing: sprites, nine-slices and rim-only nine-slices.
24367
25063
  *
24368
25064
  * @param state - The sync branch of the plugin state.
24369
25065
  * @param keys - The asset keys that went stale.
24370
25066
  */
24371
25067
  function dropPooled(state, keys) {
24372
- for (const key of keys) for (const kind of ["Sprite", "NineSlice"]) {
24373
- const pool = state.pools.get(poolKeyOf(kind, key));
24374
- if (pool === void 0) continue;
24375
- for (const object of pool) object.destroy({ texture: false });
24376
- state.pooled -= pool.length;
24377
- state.pools.delete(poolKeyOf(kind, key));
25068
+ for (const key of keys) {
25069
+ dropPool(state, poolKeyOf("Sprite", key, false));
25070
+ dropPool(state, poolKeyOf("NineSlice", key, false));
25071
+ dropPool(state, poolKeyOf("NineSlice", key, true));
24378
25072
  }
24379
25073
  }
24380
25074
  /**
25075
+ * Destroys one pool and everything in it.
25076
+ *
25077
+ * @param state - The sync branch of the plugin state.
25078
+ * @param poolKey - The pool.
25079
+ */
25080
+ function dropPool(state, poolKey) {
25081
+ const pool = state.pools.get(poolKey);
25082
+ if (pool === void 0) return;
25083
+ for (const object of pool) object.destroy({ texture: false });
25084
+ state.pooled -= pool.length;
25085
+ state.pools.delete(poolKey);
25086
+ }
25087
+ /**
24381
25088
  * Destroys everything the pools hold. Works on the state alone, because `onStop` has no context.
24382
25089
  *
24383
25090
  * @param state - The sync branch of the plugin state.
@@ -24543,85 +25250,6 @@ function clearLayers(state) {
24543
25250
  state.hudLayer = void 0;
24544
25251
  }
24545
25252
  //#endregion
24546
- //#region src/plugins/renderer/sync/outline.ts
24547
- /** Above every child of the wrapper, whatever `Order` a child carries. */
24548
- const OUTLINE_Z = Number.MAX_SAFE_INTEGER;
24549
- const CYAN = 65535;
24550
- const RED = 16711680;
24551
- /**
24552
- * The slice widths a texture asks for, in the units it is drawn in: its default borders divided
24553
- * by the resolution of its source. A texture without borders gets 0 on every side, never Pixi's 10.
24554
- *
24555
- * @param texture - The texture a nine-slice draws, or `undefined` while nothing can draw.
24556
- * @returns The four widths.
24557
- */
24558
- function bordersOf(texture) {
24559
- const borders = texture?.defaultBorders;
24560
- if (texture === void 0 || borders === void 0) return {
24561
- left: 0,
24562
- top: 0,
24563
- right: 0,
24564
- bottom: 0
24565
- };
24566
- const resolution = texture.source.resolution > 0 ? texture.source.resolution : 1;
24567
- return {
24568
- left: borders.left / resolution,
24569
- top: borders.top / resolution,
24570
- right: borders.right / resolution,
24571
- bottom: borders.bottom / resolution
24572
- };
24573
- }
24574
- /**
24575
- * The factor Pixi shrinks the corners of a nine-slice by when two of them would overlap.
24576
- *
24577
- * @param size - The drawn size.
24578
- * @param size.width - Drawn width.
24579
- * @param size.height - Drawn height.
24580
- * @param borders - The slice widths.
24581
- * @returns 1 when the corners fit, less when they overlap.
24582
- * @example
24583
- * ```ts
24584
- * sliceScale({ width: 100, height: 200 }, { left: 72, top: 76, right: 72, bottom: 76 }); // 0.6944
24585
- * ```
24586
- */
24587
- function sliceScale(size, borders) {
24588
- const across = borders.left + borders.right;
24589
- const down = borders.top + borders.bottom;
24590
- const scaleX = across > 0 && size.width < across ? size.width / across : 1;
24591
- const scaleY = down > 0 && size.height < down ? size.height / down : 1;
24592
- return Math.min(scaleX, scaleY);
24593
- }
24594
- /**
24595
- * Strokes the bounds of a nine-slice and the four lines Pixi cuts it at, into the wrapper of its
24596
- * view: cyan, red when the corners overlap or the texture is missing. The graphics is kept on the
24597
- * view and redrawn in place.
24598
- *
24599
- * @param pixi - The Pixi module the renderer loaded.
24600
- * @param wrapper - The wrapper of the view, which carries its pose.
24601
- * @param view - The view of the nine-slice.
24602
- * @param shape - Size, borders and whether the texture is missing.
24603
- */
24604
- function drawOutline(pixi, wrapper, view, shape) {
24605
- const outline = view.outline ?? new pixi.Graphics();
24606
- const { width, height, borders } = shape;
24607
- const scale = sliceScale(shape, borders);
24608
- const left = borders.left * scale;
24609
- const right = width - borders.right * scale;
24610
- const top = borders.top * scale;
24611
- const bottom = height - borders.bottom * scale;
24612
- outline.label = `outline#${shape.entity}`;
24613
- outline.zIndex = OUTLINE_Z;
24614
- outline.clear();
24615
- outline.rect(0, 0, width, height).moveTo(left, 0).lineTo(left, height).moveTo(right, 0).lineTo(right, height).moveTo(0, top).lineTo(width, top).moveTo(0, bottom).lineTo(width, bottom);
24616
- outline.stroke({
24617
- color: shape.missing || scale < 1 ? RED : CYAN,
24618
- width: 1,
24619
- pixelLine: true
24620
- });
24621
- if (outline.parent !== wrapper) wrapper.addChild(outline);
24622
- view.outline = outline;
24623
- }
24624
- //#endregion
24625
25253
  //#region src/plugins/renderer/sync/overlay-range.ts
24626
25254
  /** A range retaken for a mark in this many frames in a row is drawn live instead (O8.4; spec 23's `CHURN_LIMIT`). */
24627
25255
  const RETAKE_STREAK = 3;
@@ -26085,6 +26713,144 @@ function destroyTexture(state, texture) {
26085
26713
  texture.destroy(true);
26086
26714
  }
26087
26715
  //#endregion
26716
+ //#region src/plugins/renderer/sync/upload.ts
26717
+ /** The calls of a source nobody waits for: none. */
26718
+ const NOBODY = Object.freeze([]);
26719
+ /**
26720
+ * The resolve of a waiter before its promise exists: answers nobody.
26721
+ */
26722
+ function unanswered() {}
26723
+ /**
26724
+ * Creates the set of the sources uploaded on a device. It lives in its own non-exported function because lint rule L5
26725
+ * refuses a collection built inside an exported declaration.
26726
+ *
26727
+ * @returns An empty weak set.
26728
+ */
26729
+ function emptyDone() {
26730
+ return /* @__PURE__ */ new WeakSet();
26731
+ }
26732
+ /**
26733
+ * Counts one source for a call: queued once whoever names it first, waited for once by each call, however many of its
26734
+ * textures share it. A source uploaded on this device, or destroyed, is not waited for.
26735
+ *
26736
+ * @param upload - The upload branch of the sync state.
26737
+ * @param source - The source of one texture of the call.
26738
+ * @param waiter - The call.
26739
+ */
26740
+ function enqueue(upload, source, waiter) {
26741
+ if (source.destroyed || upload.done.has(source)) return;
26742
+ let waiters = upload.waiting.get(source);
26743
+ if (waiters === void 0) {
26744
+ waiters = [];
26745
+ upload.waiting.set(source, waiters);
26746
+ upload.queue.push(source);
26747
+ }
26748
+ if (waiters.at(-1) === waiter) return;
26749
+ waiters.push(waiter);
26750
+ waiter.left += 1;
26751
+ }
26752
+ /**
26753
+ * Queues the sources of textures for the GPU; resolves true once each went through `initSource` (the body of
26754
+ * `TexturesApi.upload`). Answers false at once while nothing draws or with `uploads: 0`, and true at once when every
26755
+ * source is on this device already. Asks for a frame.
26756
+ *
26757
+ * @param sctx - Domain context of the sync module.
26758
+ * @param textures - The textures.
26759
+ * @returns Whether every source is on the GPU.
26760
+ */
26761
+ function uploadTextures(sctx, textures) {
26762
+ const { upload } = sctx.ctx.state.sync;
26763
+ if (sctx.ctx.config.uploads === 0 || !sctx.deps.host.ready()) return Promise.resolve(false);
26764
+ const waiter = {
26765
+ left: 0,
26766
+ resolve: unanswered
26767
+ };
26768
+ for (const texture of textures) enqueue(upload, texture.source, waiter);
26769
+ if (waiter.left === 0) return Promise.resolve(true);
26770
+ sctx.deps.demand.request();
26771
+ return new Promise((resolve) => {
26772
+ waiter.resolve = resolve;
26773
+ });
26774
+ }
26775
+ /**
26776
+ * Uploads one source, the error of a throw logged: the source counts as done either way, so a failing one is logged
26777
+ * once and never stops the queue.
26778
+ *
26779
+ * @param sctx - Domain context of the sync module.
26780
+ * @param upload - The upload branch of the sync state.
26781
+ * @param source - The source.
26782
+ */
26783
+ function put(sctx, upload, source) {
26784
+ try {
26785
+ sctx.deps.host.initSource(source);
26786
+ } catch (error) {
26787
+ sctx.ctx.log.warn("renderer:upload-failed", {
26788
+ source: source.label,
26789
+ error
26790
+ });
26791
+ }
26792
+ upload.done.add(source);
26793
+ }
26794
+ /**
26795
+ * Counts a source off for the calls that wait for it; a call whose last source it was answers true.
26796
+ *
26797
+ * @param upload - The upload branch of the sync state.
26798
+ * @param source - The source uploaded or skipped.
26799
+ */
26800
+ function settle$2(upload, source) {
26801
+ const waiters = upload.waiting.get(source) ?? NOBODY;
26802
+ upload.waiting.delete(source);
26803
+ for (const waiter of waiters) {
26804
+ waiter.left -= 1;
26805
+ if (waiter.left === 0) waiter.resolve(true);
26806
+ }
26807
+ }
26808
+ /**
26809
+ * Uploads up to `renderer.uploads` queued sources before the stage pass of a drawn frame; a source destroyed while it
26810
+ * waited is skipped and costs nothing of the budget. A queue that still holds sources asks for the next frame; one
26811
+ * that ran out empties. Reads two numbers and returns while the queue is empty.
26812
+ *
26813
+ * @param sctx - Domain context of the sync module.
26814
+ * @returns True when a source went to the GPU in this frame, so the frame is no sample of the adaptive rate.
26815
+ */
26816
+ function uploadStep(sctx) {
26817
+ const { upload } = sctx.ctx.state.sync;
26818
+ if (upload.head >= upload.queue.length) return false;
26819
+ const budget = sctx.ctx.config.uploads;
26820
+ let made = 0;
26821
+ while (made < budget && upload.head < upload.queue.length) {
26822
+ const source = upload.queue[upload.head];
26823
+ upload.head += 1;
26824
+ if (source === void 0) continue;
26825
+ if (!source.destroyed) {
26826
+ put(sctx, upload, source);
26827
+ made += 1;
26828
+ }
26829
+ settle$2(upload, source);
26830
+ }
26831
+ if (upload.head < upload.queue.length) {
26832
+ sctx.deps.demand.request();
26833
+ return made > 0;
26834
+ }
26835
+ upload.queue.length = 0;
26836
+ upload.head = 0;
26837
+ return made > 0;
26838
+ }
26839
+ /**
26840
+ * Ends the queue of a device that goes, from state alone (`onStop` has no context): every waiting call answers false,
26841
+ * the queue empties, and the sources uploaded on it are forgotten, since a new device holds none of them.
26842
+ *
26843
+ * @param state - The sync branch of the plugin state.
26844
+ */
26845
+ function stopUploads(state) {
26846
+ const { upload } = state;
26847
+ for (const waiters of upload.waiting.values()) for (const waiter of waiters) waiter.resolve(false);
26848
+ upload.waiting.clear();
26849
+ upload.queue.length = 0;
26850
+ upload.head = 0;
26851
+ upload.done = emptyDone();
26852
+ }
26853
+ //#endregion
26088
26854
  //#region src/plugins/renderer/sync/views.ts
26089
26855
  /**
26090
26856
  * @file renderer/sync — one view per entity: build it from the components, write every value,
@@ -26104,6 +26870,23 @@ const PLACEHOLDER_BOX = Object.freeze({
26104
26870
  * sync pass).
26105
26871
  */
26106
26872
  const LAYOUT = createSpriteLayout();
26873
+ /** The slice widths of the nine-slice being written: per write, reused, as `LAYOUT`. */
26874
+ const BORDERS = {
26875
+ left: 0,
26876
+ top: 0,
26877
+ right: 0,
26878
+ bottom: 0
26879
+ };
26880
+ /** What the outline of the nine-slice being written is drawn from, written in place, as `LAYOUT`. */
26881
+ const OUTLINE = {
26882
+ entity: 0,
26883
+ width: 0,
26884
+ height: 0,
26885
+ borders: BORDERS,
26886
+ missing: false
26887
+ };
26888
+ /** The box a clipping nine-slice masks its children with, as the shape value `applyClip` reads, written in place. */
26889
+ const NINE_CLIP = { ...Shape.defaults };
26107
26890
  /**
26108
26891
  * The depth of a wrapper's own visual among the wrapper's children: a child at `Order` 0 or more draws above it, one
26109
26892
  * at −1 or less below it, and a ui child at `zIndex` 0 (an `Order` between −0.5 and 0) above it too (spec 16 U1.8).
@@ -26204,26 +26987,31 @@ function visualOf(sctx, entity) {
26204
26987
  if (sprite !== void 0) found.push({
26205
26988
  kind: "Sprite",
26206
26989
  textureKey: sprite.texture,
26990
+ hollow: false,
26207
26991
  display: void 0
26208
26992
  });
26209
26993
  if (nine !== void 0) found.push({
26210
26994
  kind: "NineSlice",
26211
26995
  textureKey: nine.texture,
26996
+ hollow: nine.hollow === true,
26212
26997
  display: void 0
26213
26998
  });
26214
26999
  if (ecs.has(entity, Shape)) found.push({
26215
27000
  kind: "Shape",
26216
27001
  textureKey: "",
27002
+ hollow: false,
26217
27003
  display: void 0
26218
27004
  });
26219
27005
  if (ecs.has(entity, Display)) found.push({
26220
27006
  kind: "Display",
26221
27007
  textureKey: "",
27008
+ hollow: false,
26222
27009
  display: void 0
26223
27010
  });
26224
27011
  for (const entry of adaptersOn(sctx, entity)) found.push({
26225
27012
  kind: "adapter",
26226
27013
  textureKey: "",
27014
+ hollow: false,
26227
27015
  display: entry
26228
27016
  });
26229
27017
  if (found.length > 1) sctx.ctx.log.warn("renderer: entity has more than one visual", {
@@ -26244,29 +27032,10 @@ function retargetKey(state, entity, view, key) {
26244
27032
  if (view.textureKey === key) return;
26245
27033
  untrackKey(state, entity, view.textureKey);
26246
27034
  view.textureKey = key;
26247
- view.poolKey = poolKeyOf(view.kind, key);
27035
+ view.poolKey = poolKeyOf(view.kind, key, isHollow(view.object));
26248
27036
  if (key !== "") trackKey(state, entity, key);
26249
27037
  }
26250
27038
  /**
26251
- * Resolves a key and says whether the placeholder had to step in.
26252
- *
26253
- * @param sctx - Domain context of the sync module.
26254
- * @param key - The asset key.
26255
- * @returns The texture to draw and whether it is the placeholder.
26256
- */
26257
- function textureFor(sctx, key) {
26258
- const resolved = resolveTexture(sctx, key);
26259
- if (resolved !== void 0) return {
26260
- texture: resolved,
26261
- missing: false
26262
- };
26263
- warnMissing(sctx, key);
26264
- return {
26265
- texture: placeholderTexture(sctx),
26266
- missing: true
26267
- };
26268
- }
26269
- /**
26270
27039
  * Writes the texture and the tint of a sprite, each only when it differs from what Pixi holds.
26271
27040
  *
26272
27041
  * @param sctx - Domain context of the sync module.
@@ -26475,12 +27244,12 @@ function applyOutline(sctx, view, shape, debug, changed) {
26475
27244
  sctx.ctx.state.sync.redraws += 1;
26476
27245
  }
26477
27246
  /**
26478
- * Writes one number of a nine-slice only when it differs from what Pixi holds: Pixi rebuilds the
26479
- * panel on every write of a width, even an equal one.
27247
+ * Writes one number of a nine-slice only when it differs from what the object holds: Pixi rebuilds the
27248
+ * panel on every write of a width, even an equal one, and the rim-only object rewrites its vertices.
26480
27249
  *
26481
27250
  * @param sctx - Domain context of the sync module.
26482
27251
  * @param entity - The entity.
26483
- * @param object - The nine-slice sprite.
27252
+ * @param object - Pixi's nine-slice sprite, or the engine's rim-only one.
26484
27253
  * @param field - Which number.
26485
27254
  * @param value - The new number.
26486
27255
  * @returns True when it was written.
@@ -26494,7 +27263,9 @@ function writeSlice(sctx, entity, object, field, value) {
26494
27263
  /**
26495
27264
  * Writes the `NineSlice` component onto its display object and recomputes the hit box. Pixi reads
26496
27265
  * a texture's borders only in the constructor, so they are copied here on every write: a pooled
26497
- * object, a new key and a reloaded bundle all get the insets of the texture they draw now.
27266
+ * object, a new key and a reloaded bundle all get the insets of the texture they draw now. A `hollow` that flipped
27267
+ * swaps the view's object first (F59). The hit box is the whole panel, hollow or not. A write that changes nothing,
27268
+ * or only the size, allocates nothing: the borders, the outline's shape and the clip's box are written in place.
26498
27269
  *
26499
27270
  * @param sctx - Domain context of the sync module.
26500
27271
  * @param entity - The entity.
@@ -26502,11 +27273,16 @@ function writeSlice(sctx, entity, object, field, value) {
26502
27273
  */
26503
27274
  function applyNineSlice(sctx, entity, view) {
26504
27275
  const value = sctx.ctx.deps.world.ecs.get(entity, NineSlice);
27276
+ if (value === void 0 || !isNineSlice(view.object)) return;
27277
+ if (isHollow(view.object) !== (value.hollow === true)) swapSlice(sctx, entity, view);
26505
27278
  const object = view.object;
26506
- if (value === void 0 || !isNineSlice(object)) return;
27279
+ if (!isNineSlice(object)) return;
26507
27280
  retargetKey(sctx.ctx.state.sync, entity, view, value.texture);
26508
- const { texture, missing } = textureFor(sctx, value.texture);
26509
- const borders = bordersOf(texture);
27281
+ const resolved = resolveTexture(sctx, value.texture);
27282
+ const missing = resolved === void 0;
27283
+ if (missing) warnMissing(sctx, value.texture);
27284
+ const texture = resolved ?? placeholderTexture(sctx);
27285
+ const borders = bordersInto(texture, BORDERS);
26510
27286
  view.placeholder = missing;
26511
27287
  let changed = false;
26512
27288
  if (texture !== void 0 && object.texture !== texture) {
@@ -26522,25 +27298,59 @@ function applyNineSlice(sctx, entity, view) {
26522
27298
  applyAlpha(sctx, entity, view, value);
26523
27299
  changed = writeSlice(sctx, entity, object, "width", value.width) || changed;
26524
27300
  changed = writeSlice(sctx, entity, object, "height", value.height) || changed;
26525
- view.hitBox = {
26526
- x: 0,
26527
- y: 0,
26528
- width: value.width,
26529
- height: value.height
26530
- };
26531
- applyOutline(sctx, view, {
26532
- entity,
26533
- width: value.width,
26534
- height: value.height,
26535
- borders,
26536
- missing
26537
- }, value.debug, changed);
26538
- applyClip(sctx, entity, view, {
26539
- ...Shape.defaults,
26540
- w: value.width,
26541
- h: value.height,
26542
- clip: value.clip
26543
- });
27301
+ writeHitBox(view.hitBox, value.width, value.height);
27302
+ OUTLINE.entity = entity;
27303
+ OUTLINE.width = value.width;
27304
+ OUTLINE.height = value.height;
27305
+ OUTLINE.missing = missing;
27306
+ applyOutline(sctx, view, OUTLINE, value.debug, changed);
27307
+ NINE_CLIP.w = value.width;
27308
+ NINE_CLIP.h = value.height;
27309
+ NINE_CLIP.clip = value.clip;
27310
+ applyClip(sctx, entity, view, NINE_CLIP);
27311
+ }
27312
+ /**
27313
+ * A new nine-slice object of one kind: the engine's rim-only mesh, or Pixi's `NineSliceSprite`; both of the white
27314
+ * texture until the first write.
27315
+ *
27316
+ * @param sctx - Domain context of the sync module.
27317
+ * @param pixi - The Pixi module.
27318
+ * @param hollow - True for the rim-only object.
27319
+ * @returns The object.
27320
+ */
27321
+ function newSlice(sctx, pixi, hollow) {
27322
+ return hollow ? makeHollow(sctx.ctx.state.sync, pixi) : new pixi.NineSliceSprite({ texture: pixi.Texture.WHITE });
27323
+ }
27324
+ /**
27325
+ * Gives a nine-slice view whose `hollow` flipped the other object (F59), on the write that flipped it: the view gets
27326
+ * its wrapper first (it keeps the pose, the alpha, the filters, the clip and the outline), then the other kind takes
27327
+ * the old object's place in it, out of its pool or new, and the old object goes back to its own pool. The entity, its
27328
+ * children and its hit box stay as they were; the caller writes the new object in full.
27329
+ *
27330
+ * @param sctx - Domain context of the sync module.
27331
+ * @param entity - The entity.
27332
+ * @param view - Its view, drawn by a nine-slice object.
27333
+ */
27334
+ function swapSlice(sctx, entity, view) {
27335
+ const pixi = sctx.deps.host.pixi();
27336
+ const wrapper = ensureWrapper(sctx, entity);
27337
+ if (pixi === void 0 || wrapper === void 0) return;
27338
+ const state = sctx.ctx.state.sync;
27339
+ const old = view.object;
27340
+ const hollow = !isHollow(old);
27341
+ const poolKey = poolKeyOf("NineSlice", view.textureKey, hollow);
27342
+ const next = acquire(state, poolKey) ?? newSlice(sctx, pixi, hollow);
27343
+ markView(sctx, entity);
27344
+ next.label = old.label;
27345
+ next.zIndex = old.zIndex;
27346
+ wrapper.addChildAt(next, Math.max(0, wrapper.children.indexOf(old)));
27347
+ state.entityOf.delete(old);
27348
+ state.entityOf.set(next, entity);
27349
+ view.object = next;
27350
+ releaseObject(sctx, old, view.poolKey);
27351
+ view.poolKey = poolKey;
27352
+ writeRenderable(view);
27353
+ applyTransform(sctx, entity, view);
26544
27354
  }
26545
27355
  /**
26546
27356
  * Gives a clipping view the masked container its children hang in, next to its own visual in the
@@ -26558,7 +27368,9 @@ function ensureClipped(pixi, entity, view, wrapper) {
26558
27368
  const clipped = new pixi.Container();
26559
27369
  clipped.label = `children#${entity}`;
26560
27370
  clipped.sortableChildren = true;
26561
- for (const child of wrapper.children) if (child !== view.object && child !== view.outline) clipped.addChild(child);
27371
+ let index = 0;
27372
+ for (let child = wrapper.children[0]; child !== void 0; child = wrapper.children[index]) if (child === view.object || child === view.outline) index += 1;
27373
+ else clipped.addChild(child);
26562
27374
  wrapper.addChild(clipped);
26563
27375
  view.clipped = clipped;
26564
27376
  return clipped;
@@ -26990,6 +27802,7 @@ function attach$1(sctx, entity, view) {
26990
27802
  * @param visual - Which component gives the entity its object, and its texture key.
26991
27803
  * @param visual.kind - Which component gives the entity its object.
26992
27804
  * @param visual.textureKey - The asset key the object draws.
27805
+ * @param visual.hollow - True for the rim-only object of a hollow `NineSlice`.
26993
27806
  * @returns The object, or `undefined` while inert.
26994
27807
  */
26995
27808
  function makeObject(sctx, entity, visual) {
@@ -27003,7 +27816,7 @@ function makeObject(sctx, entity, visual) {
27003
27816
  const pixi = sctx.deps.host.pixi();
27004
27817
  if (pixi === void 0) return void 0;
27005
27818
  if (visual.display !== void 0) return adapterObject(sctx, entity, visual.display);
27006
- const pooled = acquire(sctx.ctx.state.sync, poolKeyOf(visual.kind, visual.textureKey));
27819
+ const pooled = acquire(sctx.ctx.state.sync, poolKeyOf(visual.kind, visual.textureKey, visual.hollow));
27007
27820
  if (pooled !== void 0) return {
27008
27821
  object: pooled,
27009
27822
  value: void 0
@@ -27017,7 +27830,7 @@ function makeObject(sctx, entity, visual) {
27017
27830
  value: void 0
27018
27831
  };
27019
27832
  return {
27020
- object: new pixi.NineSliceSprite({ texture: pixi.Texture.WHITE }),
27833
+ object: newSlice(sctx, pixi, visual.hollow),
27021
27834
  value: void 0
27022
27835
  };
27023
27836
  }
@@ -27060,7 +27873,7 @@ function createView(sctx, entity) {
27060
27873
  const view = {
27061
27874
  object,
27062
27875
  kind: visual.kind,
27063
- poolKey: poolKeyOf(visual.kind, visual.textureKey),
27876
+ poolKey: poolKeyOf(visual.kind, visual.textureKey, visual.hollow),
27064
27877
  layer: "",
27065
27878
  textureKey: visual.textureKey,
27066
27879
  wrapper: void 0,
@@ -27185,8 +27998,9 @@ function dropView(sctx, entity) {
27185
27998
  release$1(sctx, view);
27186
27999
  }
27187
28000
  /**
27188
- * Drops every view, every layer, every pool, every filter slot and filter area, and the warm-up (its waiting promises
27189
- * answer false, its probe tree is destroyed). Works on the state alone,
28001
+ * Drops every view, every layer, every pool, every filter slot and filter area, the warm-up (its waiting promises
28002
+ * answer false, its probe tree is destroyed) and the upload queue (its waiting calls answer false). Works on the state
28003
+ * alone,
27190
28004
  * because `onStop` has no context. A `Display` object belongs to the game: it is detached, never
27191
28005
  * destroyed, and leaves without the filters, the filter area and the hidden reasons the renderer gave it.
27192
28006
  *
@@ -27231,6 +28045,7 @@ function stopSync(state) {
27231
28045
  state.areas.clear();
27232
28046
  state.viewlessEntries = false;
27233
28047
  stopWarm(state);
28048
+ stopUploads(state);
27234
28049
  state.held.clear();
27235
28050
  state.redraws = 0;
27236
28051
  state.overdraw.listeners.length = 0;
@@ -27374,7 +28189,8 @@ function withDeps$8(ctx) {
27374
28189
  * takes the same path: the backing store changes, the frame and the scale do not), then the
27375
28190
  * overlay step (spec 22), then `kept` reads the marks before `demand` decides whether the frame is
27376
28191
  * drawn and clears them; a drawn frame draws the probes of the warm-up onto the canvas first (F4: the
27377
- * stage pass clears them, in the same task, so they never show), then the kept pictures
28192
+ * stage pass clears them, in the same task, so they never show), then uploads the queued texture sources (F60), then
28193
+ * the kept pictures
27378
28194
  * (`kept.flush`, so the canvas stays one pass, E7), then the stage, then the pixel leg of the kept
27379
28195
  * oracle; `demand` settles the
27380
28196
  * frame (the dev check, the pixel oracle), and `monitor` closes it and hands a drawn one to a
@@ -27409,10 +28225,11 @@ function renderFrame(ctx, modules) {
27409
28225
  const drawn = modules.demand.decide(modules.monitor.waiting());
27410
28226
  if (drawn) {
27411
28227
  const probed = modules.sync.warmStep();
28228
+ const uploaded = modules.sync.uploadStep();
27412
28229
  modules.kept.flush();
27413
28230
  if (overdraw ? modules.sync.drawOverdraw(modules.host.render) : modules.host.render()) {
27414
28231
  ctx.deps.time.frameDrawn();
27415
- if (probed) ctx.deps.time.frameProbed();
28232
+ if (probed || uploaded) ctx.deps.time.frameProbed();
27416
28233
  }
27417
28234
  modules.kept.after();
27418
28235
  }
@@ -29616,6 +30433,7 @@ function createSyncApi(ctx, deps) {
29616
30433
  const forgetTree = () => {
29617
30434
  deps.kept.reset();
29618
30435
  forgetOverlays(state);
30436
+ stopUploads(state);
29619
30437
  state.held.clear();
29620
30438
  for (const view of state.views.values()) {
29621
30439
  state.viewOfTop.delete(view.wrapper ?? view.object);
@@ -29658,7 +30476,7 @@ function createSyncApi(ctx, deps) {
29658
30476
  state.removed.clear();
29659
30477
  state.reparented.clear();
29660
30478
  state.outlinesStale = false;
29661
- startWarm$1(sctx);
30479
+ startWarm$2(sctx);
29662
30480
  };
29663
30481
  return {
29664
30482
  hitTest: (x, y, accept) => hitTest(sctx, x, y, accept),
@@ -29685,7 +30503,8 @@ function createSyncApi(ctx, deps) {
29685
30503
  if (state.root === void 0) return;
29686
30504
  for (const key of keys) state.invalidated.add(key);
29687
30505
  dropPooled(state, keys);
29688
- }
30506
+ },
30507
+ upload: (textures) => uploadTextures(sctx, textures)
29689
30508
  },
29690
30509
  displays: { provide: (component, adapter) => provideDisplay(sctx, component, adapter, {
29691
30510
  added: markAdded,
@@ -29733,6 +30552,7 @@ function createSyncApi(ctx, deps) {
29733
30552
  stepFilters: () => applySwitched(state, deps.kept),
29734
30553
  drainOverlayEvents: (monitor) => drainOverlayEvents(sctx, monitor),
29735
30554
  warmStep: () => warmStep(sctx),
30555
+ uploadStep: () => uploadStep(sctx),
29736
30556
  onWarm: (fn) => {
29737
30557
  state.warm.listeners.push(fn);
29738
30558
  },
@@ -30524,8 +31344,22 @@ function createWarmState() {
30524
31344
  };
30525
31345
  }
30526
31346
  /**
31347
+ * Creates the empty upload queue: nothing queued, nobody waiting, nothing uploaded. Its own non-exported function, as
31348
+ * `emptyMap`.
31349
+ *
31350
+ * @returns The upload branch of the sync state.
31351
+ */
31352
+ function createUploadState() {
31353
+ return {
31354
+ queue: [],
31355
+ head: 0,
31356
+ waiting: emptyMap$6(),
31357
+ done: emptyWeakSet()
31358
+ };
31359
+ }
31360
+ /**
30527
31361
  * Creates the initial sync state: no root, no layers, no view, no orphan, no pool, no provider, no filter, no
30528
- * filter area, no slice, no hidden mask, no overlay, no warm-up, no redraw counted and no unseen walk kept.
31362
+ * filter area, no slice, no hidden mask, no overlay, no warm-up, no upload, no redraw counted and no unseen walk kept.
30529
31363
  *
30530
31364
  * @param debug - The debug switches of the config; copied, so the frozen config stays untouched.
30531
31365
  * @returns The sync branch of the plugin state.
@@ -30564,6 +31398,8 @@ function createSyncState(debug) {
30564
31398
  areas: emptyMap$6(),
30565
31399
  viewlessEntries: false,
30566
31400
  warm: createWarmState(),
31401
+ upload: createUploadState(),
31402
+ hollowClass: void 0,
30567
31403
  filterKinds: emptyWeakMap$1(),
30568
31404
  filterOwners: emptyWeakMap$1(),
30569
31405
  objectHidden: emptyWeakMap$1(),
@@ -30631,6 +31467,8 @@ const RING_FRAMES = {
30631
31467
  };
30632
31468
  /** The most probe draws of the warm-up a drawn frame may make. */
30633
31469
  const MAX_WARM = 16;
31470
+ /** The most textures the upload queue may put on the GPU in one drawn frame: about 9 ms each on a Galaxy A26. */
31471
+ const MAX_UPLOADS = 8;
30634
31472
  /**
30635
31473
  * Tells a finite number of at least 0 from anything else a config can hold.
30636
31474
  *
@@ -30677,14 +31515,14 @@ function checkAdaptiveScale(adaptiveScale) {
30677
31515
  }
30678
31516
  /**
30679
31517
  * Checks the numbers of the config a game can get wrong, at `createApp`, in every build: the
30680
- * resolution cap, the pool trim, the adaptive render scale, the performance monitor, the budget of the kept pictures
30681
- * and the probe draws of the warm-up.
31518
+ * resolution cap, the pool trim, the adaptive render scale, the performance monitor, the budget of the kept pictures,
31519
+ * the probe draws of the warm-up and the textures uploaded per drawn frame.
30682
31520
  *
30683
31521
  * @param config - The resolved plugin config.
30684
31522
  * @throws {Error} When `maxResolution` is not a number above 0, `poolTrim` or `adaptiveScale` fails its check,
30685
31523
  * `monitor.budgetMs` is neither `"rate"` nor a number above 0, `monitor.frames` is not a whole
30686
- * number from 64 to 16384, `keptMb` is not a finite number of at least 0, or `warm` is not a
30687
- * whole number from 0 to 16.
31524
+ * number from 64 to 16384, `keptMb` is not a finite number of at least 0, `warm` is not a
31525
+ * whole number from 0 to 16, or `uploads` is not a whole number from 0 to 8.
30688
31526
  */
30689
31527
  function checkConfig(config) {
30690
31528
  const { maxResolution, keptMb } = config;
@@ -30697,6 +31535,8 @@ function checkConfig(config) {
30697
31535
  if (typeof keptMb !== "number" || !Number.isFinite(keptMb) || keptMb < 0) throw new Error(`[game] renderer.keptMb is ${String(keptMb)}.\n Give the MiB kept pictures may hold, or 0 to keep none.`);
30698
31536
  const { warm } = config;
30699
31537
  if (!Number.isInteger(warm) || warm < 0 || warm > MAX_WARM) throw new Error(`[game] renderer.warm is ${String(warm)}.\n Give the probe draws per frame, a whole number from 0 to 16.`);
31538
+ const { uploads } = config;
31539
+ if (!Number.isInteger(uploads) || uploads < 0 || uploads > MAX_UPLOADS) throw new Error(`[game] renderer.uploads is ${String(uploads)}.\n Give the textures uploaded per drawn frame, a whole number from 0 to 8.`);
30700
31540
  }
30701
31541
  /**
30702
31542
  * Creates the initial renderer state: one branch per module, composed from the module state
@@ -30778,6 +31618,7 @@ const rendererPlugin = /*#__PURE__*/ createPlugin$1("renderer", {
30778
31618
  onDemand: false,
30779
31619
  keptMb: 64,
30780
31620
  warm: 1,
31621
+ uploads: 1,
30781
31622
  adaptiveScale: {
30782
31623
  floor: .75,
30783
31624
  devices: "phone"
@@ -34291,8 +35132,9 @@ function reportSettled(ctx, progress, signal) {
34291
35132
  ctx.emit("assets:bundle-progress", { ...progress });
34292
35133
  }
34293
35134
  /**
34294
- * Publishes a loaded bundle: what was loaded moves into the record, `renderer` re-resolves the
34295
- * keys, the event goes out, the frame loop wakes and the budget is enforced.
35135
+ * Publishes a loaded bundle: what was loaded moves into the record, a failure `ready` remembered
35136
+ * is forgotten, `renderer` re-resolves the keys, the event goes out, the frame loop wakes and the
35137
+ * budget is enforced.
34296
35138
  *
34297
35139
  * @param ctx - Domain context of the plugin.
34298
35140
  * @param bundle - Name of the bundle.
@@ -34309,6 +35151,7 @@ function finish$1(ctx, bundle, entry, assets, reason) {
34309
35151
  record.status = "loaded";
34310
35152
  record.inflight = void 0;
34311
35153
  touch(ctx.state, record);
35154
+ ctx.state.readyFailed.delete(bundle);
34312
35155
  ctx.deps.renderer.sync.textures.invalidate(entry.files.map((file) => file.key));
34313
35156
  ctx.emit("assets:bundle-loaded", {
34314
35157
  bundle,
@@ -34712,9 +35555,9 @@ function releaseAssets(io, assets) {
34712
35555
  assets.audio.clear();
34713
35556
  }
34714
35557
  /**
34715
- * Frees one loaded bundle: the textures are destroyed, every companion drops what travels with
34716
- * it, `renderer` is told that its keys are gone and the event goes out. A bundle that is not
34717
- * loaded is left alone.
35558
+ * Frees one loaded bundle: the textures are destroyed, its warm is forgotten, every companion drops
35559
+ * what travels with it, `renderer` is told that its keys are gone and the event goes out. A bundle
35560
+ * that is not loaded is left alone.
34718
35561
  *
34719
35562
  * @param ctx - Domain context of the plugin.
34720
35563
  * @param bundle - Name of the bundle.
@@ -34728,6 +35571,7 @@ function unloadBundle(ctx, bundle, reason) {
34728
35571
  releaseAssets(state.io, record);
34729
35572
  record.status = "idle";
34730
35573
  record.lastUsed = 0;
35574
+ state.warmed.delete(bundle);
34731
35575
  unloadCompanions(ctx, state.companions, bundle);
34732
35576
  const keys = entry.files.map((file) => file.key);
34733
35577
  ctx.deps.renderer.sync.textures.invalidate(keys);
@@ -35378,9 +36222,125 @@ function startPreload(ctx) {
35378
36222
  runQueue(ctx, queue).catch(ignoreFailure$1);
35379
36223
  }
35380
36224
  //#endregion
36225
+ //#region src/plugins/assets/ready.ts
36226
+ /**
36227
+ * @file assets plugin — a bundle ready to show. `warm` loads a bundle and puts its textures on the
36228
+ * GPU before a scene shows them (F60): on a Galaxy A26 the frame that showed 6 cold textures took
36229
+ * 55 ms, 8 ms once they were uploaded before. The `ready` effect lets a node rest on that work and
36230
+ * stay answerable (F52). Nothing here is on the frame path: one promise per bundle, and the inbox
36231
+ * event is posted from a promise.
36232
+ */
36233
+ /**
36234
+ * Runs one warm: loads the bundle when it is not loaded, then hands every texture and atlas page
36235
+ * of it to the renderer's upload queue. The answer of the upload is not read: when the renderer
36236
+ * could not upload, the first draw does. A failure drops the entry, so the next `warm` tries again.
36237
+ *
36238
+ * @param ctx - Domain context of the plugin.
36239
+ * @param bundle - Name of a bundle of the manifest.
36240
+ * @param entry - The entry of this warm in `state.warmed`.
36241
+ * @returns A promise that resolves once the upload answered.
36242
+ * @throws {Error} When the load fails or is aborted.
36243
+ */
36244
+ async function runWarm(ctx, bundle, entry) {
36245
+ try {
36246
+ await loadBundle(ctx, bundle, void 0, "request");
36247
+ const { textures, pages } = ctx.state.records.get(bundle) ?? emptyAssets();
36248
+ await ctx.deps.renderer.sync.textures.upload([...textures.values(), ...pages.values()]);
36249
+ entry.done = true;
36250
+ } catch (error) {
36251
+ if (ctx.state.warmed.get(bundle) === entry) ctx.state.warmed.delete(bundle);
36252
+ throw error;
36253
+ }
36254
+ }
36255
+ /**
36256
+ * Starts the one warm of a bundle and records it.
36257
+ *
36258
+ * @param ctx - Domain context of the plugin.
36259
+ * @param bundle - Name of a bundle of the manifest.
36260
+ * @returns The entry every later caller joins.
36261
+ */
36262
+ function startWarm$1(ctx, bundle) {
36263
+ const entry = {
36264
+ promise: Promise.resolve(),
36265
+ done: false
36266
+ };
36267
+ ctx.state.warmed.set(bundle, entry);
36268
+ entry.promise = runWarm(ctx, bundle, entry);
36269
+ return entry;
36270
+ }
36271
+ /**
36272
+ * Warms one bundle, or joins its running or finished warm. Headless it resolves at once, whatever
36273
+ * the name, as `load` does.
36274
+ *
36275
+ * @param ctx - Domain context of the plugin.
36276
+ * @param bundle - Name of a bundle of the manifest.
36277
+ * @returns A promise that resolves once the bundle is loaded and its textures went to the GPU.
36278
+ * @throws {Error} When the manifest has no such bundle, and when the load fails or is aborted.
36279
+ */
36280
+ async function warmBundle(ctx, bundle) {
36281
+ const state = ctx.state;
36282
+ if (state.io === void 0) return;
36283
+ if (state.manifest.bundles[bundle] === void 0) throw unknownBundle(bundle);
36284
+ await (state.warmed.get(bundle) ?? startWarm$1(ctx, bundle)).promise;
36285
+ }
36286
+ /**
36287
+ * Tells whether the `ready` effect counts a bundle as there: loaded and warmed, or its last warm
36288
+ * for `ready` failed.
36289
+ *
36290
+ * @param state - The plugin state.
36291
+ * @param bundle - Name of a bundle of the manifest.
36292
+ * @returns True when a node need not rest on it.
36293
+ */
36294
+ function isThere(state, bundle) {
36295
+ if (state.readyFailed.has(bundle)) return true;
36296
+ return state.records.get(bundle)?.status === "loaded" && state.warmed.get(bundle)?.done === true;
36297
+ }
36298
+ /**
36299
+ * Warms one bundle for the `ready` effect. It never rejects: a failure that is not an abort marks
36300
+ * the bundle failed and is logged, so the node resting on it moves on; an abort (an unload, a stop)
36301
+ * marks nothing, and the next `ready` starts the bundle again.
36302
+ *
36303
+ * @param ctx - Domain context of the plugin.
36304
+ * @param bundle - Name of a bundle of the manifest.
36305
+ * @returns A promise that resolves once the warm settled.
36306
+ */
36307
+ async function warmForReady(ctx, bundle) {
36308
+ try {
36309
+ await warmBundle(ctx, bundle);
36310
+ } catch (error) {
36311
+ if (isAbortError(error)) return;
36312
+ ctx.state.readyFailed.add(bundle);
36313
+ ctx.log.warn("assets:ready-failed", {
36314
+ bundle,
36315
+ error: String(error)
36316
+ });
36317
+ }
36318
+ }
36319
+ /**
36320
+ * The handler of the `ready` effect (F52). Headless it answers `true` before any name check, as the
36321
+ * `load` effect does. Every bundle there: `true`, and nothing is posted. Otherwise it starts the
36322
+ * warm of each bundle that is not there, answers `false`, and posts `{ type: "ready" }` into
36323
+ * `flow.inbox` once all of them settled. The posting does not wait on the node's signal: the warm
36324
+ * is shared work, and the inbox keeps only the latest `ready`.
36325
+ *
36326
+ * @param ctx - Domain context of the plugin.
36327
+ * @param bundles - The bundle names of the descriptor.
36328
+ * @returns True when the node may go on, false when it should rest until the inbox event `ready`.
36329
+ * @throws {Error} When the manifest has no such bundle.
36330
+ */
36331
+ function answerReady(ctx, bundles) {
36332
+ const state = ctx.state;
36333
+ if (state.io === void 0) return true;
36334
+ for (const name of bundles) if (state.manifest.bundles[name] === void 0) throw unknownBundle(name);
36335
+ const missing = bundles.filter((name) => !isThere(state, name));
36336
+ if (missing.length === 0) return true;
36337
+ Promise.all(missing.map((name) => warmForReady(ctx, name))).then(() => ctx.deps.flow.inbox.post({ type: "ready" }));
36338
+ return false;
36339
+ }
36340
+ //#endregion
35381
36341
  //#region src/plugins/assets/lifecycle.ts
35382
36342
  /**
35383
- * @file assets plugin — lifecycle: the six registrations of `onInit`, the boot of `onStart`
36343
+ * @file assets plugin — lifecycle: the seven registrations of `onInit`, the boot of `onStart`
35384
36344
  * and the teardown that frees every texture and cancels every fetch.
35385
36345
  */
35386
36346
  /**
@@ -35411,7 +36371,7 @@ function withDeps$6(ctx) {
35411
36371
  };
35412
36372
  }
35413
36373
  /**
35414
- * Reads the bundle names out of a `load` descriptor.
36374
+ * Reads the bundle names out of a `load` or a `ready` descriptor.
35415
36375
  *
35416
36376
  * @param descriptor - What the node awaited.
35417
36377
  * @returns The bundle names, in the order the node listed them.
@@ -35494,17 +36454,17 @@ async function enterNode(ctx, node, run) {
35494
36454
  }
35495
36455
  }
35496
36456
  /**
35497
- * Connects the plugin in `onInit`: the `load` stage of `onEnter`, the handler of the `load`
35498
- * effect, the texture provider of `renderer`, the start and the end of an exit in `world`, counted,
35499
- * and the system `assets.leave` of phase `sync`, which lets a leaving scene's bundles go once its
35500
- * elements played their exit (F51). The six removers go to the state, because `onStop` has nothing
35501
- * but the state.
36457
+ * Connects the plugin in `onInit`: the `load` stage of `onEnter`, the handlers of the `load` and
36458
+ * the `ready` effects (F52), both run in fast mode too, the texture provider of `renderer`, the
36459
+ * start and the end of an exit in `world`, counted, and the system `assets.leave` of phase `sync`,
36460
+ * which lets a leaving scene's bundles go once its elements played their exit (F51). The seven
36461
+ * removers go to the state, because `onStop` has nothing but the state.
35502
36462
  *
35503
36463
  * @param ctx - Kernel context of the assets plugin.
35504
36464
  */
35505
36465
  function connectAssets(ctx) {
35506
36466
  const assets = withDeps$6(ctx);
35507
- assets.state.removers.push(assets.deps.flow.onEnter("load", (node, run) => enterNode(assets, node, run)), assets.deps.flow.fx.handle("load", (descriptor, run) => runLoadEffect(assets, descriptor, run), { runInFast: true }), assets.deps.renderer.sync.textures.provide((key) => lookupTexture(assets, key)), assets.deps.world.ecs.onAdded(Exiting, () => {
36467
+ assets.state.removers.push(assets.deps.flow.onEnter("load", (node, run) => enterNode(assets, node, run)), assets.deps.flow.fx.handle("load", (descriptor, run) => runLoadEffect(assets, descriptor, run), { runInFast: true }), assets.deps.flow.fx.handle("ready", (descriptor) => answerReady(assets, bundlesOfDescriptor(descriptor)), { runInFast: true }), assets.deps.renderer.sync.textures.provide((key) => lookupTexture(assets, key)), assets.deps.world.ecs.onAdded(Exiting, () => {
35508
36468
  assets.state.exiting += 1;
35509
36469
  }), assets.deps.world.ecs.onRemoved(Exiting, () => {
35510
36470
  assets.state.exiting -= 1;
@@ -35584,7 +36544,7 @@ async function startAssets(ctx) {
35584
36544
  }
35585
36545
  /**
35586
36546
  * Frees everything in `onStop`: the preload and every running load are aborted, every texture is
35587
- * destroyed, the six registrations are removed and the registries are emptied, the companions
36547
+ * destroyed, the seven registrations are removed and the registries are emptied, the companions
35588
36548
  * too. A teardown context carries the state and nothing else, so no other plugin is called here:
35589
36549
  * no companion's `unload` either, its plugin stopped before this one.
35590
36550
  *
@@ -35607,6 +36567,8 @@ function releaseAll(state) {
35607
36567
  state.featureOfFlow.clear();
35608
36568
  state.pinned.clear();
35609
36569
  state.warned.clear();
36570
+ state.warmed.clear();
36571
+ state.readyFailed.clear();
35610
36572
  state.companions.length = 0;
35611
36573
  state.current = void 0;
35612
36574
  state.sceneId = void 0;
@@ -35753,6 +36715,7 @@ function createAssetsApi(ctx) {
35753
36715
  const assets = withDeps$6(ctx);
35754
36716
  return {
35755
36717
  load: (bundle) => loadBundle(assets, bundle, void 0, "request"),
36718
+ warm: (bundle) => warmBundle(assets, bundle),
35756
36719
  unload: (bundle) => unload(assets, bundle),
35757
36720
  isLoaded: (bundle) => isLoaded(assets, bundle),
35758
36721
  texture: (key) => lookupTexture(assets, key),
@@ -36430,7 +37393,9 @@ function createAssetsState() {
36430
37393
  sceneLeaving: [],
36431
37394
  exiting: 0,
36432
37395
  companions: [],
36433
- restPending: false
37396
+ restPending: false,
37397
+ readyFailed: emptySet$2(),
37398
+ warmed: emptyMap$3()
36434
37399
  };
36435
37400
  }
36436
37401
  /**
@@ -41339,7 +42304,7 @@ function complete(ectx) {
41339
42304
  ectx.log.info("effects:checks", {
41340
42305
  definitions: checks.checked,
41341
42306
  broken: checks.failed,
41342
- ms: tenths(pageNow() - checks.startedAt)
42307
+ ms: tenths(pageNow$1() - checks.startedAt)
41343
42308
  });
41344
42309
  checks.resolve();
41345
42310
  }
@@ -41482,7 +42447,7 @@ function checkGlsl(ectx, run, definition) {
41482
42447
  function startChecks(ectx) {
41483
42448
  const { checks } = ectx.state;
41484
42449
  const backend = ectx.deps.renderer.host.kind();
41485
- checks.startedAt = pageNow();
42450
+ checks.startedAt = pageNow$1();
41486
42451
  for (const definition of checkList(ectx)) {
41487
42452
  if (checks.status.has(definition)) continue;
41488
42453
  checks.status.set(definition, "pending");
@@ -41618,12 +42583,12 @@ async function gateChecks(ectx, node, run) {
41618
42583
  if (!ectx.state.checks.started) startChecks(ectx);
41619
42584
  const { checks } = ectx.state;
41620
42585
  if (checks.waiting === 0) return;
41621
- const began = pageNow();
42586
+ const began = pageNow$1();
41622
42587
  await Promise.race([checks.settled, whenAborted(run.signal)]);
41623
42588
  if (run.signal.aborted) return;
41624
42589
  ectx.log.info("effects:mount-waited", {
41625
42590
  node: node.path,
41626
- ms: tenths(pageNow() - began)
42591
+ ms: tenths(pageNow$1() - began)
41627
42592
  });
41628
42593
  }
41629
42594
  //#endregion
@@ -52766,6 +53731,26 @@ function nineSliceClipped(element) {
52766
53731
  };
52767
53732
  }
52768
53733
  /**
53734
+ * Reports an element whose style is `hollow` but names no nine-slice: only a nine-slice has a
53735
+ * centre to leave out, so the field does nothing there. `hollow: false` asks for nothing either way.
53736
+ *
53737
+ * @param element - The element to check.
53738
+ * @returns The finding, or `undefined`.
53739
+ * @example
53740
+ * ```ts
53741
+ * hollowWithoutNineSlice({ key: "memFrost", type: "stack", style: { hollow: true } } as Element);
53742
+ * // { rule: "hollow-without-nine-slice", key: "memFrost", detail: "stack" }
53743
+ * ```
53744
+ */
53745
+ function hollowWithoutNineSlice(element) {
53746
+ if (element.style.hollow !== true || element.style.nineSlice !== void 0) return void 0;
53747
+ return {
53748
+ rule: "hollow-without-nine-slice",
53749
+ key: nameOf(element),
53750
+ detail: element.type
53751
+ };
53752
+ }
53753
+ /**
52769
53754
  * Reports a root element whose style sets a `zIndex`: a root draws at the order of its layer,
52770
53755
  * so the value is ignored.
52771
53756
  *
@@ -52786,7 +53771,7 @@ function zIndexOnRoot(element) {
52786
53771
  };
52787
53772
  }
52788
53773
  /**
52789
- * Runs the five rules over every live element.
53774
+ * Runs the six rules over every live element.
52790
53775
  *
52791
53776
  * @param ctx - Domain context of the ui plugin.
52792
53777
  * @returns One finding per rule and element, in element order.
@@ -52801,6 +53786,7 @@ function runLint(ctx) {
52801
53786
  textOverflow(ctx, element),
52802
53787
  absoluteWithoutReason(element),
52803
53788
  nineSliceClipped(element),
53789
+ hollowWithoutNineSlice(element),
52804
53790
  zIndexOnRoot(element)
52805
53791
  ]) if (finding !== void 0) findings.push(finding);
52806
53792
  }
@@ -55532,6 +56518,7 @@ function createReconciler(ctx, modules, fields) {
55532
56518
  state.hosts.delete(element.entity);
55533
56519
  state.scrolls.delete(element.entity);
55534
56520
  state.exiting.delete(element.entity);
56521
+ state.leading.delete(element.entity);
55535
56522
  state.overlays.delete(element.entity);
55536
56523
  state.elements.delete(element.entity);
55537
56524
  ecs.despawn(element.entity);
@@ -55546,7 +56533,7 @@ function createReconciler(ctx, modules, fields) {
55546
56533
  state.exiting.delete(entity);
55547
56534
  continue;
55548
56535
  }
55549
- if (!subtreeSettled(element)) continue;
56536
+ if (!(state.leading.has(entity) ? modules.layout.settled(element) : subtreeSettled(element))) continue;
55550
56537
  despawnTree(element);
55551
56538
  }
55552
56539
  dropRemovedRoots();
@@ -56172,7 +57159,8 @@ function createReconciler(ctx, modules, fields) {
56172
57159
  * Starts the teardown of a root: every element exits with its motion, then the sweep frees the
56173
57160
  * nodes and despawns the root entity. A popup leaves by an event; a projection root leaves the
56174
57161
  * way its view did, which the world still answers in its removal hook: `"mount"` with its scene,
56175
- * a load or a restore.
57162
+ * a load or a restore. A root that goes with its screen ends with the exit of its top element
57163
+ * when that plays one, so a longer exit under it does not hold it and its bundles (F55).
56176
57164
  *
56177
57165
  * @param entity - The root entity.
56178
57166
  */
@@ -56183,7 +57171,10 @@ function createReconciler(ctx, modules, fields) {
56183
57171
  const cause = root.popup === void 0 ? ctx.deps.world.projection.causeOf(entity) ?? "change" : "change";
56184
57172
  state.removing.add(entity);
56185
57173
  hostsMoved(state);
56186
- if (element !== void 0) exitElement(element, cause);
57174
+ if (element !== void 0) {
57175
+ exitElement(element, cause);
57176
+ if (cause === "mount" && !modules.layout.settled(element)) state.leading.add(element.entity);
57177
+ }
56187
57178
  ctx.deps.time.wake();
56188
57179
  }
56189
57180
  /**
@@ -59624,6 +60615,7 @@ function createJsxState() {
59624
60615
  byKey: emptyMap$1(),
59625
60616
  instances: emptyMap$1(),
59626
60617
  exiting: emptySet(),
60618
+ leading: emptySet(),
59627
60619
  removing: emptySet(),
59628
60620
  hosts: emptySet(),
59629
60621
  hosted: emptyMap$1(),
@@ -59979,19 +60971,48 @@ function load(bundle) {
59979
60971
  };
59980
60972
  }
59981
60973
  /**
59982
- * Binds `defineBundles` and `load` to one game's bundle keys. Type-only: the same functions.
60974
+ * Creates the effect a node awaits to rest on bundles and stay answerable (F52). The handler of the
60975
+ * kind `"ready"` is registered by this plugin and runs in fast mode too. It answers `true` when every
60976
+ * bundle is loaded and its textures went to the GPU (`assets.warm`), or its last load failed, so a
60977
+ * node never hangs on it. Otherwise it starts what is missing, answers `false`, and posts the world
60978
+ * event `ready` into `flow.inbox` once all of it settled: the node rests on `inbox: ["ready"]`
60979
+ * meanwhile, and Back still answers. A headless `assets` answers `true`; an app without `assets`,
60980
+ * such as the headless app of `game.headless()`, resolves `undefined`, hence `=== false`. A name
60981
+ * the manifest lacks throws, as for `load`.
59983
60982
  *
59984
- * @returns The two helpers whose keys are checked by the compiler.
60983
+ * @param bundle - One bundle name, or a list of them.
60984
+ * @returns The descriptor a transit node awaits.
59985
60985
  * @example
59986
60986
  * ```ts
59987
- * const { load } = bundlesFor<"board" | "board.chains">();
60987
+ * ready("ep01.memory"); // { kind: "ready", payload: { bundles: ["ep01.memory"] } }
60988
+ * // The transit node in front of the rest node `wait` (inbox: ["elapsed", "ready"]):
60989
+ * if ((await fx(ready(["ep01.memory", "ui"]))) === false) return out.wait(); // false: still loading
60990
+ * return out.open(); // true: loaded and on the GPU
60991
+ * ```
60992
+ */
60993
+ function ready(bundle) {
60994
+ return {
60995
+ kind: "ready",
60996
+ payload: { bundles: typeof bundle === "string" ? [bundle] : [...bundle] }
60997
+ };
60998
+ }
60999
+ /**
61000
+ * Binds `defineBundles`, `load` and `ready` to one game's bundle keys. Type-only: the same
61001
+ * functions.
61002
+ *
61003
+ * @returns The three helpers whose keys are checked by the compiler.
61004
+ * @example
61005
+ * ```ts
61006
+ * const { load, ready } = bundlesFor<"board" | "board.chains">();
59988
61007
  * load("board.chains"); // { kind: "load", payload: { bundles: ["board.chains"] } }
61008
+ * ready("board"); // { kind: "ready", payload: { bundles: ["board"] } }
59989
61009
  * ```
59990
61010
  */
59991
61011
  function bundlesFor() {
59992
61012
  return {
59993
61013
  defineBundles,
59994
- load
61014
+ load,
61015
+ ready
59995
61016
  };
59996
61017
  }
59997
61018
  //#endregion
@@ -61197,6 +62218,7 @@ function cubicBezier(x1, y1, x2, y2) {
61197
62218
  * | renderer | monitor | { budgetMs: "rate", frames: 1024, stillFrames: 60, stillMpx: 0.25, churnPerSecond: 4, churnSeconds: 3, churnAfterTrimMs: 1000 }, the performance log of an agent build, the thresholds of its `problem` rules and `sections`, the sections it starts with (`MonitorSections`, every one on when left out); shallow merge: a game that sets `monitor` gives `budgetMs` and `frames`, a threshold it leaves out keeps its default, a section it leaves out is on |
61198
62219
  * | renderer | keptMb | 64, MiB the kept pictures may hold: a filtered view that did not change is drawn from its picture, the least recently drawn goes first; 0 keeps none |
61199
62220
  * | renderer | warm | 1, probe draws a drawn frame that create the GPU pipelines while the game loads; 0 creates each in the first frame that needs it |
62221
+ * | renderer | uploads | 1, texture sources `sync.textures.upload` puts on the GPU a drawn frame (~9 ms each on the A26); 0 turns the queue off |
61200
62222
  * | input | tapSlopPx | 12 |
61201
62223
  * | input | longPressMs | 450 |
61202
62224
  * | input | dragStartPx | 8 |
@@ -61354,4 +62376,4 @@ const screen = [
61354
62376
  uiPlugin
61355
62377
  ];
61356
62378
  //#endregion
61357
- export { spawn as $, exit as $t, types_exports$11 as A, Outline as At, textPlugin as B, rendererPlugin as Bt, types_exports$4 as C, Emitter as Ct, types_exports$8 as D, Displacement as Dt, types_exports$7 as E, ColorMatrix as Et, types_exports$16 as F, assetsPlugin as Ft, frames as G, PERF_GAP_BUCKETS as Gt, HAPTIC_KINDS as H, frameAt as Ht, types_exports$17 as I, animPlugin as It, parallel as J, SHADER_WHATS as Jt, haptic as K, PERF_LOG_VERSION as Kt, uiPlugin as L, AnimPlayer as Lt, types_exports$13 as M, debugPlugin as Mt, types_exports$14 as N, audioPlugin as Nt, types_exports$9 as O, Glow as Ot, types_exports$15 as P, scenesPlugin as Pt, sfx as Q, refuseStart as Qt, resolve as R, Animation as Rt, types_exports$3 as S, effectsPlugin as St, types_exports$6 as T, Blur as Tt, defineAnimation as U, MONITOR_SECTIONS as Ut, platformPlugin as V, PASS_KINDS as Vt, external as W, PERF_CAUSES as Wt, sequence as X, worldPlugin as Xt, play$1 as Y, DEMAND_REASONS as Yt, set as Z, flowPlugin as Zt, defineBundles as _, Traceable as _t, cubicBezier as a, lifecyclePlugin as an, inputPlugin as at, types_exports$1 as b, messageArgument as bt, hint as c, Held as ct, defineScene as d, PointerOver as dt, slot as en, spawned as et, tr as f, Pressable as ft, music as g, Touchable as gt, defineEmitter as h, Tappable as ht, screen as i, SaveUnreadableError as in, wait as it, types_exports$12 as j, defineFilter as jt, types_exports$10 as k, Noise as kt, schedule as l, Hovered as lt, flattenFeatures as m, Swipeable as mt, createPlugin as n, type as nn, tween as nt, defineEffect as o, timePlugin as on, Draggable as ot, defineFeature as p, Pressed as pt, mark as q, PROBLEM_RULES as qt, defineGame as r, modelPlugin as rn, use as rt, guide as s, clockPlugin as sn, DropTarget as st, createApp as t, to as tn, stagger as tt, projection as u, Pointer as ut, load as v, Traced as vt, types_exports$5 as w, Alpha as wt, types_exports$2 as x, messageDuration as xt, types_exports as y, i18nPlugin as yt, defineMotion as z, Frames as zt };
62379
+ export { sfx as $, refuseStart as $t, types_exports$10 as A, Noise as At, defineMotion as B, Frames as Bt, types_exports$3 as C, effectsPlugin as Ct, types_exports$7 as D, ColorMatrix as Dt, types_exports$6 as E, Blur as Et, types_exports$15 as F, scenesPlugin as Ft, external as G, PERF_CAUSES as Gt, platformPlugin as H, PASS_KINDS as Ht, types_exports$16 as I, assetsPlugin as It, mark as J, PROBLEM_RULES as Jt, frames as K, PERF_GAP_BUCKETS as Kt, types_exports$17 as L, animPlugin as Lt, types_exports$12 as M, defineFilter as Mt, types_exports$13 as N, debugPlugin as Nt, types_exports$8 as O, Displacement as Ot, types_exports$14 as P, audioPlugin as Pt, set as Q, flowPlugin as Qt, uiPlugin as R, AnimPlayer as Rt, types_exports$2 as S, messageDuration as St, types_exports$5 as T, Alpha as Tt, HAPTIC_KINDS as U, frameAt as Ut, textPlugin as V, rendererPlugin as Vt, defineAnimation as W, MONITOR_SECTIONS as Wt, play$1 as X, DEMAND_REASONS as Xt, parallel as Y, SHADER_WHATS as Yt, sequence as Z, worldPlugin as Zt, defineBundles as _, Touchable as _t, cubicBezier as a, SaveUnreadableError as an, wait as at, types_exports as b, i18nPlugin as bt, hint as c, clockPlugin as cn, DropTarget as ct, defineScene as d, Pointer as dt, exit as en, spawn as et, tr as f, PointerOver as ft, music as g, Tappable as gt, defineEmitter as h, Swipeable as ht, screen as i, modelPlugin as in, use as it, types_exports$11 as j, Outline as jt, types_exports$9 as k, Glow as kt, schedule as l, Held as lt, flattenFeatures as m, Pressed as mt, createPlugin as n, to as nn, stagger as nt, defineEffect as o, lifecyclePlugin as on, inputPlugin as ot, defineFeature as p, Pressable as pt, haptic as q, PERF_LOG_VERSION as qt, defineGame as r, type as rn, tween as rt, guide as s, timePlugin as sn, Draggable as st, createApp as t, slot as tn, spawned as tt, projection as u, Hovered as ut, load as v, Traceable as vt, types_exports$4 as w, Emitter as wt, types_exports$1 as x, messageArgument as xt, ready as y, Traced as yt, resolve as z, Animation as zt };