@moku-labs/game 0.15.3 → 0.15.5
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.
- package/dist/app/page.d.mts +1 -1
- package/dist/app/system.d.mts +3 -3
- package/dist/app.d.mts +3 -3
- package/dist/app.mjs +1 -1
- package/dist/assets.d.mts +1 -1
- package/dist/{commands-31d2BEwc.d.mts → commands-e_I5i8dK.d.mts} +5 -5
- package/dist/control.d.mts +4 -4
- package/dist/{define-DYJwRXB_.d.mts → define-CNctRrea.d.mts} +2 -2
- package/dist/{headless-D0U5AXiK.d.mts → headless-DDkRwLnJ.d.mts} +1 -1
- package/dist/{index-ByvWj9OX.d.mts → index-Bq_93oZv.d.mts} +5 -7
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/inspect.d.mts +6 -6
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{runtime-biKbodaT.d.mts → runtime-BGsDXKuO.d.mts} +1 -1
- package/dist/{src-xChMXzwH.mjs → src-DnclBdeU.mjs} +1031 -883
- package/dist/testing.d.mts +4 -4
- package/dist/testing.mjs +1 -1
- package/dist/{types-BApuIqwq.d.mts → types-6inPYTzE.d.mts} +4 -4
- package/dist/{types-CsMLBPf-.d.mts → types-C2g2KYCN.d.mts} +2 -2
- package/dist/{types-yLdzHyTl.d.mts → types-CWSMo70I.d.mts} +14 -11
- package/dist/{types-B0bmoyRT.d.mts → types-DFoxDq-g.d.mts} +2 -2
- package/dist/{types-DlC3Ytbj.d.mts → types-DnttyLeu.d.mts} +131 -134
- package/dist/{visual-G6F0Mi9H.d.mts → visual-DalSHy9z.d.mts} +6 -6
- package/dist/visual.d.mts +1 -1
- package/llms.txt +2 -3
- package/package.json +1 -1
|
@@ -3753,7 +3753,7 @@ function featureProblems(flows, features) {
|
|
|
3753
3753
|
* sceneIdOf("board"); // undefined: a scene is an object with an id
|
|
3754
3754
|
* ```
|
|
3755
3755
|
*/
|
|
3756
|
-
function sceneIdOf(value) {
|
|
3756
|
+
function sceneIdOf$1(value) {
|
|
3757
3757
|
if (typeof value !== "object" || value === null || !("id" in value)) return void 0;
|
|
3758
3758
|
return typeof value.id === "string" ? value.id : void 0;
|
|
3759
3759
|
}
|
|
@@ -3772,7 +3772,7 @@ function declaredScenes(features) {
|
|
|
3772
3772
|
for (const list of lists) {
|
|
3773
3773
|
if (!Array.isArray(list)) continue;
|
|
3774
3774
|
for (const scene of list) {
|
|
3775
|
-
const id = sceneIdOf(scene);
|
|
3775
|
+
const id = sceneIdOf$1(scene);
|
|
3776
3776
|
if (id !== void 0) ids.add(id);
|
|
3777
3777
|
}
|
|
3778
3778
|
}
|
|
@@ -8374,6 +8374,32 @@ const NO_ROWS = {
|
|
|
8374
8374
|
next: () => DONE
|
|
8375
8375
|
};
|
|
8376
8376
|
/**
|
|
8377
|
+
* Tells whether a system's query may match, before its run: every term has a store and none of them
|
|
8378
|
+
* is empty, which is the smallest store among the terms not being empty. A store met for the first
|
|
8379
|
+
* time is kept in the entry, so later checks only read sizes. An empty query may always match. An
|
|
8380
|
+
* entity that carries one term and not the others still passes: the walk finds nothing, as before.
|
|
8381
|
+
*
|
|
8382
|
+
* @param stores - Every component store of the world, by name.
|
|
8383
|
+
* @param entry - The registered system.
|
|
8384
|
+
* @returns False when a term has no entity: the system is skipped.
|
|
8385
|
+
*/
|
|
8386
|
+
function mayMatch(stores, entry) {
|
|
8387
|
+
const names = entry.plan.names;
|
|
8388
|
+
const kept = entry.stores;
|
|
8389
|
+
const count = names.length;
|
|
8390
|
+
for (let index = 0; index < count; index += 1) {
|
|
8391
|
+
let store = kept[index];
|
|
8392
|
+
if (store === void 0) {
|
|
8393
|
+
const name = names[index];
|
|
8394
|
+
store = name === void 0 ? void 0 : stores.get(name);
|
|
8395
|
+
if (store === void 0) return false;
|
|
8396
|
+
kept[index] = store;
|
|
8397
|
+
}
|
|
8398
|
+
if (store.size === 0) return false;
|
|
8399
|
+
}
|
|
8400
|
+
return true;
|
|
8401
|
+
}
|
|
8402
|
+
/**
|
|
8377
8403
|
* The rows the runner hands a system's `run`: its walk, reset; none for an empty query. A system
|
|
8378
8404
|
* entry made without a walk gets one here, once.
|
|
8379
8405
|
*
|
|
@@ -8467,6 +8493,113 @@ function rememberPlan(ctx, key, terms) {
|
|
|
8467
8493
|
return plan;
|
|
8468
8494
|
}
|
|
8469
8495
|
//#endregion
|
|
8496
|
+
//#region src/plugins/world/ecs/scenes.ts
|
|
8497
|
+
/**
|
|
8498
|
+
* Creates the set of declared scene ids. Its own function: a collection is built outside the
|
|
8499
|
+
* exported declarations.
|
|
8500
|
+
*
|
|
8501
|
+
* @returns An empty set.
|
|
8502
|
+
*/
|
|
8503
|
+
function emptyIds() {
|
|
8504
|
+
return /* @__PURE__ */ new Set();
|
|
8505
|
+
}
|
|
8506
|
+
/**
|
|
8507
|
+
* Reads the id of one entry of a feature's `scenes` list, without trusting the entry.
|
|
8508
|
+
*
|
|
8509
|
+
* @param entry - What the feature put in the list.
|
|
8510
|
+
* @returns The id, or `undefined` when the entry carries none.
|
|
8511
|
+
* @example
|
|
8512
|
+
* ```ts
|
|
8513
|
+
* sceneIdOf({ id: "board", layers: {} }); // "board"
|
|
8514
|
+
* ```
|
|
8515
|
+
*/
|
|
8516
|
+
function sceneIdOf(entry) {
|
|
8517
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
8518
|
+
const id = entry.id;
|
|
8519
|
+
return typeof id === "string" ? id : void 0;
|
|
8520
|
+
}
|
|
8521
|
+
/**
|
|
8522
|
+
* Records the scene ids every feature declares, so the systems registered from now on have their
|
|
8523
|
+
* `scenes` checked. Called once at start, before the features' systems register.
|
|
8524
|
+
*
|
|
8525
|
+
* @param scene - The scene state.
|
|
8526
|
+
* @param features - The registered features.
|
|
8527
|
+
*/
|
|
8528
|
+
function declareScenes(scene, features) {
|
|
8529
|
+
const declared = emptyIds();
|
|
8530
|
+
for (const feature of features) {
|
|
8531
|
+
const list = feature.description.scenes;
|
|
8532
|
+
if (!Array.isArray(list)) continue;
|
|
8533
|
+
for (const entry of list) {
|
|
8534
|
+
const id = sceneIdOf(entry);
|
|
8535
|
+
if (id !== void 0) declared.add(id);
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
scene.declared = declared;
|
|
8539
|
+
}
|
|
8540
|
+
/**
|
|
8541
|
+
* Throws for a name in a system's `scenes` that no feature declares as a scene: such a system
|
|
8542
|
+
* would never run. Nothing is checked before start, when the declared scenes are not known yet.
|
|
8543
|
+
* The caller guards it for dev builds.
|
|
8544
|
+
*
|
|
8545
|
+
* @param scene - The scene state.
|
|
8546
|
+
* @param definition - The system being registered.
|
|
8547
|
+
* @throws {Error} When a name in `scenes` is not a declared scene.
|
|
8548
|
+
*/
|
|
8549
|
+
function checkScenes(scene, definition) {
|
|
8550
|
+
const declared = scene.declared;
|
|
8551
|
+
if (declared === void 0 || definition.scenes === void 0) return;
|
|
8552
|
+
for (const name of definition.scenes) {
|
|
8553
|
+
if (declared.has(name)) continue;
|
|
8554
|
+
const known = [...declared].join(", ") || "none";
|
|
8555
|
+
throw new Error(`[game] System "${definition.name}" runs on the scene "${name}", which no feature declares.\n Name a scene a feature lists in its scenes (declared: ${known}).`);
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
/**
|
|
8559
|
+
* Mounts a scene for the systems: the scene mounted before starts leaving. The same scene again
|
|
8560
|
+
* changes nothing.
|
|
8561
|
+
*
|
|
8562
|
+
* @param scene - The scene state.
|
|
8563
|
+
* @param id - The id of the scene now mounted.
|
|
8564
|
+
*/
|
|
8565
|
+
function mountScene(scene, id) {
|
|
8566
|
+
const before = scene.mounted;
|
|
8567
|
+
if (before === id) return;
|
|
8568
|
+
if (before !== void 0 && !scene.leaving.includes(before)) scene.leaving.push(before);
|
|
8569
|
+
scene.mounted = id;
|
|
8570
|
+
}
|
|
8571
|
+
/**
|
|
8572
|
+
* Ends the leave of every leaving scene once nothing carries `Exiting`: the elements of the JSX
|
|
8573
|
+
* views of the scenes before played their exit. Called at the start of a phase while a scene leaves.
|
|
8574
|
+
*
|
|
8575
|
+
* @param state - The ecs branch of the plugin state.
|
|
8576
|
+
*/
|
|
8577
|
+
function endLeaving(state) {
|
|
8578
|
+
const exiting = state.stores.get(Exiting.componentName);
|
|
8579
|
+
if (exiting !== void 0 && exiting.size > 0) return;
|
|
8580
|
+
const leaving = state.scene.leaving;
|
|
8581
|
+
while (leaving.length > 0) leaving.pop();
|
|
8582
|
+
}
|
|
8583
|
+
/**
|
|
8584
|
+
* Tells whether one of a system's scenes is mounted or leaving.
|
|
8585
|
+
*
|
|
8586
|
+
* @param scene - The scene state.
|
|
8587
|
+
* @param names - The system's `scenes`.
|
|
8588
|
+
* @returns True when the system may run.
|
|
8589
|
+
*/
|
|
8590
|
+
function onScene(scene, names) {
|
|
8591
|
+
const { mounted, leaving } = scene;
|
|
8592
|
+
const count = names.length;
|
|
8593
|
+
const left = leaving.length;
|
|
8594
|
+
for (let index = 0; index < count; index += 1) {
|
|
8595
|
+
const name = names[index];
|
|
8596
|
+
if (name === void 0) continue;
|
|
8597
|
+
if (name === mounted) return true;
|
|
8598
|
+
for (let at = 0; at < left; at += 1) if (leaving[at] === name) return true;
|
|
8599
|
+
}
|
|
8600
|
+
return false;
|
|
8601
|
+
}
|
|
8602
|
+
//#endregion
|
|
8470
8603
|
//#region src/plugins/world/ecs/schema.ts
|
|
8471
8604
|
const notJson = null;
|
|
8472
8605
|
/**
|
|
@@ -8546,294 +8679,6 @@ function schemaOf(types) {
|
|
|
8546
8679
|
return [...types].map((type) => schemaOfType(type)).toSorted((left, right) => left.name.localeCompare(right.name));
|
|
8547
8680
|
}
|
|
8548
8681
|
//#endregion
|
|
8549
|
-
//#region src/plugins/world/ecs/scope.ts
|
|
8550
|
-
/** The keys of a description whose entries can make a feature active. */
|
|
8551
|
-
const SOURCES = [
|
|
8552
|
-
"nodes",
|
|
8553
|
-
"flows",
|
|
8554
|
-
"scenes",
|
|
8555
|
-
"projections",
|
|
8556
|
-
"ui"
|
|
8557
|
-
];
|
|
8558
|
-
/** The release of a hold that holds nothing. */
|
|
8559
|
-
const RELEASE_NOTHING = () => {};
|
|
8560
|
-
/**
|
|
8561
|
-
* Creates an empty table of owners. Its own function: a collection is built outside the exported
|
|
8562
|
-
* declarations.
|
|
8563
|
-
*
|
|
8564
|
-
* @returns An empty map.
|
|
8565
|
-
*/
|
|
8566
|
-
function emptyOwners$2() {
|
|
8567
|
-
return /* @__PURE__ */ new Map();
|
|
8568
|
-
}
|
|
8569
|
-
/**
|
|
8570
|
-
* Sizes the scope to the features of the game, in registration order: the only allocation of the
|
|
8571
|
-
* scope, once per start. Whatever an earlier start left is dropped first.
|
|
8572
|
-
*
|
|
8573
|
-
* @param ctx - Domain context of the world plugin.
|
|
8574
|
-
*/
|
|
8575
|
-
function openScope(ctx) {
|
|
8576
|
-
const scope = ctx.state.ecs.scope;
|
|
8577
|
-
const features = ctx.deps.flow.features.all();
|
|
8578
|
-
const count = features.length;
|
|
8579
|
-
resetScope(scope);
|
|
8580
|
-
for (const [at, feature] of features.entries()) {
|
|
8581
|
-
scope.names.push(feature.name);
|
|
8582
|
-
scope.index.set(feature.name, at);
|
|
8583
|
-
}
|
|
8584
|
-
scope.onStack = new Uint8Array(count);
|
|
8585
|
-
scope.active = new Uint8Array(count);
|
|
8586
|
-
scope.holds = new Int32Array(count);
|
|
8587
|
-
scope.views = new Int32Array(count);
|
|
8588
|
-
scope.dirty = true;
|
|
8589
|
-
}
|
|
8590
|
-
/**
|
|
8591
|
-
* Empties the scope in place, as a world that never started has it, and moves its generation on:
|
|
8592
|
-
* a release kept past this changes nothing, also once the world started again.
|
|
8593
|
-
*
|
|
8594
|
-
* @param scope - The scope.
|
|
8595
|
-
*/
|
|
8596
|
-
function resetScope(scope) {
|
|
8597
|
-
scope.generation += 1;
|
|
8598
|
-
scope.names.length = 0;
|
|
8599
|
-
scope.index.clear();
|
|
8600
|
-
scope.projections.clear();
|
|
8601
|
-
scope.owners = void 0;
|
|
8602
|
-
scope.ownersFailed = false;
|
|
8603
|
-
scope.flow = void 0;
|
|
8604
|
-
scope.onStack = /* @__PURE__ */ new Uint8Array(0);
|
|
8605
|
-
scope.active = /* @__PURE__ */ new Uint8Array(0);
|
|
8606
|
-
scope.holds = /* @__PURE__ */ new Int32Array(0);
|
|
8607
|
-
scope.views = /* @__PURE__ */ new Int32Array(0);
|
|
8608
|
-
scope.dirty = false;
|
|
8609
|
-
scope.scoped = false;
|
|
8610
|
-
}
|
|
8611
|
-
/**
|
|
8612
|
-
* The scope the systems of one feature register with: its index for `systemsRun: "active"`, -1
|
|
8613
|
-
* otherwise. A value outside the type (a hand-written `features.register`) is reported and its
|
|
8614
|
-
* systems always run.
|
|
8615
|
-
*
|
|
8616
|
-
* @param ctx - Domain context of the world plugin.
|
|
8617
|
-
* @param at - The feature's index.
|
|
8618
|
-
* @param feature - The feature and what it registered.
|
|
8619
|
-
* @param feature.name - Feature name, for the log entry.
|
|
8620
|
-
* @param feature.description - What the feature brought.
|
|
8621
|
-
* @returns The scope of its systems.
|
|
8622
|
-
*/
|
|
8623
|
-
function scopeOf(ctx, at, feature) {
|
|
8624
|
-
const run = feature.description.systemsRun;
|
|
8625
|
-
if (run === "active") return at;
|
|
8626
|
-
if (run !== void 0 && run !== "always") ctx.log.warn("world:bad-feature-entry", {
|
|
8627
|
-
feature: feature.name,
|
|
8628
|
-
key: "systemsRun"
|
|
8629
|
-
});
|
|
8630
|
-
return -1;
|
|
8631
|
-
}
|
|
8632
|
-
/**
|
|
8633
|
-
* Warns about a feature whose systems wait for it to be active while nothing can make it active:
|
|
8634
|
-
* it has systems and `systemsRun: "active"`, and no node, flow, scene, projection or ui component.
|
|
8635
|
-
* The caller guards it for dev builds.
|
|
8636
|
-
*
|
|
8637
|
-
* @param ctx - Domain context of the world plugin.
|
|
8638
|
-
* @param feature - The feature and what it registered.
|
|
8639
|
-
* @param feature.name - Feature name, for the warning.
|
|
8640
|
-
* @param feature.description - What the feature brought.
|
|
8641
|
-
*/
|
|
8642
|
-
function reportNeverActive(ctx, feature) {
|
|
8643
|
-
const { name, description } = feature;
|
|
8644
|
-
const systems = description.systems;
|
|
8645
|
-
if (description.systemsRun !== "active") return;
|
|
8646
|
-
if (!Array.isArray(systems) || systems.length === 0) return;
|
|
8647
|
-
for (const key of SOURCES) {
|
|
8648
|
-
const list = description[key];
|
|
8649
|
-
if (Array.isArray(list) && list.length > 0) return;
|
|
8650
|
-
}
|
|
8651
|
-
ctx.log.warn("world:feature-never-active", {
|
|
8652
|
-
feature: name,
|
|
8653
|
-
message: `Feature "${name}" runs its systems only while it is active, but it has no node, flow, scene, projection or ui component, so nothing makes it active and its systems never run. Remove systemsRun: "active" from it.`
|
|
8654
|
-
});
|
|
8655
|
-
}
|
|
8656
|
-
/**
|
|
8657
|
-
* Builds the owner table of rule N from one `describe()`: flow id to node name to the index of the
|
|
8658
|
-
* feature that owns the node. A node without an owner, or owned by a name that is not a feature,
|
|
8659
|
-
* is left out.
|
|
8660
|
-
*
|
|
8661
|
-
* @param graph - The described graph.
|
|
8662
|
-
* @param index - Feature name to index.
|
|
8663
|
-
* @returns The owner table.
|
|
8664
|
-
*/
|
|
8665
|
-
function ownersOf$1(graph, index) {
|
|
8666
|
-
const owners = emptyOwners$2();
|
|
8667
|
-
for (const [id, flow] of Object.entries(graph.flows)) {
|
|
8668
|
-
const nodes = emptyOwners$2();
|
|
8669
|
-
for (const [node, info] of Object.entries(flow.nodes)) {
|
|
8670
|
-
const at = info.owner === void 0 ? void 0 : index.get(info.owner);
|
|
8671
|
-
if (at !== void 0) nodes.set(node, at);
|
|
8672
|
-
}
|
|
8673
|
-
if (nodes.size > 0) owners.set(id, nodes);
|
|
8674
|
-
}
|
|
8675
|
-
return owners;
|
|
8676
|
-
}
|
|
8677
|
-
/**
|
|
8678
|
-
* Builds the owner table once, at the first stack that is not empty: the graph is sealed by
|
|
8679
|
-
* `flow.run()`. A `describe()` that throws is logged once and makes every feature count as on the
|
|
8680
|
-
* stack (fail-safe).
|
|
8681
|
-
*
|
|
8682
|
-
* @param ctx - Domain context of the world plugin.
|
|
8683
|
-
* @param scope - The scope.
|
|
8684
|
-
*/
|
|
8685
|
-
function buildOwners(ctx, scope) {
|
|
8686
|
-
try {
|
|
8687
|
-
scope.owners = ownersOf$1(ctx.deps.flow.describe(), scope.index);
|
|
8688
|
-
} catch (error) {
|
|
8689
|
-
scope.ownersFailed = true;
|
|
8690
|
-
ctx.log.error("world:scope-owners-failed", {}, asError$3(error));
|
|
8691
|
-
}
|
|
8692
|
-
}
|
|
8693
|
-
/**
|
|
8694
|
-
* Sets the stack flags of rule N from the frames of one flow state, every frame counted, the
|
|
8695
|
-
* outer frames of sub-flows included.
|
|
8696
|
-
*
|
|
8697
|
-
* @param ctx - Domain context of the world plugin.
|
|
8698
|
-
* @param scope - The scope.
|
|
8699
|
-
* @param stack - The frames of the flow state.
|
|
8700
|
-
*/
|
|
8701
|
-
function readStack$1(ctx, scope, stack) {
|
|
8702
|
-
const onStack = scope.onStack;
|
|
8703
|
-
const depth = stack.length;
|
|
8704
|
-
onStack.fill(0);
|
|
8705
|
-
if (depth === 0) return;
|
|
8706
|
-
if (scope.owners === void 0 && !scope.ownersFailed) buildOwners(ctx, scope);
|
|
8707
|
-
const owners = scope.owners;
|
|
8708
|
-
if (owners === void 0) {
|
|
8709
|
-
onStack.fill(1);
|
|
8710
|
-
return;
|
|
8711
|
-
}
|
|
8712
|
-
for (let index = 0; index < depth; index += 1) {
|
|
8713
|
-
const frame = stack[index];
|
|
8714
|
-
const at = frame === void 0 ? void 0 : owners.get(frame.flow)?.get(frame.node);
|
|
8715
|
-
if (at !== void 0) onStack[at] = 1;
|
|
8716
|
-
}
|
|
8717
|
-
}
|
|
8718
|
-
/**
|
|
8719
|
-
* Reads the flow stack again when `flow.state()` answers a new object. The state is memoised, so a
|
|
8720
|
-
* frame in which the graph did not move costs one call and one comparison.
|
|
8721
|
-
*
|
|
8722
|
-
* @param ctx - Domain context of the world plugin.
|
|
8723
|
-
* @param scope - The scope.
|
|
8724
|
-
*/
|
|
8725
|
-
function readSources(ctx, scope) {
|
|
8726
|
-
const flowState = ctx.deps.flow.state();
|
|
8727
|
-
if (flowState === scope.flow) return;
|
|
8728
|
-
scope.flow = flowState;
|
|
8729
|
-
readStack$1(ctx, scope, flowState.stack);
|
|
8730
|
-
scope.dirty = true;
|
|
8731
|
-
}
|
|
8732
|
-
/**
|
|
8733
|
-
* Evaluates the active set at the start of a phase that runs systems: a feature is active while it
|
|
8734
|
-
* is on the stack, held, or has a view. Nothing moved: one call, one comparison and one flag test.
|
|
8735
|
-
*
|
|
8736
|
-
* @param ctx - Domain context of the world plugin.
|
|
8737
|
-
*/
|
|
8738
|
-
function refreshScope(ctx) {
|
|
8739
|
-
const scope = ctx.state.ecs.scope;
|
|
8740
|
-
readSources(ctx, scope);
|
|
8741
|
-
if (!scope.dirty) return;
|
|
8742
|
-
const { onStack, holds, views, active } = scope;
|
|
8743
|
-
const count = scope.names.length;
|
|
8744
|
-
for (let at = 0; at < count; at += 1) active[at] = onStack[at] === 1 || (holds[at] ?? 0) > 0 || (views[at] ?? 0) > 0 ? 1 : 0;
|
|
8745
|
-
scope.dirty = false;
|
|
8746
|
-
}
|
|
8747
|
-
/**
|
|
8748
|
-
* The release of one hold: it takes the hold back once, never below 0, and only in the run it was
|
|
8749
|
-
* taken in. A release kept across a stop and a start finds a newer generation and changes nothing
|
|
8750
|
-
* (spec 28 W7.1).
|
|
8751
|
-
*
|
|
8752
|
-
* @param scope - The scope.
|
|
8753
|
-
* @param at - The held feature's index.
|
|
8754
|
-
* @returns The idempotent release.
|
|
8755
|
-
*/
|
|
8756
|
-
function releaseOf(scope, at) {
|
|
8757
|
-
const hold = {
|
|
8758
|
-
released: false,
|
|
8759
|
-
generation: scope.generation
|
|
8760
|
-
};
|
|
8761
|
-
return () => {
|
|
8762
|
-
if (hold.released || hold.generation !== scope.generation) return;
|
|
8763
|
-
hold.released = true;
|
|
8764
|
-
scope.holds[at] = Math.max(0, (scope.holds[at] ?? 0) - 1);
|
|
8765
|
-
scope.dirty = true;
|
|
8766
|
-
};
|
|
8767
|
-
}
|
|
8768
|
-
/**
|
|
8769
|
-
* Takes one hold on a feature. A name that is not a feature holds nothing; a dev build warns.
|
|
8770
|
-
*
|
|
8771
|
-
* @param ctx - Domain context of the world plugin.
|
|
8772
|
-
* @param feature - The feature to keep active.
|
|
8773
|
-
* @returns The release.
|
|
8774
|
-
*/
|
|
8775
|
-
function holdFeature(ctx, feature) {
|
|
8776
|
-
const scope = ctx.state.ecs.scope;
|
|
8777
|
-
const at = scope.index.get(feature);
|
|
8778
|
-
if (at === void 0) {
|
|
8779
|
-
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) ctx.log.warn("world:hold-unknown-feature", { feature });
|
|
8780
|
-
return RELEASE_NOTHING;
|
|
8781
|
-
}
|
|
8782
|
-
scope.holds[at] = (scope.holds[at] ?? 0) + 1;
|
|
8783
|
-
scope.dirty = true;
|
|
8784
|
-
return releaseOf(scope, at);
|
|
8785
|
-
}
|
|
8786
|
-
/**
|
|
8787
|
-
* Whether a feature is active now, from the sources as they are at this moment. It leaves the
|
|
8788
|
-
* active set of the running phase alone: a source that moves inside a phase counts for the
|
|
8789
|
-
* systems from the next phase.
|
|
8790
|
-
*
|
|
8791
|
-
* @param ctx - Domain context of the world plugin.
|
|
8792
|
-
* @param feature - The feature name.
|
|
8793
|
-
* @returns True while it is on the stack, held or has a view; false for a name that is not a feature.
|
|
8794
|
-
*/
|
|
8795
|
-
function isFeatureActive(ctx, feature) {
|
|
8796
|
-
const scope = ctx.state.ecs.scope;
|
|
8797
|
-
const at = scope.index.get(feature);
|
|
8798
|
-
if (at === void 0) return false;
|
|
8799
|
-
readSources(ctx, scope);
|
|
8800
|
-
return scope.onStack[at] === 1 || (scope.holds[at] ?? 0) > 0 || (scope.views[at] ?? 0) > 0;
|
|
8801
|
-
}
|
|
8802
|
-
/**
|
|
8803
|
-
* Counts a new view of a projection for the feature that registered it. A projection no feature
|
|
8804
|
-
* registered counts for nobody.
|
|
8805
|
-
*
|
|
8806
|
-
* @param scope - The scope.
|
|
8807
|
-
* @param projection - The projection name.
|
|
8808
|
-
*/
|
|
8809
|
-
function viewCame(scope, projection) {
|
|
8810
|
-
const at = scope.projections.get(projection);
|
|
8811
|
-
if (at === void 0) return;
|
|
8812
|
-
scope.views[at] = (scope.views[at] ?? 0) + 1;
|
|
8813
|
-
scope.dirty = true;
|
|
8814
|
-
}
|
|
8815
|
-
/**
|
|
8816
|
-
* Takes back the count of a despawned view, never below 0.
|
|
8817
|
-
*
|
|
8818
|
-
* @param scope - The scope.
|
|
8819
|
-
* @param projection - The projection name.
|
|
8820
|
-
*/
|
|
8821
|
-
function viewLeft(scope, projection) {
|
|
8822
|
-
const at = scope.projections.get(projection);
|
|
8823
|
-
if (at === void 0) return;
|
|
8824
|
-
scope.views[at] = Math.max(0, (scope.views[at] ?? 0) - 1);
|
|
8825
|
-
scope.dirty = true;
|
|
8826
|
-
}
|
|
8827
|
-
/**
|
|
8828
|
-
* Drops every view count: the projections were cleared.
|
|
8829
|
-
*
|
|
8830
|
-
* @param scope - The scope.
|
|
8831
|
-
*/
|
|
8832
|
-
function viewsCleared(scope) {
|
|
8833
|
-
scope.views.fill(0);
|
|
8834
|
-
scope.dirty = true;
|
|
8835
|
-
}
|
|
8836
|
-
//#endregion
|
|
8837
8682
|
//#region src/plugins/world/ecs/systems.ts
|
|
8838
8683
|
/** The one phase that runs in every mode, because `renderer.sync` lives in it. */
|
|
8839
8684
|
const ALWAYS = "sync";
|
|
@@ -8923,18 +8768,29 @@ function frameSnapshot(ctx, time) {
|
|
|
8923
8768
|
return snapshot;
|
|
8924
8769
|
}
|
|
8925
8770
|
/**
|
|
8926
|
-
*
|
|
8927
|
-
*
|
|
8928
|
-
*
|
|
8929
|
-
*
|
|
8771
|
+
* An empty slot for each term of a plan: the stores its check keeps, filled with `push`, so the
|
|
8772
|
+
* list is a packed array of its exact length from the start.
|
|
8773
|
+
*
|
|
8774
|
+
* @param count - The number of terms.
|
|
8775
|
+
* @returns The slots, all `undefined`.
|
|
8776
|
+
*/
|
|
8777
|
+
function storeSlots(count) {
|
|
8778
|
+
const slots = [];
|
|
8779
|
+
for (let index = 0; index < count; index += 1) slots.push(void 0);
|
|
8780
|
+
return slots;
|
|
8781
|
+
}
|
|
8782
|
+
/**
|
|
8783
|
+
* Registers a system at the end of its phase list and returns the remover. Its query and its
|
|
8784
|
+
* `scenes` are read here, once: a later change to the definition is not seen. A dev build checks
|
|
8785
|
+
* the `scenes` against the scenes the features declare, once the world started.
|
|
8930
8786
|
*
|
|
8931
8787
|
* @param ctx - Domain context of the world plugin.
|
|
8932
8788
|
* @param definition - The system.
|
|
8933
|
-
* @param scope - The index of its feature in the scope, or -1 for a system that always runs.
|
|
8934
8789
|
* @returns The remover; calling it twice is a no-op.
|
|
8935
|
-
* @throws {Error} When a system of that name is already registered
|
|
8790
|
+
* @throws {Error} When a system of that name is already registered, and in a dev build for a
|
|
8791
|
+
* name in `scenes` that no feature declares.
|
|
8936
8792
|
*/
|
|
8937
|
-
function registerSystem(ctx, definition
|
|
8793
|
+
function registerSystem(ctx, definition) {
|
|
8938
8794
|
const state = ctx.state.ecs;
|
|
8939
8795
|
for (const phase of [
|
|
8940
8796
|
"input",
|
|
@@ -8942,18 +8798,19 @@ function registerSystem(ctx, definition, scope = -1) {
|
|
|
8942
8798
|
"layout",
|
|
8943
8799
|
"sync"
|
|
8944
8800
|
]) if (state.systems[phase].some((entry) => entry.definition.name === definition.name)) throw new Error(`[game] System "${definition.name}" is registered twice.\n Give the second system another name, or remove the first one.`);
|
|
8801
|
+
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) checkScenes(state.scene, definition);
|
|
8945
8802
|
const plan = planOf(ctx, definition.query);
|
|
8946
8803
|
const entry = {
|
|
8947
8804
|
definition,
|
|
8948
8805
|
frame: ctx.deps.time.snapshot().frame,
|
|
8949
8806
|
plan,
|
|
8950
|
-
|
|
8807
|
+
stores: storeSlots(plan.names.length),
|
|
8808
|
+
scenes: definition.scenes === void 0 ? void 0 : [...definition.scenes],
|
|
8951
8809
|
skipped: false,
|
|
8952
8810
|
walk: plan.names.length === 0 ? void 0 : new SystemWalk(ctx, plan)
|
|
8953
8811
|
};
|
|
8954
8812
|
const list = state.systems[definition.phase];
|
|
8955
8813
|
list.push(entry);
|
|
8956
|
-
if (scope !== -1) state.scope.scoped = true;
|
|
8957
8814
|
if (state.timing !== void 0) giveRow(state, state.timing, definition.name);
|
|
8958
8815
|
return () => {
|
|
8959
8816
|
const at = list.indexOf(entry);
|
|
@@ -9002,19 +8859,19 @@ function phaseContextOf(ctx, world, time) {
|
|
|
9002
8859
|
return known;
|
|
9003
8860
|
}
|
|
9004
8861
|
/**
|
|
9005
|
-
* Tells whether a system
|
|
9006
|
-
*
|
|
9007
|
-
* skipped. No walk, no mark and no clock read happen for a
|
|
9008
|
-
*
|
|
9009
|
-
* reads false without a write of its own.
|
|
8862
|
+
* Tells whether a system runs in this phase: one of its `scenes` is mounted or leaving, and no
|
|
8863
|
+
* term of its query has an empty store. A skipped system remembers it; one that runs is told in
|
|
8864
|
+
* `resumed` whether its last chance was skipped. No walk, no mark and no clock read happen for a
|
|
8865
|
+
* skipped system: the check comes before all three.
|
|
9010
8866
|
*
|
|
9011
|
-
* @param
|
|
9012
|
-
* @param entry - The system
|
|
8867
|
+
* @param state - The ecs branch of the plugin state.
|
|
8868
|
+
* @param entry - The system.
|
|
9013
8869
|
* @param context - What its `run` receives; `resumed` is written here.
|
|
9014
8870
|
* @returns True when the system runs.
|
|
9015
8871
|
*/
|
|
9016
|
-
function wakes(
|
|
9017
|
-
|
|
8872
|
+
function wakes(state, entry, context) {
|
|
8873
|
+
const scenes = entry.scenes;
|
|
8874
|
+
if (scenes !== void 0 && !onScene(state.scene, scenes) || !mayMatch(state.stores, entry)) {
|
|
9018
8875
|
entry.skipped = true;
|
|
9019
8876
|
return false;
|
|
9020
8877
|
}
|
|
@@ -9023,21 +8880,6 @@ function wakes(active, entry, context) {
|
|
|
9023
8880
|
return true;
|
|
9024
8881
|
}
|
|
9025
8882
|
/**
|
|
9026
|
-
* Runs one system of a scoped feature when its feature is active, and leaves `resumed` false for
|
|
9027
|
-
* the systems after it.
|
|
9028
|
-
*
|
|
9029
|
-
* @param ctx - Domain context of the world plugin.
|
|
9030
|
-
* @param active - The active flags of the features.
|
|
9031
|
-
* @param entry - The system.
|
|
9032
|
-
* @param context - What its `run` receives.
|
|
9033
|
-
* @param phase - The phase, for the report of a throw.
|
|
9034
|
-
*/
|
|
9035
|
-
function runScoped(ctx, active, entry, context, phase) {
|
|
9036
|
-
if (!wakes(active, entry, context)) return;
|
|
9037
|
-
guardedRun(ctx, entry, context, phase);
|
|
9038
|
-
context.resumed = false;
|
|
9039
|
-
}
|
|
9040
|
-
/**
|
|
9041
8883
|
* Runs one system, reporting a throw: the try lives here, so the loop of a phase holds none.
|
|
9042
8884
|
*
|
|
9043
8885
|
* @param ctx - Domain context of the world plugin.
|
|
@@ -9057,9 +8899,9 @@ function guardedRun(ctx, entry, context, phase) {
|
|
|
9057
8899
|
}
|
|
9058
8900
|
/**
|
|
9059
8901
|
* Runs the systems of one phase and flushes the commands they queued. A throwing system is
|
|
9060
|
-
* reported and skipped for this frame; the frame goes on.
|
|
9061
|
-
*
|
|
9062
|
-
*
|
|
8902
|
+
* reported and skipped for this frame; the frame goes on. A system whose query has no entity, or
|
|
8903
|
+
* none of whose `scenes` is mounted or leaving, is skipped in its slot. While a scene leaves, the
|
|
8904
|
+
* phase first ends the leave when nothing carries `Exiting` any more.
|
|
9063
8905
|
*
|
|
9064
8906
|
* @param ctx - Domain context of the world plugin.
|
|
9065
8907
|
* @param world - The ecs API a system's `run` receives.
|
|
@@ -9069,16 +8911,14 @@ function guardedRun(ctx, entry, context, phase) {
|
|
|
9069
8911
|
function runPhase(ctx, world, phase, time) {
|
|
9070
8912
|
const state = ctx.state.ecs;
|
|
9071
8913
|
if (phase !== ALWAYS && effectiveMode(ctx) !== "live") return;
|
|
9072
|
-
if (state.
|
|
8914
|
+
if (state.scene.leaving.length > 0) endLeaving(state);
|
|
9073
8915
|
const context = phaseContextOf(ctx, world, time);
|
|
9074
8916
|
const entries = state.systems[phase];
|
|
9075
|
-
const active = state.scope.active;
|
|
9076
8917
|
state.running = phase;
|
|
9077
8918
|
if (state.timing === void 0) for (let index = 0; index < entries.length; index += 1) {
|
|
9078
8919
|
const entry = entries[index];
|
|
9079
8920
|
if (entry === void 0 || entry.frame >= time.frame) continue;
|
|
9080
|
-
if (entry
|
|
9081
|
-
else runScoped(ctx, active, entry, context, phase);
|
|
8921
|
+
if (wakes(state, entry, context)) guardedRun(ctx, entry, context, phase);
|
|
9082
8922
|
}
|
|
9083
8923
|
else runTimed(ctx, state.timing, entries, context, phase, time);
|
|
9084
8924
|
state.running = void 0;
|
|
@@ -9098,26 +8938,24 @@ function runPhase(ctx, world, phase, time) {
|
|
|
9098
8938
|
* @param time - The frame's `Time`.
|
|
9099
8939
|
*/
|
|
9100
8940
|
function runTimed(ctx, timing, entries, context, phase, time) {
|
|
8941
|
+
const state = ctx.state.ecs;
|
|
9101
8942
|
const count = entries.length;
|
|
9102
|
-
const active = ctx.state.ecs.scope.active;
|
|
9103
8943
|
for (let index = 0; index < count; index += 1) {
|
|
9104
8944
|
const entry = entries[index];
|
|
9105
8945
|
if (entry === void 0 || entry.frame >= time.frame) continue;
|
|
9106
|
-
if (
|
|
9107
|
-
const row = rowOf(
|
|
8946
|
+
if (!wakes(state, entry, context)) continue;
|
|
8947
|
+
const row = rowOf(state, entry.definition.name);
|
|
9108
8948
|
const started = timing.clock();
|
|
9109
8949
|
guardedRun(ctx, entry, context, phase);
|
|
9110
8950
|
timing.ms[row] = (timing.ms[row] ?? 0) + timing.clock() - started;
|
|
9111
|
-
context.resumed = false;
|
|
9112
8951
|
}
|
|
9113
8952
|
}
|
|
9114
8953
|
//#endregion
|
|
9115
8954
|
//#region src/plugins/world/ecs/api.ts
|
|
9116
8955
|
/**
|
|
9117
|
-
* Empties the world in place: entities, components, resources, systems, hooks
|
|
9118
|
-
*
|
|
9119
|
-
*
|
|
9120
|
-
* every view on it stays valid.
|
|
8956
|
+
* Empties the world in place: entities, components, resources, systems, hooks and the frame
|
|
8957
|
+
* history. The mounted scene stays: `scenes` still shows it. The state object keeps its identity,
|
|
8958
|
+
* so every view on it stays valid.
|
|
9121
8959
|
*
|
|
9122
8960
|
* @param ctx - Domain context of the world plugin.
|
|
9123
8961
|
*/
|
|
@@ -9148,7 +8986,6 @@ function clearWorldState(ctx) {
|
|
|
9148
8986
|
state.mode = "live";
|
|
9149
8987
|
state.frameSnapshot = void 0;
|
|
9150
8988
|
resetHistory(state);
|
|
9151
|
-
viewsCleared(state.scope);
|
|
9152
8989
|
}
|
|
9153
8990
|
/**
|
|
9154
8991
|
* Creates the ecs module: the public API a game and the plugins above call, plus the four methods
|
|
@@ -9161,8 +8998,6 @@ function createEcsApi(ctx) {
|
|
|
9161
8998
|
const state = ctx.state.ecs;
|
|
9162
8999
|
const api = {
|
|
9163
9000
|
system: (definition) => registerSystem(ctx, definition),
|
|
9164
|
-
hold: (feature) => holdFeature(ctx, feature),
|
|
9165
|
-
isActive: (feature) => isFeatureActive(ctx, feature),
|
|
9166
9001
|
spawn: (owner, components) => {
|
|
9167
9002
|
const entity = reserveEntity(state, owner);
|
|
9168
9003
|
queueOrApply(ctx, {
|
|
@@ -9242,6 +9077,9 @@ function createEcsApi(ctx) {
|
|
|
9242
9077
|
setMode: (mode) => {
|
|
9243
9078
|
state.mode = mode;
|
|
9244
9079
|
},
|
|
9080
|
+
setScene: (scene) => {
|
|
9081
|
+
mountScene(state.scene, scene);
|
|
9082
|
+
},
|
|
9245
9083
|
typeOf: (name) => asComponent$2(state.types.get(name)),
|
|
9246
9084
|
snapshot: () => snapshotWorld(state, effectiveMode(ctx)),
|
|
9247
9085
|
diff: (from, to) => diffHistory(state.history, from, to),
|
|
@@ -9266,15 +9104,6 @@ function createEcsApi(ctx) {
|
|
|
9266
9104
|
},
|
|
9267
9105
|
clear: () => {
|
|
9268
9106
|
clearWorldState(ctx);
|
|
9269
|
-
},
|
|
9270
|
-
viewCame: (projection) => {
|
|
9271
|
-
viewCame(state.scope, projection);
|
|
9272
|
-
},
|
|
9273
|
-
viewLeft: (projection) => {
|
|
9274
|
-
viewLeft(state.scope, projection);
|
|
9275
|
-
},
|
|
9276
|
-
viewsCleared: () => {
|
|
9277
|
-
viewsCleared(state.scope);
|
|
9278
9107
|
}
|
|
9279
9108
|
};
|
|
9280
9109
|
return api;
|
|
@@ -9388,29 +9217,24 @@ function registerEach(ctx, feature, key, accepts, register) {
|
|
|
9388
9217
|
}
|
|
9389
9218
|
/**
|
|
9390
9219
|
* Registers what every feature brought, in feature order: components first, so their schema is
|
|
9391
|
-
* known, then systems, then projections.
|
|
9392
|
-
*
|
|
9393
|
-
* for it (rule V). A dev build warns about a feature nothing can make active.
|
|
9220
|
+
* known, then systems, then projections. The scene ids of every feature are read first, so a dev
|
|
9221
|
+
* build checks the `scenes` of each system as it registers.
|
|
9394
9222
|
*
|
|
9395
9223
|
* @param ctx - Domain context of the world plugin.
|
|
9224
|
+
* @param ecs - The ecs module.
|
|
9396
9225
|
* @param projection - The projection module.
|
|
9397
9226
|
*/
|
|
9398
|
-
function registerFeatures(ctx, projection) {
|
|
9399
|
-
const
|
|
9400
|
-
|
|
9401
|
-
|
|
9227
|
+
function registerFeatures(ctx, ecs, projection) {
|
|
9228
|
+
const features = ctx.deps.flow.features.all();
|
|
9229
|
+
declareScenes(ctx.state.ecs.scene, features);
|
|
9230
|
+
for (const feature of features) {
|
|
9402
9231
|
registerEach(ctx, feature, "components", isComponentEntry, (entry) => registerType(ctx, entry));
|
|
9403
|
-
registerEach(ctx, feature, "systems", isSystemEntry, (entry) =>
|
|
9404
|
-
registerEach(ctx, feature, "projections", isProjectionEntry, (entry) =>
|
|
9405
|
-
projection.register(entry);
|
|
9406
|
-
projections.set(entry.name, at);
|
|
9407
|
-
});
|
|
9408
|
-
if (typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__) reportNeverActive(ctx, feature);
|
|
9232
|
+
registerEach(ctx, feature, "systems", isSystemEntry, (entry) => ecs.system(entry));
|
|
9233
|
+
registerEach(ctx, feature, "projections", isProjectionEntry, (entry) => projection.register(entry));
|
|
9409
9234
|
}
|
|
9410
9235
|
}
|
|
9411
9236
|
/**
|
|
9412
|
-
* Connects the world in `onStart`: it
|
|
9413
|
-
* description, registers the five frame
|
|
9237
|
+
* Connects the world in `onStart`: it reads every feature description, registers the five frame
|
|
9414
9238
|
* callbacks of the phases and listens to the released hints. In a dev build the `signals`
|
|
9415
9239
|
* callback records the frame history before it clears the change sets; in an agent build the
|
|
9416
9240
|
* timing table starts with the page clock of `time` and the `input` callback zeroes it each
|
|
@@ -9431,8 +9255,7 @@ function connectWorld(ctx) {
|
|
|
9431
9255
|
const clock = time.agentClock();
|
|
9432
9256
|
if (clock !== void 0) startTiming(worldCtx.state.ecs, clock);
|
|
9433
9257
|
}
|
|
9434
|
-
|
|
9435
|
-
registerFeatures(worldCtx, projection);
|
|
9258
|
+
registerFeatures(worldCtx, ecs, projection);
|
|
9436
9259
|
ecs.resource(Now).now = clock.now();
|
|
9437
9260
|
worldCtx.state.ecs.offFrame.push(ecs.onOwnerLeft((owner) => projection.ownerLeft(owner)), time.onFrame("input", (frame) => {
|
|
9438
9261
|
ecs.resource(Now).now = clock.now();
|
|
@@ -9450,8 +9273,8 @@ function connectWorld(ctx) {
|
|
|
9450
9273
|
}
|
|
9451
9274
|
/**
|
|
9452
9275
|
* Empties the world in `onStop`: the frame callbacks and the hint listener are removed, then
|
|
9453
|
-
* every track, view, queued view, entity and resource is dropped, the
|
|
9454
|
-
* are reset.
|
|
9276
|
+
* every track, view, queued view, entity and resource is dropped, and the scenes and the frame
|
|
9277
|
+
* history are reset.
|
|
9455
9278
|
* No `onRemoved` fires, because `renderer` stopped earlier and destroyed its own objects.
|
|
9456
9279
|
*
|
|
9457
9280
|
* @param state - The plugin state, the only thing a teardown context carries.
|
|
@@ -9502,7 +9325,9 @@ function clearWorld(state) {
|
|
|
9502
9325
|
state.ecs.mode = "live";
|
|
9503
9326
|
state.ecs.timing = void 0;
|
|
9504
9327
|
state.ecs.timingSlots.clear();
|
|
9505
|
-
|
|
9328
|
+
state.ecs.scene.mounted = void 0;
|
|
9329
|
+
state.ecs.scene.leaving.length = 0;
|
|
9330
|
+
state.ecs.scene.declared = void 0;
|
|
9506
9331
|
resetHistory(state.ecs);
|
|
9507
9332
|
}
|
|
9508
9333
|
//#endregion
|
|
@@ -9894,6 +9719,44 @@ function collectView(pctx, spec, item) {
|
|
|
9894
9719
|
return values;
|
|
9895
9720
|
}
|
|
9896
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
|
+
/**
|
|
9897
9760
|
* Writes the layer of a view.
|
|
9898
9761
|
*
|
|
9899
9762
|
* @param pctx - Domain context of the projection module.
|
|
@@ -10415,7 +10278,7 @@ function despawnView(pctx, mounted, view) {
|
|
|
10415
10278
|
view.handles = [];
|
|
10416
10279
|
cancelTracks(pctx, view.entity);
|
|
10417
10280
|
const state = pctx.ctx.state.projection;
|
|
10418
|
-
|
|
10281
|
+
state.byEntity.delete(view.entity);
|
|
10419
10282
|
state.mutes.delete(view.entity);
|
|
10420
10283
|
state.rests.delete(view.entity);
|
|
10421
10284
|
state.leaving.set(view.entity, view.cause);
|
|
@@ -10791,7 +10654,6 @@ function enterEntry(pctx, spec, mounted, entry) {
|
|
|
10791
10654
|
const view = createViewRecord(entity, spec.name, entry.key, entry.item, values, entry.direct ? "mount" : "change");
|
|
10792
10655
|
mounted.live.set(entry.key, view);
|
|
10793
10656
|
pctx.ctx.state.projection.byEntity.set(entity, view);
|
|
10794
|
-
pctx.deps.views.came(spec.name);
|
|
10795
10657
|
pctx.ctx.state.projection.membershipVersion += 1;
|
|
10796
10658
|
const hook = spec.motion?.enter;
|
|
10797
10659
|
const loop = spec.motion?.loop;
|
|
@@ -11213,6 +11075,155 @@ function reconcile(pctx, options) {
|
|
|
11213
11075
|
});
|
|
11214
11076
|
}
|
|
11215
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
|
|
11216
11227
|
//#region src/plugins/world/projection/api.ts
|
|
11217
11228
|
/**
|
|
11218
11229
|
* Tells whether a cause sets the picture instead of playing it.
|
|
@@ -11245,25 +11256,8 @@ function isConvergeCause(cause) {
|
|
|
11245
11256
|
return cause === "load" || cause === "restore";
|
|
11246
11257
|
}
|
|
11247
11258
|
/**
|
|
11248
|
-
* Creates the empty
|
|
11249
|
-
*
|
|
11250
|
-
* declaration.
|
|
11251
|
-
*
|
|
11252
|
-
* @param owner - Who owns the entities of this projection.
|
|
11253
|
-
* @param order - The `Order` its screen roots draw at.
|
|
11254
|
-
* @returns A mounted projection with nothing in it yet.
|
|
11255
|
-
*/
|
|
11256
|
-
function emptyMounted(owner, order) {
|
|
11257
|
-
return {
|
|
11258
|
-
owner,
|
|
11259
|
-
live: /* @__PURE__ */ new Map(),
|
|
11260
|
-
queue: [],
|
|
11261
|
-
items: /* @__PURE__ */ new Map(),
|
|
11262
|
-
order
|
|
11263
|
-
};
|
|
11264
|
-
}
|
|
11265
|
-
/**
|
|
11266
|
-
* 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.
|
|
11267
11261
|
*
|
|
11268
11262
|
* @returns An empty table of component name to owned fields.
|
|
11269
11263
|
*/
|
|
@@ -11349,20 +11343,6 @@ function noPeers() {
|
|
|
11349
11343
|
};
|
|
11350
11344
|
}
|
|
11351
11345
|
/**
|
|
11352
|
-
* Checks that every layer a projection names is declared by the scene.
|
|
11353
|
-
*
|
|
11354
|
-
* @param pctx - Domain context of the projection module.
|
|
11355
|
-
* @param spec - The projection spec.
|
|
11356
|
-
* @throws {Error} When `layer` or `lift` is not in the layer list.
|
|
11357
|
-
*/
|
|
11358
|
-
function checkLayers(pctx, spec) {
|
|
11359
|
-
const declared = new Set(pctx.ctx.state.projection.layers.map((layer) => layer.name));
|
|
11360
|
-
for (const name of [spec.layer, spec.lift]) {
|
|
11361
|
-
if (name === void 0 || declared.has(name)) continue;
|
|
11362
|
-
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.`);
|
|
11363
|
-
}
|
|
11364
|
-
}
|
|
11365
|
-
/**
|
|
11366
11346
|
* Creates the projection module: the public API `scenes`, `input` and `i18n` call, plus the
|
|
11367
11347
|
* methods the plugin root drives from the frame and the `model:committed` hook.
|
|
11368
11348
|
*
|
|
@@ -11427,33 +11407,10 @@ function createProjectionApi(ctx, deps) {
|
|
|
11427
11407
|
},
|
|
11428
11408
|
layers: () => state.layers,
|
|
11429
11409
|
mount: (names, owner) => {
|
|
11430
|
-
|
|
11431
|
-
for (const name of names) {
|
|
11432
|
-
const spec = state.specs.get(name);
|
|
11433
|
-
if (spec === void 0) throw new Error(`[game] Projection "${name}" is not registered.\n Register it before a scene mounts it.`);
|
|
11434
|
-
if (state.mounted.has(name)) {
|
|
11435
|
-
ctx.log.warn("world:already-mounted", { projection: name });
|
|
11436
|
-
continue;
|
|
11437
|
-
}
|
|
11438
|
-
checkLayers(pctx, spec);
|
|
11439
|
-
fresh.push(spec);
|
|
11440
|
-
}
|
|
11441
|
-
for (const spec of fresh) state.mounted.set(spec.name, emptyMounted(owner, names.indexOf(spec.name) - names.length));
|
|
11442
|
-
if (fresh.length > 0) reconcile(pctx, {
|
|
11443
|
-
names: fresh.map((spec) => spec.name),
|
|
11444
|
-
direct: true,
|
|
11445
|
-
force: true,
|
|
11446
|
-
converge: false,
|
|
11447
|
-
settle: false
|
|
11448
|
-
});
|
|
11410
|
+
mountNames(pctx, names, owner);
|
|
11449
11411
|
},
|
|
11450
11412
|
unmount: (names) => {
|
|
11451
|
-
|
|
11452
|
-
const mounted = state.mounted.get(name);
|
|
11453
|
-
if (mounted === void 0) continue;
|
|
11454
|
-
clearMounted(pctx, mounted);
|
|
11455
|
-
state.mounted.delete(name);
|
|
11456
|
-
}
|
|
11413
|
+
unmountNames(pctx, names);
|
|
11457
11414
|
},
|
|
11458
11415
|
restOf: (entity, component) => {
|
|
11459
11416
|
const stored = (state.byEntity.get(entity)?.rest ?? state.rests.get(entity))?.get(component.componentName);
|
|
@@ -11604,7 +11561,6 @@ function createProjectionApi(ctx, deps) {
|
|
|
11604
11561
|
state.mounted.clear();
|
|
11605
11562
|
state.specs.clear();
|
|
11606
11563
|
state.byEntity.clear();
|
|
11607
|
-
pctx.deps.views.cleared();
|
|
11608
11564
|
state.mutes.clear();
|
|
11609
11565
|
state.rests.clear();
|
|
11610
11566
|
state.keys.clear();
|
|
@@ -11644,12 +11600,7 @@ function createModules$2(ctx) {
|
|
|
11644
11600
|
ecs,
|
|
11645
11601
|
projection: createProjectionApi(ctx, {
|
|
11646
11602
|
ecs,
|
|
11647
|
-
components: WORLD_COMPONENTS
|
|
11648
|
-
views: {
|
|
11649
|
-
came: ecs.viewCame,
|
|
11650
|
-
left: ecs.viewLeft,
|
|
11651
|
-
cleared: ecs.viewsCleared
|
|
11652
|
-
}
|
|
11603
|
+
components: WORLD_COMPONENTS
|
|
11653
11604
|
})
|
|
11654
11605
|
};
|
|
11655
11606
|
}
|
|
@@ -11684,8 +11635,6 @@ function withKeys(diff, ecs, projection) {
|
|
|
11684
11635
|
function exposeEcs(ecs, projection) {
|
|
11685
11636
|
return {
|
|
11686
11637
|
system: ecs.system,
|
|
11687
|
-
hold: ecs.hold,
|
|
11688
|
-
isActive: ecs.isActive,
|
|
11689
11638
|
spawn: ecs.spawn,
|
|
11690
11639
|
despawn: ecs.despawn,
|
|
11691
11640
|
despawnOwnedBy: ecs.despawnOwnedBy,
|
|
@@ -11709,6 +11658,7 @@ function exposeEcs(ecs, projection) {
|
|
|
11709
11658
|
ecs.setMode(mode);
|
|
11710
11659
|
if (ecs.mode() === "fast") projection.flushAll();
|
|
11711
11660
|
},
|
|
11661
|
+
setScene: ecs.setScene,
|
|
11712
11662
|
snapshot: ecs.snapshot,
|
|
11713
11663
|
diff: (from, to) => withKeys(ecs.diff(from, to), ecs, projection),
|
|
11714
11664
|
schema: ecs.schema
|
|
@@ -11807,26 +11757,16 @@ function emptyMap$10() {
|
|
|
11807
11757
|
return /* @__PURE__ */ new Map();
|
|
11808
11758
|
}
|
|
11809
11759
|
/**
|
|
11810
|
-
* Creates the
|
|
11811
|
-
*
|
|
11760
|
+
* Creates the scene state of a world no scene was mounted in: nothing mounted, nothing leaving, the
|
|
11761
|
+
* declared scenes unknown until start.
|
|
11812
11762
|
*
|
|
11813
|
-
* @returns An empty
|
|
11763
|
+
* @returns An empty scene state.
|
|
11814
11764
|
*/
|
|
11815
|
-
function
|
|
11765
|
+
function emptyScene() {
|
|
11816
11766
|
return {
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
owners: void 0,
|
|
11821
|
-
ownersFailed: false,
|
|
11822
|
-
flow: void 0,
|
|
11823
|
-
onStack: /* @__PURE__ */ new Uint8Array(0),
|
|
11824
|
-
holds: /* @__PURE__ */ new Int32Array(0),
|
|
11825
|
-
views: /* @__PURE__ */ new Int32Array(0),
|
|
11826
|
-
active: /* @__PURE__ */ new Uint8Array(0),
|
|
11827
|
-
dirty: false,
|
|
11828
|
-
scoped: false,
|
|
11829
|
-
generation: 0
|
|
11767
|
+
mounted: void 0,
|
|
11768
|
+
leaving: [],
|
|
11769
|
+
declared: void 0
|
|
11830
11770
|
};
|
|
11831
11771
|
}
|
|
11832
11772
|
/**
|
|
@@ -11872,7 +11812,7 @@ function createEcsState() {
|
|
|
11872
11812
|
queryPlans: emptyMap$10(),
|
|
11873
11813
|
walkLists: [],
|
|
11874
11814
|
despawns: 0,
|
|
11875
|
-
|
|
11815
|
+
scene: emptyScene()
|
|
11876
11816
|
};
|
|
11877
11817
|
}
|
|
11878
11818
|
//#endregion
|
|
@@ -13012,336 +12952,486 @@ function watchPixelRatio(ctx) {
|
|
|
13012
12952
|
state.cleanups.push(() => query?.removeEventListener("change", follow));
|
|
13013
12953
|
}
|
|
13014
12954
|
//#endregion
|
|
13015
|
-
//#region src/plugins/renderer/host/sdf-
|
|
12955
|
+
//#region src/plugins/renderer/host/sdf-batcher.ts
|
|
12956
|
+
/** The key of the glyph record on the proxy graphics of every bitmap text the renderer's pipe lays out. */
|
|
12957
|
+
const SDF = Symbol("moku-sdf-glyphs");
|
|
12958
|
+
/** The name the renderer's batcher of distance-field glyphs registers under, and the element's `batcherName`. */
|
|
12959
|
+
const SDF_BATCHER = "moku-sdf";
|
|
12960
|
+
/** The name of Pixi's default batcher, which an element goes back to. */
|
|
12961
|
+
const DEFAULT_BATCHER = "default";
|
|
12962
|
+
/** Floats of one vertex: position 2, uv 2, colour 1 (unorm8x4), texture id and rounding 1 (uint16x2), distance 1. */
|
|
12963
|
+
const VERTEX_SIZE = 7;
|
|
12964
|
+
/** Bytes of one vertex. */
|
|
12965
|
+
const STRIDE = 28;
|
|
12966
|
+
/** The name of the renderer's batched SDF program, on both backends. */
|
|
12967
|
+
const PROGRAM_NAME = "moku-sdf-batch";
|
|
12968
|
+
/**
|
|
12969
|
+
* The renderer's piece of the high shader: the glyph's edge width from the vertex, at the scale of the render group,
|
|
12970
|
+
* and the coverage with the alpha applied once. The template's `outColor * vColor` applies the colour and every alpha.
|
|
12971
|
+
*/
|
|
12972
|
+
const SDF_BIT = {
|
|
12973
|
+
name: "moku-sdf-batch-bit",
|
|
12974
|
+
vertex: {
|
|
12975
|
+
header: `
|
|
12976
|
+
@in aSdfDistance: f32;
|
|
12977
|
+
@out @interpolate(flat) vSdfDistance: f32;
|
|
12978
|
+
`,
|
|
12979
|
+
main: `
|
|
12980
|
+
let sdfWorld = globalUniforms.uWorldTransformMatrix;
|
|
12981
|
+
vSdfDistance = aSdfDistance * (length(sdfWorld[0].xy) + length(sdfWorld[1].xy)) * 0.5;
|
|
12982
|
+
`
|
|
12983
|
+
},
|
|
12984
|
+
fragment: {
|
|
12985
|
+
header: `
|
|
12986
|
+
@in @interpolate(flat) vSdfDistance: f32;
|
|
12987
|
+
`,
|
|
12988
|
+
main: `
|
|
12989
|
+
outColor = vec4<f32>(calculateMSDFAlpha(outColor, vec4<f32>(vColor.rgb / max(vColor.a, 1e-4), 1.0), vSdfDistance));
|
|
12990
|
+
`
|
|
12991
|
+
}
|
|
12992
|
+
};
|
|
13016
12993
|
/**
|
|
13017
|
-
* The
|
|
13018
|
-
*
|
|
13019
|
-
*
|
|
13020
|
-
*/
|
|
13021
|
-
const
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
12994
|
+
* The same piece in GLSL, for the WebGL program. Pixi compiles its batch programs as GLSL ES 1.00 (its `in` and `out`
|
|
12995
|
+
* are macros), where `flat` is a reserved word: the distance is a plain varying, the same at the three vertices of a
|
|
12996
|
+
* glyph's triangle, as Pixi's own texture id is.
|
|
12997
|
+
*/
|
|
12998
|
+
const SDF_BIT_GL = {
|
|
12999
|
+
name: "moku-sdf-batch-bit",
|
|
13000
|
+
vertex: {
|
|
13001
|
+
header: `
|
|
13002
|
+
in float aSdfDistance;
|
|
13003
|
+
out float vSdfDistance;
|
|
13004
|
+
`,
|
|
13005
|
+
main: `
|
|
13006
|
+
vSdfDistance = aSdfDistance * (length(uWorldTransformMatrix[0].xy) + length(uWorldTransformMatrix[1].xy)) * 0.5;
|
|
13007
|
+
`
|
|
13008
|
+
},
|
|
13009
|
+
fragment: {
|
|
13010
|
+
header: `
|
|
13011
|
+
in float vSdfDistance;
|
|
13012
|
+
`,
|
|
13013
|
+
main: `
|
|
13014
|
+
outColor = vec4(calculateMSDFAlpha(outColor, vec4(vColor.rgb / max(vColor.a, 1e-4), 1.0), vSdfDistance));
|
|
13015
|
+
`
|
|
13016
|
+
}
|
|
13017
|
+
};
|
|
13032
13018
|
/**
|
|
13033
|
-
* The
|
|
13019
|
+
* The edge width one vertex of an element carries: Pixi's distance at the element's scale in its render group, times
|
|
13020
|
+
* the resolution. A text's group transform is what Pixi scales by; the render group's own scale is the shader's.
|
|
13034
13021
|
*
|
|
13035
|
-
* @param
|
|
13036
|
-
* @
|
|
13037
|
-
* @
|
|
13038
|
-
*
|
|
13039
|
-
*
|
|
13040
|
-
*
|
|
13022
|
+
* @param element - The element.
|
|
13023
|
+
* @param a - `a` of its group transform.
|
|
13024
|
+
* @param b - `b` of its group transform.
|
|
13025
|
+
* @param c - `c` of its group transform.
|
|
13026
|
+
* @param d - `d` of its group transform.
|
|
13027
|
+
* @param density - Device pixels per CSS pixel.
|
|
13028
|
+
* @returns The distance, device px at the render group's scale 1; 0 for an element without glyphs.
|
|
13041
13029
|
*/
|
|
13042
|
-
function
|
|
13043
|
-
const
|
|
13044
|
-
|
|
13030
|
+
function distanceOf(element, a, b, c, d, density) {
|
|
13031
|
+
const glyphs = element.renderable?.[SDF];
|
|
13032
|
+
if (glyphs === void 0) return 0;
|
|
13033
|
+
return glyphs.perScale * (Math.sqrt(a * a + b * b) + Math.sqrt(c * c + d * d)) * density / 2;
|
|
13045
13034
|
}
|
|
13046
13035
|
/**
|
|
13047
|
-
*
|
|
13048
|
-
*
|
|
13049
|
-
*/
|
|
13050
|
-
const DISTANCE = Symbol("sdf-text-distance");
|
|
13051
|
-
/**
|
|
13052
|
-
* Pixi's read of `uDistance`, at every upload: the distance per unit of scale at the text's world scale, or Pixi's last
|
|
13053
|
-
* write without one, times the renderer's resolution.
|
|
13036
|
+
* Keeps one shader per batch texture limit: made the first time a limit is asked for, shared by every batcher after.
|
|
13037
|
+
* Its own function, because lint rule L5 refuses a collection built inside an exported declaration.
|
|
13054
13038
|
*
|
|
13055
|
-
* @param
|
|
13056
|
-
* @returns The
|
|
13039
|
+
* @param make - Makes the shader of one texture limit.
|
|
13040
|
+
* @returns The shader for a texture limit.
|
|
13057
13041
|
*/
|
|
13058
|
-
function
|
|
13059
|
-
const
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
}
|
|
13069
|
-
/**
|
|
13070
|
-
* Pixi's write of `uDistance`, at a layout: the CSS distance and, while the pipe names the text, the distance per unit
|
|
13071
|
-
* of its group scale.
|
|
13072
|
-
*
|
|
13073
|
-
* @param this - The text's uniforms.
|
|
13074
|
-
* @param value - Pixi's distance, CSS px.
|
|
13075
|
-
*/
|
|
13076
|
-
function writeDistance(value) {
|
|
13077
|
-
const state = this[DISTANCE];
|
|
13078
|
-
const text = state.writing.text;
|
|
13079
|
-
const scale = text === void 0 ? 0 : scaleOf$1(text.groupTransform);
|
|
13080
|
-
state.cell[0] = value;
|
|
13081
|
-
state.cell[1] = scale > 0 ? value / scale : 0;
|
|
13082
|
-
state.text = text;
|
|
13083
|
-
}
|
|
13084
|
-
/**
|
|
13085
|
-
* The `uDistance` of every text's uniforms: one pair of functions, so every uniforms object has the same hidden class
|
|
13086
|
-
* and Pixi's generated sync function reads it monomorphic. A pair made per text, or an accessor laid over Pixi's data
|
|
13087
|
-
* property, sends each object to dictionary mode. The getter hands a still text's answer back boxed once (`answer`):
|
|
13088
|
-
* Pixi's sync may not inline it.
|
|
13089
|
-
*/
|
|
13090
|
-
const DISTANCE_ACCESSOR = {
|
|
13091
|
-
enumerable: true,
|
|
13092
|
-
configurable: true,
|
|
13093
|
-
get: readDistance,
|
|
13094
|
-
set: writeDistance
|
|
13095
|
-
};
|
|
13042
|
+
function cachedBy(make) {
|
|
13043
|
+
const shaders = /* @__PURE__ */ new Map();
|
|
13044
|
+
return (maxTextures) => {
|
|
13045
|
+
const known = shaders.get(maxTextures);
|
|
13046
|
+
if (known !== void 0) return known;
|
|
13047
|
+
const shader = make(maxTextures);
|
|
13048
|
+
shaders.set(maxTextures, shader);
|
|
13049
|
+
return shader;
|
|
13050
|
+
};
|
|
13051
|
+
}
|
|
13096
13052
|
/**
|
|
13097
|
-
* The
|
|
13098
|
-
*
|
|
13053
|
+
* The shaders of the batcher: Pixi's five SDF bits on both backends with the local uniforms replaced by the
|
|
13054
|
+
* renderer's piece, and Pixi's batch sampler slots for WebGL, as Pixi's own `DefaultShader` carries them.
|
|
13099
13055
|
*
|
|
13100
|
-
*
|
|
13101
|
-
*
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
|
|
13115
|
-
|
|
13116
|
-
|
|
13117
|
-
|
|
13056
|
+
* @param pixi - The loaded Pixi module.
|
|
13057
|
+
* @returns The shader for a texture limit.
|
|
13058
|
+
*/
|
|
13059
|
+
function sdfShaders(pixi) {
|
|
13060
|
+
return cachedBy((maxTextures) => new pixi.Shader({
|
|
13061
|
+
gpuProgram: pixi.compileHighShaderGpuProgram({
|
|
13062
|
+
name: PROGRAM_NAME,
|
|
13063
|
+
bits: [
|
|
13064
|
+
pixi.colorBit,
|
|
13065
|
+
pixi.generateTextureBatchBit(maxTextures),
|
|
13066
|
+
SDF_BIT,
|
|
13067
|
+
pixi.mSDFBit,
|
|
13068
|
+
pixi.roundPixelsBit
|
|
13069
|
+
]
|
|
13070
|
+
}),
|
|
13071
|
+
glProgram: pixi.compileHighShaderGlProgram({
|
|
13072
|
+
name: PROGRAM_NAME,
|
|
13073
|
+
bits: [
|
|
13074
|
+
pixi.colorBitGl,
|
|
13075
|
+
pixi.generateTextureBatchBitGl(maxTextures),
|
|
13076
|
+
SDF_BIT_GL,
|
|
13077
|
+
pixi.mSDFBitGl,
|
|
13078
|
+
pixi.roundPixelsBitGl
|
|
13079
|
+
]
|
|
13080
|
+
}),
|
|
13081
|
+
resources: { batchSamplers: pixi.getBatchSamplersUniformGroup(maxTextures) }
|
|
13082
|
+
}));
|
|
13083
|
+
}
|
|
13084
|
+
/**
|
|
13085
|
+
* The geometry of one batcher: Pixi's batch geometry with the distance after the texture id, one buffer of 7 floats a
|
|
13086
|
+
* vertex.
|
|
13118
13087
|
*
|
|
13119
13088
|
* @param pixi - The loaded Pixi module.
|
|
13120
|
-
* @
|
|
13121
|
-
|
|
13122
|
-
|
|
13123
|
-
|
|
13124
|
-
*/
|
|
13125
|
-
|
|
13126
|
-
|
|
13127
|
-
|
|
13128
|
-
|
|
13129
|
-
|
|
13130
|
-
|
|
13131
|
-
|
|
13132
|
-
|
|
13133
|
-
|
|
13134
|
-
|
|
13135
|
-
|
|
13136
|
-
|
|
13137
|
-
|
|
13138
|
-
|
|
13139
|
-
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13089
|
+
* @returns A new geometry.
|
|
13090
|
+
*/
|
|
13091
|
+
function sdfGeometry(pixi) {
|
|
13092
|
+
const buffer = new pixi.Buffer({
|
|
13093
|
+
data: /* @__PURE__ */ new Float32Array(1),
|
|
13094
|
+
label: "attribute-sdf-batch-buffer",
|
|
13095
|
+
usage: pixi.BufferUsage.VERTEX | pixi.BufferUsage.COPY_DST,
|
|
13096
|
+
shrinkToFit: false
|
|
13097
|
+
});
|
|
13098
|
+
const indexBuffer = new pixi.Buffer({
|
|
13099
|
+
data: /* @__PURE__ */ new Uint32Array(1),
|
|
13100
|
+
label: "index-sdf-batch-buffer",
|
|
13101
|
+
usage: pixi.BufferUsage.INDEX | pixi.BufferUsage.COPY_DST,
|
|
13102
|
+
shrinkToFit: false
|
|
13103
|
+
});
|
|
13104
|
+
return new pixi.Geometry({
|
|
13105
|
+
attributes: {
|
|
13106
|
+
aPosition: {
|
|
13107
|
+
buffer,
|
|
13108
|
+
format: "float32x2",
|
|
13109
|
+
stride: STRIDE,
|
|
13110
|
+
offset: 0
|
|
13111
|
+
},
|
|
13112
|
+
aUV: {
|
|
13113
|
+
buffer,
|
|
13114
|
+
format: "float32x2",
|
|
13115
|
+
stride: STRIDE,
|
|
13116
|
+
offset: 8
|
|
13117
|
+
},
|
|
13118
|
+
aColor: {
|
|
13119
|
+
buffer,
|
|
13120
|
+
format: "unorm8x4",
|
|
13121
|
+
stride: STRIDE,
|
|
13122
|
+
offset: 16
|
|
13123
|
+
},
|
|
13124
|
+
aTextureIdAndRound: {
|
|
13125
|
+
buffer,
|
|
13126
|
+
format: "uint16x2",
|
|
13127
|
+
stride: STRIDE,
|
|
13128
|
+
offset: 20
|
|
13129
|
+
},
|
|
13130
|
+
aSdfDistance: {
|
|
13131
|
+
buffer,
|
|
13132
|
+
format: "float32",
|
|
13133
|
+
stride: STRIDE,
|
|
13134
|
+
offset: 24
|
|
13135
|
+
}
|
|
13143
13136
|
},
|
|
13144
|
-
|
|
13145
|
-
value: 0,
|
|
13146
|
-
type: "f32"
|
|
13147
|
-
}
|
|
13137
|
+
indexBuffer
|
|
13148
13138
|
});
|
|
13149
|
-
const values = group.uniforms;
|
|
13150
|
-
const state = {
|
|
13151
|
-
cell: Float64Array.of(4, 0, NaN),
|
|
13152
|
-
text: void 0,
|
|
13153
|
-
writing,
|
|
13154
|
-
density,
|
|
13155
|
-
answer: void 0
|
|
13156
|
-
};
|
|
13157
|
-
const uniforms = {
|
|
13158
|
-
uColor: values.uColor,
|
|
13159
|
-
uTransformMatrix: values.uTransformMatrix,
|
|
13160
|
-
uRound: values.uRound
|
|
13161
|
-
};
|
|
13162
|
-
Object.defineProperty(uniforms, "uDistance", DISTANCE_ACCESSOR);
|
|
13163
|
-
Object.defineProperty(uniforms, DISTANCE, { value: state });
|
|
13164
|
-
group.uniforms = uniforms;
|
|
13165
|
-
return group;
|
|
13166
13139
|
}
|
|
13167
13140
|
/**
|
|
13168
|
-
*
|
|
13141
|
+
* Builds the batcher of distance-field glyphs from the module object, at call time, because Pixi's `Batcher` arrives
|
|
13142
|
+
* with the lazily loaded module. Its packing is Pixi's `DefaultBatcher`'s loop with the distance written after the
|
|
13143
|
+
* texture id; the distances are packed at the resolution the batcher pipe last drew it at (`packAt`).
|
|
13169
13144
|
*
|
|
13170
|
-
* @param
|
|
13171
|
-
* @
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13145
|
+
* @param pixi - The loaded Pixi module.
|
|
13146
|
+
* @returns The batcher class, with the extension Pixi's registry files it under.
|
|
13147
|
+
*/
|
|
13148
|
+
function createSdfBatcher(pixi) {
|
|
13149
|
+
const shaderFor = sdfShaders(pixi);
|
|
13150
|
+
/** Pixi's batcher, with a vertex that carries the edge width of its glyph. */
|
|
13151
|
+
class SdfBatcher extends pixi.Batcher {
|
|
13152
|
+
static extension = {
|
|
13153
|
+
type: [pixi.ExtensionType.Batcher],
|
|
13154
|
+
name: SDF_BATCHER
|
|
13155
|
+
};
|
|
13156
|
+
name = SDF_BATCHER;
|
|
13157
|
+
geometry = sdfGeometry(pixi);
|
|
13158
|
+
shader = shaderFor(this.maxTextures);
|
|
13159
|
+
/** Device pixels per CSS pixel the distances are packed at; a double from the start, so writes box nothing. */
|
|
13160
|
+
density = NaN;
|
|
13161
|
+
vertexSize = VERTEX_SIZE;
|
|
13162
|
+
/**
|
|
13163
|
+
* Packs the vertices of a mesh element: Pixi's default vertex and the distance.
|
|
13164
|
+
*
|
|
13165
|
+
* @param element - The element.
|
|
13166
|
+
* @param float32View - The attribute buffer as floats.
|
|
13167
|
+
* @param uint32View - The attribute buffer as unsigned integers.
|
|
13168
|
+
* @param index - The first float of the element.
|
|
13169
|
+
* @param textureId - The element's slot in its batch's textures.
|
|
13170
|
+
*/
|
|
13171
|
+
packAttributes(element, float32View, uint32View, index, textureId) {
|
|
13172
|
+
const textureIdAndRound = textureId << 16 | element.roundPixels & 65535;
|
|
13173
|
+
const { a, b, c, d, tx, ty } = element.transform;
|
|
13174
|
+
const distance = distanceOf(element, a, b, c, d, this.density);
|
|
13175
|
+
const positions = element.positions;
|
|
13176
|
+
const uvs = element.uvs;
|
|
13177
|
+
const argb = element.color;
|
|
13178
|
+
const end = element.attributeOffset + element.attributeSize;
|
|
13179
|
+
let at = index;
|
|
13180
|
+
for (let vertex = element.attributeOffset; vertex < end; vertex += 1) {
|
|
13181
|
+
const x = positions[vertex * 2] ?? 0;
|
|
13182
|
+
const y = positions[vertex * 2 + 1] ?? 0;
|
|
13183
|
+
float32View[at] = a * x + c * y + tx;
|
|
13184
|
+
float32View[at + 1] = d * y + b * x + ty;
|
|
13185
|
+
float32View[at + 2] = uvs[vertex * 2] ?? 0;
|
|
13186
|
+
float32View[at + 3] = uvs[vertex * 2 + 1] ?? 0;
|
|
13187
|
+
uint32View[at + 4] = argb;
|
|
13188
|
+
uint32View[at + 5] = textureIdAndRound;
|
|
13189
|
+
float32View[at + 6] = distance;
|
|
13190
|
+
at += VERTEX_SIZE;
|
|
13191
|
+
}
|
|
13192
|
+
}
|
|
13193
|
+
/**
|
|
13194
|
+
* Packs the four vertices of a quad element: Pixi's default quad and the distance.
|
|
13195
|
+
*
|
|
13196
|
+
* @param element - The element.
|
|
13197
|
+
* @param float32View - The attribute buffer as floats.
|
|
13198
|
+
* @param uint32View - The attribute buffer as unsigned integers.
|
|
13199
|
+
* @param index - The first float of the element.
|
|
13200
|
+
* @param textureId - The element's slot in its batch's textures.
|
|
13201
|
+
*/
|
|
13202
|
+
packQuadAttributes(element, float32View, uint32View, index, textureId) {
|
|
13203
|
+
const { a, b, c, d, tx, ty } = element.transform;
|
|
13204
|
+
const { minX, minY, maxX, maxY } = element.bounds;
|
|
13205
|
+
const uvs = element.texture.uvs;
|
|
13206
|
+
const argb = element.color;
|
|
13207
|
+
const textureIdAndRound = textureId << 16 | element.roundPixels & 65535;
|
|
13208
|
+
const distance = distanceOf(element, a, b, c, d, this.density);
|
|
13209
|
+
float32View[index] = a * minX + c * minY + tx;
|
|
13210
|
+
float32View[index + 1] = d * minY + b * minX + ty;
|
|
13211
|
+
float32View[index + 2] = uvs.x0;
|
|
13212
|
+
float32View[index + 3] = uvs.y0;
|
|
13213
|
+
uint32View[index + 4] = argb;
|
|
13214
|
+
uint32View[index + 5] = textureIdAndRound;
|
|
13215
|
+
float32View[index + 6] = distance;
|
|
13216
|
+
float32View[index + 7] = a * maxX + c * minY + tx;
|
|
13217
|
+
float32View[index + 8] = d * minY + b * maxX + ty;
|
|
13218
|
+
float32View[index + 9] = uvs.x1;
|
|
13219
|
+
float32View[index + 10] = uvs.y1;
|
|
13220
|
+
uint32View[index + 11] = argb;
|
|
13221
|
+
uint32View[index + 12] = textureIdAndRound;
|
|
13222
|
+
float32View[index + 13] = distance;
|
|
13223
|
+
float32View[index + 14] = a * maxX + c * maxY + tx;
|
|
13224
|
+
float32View[index + 15] = d * maxY + b * maxX + ty;
|
|
13225
|
+
float32View[index + 16] = uvs.x2;
|
|
13226
|
+
float32View[index + 17] = uvs.y2;
|
|
13227
|
+
uint32View[index + 18] = argb;
|
|
13228
|
+
uint32View[index + 19] = textureIdAndRound;
|
|
13229
|
+
float32View[index + 20] = distance;
|
|
13230
|
+
float32View[index + 21] = a * minX + c * maxY + tx;
|
|
13231
|
+
float32View[index + 22] = d * maxY + b * minX + ty;
|
|
13232
|
+
float32View[index + 23] = uvs.x3;
|
|
13233
|
+
float32View[index + 24] = uvs.y3;
|
|
13234
|
+
uint32View[index + 25] = argb;
|
|
13235
|
+
uint32View[index + 26] = textureIdAndRound;
|
|
13236
|
+
float32View[index + 27] = distance;
|
|
13237
|
+
}
|
|
13238
|
+
/**
|
|
13239
|
+
* Packs every element of the drawn batches again at a resolution and hands the buffer to the GPU.
|
|
13240
|
+
*
|
|
13241
|
+
* @param density - Device pixels per CSS pixel.
|
|
13242
|
+
*/
|
|
13243
|
+
packAt(density) {
|
|
13244
|
+
this.density = density;
|
|
13245
|
+
for (let batch = 0; batch < this.batchIndex; batch += 1) {
|
|
13246
|
+
const elements = this.batches[batch]?.elements;
|
|
13247
|
+
if (elements === void 0) continue;
|
|
13248
|
+
for (let at = 0; at < elements.length; at += 1) {
|
|
13249
|
+
const element = elements[at];
|
|
13250
|
+
if (element !== void 0) this.updateElement(element);
|
|
13251
|
+
}
|
|
13252
|
+
}
|
|
13253
|
+
this.dirty = false;
|
|
13254
|
+
this.geometry.buffers[0]?.update(this.attributeSize * 4);
|
|
13255
|
+
}
|
|
13256
|
+
}
|
|
13257
|
+
return SdfBatcher;
|
|
13176
13258
|
}
|
|
13177
13259
|
/**
|
|
13178
|
-
*
|
|
13179
|
-
* for, shared by every text after. Its own function, because lint rule L5 refuses a collection
|
|
13180
|
-
* built inside an exported declaration.
|
|
13260
|
+
* Tells whether a batch's batcher is the renderer's batcher of distance-field glyphs.
|
|
13181
13261
|
*
|
|
13182
|
-
* @param
|
|
13183
|
-
* @returns
|
|
13262
|
+
* @param batcher - The batcher of a batch.
|
|
13263
|
+
* @returns True for the SDF batcher.
|
|
13184
13264
|
*/
|
|
13185
|
-
function
|
|
13186
|
-
|
|
13187
|
-
return (maxTextures) => {
|
|
13188
|
-
const known = programs.get(maxTextures);
|
|
13189
|
-
if (known !== void 0) return known;
|
|
13190
|
-
const program = compile(maxTextures);
|
|
13191
|
-
programs.set(maxTextures, program);
|
|
13192
|
-
return program;
|
|
13193
|
-
};
|
|
13265
|
+
function isSdfBatcher(batcher) {
|
|
13266
|
+
return batcher.name === SDF_BATCHER;
|
|
13194
13267
|
}
|
|
13195
13268
|
/**
|
|
13196
|
-
*
|
|
13197
|
-
*
|
|
13269
|
+
* Builds the renderer's batcher pipe from the module object, at call time. It keeps the base's `static extension`
|
|
13270
|
+
* object, so the registry files it under the same types and name, and Pixi's batcher registry, which `getBatcher`
|
|
13271
|
+
* reads. `addToBatch` names the batcher of an element before Pixi adds it: the glyphs of a distance-field text go to
|
|
13272
|
+
* `SDF_BATCHER`; a pooled element that carried that name before, now another graphics' or a plain font's, goes back to
|
|
13273
|
+
* Pixi's default. `execute` packs the distances of a batcher again when the renderer's resolution changed since it
|
|
13274
|
+
* last drew, before Pixi binds its geometry. The monitor's counting pipe of a dev build extends this pipe.
|
|
13198
13275
|
*
|
|
13199
13276
|
* @param pixi - The loaded Pixi module.
|
|
13200
|
-
* @returns The
|
|
13277
|
+
* @returns The pipe class.
|
|
13201
13278
|
*/
|
|
13202
|
-
function
|
|
13203
|
-
|
|
13204
|
-
|
|
13205
|
-
|
|
13206
|
-
|
|
13207
|
-
|
|
13208
|
-
|
|
13209
|
-
|
|
13210
|
-
|
|
13211
|
-
|
|
13212
|
-
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
13219
|
-
|
|
13220
|
-
|
|
13221
|
-
|
|
13279
|
+
function createSdfBatcherPipe(pixi) {
|
|
13280
|
+
/** Pixi's batcher pipe, which routes the glyphs of distance-field text to the renderer's batcher. */
|
|
13281
|
+
class SdfRoutingBatcherPipe extends pixi.BatcherPipe {
|
|
13282
|
+
static extension = pixi.BatcherPipe.extension;
|
|
13283
|
+
/**
|
|
13284
|
+
* Names the element's batcher, then adds it as Pixi does.
|
|
13285
|
+
*
|
|
13286
|
+
* @param element - The element.
|
|
13287
|
+
* @param instructionSet - The instructions of its render group.
|
|
13288
|
+
*/
|
|
13289
|
+
addToBatch(element, instructionSet) {
|
|
13290
|
+
routeElement(element);
|
|
13291
|
+
super.addToBatch(element, instructionSet);
|
|
13292
|
+
}
|
|
13293
|
+
/**
|
|
13294
|
+
* Draws a batch as Pixi does; the first batch of an SDF batcher's run first packs the distances again when the
|
|
13295
|
+
* resolution moved.
|
|
13296
|
+
*
|
|
13297
|
+
* @param batch - The batch.
|
|
13298
|
+
*/
|
|
13299
|
+
execute(batch) {
|
|
13300
|
+
const batcher = batch.batcher;
|
|
13301
|
+
if (batch.action === "startBatch" && isSdfBatcher(batcher)) {
|
|
13302
|
+
const resolution = this.renderer.resolution;
|
|
13303
|
+
if (batcher.density !== resolution) batcher.packAt(resolution);
|
|
13304
|
+
}
|
|
13305
|
+
super.execute(batch);
|
|
13306
|
+
}
|
|
13307
|
+
}
|
|
13308
|
+
return SdfRoutingBatcherPipe;
|
|
13222
13309
|
}
|
|
13223
13310
|
/**
|
|
13224
|
-
*
|
|
13225
|
-
*
|
|
13311
|
+
* Names the batcher of one element: `SDF_BATCHER` for a glyph of a distance-field text, Pixi's default again for an
|
|
13312
|
+
* element that carried that name from an earlier owner, as it was otherwise.
|
|
13226
13313
|
*
|
|
13227
|
-
* @param
|
|
13228
|
-
* @returns The WebGL program for a texture limit.
|
|
13314
|
+
* @param element - The element.
|
|
13229
13315
|
*/
|
|
13230
|
-
function
|
|
13231
|
-
const
|
|
13232
|
-
|
|
13233
|
-
|
|
13234
|
-
name: TEXT_BIT_NAME,
|
|
13235
|
-
fragment: {
|
|
13236
|
-
...base.fragment,
|
|
13237
|
-
main: SINGLE_ALPHA_STEP_GL
|
|
13238
|
-
}
|
|
13239
|
-
};
|
|
13240
|
-
return cachedBy((maxTextures) => pixi.compileHighShaderGlProgram({
|
|
13241
|
-
name: PROGRAM_NAME,
|
|
13242
|
-
bits: [
|
|
13243
|
-
pixi.colorBitGl,
|
|
13244
|
-
pixi.generateTextureBatchBitGl(maxTextures),
|
|
13245
|
-
textBit,
|
|
13246
|
-
pixi.mSDFBitGl,
|
|
13247
|
-
pixi.roundPixelsBitGl
|
|
13248
|
-
]
|
|
13249
|
-
}));
|
|
13316
|
+
function routeElement(element) {
|
|
13317
|
+
const glyphs = element.renderable?.[SDF];
|
|
13318
|
+
if (glyphs !== void 0 && glyphs.perScale > 0) element.batcherName = SDF_BATCHER;
|
|
13319
|
+
else if (element.batcherName === "moku-sdf") element.batcherName = DEFAULT_BATCHER;
|
|
13250
13320
|
}
|
|
13321
|
+
//#endregion
|
|
13322
|
+
//#region src/plugins/renderer/host/sdf-text.ts
|
|
13323
|
+
/** The suffix of a bitmap font's key in Pixi's cache, the one `_updateDistanceField` reads. */
|
|
13324
|
+
const CACHE_SUFFIX$1 = "-bitmap";
|
|
13251
13325
|
/**
|
|
13252
|
-
*
|
|
13253
|
-
* Pixi's
|
|
13254
|
-
*
|
|
13255
|
-
*
|
|
13256
|
-
*
|
|
13257
|
-
*
|
|
13258
|
-
|
|
13259
|
-
|
|
13326
|
+
* Measures the edge of a text's glyphs again when its font family or size changed since: Pixi's distance at scale 1
|
|
13327
|
+
* from the font in Pixi's cache, 0 for a font without a distance field or not in the cache.
|
|
13328
|
+
*
|
|
13329
|
+
* @param pixi - The loaded Pixi module.
|
|
13330
|
+
* @param glyphs - The text's glyph record.
|
|
13331
|
+
* @param style - The text's style.
|
|
13332
|
+
*/
|
|
13333
|
+
function measureGlyphs(pixi, glyphs, style) {
|
|
13334
|
+
const family = style.fontFamily;
|
|
13335
|
+
const size = style.fontSize;
|
|
13336
|
+
if (family === glyphs.family && size === glyphs.size) return;
|
|
13337
|
+
const key = `${String(family)}${CACHE_SUFFIX$1}`;
|
|
13338
|
+
const font = pixi.Cache.has(key) ? pixi.Cache.get(key) : void 0;
|
|
13339
|
+
const field = font?.distanceField;
|
|
13340
|
+
glyphs.family = family;
|
|
13341
|
+
glyphs.size = size;
|
|
13342
|
+
glyphs.perScale = font === void 0 || field === void 0 || field.type === "none" ? 0 : field.range * (1 / (font.baseRenderedFontSize / size));
|
|
13343
|
+
}
|
|
13344
|
+
/**
|
|
13345
|
+
* Builds the renderer's bitmap text pipe from the module object, at call time, because Pixi's pipe arrives with the
|
|
13346
|
+
* lazily loaded module. It keeps the base's `static extension` object, so the registry files it under the same types
|
|
13347
|
+
* and name. Every text's proxy graphics gets a batchable context and its glyph record when Pixi makes it; the record is
|
|
13348
|
+
* brought up to date before Pixi adds or updates the text, so the batcher packs the edge of the text's font.
|
|
13260
13349
|
*
|
|
13261
13350
|
* @param pixi - The loaded Pixi module.
|
|
13262
13351
|
* @returns The pipe class.
|
|
13263
13352
|
*/
|
|
13264
13353
|
function createSdfTextPipe(pixi) {
|
|
13265
|
-
|
|
13266
|
-
|
|
13267
|
-
/** Pixi's bitmap text pipe, with an SDF shader that applies the alpha once. */
|
|
13268
|
-
class SingleAlphaBitmapTextPipe extends pixi.BitmapTextPipe {
|
|
13354
|
+
/** Pixi's bitmap text pipe, with distance-field text drawn by the renderer's batcher. */
|
|
13355
|
+
class SdfTextPipe extends pixi.BitmapTextPipe {
|
|
13269
13356
|
static extension = pixi.BitmapTextPipe.extension;
|
|
13270
|
-
/** The text whose distance Pixi writes now, read by the uniforms of every text of this pipe. */
|
|
13271
|
-
writing = { text: void 0 };
|
|
13272
13357
|
/**
|
|
13273
|
-
*
|
|
13274
|
-
*
|
|
13275
|
-
*
|
|
13276
|
-
*
|
|
13358
|
+
* Makes a text's proxy graphics as Pixi does, batchable at any length, with the edge of its glyphs.
|
|
13359
|
+
*
|
|
13360
|
+
* @param text - The text.
|
|
13361
|
+
* @returns The proxy.
|
|
13277
13362
|
*/
|
|
13278
|
-
|
|
13363
|
+
initGpuText(text) {
|
|
13364
|
+
const proxy = super.initGpuText(text);
|
|
13365
|
+
const glyphs = {
|
|
13366
|
+
perScale: NaN,
|
|
13367
|
+
family: void 0,
|
|
13368
|
+
size: NaN
|
|
13369
|
+
};
|
|
13370
|
+
proxy.context.batchMode = "batch";
|
|
13371
|
+
proxy[SDF] = glyphs;
|
|
13372
|
+
measureGlyphs(pixi, glyphs, text._style);
|
|
13373
|
+
return proxy;
|
|
13374
|
+
}
|
|
13279
13375
|
/**
|
|
13280
|
-
* Adds a text to the instructions as Pixi does,
|
|
13376
|
+
* Adds a text to the instructions as Pixi does, its glyph edge measured first.
|
|
13281
13377
|
*
|
|
13282
13378
|
* @param text - The text.
|
|
13283
13379
|
* @param instructionSet - The instructions of its render group.
|
|
13284
13380
|
*/
|
|
13285
13381
|
addRenderable(text, instructionSet) {
|
|
13286
|
-
this.
|
|
13287
|
-
this.laying = text;
|
|
13382
|
+
this.measure(text);
|
|
13288
13383
|
super.addRenderable(text, instructionSet);
|
|
13289
|
-
this.laying = void 0;
|
|
13290
|
-
this.writing.text = void 0;
|
|
13291
13384
|
}
|
|
13292
13385
|
/**
|
|
13293
|
-
* Updates a text as Pixi does,
|
|
13386
|
+
* Updates a text as Pixi does, its glyph edge measured first.
|
|
13294
13387
|
*
|
|
13295
13388
|
* @param text - The text.
|
|
13296
13389
|
*/
|
|
13297
13390
|
updateRenderable(text) {
|
|
13298
|
-
this.
|
|
13391
|
+
this.measure(text);
|
|
13299
13392
|
super.updateRenderable(text);
|
|
13300
|
-
this.writing.text = void 0;
|
|
13301
13393
|
}
|
|
13302
13394
|
/**
|
|
13303
|
-
*
|
|
13304
|
-
* else a new one.
|
|
13395
|
+
* No shader: Pixi then leaves the text's context without one, and batchable.
|
|
13305
13396
|
*
|
|
13306
|
-
* @returns
|
|
13397
|
+
* @returns `null`.
|
|
13307
13398
|
*/
|
|
13308
13399
|
getSdfShader() {
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13314
|
-
|
|
13315
|
-
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
resources: {
|
|
13320
|
-
localUniforms: createLocalUniforms(pixi, density, this.writing),
|
|
13321
|
-
batchSamplers: pixi.getBatchSamplersUniformGroup(maxTextures)
|
|
13322
|
-
}
|
|
13323
|
-
});
|
|
13400
|
+
return null;
|
|
13401
|
+
}
|
|
13402
|
+
/**
|
|
13403
|
+
* Measures the glyph edge of a text Pixi made a proxy for already; a new text's proxy measures its own.
|
|
13404
|
+
*
|
|
13405
|
+
* @param text - The text.
|
|
13406
|
+
*/
|
|
13407
|
+
measure(text) {
|
|
13408
|
+
const proxy = text._gpuData[this._renderer.uid];
|
|
13409
|
+
if (proxy !== void 0) measureGlyphs(pixi, proxy[SDF], text._style);
|
|
13324
13410
|
}
|
|
13325
13411
|
}
|
|
13326
|
-
return
|
|
13412
|
+
return SdfTextPipe;
|
|
13327
13413
|
}
|
|
13328
13414
|
/**
|
|
13329
|
-
*
|
|
13330
|
-
*
|
|
13331
|
-
* afterwards draws
|
|
13332
|
-
*
|
|
13415
|
+
* Installs the three pieces of batched distance-field text in the module's extension registry: the batcher under its
|
|
13416
|
+
* own name, and the bitmap text pipe and the batch pipe under Pixi's, on both backends. Every application made
|
|
13417
|
+
* afterwards draws distance-field text with them. Every class is built before the registry is touched, so a module
|
|
13418
|
+
* without the pieces throws with the registry as it was. The registry is global to the module object, so the remover
|
|
13419
|
+
* matters: it swaps Pixi's own pipes back and leaves the batcher, which a pooled element may still name. A counting batch pipe of the monitor, which extends the routing pipe, is
|
|
13420
|
+
* installed after this one, in its place.
|
|
13333
13421
|
*
|
|
13334
13422
|
* @param pixi - The loaded Pixi module.
|
|
13335
13423
|
* @returns The remover.
|
|
13336
|
-
* @throws {TypeError} When the module has no `BitmapTextPipe` to subclass.
|
|
13337
|
-
*/
|
|
13338
|
-
function
|
|
13339
|
-
const
|
|
13340
|
-
|
|
13341
|
-
pixi
|
|
13424
|
+
* @throws {TypeError} When the module has no `Batcher`, `BitmapTextPipe` or `BatcherPipe` to subclass.
|
|
13425
|
+
*/
|
|
13426
|
+
function installSdfText(pixi) {
|
|
13427
|
+
const batcher = createSdfBatcher(pixi);
|
|
13428
|
+
const textPipe = createSdfTextPipe(pixi);
|
|
13429
|
+
const batchPipe = createSdfBatcherPipe(pixi);
|
|
13430
|
+
pixi.extensions.remove(pixi.BitmapTextPipe, pixi.BatcherPipe);
|
|
13431
|
+
pixi.extensions.add(batcher, textPipe, batchPipe);
|
|
13342
13432
|
return () => {
|
|
13343
|
-
pixi.extensions.remove(
|
|
13344
|
-
pixi.extensions.add(pixi.BitmapTextPipe);
|
|
13433
|
+
pixi.extensions.remove(textPipe, batchPipe);
|
|
13434
|
+
pixi.extensions.add(pixi.BitmapTextPipe, pixi.BatcherPipe);
|
|
13345
13435
|
};
|
|
13346
13436
|
}
|
|
13347
13437
|
//#endregion
|
|
@@ -13478,26 +13568,27 @@ function installCounting$1(ctx, pixi, deps) {
|
|
|
13478
13568
|
} else installPasses(ctx, pixi, deps.installPasses);
|
|
13479
13569
|
}
|
|
13480
13570
|
/**
|
|
13481
|
-
*
|
|
13482
|
-
*
|
|
13483
|
-
*
|
|
13484
|
-
* Every build installs it. A Pixi without the pieces draws
|
|
13571
|
+
* Installs the renderer's batched distance-field text: its batcher, its bitmap text pipe and its batch pipe, which draw
|
|
13572
|
+
* consecutive labels in one draw with the alpha applied once. It runs before `new Application()`, because Pixi builds
|
|
13573
|
+
* its pipes inside `init`, and before the draw counter, whose batch pipe extends the routing one and takes its place;
|
|
13574
|
+
* once per host state: a restore reuses the installed pieces. Every build installs it. A Pixi without the pieces draws
|
|
13575
|
+
* on with its own pipes, with a warning.
|
|
13485
13576
|
*
|
|
13486
13577
|
* @param ctx - Domain context of the renderer plugin.
|
|
13487
13578
|
* @param pixi - The module the application is built from.
|
|
13488
13579
|
*/
|
|
13489
|
-
function
|
|
13580
|
+
function installTextBatch(ctx, pixi) {
|
|
13490
13581
|
const state = ctx.state.host;
|
|
13491
13582
|
if (state.uninstallSdfText !== void 0) return;
|
|
13492
13583
|
try {
|
|
13493
|
-
state.uninstallSdfText =
|
|
13584
|
+
state.uninstallSdfText = installSdfText(pixi);
|
|
13494
13585
|
} catch (error) {
|
|
13495
13586
|
ctx.log.warn("renderer: distance-field text applies its alpha twice", { error });
|
|
13496
13587
|
}
|
|
13497
13588
|
}
|
|
13498
13589
|
/**
|
|
13499
13590
|
* Swaps Pixi's render-group pipe for the renderer's, which binds a filter's input again after a group cached as a
|
|
13500
|
-
* texture was refreshed inside the filter (F10). Like the
|
|
13591
|
+
* texture was refreshed inside the filter (F10). Like the batched text it runs before `new Application()`, once per
|
|
13501
13592
|
* host state, in every build. A Pixi without the pieces draws on with its own pipe, with a warning.
|
|
13502
13593
|
*
|
|
13503
13594
|
* @param ctx - Domain context of the renderer plugin.
|
|
@@ -13531,8 +13622,8 @@ async function createApplication(ctx, mount, deps) {
|
|
|
13531
13622
|
const pixi = loaded ?? await ctx.config.loadPixi();
|
|
13532
13623
|
if (typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ && loaded === void 0) startupMark("moku:pixi-end");
|
|
13533
13624
|
state.pixi = pixi;
|
|
13625
|
+
installTextBatch(ctx, pixi);
|
|
13534
13626
|
installCounting$1(ctx, pixi, deps);
|
|
13535
|
-
installSdfText(ctx, pixi);
|
|
13536
13627
|
installRenderGroup(ctx, pixi);
|
|
13537
13628
|
state.resolution.dpr = devicePixels();
|
|
13538
13629
|
state.resolution.cap = ctx.config.maxResolution;
|
|
@@ -13577,7 +13668,7 @@ function destroyApplication(state) {
|
|
|
13577
13668
|
/**
|
|
13578
13669
|
* Undoes everything `init` created. Works on the state alone, because `onStop` has no context.
|
|
13579
13670
|
* `ready` drops first, so a device that goes away during the teardown is ignored. The draw
|
|
13580
|
-
* counter, the
|
|
13671
|
+
* counter, the batched text and the render-group pipe swap Pixi's own classes back, so the next application on
|
|
13581
13672
|
* the page starts from them. The pool trim starts over and its listeners go; Pixi's pool itself is left.
|
|
13582
13673
|
*
|
|
13583
13674
|
* @param state - The host branch of the plugin state.
|
|
@@ -26837,19 +26928,22 @@ function countUpload(sinks, source) {
|
|
|
26837
26928
|
/**
|
|
26838
26929
|
* Builds the seven counting subclasses from the module object. They are made at call time,
|
|
26839
26930
|
* because the base classes arrive with the lazily loaded module. Each keeps the base's `static
|
|
26840
|
-
* extension` object as it is, so the registry files it under the same type and name.
|
|
26931
|
+
* extension` object as it is, so the registry files it under the same type and name. The batch pipe extends the one
|
|
26932
|
+
* it is given: the routing pipe of the renderer's batched text, which the plugin root injects, so the counting pipe
|
|
26933
|
+
* that takes its place routes the same.
|
|
26841
26934
|
*
|
|
26842
26935
|
* @param pixi - The loaded Pixi module.
|
|
26843
26936
|
* @param sinks - What every instance counts into.
|
|
26937
|
+
* @param batcherPipe - The batch pipe the counting one extends; Pixi's own when none is given.
|
|
26844
26938
|
* @returns The seven counting classes (BatcherPipe among them).
|
|
26845
26939
|
*/
|
|
26846
|
-
function createCountingClasses(pixi, sinks) {
|
|
26940
|
+
function createCountingClasses(pixi, sinks, batcherPipe = pixi.BatcherPipe) {
|
|
26847
26941
|
const counter = sinks.draws;
|
|
26848
26942
|
/**
|
|
26849
26943
|
* Counts every rebuild of a render group's instruction list: Pixi's render group system calls `buildStart` once per
|
|
26850
26944
|
* rebuild and nowhere else. It inherits the static batcher registry, so batchers resolve as before.
|
|
26851
26945
|
*/
|
|
26852
|
-
class CountingBatcherPipe extends
|
|
26946
|
+
class CountingBatcherPipe extends batcherPipe {
|
|
26853
26947
|
static extension = pixi.BatcherPipe.extension;
|
|
26854
26948
|
/**
|
|
26855
26949
|
* Counts the rebuild, in the frame's counter only inside a frame, then begins it.
|
|
@@ -27056,10 +27150,11 @@ function createCountingClasses(pixi, sinks) {
|
|
|
27056
27150
|
*
|
|
27057
27151
|
* @param pixi - The loaded Pixi module.
|
|
27058
27152
|
* @param sinks - The sinks of the renderer that installs it.
|
|
27153
|
+
* @param batcherPipe - The batch pipe the counting one extends: the routing pipe of the batched text.
|
|
27059
27154
|
* @returns The remover.
|
|
27060
27155
|
*/
|
|
27061
|
-
function installCounting(pixi, sinks) {
|
|
27062
|
-
const counting = createCountingClasses(pixi, sinks);
|
|
27156
|
+
function installCounting(pixi, sinks, batcherPipe = pixi.BatcherPipe) {
|
|
27157
|
+
const counting = createCountingClasses(pixi, sinks, batcherPipe);
|
|
27063
27158
|
const bases = [
|
|
27064
27159
|
pixi.BatcherPipe,
|
|
27065
27160
|
pixi.GpuBatchAdaptor,
|
|
@@ -29427,14 +29522,14 @@ function createViewportApi(ctx, deps) {
|
|
|
29427
29522
|
/**
|
|
29428
29523
|
* The counting `host` installs before `new Application()`: the seven counting classes (BatcherPipe among them) of
|
|
29429
29524
|
* `monitor` in a dev build and in a debug build, bound to the sinks in the monitor branch of the
|
|
29430
|
-
* state, none in production
|
|
29431
|
-
* module leaves the bundle.
|
|
29525
|
+
* state, none in production; the counting batch pipe extends the routing pipe of the batched text. A production
|
|
29526
|
+
* `define` folds the dev or agent guard, so the counting module leaves the bundle.
|
|
29432
29527
|
*
|
|
29433
29528
|
* @param ctx - Domain context of the renderer plugin.
|
|
29434
29529
|
* @returns The bound installer, or `undefined` without the dev flag and the agent flag.
|
|
29435
29530
|
*/
|
|
29436
29531
|
function countingOf(ctx) {
|
|
29437
|
-
return typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? (pixi) => installCounting(pixi, ctx.state.monitor.sinks) : void 0;
|
|
29532
|
+
return typeof __MOKU_GAME_DEV__ !== "undefined" && __MOKU_GAME_DEV__ || typeof __MOKU_GAME_AGENT__ !== "undefined" && __MOKU_GAME_AGENT__ ? (pixi) => installCounting(pixi, ctx.state.monitor.sinks, createSdfBatcherPipe(pixi)) : void 0;
|
|
29438
29533
|
}
|
|
29439
29534
|
/**
|
|
29440
29535
|
* The writers of `sync` the kept cache draws through: the one owner of `renderable`, the one writer of a view's
|
|
@@ -32132,14 +32227,14 @@ function cancelTracksOf(actx, entity) {
|
|
|
32132
32227
|
//#endregion
|
|
32133
32228
|
//#region src/plugins/anim/tween/driver.ts
|
|
32134
32229
|
/** The empty field set of every track nobody muted: one frozen set, shared, read every frame. */
|
|
32135
|
-
const NO_FIELDS = Object.freeze(/* @__PURE__ */ new Set());
|
|
32230
|
+
const NO_FIELDS$1 = Object.freeze(/* @__PURE__ */ new Set());
|
|
32136
32231
|
/**
|
|
32137
32232
|
* The muted fields of a timeline step's track: none.
|
|
32138
32233
|
*
|
|
32139
32234
|
* @returns The shared empty set; a frame builds no set.
|
|
32140
32235
|
*/
|
|
32141
32236
|
function noFields() {
|
|
32142
|
-
return NO_FIELDS;
|
|
32237
|
+
return NO_FIELDS$1;
|
|
32143
32238
|
}
|
|
32144
32239
|
/**
|
|
32145
32240
|
* Creates the tween engine `world` runs `ViewHandle.tween`, `toRest` and `all` on. Installed with
|
|
@@ -35770,24 +35865,22 @@ function overTargetOf(ctx, node, run) {
|
|
|
35770
35865
|
return takePending(ctx);
|
|
35771
35866
|
}
|
|
35772
35867
|
/**
|
|
35773
|
-
*
|
|
35774
|
-
*
|
|
35775
|
-
*
|
|
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.
|
|
35776
35871
|
*
|
|
35777
|
-
* @param
|
|
35778
|
-
* @param scene - The
|
|
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.
|
|
35779
35875
|
*/
|
|
35780
|
-
function
|
|
35781
|
-
|
|
35782
|
-
const release = state.held;
|
|
35783
|
-
const owner = state.owners.get(scene);
|
|
35784
|
-
state.held = owner === void 0 ? void 0 : ctx.deps.world.ecs.hold(owner);
|
|
35785
|
-
release?.();
|
|
35876
|
+
function leavingOf(previous, scene) {
|
|
35877
|
+
return previous.projections.filter((name) => !scene.projections.includes(name));
|
|
35786
35878
|
}
|
|
35787
35879
|
/**
|
|
35788
|
-
* Builds the scene in one synchronous block: the layers, then the projections
|
|
35789
|
-
* out, then the
|
|
35790
|
-
*
|
|
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.
|
|
35791
35884
|
*
|
|
35792
35885
|
* @param ctx - Domain context of the plugin.
|
|
35793
35886
|
* @param scene - The scene whose bundles are there.
|
|
@@ -35799,10 +35892,10 @@ function apply(ctx, scene, named) {
|
|
|
35799
35892
|
const from = state.current;
|
|
35800
35893
|
const previous = from === void 0 ? void 0 : state.scenes.get(from);
|
|
35801
35894
|
projection.setLayers(scene.layers);
|
|
35802
|
-
if (previous !== void 0) projection.unmount(previous
|
|
35895
|
+
if (previous !== void 0) projection.unmount(leavingOf(previous, scene));
|
|
35803
35896
|
projection.mount(scene.projections, state.owner);
|
|
35804
35897
|
state.current = scene.id;
|
|
35805
|
-
|
|
35898
|
+
ctx.deps.world.ecs.setScene(scene.id);
|
|
35806
35899
|
state.named = named;
|
|
35807
35900
|
ctx.deps.assets.sceneBundles(named);
|
|
35808
35901
|
ctx.deps.time.wake();
|
|
@@ -35916,16 +36009,22 @@ function withDeps$5(ctx) {
|
|
|
35916
36009
|
};
|
|
35917
36010
|
}
|
|
35918
36011
|
/**
|
|
35919
|
-
*
|
|
35920
|
-
*
|
|
35921
|
-
*
|
|
35922
|
-
|
|
36012
|
+
* Records which feature declared which scene, so a duplicate id can name both of them.
|
|
36013
|
+
*
|
|
36014
|
+
* @returns An empty table of scene id to feature name.
|
|
36015
|
+
*/
|
|
36016
|
+
function emptyOwners$1() {
|
|
36017
|
+
return /* @__PURE__ */ new Map();
|
|
36018
|
+
}
|
|
36019
|
+
/**
|
|
36020
|
+
* Reads the `scenes` key of every registered feature into the registry. Features register in
|
|
36021
|
+
* their own `onInit`, which runs before this `onStart`, so every scene of the game is here.
|
|
35923
36022
|
*
|
|
35924
36023
|
* @param ctx - Domain context of the plugin.
|
|
35925
36024
|
* @throws {Error} When two features declare the same scene id.
|
|
35926
36025
|
*/
|
|
35927
36026
|
function readScenes(ctx) {
|
|
35928
|
-
const owners =
|
|
36027
|
+
const owners = emptyOwners$1();
|
|
35929
36028
|
for (const feature of ctx.deps.flow.features.all()) {
|
|
35930
36029
|
const list = feature.description.scenes;
|
|
35931
36030
|
if (!Array.isArray(list)) continue;
|
|
@@ -35956,9 +36055,9 @@ function startScenes(ctx) {
|
|
|
35956
36055
|
scenes.state.teardown = scenes.deps.flow.onEnter("scene", (node, run) => enterScene(scenes, node, run));
|
|
35957
36056
|
}
|
|
35958
36057
|
/**
|
|
35959
|
-
* Removes the `onEnter` callback in `onStop` and forgets every scene
|
|
35960
|
-
* one named
|
|
35961
|
-
* context may not call another plugin, and `world.onStop` clears every entity
|
|
36058
|
+
* Removes the `onEnter` callback in `onStop` and forgets every scene and the bundles the mounted
|
|
36059
|
+
* one named. Nothing is unmounted: a
|
|
36060
|
+
* teardown context may not call another plugin, and `world.onStop` clears every entity anyway.
|
|
35962
36061
|
*
|
|
35963
36062
|
* @param teardown - What the teardown context carries.
|
|
35964
36063
|
* @param teardown.state - The plugin state.
|
|
@@ -35973,8 +36072,6 @@ function stopScenes(teardown) {
|
|
|
35973
36072
|
state.teardown?.();
|
|
35974
36073
|
state.teardown = void 0;
|
|
35975
36074
|
state.scenes.clear();
|
|
35976
|
-
state.owners.clear();
|
|
35977
|
-
state.held = void 0;
|
|
35978
36075
|
state.current = void 0;
|
|
35979
36076
|
state.pending = void 0;
|
|
35980
36077
|
state.named = [];
|
|
@@ -35991,15 +36088,6 @@ function emptyRegistry$1() {
|
|
|
35991
36088
|
return /* @__PURE__ */ new Map();
|
|
35992
36089
|
}
|
|
35993
36090
|
/**
|
|
35994
|
-
* Creates the table of which feature declared which scene, for the same reason as
|
|
35995
|
-
* `emptyRegistry`.
|
|
35996
|
-
*
|
|
35997
|
-
* @returns An empty table of scene id to feature name.
|
|
35998
|
-
*/
|
|
35999
|
-
function emptyOwners$1() {
|
|
36000
|
-
return /* @__PURE__ */ new Map();
|
|
36001
|
-
}
|
|
36002
|
-
/**
|
|
36003
36091
|
* Creates the one owner every mount of this plugin carries. One is enough: a single scene is
|
|
36004
36092
|
* mounted at a time and `unmount` is what flushes.
|
|
36005
36093
|
*
|
|
@@ -36012,8 +36100,8 @@ function sceneOwner() {
|
|
|
36012
36100
|
};
|
|
36013
36101
|
}
|
|
36014
36102
|
/**
|
|
36015
|
-
* Creates the initial scenes state: nothing declared, nothing mounted, nothing pending
|
|
36016
|
-
*
|
|
36103
|
+
* Creates the initial scenes state: nothing declared, nothing mounted, nothing pending. The
|
|
36104
|
+
* registry is filled in `onStart` from the feature descriptions.
|
|
36017
36105
|
*
|
|
36018
36106
|
* @returns A fresh state, owned by one app.
|
|
36019
36107
|
*/
|
|
@@ -36024,9 +36112,7 @@ function createScenesState() {
|
|
|
36024
36112
|
pending: void 0,
|
|
36025
36113
|
named: [],
|
|
36026
36114
|
owner: sceneOwner(),
|
|
36027
|
-
teardown: void 0
|
|
36028
|
-
owners: emptyOwners$1(),
|
|
36029
|
-
held: void 0
|
|
36115
|
+
teardown: void 0
|
|
36030
36116
|
};
|
|
36031
36117
|
}
|
|
36032
36118
|
//#endregion
|
|
@@ -42258,8 +42344,9 @@ function countParticles(ectx) {
|
|
|
42258
42344
|
state.over.particles = above;
|
|
42259
42345
|
}
|
|
42260
42346
|
/**
|
|
42261
|
-
* Builds the `animate` system of the
|
|
42262
|
-
*
|
|
42347
|
+
* Builds the `animate` system of the emitters. It runs while an entity carries `Emitter` (the world
|
|
42348
|
+
* skips a system whose query has no entity) and returns at once while the renderer does not draw.
|
|
42349
|
+
* Time is the frame's `delta` only, so `time.step(dt)` drives every test.
|
|
42263
42350
|
*
|
|
42264
42351
|
* @param ectx - Domain context of the effects plugin.
|
|
42265
42352
|
* @returns The system `world.ecs.system` takes.
|
|
@@ -42275,6 +42362,26 @@ function createParticleSystem(ectx) {
|
|
|
42275
42362
|
if (pixi === void 0) return;
|
|
42276
42363
|
const reducedMotion = ectx.deps.anim.reducedMotion();
|
|
42277
42364
|
for (const [entity, emitter] of rows) runEmitter(ectx, pixi, entity, emitter, context.time, reducedMotion);
|
|
42365
|
+
}
|
|
42366
|
+
});
|
|
42367
|
+
}
|
|
42368
|
+
/**
|
|
42369
|
+
* Builds the `animate` system of the orphans, registered right after the emitters: the particles
|
|
42370
|
+
* of an emitter whose host left fly on after the last `Emitter` is gone, so this system has no
|
|
42371
|
+
* query and runs every frame. It returns at once while the renderer does not draw, then steps the
|
|
42372
|
+
* orphans and keeps the particle budget.
|
|
42373
|
+
*
|
|
42374
|
+
* @param ectx - Domain context of the effects plugin.
|
|
42375
|
+
* @returns The system `world.ecs.system` takes.
|
|
42376
|
+
*/
|
|
42377
|
+
function createOrphanSystem(ectx) {
|
|
42378
|
+
return system({
|
|
42379
|
+
name: "effects:orphans",
|
|
42380
|
+
phase: "animate",
|
|
42381
|
+
query: [],
|
|
42382
|
+
run: (_rows, context) => {
|
|
42383
|
+
const host = ectx.deps.renderer.host;
|
|
42384
|
+
if (!host.ready() || host.pixi() === void 0) return;
|
|
42278
42385
|
runOrphans(ectx, context.time);
|
|
42279
42386
|
countParticles(ectx);
|
|
42280
42387
|
}
|
|
@@ -42602,7 +42709,7 @@ function warnScreenReads(ectx, entries) {
|
|
|
42602
42709
|
});
|
|
42603
42710
|
}
|
|
42604
42711
|
/**
|
|
42605
|
-
* Opens the world side: the
|
|
42712
|
+
* Opens the world side: the three systems, the hook that retires an emitter's instance, and the two
|
|
42606
42713
|
* hooks per filter kind; then records the filter components that are on entities already.
|
|
42607
42714
|
*
|
|
42608
42715
|
* @param ectx - Domain context of the effects plugin.
|
|
@@ -42610,7 +42717,7 @@ function warnScreenReads(ectx, entries) {
|
|
|
42610
42717
|
*/
|
|
42611
42718
|
function openWorld(ectx, kinds) {
|
|
42612
42719
|
const ecs = ectx.deps.world.ecs;
|
|
42613
|
-
ectx.state.removers.push(ecs.system(createParticleSystem(ectx)), ecs.system(createFilterSystem(ectx)), ecs.onRemoved(Emitter, (entity) => forgetEmitter(ectx, entity)), ...kinds.map((kind) => ecs.onRemoved(kind.component, (entity) => forgetFilter(ectx, kind, entity))), ...kinds.map((kind) => ecs.onAdded(kind.component, (entity) => trackFilter(ectx, kind, entity))));
|
|
42720
|
+
ectx.state.removers.push(ecs.system(createParticleSystem(ectx)), ecs.system(createOrphanSystem(ectx)), ecs.system(createFilterSystem(ectx)), ecs.onRemoved(Emitter, (entity) => forgetEmitter(ectx, entity)), ...kinds.map((kind) => ecs.onRemoved(kind.component, (entity) => forgetFilter(ectx, kind, entity))), ...kinds.map((kind) => ecs.onAdded(kind.component, (entity) => trackFilter(ectx, kind, entity))));
|
|
42614
42721
|
for (const kind of kinds) for (const [entity] of ecs.query(kind.component)) trackFilter(ectx, kind, entity);
|
|
42615
42722
|
}
|
|
42616
42723
|
/**
|
|
@@ -47812,20 +47919,68 @@ function resolveContent(ctx, entity, text) {
|
|
|
47812
47919
|
return resolved;
|
|
47813
47920
|
}
|
|
47814
47921
|
/**
|
|
47815
|
-
* The
|
|
47816
|
-
* label is visited every frame: `ui` writes `Text` after this system and change sets clear in
|
|
47817
|
-
* phase `signals`, so `changed(Text)` cannot tell which moved. The locale is read once per run,
|
|
47818
|
-
* the clock at most once and only when a countdown label asks.
|
|
47922
|
+
* The frame the system and the step after the layout flush share, made once per plugin.
|
|
47819
47923
|
*
|
|
47820
47924
|
* @param ctx - Domain context of the text plugin.
|
|
47821
|
-
* @returns
|
|
47925
|
+
* @returns A frame with no locale and no moment read yet.
|
|
47822
47926
|
*/
|
|
47823
|
-
function
|
|
47824
|
-
|
|
47927
|
+
function createTextFrame(ctx) {
|
|
47928
|
+
return {
|
|
47825
47929
|
now: NaN,
|
|
47826
47930
|
locale: "",
|
|
47827
47931
|
duration: (ms) => ctx.deps.i18n.duration(ms)
|
|
47828
47932
|
};
|
|
47933
|
+
}
|
|
47934
|
+
/**
|
|
47935
|
+
* Empties a list of labels in place: popped, so it keeps its store for the next batch.
|
|
47936
|
+
*
|
|
47937
|
+
* @param labels - The list.
|
|
47938
|
+
*/
|
|
47939
|
+
function forgetAdded(labels) {
|
|
47940
|
+
while (labels.length > 0) labels.pop();
|
|
47941
|
+
}
|
|
47942
|
+
/** A patch with no field: the write marks `Text` changed and changes nothing. Frozen, so no caller fills it. */
|
|
47943
|
+
const NO_FIELDS = Object.freeze({});
|
|
47944
|
+
/**
|
|
47945
|
+
* Writes `Text` once more, with no field, for the labels the step after the last layout flush
|
|
47946
|
+
* resolved. `ui` measured them in its solve, before they were resolved, and the change of that
|
|
47947
|
+
* frame is gone; this one reaches its remeasure in the same layout phase, so a label whose string
|
|
47948
|
+
* measures otherwise than what `ui` measured gets its size. A label whose `Text` left is skipped.
|
|
47949
|
+
*
|
|
47950
|
+
* @param ctx - Domain context of the text plugin.
|
|
47951
|
+
*/
|
|
47952
|
+
function writeOnceMore(ctx) {
|
|
47953
|
+
const ecs = ctx.deps.world.ecs;
|
|
47954
|
+
for (const entity of ctx.state.rewrite) if (ecs.get(entity, Text) !== void 0) ecs.set(entity, Text, NO_FIELDS);
|
|
47955
|
+
forgetAdded(ctx.state.rewrite);
|
|
47956
|
+
}
|
|
47957
|
+
/**
|
|
47958
|
+
* Notes a label whose `Text` came, as the world's `onAdded` hook calls it: it is marked for the
|
|
47959
|
+
* next layout phase, and while the world is live it is listed for the step after the layout
|
|
47960
|
+
* flush. In another mode no layout phase runs and the list would only grow: the label waits for
|
|
47961
|
+
* the next live run of the system, which resolves every marked label.
|
|
47962
|
+
*
|
|
47963
|
+
* @param ctx - Domain context of the text plugin.
|
|
47964
|
+
* @param entity - The entity that got a `Text`.
|
|
47965
|
+
*/
|
|
47966
|
+
function noteAdded(ctx, entity) {
|
|
47967
|
+
ctx.state.dirty.add(entity);
|
|
47968
|
+
if (ctx.deps.world.ecs.mode() === "live") ctx.state.added.push(entity);
|
|
47969
|
+
}
|
|
47970
|
+
/**
|
|
47971
|
+
* The one system of the plugin: phase `layout`, before the two of `ui`, over every label. Every
|
|
47972
|
+
* label is visited every frame: `ui` writes `Text` after this system and change sets clear in
|
|
47973
|
+
* phase `signals`, so `changed(Text)` cannot tell which moved. The locale is read once per run,
|
|
47974
|
+
* the clock at most once and only when a countdown label asks. The labels that came before the run
|
|
47975
|
+
* are in its walk, so the list of the labels that came starts again empty: the step after the
|
|
47976
|
+
* layout flush takes only the ones the flush brings. The labels that step resolved last get their
|
|
47977
|
+
* `Text` written once more first, so `ui` measures the string they show.
|
|
47978
|
+
*
|
|
47979
|
+
* @param ctx - Domain context of the text plugin.
|
|
47980
|
+
* @param frame - What the runs share with the step after the layout flush.
|
|
47981
|
+
* @returns The system definition `world.ecs.system` takes.
|
|
47982
|
+
*/
|
|
47983
|
+
function createTextSystem(ctx, frame) {
|
|
47829
47984
|
return system({
|
|
47830
47985
|
name: TEXT_SYSTEM_NAME,
|
|
47831
47986
|
phase: "layout",
|
|
@@ -47833,11 +47988,42 @@ function createTextSystem(ctx) {
|
|
|
47833
47988
|
run: (entities) => {
|
|
47834
47989
|
frame.now = NaN;
|
|
47835
47990
|
frame.locale = ctx.deps.i18n.locale();
|
|
47991
|
+
forgetAdded(ctx.state.added);
|
|
47992
|
+
if (ctx.state.rewrite.length > 0) writeOnceMore(ctx);
|
|
47836
47993
|
for (const row of entities) stepText(ctx, row[0], row[1], frame);
|
|
47837
47994
|
}
|
|
47838
47995
|
});
|
|
47839
47996
|
}
|
|
47840
47997
|
/**
|
|
47998
|
+
* Resolves the labels whose `Text` came after the layout system ran. `ui` adds a label in its
|
|
47999
|
+
* solve, and the flush at the end of phase `layout` attaches it with the rest of its batch, a bound
|
|
48000
|
+
* component included; this runs right after that flush and before phase `sync`, so the renderer
|
|
48001
|
+
* builds the label with its string, and lists it, so the next system run writes its `Text` once
|
|
48002
|
+
* more for the remeasure of `ui`. A label whose content changed is not among them: it resolves in
|
|
48003
|
+
* the next layout phase, as before. While the world is not live its layout phase did not run: the
|
|
48004
|
+
* list is dropped, and the labels, marked, wait for the next live run, as every label does.
|
|
48005
|
+
*
|
|
48006
|
+
* @param ctx - Domain context of the text plugin.
|
|
48007
|
+
* @param frame - What the system's runs share.
|
|
48008
|
+
*/
|
|
48009
|
+
function resolveAdded(ctx, frame) {
|
|
48010
|
+
const added = ctx.state.added;
|
|
48011
|
+
if (added.length === 0) return;
|
|
48012
|
+
if (ctx.deps.world.ecs.mode() !== "live") {
|
|
48013
|
+
added.length = 0;
|
|
48014
|
+
return;
|
|
48015
|
+
}
|
|
48016
|
+
frame.now = NaN;
|
|
48017
|
+
frame.locale = ctx.deps.i18n.locale();
|
|
48018
|
+
for (const entity of added) {
|
|
48019
|
+
const text = ctx.deps.world.ecs.get(entity, Text);
|
|
48020
|
+
if (text === void 0) continue;
|
|
48021
|
+
stepText(ctx, entity, text, frame);
|
|
48022
|
+
ctx.state.rewrite.push(entity);
|
|
48023
|
+
}
|
|
48024
|
+
forgetAdded(added);
|
|
48025
|
+
}
|
|
48026
|
+
/**
|
|
47841
48027
|
* Marks labels for the next layout phase: every one of them, or only the ones `keep` names. A
|
|
47842
48028
|
* locale change names the labels whose content is a message; a dev hot swap names the labels
|
|
47843
48029
|
* drawn with a replaced font or icon.
|
|
@@ -48583,9 +48769,12 @@ function replaceStyles(ctx, styles) {
|
|
|
48583
48769
|
ctx.deps.time.wake();
|
|
48584
48770
|
}
|
|
48585
48771
|
/**
|
|
48586
|
-
* Opens what the plugin owns: the style table, the font keys, the layout system, the
|
|
48587
|
-
* hooks of `Text` and the display adapter. The
|
|
48588
|
-
*
|
|
48772
|
+
* Opens what the plugin owns: the style table, the font keys, the layout system, the step after
|
|
48773
|
+
* the layout flush, the two world hooks of `Text` and the display adapter. The step is a frame
|
|
48774
|
+
* callback of phase `layout`: `world` registered its own in its `onStart`, which runs before this
|
|
48775
|
+
* one (`text` depends on `world`), so the step runs after the world's layout phase and its flush,
|
|
48776
|
+
* and before phase `sync`. The fonts of a bundle that is already loaded are read at once, because
|
|
48777
|
+
* the boot bundle lands before this runs.
|
|
48589
48778
|
*
|
|
48590
48779
|
* @param ctx - Kernel context of the text plugin.
|
|
48591
48780
|
* @throws {Error} When two features define the same style name.
|
|
@@ -48593,18 +48782,18 @@ function replaceStyles(ctx, styles) {
|
|
|
48593
48782
|
function startText(ctx) {
|
|
48594
48783
|
const tctx = withDeps$1(ctx);
|
|
48595
48784
|
const ecs = tctx.deps.world.ecs;
|
|
48785
|
+
const frame = createTextFrame(tctx);
|
|
48596
48786
|
buildStyles(tctx);
|
|
48597
48787
|
collectFonts(tctx);
|
|
48598
|
-
tctx.state.removers.push(ecs.system(createTextSystem(tctx)), ecs.onAdded(Text, (entity) => {
|
|
48599
|
-
tctx.state.dirty.add(entity);
|
|
48600
|
-
}), ecs.onRemoved(Text, (entity) => {
|
|
48788
|
+
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) => {
|
|
48601
48789
|
forgetText(tctx.state, entity);
|
|
48602
48790
|
}), tctx.deps.renderer.sync.displays.provide(Text, createTextAdapter(tctx)));
|
|
48603
48791
|
installFonts(tctx);
|
|
48604
48792
|
}
|
|
48605
48793
|
/**
|
|
48606
|
-
* Closes what the plugin opened: the system, the
|
|
48607
|
-
* objects the adapter built are the renderer's, and it frees them in its
|
|
48794
|
+
* Closes what the plugin opened: the system, the step after the layout flush, the two world hooks
|
|
48795
|
+
* and the adapter. The Pixi objects the adapter built are the renderer's, and it frees them in its
|
|
48796
|
+
* own teardown.
|
|
48608
48797
|
*
|
|
48609
48798
|
* @param state - The plugin state, the only thing a teardown context carries.
|
|
48610
48799
|
*/
|
|
@@ -48621,6 +48810,8 @@ function stopText(state) {
|
|
|
48621
48810
|
state.measured.clear();
|
|
48622
48811
|
state.cache.clear();
|
|
48623
48812
|
state.dirty.clear();
|
|
48813
|
+
state.added.length = 0;
|
|
48814
|
+
state.rewrite.length = 0;
|
|
48624
48815
|
state.warned.clear();
|
|
48625
48816
|
}
|
|
48626
48817
|
//#endregion
|
|
@@ -48770,6 +48961,8 @@ function createTextState$1() {
|
|
|
48770
48961
|
cache: emptyMap$2(),
|
|
48771
48962
|
drawn: emptyWeakMap(),
|
|
48772
48963
|
dirty: emptySet$1(),
|
|
48964
|
+
added: [],
|
|
48965
|
+
rewrite: [],
|
|
48773
48966
|
generation: 0,
|
|
48774
48967
|
warned: emptySet$1(),
|
|
48775
48968
|
removers: []
|
|
@@ -53470,16 +53663,6 @@ function entitySet(entities) {
|
|
|
53470
53663
|
return new Set(entities);
|
|
53471
53664
|
}
|
|
53472
53665
|
/**
|
|
53473
|
-
* Takes the hold of the feature a root shows, when the root belongs to a feature.
|
|
53474
|
-
*
|
|
53475
|
-
* @param ctx - Domain context of the ui plugin.
|
|
53476
|
-
* @param feature - The feature of the root's component or projection, if any.
|
|
53477
|
-
* @returns The release, or `undefined` for a root of no feature.
|
|
53478
|
-
*/
|
|
53479
|
-
function holdFor(ctx, feature) {
|
|
53480
|
-
return feature === void 0 ? void 0 : ctx.deps.world.ecs.hold(feature);
|
|
53481
|
-
}
|
|
53482
|
-
/**
|
|
53483
53666
|
* Builds the reconcile half of the jsx module: the two systems of the frame and the root
|
|
53484
53667
|
* bookkeeping around them.
|
|
53485
53668
|
*
|
|
@@ -53511,6 +53694,7 @@ function createReconciler(ctx, modules, fields) {
|
|
|
53511
53694
|
};
|
|
53512
53695
|
let slotsVersion = -1;
|
|
53513
53696
|
let slotsRoots = -1;
|
|
53697
|
+
let placedUnder;
|
|
53514
53698
|
/**
|
|
53515
53699
|
* Registers the key of an element under its root, so `entityOf` and `find` answer for it. A key
|
|
53516
53700
|
* `ui` gave a node of its own, a spacer's, is never registered.
|
|
@@ -54084,7 +54268,6 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54084
54268
|
if (root === void 0) continue;
|
|
54085
54269
|
if (root.element !== void 0 && state.elements.has(root.element)) continue;
|
|
54086
54270
|
state.removing.delete(entity);
|
|
54087
|
-
root.held?.();
|
|
54088
54271
|
state.roots.delete(entity);
|
|
54089
54272
|
state.byKey.delete(entity);
|
|
54090
54273
|
ecs.despawn(entity);
|
|
@@ -54502,21 +54685,27 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54502
54685
|
if (element?.live === true && root.slot === void 0) ecs.set(element.entity, Order, { value: order });
|
|
54503
54686
|
}
|
|
54504
54687
|
/**
|
|
54505
|
-
*
|
|
54506
|
-
* projection
|
|
54507
|
-
*
|
|
54508
|
-
*
|
|
54509
|
-
|
|
54688
|
+
* Gives every root the order its entity holds once a new scene stands: a scene that keeps a
|
|
54689
|
+
* projection both scenes list places it again (F23), and its tree stands, so no root is dirty.
|
|
54690
|
+
* The world places it only under a new layer list, so the walk runs once per new list, in the
|
|
54691
|
+
* first layout phase that runs after it, also when the switch came while the world was paused.
|
|
54692
|
+
*/
|
|
54693
|
+
function reorderRoots() {
|
|
54694
|
+
const layers = ctx.deps.world.projection.layers();
|
|
54695
|
+
if (layers === placedUnder) return;
|
|
54696
|
+
placedUnder = layers;
|
|
54697
|
+
for (const root of state.roots.values()) if (!state.removing.has(root.entity)) refreshRootOrder(root);
|
|
54698
|
+
}
|
|
54699
|
+
/**
|
|
54700
|
+
* Reconciles one dirty root: reads its current tree and projection name, then diffs it. The root
|
|
54701
|
+
* stays dirty while the layout engine is not loaded, and a failure is logged for this root only.
|
|
54510
54702
|
*
|
|
54511
54703
|
* @param root - A root with work this frame.
|
|
54512
54704
|
*/
|
|
54513
54705
|
function reconcileDirtyRoot(root) {
|
|
54514
54706
|
const tree = ecs.get(root.entity, Tree);
|
|
54515
54707
|
const place = ctx.deps.world.projection.keyOf(root.entity);
|
|
54516
|
-
if (place !== void 0)
|
|
54517
|
-
root.name = place.projection;
|
|
54518
|
-
if (root.popup === void 0) root.held ??= holdFor(ctx, state.featureOfProjection.get(place.projection));
|
|
54519
|
-
}
|
|
54708
|
+
if (place !== void 0) root.name = place.projection;
|
|
54520
54709
|
if (tree !== void 0) root.tree = tree.node;
|
|
54521
54710
|
refreshRootOrder(root);
|
|
54522
54711
|
root.dirty = !modules.layout.loaded();
|
|
@@ -54529,14 +54718,15 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54529
54718
|
}
|
|
54530
54719
|
/**
|
|
54531
54720
|
* The first of the two systems of phase `layout`: the sweep, the roots a restore asked to build
|
|
54532
|
-
* again, the popups that may leave, the pull of the text being
|
|
54533
|
-
* windowed ranges, the bound texts that show a string of
|
|
54534
|
-
* roots and the hosted views.
|
|
54721
|
+
* again, the roots a scene placed again, the popups that may leave, the pull of the text being
|
|
54722
|
+
* typed, the scroll offsets and the windowed ranges, the bound texts that show a string of
|
|
54723
|
+
* another size, the viewport, the dirty roots and the hosted views.
|
|
54535
54724
|
*/
|
|
54536
54725
|
function reconcile() {
|
|
54537
54726
|
sweep();
|
|
54538
54727
|
state.reconciles += 1;
|
|
54539
54728
|
if (state.remount) remountRoots();
|
|
54729
|
+
reorderRoots();
|
|
54540
54730
|
settlePopups(ctx, state, unmountRoot);
|
|
54541
54731
|
fields.pull();
|
|
54542
54732
|
const containers = scrollContainers();
|
|
@@ -54593,10 +54783,7 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54593
54783
|
/**
|
|
54594
54784
|
* Records a root: a projection view that returned a tree, or a popup this plugin spawned. The
|
|
54595
54785
|
* `Tree` hook and the popup handler both call it, in either order, so a second call fills in
|
|
54596
|
-
* what the first one could not know.
|
|
54597
|
-
* first recorded by the `Tree` hook takes that hold when the popup handler names the component,
|
|
54598
|
-
* and a reclaimed root keeps the hold it has. A projection root takes its projection's hold at
|
|
54599
|
-
* its first reconcile.
|
|
54786
|
+
* what the first one could not know.
|
|
54600
54787
|
*
|
|
54601
54788
|
* @param entity - The entity that carries `Tree`.
|
|
54602
54789
|
* @param name - The projection name, or the component name of a popup.
|
|
@@ -54612,7 +54799,6 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54612
54799
|
known.popup = popup;
|
|
54613
54800
|
known.name = name;
|
|
54614
54801
|
known.cause = "change";
|
|
54615
|
-
known.held ??= holdFor(ctx, state.featureOfComponent.get(name));
|
|
54616
54802
|
}
|
|
54617
54803
|
return;
|
|
54618
54804
|
}
|
|
@@ -54636,8 +54822,7 @@ function createReconciler(ctx, modules, fields) {
|
|
|
54636
54822
|
cause: popup === void 0 ? void 0 : "change",
|
|
54637
54823
|
slot: void 0,
|
|
54638
54824
|
slotRect: void 0,
|
|
54639
|
-
pending: false
|
|
54640
|
-
held: popup === void 0 ? void 0 : holdFor(ctx, state.featureOfComponent.get(name))
|
|
54825
|
+
pending: false
|
|
54641
54826
|
});
|
|
54642
54827
|
}
|
|
54643
54828
|
/**
|
|
@@ -56872,45 +57057,14 @@ function isComponent(entry) {
|
|
|
56872
57057
|
return typeof entry === "function" && entry.isUiComponent === true;
|
|
56873
57058
|
}
|
|
56874
57059
|
/**
|
|
56875
|
-
*
|
|
56876
|
-
*
|
|
56877
|
-
* @param entry - What the feature put under its `projections` key.
|
|
56878
|
-
* @returns The name, or `undefined` when the entry carries none.
|
|
56879
|
-
* @example
|
|
56880
|
-
* ```ts
|
|
56881
|
-
* projectionName({ name: "shop.sheet", layer: "ui" }); // "shop.sheet"
|
|
56882
|
-
* ```
|
|
56883
|
-
*/
|
|
56884
|
-
function projectionName(entry) {
|
|
56885
|
-
if (typeof entry !== "object" || entry === null) return void 0;
|
|
56886
|
-
const name = entry.name;
|
|
56887
|
-
return typeof name === "string" ? name : void 0;
|
|
56888
|
-
}
|
|
56889
|
-
/**
|
|
56890
|
-
* Fills the component registry from the `ui` key of every feature, in feature order, and records
|
|
56891
|
-
* which feature each component and each projection belongs to: a root holds that feature while it
|
|
56892
|
-
* is shown, so the feature's systems run with `systemsRun: "active"`. Two tables, because a
|
|
56893
|
-
* component and a projection may share a name.
|
|
57060
|
+
* Fills the component registry from the `ui` key of every feature, in feature order.
|
|
56894
57061
|
*
|
|
56895
57062
|
* @param ctx - Domain context of the ui plugin.
|
|
56896
57063
|
* @param jsx - The jsx module, which owns the registry.
|
|
56897
57064
|
* @throws {Error} When two features define the same component name.
|
|
56898
57065
|
*/
|
|
56899
57066
|
function registerComponents(ctx, jsx) {
|
|
56900
|
-
const
|
|
56901
|
-
for (const feature of ctx.deps.flow.features.all()) {
|
|
56902
|
-
for (const entry of feature.description.ui ?? []) {
|
|
56903
|
-
if (!isComponent(entry)) continue;
|
|
56904
|
-
jsx.register(entry);
|
|
56905
|
-
featureOfComponent.set(entry.name, feature.name);
|
|
56906
|
-
}
|
|
56907
|
-
const projections = feature.description.projections;
|
|
56908
|
-
if (!Array.isArray(projections)) continue;
|
|
56909
|
-
for (const entry of projections) {
|
|
56910
|
-
const name = projectionName(entry);
|
|
56911
|
-
if (name !== void 0) featureOfProjection.set(name, feature.name);
|
|
56912
|
-
}
|
|
56913
|
-
}
|
|
57067
|
+
for (const feature of ctx.deps.flow.features.all()) for (const entry of feature.description.ui ?? []) if (isComponent(entry)) jsx.register(entry);
|
|
56914
57068
|
}
|
|
56915
57069
|
/**
|
|
56916
57070
|
* Opens the two systems of phase `layout`, the seven world hooks of `Tree`, `Box`, `Pressed` and
|
|
@@ -56990,8 +57144,6 @@ function stopUi(state) {
|
|
|
56990
57144
|
state.layout.scrolling = void 0;
|
|
56991
57145
|
state.layout.iconLine = void 0;
|
|
56992
57146
|
state.jsx.components.clear();
|
|
56993
|
-
state.jsx.featureOfComponent.clear();
|
|
56994
|
-
state.jsx.featureOfProjection.clear();
|
|
56995
57147
|
state.jsx.roots.clear();
|
|
56996
57148
|
state.jsx.elements.clear();
|
|
56997
57149
|
state.jsx.byIdentity.clear();
|
|
@@ -57387,9 +57539,7 @@ function createJsxState() {
|
|
|
57387
57539
|
written: 0,
|
|
57388
57540
|
debugTaps: [],
|
|
57389
57541
|
debugWarned: false,
|
|
57390
|
-
overlays: emptySet()
|
|
57391
|
-
featureOfComponent: emptyMap$1(),
|
|
57392
|
-
featureOfProjection: emptyMap$1()
|
|
57542
|
+
overlays: emptySet()
|
|
57393
57543
|
};
|
|
57394
57544
|
}
|
|
57395
57545
|
//#endregion
|
|
@@ -58135,18 +58285,6 @@ function assertFeatures(name, children) {
|
|
|
58135
58285
|
for (const [index, child] of children.entries()) if (typeof child?.logicOnly !== "object" || !Array.isArray(child.features)) throw notAFeature(`Feature "${name}"`, `features[${index}]`);
|
|
58136
58286
|
}
|
|
58137
58287
|
/**
|
|
58138
|
-
* Throws for a `systemsRun` outside its type. The type already says so; a game in JavaScript, or
|
|
58139
|
-
* a factory that passes a string through, is caught here, before the world reads it.
|
|
58140
|
-
*
|
|
58141
|
-
* @param name - The feature that declares it.
|
|
58142
|
-
* @param value - What the description holds under `systemsRun`.
|
|
58143
|
-
* @throws {Error} When the value is present and neither `"always"` nor `"active"`.
|
|
58144
|
-
*/
|
|
58145
|
-
function assertSystemsRun(name, value) {
|
|
58146
|
-
if (value === void 0 || value === "always" || value === "active") return;
|
|
58147
|
-
throw new Error(`[game] Feature "${name}" has systemsRun "${String(value)}".\n Use "always" (the default) or "active".`);
|
|
58148
|
-
}
|
|
58149
|
-
/**
|
|
58150
58288
|
* Builds the plugin of one feature: it depends on `flow`, checks in `onInit` that the app composes
|
|
58151
58289
|
* every declared sub-feature, then registers its description, before any plugin above reads the
|
|
58152
58290
|
* registry in `onStart`.
|
|
@@ -58185,14 +58323,12 @@ function featurePluginOf(name, description, children) {
|
|
|
58185
58323
|
* same plugin built from the V1 keys of the description, for headless tests. A feature that
|
|
58186
58324
|
* declares `features` is a parent: its `onInit` throws when one of them is not composed. The
|
|
58187
58325
|
* plugin carries them in `features`, its `logicOnly` carries their `logicOnly`.
|
|
58188
|
-
* `systemsRun: "active"` runs the feature's systems only while it is active (see `SystemsRun`).
|
|
58189
58326
|
*
|
|
58190
58327
|
* @param name - Feature name. Shares the namespace with plugin names.
|
|
58191
58328
|
* @param description - What the feature brings: nodes, flows, slot contributions, sub-features.
|
|
58192
58329
|
* @returns The feature plugin, with its `logicOnly` twin.
|
|
58193
|
-
* @throws {Error} When the name is reserved or belongs to an engine plugin, for an entry of
|
|
58194
|
-
* `features` that `defineFeature` did not make
|
|
58195
|
-
* `"always"` nor `"active"`.
|
|
58330
|
+
* @throws {Error} When the name is reserved or belongs to an engine plugin, and for an entry of
|
|
58331
|
+
* `features` that `defineFeature` did not make.
|
|
58196
58332
|
* @example
|
|
58197
58333
|
* ```ts
|
|
58198
58334
|
* // The reward popup is a feature: the main flow only declares slot("afterOrder").
|
|
@@ -58207,7 +58343,7 @@ function featurePluginOf(name, description, children) {
|
|
|
58207
58343
|
*/
|
|
58208
58344
|
function defineFeature(name, description) {
|
|
58209
58345
|
if (takenNames().has(name)) throw new Error(`[game] Feature name "${name}" is reserved or already a plugin.\n Choose another feature name.`);
|
|
58210
|
-
|
|
58346
|
+
assertNoSystemsRun(name, description);
|
|
58211
58347
|
const children = Object.freeze([...description.features ?? []]);
|
|
58212
58348
|
assertFeatures(name, children);
|
|
58213
58349
|
const logicOnly = Object.assign(featurePluginOf(name, logicKeysOf(description), children), { features: Object.freeze(children.map((child) => child.logicOnly)) });
|
|
@@ -58217,6 +58353,18 @@ function defineFeature(name, description) {
|
|
|
58217
58353
|
});
|
|
58218
58354
|
}
|
|
58219
58355
|
/**
|
|
58356
|
+
* Throws for a feature that still writes `systemsRun`, removed after 0.15.3: the type refuses it,
|
|
58357
|
+
* this catches a game in JavaScript or a factory that passes the key through.
|
|
58358
|
+
*
|
|
58359
|
+
* @param name - Feature name.
|
|
58360
|
+
* @param description - What the feature brings.
|
|
58361
|
+
* @throws {Error} When the description carries `systemsRun`.
|
|
58362
|
+
*/
|
|
58363
|
+
function assertNoSystemsRun(name, description) {
|
|
58364
|
+
if (!("systemsRun" in description)) return;
|
|
58365
|
+
throw new Error(`[game] Feature "${name}" sets systemsRun, which the engine no longer has.\n Delete it: a system whose query has no entity is not run (world README, "When a system runs").`);
|
|
58366
|
+
}
|
|
58367
|
+
/**
|
|
58220
58368
|
* Tells whether an entry of a feature tree carries its `features` list, as every plugin of
|
|
58221
58369
|
* `defineFeature` does. A plain plugin, a cast object or a hole in the array does not.
|
|
58222
58370
|
*
|