@moku-labs/game 0.15.9 → 0.15.11

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 (33) 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/{commands-5woFUxUr.d.mts → commands-C3yVtCg1.d.mts} +5 -5
  8. package/dist/control.d.mts +4 -4
  9. package/dist/{define-B8A5IoeS.d.mts → define-DbxBzO-G.d.mts} +2 -2
  10. package/dist/{headless-CrxofjLt.d.mts → headless-DHtavbqk.d.mts} +1 -1
  11. package/dist/{index-DT9aYVGd.d.mts → index-CsTMiO0x.d.mts} +3 -3
  12. package/dist/index.d.mts +4 -4
  13. package/dist/index.mjs +2 -2
  14. package/dist/inspect.d.mts +6 -6
  15. package/dist/inspect.mjs +1 -1
  16. package/dist/jsx-dev-runtime.d.mts +1 -1
  17. package/dist/jsx-runtime.d.mts +1 -1
  18. package/dist/{runtime-BZQQPq56.d.mts → runtime-Bxy2zoCG.d.mts} +1 -1
  19. package/dist/{sources-QCowxSpC.mjs → sources-CJgUypyC.mjs} +1 -1
  20. package/dist/{src-Cs1bP3v7.mjs → src-0m2mjRDI.mjs} +231 -55
  21. package/dist/testing.d.mts +4 -4
  22. package/dist/testing.mjs +1 -1
  23. package/dist/{types-DDmfkONF.d.mts → types-CpBnGqDU.d.mts} +3 -3
  24. package/dist/{types-i77Nrrv-.d.mts → types-CuEK0Vam.d.mts} +3733 -3719
  25. package/dist/{types-DK-IYk-s.d.mts → types-CveBhKTs.d.mts} +4 -4
  26. package/dist/{types-D8roM1u3.d.mts → types-D5Pmo8BD.d.mts} +5 -3
  27. package/dist/{types-YXykgTOp.d.mts → types-_a6-ALzz.d.mts} +2 -2
  28. package/dist/{visual-1doiBOD3.d.mts → visual-B5Sypqal.d.mts} +6 -6
  29. package/dist/{visual-CIUx49C0.mjs → visual-Dwfy9wDb.mjs} +6 -2
  30. package/dist/visual.d.mts +1 -1
  31. package/dist/visual.mjs +1 -1
  32. package/llms.txt +2 -2
  33. package/package.json +1 -1
@@ -2,7 +2,7 @@ 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
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 { A as Now, C as Sprite, E as componentsFor, F as resource, I as system, L as systemsFor, M as Tree, N as component, O as Exiting, R as tag, S as Shape, T as arcDefaults, _ as HudPart, b as OverlayLevel, c as Countdown, d as builtInStyles, g as Display, h as textFor, i as samePose, j as Order, k as Layer, l as Text, m as readStyle, n as drawnRect, o as visualOf$1, r as fitScaleOf, s as visualRectOf, t as CONTENT$1, v as NineSlice, w as Transform, x as Parent, y as Overlay } from "./visual-CIUx49C0.mjs";
5
+ import { A as Now, C as Sprite, E as componentsFor, F as resource, I as system, L as systemsFor, M as Tree, N as component, O as Exiting, R as tag, S as Shape, T as arcDefaults, _ as HudPart, b as OverlayLevel, c as Countdown, d as builtInStyles, g as Display, h as textFor, i as samePose, j as Order, k as Layer, l as Text, m as readStyle, n as drawnRect, o as visualOf$1, r as fitScaleOf, s as visualRectOf, t as CONTENT$1, v as NineSlice, w as Transform, x as Parent, y as Overlay } from "./visual-Dwfy9wDb.mjs";
6
6
  import { a as isDev } from "./dev-BNndV-hU.mjs";
7
7
  import { i as isComponentDefinition, n as flatten, r as defineComponent } from "./flatten-BkXuscAV.mjs";
8
8
  import { envPlugin, logPlugin } from "@moku-labs/common/browser";
@@ -16484,8 +16484,9 @@ function writeTakeKeys(record, now, box) {
16484
16484
  }
16485
16485
  /**
16486
16486
  * Takes or retakes the picture of a record: stages 1, 2 (when its first filter has inputs) and 3, at the phase of this
16487
- * frame. Refused, and the view drawn live, for a `Display` in the subtree without `onDemand`, empty bounds, a picture
16488
- * over 4096 device pixels, or a budget that has no room (P6.3, warned once).
16487
+ * frame. The bounds are cut to the filter area of the top when it holds one (F43). Refused, and the view drawn live,
16488
+ * for a `Display` in the subtree without `onDemand`, empty bounds, a picture over 4096 device pixels, or a budget that
16489
+ * has no room (P6.3, warned once).
16489
16490
  *
16490
16491
  * @param kctx - The kept context.
16491
16492
  * @param pixi - The module that draws.
@@ -16498,7 +16499,7 @@ function writeTakeKeys(record, now, box) {
16498
16499
  function takePicture(kctx, pixi, matrices, record, view, now) {
16499
16500
  const { top } = record;
16500
16501
  if (!kctx.ctx.config.onDemand && holdsDisplay(kctx, top)) return false;
16501
- const local = top.getLocalBounds();
16502
+ const local = clippedToArea(top.getLocalBounds(), top.filterArea);
16502
16503
  if (isEmptyBounds(local)) return false;
16503
16504
  const box = linearBounds(now.a, now.b, now.c, now.d, local);
16504
16505
  const x = gridSpan(now.tx + box.x0, now.tx + box.x1, now.rs);
@@ -16530,6 +16531,27 @@ function takePicture(kctx, pixi, matrices, record, view, now) {
16530
16531
  return ok;
16531
16532
  }
16532
16533
  /**
16534
+ * The local bounds of a top cut to its filter area, both in the top's own units: its filters draw nothing outside
16535
+ * the area. With no area the bounds are returned as they are. Pure.
16536
+ *
16537
+ * @param bounds - The top's local bounds.
16538
+ * @param area - The filter area on the top, or none.
16539
+ * @returns The bounds the picture takes.
16540
+ * @example
16541
+ * ```ts
16542
+ * clippedToArea({ minX: 0, minY: 0, maxX: 400, maxY: 400 }, { x: 100, y: 100, width: 40, height: 40 }); // { minX: 100, minY: 100, maxX: 140, maxY: 140 }
16543
+ * ```
16544
+ */
16545
+ function clippedToArea(bounds, area) {
16546
+ if (area === null || area === void 0) return bounds;
16547
+ return {
16548
+ minX: Math.max(bounds.minX, area.x),
16549
+ minY: Math.max(bounds.minY, area.y),
16550
+ maxX: Math.min(bounds.maxX, area.x + area.width),
16551
+ maxY: Math.min(bounds.maxY, area.y + area.height)
16552
+ };
16553
+ }
16554
+ /**
16533
16555
  * Tells whether local bounds hold nothing to draw: no width or no height. Pure.
16534
16556
  *
16535
16557
  * @param bounds - The bounds of a subtree in its own space.
@@ -25455,9 +25477,9 @@ function holdsLoops(state, range) {
25455
25477
  }
25456
25478
  /**
25457
25479
  * Tells whether the endless loops of a view should write nothing (the overlay cases of `loopsHeld`, O9.2): its view
25458
- * lies in a range an opaque overlay covers, or in a held picture of a blurred range, or in a blurred range the
25459
- * fallback draws live; the last two touch the range, so the held loops write once at landing and at the first exit
25460
- * frame.
25480
+ * lies in a range an opaque overlay covers, or in a held picture of a range that is blurred or whose owner has
25481
+ * `hold: true`, or in such a range the fallback draws live; all but the cover touch the range, so the held loops
25482
+ * write once at landing and at the first exit frame. Only the owner's `hold` counts: the range is the owner's.
25461
25483
  *
25462
25484
  * @param state - The sync state.
25463
25485
  * @param entity - The entity a loop is about to write.
@@ -25471,7 +25493,8 @@ function rangeHolds(state, entity) {
25471
25493
  if (view === void 0 || root === void 0) return false;
25472
25494
  if (!inRange(range.set, view.wrapper ?? view.object, root)) return false;
25473
25495
  if (range.reason === "cover") return true;
25474
- if (!range.blurred) return false;
25496
+ const ownerHolds = state.overlay.records.get(range.owner)?.value.hold === true;
25497
+ if (!range.blurred && !ownerHolds) return false;
25475
25498
  range.touched = true;
25476
25499
  return true;
25477
25500
  }
@@ -27329,7 +27352,9 @@ function displayObjectOf(sctx, entity) {
27329
27352
  }
27330
27353
  /**
27331
27354
  * Detaches the children of a parent that left, and names the ones that stay. A child that leaves in the same teardown
27332
- * is no orphan: its view goes in this very pass (it waits in `removed`), or it plays its exit (`Exiting`).
27355
+ * is no orphan: its view goes in this very pass (it waits in `removed`), or it plays its exit (`Exiting`). Nor is a
27356
+ * child whose `Parent` names another entity, or none, by now: it moved in the same frame (a hosted view a restore
27357
+ * hung under the rebuilt host, F44), and the later steps of this pass hang it where it belongs.
27333
27358
  *
27334
27359
  * @param sctx - Domain context of the sync module.
27335
27360
  * @param entity - The parent entity.
@@ -27337,7 +27362,6 @@ function displayObjectOf(sctx, entity) {
27337
27362
  */
27338
27363
  function detachChildren(sctx, entity, host) {
27339
27364
  const state = sctx.ctx.state.sync;
27340
- const ecs = sctx.ctx.deps.world.ecs;
27341
27365
  const orphans = [];
27342
27366
  for (const [child, childView] of state.views) {
27343
27367
  if (child === entity) continue;
@@ -27346,7 +27370,7 @@ function detachChildren(sctx, entity, host) {
27346
27370
  markView(sctx, child);
27347
27371
  detach$1(object);
27348
27372
  state.orphans.add(child);
27349
- if (!state.removed.has(child) && !isLeaving(ecs, child)) orphans.push(child);
27373
+ if (isLostOrphan(sctx, child, entity)) orphans.push(child);
27350
27374
  }
27351
27375
  if (orphans.length > 0) sctx.ctx.log.warn("renderer: parent left before its children", {
27352
27376
  parent: entity,
@@ -27354,6 +27378,19 @@ function detachChildren(sctx, entity, host) {
27354
27378
  });
27355
27379
  }
27356
27380
  /**
27381
+ * Tells whether a child of a parent that left is an orphan worth a warning: it does not go in this pass, it plays no
27382
+ * exit, and its `Parent` still names the parent that left.
27383
+ *
27384
+ * @param sctx - Domain context of the sync module.
27385
+ * @param child - The child entity.
27386
+ * @param parent - The parent that left.
27387
+ * @returns True when the child stays behind with no parent.
27388
+ */
27389
+ function isLostOrphan(sctx, child, parent) {
27390
+ const ecs = sctx.ctx.deps.world.ecs;
27391
+ return !sctx.ctx.state.sync.removed.has(child) && !isLeaving(ecs, child) && parentOf$1(ecs, child) === parent;
27392
+ }
27393
+ /**
27357
27394
  * Takes a view out of the tree and gives its object back to the pool.
27358
27395
  *
27359
27396
  * @param sctx - Domain context of the sync module.
@@ -28857,7 +28894,8 @@ function isKeyList(value) {
28857
28894
  }
28858
28895
  /**
28859
28896
  * Copies the marker into the record's own value, clamped (O1): `dim` into [0, 1], `fade` to 0 or more, an unknown
28860
- * `cover` as `"none"`, a `hud` that is not `"all"`, `"none"` or a key list as `"all"`; one dev warning per field.
28897
+ * `cover` as `"none"`, a `hud` that is not `"all"`, `"none"` or a key list as `"all"`, a `hold` that is no boolean as
28898
+ * `false`; one dev warning per field.
28861
28899
  *
28862
28900
  * @param sctx - Domain context of the sync module.
28863
28901
  * @param entity - The marked entity.
@@ -28869,15 +28907,18 @@ function clampInto(sctx, entity, into, raw) {
28869
28907
  const fade = Number.isFinite(raw.fade) ? Math.max(0, raw.fade) : 0;
28870
28908
  const cover = raw.cover === "opaque" ? "opaque" : "none";
28871
28909
  const hud = raw.hud === "all" || raw.hud === "none" || isKeyList(raw.hud) ? raw.hud : "all";
28910
+ const hold = raw.hold === true;
28872
28911
  if (dim !== raw.dim) warnOnce$4(sctx, "renderer:overlay-value", entity, { field: "dim" });
28873
28912
  if (fade !== raw.fade) warnOnce$4(sctx, "renderer:overlay-value", entity, { field: "fade" });
28874
28913
  if (cover !== raw.cover) warnOnce$4(sctx, "renderer:overlay-value", entity, { field: "cover" });
28875
28914
  if (hud !== raw.hud) warnOnce$4(sctx, "renderer:overlay-value", entity, { field: "hud" });
28915
+ if (hold !== raw.hold) warnOnce$4(sctx, "renderer:overlay-value", entity, { field: "hold" });
28876
28916
  into.dim = dim;
28877
28917
  into.dimColor = Number.isFinite(raw.dimColor) ? raw.dimColor : 0;
28878
28918
  into.cover = cover;
28879
28919
  into.hud = hud;
28880
28920
  into.fade = fade;
28921
+ into.hold = hold;
28881
28922
  }
28882
28923
  /**
28883
28924
  * The root of an entity's `Parent` chain, depth-bounded.
@@ -32939,9 +32980,23 @@ function startTrack$1(actx, entity, component, to, options, muted, driven = fals
32939
32980
  countPlaying(actx.deps.world.ecs, entity, 1);
32940
32981
  actx.deps.time.wake();
32941
32982
  warnIfCrowded(actx);
32983
+ if (landsAtStart(track, options)) writeFields(actx, track, true);
32942
32984
  return track;
32943
32985
  }
32944
32986
  /**
32987
+ * Tells whether a track writes its target as it starts: an absolute one of 0 ms with no delay,
32988
+ * no repeat and no keys, started so by its caller (not cut by reduced motion). A rest pose a
32989
+ * caller sets in phase `layout` so lands with the fields written beside it in that frame (F47);
32990
+ * the next frame step ends it, where its `Animation` count lands too.
32991
+ *
32992
+ * @param track - The track that starts.
32993
+ * @param options - What its caller asked for.
32994
+ * @returns True when it lands now.
32995
+ */
32996
+ function landsAtStart(track, options) {
32997
+ return options.ms === 0 && track.delayMs === 0 && !track.additive && !track.driven && track.repeatsLeft === 0 && track.segments === void 0;
32998
+ }
32999
+ /**
32945
33000
  * Tells whether a loop stands on its first key instead of walking: with reduced motion on, unless
32946
33001
  * the author kept it, or when it has no length to walk. It stays in the table either way, so it
32947
33002
  * never ends by itself.
@@ -42352,16 +42407,20 @@ function boxCovers(ectx, entity, width, height, size) {
42352
42407
  return width * scale >= size.width && height * scale >= size.height;
42353
42408
  }
42354
42409
  /**
42355
- * Tells whether a filtered view covers the whole viewport: the box its sprite, its nine-slice or
42356
- * its shape declares, times its scale, covers the reference size on both axes. A sprite without a
42357
- * size declares none. Read from components, never from Pixi bounds.
42410
+ * Tells whether a filtered view covers the whole viewport. A view whose enabled filters declare an
42411
+ * area draws only there: that area, times the view's scale, covers the reference size on both axes
42412
+ * (F43, a lens over a full-screen sprite is not full screen). Otherwise the box its sprite, its
42413
+ * nine-slice or its shape declares, times its scale, does. A sprite without a size declares none.
42414
+ * Read from components and the area handed over last, never from Pixi bounds.
42358
42415
  *
42359
42416
  * @param ectx - Domain context of the effects plugin.
42360
- * @param entity - The entity.
42417
+ * @param view - The view's record.
42361
42418
  * @param size - The viewport size.
42362
42419
  * @returns True for a full-screen view.
42363
42420
  */
42364
- function coversScreen(ectx, entity, size) {
42421
+ function coversScreen(ectx, view, size) {
42422
+ const { entity, areaSent } = view;
42423
+ if (areaSent.some) return boxCovers(ectx, entity, areaSent.width, areaSent.height, size);
42365
42424
  const ecs = ectx.deps.world.ecs;
42366
42425
  const sprite = ecs.get(entity, Sprite);
42367
42426
  if (sprite !== void 0) {
@@ -42436,7 +42495,7 @@ function countFullScreen(ectx, size) {
42436
42495
  let count = 0;
42437
42496
  for (const view of ectx.state.viewList) {
42438
42497
  if (view.gone) continue;
42439
- if (hasEnabledFilter(ectx, view) && coversScreen(ectx, view.entity, size)) count += 1;
42498
+ if (hasEnabledFilter(ectx, view) && coversScreen(ectx, view, size)) count += 1;
42440
42499
  }
42441
42500
  return count;
42442
42501
  }
@@ -49353,24 +49412,37 @@ function createTextSystem(ctx, frame) {
49353
49412
  });
49354
49413
  }
49355
49414
  /**
49356
- * Resolves the labels whose `Text` came after the layout system ran. `ui` adds a label in its
49357
- * solve, and the flush at the end of phase `layout` attaches it with the rest of its batch, a bound
49358
- * component included; this runs right after that flush and before phase `sync`, so the renderer
49359
- * builds the label with its string, and lists it, so the next system run writes its `Text` once
49360
- * more for the remeasure of `ui`. A label whose content changed is not among them: it resolves in
49361
- * the next layout phase, as before. While the world is not live its layout phase did not run: the
49362
- * list is dropped, and the labels, marked, wait for the next live run, as every label does.
49415
+ * Resolves the labels whose `Text` came or changed after the layout system ran. `ui` adds a label in
49416
+ * its solve, and the flush at the end of phase `layout` attaches it with the rest of its batch, a
49417
+ * bound component included; this runs right after that flush and before phase `sync`, so the
49418
+ * renderer builds the label with its string, and lists it, so the next system run writes its `Text`
49419
+ * once more for the remeasure of `ui`. A label whose content or style `ui` changed in its reconcile
49420
+ * is resolved here too, in the frame of the change (F45): `ui` measured its content itself, so it is
49421
+ * not listed. While the world is not live its layout phase did not run: the list is dropped, and the
49422
+ * labels, marked, wait for the next live run, as every label does.
49363
49423
  *
49364
49424
  * @param ctx - Domain context of the text plugin.
49365
49425
  * @param frame - What the system's runs share.
49366
49426
  */
49367
49427
  function resolveAdded(ctx, frame) {
49368
49428
  const added = ctx.state.added;
49369
- if (added.length === 0) return;
49370
- if (ctx.deps.world.ecs.mode() !== "live") {
49371
- added.length = 0;
49372
- return;
49429
+ if (added.length > 0) {
49430
+ if (ctx.deps.world.ecs.mode() !== "live") {
49431
+ added.length = 0;
49432
+ return;
49433
+ }
49434
+ resolveCame(ctx, frame, added);
49373
49435
  }
49436
+ resolveChanged(ctx, frame);
49437
+ }
49438
+ /**
49439
+ * Resolves the labels whose `Text` came with the layout flush, and lists them for the remeasure.
49440
+ *
49441
+ * @param ctx - Domain context of the text plugin.
49442
+ * @param frame - What the system's runs share.
49443
+ * @param added - The labels that came.
49444
+ */
49445
+ function resolveCame(ctx, frame, added) {
49374
49446
  frame.now = NaN;
49375
49447
  frame.locale = ctx.deps.i18n.locale();
49376
49448
  for (const entity of added) {
@@ -49382,6 +49454,25 @@ function resolveAdded(ctx, frame) {
49382
49454
  forgetAdded(added);
49383
49455
  }
49384
49456
  /**
49457
+ * Resolves the labels of this frame's `changed(Text)` whose content or style moved since they were
49458
+ * resolved: `ui` writes `Text` after the layout system. The labels this frame resolved already
49459
+ * stand on what they show; a bound label shows its value, as in the system.
49460
+ *
49461
+ * @param ctx - Domain context of the text plugin.
49462
+ * @param frame - What the system's runs share; the locale of this frame is already in it.
49463
+ */
49464
+ function resolveChanged(ctx, frame) {
49465
+ const ecs = ctx.deps.world.ecs;
49466
+ let live = false;
49467
+ for (const entity of ecs.changed(Text)) {
49468
+ const text = ecs.get(entity, Text);
49469
+ if (text === void 0 || !isStale(ctx, entity, text, frame)) continue;
49470
+ if (!live && ecs.mode() !== "live") return;
49471
+ live = true;
49472
+ stepText(ctx, entity, text, frame);
49473
+ }
49474
+ }
49475
+ /**
49385
49476
  * Marks labels for the next layout phase: every one of them, or only the ones `keep` names. A
49386
49477
  * locale change names the labels whose content is a message; a dev hot swap names the labels
49387
49478
  * drawn with a replaced font or icon.
@@ -53973,7 +54064,10 @@ function recordRest(ctx, element, value) {
53973
54064
  * @param name - The component name of the extra.
53974
54065
  */
53975
54066
  function cancelExtra(element, name) {
53976
- element.extraHandles.get(name)?.cancel();
54067
+ const last = element.extraHandles.get(name);
54068
+ if (last === void 0) return;
54069
+ last.cancel();
54070
+ element.endless?.delete(last);
53977
54071
  element.extraHandles.delete(name);
53978
54072
  }
53979
54073
  //#endregion
@@ -55687,10 +55781,11 @@ function createReconciler(ctx, modules, fields) {
55687
55781
  */
55688
55782
  function subtreeSettled(element) {
55689
55783
  if (!modules.layout.settled(element)) return false;
55690
- return element.children.every((child) => {
55784
+ for (const child of element.children) {
55691
55785
  const childElement = state.elements.get(child);
55692
- return childElement === void 0 || subtreeSettled(childElement);
55693
- });
55786
+ if (childElement !== void 0 && !subtreeSettled(childElement)) return false;
55787
+ }
55788
+ return true;
55694
55789
  }
55695
55790
  /**
55696
55791
  * Drops the identity, the key and the key registration of an element and of everything under
@@ -55745,6 +55840,24 @@ function createReconciler(ctx, modules, fields) {
55745
55840
  if (!subtreeSettled(element)) continue;
55746
55841
  despawnTree(element);
55747
55842
  }
55843
+ dropRemovedRoots();
55844
+ }
55845
+ /**
55846
+ * Despawns every exiting element at once, finished or not, and drops the roots that were leaving:
55847
+ * what a restore or a load does, as a page reload would (F42).
55848
+ */
55849
+ function dropLeaving() {
55850
+ for (const entity of state.exiting) {
55851
+ const element = state.elements.get(entity);
55852
+ if (element === void 0) state.exiting.delete(entity);
55853
+ else despawnTree(element);
55854
+ }
55855
+ dropRemovedRoots();
55856
+ }
55857
+ /**
55858
+ * Drops every removed root whose element tree is gone, and closes its popup.
55859
+ */
55860
+ function dropRemovedRoots() {
55748
55861
  for (const entity of state.removing) {
55749
55862
  const root = state.roots.get(entity);
55750
55863
  if (root === void 0) continue;
@@ -56079,11 +56192,12 @@ function createReconciler(ctx, modules, fields) {
56079
56192
  * element stands exactly as if its root had just been mounted with the restored state: a new
56080
56193
  * entity with the components of the description, then its `enter` hook, then its loop. What a
56081
56194
  * mount does not bring is gone with the old tree: the local state of a component, a scroll
56082
- * offset, the keyboard focus, a text being edited, a hover. A root that is leaving keeps
56083
- * leaving.
56195
+ * offset, the keyboard focus, a text being edited, a hover. An element or a root that is
56196
+ * leaving is dropped at once, as a page reload drops it.
56084
56197
  */
56085
56198
  function remountRoots() {
56086
56199
  state.remount = false;
56200
+ dropLeaving();
56087
56201
  for (const root of state.roots.values()) {
56088
56202
  if (state.removing.has(root.entity)) continue;
56089
56203
  const element = root.element === void 0 ? void 0 : lookup(root.element);
@@ -56247,8 +56361,8 @@ function createReconciler(ctx, modules, fields) {
56247
56361
  /**
56248
56362
  * The second system of phase `layout`: every root takes its slot, then one solve per marked
56249
56363
  * root, a root in a slot after the root of its slot and in its content box, then the rects, the
56250
- * rest poses and the hooks, the scrolls settled on what the solve and the render gave them, and
56251
- * the hosting of the hosts that came. The hints released since the last frame step are dropped
56364
+ * rest poses and the hooks, the scrolls settled on what the solve and the render gave them, the
56365
+ * hosting of the hosts that came, and a sweep of what left with nothing to wait for. The hints released since the last frame step are dropped
56252
56366
  * at its end, so the `change.Box` and `change.Transform` hooks the solve plays get the hint the
56253
56367
  * reconcile routed, and the next frame starts with none.
56254
56368
  *
@@ -56268,6 +56382,7 @@ function createReconciler(ctx, modules, fields) {
56268
56382
  hostViews(ctx, state, state.risen);
56269
56383
  state.risen.clear();
56270
56384
  }
56385
+ if (state.exiting.size > 0 || state.removing.size > 0) sweep();
56271
56386
  state.hints.length = 0;
56272
56387
  writeCounters();
56273
56388
  }
@@ -56850,14 +56965,15 @@ function noteTrack(span, options) {
56850
56965
  span.reduced = options.reduced;
56851
56966
  }
56852
56967
  /**
56853
- * The view handle of a hook whose tracks are measured: every call goes to the real handle, `tween` and `toRest` note
56854
- * their track first.
56968
+ * The view handle of a watched hook: every call goes to the real handle; `tween` notes whether its track repeats
56969
+ * forever, and with a span `tween` and `toRest` note their track for the level of an overlay.
56855
56970
  *
56856
56971
  * @param handle - The real handle.
56857
- * @param span - The span being measured.
56858
- * @returns The measuring handle.
56972
+ * @param span - The span being measured, or nothing.
56973
+ * @param watch - Where a track that repeats forever is noted.
56974
+ * @returns The watching handle.
56859
56975
  */
56860
- function spanned(handle, span) {
56976
+ function watched(handle, span, watch) {
56861
56977
  return {
56862
56978
  entity: handle.entity,
56863
56979
  key: handle.key,
@@ -56865,11 +56981,12 @@ function spanned(handle, span) {
56865
56981
  rest: (component) => handle.rest(component),
56866
56982
  set: (component, patch) => handle.set(component, patch),
56867
56983
  tween: (component, to, options) => {
56868
- noteTrack(span, options);
56984
+ if (options.repeat === "forever") watch.forever = true;
56985
+ if (span !== void 0) noteTrack(span, options);
56869
56986
  return handle.tween(component, to, options);
56870
56987
  },
56871
56988
  toRest: (component, options) => {
56872
- noteTrack(span, options);
56989
+ if (span !== void 0) noteTrack(span, options);
56873
56990
  return handle.toRest(component, options);
56874
56991
  },
56875
56992
  all: (handles) => handle.all(handles),
@@ -56878,8 +56995,9 @@ function spanned(handle, span) {
56878
56995
  }
56879
56996
  /**
56880
56997
  * Runs one motion hook and keeps the handle it returned, so the exit sweep knows when the
56881
- * element is still playing. Without a hook the value the solve wrote stands; a throwing hook is
56882
- * logged and the same happens.
56998
+ * element is still playing. A motion whose hook started a track that repeats forever is also
56999
+ * noted as endless: the sweep never waits for it. Without a hook the value the solve wrote stands;
57000
+ * a throwing hook is logged and the same happens.
56883
57001
  *
56884
57002
  * @param ctx - Domain context of the ui plugin.
56885
57003
  * @param element - The element the hook belongs to.
@@ -56892,9 +57010,11 @@ function play(ctx, element, hook, span) {
56892
57010
  const handle = handleOf(ctx, element);
56893
57011
  if (handle === void 0) return void 0;
56894
57012
  try {
56895
- const motion = hook(span === void 0 ? handle : spanned(handle, span));
57013
+ const watch = { forever: false };
57014
+ const motion = hook(watched(handle, span, watch));
56896
57015
  if (motion === void 0) return void 0;
56897
57016
  element.handles.push(motion);
57017
+ if (watch.forever) noteEndless(element, motion);
56898
57018
  return motion;
56899
57019
  } catch (error) {
56900
57020
  ctx.log.error("ui:motion-failed", {
@@ -56905,6 +57025,16 @@ function play(ctx, element, hook, span) {
56905
57025
  }
56906
57026
  }
56907
57027
  /**
57028
+ * Notes a motion that never ends on its element, the set made with the first one.
57029
+ *
57030
+ * @param element - The element the motion plays on.
57031
+ * @param motion - The endless motion.
57032
+ */
57033
+ function noteEndless(element, motion) {
57034
+ element.endless ??= /* @__PURE__ */ new Set();
57035
+ element.endless.add(motion);
57036
+ }
57037
+ /**
56908
57038
  * Plays the `change` hook of one extra component whose fields changed. The motion the last change
56909
57039
  * of that name started is cancelled first, so its value stays where it is and the new motion
56910
57040
  * starts from there; the new motion is kept with the element's handles, so an exit waits for it.
@@ -56919,8 +57049,12 @@ function play(ctx, element, hook, span) {
56919
57049
  * @returns True when the hook returned a motion: the caller writes nothing directly.
56920
57050
  */
56921
57051
  function playExtraChange(ctx, element, name, values, hint) {
56922
- element.extraHandles.get(name)?.cancel();
56923
- element.extraHandles.delete(name);
57052
+ const last = element.extraHandles.get(name);
57053
+ if (last !== void 0) {
57054
+ last.cancel();
57055
+ element.endless?.delete(last);
57056
+ element.extraHandles.delete(name);
57057
+ }
56924
57058
  const hook = element.motion?.change?.[name];
56925
57059
  if (hook === void 0) return false;
56926
57060
  const motion = play(ctx, element, (view) => hook(view, values.previous, values.next, hint));
@@ -56956,19 +57090,23 @@ function startLoop(ctx, element) {
56956
57090
  }
56957
57091
  /**
56958
57092
  * Tells whether every motion of an element has finished, which is when an exiting element may
56959
- * be despawned.
57093
+ * be despawned. A motion over a track that repeats forever never finishes, so it does not count:
57094
+ * waiting for it would keep the element for the life of the page (F42).
56960
57095
  *
56961
57096
  * @param element - The element to ask about.
56962
- * @returns True when nothing is still playing.
57097
+ * @returns True when nothing that can end is still playing.
56963
57098
  */
56964
57099
  function still(element) {
56965
- return element.handles.every((handle) => !handle.active());
57100
+ const { handles, endless } = element;
57101
+ for (const handle of handles) if (handle.active() && endless?.has(handle) !== true) return false;
57102
+ return true;
56966
57103
  }
56967
57104
  /**
56968
57105
  * Moves the rest `Transform` of an element to where its rect, style and fit put it now. A live
56969
57106
  * element plays its `change.Transform` motion when it has one, else takes the pose through a
56970
- * 0 ms rest track, which lands on the next frame step and lets an additive loop re-base on the
56971
- * new rest; an element that has not spawned yet only records it, and so does one whose
57107
+ * 0 ms rest track. That track writes the pose as it starts, so the pivot and the position land in
57108
+ * the same frame, and ends at the next frame step, where an additive loop re-bases on the new
57109
+ * rest. An element that has not spawned yet only records it, and so does one whose
56972
57110
  * `change.Box` motion already played. The content of a scroll keeps its `Transform` and records
56973
57111
  * no rest with the world: the scroll step owns it.
56974
57112
  *
@@ -57081,10 +57219,14 @@ function placeChildren(state, parent, children) {
57081
57219
  * @file ui/layout — exiting elements, policy "leave": the element leaves the flow in the same
57082
57220
  * reconcile, so its siblings reflow this frame while it plays its exit over them.
57083
57221
  */
57222
+ /** The dev warning of an element that left while a motion of it repeated forever. */
57223
+ const EXIT_WAITS_FOREVER = "ui:exit-waits-forever";
57084
57224
  /**
57085
57225
  * Starts the exit of one element: out of the Yoga tree at once, tagged `Exiting` so `input`
57086
57226
  * never hits it, then the hook plays over the siblings that already moved. A `change` motion of
57087
57227
  * an extra component that still runs, a counter's roll, is kept, so the sweep waits for it too.
57228
+ * One that repeats forever, a hot pulse, is cancelled where it stands: it would never end, and a
57229
+ * dev build names the element once (F42).
57088
57230
  *
57089
57231
  * @param ctx - Domain context of the ui plugin.
57090
57232
  * @param state - The layout state.
@@ -57098,11 +57240,45 @@ function beginExit(ctx, state, element, cause, span) {
57098
57240
  element.dropKey = void 0;
57099
57241
  if (!element.live) return;
57100
57242
  ctx.deps.world.ecs.tag(element.entity, Exiting);
57101
- element.handles = [...element.extraHandles.values()].filter((handle) => handle.active());
57243
+ element.handles = keptExtras(ctx, element);
57102
57244
  const hook = element.motion?.exit;
57103
57245
  play(ctx, element, hook === void 0 ? void 0 : (view) => hook(view, element.node, void 0, cause), span);
57104
57246
  }
57105
57247
  /**
57248
+ * The `change` motions of the extras an exit waits for: the ones still running that can end. One
57249
+ * that repeats forever is cancelled where it stands, and a dev build warns once per element.
57250
+ *
57251
+ * @param ctx - Domain context of the ui plugin.
57252
+ * @param element - The element that starts its exit.
57253
+ * @returns The motions the sweep waits for.
57254
+ */
57255
+ function keptExtras(ctx, element) {
57256
+ const kept = [];
57257
+ for (const handle of element.extraHandles.values()) {
57258
+ if (!handle.active()) continue;
57259
+ if (element.endless?.has(handle) !== true) {
57260
+ kept.push(handle);
57261
+ continue;
57262
+ }
57263
+ handle.cancel();
57264
+ warnEndless(ctx, element);
57265
+ }
57266
+ return kept;
57267
+ }
57268
+ /**
57269
+ * Names an element that left while a motion of it repeated forever, once per element, in a dev
57270
+ * build: the motion is cut where it stands, which the author may want to end in the exit instead.
57271
+ *
57272
+ * @param ctx - Domain context of the ui plugin.
57273
+ * @param element - The leaving element.
57274
+ */
57275
+ function warnEndless(ctx, element) {
57276
+ if (typeof __MOKU_GAME_DEV__ === "undefined" || !__MOKU_GAME_DEV__) return;
57277
+ if (element.warned.has(EXIT_WAITS_FOREVER)) return;
57278
+ element.warned.add(EXIT_WAITS_FOREVER);
57279
+ ctx.log.warn(EXIT_WAITS_FOREVER, { key: element.key ?? element.identity });
57280
+ }
57281
+ /**
57106
57282
  * Tells whether an exiting element may be despawned: no hook, or every hook finished.
57107
57283
  *
57108
57284
  * @param element - The exiting element.
@@ -1,7 +1,7 @@
1
- import { Cr as Json, Yn as FeaturePlugin, Zn as AnyFlow, _r as FakeClock, dr as Hint, fr as NodeFx, hr as ClockSource, rr as NodeTable, sr as Descriptor } from "./types-i77Nrrv-.mjs";
2
- import { t as createApp } from "./index-DT9aYVGd.mjs";
3
- import { b as saveOf, v as WithGameState, y as memory } from "./types-DK-IYk-s.mjs";
4
- import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-CrxofjLt.mjs";
1
+ import { Qn as AnyFlow, Xn as FeaturePlugin, cr as Descriptor, fr as Hint, gr as ClockSource, ir as NodeTable, pr as NodeFx, vr as FakeClock, wr as Json } from "./types-CuEK0Vam.mjs";
2
+ import { t as createApp } from "./index-CsTMiO0x.mjs";
3
+ import { b as saveOf, v as WithGameState, y as memory } from "./types-CveBhKTs.mjs";
4
+ import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-DHtavbqk.mjs";
5
5
  import { AnyPluginInstance } from "@moku-labs/core";
6
6
 
7
7
  //#region src/plugins/clock/fake.d.ts
package/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { an as refuseStart, ln as type, m as flattenFeatures, on as exit, t as createApp } from "./src-Cs1bP3v7.mjs";
1
+ import { an as refuseStart, ln as type, m as flattenFeatures, on as exit, t as createApp } from "./src-0m2mjRDI.mjs";
2
2
  import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
3
3
  import { t as fakeClock } from "./fake-6mazmHK3.mjs";
4
4
  import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-BA6ffzqD.mjs";
@@ -1,5 +1,5 @@
1
- import { $r as Require, A as PixiModule, An as TrackOptions, At as FilterSlotInputs, Cr as Json, E as Api$4, En as NumericFields, M as PixiTexture, Mt as Point, Nn as Api$2, Pt as FilterCache, Qr as Events$2, Qt as Api$3, Sn as Ease, Sr as Events$3, Tn as MotionHandle, Ur as Api$1, an as AnyComponentValue, br as Api$6, cn as ComponentType, j as PixiSprite, jn as TrackSegment, k as PixiFilter, kn as ReducedMotion, kt as FilterSlot, on as ComponentHandle, or as TypeTag, rn as AnyComponent, s as Events$1, sr as Descriptor, t as Api$5, un as Entity, v as Events$4, zr as Events$5 } from "./types-i77Nrrv-.mjs";
2
- import { C as TextApi, L as I18nApi, M as InputApi, Y as StylesState, c as UiNode, i as Finding, n as ElementChange, o as JsxState, r as ElementMotion } from "./types-D8roM1u3.mjs";
1
+ import { $r as Events$2, $t as Api$3, An as ReducedMotion, Br as Events$5, Cn as Ease, Cr as Events$3, D as FilterSlotInputs, Dn as NumericFields, E as FilterSlot, En as MotionHandle, K as Api$4, Mn as TrackSegment, O as SyncApi, Pn as Api$2, Q as PixiTexture, Wr as Api$1, X as PixiModule, Y as PixiFilter, Z as PixiSprite, cr as Descriptor, dn as Entity, ei as Require, in as AnyComponent, jn as TrackOptions, ln as ComponentType, lt as FilterCache, on as AnyComponentValue, s as Events$1, sn as ComponentHandle, sr as TypeTag, st as Point, t as Api$5, v as Events$4, wr as Json, xr as Api$6 } from "./types-CuEK0Vam.mjs";
2
+ import { C as TextApi, L as I18nApi, M as InputApi, Y as StylesState, c as UiNode, i as Finding, n as ElementChange, o as JsxState, r as ElementMotion } from "./types-D5Pmo8BD.mjs";
3
3
  import { Log } from "@moku-labs/common/browser";
4
4
  import { PluginCtx } from "@moku-labs/core";
5
5
  import { Node, Yoga } from "yoga-layout/load";
@@ -1093,7 +1093,7 @@ type State$3 = {
1093
1093
  * nothing while its clock runs on (spec 22 O9), told what the loop reveals (spec 26 R1.7). `undefined` before the
1094
1094
  * start and after the stop: nothing is held.
1095
1095
  */
1096
- loopsHeld: ((entity: Entity, reveals?: "alpha" | "place" | "both") => boolean) | undefined;
1096
+ loopsHeld: SyncApi["loopsHeld"] | undefined;
1097
1097
  };
1098
1098
  /**
1099
1099
  * anim plugin API, `app.anim`: play a choreography, end everything, count what runs and listen