@moku-labs/game 0.15.36 → 0.15.38

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.
@@ -838,7 +838,7 @@ const BUSY = 4;
838
838
  /** The thermal rank from which the rule never steps up: `"moderate"` and every hotter status. */
839
839
  const HEAT_LIMIT_RANK = 2;
840
840
  /** Milliseconds in a second: turns a rate into the period of one frame. */
841
- const SECOND_MS$1 = 1e3;
841
+ const SECOND_MS$2 = 1e3;
842
842
  /** Every choice `setRate` takes: `"auto"` and the three fixed rates. */
843
843
  const RATE_CHOICES = Object.freeze([
844
844
  "auto",
@@ -1432,8 +1432,8 @@ function judgeFrame$1(ctx, g, intervalMs, at) {
1432
1432
  const { fps } = ctx.state.rate;
1433
1433
  const higher = stepAbove(g.ladder, fps);
1434
1434
  const missed = intervalMs > missLineMs(fps, ctx.state.rate.display.hz ?? fps);
1435
- const heavy = higher !== void 0 && g.lastWorkMs > .5 * SECOND_MS$1 / higher;
1436
- const busy = missed && g.lastWorkMs > SECOND_MS$1 / fps;
1435
+ const heavy = higher !== void 0 && g.lastWorkMs > .5 * SECOND_MS$2 / higher;
1436
+ const busy = missed && g.lastWorkMs > SECOND_MS$2 / fps;
1437
1437
  const slot = g.written % 512;
1438
1438
  evict$2(g, at);
1439
1439
  g.sampleAt[slot] = at;
@@ -9647,8 +9647,9 @@ function inertHandle() {
9647
9647
  return INERT;
9648
9648
  }
9649
9649
  /**
9650
- * The fields another writer owns on one component of one entity: the fields a running driver
9651
- * holds, and the fields the component type gives to a plugin for good.
9650
+ * The fields another writer owns on one component of one entity: the fields a `mute()` holds and
9651
+ * the `initialOnly` fields of a view's projection (one table, `state.mutes`), and the fields the
9652
+ * component type gives to a plugin for good.
9652
9653
  *
9653
9654
  * @param pctx - Domain context of the projection module.
9654
9655
  * @param entity - The entity to ask about.
@@ -10803,6 +10804,221 @@ function routeHint(hints, projection, key) {
10803
10804
  }
10804
10805
  }
10805
10806
  //#endregion
10807
+ //#region src/plugins/world/projection/mutes.ts
10808
+ /**
10809
+ * Tells a table `mute()` made apart from a shared `initialOnly` seed, which is frozen.
10810
+ *
10811
+ * @param table - A table of `state.mutes`.
10812
+ * @returns True when the table may be written.
10813
+ */
10814
+ function isOwnTable(table) {
10815
+ return !Object.isFrozen(table);
10816
+ }
10817
+ /**
10818
+ * Tells a field set `mute()` made apart from a set of a shared `initialOnly` seed, which is frozen.
10819
+ *
10820
+ * @param fields - A field set of one table.
10821
+ * @returns True when the set may be written.
10822
+ */
10823
+ function isOwnFields(fields) {
10824
+ return !Object.isFrozen(fields);
10825
+ }
10826
+ /**
10827
+ * The table a mute may write: the entity's own one, a copy of the seed it was born with, or a new one.
10828
+ *
10829
+ * @param stored - The table the entity has now, if any.
10830
+ * @returns A writable table.
10831
+ */
10832
+ function ownTable(stored) {
10833
+ if (stored === void 0) return /* @__PURE__ */ new Map();
10834
+ return isOwnTable(stored) ? stored : new Map(stored);
10835
+ }
10836
+ /**
10837
+ * The field set a mute may write: the component's own one, a copy of its seed set, or a new one.
10838
+ *
10839
+ * @param stored - The field set the component has now, if any.
10840
+ * @returns A writable set.
10841
+ */
10842
+ function ownFields(stored) {
10843
+ if (stored === void 0) return /* @__PURE__ */ new Set();
10844
+ return isOwnFields(stored) ? stored : new Set(stored);
10845
+ }
10846
+ /**
10847
+ * The `initialOnly` fields of one component of a view, from the spec of its projection.
10848
+ *
10849
+ * @param state - The projection branch of the plugin state.
10850
+ * @param entity - The entity of the view.
10851
+ * @param name - The component name.
10852
+ * @returns The seed set, or `undefined` for an entity that is no view of such a projection.
10853
+ */
10854
+ function initialFieldsOf(state, entity, name) {
10855
+ const projection = state.byEntity.get(entity)?.projection;
10856
+ const spec = projection === void 0 ? void 0 : state.specs.get(projection);
10857
+ return spec === void 0 ? void 0 : state.initialOnly.get(spec)?.get(name);
10858
+ }
10859
+ /**
10860
+ * Deletes a released field set once it is empty, and the entity's table once no component is
10861
+ * left. Only the table and the set this mute filled are touched, so a remover that runs after a
10862
+ * clear or a new birth on the same id never drops a newer mute.
10863
+ *
10864
+ * @param mutes - The mute tables of every entity.
10865
+ * @param entity - The muted entity.
10866
+ * @param table - The table the mute wrote.
10867
+ * @param name - The muted component's name.
10868
+ * @param owned - The field set the remover released from.
10869
+ */
10870
+ function dropEmptyMute(mutes, entity, table, name, owned) {
10871
+ if (owned.size > 0 || mutes.get(entity) !== table || table.get(name) !== owned) return;
10872
+ table.delete(name);
10873
+ if (table.size === 0) mutes.delete(entity);
10874
+ }
10875
+ /**
10876
+ * Hands fields of one component of one entity to another writer, until the remover runs. A view
10877
+ * born with an `initialOnly` seed gets its own copy first. The remover keeps the seed fields, asked
10878
+ * when it runs: a mute made inside the spawn came before the view was known.
10879
+ *
10880
+ * @param state - The projection branch of the plugin state.
10881
+ * @param entity - The entity.
10882
+ * @param name - The component name.
10883
+ * @param fields - The field names the caller owns.
10884
+ * @returns The remover; a no-op when called twice.
10885
+ */
10886
+ function muteFields(state, entity, name, fields) {
10887
+ const table = ownTable(state.mutes.get(entity));
10888
+ const owned = ownFields(table.get(name));
10889
+ for (const field of fields) owned.add(field);
10890
+ forgetJoined(state, owned);
10891
+ table.set(name, owned);
10892
+ state.mutes.set(entity, table);
10893
+ let released = false;
10894
+ return () => {
10895
+ if (released) return;
10896
+ released = true;
10897
+ const kept = initialFieldsOf(state, entity, name);
10898
+ for (const field of fields) if (kept?.has(field) !== true) owned.delete(field);
10899
+ forgetJoined(state, owned);
10900
+ dropEmptyMute(state.mutes, entity, table, name, owned);
10901
+ };
10902
+ }
10903
+ /**
10904
+ * Builds the frozen seed of a projection's `initialOnly` list: component name to its fields, a
10905
+ * component named twice with both lists joined.
10906
+ *
10907
+ * @param spec - The projection spec.
10908
+ * @param pairs - Its `initialOnly` list, not empty.
10909
+ * @returns The seed.
10910
+ * @throws {Error} For a pair with no field.
10911
+ */
10912
+ function buildSeed(spec, pairs) {
10913
+ const seed = /* @__PURE__ */ new Map();
10914
+ for (const [component, fields] of pairs) {
10915
+ const name = component.componentName;
10916
+ if (fields.length === 0) throw new Error(`[game] Projection "${spec.name}" lists ${name} in initialOnly with no field.\n Name the fields only the birth of a view writes, or drop the pair.`);
10917
+ seed.set(name, Object.freeze(/* @__PURE__ */ new Set([...seed.get(name) ?? [], ...fields])));
10918
+ }
10919
+ return Object.freeze(seed);
10920
+ }
10921
+ /**
10922
+ * Checks the `initialOnly` list of a spec and keeps its seed: once per spec, since a name is
10923
+ * registered once and `replace` brings a new spec. A spec without the option, or with an empty
10924
+ * list, has none.
10925
+ *
10926
+ * @param state - The projection branch of the plugin state.
10927
+ * @param spec - The spec `register` or `replace` stores.
10928
+ * @returns The seed, or `undefined`.
10929
+ * @throws {Error} For a pair with no field; nothing is kept then.
10930
+ */
10931
+ function learnSeed(state, spec) {
10932
+ const pairs = spec.initialOnly;
10933
+ if (pairs === void 0 || pairs.length === 0) return void 0;
10934
+ const seed = buildSeed(spec, pairs);
10935
+ state.initialOnly.set(spec, seed);
10936
+ return seed;
10937
+ }
10938
+ /**
10939
+ * Joins a seed into a table of the entity's own, in place, so the remover of every mute that wrote
10940
+ * the table still holds its table and its sets: a component the table lacks gets the shared set
10941
+ * of the seed, the set a mute wrote gets the fields of the seed.
10942
+ *
10943
+ * @param state - The projection branch of the plugin state.
10944
+ * @param table - The entity's own table.
10945
+ * @param seed - The seed to join.
10946
+ */
10947
+ function joinSeed(state, table, seed) {
10948
+ for (const [name, fields] of seed) {
10949
+ const stored = table.get(name);
10950
+ if (stored === void 0) {
10951
+ table.set(name, fields);
10952
+ continue;
10953
+ }
10954
+ const owned = ownFields(stored);
10955
+ for (const field of fields) owned.add(field);
10956
+ forgetJoined(state, owned);
10957
+ table.set(name, owned);
10958
+ }
10959
+ }
10960
+ /**
10961
+ * Gives a view that was just born the seed of its projection, so from now on nothing of the
10962
+ * projection writes or compares those fields. The birth itself wrote them from `view`. A view a
10963
+ * listener of its spawn muted already keeps its table, and the seed joins it.
10964
+ *
10965
+ * @param state - The projection branch of the plugin state.
10966
+ * @param spec - The projection spec.
10967
+ * @param entity - The entity of the new view.
10968
+ */
10969
+ function seedMutes(state, spec, entity) {
10970
+ if (spec.initialOnly === void 0) return;
10971
+ const seed = state.initialOnly.get(spec);
10972
+ if (seed === void 0) return;
10973
+ const stored = state.mutes.get(entity);
10974
+ if (stored === void 0) {
10975
+ state.mutes.set(entity, seed);
10976
+ return;
10977
+ }
10978
+ const table = ownTable(stored);
10979
+ joinSeed(state, table, seed);
10980
+ state.mutes.set(entity, table);
10981
+ }
10982
+ /**
10983
+ * Moves a view with a table of its own from one seed to the next: a set of the old seed goes, the
10984
+ * new seed joins. A set a mute wrote keeps every field it holds, an old seed's field it copied too,
10985
+ * since the table does not say which mute named it. A table left empty goes.
10986
+ *
10987
+ * @param state - The projection branch of the plugin state.
10988
+ * @param entity - The entity of the view.
10989
+ * @param stored - The table the view has now, if any.
10990
+ * @param from - The seed of the old spec, if any.
10991
+ * @param to - The seed of the new spec, if any.
10992
+ */
10993
+ function reseedOwn(state, entity, stored, from, to) {
10994
+ const table = ownTable(stored);
10995
+ for (const [name, fields] of from ?? []) if (table.get(name) === fields) table.delete(name);
10996
+ if (to !== void 0) joinSeed(state, table, to);
10997
+ if (table.size === 0) state.mutes.delete(entity);
10998
+ else state.mutes.set(entity, table);
10999
+ }
11000
+ /**
11001
+ * Moves the views of a replaced projection from the seed of the old spec to the seed of the new
11002
+ * one. A view that holds the old seed, or none when the old spec had none, takes the new seed. A
11003
+ * view a `mute()` gave its own table keeps it: the sets of the old seed in it go, the new seed
11004
+ * joins it.
11005
+ *
11006
+ * @param state - The projection branch of the plugin state.
11007
+ * @param name - The projection name.
11008
+ * @param from - The seed of the old spec, if any.
11009
+ * @param to - The seed of the new spec, if any.
11010
+ */
11011
+ function reseedViews(state, name, from, to) {
11012
+ if (from === to) return;
11013
+ for (const view of state.byEntity.values()) {
11014
+ if (view.projection !== name) continue;
11015
+ const stored = state.mutes.get(view.entity);
11016
+ if (stored !== from) reseedOwn(state, view.entity, stored, from, to);
11017
+ else if (to === void 0) state.mutes.delete(view.entity);
11018
+ else state.mutes.set(view.entity, to);
11019
+ }
11020
+ }
11021
+ //#endregion
10806
11022
  //#region src/plugins/world/projection/reconcile.ts
10807
11023
  /**
10808
11024
  * Creates the empty set of used hints. It lives in its own non-exported function because lint
@@ -10894,6 +11110,7 @@ function enterEntry(pctx, spec, mounted, entry) {
10894
11110
  const view = createViewRecord(entity, spec.name, entry.key, entry.item, values, entry.direct ? "mount" : "change");
10895
11111
  const state = pctx.ctx.state.projection;
10896
11112
  const building = state.building;
11113
+ seedMutes(state, spec, entity);
10897
11114
  mounted.live.set(entry.key, view);
10898
11115
  state.byEntity.set(entity, view);
10899
11116
  state.membershipVersion += 1;
@@ -11739,39 +11956,6 @@ function isConvergeCause(cause) {
11739
11956
  return cause === "load" || cause === "restore";
11740
11957
  }
11741
11958
  /**
11742
- * Creates the empty mute table of one entity. It lives in its own non-exported function because
11743
- * lint rule L5 refuses a collection built inside an exported declaration.
11744
- *
11745
- * @returns An empty table of component name to owned fields.
11746
- */
11747
- function emptyMuteTable() {
11748
- return /* @__PURE__ */ new Map();
11749
- }
11750
- /**
11751
- * Creates the empty field set of one muted component.
11752
- *
11753
- * @returns An empty set of field names.
11754
- */
11755
- function emptyFieldSet() {
11756
- return /* @__PURE__ */ new Set();
11757
- }
11758
- /**
11759
- * Deletes a released field set once it is empty, and the entity's table once no component is
11760
- * left. Only the set and table still stored are touched, so a remover that runs after a clear
11761
- * never drops a newer mute.
11762
- *
11763
- * @param mutes - The mute tables of every entity.
11764
- * @param entity - The muted entity.
11765
- * @param name - The muted component's name.
11766
- * @param owned - The field set the remover released from.
11767
- */
11768
- function dropEmptyMute(mutes, entity, name, owned) {
11769
- const byComponent = mutes.get(entity);
11770
- if (owned.size > 0 || byComponent?.get(name) !== owned) return;
11771
- byComponent.delete(name);
11772
- if (byComponent.size === 0) mutes.delete(entity);
11773
- }
11774
- /**
11775
11959
  * Creates the empty dirty record the next reconcile reads.
11776
11960
  *
11777
11961
  * @returns Nothing to reconcile yet.
@@ -11876,13 +12060,18 @@ function createProjectionApi(ctx, deps) {
11876
12060
  return {
11877
12061
  register: (spec) => {
11878
12062
  if (state.specs.has(spec.name)) throw new Error(`[game] Projection "${spec.name}" is registered twice.\n Give the second projection another name.`);
12063
+ learnSeed(state, spec);
11879
12064
  state.specs.set(spec.name, spec);
11880
12065
  },
11881
12066
  replace: (spec) => {
11882
- if (!state.specs.has(spec.name)) throw new Error(`[game] Projection "${spec.name}" is not registered.\n Register it before replacing it.`);
12067
+ const previous = state.specs.get(spec.name);
12068
+ if (previous === void 0) throw new Error(`[game] Projection "${spec.name}" is not registered.\n Register it before replacing it.`);
11883
12069
  const mounted = state.mounted.has(spec.name);
11884
12070
  if (mounted) checkLayers(spec, state.layers);
12071
+ const from = state.initialOnly.get(previous);
12072
+ const seed = learnSeed(state, spec);
11885
12073
  state.specs.set(spec.name, spec);
12074
+ reseedViews(state, spec.name, from, seed);
11886
12075
  if (mounted) forceRerun();
11887
12076
  },
11888
12077
  setLayers: (list) => {
@@ -11915,22 +12104,7 @@ function createProjectionApi(ctx, deps) {
11915
12104
  playSettle(pctx, view, spec, restingPeers(mounted), looseComponents(pctx, view));
11916
12105
  dropIfStill(pctx, view);
11917
12106
  },
11918
- mute: (entity, component, fields) => {
11919
- const byComponent = state.mutes.get(entity) ?? emptyMuteTable();
11920
- const owned = byComponent.get(component.componentName) ?? emptyFieldSet();
11921
- for (const field of fields) owned.add(field);
11922
- forgetJoined(state, owned);
11923
- byComponent.set(component.componentName, owned);
11924
- state.mutes.set(entity, byComponent);
11925
- let released = false;
11926
- return () => {
11927
- if (released) return;
11928
- released = true;
11929
- for (const field of fields) owned.delete(field);
11930
- forgetJoined(state, owned);
11931
- dropEmptyMute(state.mutes, entity, component.componentName, owned);
11932
- };
11933
- },
12107
+ mute: (entity, component, fields) => muteFields(state, entity, component.componentName, fields),
11934
12108
  lift: (entity, on) => {
11935
12109
  const view = state.byEntity.get(entity);
11936
12110
  if (view === void 0) return;
@@ -12380,6 +12554,7 @@ function createProjectionState() {
12380
12554
  keys: emptyMap$9(),
12381
12555
  keysByEntity: emptyMap$9(),
12382
12556
  mutes: emptyMap$9(),
12557
+ initialOnly: emptyWeakMap$2(),
12383
12558
  ownedSets: emptyWeakMap$2(),
12384
12559
  joinedMutes: emptyWeakMap$2(),
12385
12560
  converged: [],
@@ -29685,7 +29860,7 @@ function layerOf(sctx, entity) {
29685
29860
  //#endregion
29686
29861
  //#region src/plugins/renderer/sync/build.ts
29687
29862
  /** Views made between two reads of the clock: a read costs more than a small view. */
29688
- const CLOCK_EVERY = 8;
29863
+ const CLOCK_EVERY$1 = 8;
29689
29864
  /**
29690
29865
  * Tells whether a scene is being built: `left()` answers a finite number only while a build runs.
29691
29866
  *
@@ -29783,7 +29958,7 @@ function makeAside(sctx, report) {
29783
29958
  made += makeParentFirst(sctx, entity, report);
29784
29959
  if (made >= check) {
29785
29960
  if (projection.left() <= 0) break;
29786
- check = made + CLOCK_EVERY;
29961
+ check = made + CLOCK_EVERY$1;
29787
29962
  }
29788
29963
  }
29789
29964
  if (aside.head >= aside.count) {
@@ -38719,10 +38894,12 @@ function swapIn(ctx, scene, named, build) {
38719
38894
  * `world.projection.built()` first answers true commits it with the swap of a one-block switch. An
38720
38895
  * abort of the node discards the build and leaves the world as it stood.
38721
38896
  */
38722
- /** Half a second in ms: half of a frame interval is this over the frame rate. */
38723
- const HALF_SECOND_MS = 500;
38724
- /** The budget of a frame when `time` answers no frame rate to halve: half a 60 Hz frame, ms. */
38725
- const FALLBACK_BUDGET_MS = HALF_SECOND_MS / 60;
38897
+ /** A second in ms: the frame interval is this over the frame rate. */
38898
+ const SECOND_MS$1 = 1e3;
38899
+ /** The share of a frame interval `"auto"` gives the building work: two thirds. */
38900
+ const BUILD_SHARE = 2 / 3;
38901
+ /** The budget of a frame when `time` answers no usable frame rate: two thirds of a 60 Hz frame, ≈ 11.1 ms. */
38902
+ const FALLBACK_BUDGET_MS = SECOND_MS$1 / 60 * BUILD_SHARE;
38726
38903
  /**
38727
38904
  * Tells whether a value is a number a frame can count with: finite and above 0. Pure.
38728
38905
  *
@@ -38752,8 +38929,8 @@ function agentNow(ctx) {
38752
38929
  }
38753
38930
  /**
38754
38931
  * The building budget of a frame for a build that starts now: the number a game configured, or
38755
- * for `"auto"` half of the frame interval the loop runs at, `500 / time.rate().fps`, read once
38756
- * here. A rate that is not a finite number above 0 gives half a 60 Hz frame.
38932
+ * for `"auto"` two thirds of the frame interval the loop runs at, `(1000 / time.rate().fps) * 2 / 3`,
38933
+ * read once here. A rate that is not a finite number above 0 gives two thirds of a 60 Hz frame.
38757
38934
  *
38758
38935
  * @param ctx - Domain context of the plugin.
38759
38936
  * @returns Milliseconds per frame the building work may take.
@@ -38762,7 +38939,7 @@ function budgetOf(ctx) {
38762
38939
  const { buildMs } = ctx.config;
38763
38940
  if (buildMs !== "auto") return buildMs;
38764
38941
  const { fps } = ctx.deps.time.rate();
38765
- return isPositiveFinite(fps) ? HALF_SECOND_MS / fps : FALLBACK_BUDGET_MS;
38942
+ return isPositiveFinite(fps) ? SECOND_MS$1 / fps * BUILD_SHARE : FALLBACK_BUDGET_MS;
38766
38943
  }
38767
38944
  /**
38768
38945
  * Ends the build with the swap: its listeners go and the record is dropped, then the swap of a
@@ -39371,7 +39548,7 @@ function createScenesState() {
39371
39548
  * projections: [wardRoom]
39372
39549
  * });
39373
39550
  *
39374
- * // A game gives the next scene a fixed 3 ms of each frame (half of it by default) while the old one stays on the screen.
39551
+ * // A game gives the next scene a fixed 3 ms of each frame (two thirds of it by default) while the old one stays on the screen.
39375
39552
  * createApp({ plugins: [...screen, boardFeature], pluginConfigs: { scenes: { buildMs: 3 } } });
39376
39553
  * ```
39377
39554
  */
@@ -53712,7 +53889,9 @@ function dropHost(state, entity) {
53712
53889
  if (state.hosts.delete(entity)) hostsMoved(state);
53713
53890
  }
53714
53891
  /**
53715
- * The element of a host entity while it hosts: mounted, live and not leaving.
53892
+ * The element of a host entity while it hosts: mounted, live and not leaving. A host of a root
53893
+ * built aside that is applied in parts hosts nothing until the root is whole (spec 33 A7): the
53894
+ * last part gives it its views.
53716
53895
  *
53717
53896
  * @param state - The jsx state.
53718
53897
  * @param host - An entity of the host set.
@@ -53720,7 +53899,8 @@ function dropHost(state, entity) {
53720
53899
  */
53721
53900
  function liveHost(state, host) {
53722
53901
  const element = state.elements.get(host);
53723
- return element === void 0 || !element.live || state.exiting.has(host) ? void 0 : element;
53902
+ if (element === void 0 || !element.live || state.exiting.has(host)) return void 0;
53903
+ return state.roots.get(element.root)?.applying === void 0 ? element : void 0;
53724
53904
  }
53725
53905
  /**
53726
53906
  * The host that owns the views of a projection: the first live host that names it. A view
@@ -55810,7 +55990,7 @@ function coveredByLive(state, root) {
55810
55990
  * @param root - A mounted root.
55811
55991
  * @returns True when the settle step has to look at it.
55812
55992
  */
55813
- function waits(state, root) {
55993
+ function waits$1(state, root) {
55814
55994
  return (root.popup?.released === true || root.covered) && !state.removing.has(root.entity);
55815
55995
  }
55816
55996
  /**
@@ -55821,7 +56001,7 @@ function waits(state, root) {
55821
56001
  * @returns True when at least one root waits.
55822
56002
  */
55823
56003
  function anyWaits(state) {
55824
- for (const root of state.roots.values()) if (waits(state, root)) return true;
56004
+ for (const root of state.roots.values()) if (waits$1(state, root)) return true;
55825
56005
  return false;
55826
56006
  }
55827
56007
  /**
@@ -55844,7 +56024,7 @@ function settlePopups(ctx, state, unmount) {
55844
56024
  * @param unmount - Starts the exit of a root.
55845
56025
  */
55846
56026
  function settleWaiting(ctx, state, unmount) {
55847
- const waiting = [...state.roots.values()].filter((root) => waits(state, root));
56027
+ const waiting = [...state.roots.values()].filter((root) => waits$1(state, root));
55848
56028
  const rests = flowRests(ctx.deps.flow.state());
55849
56029
  waiting.sort((first, second) => second.order - first.order);
55850
56030
  for (const root of waiting) {
@@ -57184,6 +57364,320 @@ function settleOverlays(ctx, state, layout) {
57184
57364
  }
57185
57365
  }
57186
57366
  //#endregion
57367
+ //#region src/plugins/ui/jsx/parts.ts
57368
+ /**
57369
+ * How many elements a root built aside makes or applies in a frame between two reads of the
57370
+ * frame's budget, from the first read on (spec 33 A2, A6): the clock is read every 8 elements.
57371
+ */
57372
+ const CLOCK_EVERY = 8;
57373
+ /**
57374
+ * The most elements of roots built aside one frame applies. The components `applyFirst` adds are
57375
+ * flushed after the system, where the budget does not see them: the cap keeps that flush short
57376
+ * (spec 33 A6; 275 elements in one frame took 3.6 ms in Node, about 15 ms on the A26).
57377
+ */
57378
+ const APPLY_CAP = 100;
57379
+ /** What a parked part holds once it is back in the pool: no nodes and no element, so the pool keeps no tree alive. */
57380
+ const NO_NODES = [];
57381
+ /**
57382
+ * Tells whether a root has parked parts.
57383
+ *
57384
+ * @param root - The root.
57385
+ * @returns True while children of its elements wait.
57386
+ */
57387
+ function hasParked(root) {
57388
+ return (root.parked?.length ?? 0) > 0;
57389
+ }
57390
+ /**
57391
+ * Tells whether parts of a root built aside wait: children parked, or its application open.
57392
+ * Two field reads; false for every other root.
57393
+ *
57394
+ * @param root - The root, if it is still known.
57395
+ * @returns True while its build waits for a later frame.
57396
+ */
57397
+ function waits(root) {
57398
+ return root !== void 0 && (root.applying !== void 0 || hasParked(root));
57399
+ }
57400
+ /**
57401
+ * Builds the parts half of the reconcile: the park queue of each root built aside and the pools of
57402
+ * its parts, the drain of the queue, the hold of a dirty root, and the application in parts.
57403
+ *
57404
+ * @param ctx - Domain context of the ui plugin.
57405
+ * @param reconcile - The diff and the application the reconcile lends.
57406
+ * @returns The steps the reconcile calls.
57407
+ */
57408
+ function createParts(ctx, reconcile) {
57409
+ const state = ctx.state.jsx;
57410
+ const lookup = (entity) => state.elements.get(entity);
57411
+ let entered = 0;
57412
+ let spent = false;
57413
+ let unitRan = false;
57414
+ let applied = 0;
57415
+ let landed = false;
57416
+ const parkedPool = [];
57417
+ const cursorPool = [];
57418
+ /**
57419
+ * Starts the frame's reconcile: no element entered, the budget not read, no building unit run.
57420
+ */
57421
+ function beginReconcile() {
57422
+ entered = 0;
57423
+ spent = false;
57424
+ unitRan = false;
57425
+ }
57426
+ /**
57427
+ * Starts the frame's solve: no element applied in parts, no application ended.
57428
+ */
57429
+ function beginSolve() {
57430
+ applied = 0;
57431
+ landed = false;
57432
+ }
57433
+ /**
57434
+ * Tells whether an application of a root built aside ended in this solve: its scrolls settle now.
57435
+ *
57436
+ * @returns True after the last part of an application.
57437
+ */
57438
+ function hasLanded() {
57439
+ return landed;
57440
+ }
57441
+ /**
57442
+ * Tells whether the frame's building budget is spent, read once every `CLOCK_EVERY` units of
57443
+ * work: a read of the clock costs more than a small unit.
57444
+ *
57445
+ * @param count - The units of work made in this frame so far.
57446
+ * @returns True when the count is a reading point and no budget is left.
57447
+ */
57448
+ function spentAt(count) {
57449
+ return count > 0 && count % CLOCK_EVERY === 0 && ctx.deps.world.projection.left() <= 0;
57450
+ }
57451
+ /**
57452
+ * Counts an element entered for a root built aside and tells whether its children wait for a
57453
+ * later frame. From the 8th element of the frame the budget is read every 8; once it is spent,
57454
+ * the children of every next element wait. A row a range change scrolls in never parks: the
57455
+ * window moves in one block, as on a live root.
57456
+ *
57457
+ * @returns True when the children of the element wait.
57458
+ */
57459
+ function parks() {
57460
+ if (reconcile.isMoving()) return false;
57461
+ entered += 1;
57462
+ if (!spent) spent = spentAt(entered);
57463
+ return spent;
57464
+ }
57465
+ /**
57466
+ * Leaves the children of a new element of a root built aside for a later frame: the element
57467
+ * itself exists, so the child list of its parent is complete. The part is queued behind the
57468
+ * root's other parts, with the cause of the diff running now.
57469
+ *
57470
+ * @param root - The root built aside.
57471
+ * @param element - The element that entered.
57472
+ * @param nodes - The children it would have diffed.
57473
+ * @param instance - The identity of the nearest component instance.
57474
+ */
57475
+ function park(root, element, nodes, instance) {
57476
+ const cause = state.building;
57477
+ const part = parkedPool.pop() ?? {
57478
+ element,
57479
+ nodes,
57480
+ instance,
57481
+ cause
57482
+ };
57483
+ part.element = element;
57484
+ part.nodes = nodes;
57485
+ part.instance = instance;
57486
+ part.cause = cause;
57487
+ root.parked ??= [];
57488
+ root.parked.push(part);
57489
+ }
57490
+ /**
57491
+ * Puts a parked part back in the pool, its element and nodes dropped.
57492
+ *
57493
+ * @param part - A part that was diffed or forgotten.
57494
+ */
57495
+ function recycle(part) {
57496
+ part.element = void 0;
57497
+ part.nodes = NO_NODES;
57498
+ parkedPool.push(part);
57499
+ }
57500
+ /**
57501
+ * Tells whether the next building unit may run in this frame: the first one always does, so a
57502
+ * build always moves; a later one while the budget lasts.
57503
+ *
57504
+ * @returns True when the unit runs.
57505
+ */
57506
+ function mayRun() {
57507
+ return !unitRan || ctx.deps.world.projection.left() > 0;
57508
+ }
57509
+ /**
57510
+ * Diffs one parked part under the cause it was parked with. A part whose element left meanwhile
57511
+ * is dropped. A part that throws fails its root alone, as the root's own diff does.
57512
+ *
57513
+ * @param root - The root built aside.
57514
+ * @param part - The part.
57515
+ */
57516
+ function diffPart(root, part) {
57517
+ const element = part.element;
57518
+ if (element === void 0 || lookup(element.entity) !== element) return;
57519
+ const cause = state.building;
57520
+ unitRan = true;
57521
+ state.building = part.cause;
57522
+ try {
57523
+ reconcile.diffChildren(root, element, part.nodes, part.instance);
57524
+ } catch (error) {
57525
+ ctx.log.error("ui:root-failed", { root: root.name }, asError(error));
57526
+ } finally {
57527
+ state.building = cause;
57528
+ }
57529
+ root.needsSolve = true;
57530
+ }
57531
+ /**
57532
+ * Diffs the parked parts of the roots built aside, each root's in the order they were parked,
57533
+ * while the frame's budget lasts (spec 33 A3). Children that park again go to the end of their
57534
+ * root's queue.
57535
+ */
57536
+ function drainParked() {
57537
+ if (state.asideRoots === 0) return;
57538
+ for (const root of state.roots.values()) {
57539
+ const parked = root.parked;
57540
+ while (parked !== void 0 && parked.length > 0) {
57541
+ if (!mayRun()) return;
57542
+ const part = parked.shift();
57543
+ if (part === void 0) break;
57544
+ diffPart(root, part);
57545
+ recycle(part);
57546
+ }
57547
+ }
57548
+ }
57549
+ /**
57550
+ * Holds a dirty root built aside for a later frame instead of diffing it now: while parked parts
57551
+ * of it wait, so no element is made twice, and once the frame's budget is spent after its first
57552
+ * building unit. It stays dirty: the next frame lists it again and diffs its newest tree once
57553
+ * (spec 33 A1, A4). The drain runs before, until its queue is empty or the budget is spent, so a
57554
+ * root with parked parts meets a spent budget here too. A live root is never held.
57555
+ *
57556
+ * @param root - A root with work this frame.
57557
+ * @returns True when the root waits.
57558
+ */
57559
+ function holdRoot(root) {
57560
+ if (!ctx.deps.world.projection.isBuilding(root.entity)) return false;
57561
+ if (hasParked(root) || !mayRun()) {
57562
+ root.dirty = true;
57563
+ return true;
57564
+ }
57565
+ unitRan = true;
57566
+ return false;
57567
+ }
57568
+ /**
57569
+ * Forgets the parked parts and the open application of a root whose build ends here: a drop, a
57570
+ * discard, its exit or a restore.
57571
+ *
57572
+ * @param root - The root.
57573
+ */
57574
+ function forgetParts(root) {
57575
+ const parked = root.parked;
57576
+ while (parked !== void 0 && parked.length > 0) {
57577
+ const part = parked.pop();
57578
+ if (part !== void 0) recycle(part);
57579
+ }
57580
+ const cursor = root.applying;
57581
+ if (cursor === void 0) return;
57582
+ while (cursor.stack.length > 0) cursor.stack.pop();
57583
+ delete root.applying;
57584
+ cursorPool.push(cursor);
57585
+ }
57586
+ /**
57587
+ * Opens the application of a solved root built aside and applies its first part (spec 33 A6).
57588
+ *
57589
+ * @param root - The root built aside.
57590
+ * @param element - Its root element.
57591
+ * @param quiet - Whether its live elements take their rects with no motion.
57592
+ */
57593
+ function applyInParts(root, element, quiet) {
57594
+ const cursor = cursorPool.pop() ?? {
57595
+ stack: [],
57596
+ quiet
57597
+ };
57598
+ cursor.quiet = quiet;
57599
+ cursor.stack.push(element);
57600
+ root.applying = cursor;
57601
+ applyPart(root, cursor);
57602
+ }
57603
+ /**
57604
+ * Applies the next elements of an open application, top-down, parent before children in the
57605
+ * order of `applyRects`, until the frame's budget is spent (read every 8 elements of the frame)
57606
+ * or the frame applied `APPLY_CAP` elements of roots built aside. An element that left on the
57607
+ * way is skipped. The last element closes the application.
57608
+ *
57609
+ * @param root - The root built aside.
57610
+ * @param cursor - Its open application.
57611
+ */
57612
+ function applyPart(root, cursor) {
57613
+ const stack = cursor.stack;
57614
+ while (stack.length > 0 && applied < APPLY_CAP) {
57615
+ if (spentAt(applied)) return;
57616
+ const element = stack.pop();
57617
+ if (element === void 0 || lookup(element.entity) !== element) continue;
57618
+ reconcile.applyElement(element, cursor.quiet);
57619
+ applied += 1;
57620
+ stackChildren(stack, element);
57621
+ }
57622
+ if (stack.length === 0) closeApplication(root, cursor);
57623
+ }
57624
+ /**
57625
+ * Puts the children of an applied element on the stack of its application, the first one on top,
57626
+ * so they come next, in the order of `applyRects`.
57627
+ *
57628
+ * @param stack - The elements still to apply.
57629
+ * @param element - The element just applied.
57630
+ */
57631
+ function stackChildren(stack, element) {
57632
+ for (let index = element.children.length - 1; index >= 0; index -= 1) {
57633
+ const child = element.children[index];
57634
+ const childElement = child === void 0 ? void 0 : lookup(child);
57635
+ if (childElement !== void 0) stack.push(childElement);
57636
+ }
57637
+ }
57638
+ /**
57639
+ * Ends the application of a root built aside: its scrolls settle in this solve, and its live
57640
+ * hosts take their views in it, now that the root is whole (spec 33 A7).
57641
+ *
57642
+ * @param root - The root built aside.
57643
+ * @param cursor - Its application, empty.
57644
+ */
57645
+ function closeApplication(root, cursor) {
57646
+ delete root.applying;
57647
+ cursorPool.push(cursor);
57648
+ landed = true;
57649
+ for (const host of state.hosts) if (lookup(host)?.root === root.entity) state.risen.add(host);
57650
+ }
57651
+ /**
57652
+ * Applies the next part of every open application of a root built aside, in mount order, while
57653
+ * the frame's budget lasts. It runs before any root is solved: a root whose application ends
57654
+ * here is solved again in the same call when it asks for it.
57655
+ */
57656
+ function applyParts() {
57657
+ if (state.asideRoots === 0) return;
57658
+ for (const root of state.roots.values()) {
57659
+ const cursor = root.applying;
57660
+ if (cursor === void 0) continue;
57661
+ if (!mayRun()) return;
57662
+ unitRan = true;
57663
+ applyPart(root, cursor);
57664
+ }
57665
+ }
57666
+ return {
57667
+ beginReconcile,
57668
+ beginSolve,
57669
+ hasLanded,
57670
+ parks,
57671
+ park,
57672
+ waits,
57673
+ drainParked,
57674
+ holdRoot,
57675
+ forgetParts,
57676
+ applyInParts,
57677
+ applyParts
57678
+ };
57679
+ }
57680
+ //#endregion
57187
57681
  //#region src/plugins/ui/jsx/range.ts
57188
57682
  /**
57189
57683
  * Builds the windowed-scroll half of the reconcile: the row style, the slots a mount or a patch
@@ -57559,8 +58053,8 @@ function sameValue(left, right) {
57559
58053
  * that stays is compared prop by prop, by value: one that says what it said is neither styled nor
57560
58054
  * written. Beside it:
57561
58055
  * the range of a windowed scroll (`range.ts`), the rest and the motion of the extras
57562
- * (`extras.ts`), the hint routed to a keyed element (`hints.ts`) and the remeasure of the texts
57563
- * `text` resolved again (`remeasure.ts`).
58056
+ * (`extras.ts`), the hint routed to a keyed element (`hints.ts`), the remeasure of the texts
58057
+ * `text` resolved again (`remeasure.ts`) and a scene build in parts (`parts.ts`).
57564
58058
  */
57565
58059
  /** The four components an element may be drawn with; a state change can trade one for another. */
57566
58060
  const VISUALS = [
@@ -58067,6 +58561,11 @@ function createReconciler(ctx, modules, fields) {
58067
58561
  forgetSubtree,
58068
58562
  despawnTree
58069
58563
  });
58564
+ const parts = createParts(ctx, {
58565
+ diffChildren,
58566
+ applyElement,
58567
+ isMoving: range.isMoving
58568
+ });
58070
58569
  const dirtyList = [];
58071
58570
  const scrollList = [];
58072
58571
  const movedRoots = [];
@@ -58137,7 +58636,8 @@ function createReconciler(ctx, modules, fields) {
58137
58636
  function enter(root, parent, node, identity, instance, index) {
58138
58637
  checkTag(node);
58139
58638
  const entity = ecs.spawn(UI_OWNER, []);
58140
- if (isAside(root.entity)) ctx.deps.world.projection.adopt(entity);
58639
+ const aside = isAside(root.entity);
58640
+ if (aside) ctx.deps.world.projection.adopt(entity);
58141
58641
  const is = isFlagsOf(node, {
58142
58642
  pressed: false,
58143
58643
  hover: false,
@@ -58203,7 +58703,9 @@ function createReconciler(ctx, modules, fields) {
58203
58703
  epoch: state.epoch,
58204
58704
  rowHeight: range.rowHeightOf(parent, node)
58205
58705
  };
58206
- diffChildren(root, element, childrenOf(node, range.slotsFor(element), element.scroll?.axis), instance);
58706
+ const nodes = childrenOf(node, range.slotsFor(element), element.scroll?.axis);
58707
+ if (aside && parts.parks() && nodes.length > 0) parts.park(root, element, nodes, instance);
58708
+ else diffChildren(root, element, nodes, instance);
58207
58709
  root.needsSolve = true;
58208
58710
  return entity;
58209
58711
  }
@@ -58875,16 +59377,25 @@ function createReconciler(ctx, modules, fields) {
58875
59377
  * @param quiet - Whether a live element takes its rect with no motion.
58876
59378
  */
58877
59379
  function applyRects(element, quiet = false) {
58878
- const parent = parentRect(element);
58879
- if (element.live) applyLive(element, parent, quiet);
58880
- else applyFirst(element, parent);
58881
- element.moved = false;
59380
+ applyElement(element, quiet);
58882
59381
  for (const child of element.children) {
58883
59382
  const childElement = lookup(child);
58884
59383
  if (childElement !== void 0) applyRects(childElement, quiet);
58885
59384
  }
58886
59385
  }
58887
59386
  /**
59387
+ * Applies the solved rect of one element: a live one moves, a new one spawns its components.
59388
+ *
59389
+ * @param element - The element to apply.
59390
+ * @param quiet - Whether a live element takes its rect with no motion.
59391
+ */
59392
+ function applyElement(element, quiet) {
59393
+ const parent = parentRect(element);
59394
+ if (element.live) applyLive(element, parent, quiet);
59395
+ else applyFirst(element, parent);
59396
+ element.moved = false;
59397
+ }
59398
+ /**
58888
59399
  * Where the element of a root draws: its layer and order, and its slot with the place the slot
58889
59400
  * lists the root's projection at.
58890
59401
  *
@@ -59031,7 +59542,8 @@ function createReconciler(ctx, modules, fields) {
59031
59542
  }
59032
59543
  /**
59033
59544
  * Solves one root in a box and applies its rects: a root on the viewport when it is marked, a
59034
- * root in a slot also when its slot was resized.
59545
+ * root in a slot also when its slot was resized. A root built aside waits while parts of it wait,
59546
+ * and is applied in parts.
59035
59547
  *
59036
59548
  * @param root - The root.
59037
59549
  * @param box - The viewport, or the slot's content box.
@@ -59040,7 +59552,7 @@ function createReconciler(ctx, modules, fields) {
59040
59552
  */
59041
59553
  function solveOne(root, box, quiet) {
59042
59554
  const element = root.element === void 0 ? void 0 : lookup(root.element);
59043
- if (element === void 0) return false;
59555
+ if (element === void 0 || parts.waits(root)) return false;
59044
59556
  const resized = root.slot !== void 0 && !sameBox(root.slotRect, box);
59045
59557
  if (!root.needsSolve && !resized) return false;
59046
59558
  if (root.slot !== void 0) root.slotRect = {
@@ -59051,7 +59563,8 @@ function createReconciler(ctx, modules, fields) {
59051
59563
  };
59052
59564
  modules.layout.solve(element, box, lookup);
59053
59565
  root.needsSolve = false;
59054
- applyRects(element, quiet);
59566
+ if (root.aside !== void 0 && isAside(root.entity) && !element.live) parts.applyInParts(root, element, quiet);
59567
+ else applyRects(element, quiet);
59055
59568
  return true;
59056
59569
  }
59057
59570
  /**
@@ -59213,6 +59726,7 @@ function createReconciler(ctx, modules, fields) {
59213
59726
  for (const root of state.roots.values()) {
59214
59727
  if (state.removing.has(root.entity)) continue;
59215
59728
  const element = root.element === void 0 ? void 0 : lookup(root.element);
59729
+ parts.forgetParts(root);
59216
59730
  if (element !== void 0) {
59217
59731
  forgetSubtree(element);
59218
59732
  forgetInstances(state, element.identity);
@@ -59247,7 +59761,9 @@ function createReconciler(ctx, modules, fields) {
59247
59761
  }
59248
59762
  /**
59249
59763
  * The live scroll containers, which the frame step moves with the finger. Read from the set the
59250
- * enter fills and the exit empties, so an idle frame walks no other element.
59764
+ * enter fills and the exit empties, so an idle frame walks no other element. A scroll of a root
59765
+ * built aside waits while parts of the root wait: it is ranged, moved and settled once the root
59766
+ * is whole (spec 33 A7).
59251
59767
  *
59252
59768
  * @returns Every live element whose tag is `scroll`: the reconciler's own list, valid until the
59253
59769
  * next call.
@@ -59256,7 +59772,9 @@ function createReconciler(ctx, modules, fields) {
59256
59772
  while (scrollList.length > 0) scrollList.pop();
59257
59773
  for (const entity of state.scrolls) {
59258
59774
  const element = state.elements.get(entity);
59259
- if (element?.live === true) scrollList.push(element);
59775
+ if (element?.live !== true) continue;
59776
+ if (state.asideRoots > 0 && parts.waits(state.roots.get(element.root))) continue;
59777
+ scrollList.push(element);
59260
59778
  }
59261
59779
  return scrollList;
59262
59780
  }
@@ -59361,16 +59879,16 @@ function createReconciler(ctx, modules, fields) {
59361
59879
  }
59362
59880
  /**
59363
59881
  * The part of a scene build `ui` does, which `world` asks through `onBuild` (spec 31): done when no
59364
- * root of the build is dirty (its tree not diffed yet, or the layout engine not loaded) and none
59365
- * waits for its solve. A root whose first diff threw has no element to solve and does not hold
59366
- * the swap.
59882
+ * root of the build is dirty (its tree not diffed yet, or the layout engine not loaded), has
59883
+ * parked parts, waits for its solve or is applied in parts (spec 33 A8). A root whose first diff
59884
+ * threw has no element to solve and does not hold the swap.
59367
59885
  *
59368
59886
  * @returns True when every root built aside is reconciled and solved.
59369
59887
  */
59370
59888
  function asideBuilt() {
59371
59889
  for (const root of state.roots.values()) {
59372
59890
  const solves = root.needsSolve && root.element !== void 0 && lookup(root.element) !== void 0;
59373
- if ((root.dirty || solves) && isAside(root.entity)) return false;
59891
+ if ((root.dirty || solves || parts.waits(root)) && isAside(root.entity)) return false;
59374
59892
  }
59375
59893
  return true;
59376
59894
  }
@@ -59378,11 +59896,13 @@ function createReconciler(ctx, modules, fields) {
59378
59896
  * The first of the two systems of phase `layout`: the sweep, the roots a scene build committed, the
59379
59897
  * roots a restore asked to build again, the roots a scene placed again, the popups that may leave,
59380
59898
  * the pull of the text being typed, the scroll offsets and the windowed ranges, the bound texts
59381
- * that show a string of another size, the viewport, the dirty roots and the hosted views.
59899
+ * that show a string of another size, the viewport, the parked parts of a scene build, the dirty
59900
+ * roots and the hosted views.
59382
59901
  *
59383
59902
  * @param time - The frame's `Time`: the scrolls move by its `delta`.
59384
59903
  */
59385
59904
  function reconcile(time = NO_TIME) {
59905
+ parts.beginReconcile();
59386
59906
  sweep();
59387
59907
  state.reconciles += 1;
59388
59908
  lookWriter.pass += 1;
@@ -59400,9 +59920,10 @@ function createReconciler(ctx, modules, fields) {
59400
59920
  if (viewportChanged) state.epoch += 1;
59401
59921
  const count = dirtyRoots(viewportChanged);
59402
59922
  rendered = count;
59923
+ parts.drainParked();
59403
59924
  for (let index = 0; index < count; index += 1) {
59404
59925
  const root = dirtyList[index];
59405
- if (root !== void 0) reconcileDirtyRoot(root);
59926
+ if (root !== void 0 && !parts.holdRoot(root)) reconcileDirtyRoot(root);
59406
59927
  }
59407
59928
  hostViews(ctx, state);
59408
59929
  writeCounters();
@@ -59423,12 +59944,13 @@ function createReconciler(ctx, modules, fields) {
59423
59944
  counters.written = state.written;
59424
59945
  }
59425
59946
  /**
59426
- * The second system of phase `layout`: every root takes its slot, then one solve per marked
59427
- * root, a root in a slot after the root of its slot and in its content box, then the rects, the
59428
- * rest poses and the hooks, the scrolls settled on what the solve and the render gave them, the
59429
- * hosting of the hosts that came, a sweep of what left with nothing to wait for, and, in the swap
59430
- * frame of a scene build, the start pose of the enters it held. The hints released since the last frame step are dropped
59431
- * at its end, so the `change.Box` and `change.Transform` hooks the solve plays get the hint the
59947
+ * The second system of phase `layout`: every root takes its slot, the open applications of roots
59948
+ * built aside take their next part, then one solve per marked root, a root in a slot after the
59949
+ * root of its slot and in its content box, then the rects, the rest poses and the hooks, the
59950
+ * scrolls settled on what the solve and the render gave them, the hosting of the hosts that came,
59951
+ * a sweep of what left with nothing to wait for, and, in the swap frame of a scene build, the
59952
+ * start pose of the enters it held. The hints released since the last frame step are dropped at
59953
+ * its end, so the `change.Box` and `change.Transform` hooks the solve plays get the hint the
59432
59954
  * reconcile routed, and the next frame starts with none.
59433
59955
  *
59434
59956
  * @param time - The frame's `Time`: a scroll left moving asks for a frame only while it runs.
@@ -59437,10 +59959,12 @@ function createReconciler(ctx, modules, fields) {
59437
59959
  const viewport = modules.styles.viewport();
59438
59960
  viewportBox.w = viewport?.width ?? 0;
59439
59961
  viewportBox.h = viewport?.height ?? 0;
59962
+ parts.beginSolve();
59440
59963
  assignSlots();
59964
+ parts.applyParts();
59441
59965
  const changed = layOutRoots(viewportBox);
59442
59966
  if (changed) ctx.deps.time.wake();
59443
- if (changed || rendered > 0) modules.layout.settleScroll(scrollContainers(), lookup, time);
59967
+ if (changed || rendered > 0 || parts.hasLanded()) modules.layout.settleScroll(scrollContainers(), lookup, time);
59444
59968
  rendered = 0;
59445
59969
  settleOverlays(ctx, state, modules.layout);
59446
59970
  if (state.risen.size > 0) {
@@ -59516,6 +60040,7 @@ function createReconciler(ctx, modules, fields) {
59516
60040
  if (root === void 0 || state.removing.has(entity)) return;
59517
60041
  const element = root.element === void 0 ? void 0 : lookup(root.element);
59518
60042
  const cause = root.popup === void 0 ? ctx.deps.world.projection.causeOf(entity) ?? "change" : "change";
60043
+ parts.forgetParts(root);
59519
60044
  if (cause === "discard" || isAside(entity)) {
59520
60045
  dropRoot(root, element);
59521
60046
  return;
@@ -64225,9 +64750,11 @@ function scenesFor() {
64225
64750
  /**
64226
64751
  * Types a projection: keyed items of the model turned into entities. `Item` is inferred from the
64227
64752
  * return of `from` and reaches `key`, `view` and every motion hook; `layer` and `lift` stay
64228
- * literal, so `defineScene` can check them against the layers of the scene.
64753
+ * literal, so `defineScene` can check them against the layers of the scene. Each pair of
64754
+ * `initialOnly` has its field names checked against its own component.
64229
64755
  *
64230
- * @param spec - Name, layer, optional lift layer, `from`, `key`, `view` and the motion hooks.
64756
+ * @param spec - Name, layer, optional lift layer, `from`, `key`, `view`, the motion hooks and
64757
+ * the `initialOnly` fields.
64231
64758
  * @returns The same spec, typed.
64232
64759
  * @example
64233
64760
  * ```ts
@@ -64628,7 +65155,7 @@ function cubicBezier(x1, y1, x2, y2) {
64628
65155
  * | assets | preloadDepth | 2 |
64629
65156
  * | assets | baseUrl | undefined |
64630
65157
  * | assets | io | undefined, the browser fetch and the renderer textures |
64631
- * | scenes | buildMs | "auto", half of a frame (500 / time.rate().fps), read at each build start; a number fixes it |
65158
+ * | scenes | buildMs | "auto", two thirds of a frame ((1000 / time.rate().fps) * 2 / 3, 11.1 ms at 60 Hz), read at each build start; a number fixes it |
64632
65159
  * | anim | maxTracks | 2000 |
64633
65160
  * | anim | reducedMotion | false |
64634
65161
  * | i18n | locale | "en" |