@jgengine/core 0.8.0 → 0.9.0
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/CHANGELOG.md +16 -0
- package/dist/ai/flock.d.ts +36 -0
- package/dist/ai/flock.js +182 -0
- package/dist/ai/groupAssist.d.ts +19 -0
- package/dist/ai/groupAssist.js +59 -0
- package/dist/ai/laneSelect.d.ts +21 -0
- package/dist/ai/laneSelect.js +37 -0
- package/dist/ai/mobBrain.d.ts +68 -0
- package/dist/ai/mobBrain.js +115 -0
- package/dist/ai/spawnDirector.js +1 -1
- package/dist/ai/threat.d.ts +2 -0
- package/dist/ai/threat.js +34 -0
- package/dist/cartridge/runtime.d.ts +15 -0
- package/dist/cartridge/runtime.js +556 -0
- package/dist/cartridge/spec.d.ts +188 -0
- package/dist/cartridge/spec.js +19 -0
- package/dist/cartridge/validate.d.ts +2 -0
- package/dist/cartridge/validate.js +78 -0
- package/dist/combat/abilityKit.d.ts +17 -2
- package/dist/combat/abilityKit.js +62 -13
- package/dist/combat/castRunner.d.ts +40 -0
- package/dist/combat/castRunner.js +56 -0
- package/dist/combat/comboPoints.d.ts +14 -0
- package/dist/combat/comboPoints.js +52 -0
- package/dist/combat/defensiveWindow.d.ts +1 -1
- package/dist/combat/defensiveWindow.js +5 -3
- package/dist/combat/projectiles.d.ts +33 -12
- package/dist/combat/projectiles.js +201 -129
- package/dist/combat/resistance.d.ts +16 -4
- package/dist/combat/resistance.js +43 -7
- package/dist/combat/resourcePool.d.ts +20 -0
- package/dist/combat/resourcePool.js +55 -0
- package/dist/combat/shotOrigin.d.ts +40 -0
- package/dist/combat/shotOrigin.js +57 -0
- package/dist/combat/telegraph.d.ts +25 -0
- package/dist/combat/telegraph.js +47 -0
- package/dist/crafting/recipe.js +4 -1
- package/dist/devtools/devtools.d.ts +72 -24
- package/dist/devtools/devtools.js +449 -101
- package/dist/devtools/transformTunables.js +3 -3
- package/dist/devtools/tunableSchema.d.ts +140 -0
- package/dist/devtools/tunableSchema.js +476 -0
- package/dist/faction/factions.d.ts +33 -0
- package/dist/faction/factions.js +76 -0
- package/dist/faction/reputation.d.ts +32 -0
- package/dist/faction/reputation.js +82 -0
- package/dist/game/chat.d.ts +2 -0
- package/dist/game/chat.js +9 -1
- package/dist/game/chatFilter.d.ts +15 -0
- package/dist/game/chatFilter.js +55 -0
- package/dist/game/controlGate.d.ts +4 -0
- package/dist/game/controlGate.js +7 -0
- package/dist/game/defineGame.d.ts +2 -0
- package/dist/game/events.d.ts +10 -0
- package/dist/game/gamePhase.d.ts +20 -0
- package/dist/game/gamePhase.js +21 -0
- package/dist/game/lootTable.d.ts +10 -1
- package/dist/game/lootTable.js +27 -7
- package/dist/game/playableGame.d.ts +50 -1
- package/dist/game/playableGame.js +1 -1
- package/dist/game/quest.d.ts +7 -3
- package/dist/game/quest.js +17 -8
- package/dist/game/race.d.ts +39 -1
- package/dist/game/race.js +96 -11
- package/dist/game/recordBook.d.ts +29 -0
- package/dist/game/recordBook.js +86 -0
- package/dist/game/talents.d.ts +47 -0
- package/dist/game/talents.js +140 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/input/axisInput.d.ts +26 -3
- package/dist/input/axisInput.js +44 -4
- package/dist/input/bindingOverrides.d.ts +25 -0
- package/dist/input/bindingOverrides.js +95 -0
- package/dist/input/pointerAxis.d.ts +30 -0
- package/dist/input/pointerAxis.js +31 -0
- package/dist/input/touchScheme.d.ts +6 -0
- package/dist/input/touchScheme.js +45 -7
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +23 -1
- package/dist/movement/dash.d.ts +3 -0
- package/dist/movement/dash.js +19 -5
- package/dist/movement/glideModel.d.ts +44 -0
- package/dist/movement/glideModel.js +54 -0
- package/dist/movement/grappleSwing.d.ts +34 -0
- package/dist/movement/grappleSwing.js +56 -0
- package/dist/movement/leaderTrail.d.ts +29 -0
- package/dist/movement/leaderTrail.js +67 -0
- package/dist/movement/steering.d.ts +27 -0
- package/dist/movement/steering.js +40 -0
- package/dist/multiplayer/feedWriteGate.d.ts +10 -0
- package/dist/multiplayer/feedWriteGate.js +12 -0
- package/dist/multiplayer/presenceModel.d.ts +14 -2
- package/dist/multiplayer/presenceModel.js +15 -3
- package/dist/nav/corridors.d.ts +30 -0
- package/dist/nav/corridors.js +69 -0
- package/dist/nav/navGrid.js +30 -4
- package/dist/nav/railGraph.d.ts +66 -0
- package/dist/nav/railGraph.js +129 -0
- package/dist/nav/timetable.d.ts +41 -0
- package/dist/nav/timetable.js +94 -0
- package/dist/physics/buoyancy.d.ts +6 -0
- package/dist/physics/buoyancy.js +14 -0
- package/dist/physics/flowTube.d.ts +32 -0
- package/dist/physics/flowTube.js +64 -0
- package/dist/physics/forceVolume.d.ts +27 -0
- package/dist/physics/forceVolume.js +38 -0
- package/dist/physics/kinematicVehicle.d.ts +52 -0
- package/dist/physics/kinematicVehicle.js +81 -0
- package/dist/physics/physicsWorld.d.ts +13 -6
- package/dist/physics/physicsWorld.js +169 -40
- package/dist/physics/ragdoll.js +1 -1
- package/dist/physics/vehicleBody.js +2 -1
- package/dist/random/seedLink.d.ts +5 -0
- package/dist/random/seedLink.js +35 -0
- package/dist/runtime/cameraDirector.d.ts +6 -1
- package/dist/runtime/cameraDirector.js +6 -0
- package/dist/runtime/commandRunner.d.ts +2 -2
- package/dist/runtime/commandRunner.js +2 -2
- package/dist/runtime/gameContext.d.ts +21 -0
- package/dist/runtime/gameContext.js +184 -9
- package/dist/runtime/gameRuntime.d.ts +5 -1
- package/dist/runtime/gameRuntime.js +13 -12
- package/dist/runtime/hostPersistence.d.ts +9 -1
- package/dist/runtime/hostPersistence.js +23 -8
- package/dist/runtime/inputSnapshot.d.ts +5 -0
- package/dist/runtime/inputSnapshot.js +5 -0
- package/dist/runtime/motionIntents.d.ts +14 -6
- package/dist/runtime/motionIntents.js +20 -2
- package/dist/runtime/snapshot.d.ts +1 -0
- package/dist/runtime/snapshot.js +1 -1
- package/dist/runtime/visibility.d.ts +74 -0
- package/dist/runtime/visibility.js +127 -0
- package/dist/scene/colliders.d.ts +44 -0
- package/dist/scene/colliders.js +77 -0
- package/dist/scene/entityStore.d.ts +22 -5
- package/dist/scene/entityStore.js +59 -12
- package/dist/scene/objectQuery.d.ts +13 -2
- package/dist/scene/objectQuery.js +34 -2
- package/dist/scene/objectStore.d.ts +2 -0
- package/dist/scene/objectStore.js +31 -12
- package/dist/scene/sceneRaycast.d.ts +80 -0
- package/dist/scene/sceneRaycast.js +284 -0
- package/dist/scene/spatial.d.ts +11 -16
- package/dist/scene/spatial.js +99 -38
- package/dist/sensor/recordingBuffer.d.ts +6 -0
- package/dist/sensor/recordingBuffer.js +7 -0
- package/dist/sensor/replayLoop.d.ts +39 -0
- package/dist/sensor/replayLoop.js +64 -0
- package/dist/sensor/visionCone.d.ts +31 -0
- package/dist/sensor/visionCone.js +72 -0
- package/dist/session/extraction.d.ts +2 -2
- package/dist/session/extraction.js +13 -2
- package/dist/settings/settingsModel.d.ts +97 -0
- package/dist/settings/settingsModel.js +100 -0
- package/dist/stats/statModifiers.d.ts +5 -2
- package/dist/stats/statModifiers.js +32 -14
- package/dist/store/observableKeyedStore.js +8 -2
- package/dist/survival/decayMeter.d.ts +1 -1
- package/dist/survival/decayMeter.js +0 -1
- package/dist/time/beatClock.d.ts +18 -0
- package/dist/time/beatClock.js +24 -0
- package/dist/time/idleProgress.d.ts +29 -0
- package/dist/time/idleProgress.js +31 -0
- package/dist/time/serverTick.d.ts +7 -1
- package/dist/time/serverTick.js +27 -4
- package/dist/time/stateSchedule.d.ts +57 -0
- package/dist/time/stateSchedule.js +121 -0
- package/dist/turn/commit.d.ts +2 -2
- package/dist/turn/commit.js +2 -2
- package/dist/ui/hudLayout.d.ts +58 -0
- package/dist/ui/hudLayout.js +207 -0
- package/dist/ui/hudScale.d.ts +44 -0
- package/dist/ui/hudScale.js +55 -0
- package/dist/visibility/assetStreaming.d.ts +66 -0
- package/dist/visibility/assetStreaming.js +201 -0
- package/dist/visibility/bounds.d.ts +66 -0
- package/dist/visibility/bounds.js +113 -0
- package/dist/visibility/camera.d.ts +21 -0
- package/dist/visibility/camera.js +3 -0
- package/dist/visibility/config.d.ts +21 -0
- package/dist/visibility/config.js +1 -0
- package/dist/visibility/diagnostics.d.ts +26 -0
- package/dist/visibility/diagnostics.js +36 -0
- package/dist/visibility/distance.d.ts +11 -0
- package/dist/visibility/distance.js +30 -0
- package/dist/visibility/frustum.d.ts +47 -0
- package/dist/visibility/frustum.js +186 -0
- package/dist/visibility/occlusion.d.ts +36 -0
- package/dist/visibility/occlusion.js +45 -0
- package/dist/visibility/settings.d.ts +75 -0
- package/dist/visibility/settings.js +49 -0
- package/dist/visibility/simulationCulling.d.ts +34 -0
- package/dist/visibility/simulationCulling.js +36 -0
- package/dist/visibility/spatialIndex.d.ts +45 -0
- package/dist/visibility/spatialIndex.js +198 -0
- package/dist/visibility/visibilitySystem.d.ts +90 -0
- package/dist/visibility/visibilitySystem.js +257 -0
- package/dist/world/buildings.d.ts +6 -0
- package/dist/world/buildings.js +152 -0
- package/dist/world/carve.d.ts +7 -2
- package/dist/world/carve.js +17 -7
- package/dist/world/cellStates.d.ts +45 -0
- package/dist/world/cellStates.js +103 -0
- package/dist/world/environmentSummary.d.ts +10 -2
- package/dist/world/environmentSummary.js +11 -2
- package/dist/world/features.d.ts +56 -8
- package/dist/world/features.js +24 -3
- package/dist/world/gridInstances.d.ts +7 -8
- package/dist/world/gridInstances.js +145 -7
- package/dist/world/lod.d.ts +17 -0
- package/dist/world/lod.js +66 -0
- package/dist/world/mapLayers.d.ts +60 -0
- package/dist/world/mapLayers.js +44 -0
- package/dist/world/massing.d.ts +45 -0
- package/dist/world/massing.js +581 -0
- package/dist/world/minimap.d.ts +13 -1
- package/dist/world/minimap.js +21 -2
- package/dist/world/terrain.d.ts +42 -3
- package/dist/world/terrain.js +197 -16
- package/dist/world/water.d.ts +7 -0
- package/dist/world/water.js +19 -4
- package/dist/world/weather.d.ts +3 -18
- package/dist/world/weather.js +4 -17
- package/dist/world/windZones.d.ts +47 -0
- package/dist/world/windZones.js +59 -0
- package/llms.txt +2585 -1704
- package/package.json +12 -2
|
@@ -1,32 +1,20 @@
|
|
|
1
1
|
import { createChangeSignal } from "../store/changeSignal.js";
|
|
2
|
+
import { CONTROL_SCHEMA_VERSION, MAX_SCAN_DEPTH, MAX_SCAN_TARGETS, MAX_TABLE_ENTRIES, OVERRIDES_FORMAT_VERSION, choiceValues, cloneValue, discoverableKind, expandAxisMeta, inferKind, isScannableContainer, joinTunablePath, normalizeColorValue, ownWritableDataKeys, parseOverridesPayload, resolveChoices, sliderBounds, validateControlValue, } from "./tunableSchema.js";
|
|
3
|
+
export { CONTROL_SCHEMA_VERSION, OVERRIDES_FORMAT_VERSION, convertAngle, escapePathSegment, formatColor, joinTunablePath, normalizeAngle, normalizeColorValue, parseColor, parseOverridesPayload, splitTunablePath, unescapePathSegment, validateControlValue, } from "./tunableSchema.js";
|
|
2
4
|
const FRAME_CAPACITY = 240;
|
|
3
5
|
const RECENT_FRAMES = 60;
|
|
4
|
-
const LONG_FRAME_MS = 33.4;
|
|
6
|
+
export const LONG_FRAME_MS = 33.4;
|
|
5
7
|
const LATENCY_CAPACITY = 120;
|
|
6
8
|
const LOG_CAPACITY = 200;
|
|
7
9
|
const LOG_MESSAGE_MAX = 400;
|
|
8
|
-
const
|
|
10
|
+
const LONG_FRAME_CAPACITY = 40;
|
|
11
|
+
const MAX_PHASES_PER_FRAME = 24;
|
|
12
|
+
const MAX_LONG_FRAME_PHASES = 8;
|
|
13
|
+
const PHASE_HOT_MS = 2;
|
|
9
14
|
function nowMs() {
|
|
10
15
|
const perf = globalThis.performance;
|
|
11
16
|
return perf !== undefined ? perf.now() : Date.now();
|
|
12
17
|
}
|
|
13
|
-
function inferKind(initial, hasOptions) {
|
|
14
|
-
if (hasOptions)
|
|
15
|
-
return "select";
|
|
16
|
-
if (typeof initial === "number")
|
|
17
|
-
return "slider";
|
|
18
|
-
if (typeof initial === "boolean")
|
|
19
|
-
return "toggle";
|
|
20
|
-
if (typeof initial === "string" && COLOR_PATTERN.test(initial))
|
|
21
|
-
return "color";
|
|
22
|
-
return "text";
|
|
23
|
-
}
|
|
24
|
-
function sliderBounds(initial, options) {
|
|
25
|
-
const min = options?.min ?? (initial < 0 ? initial * 2 : 0);
|
|
26
|
-
const max = options?.max ?? (initial > 0 ? initial * 2 : initial < 0 ? 0 : 1);
|
|
27
|
-
const step = options?.step ?? (max - min) / 100;
|
|
28
|
-
return { min, max, step };
|
|
29
|
-
}
|
|
30
18
|
function formatLogArg(arg) {
|
|
31
19
|
if (typeof arg === "string")
|
|
32
20
|
return arg;
|
|
@@ -49,45 +37,148 @@ function percentile(sorted, fraction) {
|
|
|
49
37
|
const index = Math.min(sorted.length - 1, Math.floor(sorted.length * fraction));
|
|
50
38
|
return sorted[index];
|
|
51
39
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
function clampMs(value) {
|
|
41
|
+
return value > 0 && Number.isFinite(value) ? value : 0;
|
|
42
|
+
}
|
|
43
|
+
function rankPhases(phases) {
|
|
44
|
+
const entries = Object.entries(phases)
|
|
45
|
+
.filter(([, ms]) => Number.isFinite(ms) && ms > 0)
|
|
46
|
+
.map(([name, ms]) => ({ name, ms }))
|
|
47
|
+
.sort((a, b) => b.ms - a.ms)
|
|
48
|
+
.slice(0, MAX_LONG_FRAME_PHASES);
|
|
49
|
+
const total = entries.reduce((sum, entry) => sum + entry.ms, 0);
|
|
50
|
+
return entries.map((entry) => ({
|
|
51
|
+
name: entry.name,
|
|
52
|
+
ms: entry.ms,
|
|
53
|
+
pct: total > 0 ? (entry.ms / total) * 100 : 0,
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
function explainLongFrame(frameMs, simMs, outsideMs, ranked) {
|
|
57
|
+
const top = ranked[0] ?? null;
|
|
58
|
+
const topPhaseMs = top?.ms ?? 0;
|
|
59
|
+
const phaseBits = ranked
|
|
60
|
+
.filter((entry) => entry.ms >= PHASE_HOT_MS)
|
|
61
|
+
.slice(0, 3)
|
|
62
|
+
.map((entry) => `${entry.name} ${entry.ms.toFixed(1)}ms`);
|
|
63
|
+
const outsideDominates = outsideMs > topPhaseMs && outsideMs >= simMs * 0.55 && outsideMs >= frameMs * 0.4;
|
|
64
|
+
if (outsideDominates) {
|
|
65
|
+
const detail = phaseBits.length > 0 ? ` · sim was ${phaseBits.join(", ")}` : simMs > 1 ? ` · sim ${simMs.toFixed(1)}ms` : "";
|
|
66
|
+
return {
|
|
67
|
+
topPhase: top?.name ?? null,
|
|
68
|
+
culprit: "outside-sim",
|
|
69
|
+
reason: `display/render/browser hitch ${outsideMs.toFixed(1)}ms outside sim${detail}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (top !== null && top.ms >= PHASE_HOT_MS) {
|
|
73
|
+
const rest = phaseBits.slice(1);
|
|
74
|
+
const restText = rest.length > 0 ? ` · also ${rest.join(", ")}` : "";
|
|
75
|
+
const outsideText = outsideMs >= PHASE_HOT_MS ? ` · +${outsideMs.toFixed(1)}ms outside sim` : "";
|
|
76
|
+
return {
|
|
77
|
+
topPhase: top.name,
|
|
78
|
+
culprit: top.name,
|
|
79
|
+
reason: `${top.name} ${top.ms.toFixed(1)}ms (${top.pct.toFixed(0)}% of timed sim)${restText}${outsideText}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (simMs >= frameMs * 0.5) {
|
|
83
|
+
return {
|
|
84
|
+
topPhase: null,
|
|
85
|
+
culprit: "sim",
|
|
86
|
+
reason: `sim ${simMs.toFixed(1)}ms with no named phase ≥${PHASE_HOT_MS}ms — wrap hot work in measure("name", fn)`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
topPhase: null,
|
|
91
|
+
culprit: "outside-sim",
|
|
92
|
+
reason: `frame ${frameMs.toFixed(1)}ms · sim ${simMs.toFixed(1)}ms · outside ${outsideMs.toFixed(1)}ms (render/React/GC/tab throttle)`,
|
|
93
|
+
};
|
|
61
94
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
95
|
+
function collectScanTargets(root, meta) {
|
|
96
|
+
const skipped = [];
|
|
97
|
+
if (!isScannableContainer(root)) {
|
|
98
|
+
if (root !== null && typeof root === "object") {
|
|
99
|
+
skipped.push({ path: "", reason: "not a plain object or array" });
|
|
100
|
+
}
|
|
101
|
+
return { targets: [], skipped };
|
|
102
|
+
}
|
|
103
|
+
const out = [];
|
|
104
|
+
const visited = new Set([root]);
|
|
105
|
+
let frontier = [{ path: "", target: root }];
|
|
106
|
+
for (let depth = 0; depth <= MAX_SCAN_DEPTH && frontier.length > 0; depth += 1) {
|
|
107
|
+
const next = [];
|
|
108
|
+
for (const { path, target } of frontier) {
|
|
109
|
+
const keys = ownWritableDataKeys(target);
|
|
110
|
+
let scalarCount = 0;
|
|
111
|
+
for (const key of keys) {
|
|
112
|
+
const value = target[key];
|
|
113
|
+
const childPath = joinTunablePath(path, key);
|
|
114
|
+
const fieldMeta = meta?.[childPath];
|
|
115
|
+
const kind = discoverableKind(value, fieldMeta);
|
|
116
|
+
if (kind !== null) {
|
|
117
|
+
scalarCount += 1;
|
|
118
|
+
if (scalarCount > MAX_TABLE_ENTRIES) {
|
|
119
|
+
skipped.push({ path: path === "" ? "*" : path, reason: `more than ${MAX_TABLE_ENTRIES} scalar entries` });
|
|
120
|
+
scalarCount = -1;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (scalarCount < 0)
|
|
126
|
+
continue;
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
const value = target[key];
|
|
129
|
+
const childPath = joinTunablePath(path, key);
|
|
130
|
+
const fieldMeta = meta?.[childPath];
|
|
131
|
+
const kind = discoverableKind(value, fieldMeta);
|
|
132
|
+
if (kind !== null) {
|
|
133
|
+
if (out.length >= MAX_SCAN_TARGETS) {
|
|
134
|
+
skipped.push({ path: childPath, reason: `scan cap ${MAX_SCAN_TARGETS}` });
|
|
135
|
+
return { targets: out, skipped };
|
|
136
|
+
}
|
|
137
|
+
out.push({ path: childPath, target, key, kind, options: fieldMeta });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (value !== null && typeof value === "object") {
|
|
141
|
+
if (!isScannableContainer(value)) {
|
|
142
|
+
skipped.push({ path: childPath, reason: "unsupported structure" });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (visited.has(value)) {
|
|
146
|
+
skipped.push({ path: childPath, reason: "cycle" });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (depth === MAX_SCAN_DEPTH) {
|
|
150
|
+
skipped.push({ path: childPath, reason: `depth > ${MAX_SCAN_DEPTH}` });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
visited.add(value);
|
|
154
|
+
next.push({ path: childPath, target: value });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
frontier = next;
|
|
75
159
|
}
|
|
76
|
-
return
|
|
160
|
+
return { targets: out, skipped };
|
|
77
161
|
}
|
|
78
162
|
export function createDevtools() {
|
|
79
163
|
const signal = createChangeSignal();
|
|
80
164
|
const frameMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
81
165
|
const simMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
166
|
+
const outsideMsBuffer = new Float64Array(FRAME_CAPACITY);
|
|
82
167
|
let frameIndex = 0;
|
|
83
168
|
let frameCount = 0;
|
|
84
169
|
let renderSample = null;
|
|
85
170
|
const logEntries = [];
|
|
86
171
|
let consoleCaptured = false;
|
|
87
172
|
const latencySamples = [];
|
|
173
|
+
const longFrameEvents = [];
|
|
174
|
+
const phaseAccumulators = new Map();
|
|
175
|
+
let currentPhases = {};
|
|
176
|
+
let currentPhaseCount = 0;
|
|
88
177
|
const controlRecords = new Map();
|
|
89
178
|
const discoveredRecords = new Map();
|
|
90
179
|
const probeReaders = new Map();
|
|
180
|
+
let boundProps = new WeakMap();
|
|
181
|
+
let discoverySkips = [];
|
|
91
182
|
const readProbes = () => {
|
|
92
183
|
const values = {};
|
|
93
184
|
for (const [name, read] of probeReaders) {
|
|
@@ -105,32 +196,124 @@ export function createDevtools() {
|
|
|
105
196
|
if (logEntries.length > LOG_CAPACITY)
|
|
106
197
|
logEntries.splice(0, logEntries.length - LOG_CAPACITY);
|
|
107
198
|
};
|
|
199
|
+
const addPhaseMs = (name, ms) => {
|
|
200
|
+
const elapsed = clampMs(ms);
|
|
201
|
+
if (elapsed === 0 || name.length === 0)
|
|
202
|
+
return;
|
|
203
|
+
if (currentPhases[name] === undefined) {
|
|
204
|
+
if (currentPhaseCount >= MAX_PHASES_PER_FRAME)
|
|
205
|
+
return;
|
|
206
|
+
currentPhaseCount += 1;
|
|
207
|
+
currentPhases[name] = elapsed;
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
currentPhases[name] = currentPhases[name] + elapsed;
|
|
211
|
+
};
|
|
212
|
+
const resetCurrentPhases = () => {
|
|
213
|
+
currentPhases = {};
|
|
214
|
+
currentPhaseCount = 0;
|
|
215
|
+
};
|
|
216
|
+
const recordPhaseStats = (phases) => {
|
|
217
|
+
for (const [name, ms] of Object.entries(phases)) {
|
|
218
|
+
const elapsed = clampMs(ms);
|
|
219
|
+
if (elapsed === 0)
|
|
220
|
+
continue;
|
|
221
|
+
const existing = phaseAccumulators.get(name);
|
|
222
|
+
if (existing === undefined) {
|
|
223
|
+
phaseAccumulators.set(name, { sumMs: elapsed, maxMs: elapsed, lastMs: elapsed, samples: 1 });
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
existing.sumMs += elapsed;
|
|
227
|
+
existing.maxMs = Math.max(existing.maxMs, elapsed);
|
|
228
|
+
existing.lastMs = elapsed;
|
|
229
|
+
existing.samples += 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const phaseStatsList = (avgSimMs) => {
|
|
234
|
+
const list = [];
|
|
235
|
+
for (const [name, acc] of phaseAccumulators) {
|
|
236
|
+
const avgMs = acc.sumMs / acc.samples;
|
|
237
|
+
list.push({
|
|
238
|
+
name,
|
|
239
|
+
avgMs,
|
|
240
|
+
maxMs: acc.maxMs,
|
|
241
|
+
lastMs: acc.lastMs,
|
|
242
|
+
samples: acc.samples,
|
|
243
|
+
pctOfSim: avgSimMs > 0 ? (avgMs / avgSimMs) * 100 : 0,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
list.sort((a, b) => b.avgMs - a.avgMs);
|
|
247
|
+
return list;
|
|
248
|
+
};
|
|
108
249
|
const frameStats = () => {
|
|
109
250
|
if (frameCount === 0)
|
|
110
251
|
return null;
|
|
111
252
|
const size = Math.min(frameCount, FRAME_CAPACITY);
|
|
112
253
|
const frames = [];
|
|
113
254
|
const sims = [];
|
|
255
|
+
const outsides = [];
|
|
114
256
|
for (let offset = 0; offset < size; offset += 1) {
|
|
115
257
|
const at = (frameIndex - size + offset + FRAME_CAPACITY) % FRAME_CAPACITY;
|
|
116
258
|
frames.push(frameMsBuffer[at]);
|
|
117
259
|
sims.push(simMsBuffer[at]);
|
|
260
|
+
outsides.push(outsideMsBuffer[at]);
|
|
118
261
|
}
|
|
119
262
|
const recent = frames.slice(-RECENT_FRAMES);
|
|
120
263
|
const recentAvg = recent.reduce((sum, ms) => sum + ms, 0) / recent.length;
|
|
121
264
|
const sorted = [...frames].sort((a, b) => a - b);
|
|
265
|
+
const avgSimMs = sims.reduce((sum, ms) => sum + ms, 0) / sims.length;
|
|
122
266
|
return {
|
|
123
267
|
fps: recentAvg > 0 ? 1000 / recentAvg : 0,
|
|
124
268
|
avgFrameMs: frames.reduce((sum, ms) => sum + ms, 0) / frames.length,
|
|
125
269
|
p95FrameMs: percentile(sorted, 0.95),
|
|
126
270
|
maxFrameMs: sorted[sorted.length - 1],
|
|
127
|
-
avgSimMs
|
|
271
|
+
avgSimMs,
|
|
128
272
|
maxSimMs: sims.reduce((max, ms) => Math.max(max, ms), 0),
|
|
273
|
+
avgOutsideMs: outsides.reduce((sum, ms) => sum + ms, 0) / outsides.length,
|
|
274
|
+
maxOutsideMs: outsides.reduce((max, ms) => Math.max(max, ms), 0),
|
|
129
275
|
longFrames: frames.filter((ms) => ms > LONG_FRAME_MS).length,
|
|
130
276
|
samples: size,
|
|
131
277
|
recentFrameMs: recent,
|
|
278
|
+
phases: phaseStatsList(avgSimMs),
|
|
132
279
|
};
|
|
133
280
|
};
|
|
281
|
+
const recordFrame = (sample) => {
|
|
282
|
+
const frameMs = clampMs(sample.frameMs);
|
|
283
|
+
const simMs = clampMs(sample.simMs);
|
|
284
|
+
const outsideMs = clampMs(frameMs - simMs);
|
|
285
|
+
const phases = sample.phases !== undefined
|
|
286
|
+
? Object.fromEntries(Object.entries(sample.phases)
|
|
287
|
+
.filter(([, ms]) => Number.isFinite(ms) && ms > 0)
|
|
288
|
+
.slice(0, MAX_PHASES_PER_FRAME))
|
|
289
|
+
: { ...currentPhases };
|
|
290
|
+
frameMsBuffer[frameIndex] = frameMs;
|
|
291
|
+
simMsBuffer[frameIndex] = simMs;
|
|
292
|
+
outsideMsBuffer[frameIndex] = outsideMs;
|
|
293
|
+
frameIndex = (frameIndex + 1) % FRAME_CAPACITY;
|
|
294
|
+
frameCount += 1;
|
|
295
|
+
recordPhaseStats(phases);
|
|
296
|
+
if (frameMs > LONG_FRAME_MS) {
|
|
297
|
+
const ranked = rankPhases(phases);
|
|
298
|
+
const explained = explainLongFrame(frameMs, simMs, outsideMs, ranked);
|
|
299
|
+
longFrameEvents.push({
|
|
300
|
+
at: Date.now(),
|
|
301
|
+
frameMs,
|
|
302
|
+
simMs,
|
|
303
|
+
outsideMs,
|
|
304
|
+
phases: ranked,
|
|
305
|
+
topPhase: explained.topPhase,
|
|
306
|
+
culprit: explained.culprit,
|
|
307
|
+
reason: explained.reason,
|
|
308
|
+
probes: readProbes(),
|
|
309
|
+
render: renderSample,
|
|
310
|
+
});
|
|
311
|
+
if (longFrameEvents.length > LONG_FRAME_CAPACITY) {
|
|
312
|
+
longFrameEvents.splice(0, longFrameEvents.length - LONG_FRAME_CAPACITY);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
resetCurrentPhases();
|
|
316
|
+
};
|
|
134
317
|
const latencyStats = () => {
|
|
135
318
|
if (latencySamples.length === 0)
|
|
136
319
|
return null;
|
|
@@ -150,6 +333,18 @@ export function createDevtools() {
|
|
|
150
333
|
samples: latencySamples.length,
|
|
151
334
|
};
|
|
152
335
|
};
|
|
336
|
+
const validationContext = (record) => ({
|
|
337
|
+
min: record.min,
|
|
338
|
+
max: record.max,
|
|
339
|
+
step: record.step,
|
|
340
|
+
integer: record.integer,
|
|
341
|
+
unit: record.unit,
|
|
342
|
+
wrap: record.wrap,
|
|
343
|
+
choices: record.choices,
|
|
344
|
+
options: record.options,
|
|
345
|
+
axisBounds: record.axisBounds,
|
|
346
|
+
alpha: record.hasAlpha,
|
|
347
|
+
});
|
|
153
348
|
const toControl = (record) => ({
|
|
154
349
|
name: record.name,
|
|
155
350
|
kind: record.kind,
|
|
@@ -159,33 +354,96 @@ export function createDevtools() {
|
|
|
159
354
|
min: record.min,
|
|
160
355
|
max: record.max,
|
|
161
356
|
step: record.step,
|
|
162
|
-
options: record.options,
|
|
357
|
+
options: record.options ?? choiceValues(record.choices),
|
|
358
|
+
choices: record.choices,
|
|
359
|
+
unit: record.unit,
|
|
360
|
+
displayUnit: record.displayUnit,
|
|
361
|
+
wrap: record.wrap,
|
|
362
|
+
integer: record.integer,
|
|
363
|
+
axisLabels: record.axisBounds?.labels,
|
|
364
|
+
axisMin: record.axisBounds?.min,
|
|
365
|
+
axisMax: record.axisBounds?.max,
|
|
366
|
+
axisStep: record.axisBounds?.step,
|
|
367
|
+
hasAlpha: record.hasAlpha,
|
|
368
|
+
schemaVersion: record.schemaVersion,
|
|
163
369
|
read: () => record.value,
|
|
164
370
|
write: (value) => writeControl(record, value),
|
|
165
|
-
reset: () =>
|
|
371
|
+
reset: () => {
|
|
372
|
+
writeControl(record, cloneValue(record.initial));
|
|
373
|
+
},
|
|
166
374
|
});
|
|
167
375
|
const writeControl = (record, raw) => {
|
|
168
|
-
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
376
|
+
const validated = validateControlValue(record.kind, raw, validationContext(record));
|
|
377
|
+
if (!validated.ok)
|
|
378
|
+
return false;
|
|
379
|
+
const next = validated.value;
|
|
380
|
+
if (record.kind === "vec2" || record.kind === "vec3" || record.kind === "vec4" || record.kind === "interval") {
|
|
381
|
+
if (JSON.stringify(record.value) === JSON.stringify(next))
|
|
382
|
+
return true;
|
|
383
|
+
record.value = cloneValue(next);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
if (Object.is(record.value, next))
|
|
387
|
+
return true;
|
|
388
|
+
record.value = next;
|
|
174
389
|
}
|
|
175
|
-
if (Object.is(record.value, next))
|
|
176
|
-
return;
|
|
177
|
-
record.value = next;
|
|
178
390
|
for (const listener of record.listeners)
|
|
179
|
-
listener(
|
|
391
|
+
listener(record.value);
|
|
180
392
|
signal.notify();
|
|
393
|
+
return true;
|
|
394
|
+
};
|
|
395
|
+
const hydrateRecord = (record, initial, options, kind) => {
|
|
396
|
+
record.kind = kind;
|
|
397
|
+
record.initial = cloneValue(initial);
|
|
398
|
+
record.min = undefined;
|
|
399
|
+
record.max = undefined;
|
|
400
|
+
record.step = undefined;
|
|
401
|
+
record.options = options?.options;
|
|
402
|
+
record.choices = resolveChoices(options?.options, options?.choices);
|
|
403
|
+
record.unit = options?.unit;
|
|
404
|
+
record.displayUnit = options?.displayUnit ?? options?.unit;
|
|
405
|
+
record.wrap = options?.wrap;
|
|
406
|
+
record.integer = options?.integer;
|
|
407
|
+
record.axisBounds = undefined;
|
|
408
|
+
record.hasAlpha = undefined;
|
|
409
|
+
record.schemaVersion = CONTROL_SCHEMA_VERSION;
|
|
410
|
+
if (kind === "slider" || kind === "angle") {
|
|
411
|
+
const bounds = sliderBounds(initial, options);
|
|
412
|
+
record.min = bounds.min;
|
|
413
|
+
record.max = bounds.max;
|
|
414
|
+
record.step = bounds.step;
|
|
415
|
+
if (kind === "angle") {
|
|
416
|
+
record.unit = options?.unit ?? "rad";
|
|
417
|
+
record.displayUnit = options?.displayUnit ?? "deg";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else if (kind === "vec2" || kind === "vec3" || kind === "vec4") {
|
|
421
|
+
const sample = Array.isArray(initial) ? initial : [];
|
|
422
|
+
record.axisBounds = expandAxisMeta(sample.length, options?.axisLabels, options?.axisMin ?? options?.min, options?.axisMax ?? options?.max, options?.axisStep ?? options?.step, sample);
|
|
423
|
+
}
|
|
424
|
+
else if (kind === "interval") {
|
|
425
|
+
record.min = options?.min;
|
|
426
|
+
record.max = options?.max;
|
|
427
|
+
record.step = options?.step;
|
|
428
|
+
record.integer = options?.integer;
|
|
429
|
+
}
|
|
430
|
+
else if (kind === "color") {
|
|
431
|
+
const normalized = normalizeColorValue(initial, options?.alpha);
|
|
432
|
+
if (normalized !== null) {
|
|
433
|
+
record.initial = normalized;
|
|
434
|
+
if (Object.is(record.value, initial) || record.value === initial) {
|
|
435
|
+
record.value = normalized;
|
|
436
|
+
}
|
|
437
|
+
record.hasAlpha = options?.alpha === true || normalized.length === 9;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
181
440
|
};
|
|
182
441
|
const register = (name, initial, options) => {
|
|
183
|
-
const kind = inferKind(initial, options
|
|
442
|
+
const kind = inferKind(initial, options);
|
|
184
443
|
const existing = controlRecords.get(name);
|
|
185
444
|
const slashIndex = name.indexOf("/");
|
|
186
445
|
const derivedGroup = slashIndex > 0 ? name.slice(0, slashIndex) : "general";
|
|
187
446
|
const derivedLabel = slashIndex > 0 ? name.slice(slashIndex + 1) : name;
|
|
188
|
-
const bounds = kind === "slider" ? sliderBounds(initial, options) : undefined;
|
|
189
447
|
const record = existing !== undefined && existing.kind === kind
|
|
190
448
|
? existing
|
|
191
449
|
: {
|
|
@@ -193,17 +451,17 @@ export function createDevtools() {
|
|
|
193
451
|
kind,
|
|
194
452
|
label: "",
|
|
195
453
|
group: "",
|
|
196
|
-
initial,
|
|
197
|
-
value: initial,
|
|
454
|
+
initial: cloneValue(initial),
|
|
455
|
+
value: cloneValue(initial),
|
|
456
|
+
schemaVersion: CONTROL_SCHEMA_VERSION,
|
|
198
457
|
listeners: new Set(),
|
|
199
458
|
};
|
|
459
|
+
if (existing === undefined || existing.kind !== kind) {
|
|
460
|
+
record.value = cloneValue(initial);
|
|
461
|
+
}
|
|
200
462
|
record.label = options?.label ?? derivedLabel;
|
|
201
463
|
record.group = options?.group ?? derivedGroup;
|
|
202
|
-
record
|
|
203
|
-
record.min = bounds?.min;
|
|
204
|
-
record.max = bounds?.max;
|
|
205
|
-
record.step = bounds?.step;
|
|
206
|
-
record.options = options?.options;
|
|
464
|
+
hydrateRecord(record, initial, options, kind);
|
|
207
465
|
if (options?.onChange !== undefined)
|
|
208
466
|
record.listeners.add(options.onChange);
|
|
209
467
|
controlRecords.set(name, record);
|
|
@@ -211,19 +469,21 @@ export function createDevtools() {
|
|
|
211
469
|
return {
|
|
212
470
|
name,
|
|
213
471
|
kind,
|
|
214
|
-
initial,
|
|
472
|
+
initial: record.initial,
|
|
215
473
|
get value() {
|
|
216
474
|
return record.value;
|
|
217
475
|
},
|
|
218
476
|
set: (value) => writeControl(record, value),
|
|
219
|
-
reset: () =>
|
|
477
|
+
reset: () => {
|
|
478
|
+
writeControl(record, cloneValue(record.initial));
|
|
479
|
+
},
|
|
220
480
|
subscribe: (listener) => {
|
|
221
481
|
record.listeners.add(listener);
|
|
222
482
|
return () => record.listeners.delete(listener);
|
|
223
483
|
},
|
|
224
484
|
};
|
|
225
485
|
};
|
|
226
|
-
const bindDiscovered = (table, key, kind, accessor, source) => {
|
|
486
|
+
const bindDiscovered = (table, key, kind, accessor, source, options) => {
|
|
227
487
|
const id = `${table}/${key}`;
|
|
228
488
|
const existing = discoveredRecords.get(id);
|
|
229
489
|
if (existing === undefined) {
|
|
@@ -232,9 +492,10 @@ export function createDevtools() {
|
|
|
232
492
|
table,
|
|
233
493
|
key,
|
|
234
494
|
kind,
|
|
235
|
-
initial: accessor.initial,
|
|
495
|
+
initial: cloneValue(accessor.initial),
|
|
236
496
|
enabled: false,
|
|
237
497
|
source,
|
|
498
|
+
options,
|
|
238
499
|
get: accessor.get,
|
|
239
500
|
set: accessor.set,
|
|
240
501
|
});
|
|
@@ -245,31 +506,48 @@ export function createDevtools() {
|
|
|
245
506
|
existing.source = source;
|
|
246
507
|
existing.get = accessor.get;
|
|
247
508
|
existing.set = accessor.set;
|
|
509
|
+
existing.kind = kind;
|
|
510
|
+
existing.options = options ?? existing.options;
|
|
248
511
|
const control = controlRecords.get(id);
|
|
249
512
|
if (existing.enabled && control !== undefined)
|
|
250
|
-
accessor.set(control.value);
|
|
513
|
+
accessor.set(cloneValue(control.value));
|
|
251
514
|
return true;
|
|
252
515
|
};
|
|
253
|
-
const scanTable = (table, candidate) => {
|
|
254
|
-
const
|
|
255
|
-
|
|
516
|
+
const scanTable = (table, candidate, meta) => {
|
|
517
|
+
const { targets, skipped } = collectScanTargets(candidate, meta);
|
|
518
|
+
discoverySkips = [
|
|
519
|
+
...discoverySkips.filter((entry) => entry.path !== table && !entry.path.startsWith(`${table}/`)),
|
|
520
|
+
...skipped.map((entry) => ({
|
|
521
|
+
path: entry.path === "" ? table : `${table}/${entry.path}`,
|
|
522
|
+
reason: entry.reason,
|
|
523
|
+
})),
|
|
524
|
+
];
|
|
525
|
+
if (targets.length === 0)
|
|
256
526
|
return 0;
|
|
257
|
-
const target = candidate;
|
|
258
527
|
let changed = false;
|
|
259
|
-
|
|
528
|
+
let bound = 0;
|
|
529
|
+
for (const { path, target, key, kind, options } of targets) {
|
|
530
|
+
const boundKeys = boundProps.get(target);
|
|
531
|
+
if (boundKeys?.has(key) === true && !discoveredRecords.has(`${table}/${path}`))
|
|
532
|
+
continue;
|
|
260
533
|
const accessor = {
|
|
261
|
-
initial: target[key],
|
|
534
|
+
initial: cloneValue(target[key]),
|
|
262
535
|
get: () => target[key],
|
|
263
536
|
set: (value) => {
|
|
264
537
|
target[key] = value;
|
|
265
538
|
},
|
|
266
539
|
};
|
|
267
|
-
if (bindDiscovered(table,
|
|
540
|
+
if (bindDiscovered(table, path, kind, accessor, target, options))
|
|
268
541
|
changed = true;
|
|
542
|
+
bound += 1;
|
|
543
|
+
if (boundKeys === undefined)
|
|
544
|
+
boundProps.set(target, new Set([key]));
|
|
545
|
+
else
|
|
546
|
+
boundKeys.add(key);
|
|
269
547
|
}
|
|
270
548
|
if (changed)
|
|
271
549
|
signal.notify();
|
|
272
|
-
return
|
|
550
|
+
return bound;
|
|
273
551
|
};
|
|
274
552
|
const enableDiscovered = (id) => {
|
|
275
553
|
const record = discoveredRecords.get(id);
|
|
@@ -277,20 +555,42 @@ export function createDevtools() {
|
|
|
277
555
|
return;
|
|
278
556
|
record.enabled = true;
|
|
279
557
|
const current = record.get();
|
|
280
|
-
const handle = register(id, record.initial, {
|
|
281
|
-
|
|
282
|
-
|
|
558
|
+
const handle = register(id, record.initial, {
|
|
559
|
+
...(record.options ?? {}),
|
|
560
|
+
group: record.options?.group ?? record.table,
|
|
561
|
+
label: record.options?.label ?? record.key,
|
|
562
|
+
kind: record.kind,
|
|
563
|
+
});
|
|
564
|
+
handle.subscribe((value) => record.set(cloneValue(value)));
|
|
565
|
+
if (JSON.stringify(current) !== JSON.stringify(record.initial))
|
|
283
566
|
handle.set(current);
|
|
284
567
|
};
|
|
285
568
|
return {
|
|
286
569
|
frame: {
|
|
287
|
-
record
|
|
288
|
-
frameMsBuffer[frameIndex] = frameMs;
|
|
289
|
-
simMsBuffer[frameIndex] = simMs;
|
|
290
|
-
frameIndex = (frameIndex + 1) % FRAME_CAPACITY;
|
|
291
|
-
frameCount += 1;
|
|
292
|
-
},
|
|
570
|
+
record: recordFrame,
|
|
293
571
|
stats: frameStats,
|
|
572
|
+
longFrames: () => longFrameEvents,
|
|
573
|
+
clearLongFrames() {
|
|
574
|
+
longFrameEvents.length = 0;
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
profile: {
|
|
578
|
+
begin(name) {
|
|
579
|
+
const start = nowMs();
|
|
580
|
+
return () => addPhaseMs(name, nowMs() - start);
|
|
581
|
+
},
|
|
582
|
+
measure(name, fn) {
|
|
583
|
+
const start = nowMs();
|
|
584
|
+
try {
|
|
585
|
+
return fn();
|
|
586
|
+
}
|
|
587
|
+
finally {
|
|
588
|
+
addPhaseMs(name, nowMs() - start);
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
add: addPhaseMs,
|
|
592
|
+
current: () => ({ ...currentPhases }),
|
|
593
|
+
reset: resetCurrentPhases,
|
|
294
594
|
},
|
|
295
595
|
render: {
|
|
296
596
|
record(sample) {
|
|
@@ -343,22 +643,22 @@ export function createDevtools() {
|
|
|
343
643
|
},
|
|
344
644
|
resetAll() {
|
|
345
645
|
for (const record of controlRecords.values())
|
|
346
|
-
writeControl(record, record.initial);
|
|
646
|
+
writeControl(record, cloneValue(record.initial));
|
|
347
647
|
},
|
|
348
648
|
},
|
|
349
649
|
discover: {
|
|
350
|
-
bind(table, key, accessor) {
|
|
351
|
-
const kind = discoverableKind(accessor.initial);
|
|
650
|
+
bind(table, key, accessor, options) {
|
|
651
|
+
const kind = discoverableKind(accessor.initial, options);
|
|
352
652
|
if (kind === null)
|
|
353
653
|
return;
|
|
354
|
-
if (bindDiscovered(table, key, kind, accessor, null))
|
|
654
|
+
if (bindDiscovered(table, key, kind, accessor, null, options))
|
|
355
655
|
signal.notify();
|
|
356
656
|
},
|
|
357
657
|
scanTable,
|
|
358
|
-
scanModule(exports) {
|
|
658
|
+
scanModule(exports, meta) {
|
|
359
659
|
let total = 0;
|
|
360
660
|
for (const [name, value] of Object.entries(exports)) {
|
|
361
|
-
total += scanTable(name, value);
|
|
661
|
+
total += scanTable(name, value, meta);
|
|
362
662
|
}
|
|
363
663
|
return total;
|
|
364
664
|
},
|
|
@@ -371,6 +671,7 @@ export function createDevtools() {
|
|
|
371
671
|
enabled: record.enabled,
|
|
372
672
|
read: () => record.get(),
|
|
373
673
|
})),
|
|
674
|
+
skipped: () => discoverySkips,
|
|
374
675
|
enable(id) {
|
|
375
676
|
enableDiscovered(id);
|
|
376
677
|
signal.notify();
|
|
@@ -380,18 +681,20 @@ export function createDevtools() {
|
|
|
380
681
|
if (record === undefined || !record.enabled)
|
|
381
682
|
return;
|
|
382
683
|
record.enabled = false;
|
|
383
|
-
record.set(record.initial);
|
|
684
|
+
record.set(cloneValue(record.initial));
|
|
384
685
|
controlRecords.delete(id);
|
|
385
686
|
signal.notify();
|
|
386
687
|
},
|
|
387
688
|
clear() {
|
|
388
689
|
for (const record of discoveredRecords.values()) {
|
|
389
690
|
if (record.enabled) {
|
|
390
|
-
record.set(record.initial);
|
|
691
|
+
record.set(cloneValue(record.initial));
|
|
391
692
|
controlRecords.delete(record.id);
|
|
392
693
|
}
|
|
393
694
|
}
|
|
394
695
|
discoveredRecords.clear();
|
|
696
|
+
discoverySkips = [];
|
|
697
|
+
boundProps = new WeakMap();
|
|
395
698
|
signal.notify();
|
|
396
699
|
},
|
|
397
700
|
},
|
|
@@ -399,21 +702,61 @@ export function createDevtools() {
|
|
|
399
702
|
export() {
|
|
400
703
|
const enabled = [...discoveredRecords.values()].filter((r) => r.enabled).map((r) => r.id);
|
|
401
704
|
const values = {};
|
|
705
|
+
const schemas = {};
|
|
402
706
|
for (const record of controlRecords.values()) {
|
|
403
|
-
|
|
404
|
-
|
|
707
|
+
schemas[record.name] = { kind: record.kind, schemaVersion: record.schemaVersion };
|
|
708
|
+
const same = record.kind === "vec2" ||
|
|
709
|
+
record.kind === "vec3" ||
|
|
710
|
+
record.kind === "vec4" ||
|
|
711
|
+
record.kind === "interval"
|
|
712
|
+
? JSON.stringify(record.value) === JSON.stringify(record.initial)
|
|
713
|
+
: Object.is(record.value, record.initial);
|
|
714
|
+
if (!same)
|
|
715
|
+
values[record.name] = cloneValue(record.value);
|
|
405
716
|
}
|
|
406
|
-
return {
|
|
717
|
+
return {
|
|
718
|
+
version: OVERRIDES_FORMAT_VERSION,
|
|
719
|
+
enabled,
|
|
720
|
+
values,
|
|
721
|
+
schemas,
|
|
722
|
+
};
|
|
407
723
|
},
|
|
408
|
-
apply(
|
|
409
|
-
|
|
724
|
+
apply(raw) {
|
|
725
|
+
const parsed = parseOverridesPayload(raw);
|
|
726
|
+
const diagnostics = [...parsed.diagnostics];
|
|
727
|
+
const skipped = [];
|
|
728
|
+
if (parsed.overrides === null) {
|
|
729
|
+
return { applied: 0, skipped, diagnostics };
|
|
730
|
+
}
|
|
731
|
+
const overrides = parsed.overrides;
|
|
732
|
+
let applied = 0;
|
|
733
|
+
for (const id of overrides.enabled) {
|
|
734
|
+
if (!discoveredRecords.has(id) && !controlRecords.has(id)) {
|
|
735
|
+
skipped.push({ id, reason: "unknown control id" });
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
410
738
|
enableDiscovered(id);
|
|
739
|
+
applied += 1;
|
|
740
|
+
}
|
|
411
741
|
for (const [name, value] of Object.entries(overrides.values)) {
|
|
412
742
|
const record = controlRecords.get(name);
|
|
413
|
-
if (record
|
|
414
|
-
|
|
743
|
+
if (record === undefined) {
|
|
744
|
+
skipped.push({ id: name, reason: "control not registered" });
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const schema = overrides.schemas?.[name];
|
|
748
|
+
if (schema !== undefined && schema.kind !== record.kind) {
|
|
749
|
+
skipped.push({ id: name, reason: `kind mismatch stored=${schema.kind} current=${record.kind}` });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (!writeControl(record, value)) {
|
|
753
|
+
skipped.push({ id: name, reason: "value failed validation" });
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
applied += 1;
|
|
415
757
|
}
|
|
416
758
|
signal.notify();
|
|
759
|
+
return { applied, skipped, diagnostics };
|
|
417
760
|
},
|
|
418
761
|
},
|
|
419
762
|
probes: {
|
|
@@ -434,6 +777,7 @@ export function createDevtools() {
|
|
|
434
777
|
frame: frameStats(),
|
|
435
778
|
render: renderSample,
|
|
436
779
|
latency: latencyStats(),
|
|
780
|
+
longFrames: [...longFrameEvents],
|
|
437
781
|
logs: [...logEntries],
|
|
438
782
|
probes: readProbes(),
|
|
439
783
|
controls: [...controlRecords.values()].map((record) => ({
|
|
@@ -448,6 +792,7 @@ export function createDevtools() {
|
|
|
448
792
|
value: record.get(),
|
|
449
793
|
enabled: record.enabled,
|
|
450
794
|
})),
|
|
795
|
+
skipped: [...discoverySkips],
|
|
451
796
|
};
|
|
452
797
|
},
|
|
453
798
|
};
|
|
@@ -459,6 +804,9 @@ export function tunable(name, initial, options) {
|
|
|
459
804
|
export function snapshotDevtools() {
|
|
460
805
|
return devtools.snapshot();
|
|
461
806
|
}
|
|
807
|
+
export function measureProfile(name, fn) {
|
|
808
|
+
return devtools.profile.measure(name, fn);
|
|
809
|
+
}
|
|
462
810
|
export function instrumentLatency(target, methods, record = devtools.latency.record) {
|
|
463
811
|
const wrapped = { ...target };
|
|
464
812
|
for (const method of methods) {
|