@moku-labs/game 0.15.4 → 0.15.6

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.
@@ -9719,6 +9719,44 @@ function collectView(pctx, spec, item) {
9719
9719
  return values;
9720
9720
  }
9721
9721
  /**
9722
+ * Tells whether a view is a screen root that draws at the place of its projection in a list: its
9723
+ * view described a node, so `collectView` gave it a `Tree` and the `Order` of that place. A view
9724
+ * that returned its own components keeps whatever `Order` it gave itself.
9725
+ *
9726
+ * @param pctx - Domain context of the projection module.
9727
+ * @param view - A live view of the projection.
9728
+ * @param order - The `Order` of the place the projection had.
9729
+ * @returns True when the view draws at that place.
9730
+ */
9731
+ function drawsAtPlace(pctx, view, order) {
9732
+ const { Order, Tree } = pctx.deps.components;
9733
+ const rest = view.rest.get(Order.componentName)?.value;
9734
+ return view.rest.has(Tree.componentName) && typeof rest === "object" && "value" in rest && rest.value === order;
9735
+ }
9736
+ /**
9737
+ * Keeps a mounted projection for the scene that comes, as a scene switch does for a projection
9738
+ * both scenes list: it stays mounted into the new layer list, and its screen roots take the
9739
+ * `Order` of their place in the new list, on the entity and in the rest pose, so the next node of
9740
+ * a root compares equal and writes none. Nothing else is touched: no view is spawned or
9741
+ * despawned, no motion is cut, no hook runs. A view on its way out keeps the order it leaves with.
9742
+ *
9743
+ * @param pctx - Domain context of the projection module.
9744
+ * @param mounted - The mounted projection the new list names again.
9745
+ * @param order - The `Order` of its place in the new list.
9746
+ */
9747
+ function placeMounted(pctx, mounted, order) {
9748
+ const previous = mounted.order;
9749
+ mounted.layers = pctx.ctx.state.projection.layers;
9750
+ if (previous === order) return;
9751
+ mounted.order = order;
9752
+ for (const view of mounted.live.values()) {
9753
+ if (!drawsAtPlace(pctx, view, previous)) continue;
9754
+ const value = pctx.deps.components.Order({ value: order });
9755
+ view.rest.set(value.type.componentName, value);
9756
+ writeValue(pctx, view.entity, value);
9757
+ }
9758
+ }
9759
+ /**
9722
9760
  * Writes the layer of a view.
9723
9761
  *
9724
9762
  * @param pctx - Domain context of the projection module.
@@ -11037,6 +11075,155 @@ function reconcile(pctx, options) {
11037
11075
  });
11038
11076
  }
11039
11077
  //#endregion
11078
+ //#region src/plugins/world/projection/mount.ts
11079
+ /**
11080
+ * Creates the empty view tables of a freshly mounted projection. These live in their own
11081
+ * non-exported functions because lint rule L5 refuses a collection built inside an exported
11082
+ * declaration.
11083
+ *
11084
+ * @param owner - Who owns the entities of this projection.
11085
+ * @param order - The `Order` its screen roots draw at.
11086
+ * @param layers - The layer list of the scene it is mounted into.
11087
+ * @returns A mounted projection with nothing in it yet.
11088
+ */
11089
+ function emptyMounted(owner, order, layers) {
11090
+ return {
11091
+ owner,
11092
+ live: /* @__PURE__ */ new Map(),
11093
+ queue: [],
11094
+ items: /* @__PURE__ */ new Map(),
11095
+ order,
11096
+ layers
11097
+ };
11098
+ }
11099
+ /**
11100
+ * Checks that every layer a projection names is declared by the scene.
11101
+ *
11102
+ * @param pctx - Domain context of the projection module.
11103
+ * @param spec - The projection spec.
11104
+ * @throws {Error} When `layer` or `lift` is not in the layer list.
11105
+ */
11106
+ function checkLayers(pctx, spec) {
11107
+ const declared = new Set(pctx.ctx.state.projection.layers.map((layer) => layer.name));
11108
+ for (const name of [spec.layer, spec.lift]) {
11109
+ if (name === void 0 || declared.has(name)) continue;
11110
+ throw new Error(`[game] Projection "${spec.name}" names layer "${name}", which the scene does not declare.\n Add it to the layers of the scene.`);
11111
+ }
11112
+ }
11113
+ /**
11114
+ * Tells whether two layer lists declare one layer alike: every field of its entry (`sort`,
11115
+ * `above`, whatever a layer declares) the same in both. The renderer keeps the container of a
11116
+ * layer that stays, but sorts a view again only when the view changes, so a view kept under
11117
+ * another rule would stand at its old place.
11118
+ *
11119
+ * @param before - The layer list the projection was mounted under.
11120
+ * @param after - The layer list of the coming scene.
11121
+ * @param name - The layer name.
11122
+ * @returns True when both lists hold the layer with the same fields.
11123
+ * @example
11124
+ * ```ts
11125
+ * sameLayer([{ name: "ui", sort: "none" }], [{ name: "ui", sort: "order" }], "ui"); // false
11126
+ * ```
11127
+ */
11128
+ function sameLayer(before, after, name) {
11129
+ const first = before.find((layer) => layer.name === name);
11130
+ const second = after.find((layer) => layer.name === name);
11131
+ if (first === void 0 || second === void 0) return false;
11132
+ const fields = Object.entries(first);
11133
+ return fields.length === Object.keys(second).length && fields.every(([field, value]) => Object.getOwnPropertyDescriptor(second, field)?.value === value);
11134
+ }
11135
+ /**
11136
+ * Tells whether a projection draws under the same rules in two layer lists: its `layer`, and its
11137
+ * `lift` when it has one, declared alike in both.
11138
+ *
11139
+ * @param before - The layer list the projection was mounted under.
11140
+ * @param after - The layer list of the coming scene.
11141
+ * @param spec - The projection spec.
11142
+ * @returns True when the projection can stay mounted.
11143
+ */
11144
+ function drawsAlike(before, after, spec) {
11145
+ return sameLayer(before, after, spec.layer) && (spec.lift === void 0 || sameLayer(before, after, spec.lift));
11146
+ }
11147
+ /**
11148
+ * Unmounts projections: every motion of their views is finished, and the live views and the
11149
+ * despawn queue are despawned in the same call. An unknown or unmounted name is skipped.
11150
+ *
11151
+ * @param pctx - Domain context of the projection module.
11152
+ * @param names - Names of the projections to unmount.
11153
+ */
11154
+ function unmountNames(pctx, names) {
11155
+ const state = pctx.ctx.state.projection;
11156
+ for (const name of names) {
11157
+ const mounted = state.mounted.get(name);
11158
+ if (mounted === void 0) continue;
11159
+ clearMounted(pctx, mounted);
11160
+ state.mounted.delete(name);
11161
+ }
11162
+ }
11163
+ /**
11164
+ * The first pass of a mount, which touches nothing: every name is checked, a projection mounted
11165
+ * into this scene already is reported and passed over, and the rest is sorted into the ones that
11166
+ * come and the ones a scene before mounted that have to go and come back.
11167
+ *
11168
+ * @param pctx - Domain context of the projection module.
11169
+ * @param names - Names of the projections to mount.
11170
+ * @returns The specs to mount and reconcile, and the names to unmount first.
11171
+ * @throws {Error} For an unknown name, or a `layer` or `lift` the scene does not declare.
11172
+ */
11173
+ function sortMount(pctx, names) {
11174
+ const state = pctx.ctx.state.projection;
11175
+ const fresh = [];
11176
+ const again = [];
11177
+ for (const name of names) {
11178
+ const spec = state.specs.get(name);
11179
+ if (spec === void 0) throw new Error(`[game] Projection "${name}" is not registered.\n Register it before a scene mounts it.`);
11180
+ const mounted = state.mounted.get(name);
11181
+ if (mounted?.layers === state.layers) {
11182
+ pctx.ctx.log.warn("world:already-mounted", { projection: name });
11183
+ continue;
11184
+ }
11185
+ checkLayers(pctx, spec);
11186
+ if (mounted !== void 0 && drawsAlike(mounted.layers, state.layers, spec)) continue;
11187
+ if (mounted !== void 0) again.push(name);
11188
+ fresh.push(spec);
11189
+ }
11190
+ return {
11191
+ fresh,
11192
+ again
11193
+ };
11194
+ }
11195
+ /**
11196
+ * Mounts the projections of a list under an owner. A new one is reconciled at once, direct. One
11197
+ * a scene before mounted stays when the current layer list declares its layers alike, and takes
11198
+ * its place in this list; one whose layer is declared otherwise is unmounted and mounted again.
11199
+ * One mounted into this scene already is reported and changes nothing. Nothing is mounted when
11200
+ * the call throws.
11201
+ *
11202
+ * @param pctx - Domain context of the projection module.
11203
+ * @param names - Names of the projections to mount.
11204
+ * @param owner - Who owns the entities of the new ones.
11205
+ * @throws {Error} For an unknown name, or a `layer` or `lift` the scene does not declare.
11206
+ */
11207
+ function mountNames(pctx, names, owner) {
11208
+ const state = pctx.ctx.state.projection;
11209
+ const { fresh, again } = sortMount(pctx, names);
11210
+ unmountNames(pctx, again);
11211
+ for (const name of names) {
11212
+ const order = names.indexOf(name) - names.length;
11213
+ const mounted = state.mounted.get(name);
11214
+ if (mounted === void 0) state.mounted.set(name, emptyMounted(owner, order, state.layers));
11215
+ else if (mounted.layers !== state.layers) placeMounted(pctx, mounted, order);
11216
+ }
11217
+ if (fresh.length === 0) return;
11218
+ reconcile(pctx, {
11219
+ names: fresh.map((spec) => spec.name),
11220
+ direct: true,
11221
+ force: true,
11222
+ converge: false,
11223
+ settle: false
11224
+ });
11225
+ }
11226
+ //#endregion
11040
11227
  //#region src/plugins/world/projection/api.ts
11041
11228
  /**
11042
11229
  * Tells whether a cause sets the picture instead of playing it.
@@ -11069,25 +11256,8 @@ function isConvergeCause(cause) {
11069
11256
  return cause === "load" || cause === "restore";
11070
11257
  }
11071
11258
  /**
11072
- * Creates the empty view tables of a freshly mounted projection. These live in their own
11073
- * non-exported functions because lint rule L5 refuses a collection built inside an exported
11074
- * declaration.
11075
- *
11076
- * @param owner - Who owns the entities of this projection.
11077
- * @param order - The `Order` its screen roots draw at.
11078
- * @returns A mounted projection with nothing in it yet.
11079
- */
11080
- function emptyMounted(owner, order) {
11081
- return {
11082
- owner,
11083
- live: /* @__PURE__ */ new Map(),
11084
- queue: [],
11085
- items: /* @__PURE__ */ new Map(),
11086
- order
11087
- };
11088
- }
11089
- /**
11090
- * Creates the empty mute table of one entity.
11259
+ * Creates the empty mute table of one entity. It lives in its own non-exported function because
11260
+ * lint rule L5 refuses a collection built inside an exported declaration.
11091
11261
  *
11092
11262
  * @returns An empty table of component name to owned fields.
11093
11263
  */
@@ -11173,20 +11343,6 @@ function noPeers() {
11173
11343
  };
11174
11344
  }
11175
11345
  /**
11176
- * Checks that every layer a projection names is declared by the scene.
11177
- *
11178
- * @param pctx - Domain context of the projection module.
11179
- * @param spec - The projection spec.
11180
- * @throws {Error} When `layer` or `lift` is not in the layer list.
11181
- */
11182
- function checkLayers(pctx, spec) {
11183
- const declared = new Set(pctx.ctx.state.projection.layers.map((layer) => layer.name));
11184
- for (const name of [spec.layer, spec.lift]) {
11185
- if (name === void 0 || declared.has(name)) continue;
11186
- throw new Error(`[game] Projection "${spec.name}" names layer "${name}", which the scene does not declare.\n Add it to the layers of the scene.`);
11187
- }
11188
- }
11189
- /**
11190
11346
  * Creates the projection module: the public API `scenes`, `input` and `i18n` call, plus the
11191
11347
  * methods the plugin root drives from the frame and the `model:committed` hook.
11192
11348
  *
@@ -11251,33 +11407,10 @@ function createProjectionApi(ctx, deps) {
11251
11407
  },
11252
11408
  layers: () => state.layers,
11253
11409
  mount: (names, owner) => {
11254
- const fresh = [];
11255
- for (const name of names) {
11256
- const spec = state.specs.get(name);
11257
- if (spec === void 0) throw new Error(`[game] Projection "${name}" is not registered.\n Register it before a scene mounts it.`);
11258
- if (state.mounted.has(name)) {
11259
- ctx.log.warn("world:already-mounted", { projection: name });
11260
- continue;
11261
- }
11262
- checkLayers(pctx, spec);
11263
- fresh.push(spec);
11264
- }
11265
- for (const spec of fresh) state.mounted.set(spec.name, emptyMounted(owner, names.indexOf(spec.name) - names.length));
11266
- if (fresh.length > 0) reconcile(pctx, {
11267
- names: fresh.map((spec) => spec.name),
11268
- direct: true,
11269
- force: true,
11270
- converge: false,
11271
- settle: false
11272
- });
11410
+ mountNames(pctx, names, owner);
11273
11411
  },
11274
11412
  unmount: (names) => {
11275
- for (const name of names) {
11276
- const mounted = state.mounted.get(name);
11277
- if (mounted === void 0) continue;
11278
- clearMounted(pctx, mounted);
11279
- state.mounted.delete(name);
11280
- }
11413
+ unmountNames(pctx, names);
11281
11414
  },
11282
11415
  restOf: (entity, component) => {
11283
11416
  const stored = (state.byEntity.get(entity)?.rest ?? state.rests.get(entity))?.get(component.componentName);
@@ -12184,7 +12317,7 @@ function withinRatio(differing, bytes) {
12184
12317
  * @param host - The host module (`resolution()`).
12185
12318
  * @returns Device pixels per CSS pixel; `undefined` for the half resolution of `host.pixels`.
12186
12319
  */
12187
- function readResolution(ctx, host) {
12320
+ function readResolution$1(ctx, host) {
12188
12321
  return hasPictures(ctx.state.kept) ? host.resolution() : void 0;
12189
12322
  }
12190
12323
  /**
@@ -12208,7 +12341,7 @@ function hasPictures(kept) {
12208
12341
  * @param decided - The decision of this frame, changed in place when a missed frame is drawn.
12209
12342
  */
12210
12343
  function verifyFrame(ctx, state, host, decided) {
12211
- const resolution = readResolution(ctx, host);
12344
+ const resolution = readResolution$1(ctx, host);
12212
12345
  if (decided.reason !== "none") {
12213
12346
  state.verify.reference = host.pixels(resolution);
12214
12347
  return;
@@ -15224,7 +15357,7 @@ const BEFORE = {
15224
15357
  * @param resolution - The target's resolution.
15225
15358
  * @returns Texels per CSS px.
15226
15359
  */
15227
- function resolutionOf(filter, resolution) {
15360
+ function resolutionOf$1(filter, resolution) {
15228
15361
  const own = filter.resolution;
15229
15362
  return own === "inherit" ? resolution : own;
15230
15363
  }
@@ -15244,7 +15377,7 @@ function readStack(slots, resolution, into) {
15244
15377
  const filter = slots[index]?.filter;
15245
15378
  if (filter === void 0 || !filter.enabled) continue;
15246
15379
  enabled += 1;
15247
- rs = Math.min(rs, resolutionOf(filter, resolution));
15380
+ rs = Math.min(rs, resolutionOf$1(filter, resolution));
15248
15381
  padding += filter.padding;
15249
15382
  }
15250
15383
  into.enabled = enabled;
@@ -21564,7 +21697,7 @@ const NO_DETAIL = Object.freeze({});
21564
21697
  /** The fewest device pixels per CSS pixel `setResolution` draws at. */
21565
21698
  const MIN_RESOLUTION = .5;
21566
21699
  /** The most device pixels per CSS pixel `setResolution` draws at. */
21567
- const MAX_RESOLUTION = 4;
21700
+ const MAX_RESOLUTION$1 = 4;
21568
21701
  /**
21569
21702
  * Message of a resolution `setResolution` refuses.
21570
21703
  *
@@ -21572,7 +21705,7 @@ const MAX_RESOLUTION = 4;
21572
21705
  * @returns The message.
21573
21706
  */
21574
21707
  function resolutionMessage(value) {
21575
- return `[game] renderer.monitor.setResolution(${String(value)}) needs a number from ${MIN_RESOLUTION} to ${MAX_RESOLUTION}.\n Pass undefined to go back to maxResolution.`;
21708
+ return `[game] renderer.monitor.setResolution(${String(value)}) needs a number from ${MIN_RESOLUTION} to ${MAX_RESOLUTION$1}.\n Pass undefined to go back to maxResolution.`;
21576
21709
  }
21577
21710
  /**
21578
21711
  * Tells whether `setResolution` takes a value: a number from `MIN_RESOLUTION` to `MAX_RESOLUTION`, or `undefined` to
@@ -21586,7 +21719,7 @@ function resolutionMessage(value) {
21586
21719
  * ```
21587
21720
  */
21588
21721
  function takesResolution(value) {
21589
- return value === void 0 || typeof value === "number" && value >= MIN_RESOLUTION && value <= MAX_RESOLUTION;
21722
+ return value === void 0 || typeof value === "number" && value >= MIN_RESOLUTION && value <= MAX_RESOLUTION$1;
21590
21723
  }
21591
21724
  /**
21592
21725
  * Creates the monitor module: the counters of the renderer, the capture of a frame, and the
@@ -32094,14 +32227,14 @@ function cancelTracksOf(actx, entity) {
32094
32227
  //#endregion
32095
32228
  //#region src/plugins/anim/tween/driver.ts
32096
32229
  /** The empty field set of every track nobody muted: one frozen set, shared, read every frame. */
32097
- const NO_FIELDS = Object.freeze(/* @__PURE__ */ new Set());
32230
+ const NO_FIELDS$1 = Object.freeze(/* @__PURE__ */ new Set());
32098
32231
  /**
32099
32232
  * The muted fields of a timeline step's track: none.
32100
32233
  *
32101
32234
  * @returns The shared empty set; a frame builds no set.
32102
32235
  */
32103
32236
  function noFields() {
32104
- return NO_FIELDS;
32237
+ return NO_FIELDS$1;
32105
32238
  }
32106
32239
  /**
32107
32240
  * Creates the tween engine `world` runs `ViewHandle.tween`, `toRest` and `all` on. Installed with
@@ -35732,9 +35865,22 @@ function overTargetOf(ctx, node, run) {
35732
35865
  return takePending(ctx);
35733
35866
  }
35734
35867
  /**
35735
- * Builds the scene in one synchronous block: the layers, then the projections of the old scene
35736
- * out, then the ones of the new scene in, the new scene to the world's systems, then the list the
35737
- * scene names to `assets`, then the frame loop out of its idle cap, and only then the event.
35868
+ * The projections of the scene that leaves which the scene that comes does not list. A projection
35869
+ * both list stays mounted: the layers of a projection are the ones its spec names, and both scenes
35870
+ * declare them, so its views stay in the same layer containers.
35871
+ *
35872
+ * @param previous - The scene that leaves.
35873
+ * @param scene - The scene that comes.
35874
+ * @returns The names to unmount, in the order the leaving scene lists them.
35875
+ */
35876
+ function leavingOf(previous, scene) {
35877
+ return previous.projections.filter((name) => !scene.projections.includes(name));
35878
+ }
35879
+ /**
35880
+ * Builds the scene in one synchronous block: the layers, then the projections only the old scene
35881
+ * lists out, then the list of the new scene in (a projection both list stays and takes its new
35882
+ * place), the new scene to the world's systems, then the list the scene names to `assets`, then
35883
+ * the frame loop out of its idle cap, and only then the event.
35738
35884
  *
35739
35885
  * @param ctx - Domain context of the plugin.
35740
35886
  * @param scene - The scene whose bundles are there.
@@ -35746,7 +35892,7 @@ function apply(ctx, scene, named) {
35746
35892
  const from = state.current;
35747
35893
  const previous = from === void 0 ? void 0 : state.scenes.get(from);
35748
35894
  projection.setLayers(scene.layers);
35749
- if (previous !== void 0) projection.unmount(previous.projections);
35895
+ if (previous !== void 0) projection.unmount(leavingOf(previous, scene));
35750
35896
  projection.mount(scene.projections, state.owner);
35751
35897
  state.current = scene.id;
35752
35898
  ctx.deps.world.ecs.setScene(scene.id);
@@ -37867,6 +38013,19 @@ function readArea(id, area) {
37867
38013
  return area;
37868
38014
  }
37869
38015
  /**
38016
+ * Reads the declared resolution: texels per CSS px, or 0 for the canvas's. A JavaScript caller may pass anything.
38017
+ *
38018
+ * @param id - The filter id.
38019
+ * @param resolution - What the game declared.
38020
+ * @returns The resolution; 0 when it was left out.
38021
+ * @throws {Error} When it is given and is not a number from 0 to `MAX_RESOLUTION`.
38022
+ */
38023
+ function readResolution(id, resolution) {
38024
+ if (resolution === void 0) return 0;
38025
+ if (typeof resolution === "number" && resolution >= 0 && resolution <= 4) return resolution;
38026
+ throw new Error(`[game] Filter "${id}": resolution must be from 0 to 4, 0 for the resolution of the canvas.\n Got ${String(resolution)}.`);
38027
+ }
38028
+ /**
37870
38029
  * Tells whether a value is a list of 2 to 4 finite numbers.
37871
38030
  *
37872
38031
  * @param value - A uniform declaration.
@@ -38157,18 +38316,20 @@ function cacheWithInputs(cache, inputs) {
38157
38316
  * `enabled: true` and `order: 0`. An input `blur` gives the body `sampleBlur(uv)`: the last texture
38158
38317
  * of its chain, read at the body's `uv`. A `fallback` draws instead when the filter is broken.
38159
38318
  * An `area` answers, from the value, the rectangle in the entity's own units outside of which the
38160
- * filter draws nothing: Pixi then renders and fills only that box, not the view's bounds.
38319
+ * filter draws nothing: Pixi then renders and fills only that box, not the view's bounds. A
38320
+ * `resolution` draws the view and its filters at that many texels per CSS px instead of the
38321
+ * canvas's.
38161
38322
  * Register it through the `filters` key of a feature. The README shows a filter with a blur input
38162
38323
  * and one with an `area`.
38163
38324
  *
38164
38325
  * @param id - The filter id, also the component name; unique, and not one of the built-ins.
38165
38326
  * @param spec - Both bodies, the uniforms, the passes one apply costs, the padding, the inputs, the fallback, the
38166
- * cache category and the area.
38327
+ * cache category, the area and the resolution.
38167
38328
  * @returns The component type, with the frozen `filter` definition attached.
38168
38329
  * @throws {Error} When the id is empty, the WGSL has no `mainFragment`, the GLSL no `void main(`,
38169
38330
  * a uniform is reserved, badly named or of no known form, the passes or the padding break
38170
38331
  * their rules, an input or the fallback breaks one of its rules, `passes` is given with inputs,
38171
- * or `area` is not a function.
38332
+ * `area` is not a function, or `resolution` is not a number from 0 to 4.
38172
38333
  * @example
38173
38334
  * ```ts
38174
38335
  * // features/board/effects.ts: a gold tint a motion hook tweens on a merge, on WebGPU and WebGL.
@@ -38190,6 +38351,7 @@ function defineFilter(id, spec) {
38190
38351
  checkCost(id, passes, padding, uniforms);
38191
38352
  checkCache(id, spec.cache);
38192
38353
  const area = readArea(id, spec.area);
38354
+ const resolution = readResolution(id, spec.resolution);
38193
38355
  const origin = callsCanvasPixel(spec.wgsl, spec.glsl) ? KEPT_ORIGIN : void 0;
38194
38356
  const cache = cacheWithInputs(cacheOf(spec.wgsl, spec.glsl, spec.cache), inputs);
38195
38357
  const filter = Object.freeze({
@@ -38203,6 +38365,7 @@ function defineFilter(id, spec) {
38203
38365
  fallback,
38204
38366
  cache,
38205
38367
  origin,
38368
+ resolution,
38206
38369
  ...area === void 0 ? {} : { area }
38207
38370
  });
38208
38371
  if (spec.cache !== false && (readsScreenIn(spec.wgsl) || readsScreenIn(spec.glsl))) screenReaders.add(filter);
@@ -38423,6 +38586,7 @@ const displacementDefaults = {
38423
38586
  };
38424
38587
  const alphaDefaults = {
38425
38588
  alpha: 1,
38589
+ resolution: 0,
38426
38590
  enabled: true,
38427
38591
  order: 0
38428
38592
  };
@@ -38519,11 +38683,16 @@ const Noise = /*#__PURE__*/ component("effects.noise", noiseDefaults);
38519
38683
  const Displacement = /*#__PURE__*/ component("effects.displacement", displacementDefaults);
38520
38684
  /**
38521
38685
  * Pixi's alpha over a view and its subtree as one layer, so overlapping children do not show
38522
- * through each other.
38686
+ * through each other. `resolution: 0` draws at the resolution of the canvas; a number draws the
38687
+ * subtree at that many texels per CSS px, at most 4, and every other filter of the view with it
38688
+ * (Pixi's smallest resolution wins). At `alpha: 1` and `resolution: 0` the filter is switched off
38689
+ * after 8 frames there: it would copy the subtree unchanged through a pass of its own, and a
38690
+ * keyframe that only touches 1 switches nothing. One render pass per apply.
38523
38691
  *
38524
38692
  * @example
38525
38693
  * ```ts
38526
- * Alpha({ alpha: 0.5 }).value; // { alpha: 0.5, enabled: true, order: 0 }
38694
+ * // The fog of a memory screen, drawn at half a texel per CSS px through one pass.
38695
+ * Alpha({ resolution: 0.5 }).value; // { alpha: 1, resolution: 0.5, enabled: true, order: 0 }
38527
38696
  * ```
38528
38697
  */
38529
38698
  const Alpha = /*#__PURE__*/ component("effects.alpha", alphaDefaults);
@@ -38648,6 +38817,20 @@ function brokenMark() {
38648
38817
  //#endregion
38649
38818
  //#region src/plugins/effects/filters/instance.ts
38650
38819
  /**
38820
+ * @file effects/filters — the only file that builds, writes and destroys Pixi filters. Ours: a
38821
+ * `Filter` over a `GpuProgram` of the assembled WGSL and a `GlProgram` of the assembled GLSL, with
38822
+ * one `UniformGroup` both read. The Pixi-core kinds: their own class, written through their setters
38823
+ * when a field differs from what they were last given. Every kind but `Blur` renders at
38824
+ * `resolution: "inherit"`, the resolution of the canvas, so a filtered view stays sharp on DPR 2-3
38825
+ * (decision 32), unless its value (`Alpha`) or its definition (`defineFilter`) gives a resolution of
38826
+ * its own. An `Alpha` at 1 drawn at the canvas's is switched off once it rested there: it would copy
38827
+ * the view unchanged through a pass. Every write compares first: a value the filter holds already writes nothing and
38828
+ * asks the renderer for no frame. A destroy frees the uniform buffers first: `Filter.destroy()`
38829
+ * leaves them to a GC that runs after a minute (P10). A filter with inputs draws each place of its
38830
+ * chains through Pixi's public `applyFilter` from inside its own `apply`, into pooled textures given
38831
+ * back in a `finally`; a stand-in draws in place of a broken kind or instance.
38832
+ */
38833
+ /**
38651
38834
  * The resolution of every filter but `Blur`: the render target's, so a filter draws at the
38652
38835
  * resolution of the canvas. Pixi's default of 1 would draw it at half the pixels on DPR 2.
38653
38836
  */
@@ -38662,6 +38845,20 @@ const PROBE_VALUE = Object.freeze({
38662
38845
  /** The numbers of a vector field that holds none. */
38663
38846
  const NO_NUMBERS = Object.freeze([]);
38664
38847
  /**
38848
+ * The resolution Pixi is given for a resolution of a value or a definition: the number, at most `MAX_RESOLUTION`, or
38849
+ * the canvas's for 0 (or less, or not a number).
38850
+ *
38851
+ * @param resolution - Texels per CSS px; 0 for the canvas's.
38852
+ * @returns What a filter's `resolution` takes.
38853
+ * @example
38854
+ * ```ts
38855
+ * resolutionOf(50); // 4
38856
+ * ```
38857
+ */
38858
+ function resolutionOf(resolution) {
38859
+ return resolution > 0 ? Math.min(resolution, 4) : CANVAS_RESOLUTION;
38860
+ }
38861
+ /**
38665
38862
  * Tells whether two numbers are the same value for the GPU: equal, or both NaN.
38666
38863
  *
38667
38864
  * @param a - One number.
@@ -38822,7 +39019,7 @@ function createOurs(pixi, definition, value) {
38822
39019
  glProgram,
38823
39020
  resources,
38824
39021
  padding: paddingOf$1(definition, value),
38825
- resolution: CANVAS_RESOLUTION
39022
+ resolution: resolutionOf(definition.resolution)
38826
39023
  }),
38827
39024
  group,
38828
39025
  definition,
@@ -38864,6 +39061,43 @@ function blurResolution(ectx, value) {
38864
39061
  return ectx.state.phone ? ectx.config.blur.phoneResolution : 1;
38865
39062
  }
38866
39063
  /**
39064
+ * Tells whether an `Alpha` value draws its view unchanged: switched on, at alpha 1, at the canvas's resolution. Its
39065
+ * filter would copy the view through a pass of its own. The fields are read by name: a fade reads them every frame.
39066
+ *
39067
+ * @param value - The stored `Alpha` value.
39068
+ * @returns True when the filter may be switched off.
39069
+ * @example
39070
+ * ```ts
39071
+ * drawsUnchanged({ enabled: true, order: 0, alpha: 1, resolution: 0 }); // true
39072
+ * ```
39073
+ */
39074
+ function drawsUnchanged(value) {
39075
+ const { alpha, resolution } = value;
39076
+ return value.enabled && alpha === 1 && !(typeof resolution === "number" && resolution > 0);
39077
+ }
39078
+ /**
39079
+ * The `enabled` switch of an instance for the stored value of this sync: the value's own, at once, but an `Alpha` that
39080
+ * draws its view unchanged is switched off only at its `CALM_AFTER`-th sync in a row so while its filter is on, counted
39081
+ * in `calm`: keyframes that touch 1 keep the pass and switch nothing, a fade that ends at 1 lets it go. The filter
39082
+ * sync syncs the view again while `calm` counts.
39083
+ *
39084
+ * @param instance - The instance.
39085
+ * @param value - The stored filter value.
39086
+ * @returns True when Pixi is to draw the filter.
39087
+ */
39088
+ function switchOf(instance, value) {
39089
+ if (instance.kind !== "alpha") return value.enabled;
39090
+ if (!drawsUnchanged(value)) {
39091
+ instance.calm = 0;
39092
+ return value.enabled;
39093
+ }
39094
+ if (!instance.filter.enabled) return false;
39095
+ instance.calm += 1;
39096
+ if (instance.calm < 8) return true;
39097
+ instance.calm = 0;
39098
+ return false;
39099
+ }
39100
+ /**
38867
39101
  * Composes a colour matrix from the identity: brightness, saturation, contrast, hue, and the
38868
39102
  * grayscale as a negative saturation. `ColorMatrixFilter` multiplies, so it starts from `reset()`.
38869
39103
  *
@@ -39005,10 +39239,11 @@ function createCore(ectx, pixi, core, value, probeMap) {
39005
39239
  kind: "alpha",
39006
39240
  filter: new pixi.AlphaFilter({
39007
39241
  alpha: numberOf$1(value, "alpha"),
39008
- resolution: CANVAS_RESOLUTION
39242
+ resolution: resolutionOf(numberOf$1(value, "resolution"))
39009
39243
  }),
39010
39244
  ...appliedOf(value),
39011
- passes: 1
39245
+ passes: 1,
39246
+ calm: 0
39012
39247
  };
39013
39248
  }
39014
39249
  }
@@ -39030,7 +39265,7 @@ function createCore(ectx, pixi, core, value, probeMap) {
39030
39265
  function createFilter(ectx, pixi, kind, value, probeMap) {
39031
39266
  const body = kind.source === "wgsl" ? createOurs(pixi, kind.definition, value) : createCore(ectx, pixi, kind.core, value, probeMap);
39032
39267
  if (body === void 0) return void 0;
39033
- body.filter.enabled = value.enabled;
39268
+ body.filter.enabled = value.enabled && !(body.kind === "alpha" && drawsUnchanged(value));
39034
39269
  if (body.kind === "wgsl") body.areaStale = body.definition.area !== void 0;
39035
39270
  const instance = Object.assign(body, {
39036
39271
  order: value.order,
@@ -39255,7 +39490,8 @@ function wireInputs(ectx, pixi, instance) {
39255
39490
  };
39256
39491
  instance.inputs = inputs;
39257
39492
  instance.filter.apply = (manager, input, output, clearMode) => applyInputs(ectx, pixi, instance, inputs, manager, input, output, clearMode);
39258
- evaluate(ectx, pixi, instance, inputs, ectx.deps.renderer.host.resolution());
39493
+ const { resolution } = instance.definition;
39494
+ evaluate(ectx, pixi, instance, inputs, resolution > 0 ? resolution : ectx.deps.renderer.host.resolution());
39259
39495
  }
39260
39496
  /**
39261
39497
  * Reads one property of a value a chain returned, whatever its type.
@@ -39730,6 +39966,7 @@ function sameField(a, b) {
39730
39966
  */
39731
39967
  function coreDiffers(instance, value) {
39732
39968
  const { fields, applied } = instance;
39969
+ if (instance.kind === "alpha") return !sameField(applied.alpha, value.alpha) || !sameField(applied.resolution, value.resolution);
39733
39970
  for (const field of fields) if (instance.kind === "displacement" && field === "map") {
39734
39971
  if (mapOf(value) !== instance.map) return true;
39735
39972
  } else if (!sameField(applied[field], value[field])) return true;
@@ -39759,6 +39996,12 @@ function mapOf(value) {
39759
39996
  function writeCore(ectx, instance, value) {
39760
39997
  setCore(ectx, instance, value);
39761
39998
  const { fields, applied } = instance;
39999
+ if (instance.kind === "alpha") {
40000
+ const { alpha, resolution } = value;
40001
+ if (alpha !== void 0) applied.alpha = alpha;
40002
+ if (resolution !== void 0 && resolution !== applied.resolution) applied.resolution = resolution;
40003
+ return;
40004
+ }
39762
40005
  for (const field of fields) {
39763
40006
  const next = value[field];
39764
40007
  if (next !== void 0) applied[field] = next;
@@ -39800,7 +40043,11 @@ function setCore(ectx, instance, value) {
39800
40043
  instance.map = map;
39801
40044
  return;
39802
40045
  }
39803
- default: instance.filter.alpha = numberOf$1(value, "alpha");
40046
+ default: {
40047
+ const { alpha, resolution } = value;
40048
+ instance.filter.alpha = typeof alpha === "number" ? alpha : 0;
40049
+ if (resolution !== instance.applied.resolution) instance.filter.resolution = resolutionOf(typeof resolution === "number" ? resolution : 0);
40050
+ }
39804
40051
  }
39805
40052
  }
39806
40053
  /**
@@ -39826,8 +40073,9 @@ function switchOffBroken(instance, before) {
39826
40073
  }
39827
40074
  /**
39828
40075
  * Brings one instance up to the stored value for a frame, comparing before anything is written.
39829
- * A broken instance stays off whatever its component says. Switching on or off is a visible
39830
- * change; so is a new value of a filter that is on. A filter that is off and stays off still gets
40076
+ * A broken instance stays off whatever its component says, and an `Alpha` at 1 drawn at the
40077
+ * canvas's resolution is switched off at its `CALM_AFTER`-th sync there. Switching on or off is a
40078
+ * visible change; so is a new value of a filter that is on. A filter that is off and stays off still gets
39831
40079
  * its new values written, so it is current when it comes back on, but asks for nothing: an off
39832
40080
  * filter draws no pixel. Ours compare on every sync of their view (a view syncs when something of it changed); a core
39833
40081
  * kind only when its component changed.
@@ -39844,13 +40092,14 @@ function writeFilter(ectx, instance, value, changed, before) {
39844
40092
  switchOffBroken(instance, before);
39845
40093
  return false;
39846
40094
  }
39847
- const toggled = instance.filter.enabled !== value.enabled;
40095
+ const on = switchOf(instance, value);
40096
+ const toggled = instance.filter.enabled !== on;
39848
40097
  const differs = instance.kind === "wgsl" ? oursDiffer(instance, instance.definition, value) : changed && coreDiffers(instance, value);
39849
40098
  if (!toggled && !differs) return false;
39850
40099
  staleArea(instance);
39851
- const visible = toggled || value.enabled;
40100
+ const visible = toggled || on;
39852
40101
  if (visible) before();
39853
- if (toggled) instance.filter.enabled = value.enabled;
40102
+ if (toggled) instance.filter.enabled = on;
39854
40103
  if (differs) {
39855
40104
  if (instance.kind === "wgsl") {
39856
40105
  writeOurs(instance, instance.definition, value);
@@ -40595,8 +40844,10 @@ function judgeArea(ectx, instance) {
40595
40844
  }
40596
40845
  /**
40597
40846
  * The union of the areas of a view's enabled filters, into the scratch box: none while no filter is enabled, and none
40598
- * when an enabled filter has no area (every built-in kind, or a definition that answered `undefined`) — it draws over
40599
- * the view's bounds. A disabled filter, and a kind a debug switch turned off (it is not placed), do not count.
40847
+ * when an enabled filter has no area (every other built-in kind, or a definition that answered `undefined`) — it draws
40848
+ * over the view's bounds. An `Alpha` does not count: it only scales the pixels the others draw, and makes none where
40849
+ * they draw nothing; alone it leaves the view without an area. A disabled filter, and a kind a debug switch turned off
40850
+ * (it is not placed), do not count either.
40600
40851
  *
40601
40852
  * @param view - The view's record, its instances placed.
40602
40853
  */
@@ -40607,7 +40858,7 @@ function unionOf(view) {
40607
40858
  let bottom = Number.NEGATIVE_INFINITY;
40608
40859
  union.some = false;
40609
40860
  for (const instance of view.placed) {
40610
- if (!instance.filter.enabled) continue;
40861
+ if (!instance.filter.enabled || instance.kind === "alpha") continue;
40611
40862
  if (instance.kind !== "wgsl" || !instance.area.some) {
40612
40863
  union.some = false;
40613
40864
  return;
@@ -40619,6 +40870,7 @@ function unionOf(view) {
40619
40870
  right = Math.max(right, area.x + area.width);
40620
40871
  bottom = Math.max(bottom, area.y + area.height);
40621
40872
  }
40873
+ if (!union.some) return;
40622
40874
  union.x = left;
40623
40875
  union.y = top;
40624
40876
  union.width = right - left;
@@ -40834,7 +41086,7 @@ function syncKind(ectx, pixi, entity, view, kind, changed, off) {
40834
41086
  writeFilter(ectx, instance, value, changed, off ? NO_SIGNAL : view.signal);
40835
41087
  instance.order = value.order;
40836
41088
  refreshInputs(ectx, pixi, instance, off);
40837
- if (instance.kind === "wgsl" && (instance.inputs?.retired.length ?? 0) > 0) markDirty$1(ectx, view);
41089
+ if (instance.kind === "wgsl" && (instance.inputs?.retired.length ?? 0) > 0 || instance.kind === "alpha" && instance.calm > 0) markDirty$1(ectx, view);
40838
41090
  return installReplacement(view, kind, instance);
40839
41091
  }
40840
41092
  instance = firstInstance(ectx, pixi, kind, value);
@@ -41101,7 +41353,8 @@ function dropProbe(ectx, probe) {
41101
41353
  }
41102
41354
  /**
41103
41355
  * A probe of one Pixi-core kind at the frozen defaults of its component; a `Displacement` draws over Pixi's white
41104
- * texture, since its program does not depend on the map. Its `apply` is guarded like any instance's.
41356
+ * texture, since its program does not depend on the map. A probe is switched on whatever its defaults: an `Alpha` at 1
41357
+ * would be off, and Pixi would warm nothing. Its `apply` is guarded like any instance's.
41105
41358
  *
41106
41359
  * @param ectx - Domain context of the effects plugin.
41107
41360
  * @param pixi - The module the renderer loaded.
@@ -41112,6 +41365,7 @@ function coreProbe(ectx, pixi, id) {
41112
41365
  const kind = ectx.state.kinds.get(id);
41113
41366
  const instance = kind === void 0 ? void 0 : createFilter(ectx, pixi, kind, kind.component.defaults, pixi.Texture.WHITE);
41114
41367
  if (instance === void 0) return void 0;
41368
+ instance.filter.enabled = true;
41115
41369
  return {
41116
41370
  filter: instance.filter,
41117
41371
  destroy: () => destroyFilter(instance)
@@ -47773,20 +48027,68 @@ function resolveContent(ctx, entity, text) {
47773
48027
  return resolved;
47774
48028
  }
47775
48029
  /**
47776
- * The one system of the plugin: phase `layout`, before the two of `ui`, over every label. Every
47777
- * label is visited every frame: `ui` writes `Text` after this system and change sets clear in
47778
- * phase `signals`, so `changed(Text)` cannot tell which moved. The locale is read once per run,
47779
- * the clock at most once and only when a countdown label asks.
48030
+ * The frame the system and the step after the layout flush share, made once per plugin.
47780
48031
  *
47781
48032
  * @param ctx - Domain context of the text plugin.
47782
- * @returns The system definition `world.ecs.system` takes.
48033
+ * @returns A frame with no locale and no moment read yet.
47783
48034
  */
47784
- function createTextSystem(ctx) {
47785
- const frame = {
48035
+ function createTextFrame(ctx) {
48036
+ return {
47786
48037
  now: NaN,
47787
48038
  locale: "",
47788
48039
  duration: (ms) => ctx.deps.i18n.duration(ms)
47789
48040
  };
48041
+ }
48042
+ /**
48043
+ * Empties a list of labels in place: popped, so it keeps its store for the next batch.
48044
+ *
48045
+ * @param labels - The list.
48046
+ */
48047
+ function forgetAdded(labels) {
48048
+ while (labels.length > 0) labels.pop();
48049
+ }
48050
+ /** A patch with no field: the write marks `Text` changed and changes nothing. Frozen, so no caller fills it. */
48051
+ const NO_FIELDS = Object.freeze({});
48052
+ /**
48053
+ * Writes `Text` once more, with no field, for the labels the step after the last layout flush
48054
+ * resolved. `ui` measured them in its solve, before they were resolved, and the change of that
48055
+ * frame is gone; this one reaches its remeasure in the same layout phase, so a label whose string
48056
+ * measures otherwise than what `ui` measured gets its size. A label whose `Text` left is skipped.
48057
+ *
48058
+ * @param ctx - Domain context of the text plugin.
48059
+ */
48060
+ function writeOnceMore(ctx) {
48061
+ const ecs = ctx.deps.world.ecs;
48062
+ for (const entity of ctx.state.rewrite) if (ecs.get(entity, Text) !== void 0) ecs.set(entity, Text, NO_FIELDS);
48063
+ forgetAdded(ctx.state.rewrite);
48064
+ }
48065
+ /**
48066
+ * Notes a label whose `Text` came, as the world's `onAdded` hook calls it: it is marked for the
48067
+ * next layout phase, and while the world is live it is listed for the step after the layout
48068
+ * flush. In another mode no layout phase runs and the list would only grow: the label waits for
48069
+ * the next live run of the system, which resolves every marked label.
48070
+ *
48071
+ * @param ctx - Domain context of the text plugin.
48072
+ * @param entity - The entity that got a `Text`.
48073
+ */
48074
+ function noteAdded(ctx, entity) {
48075
+ ctx.state.dirty.add(entity);
48076
+ if (ctx.deps.world.ecs.mode() === "live") ctx.state.added.push(entity);
48077
+ }
48078
+ /**
48079
+ * The one system of the plugin: phase `layout`, before the two of `ui`, over every label. Every
48080
+ * label is visited every frame: `ui` writes `Text` after this system and change sets clear in
48081
+ * phase `signals`, so `changed(Text)` cannot tell which moved. The locale is read once per run,
48082
+ * the clock at most once and only when a countdown label asks. The labels that came before the run
48083
+ * are in its walk, so the list of the labels that came starts again empty: the step after the
48084
+ * layout flush takes only the ones the flush brings. The labels that step resolved last get their
48085
+ * `Text` written once more first, so `ui` measures the string they show.
48086
+ *
48087
+ * @param ctx - Domain context of the text plugin.
48088
+ * @param frame - What the runs share with the step after the layout flush.
48089
+ * @returns The system definition `world.ecs.system` takes.
48090
+ */
48091
+ function createTextSystem(ctx, frame) {
47790
48092
  return system({
47791
48093
  name: TEXT_SYSTEM_NAME,
47792
48094
  phase: "layout",
@@ -47794,11 +48096,42 @@ function createTextSystem(ctx) {
47794
48096
  run: (entities) => {
47795
48097
  frame.now = NaN;
47796
48098
  frame.locale = ctx.deps.i18n.locale();
48099
+ forgetAdded(ctx.state.added);
48100
+ if (ctx.state.rewrite.length > 0) writeOnceMore(ctx);
47797
48101
  for (const row of entities) stepText(ctx, row[0], row[1], frame);
47798
48102
  }
47799
48103
  });
47800
48104
  }
47801
48105
  /**
48106
+ * Resolves the labels whose `Text` came after the layout system ran. `ui` adds a label in its
48107
+ * solve, and the flush at the end of phase `layout` attaches it with the rest of its batch, a bound
48108
+ * component included; this runs right after that flush and before phase `sync`, so the renderer
48109
+ * builds the label with its string, and lists it, so the next system run writes its `Text` once
48110
+ * more for the remeasure of `ui`. A label whose content changed is not among them: it resolves in
48111
+ * the next layout phase, as before. While the world is not live its layout phase did not run: the
48112
+ * list is dropped, and the labels, marked, wait for the next live run, as every label does.
48113
+ *
48114
+ * @param ctx - Domain context of the text plugin.
48115
+ * @param frame - What the system's runs share.
48116
+ */
48117
+ function resolveAdded(ctx, frame) {
48118
+ const added = ctx.state.added;
48119
+ if (added.length === 0) return;
48120
+ if (ctx.deps.world.ecs.mode() !== "live") {
48121
+ added.length = 0;
48122
+ return;
48123
+ }
48124
+ frame.now = NaN;
48125
+ frame.locale = ctx.deps.i18n.locale();
48126
+ for (const entity of added) {
48127
+ const text = ctx.deps.world.ecs.get(entity, Text);
48128
+ if (text === void 0) continue;
48129
+ stepText(ctx, entity, text, frame);
48130
+ ctx.state.rewrite.push(entity);
48131
+ }
48132
+ forgetAdded(added);
48133
+ }
48134
+ /**
47802
48135
  * Marks labels for the next layout phase: every one of them, or only the ones `keep` names. A
47803
48136
  * locale change names the labels whose content is a message; a dev hot swap names the labels
47804
48137
  * drawn with a replaced font or icon.
@@ -48544,9 +48877,12 @@ function replaceStyles(ctx, styles) {
48544
48877
  ctx.deps.time.wake();
48545
48878
  }
48546
48879
  /**
48547
- * Opens what the plugin owns: the style table, the font keys, the layout system, the two world
48548
- * hooks of `Text` and the display adapter. The fonts of a bundle that is already loaded are read
48549
- * at once, because the boot bundle lands before this runs.
48880
+ * Opens what the plugin owns: the style table, the font keys, the layout system, the step after
48881
+ * the layout flush, the two world hooks of `Text` and the display adapter. The step is a frame
48882
+ * callback of phase `layout`: `world` registered its own in its `onStart`, which runs before this
48883
+ * one (`text` depends on `world`), so the step runs after the world's layout phase and its flush,
48884
+ * and before phase `sync`. The fonts of a bundle that is already loaded are read at once, because
48885
+ * the boot bundle lands before this runs.
48550
48886
  *
48551
48887
  * @param ctx - Kernel context of the text plugin.
48552
48888
  * @throws {Error} When two features define the same style name.
@@ -48554,18 +48890,18 @@ function replaceStyles(ctx, styles) {
48554
48890
  function startText(ctx) {
48555
48891
  const tctx = withDeps$1(ctx);
48556
48892
  const ecs = tctx.deps.world.ecs;
48893
+ const frame = createTextFrame(tctx);
48557
48894
  buildStyles(tctx);
48558
48895
  collectFonts(tctx);
48559
- tctx.state.removers.push(ecs.system(createTextSystem(tctx)), ecs.onAdded(Text, (entity) => {
48560
- tctx.state.dirty.add(entity);
48561
- }), ecs.onRemoved(Text, (entity) => {
48896
+ tctx.state.removers.push(ecs.system(createTextSystem(tctx, frame)), tctx.deps.time.onFrame("layout", () => resolveAdded(tctx, frame)), ecs.onAdded(Text, (entity) => noteAdded(tctx, entity)), ecs.onRemoved(Text, (entity) => {
48562
48897
  forgetText(tctx.state, entity);
48563
48898
  }), tctx.deps.renderer.sync.displays.provide(Text, createTextAdapter(tctx)));
48564
48899
  installFonts(tctx);
48565
48900
  }
48566
48901
  /**
48567
- * Closes what the plugin opened: the system, the two world hooks and the adapter. The Pixi
48568
- * objects the adapter built are the renderer's, and it frees them in its own teardown.
48902
+ * Closes what the plugin opened: the system, the step after the layout flush, the two world hooks
48903
+ * and the adapter. The Pixi objects the adapter built are the renderer's, and it frees them in its
48904
+ * own teardown.
48569
48905
  *
48570
48906
  * @param state - The plugin state, the only thing a teardown context carries.
48571
48907
  */
@@ -48582,6 +48918,8 @@ function stopText(state) {
48582
48918
  state.measured.clear();
48583
48919
  state.cache.clear();
48584
48920
  state.dirty.clear();
48921
+ state.added.length = 0;
48922
+ state.rewrite.length = 0;
48585
48923
  state.warned.clear();
48586
48924
  }
48587
48925
  //#endregion
@@ -48731,6 +49069,8 @@ function createTextState$1() {
48731
49069
  cache: emptyMap$2(),
48732
49070
  drawn: emptyWeakMap(),
48733
49071
  dirty: emptySet$1(),
49072
+ added: [],
49073
+ rewrite: [],
48734
49074
  generation: 0,
48735
49075
  warned: emptySet$1(),
48736
49076
  removers: []
@@ -53462,6 +53802,7 @@ function createReconciler(ctx, modules, fields) {
53462
53802
  };
53463
53803
  let slotsVersion = -1;
53464
53804
  let slotsRoots = -1;
53805
+ let placedUnder;
53465
53806
  /**
53466
53807
  * Registers the key of an element under its root, so `entityOf` and `find` answer for it. A key
53467
53808
  * `ui` gave a node of its own, a spacer's, is never registered.
@@ -54452,6 +54793,18 @@ function createReconciler(ctx, modules, fields) {
54452
54793
  if (element?.live === true && root.slot === void 0) ecs.set(element.entity, Order, { value: order });
54453
54794
  }
54454
54795
  /**
54796
+ * Gives every root the order its entity holds once a new scene stands: a scene that keeps a
54797
+ * projection both scenes list places it again (F23), and its tree stands, so no root is dirty.
54798
+ * The world places it only under a new layer list, so the walk runs once per new list, in the
54799
+ * first layout phase that runs after it, also when the switch came while the world was paused.
54800
+ */
54801
+ function reorderRoots() {
54802
+ const layers = ctx.deps.world.projection.layers();
54803
+ if (layers === placedUnder) return;
54804
+ placedUnder = layers;
54805
+ for (const root of state.roots.values()) if (!state.removing.has(root.entity)) refreshRootOrder(root);
54806
+ }
54807
+ /**
54455
54808
  * Reconciles one dirty root: reads its current tree and projection name, then diffs it. The root
54456
54809
  * stays dirty while the layout engine is not loaded, and a failure is logged for this root only.
54457
54810
  *
@@ -54473,14 +54826,15 @@ function createReconciler(ctx, modules, fields) {
54473
54826
  }
54474
54827
  /**
54475
54828
  * The first of the two systems of phase `layout`: the sweep, the roots a restore asked to build
54476
- * again, the popups that may leave, the pull of the text being typed, the scroll offsets and the
54477
- * windowed ranges, the bound texts that show a string of another size, the viewport, the dirty
54478
- * roots and the hosted views.
54829
+ * again, the roots a scene placed again, the popups that may leave, the pull of the text being
54830
+ * typed, the scroll offsets and the windowed ranges, the bound texts that show a string of
54831
+ * another size, the viewport, the dirty roots and the hosted views.
54479
54832
  */
54480
54833
  function reconcile() {
54481
54834
  sweep();
54482
54835
  state.reconciles += 1;
54483
54836
  if (state.remount) remountRoots();
54837
+ reorderRoots();
54484
54838
  settlePopups(ctx, state, unmountRoot);
54485
54839
  fields.pull();
54486
54840
  const containers = scrollContainers();