@driftengine/tools 4.8.1 → 4.8.2
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/README.md +22 -4
- package/dist/console.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inspector.d.ts +4 -1
- package/dist/inspector.js +24 -3
- package/dist/network.d.ts +16 -4
- package/dist/network.js +1 -1
- package/dist/overlay.d.ts +5 -0
- package/dist/overlay.js +50 -8
- package/package.json +6 -6
- package/src/console.ts +7 -0
- package/src/index.ts +1 -1
- package/src/inspector.ts +31 -4
- package/src/network.ts +12 -3
- package/src/overlay.ts +50 -8
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ The editor's panels, as something a shipped game can carry: an inspector, a cons
|
|
|
4
4
|
a network panel, with the command stack that makes their edits undoable, and the overlay that puts
|
|
5
5
|
them on screen on a key.
|
|
6
6
|
|
|
7
|
-
**5,
|
|
7
|
+
**5,780 bytes gzipped**, measured by `scripts/size-gate.test.mjs` against
|
|
8
8
|
`scripts/fixtures/size/tools-only.ts`. Optional — nothing in `@driftengine/core` imports it, so a
|
|
9
9
|
game that never asks for these pays nothing.
|
|
10
10
|
|
|
@@ -30,14 +30,32 @@ undo stack" has no worst offender. A game carrying these panels carries that rul
|
|
|
30
30
|
nothing extra.
|
|
31
31
|
|
|
32
32
|
```ts
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
createSelection,
|
|
35
|
+
createUndoStack,
|
|
36
|
+
entitiesInspectable,
|
|
37
|
+
selectOnly,
|
|
38
|
+
selectedEntities,
|
|
39
|
+
setFieldCommand,
|
|
40
|
+
} from '@driftengine/tools';
|
|
34
41
|
|
|
35
42
|
const undo = createUndoStack(200);
|
|
36
|
-
const
|
|
37
|
-
|
|
43
|
+
const selection = createSelection();
|
|
44
|
+
selectOnly(selection, ball);
|
|
45
|
+
/* One edit to every selected entity, as one entry: `undo.undo()` takes it back. */
|
|
46
|
+
const command = setFieldCommand(
|
|
47
|
+
entitiesInspectable(world, [Bounce]),
|
|
48
|
+
selectedEntities(selection),
|
|
49
|
+
'Bounce',
|
|
50
|
+
['amount'],
|
|
51
|
+
[0.6],
|
|
52
|
+
);
|
|
38
53
|
if (command !== null) undo.push(command);
|
|
39
54
|
```
|
|
40
55
|
|
|
56
|
+
The inspector panel shows a selection's fields and edits none of them yet — its `route` answers
|
|
57
|
+
nothing — so a game edits through `setFieldCommand` as above, from whatever key or control it likes.
|
|
58
|
+
|
|
41
59
|
## The overlay, because a panel that nothing mounts is a tree nobody sees
|
|
42
60
|
|
|
43
61
|
Every panel above builds a `UiNode` tree and stops there. For 4.0.0 the half that turns a tree into
|
package/dist/console.js
CHANGED
|
@@ -133,10 +133,15 @@ export const consolePanel = {
|
|
|
133
133
|
id: 'console',
|
|
134
134
|
title: 'Console',
|
|
135
135
|
build(world, view, root) {
|
|
136
|
+
/* The window is the height the panel was given, where it was given one: the overlay sets it on
|
|
137
|
+
the root before it builds. `viewHeight` is what a root with no exact height falls back to. */
|
|
138
|
+
if (typeof root.height === 'number' && root.height > 0)
|
|
139
|
+
view.viewHeight = root.height;
|
|
136
140
|
view.shown = filteredEntries(world.log, view.filter);
|
|
137
141
|
if (view.shown.length === 0) {
|
|
138
142
|
emptyPanel(root, 'Nothing logged');
|
|
139
143
|
view.scrollY = 0;
|
|
144
|
+
root.scrollY = 0;
|
|
140
145
|
return;
|
|
141
146
|
}
|
|
142
147
|
root.children.length = 0;
|
|
@@ -153,6 +158,8 @@ export const consolePanel = {
|
|
|
153
158
|
/* The follow happens here and not on append, because only a build knows how tall the list is. */
|
|
154
159
|
if (view.pinned)
|
|
155
160
|
view.scrollY = maxScroll(view, view.shown.length);
|
|
161
|
+
/* And onto the root, which is what ui2d offsets the rows by: the scroll that is drawn. */
|
|
162
|
+
root.scrollY = view.scrollY;
|
|
156
163
|
},
|
|
157
164
|
route(_world, view, event) {
|
|
158
165
|
if (event.kind === 'wheel') {
|
|
@@ -162,6 +169,7 @@ export const consolePanel = {
|
|
|
162
169
|
const limit = maxScroll(view, view.shown.length);
|
|
163
170
|
view.scrollY = Math.min(limit, Math.max(0, view.scrollY + event.dy));
|
|
164
171
|
view.pinned = view.scrollY >= limit;
|
|
172
|
+
view.root.scrollY = view.scrollY;
|
|
165
173
|
return null;
|
|
166
174
|
}
|
|
167
175
|
if (event.kind !== 'pointer' || event.phase !== 'up' || event.button !== 0)
|
package/dist/index.d.ts
CHANGED
|
@@ -35,4 +35,4 @@ export type { NetworkReadout, NetworkView, NetworkWorld, RollbackHistory } from
|
|
|
35
35
|
export type { SessionLike, SessionRecorder } from './network.ts';
|
|
36
36
|
export { componentDivergence, createNetworkView, createRollbackHistory, createSessionRecorder, networkPanel, observeSession, pushRollback, rollbackValues, sessionReadout, } from './network.ts';
|
|
37
37
|
export type { OverlayPainter, OverlaySite, PanelBinding, ToolsOverlay, ToolsOverlayOptions, } from './overlay.ts';
|
|
38
|
-
export { bindPanel, createToolsOverlay, paintOverlay } from './overlay.ts';
|
|
38
|
+
export { PANEL_TITLE_HEIGHT, bindPanel, createToolsOverlay, paintOverlay } from './overlay.ts';
|
package/dist/index.js
CHANGED
|
@@ -7,4 +7,4 @@ export { MIXED, addComponentCommand, createInspectorView, entitiesInspectable, f
|
|
|
7
7
|
export { DISPLAY_LIMIT, appendLog, consolePanel, createConsoleView, createLogFilter, createLogRing, displayText, filteredEntries, jumpToSourceCommand, logEntries, } from './console.js';
|
|
8
8
|
export { createFrameHistory, createGpuPassTimings, createProfilerView, formatMs, historyValues, occupancy, profilerPanel, profilerRows, pushFrame, recordGpuSample, } from './profiler.js';
|
|
9
9
|
export { componentDivergence, createNetworkView, createRollbackHistory, createSessionRecorder, networkPanel, observeSession, pushRollback, rollbackValues, sessionReadout, } from './network.js';
|
|
10
|
-
export { bindPanel, createToolsOverlay, paintOverlay } from './overlay.js';
|
|
10
|
+
export { PANEL_TITLE_HEIGHT, bindPanel, createToolsOverlay, paintOverlay } from './overlay.js';
|
package/dist/inspector.d.ts
CHANGED
|
@@ -30,7 +30,10 @@ export interface InspectorRow {
|
|
|
30
30
|
readonly component: string;
|
|
31
31
|
/** What the row is called: a field's name, or the prefix the vector's three share. */
|
|
32
32
|
readonly label: string;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The declared type, verbatim. For a vector, the type its three parts share, or empty where they
|
|
35
|
+
* do not share one.
|
|
36
|
+
*/
|
|
34
37
|
readonly type: string;
|
|
35
38
|
readonly kind: RowKind;
|
|
36
39
|
readonly enumName: string;
|
package/dist/inspector.js
CHANGED
|
@@ -104,10 +104,11 @@ export function inspectorRows(world, entities) {
|
|
|
104
104
|
continue;
|
|
105
105
|
emitted.add(part.prefix);
|
|
106
106
|
const slots = vectors.get(part.prefix);
|
|
107
|
+
const types = new Set(fields.filter((one) => slots.includes(one.name)).map((one) => one.type));
|
|
107
108
|
rows.push({
|
|
108
109
|
component,
|
|
109
110
|
label: part.prefix,
|
|
110
|
-
type: '',
|
|
111
|
+
type: types.size === 1 ? field.type : '',
|
|
111
112
|
kind: 'vector',
|
|
112
113
|
enumName: '',
|
|
113
114
|
fields: slots,
|
|
@@ -227,12 +228,32 @@ export function rowText(row) {
|
|
|
227
228
|
if (row.value === MIXED)
|
|
228
229
|
return `${row.label} —`;
|
|
229
230
|
if (row.kind === 'vector') {
|
|
230
|
-
const parts = row.value.map((part) =>
|
|
231
|
+
const parts = row.value.map((part) => part === MIXED ? '—' : valueText(part, row.type));
|
|
231
232
|
return `${row.label} ${parts.join(', ')}`;
|
|
232
233
|
}
|
|
233
234
|
if (row.kind === 'boolean')
|
|
234
235
|
return `${row.label} ${row.value ? 'true' : 'false'}`;
|
|
235
|
-
return `${row.label} ${
|
|
236
|
+
return `${row.label} ${valueText(row.value, row.type)}`;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* One value as text: an `f32` as the shortest decimal that stores as the same `f32`.
|
|
240
|
+
*
|
|
241
|
+
* **What was written rather than what is stored.** A `Float32Array` hands back the nearest double,
|
|
242
|
+
* so a bounce of 0.55 reads `0.550000011920929`; the digits past the seventh are the storage, and
|
|
243
|
+
* the shortest decimal that rounds back to the same `f32` is what anybody wrote. Nine significant
|
|
244
|
+
* digits always round-trip a single, so the search ends there. An `f64` and anything else keep
|
|
245
|
+
* `String`, whose own answer is already the shortest for a double.
|
|
246
|
+
*/
|
|
247
|
+
function valueText(value, type) {
|
|
248
|
+
if (type !== 'f32' || typeof value !== 'number' || !Number.isFinite(value))
|
|
249
|
+
return String(value);
|
|
250
|
+
const stored = Math.fround(value);
|
|
251
|
+
for (let digits = 1; digits < 9; digits += 1) {
|
|
252
|
+
const shorter = Number(stored.toPrecision(digits));
|
|
253
|
+
if (Math.fround(shorter) === stored)
|
|
254
|
+
return String(shorter);
|
|
255
|
+
}
|
|
256
|
+
return String(Number(stored.toPrecision(9)));
|
|
236
257
|
}
|
|
237
258
|
export const inspectorPanel = {
|
|
238
259
|
id: 'inspector',
|
package/dist/network.d.ts
CHANGED
|
@@ -62,14 +62,25 @@ export declare const networkPanel: Panel<NetworkWorld, NetworkView>;
|
|
|
62
62
|
* Structural rather than imported, so nothing here loads `@driftengine/network` at run time and a
|
|
63
63
|
* consumer whose session is its own shape can still be read.
|
|
64
64
|
*/
|
|
65
|
-
|
|
65
|
+
/**
|
|
66
|
+
* A session as the network panel reads it, which a `LockstepSession` is as it stands.
|
|
67
|
+
*
|
|
68
|
+
* **The depth by either name**: `rewindDepth` is what a session answers, and `loop.depth` is the
|
|
69
|
+
* shape a game built by hand before it did — from the session's fields and the `RewindLoop` it
|
|
70
|
+
* made — which still reads as it did.
|
|
71
|
+
*/
|
|
72
|
+
export type SessionLike = SessionFields & ({
|
|
73
|
+
readonly rewindDepth: number;
|
|
74
|
+
} | {
|
|
75
|
+
readonly loop: {
|
|
76
|
+
readonly depth: number;
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
interface SessionFields {
|
|
66
80
|
readonly participants: number;
|
|
67
81
|
readonly inputDelay: number;
|
|
68
82
|
/** The disagreement the session is currently holding, or null. Latched, not an event. */
|
|
69
83
|
readonly desync: Desync | null;
|
|
70
|
-
readonly loop: {
|
|
71
|
-
readonly depth: number;
|
|
72
|
-
};
|
|
73
84
|
}
|
|
74
85
|
/** What a recorder keeps: the disagreements it has seen, oldest first. */
|
|
75
86
|
export interface SessionRecorder {
|
|
@@ -98,3 +109,4 @@ export declare function observeSession(recorder: SessionRecorder, session: Sessi
|
|
|
98
109
|
* passing zero is saying it has not measured, which the panel renders as such.
|
|
99
110
|
*/
|
|
100
111
|
export declare function sessionReadout(session: SessionLike, recorder: SessionRecorder, snapshotBytes: number, componentHashes?: NetworkReadout['componentHashes']): NetworkReadout;
|
|
112
|
+
export {};
|
package/dist/network.js
CHANGED
|
@@ -156,7 +156,7 @@ export function sessionReadout(session, recorder, snapshotBytes, componentHashes
|
|
|
156
156
|
inputDelay: session.inputDelay,
|
|
157
157
|
participants: session.participants,
|
|
158
158
|
snapshotBytes,
|
|
159
|
-
snapshotCount: session.loop.depth,
|
|
159
|
+
snapshotCount: 'rewindDepth' in session ? session.rewindDepth : session.loop.depth,
|
|
160
160
|
desyncs: recorder.desyncs,
|
|
161
161
|
componentHashes,
|
|
162
162
|
};
|
package/dist/overlay.d.ts
CHANGED
|
@@ -60,6 +60,11 @@ export interface ToolsOverlay {
|
|
|
60
60
|
invalidate(): void;
|
|
61
61
|
dispose(): void;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* How tall the title row above every panel is, in the overlay's pixels. A panel's own space starts
|
|
65
|
+
* below it, so a host hit-testing a site by hand reads the content from `site.y` plus this.
|
|
66
|
+
*/
|
|
67
|
+
export declare const PANEL_TITLE_HEIGHT = 18;
|
|
63
68
|
export declare function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay;
|
|
64
69
|
/**
|
|
65
70
|
* Draw the overlay, or draw nothing because it is closed.
|
package/dist/overlay.js
CHANGED
|
@@ -41,6 +41,18 @@ export function bindPanel(panel, world, view) {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
const DEFAULT_WIDTH = 320;
|
|
44
|
+
/**
|
|
45
|
+
* How tall the title row above every panel is, in the overlay's pixels. A panel's own space starts
|
|
46
|
+
* below it, so a host hit-testing a site by hand reads the content from `site.y` plus this.
|
|
47
|
+
*/
|
|
48
|
+
export const PANEL_TITLE_HEIGHT = 18;
|
|
49
|
+
/**
|
|
50
|
+
* What every panel is drawn on, and its title on: dark and nearly opaque, so the rows read over a
|
|
51
|
+
* bright scene. The overlay carried no background until 4.8.2 and a host laid one under each site.
|
|
52
|
+
*/
|
|
53
|
+
const PANEL_BACKGROUND = new Float32Array([0.063, 0.071, 0.094, 0.9]);
|
|
54
|
+
const TITLE_BACKGROUND = new Float32Array([0.122, 0.133, 0.176, 0.95]);
|
|
55
|
+
const TITLE_TINT = new Float32Array([0.62, 0.66, 0.75, 1]);
|
|
44
56
|
const DEFAULT_KEY = 'F3';
|
|
45
57
|
export function createToolsOverlay(options) {
|
|
46
58
|
const root = createUiNode({ name: 'tools-overlay', width: 'grow', height: 'grow' });
|
|
@@ -50,6 +62,7 @@ export function createToolsOverlay(options) {
|
|
|
50
62
|
const columnWidth = options.width ?? DEFAULT_WIDTH;
|
|
51
63
|
const panels = options.panels;
|
|
52
64
|
const roots = new Map();
|
|
65
|
+
const titles = new Map();
|
|
53
66
|
/* Pooled and refilled: this is read every frame the overlay is open and must not allocate. */
|
|
54
67
|
const sites = [];
|
|
55
68
|
let width = 0;
|
|
@@ -78,17 +91,33 @@ export function createToolsOverlay(options) {
|
|
|
78
91
|
const top = each * at;
|
|
79
92
|
const tall = at === panels.length - 1 ? height - top : each;
|
|
80
93
|
let panelRoot = roots.get(panel.id);
|
|
81
|
-
|
|
94
|
+
let title = titles.get(panel.id);
|
|
95
|
+
if (panelRoot === undefined || title === undefined) {
|
|
82
96
|
panelRoot = createPanelRoot(panel);
|
|
97
|
+
panelRoot.background = PANEL_BACKGROUND;
|
|
83
98
|
roots.set(panel.id, panelRoot);
|
|
99
|
+
title = createUiNode({
|
|
100
|
+
text: panel.title,
|
|
101
|
+
background: TITLE_BACKGROUND,
|
|
102
|
+
tint: TITLE_TINT,
|
|
103
|
+
name: `title:${panel.id}`,
|
|
104
|
+
});
|
|
105
|
+
titles.set(panel.id, title);
|
|
84
106
|
}
|
|
107
|
+
/* The title above the panel's own space, which starts below it: a panel never learns it has
|
|
108
|
+
one, and its rows hit-test exactly as they did. */
|
|
109
|
+
title.absolute = true;
|
|
110
|
+
title.x = 0;
|
|
111
|
+
title.y = top;
|
|
112
|
+
title.width = w;
|
|
113
|
+
title.height = Math.min(PANEL_TITLE_HEIGHT, tall);
|
|
85
114
|
panelRoot.absolute = true;
|
|
86
115
|
panelRoot.x = 0;
|
|
87
|
-
panelRoot.y = top;
|
|
116
|
+
panelRoot.y = top + PANEL_TITLE_HEIGHT;
|
|
88
117
|
panelRoot.width = w;
|
|
89
|
-
panelRoot.height = tall;
|
|
118
|
+
panelRoot.height = Math.max(0, tall - PANEL_TITLE_HEIGHT);
|
|
90
119
|
panel.build(panelRoot);
|
|
91
|
-
root.children.push(panelRoot);
|
|
120
|
+
root.children.push(title, panelRoot);
|
|
92
121
|
sites.push({ id: panel.id, x: 0, y: top, w, h: tall });
|
|
93
122
|
}
|
|
94
123
|
layoutUiTree(root, x, 0, w, height);
|
|
@@ -149,13 +178,14 @@ export function createToolsOverlay(options) {
|
|
|
149
178
|
if (!visible)
|
|
150
179
|
return false;
|
|
151
180
|
if (event.kind === 'key') {
|
|
152
|
-
|
|
181
|
+
/* Either case: a browser reports Ctrl+Shift+Z with the key `'Z'`, shift having raised it. */
|
|
182
|
+
if (event.ctrl && (event.key === 'z' || event.key === 'Z')) {
|
|
153
183
|
const moved = event.shift ? undo.redo() : undo.undo();
|
|
154
184
|
if (moved)
|
|
155
185
|
dirty = true;
|
|
156
186
|
return moved;
|
|
157
187
|
}
|
|
158
|
-
if (event.ctrl && event.key === 'y') {
|
|
188
|
+
if (event.ctrl && (event.key === 'y' || event.key === 'Y')) {
|
|
159
189
|
const moved = undo.redo();
|
|
160
190
|
if (moved)
|
|
161
191
|
dirty = true;
|
|
@@ -174,8 +204,18 @@ export function createToolsOverlay(options) {
|
|
|
174
204
|
const panel = panelWith(site.id);
|
|
175
205
|
if (panel === null)
|
|
176
206
|
return false;
|
|
207
|
+
const contentTop = site.y + PANEL_TITLE_HEIGHT;
|
|
208
|
+
/* The title row is the overlay's, not the panel's: it scrolls nothing and routes nothing. */
|
|
209
|
+
if (event.y < contentTop)
|
|
210
|
+
return event.kind === 'wheel';
|
|
177
211
|
/* In the panel's own space: a panel never learns where on screen it is. */
|
|
178
|
-
|
|
212
|
+
const handled = take(panel.route(shift(event, -(root.rect.x + site.x), -contentTop)));
|
|
213
|
+
if (event.kind !== 'wheel')
|
|
214
|
+
return handled;
|
|
215
|
+
/* A wheel over a panel scrolls the panel and is the overlay's whether or not it made a
|
|
216
|
+
command, or the page scrolls under the panel as well. Laid out again, for the new scroll. */
|
|
217
|
+
dirty = true;
|
|
218
|
+
return true;
|
|
179
219
|
},
|
|
180
220
|
invalidate() {
|
|
181
221
|
dirty = true;
|
|
@@ -263,11 +303,13 @@ function paintNode(painter, node) {
|
|
|
263
303
|
if (node.clip)
|
|
264
304
|
painter.unclip();
|
|
265
305
|
}
|
|
306
|
+
/** `#rrggbb`, or `#rrggbbaa` where the colour is not opaque, which the painter's contract allows. */
|
|
266
307
|
function cssColour(rgba) {
|
|
267
308
|
if (rgba === null)
|
|
268
309
|
return '#ffffff';
|
|
269
310
|
let out = '#';
|
|
270
|
-
|
|
311
|
+
const channels = (rgba[3] ?? 1) < 1 ? 4 : 3;
|
|
312
|
+
for (let at = 0; at < channels; at += 1) {
|
|
271
313
|
const channel = Math.round(Math.min(1, Math.max(0, rgba[at] ?? 0)) * 255);
|
|
272
314
|
out += channel.toString(16).padStart(2, '0');
|
|
273
315
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/tools",
|
|
3
|
-
"version": "4.8.
|
|
3
|
+
"version": "4.8.2",
|
|
4
4
|
"description": "The editor panels a shipped game can carry, and the command stack that makes their edits undoable",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"peerDependencies": {
|
|
30
|
-
"@driftengine/core": "4.8.
|
|
31
|
-
"@driftengine/editor": "4.8.
|
|
32
|
-
"@driftengine/network": "4.8.
|
|
33
|
-
"@driftengine/texture": "4.8.
|
|
34
|
-
"@driftengine/ui2d": "4.8.
|
|
30
|
+
"@driftengine/core": "4.8.2",
|
|
31
|
+
"@driftengine/editor": "4.8.2",
|
|
32
|
+
"@driftengine/network": "4.8.2",
|
|
33
|
+
"@driftengine/texture": "4.8.2",
|
|
34
|
+
"@driftengine/ui2d": "4.8.2"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|
package/src/console.ts
CHANGED
|
@@ -215,10 +215,14 @@ export const consolePanel: Panel<ConsoleWorld, ConsoleView> = {
|
|
|
215
215
|
title: 'Console',
|
|
216
216
|
|
|
217
217
|
build(world, view, root): void {
|
|
218
|
+
/* The window is the height the panel was given, where it was given one: the overlay sets it on
|
|
219
|
+
the root before it builds. `viewHeight` is what a root with no exact height falls back to. */
|
|
220
|
+
if (typeof root.height === 'number' && root.height > 0) view.viewHeight = root.height;
|
|
218
221
|
view.shown = filteredEntries(world.log, view.filter);
|
|
219
222
|
if (view.shown.length === 0) {
|
|
220
223
|
emptyPanel(root, 'Nothing logged');
|
|
221
224
|
view.scrollY = 0;
|
|
225
|
+
root.scrollY = 0;
|
|
222
226
|
return;
|
|
223
227
|
}
|
|
224
228
|
|
|
@@ -238,6 +242,8 @@ export const consolePanel: Panel<ConsoleWorld, ConsoleView> = {
|
|
|
238
242
|
}
|
|
239
243
|
/* The follow happens here and not on append, because only a build knows how tall the list is. */
|
|
240
244
|
if (view.pinned) view.scrollY = maxScroll(view, view.shown.length);
|
|
245
|
+
/* And onto the root, which is what ui2d offsets the rows by: the scroll that is drawn. */
|
|
246
|
+
root.scrollY = view.scrollY;
|
|
241
247
|
},
|
|
242
248
|
|
|
243
249
|
route(_world, view, event): Command | null {
|
|
@@ -248,6 +254,7 @@ export const consolePanel: Panel<ConsoleWorld, ConsoleView> = {
|
|
|
248
254
|
const limit = maxScroll(view, view.shown.length);
|
|
249
255
|
view.scrollY = Math.min(limit, Math.max(0, view.scrollY + event.dy));
|
|
250
256
|
view.pinned = view.scrollY >= limit;
|
|
257
|
+
view.root.scrollY = view.scrollY;
|
|
251
258
|
return null;
|
|
252
259
|
}
|
|
253
260
|
if (event.kind !== 'pointer' || event.phase !== 'up' || event.button !== 0) return null;
|
package/src/index.ts
CHANGED
package/src/inspector.ts
CHANGED
|
@@ -48,7 +48,10 @@ export interface InspectorRow {
|
|
|
48
48
|
readonly component: string;
|
|
49
49
|
/** What the row is called: a field's name, or the prefix the vector's three share. */
|
|
50
50
|
readonly label: string;
|
|
51
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The declared type, verbatim. For a vector, the type its three parts share, or empty where they
|
|
53
|
+
* do not share one.
|
|
54
|
+
*/
|
|
52
55
|
readonly type: string;
|
|
53
56
|
readonly kind: RowKind;
|
|
54
57
|
readonly enumName: string;
|
|
@@ -153,10 +156,13 @@ export function inspectorRows(
|
|
|
153
156
|
if (emitted.has(part.prefix)) continue;
|
|
154
157
|
emitted.add(part.prefix);
|
|
155
158
|
const slots = vectors.get(part.prefix) as string[];
|
|
159
|
+
const types = new Set(
|
|
160
|
+
fields.filter((one) => slots.includes(one.name)).map((one) => one.type),
|
|
161
|
+
);
|
|
156
162
|
rows.push({
|
|
157
163
|
component,
|
|
158
164
|
label: part.prefix,
|
|
159
|
-
type: '',
|
|
165
|
+
type: types.size === 1 ? field.type : '',
|
|
160
166
|
kind: 'vector',
|
|
161
167
|
enumName: '',
|
|
162
168
|
fields: slots,
|
|
@@ -307,11 +313,32 @@ export function createInspectorView(options: {
|
|
|
307
313
|
export function rowText(row: InspectorRow): string {
|
|
308
314
|
if (row.value === MIXED) return `${row.label} —`;
|
|
309
315
|
if (row.kind === 'vector') {
|
|
310
|
-
const parts = (row.value as unknown[]).map((part) =>
|
|
316
|
+
const parts = (row.value as unknown[]).map((part) =>
|
|
317
|
+
part === MIXED ? '—' : valueText(part, row.type),
|
|
318
|
+
);
|
|
311
319
|
return `${row.label} ${parts.join(', ')}`;
|
|
312
320
|
}
|
|
313
321
|
if (row.kind === 'boolean') return `${row.label} ${row.value ? 'true' : 'false'}`;
|
|
314
|
-
return `${row.label} ${
|
|
322
|
+
return `${row.label} ${valueText(row.value, row.type)}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* One value as text: an `f32` as the shortest decimal that stores as the same `f32`.
|
|
327
|
+
*
|
|
328
|
+
* **What was written rather than what is stored.** A `Float32Array` hands back the nearest double,
|
|
329
|
+
* so a bounce of 0.55 reads `0.550000011920929`; the digits past the seventh are the storage, and
|
|
330
|
+
* the shortest decimal that rounds back to the same `f32` is what anybody wrote. Nine significant
|
|
331
|
+
* digits always round-trip a single, so the search ends there. An `f64` and anything else keep
|
|
332
|
+
* `String`, whose own answer is already the shortest for a double.
|
|
333
|
+
*/
|
|
334
|
+
function valueText(value: unknown, type: string): string {
|
|
335
|
+
if (type !== 'f32' || typeof value !== 'number' || !Number.isFinite(value)) return String(value);
|
|
336
|
+
const stored = Math.fround(value);
|
|
337
|
+
for (let digits = 1; digits < 9; digits += 1) {
|
|
338
|
+
const shorter = Number(stored.toPrecision(digits));
|
|
339
|
+
if (Math.fround(shorter) === stored) return String(shorter);
|
|
340
|
+
}
|
|
341
|
+
return String(Number(stored.toPrecision(9)));
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
export const inspectorPanel: Panel<InspectorWorld, InspectorView> = {
|
package/src/network.ts
CHANGED
|
@@ -190,12 +190,21 @@ export const networkPanel: Panel<NetworkWorld, NetworkView> = {
|
|
|
190
190
|
* Structural rather than imported, so nothing here loads `@driftengine/network` at run time and a
|
|
191
191
|
* consumer whose session is its own shape can still be read.
|
|
192
192
|
*/
|
|
193
|
-
|
|
193
|
+
/**
|
|
194
|
+
* A session as the network panel reads it, which a `LockstepSession` is as it stands.
|
|
195
|
+
*
|
|
196
|
+
* **The depth by either name**: `rewindDepth` is what a session answers, and `loop.depth` is the
|
|
197
|
+
* shape a game built by hand before it did — from the session's fields and the `RewindLoop` it
|
|
198
|
+
* made — which still reads as it did.
|
|
199
|
+
*/
|
|
200
|
+
export type SessionLike = SessionFields &
|
|
201
|
+
({ readonly rewindDepth: number } | { readonly loop: { readonly depth: number } });
|
|
202
|
+
|
|
203
|
+
interface SessionFields {
|
|
194
204
|
readonly participants: number;
|
|
195
205
|
readonly inputDelay: number;
|
|
196
206
|
/** The disagreement the session is currently holding, or null. Latched, not an event. */
|
|
197
207
|
readonly desync: Desync | null;
|
|
198
|
-
readonly loop: { readonly depth: number };
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
/** What a recorder keeps: the disagreements it has seen, oldest first. */
|
|
@@ -246,7 +255,7 @@ export function sessionReadout(
|
|
|
246
255
|
inputDelay: session.inputDelay,
|
|
247
256
|
participants: session.participants,
|
|
248
257
|
snapshotBytes,
|
|
249
|
-
snapshotCount: session.loop.depth,
|
|
258
|
+
snapshotCount: 'rewindDepth' in session ? session.rewindDepth : session.loop.depth,
|
|
250
259
|
desyncs: recorder.desyncs,
|
|
251
260
|
componentHashes,
|
|
252
261
|
};
|
package/src/overlay.ts
CHANGED
|
@@ -111,6 +111,20 @@ export interface ToolsOverlay {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const DEFAULT_WIDTH = 320;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* How tall the title row above every panel is, in the overlay's pixels. A panel's own space starts
|
|
117
|
+
* below it, so a host hit-testing a site by hand reads the content from `site.y` plus this.
|
|
118
|
+
*/
|
|
119
|
+
export const PANEL_TITLE_HEIGHT = 18;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* What every panel is drawn on, and its title on: dark and nearly opaque, so the rows read over a
|
|
123
|
+
* bright scene. The overlay carried no background until 4.8.2 and a host laid one under each site.
|
|
124
|
+
*/
|
|
125
|
+
const PANEL_BACKGROUND = new Float32Array([0.063, 0.071, 0.094, 0.9]);
|
|
126
|
+
const TITLE_BACKGROUND = new Float32Array([0.122, 0.133, 0.176, 0.95]);
|
|
127
|
+
const TITLE_TINT = new Float32Array([0.62, 0.66, 0.75, 1]);
|
|
114
128
|
const DEFAULT_KEY = 'F3';
|
|
115
129
|
|
|
116
130
|
export function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay {
|
|
@@ -122,6 +136,7 @@ export function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay {
|
|
|
122
136
|
|
|
123
137
|
const panels = options.panels;
|
|
124
138
|
const roots = new Map<string, UiNode>();
|
|
139
|
+
const titles = new Map<string, UiNode>();
|
|
125
140
|
/* Pooled and refilled: this is read every frame the overlay is open and must not allocate. */
|
|
126
141
|
const sites: OverlaySite[] = [];
|
|
127
142
|
|
|
@@ -154,17 +169,33 @@ export function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay {
|
|
|
154
169
|
const tall = at === panels.length - 1 ? height - top : each;
|
|
155
170
|
|
|
156
171
|
let panelRoot = roots.get(panel.id);
|
|
157
|
-
|
|
172
|
+
let title = titles.get(panel.id);
|
|
173
|
+
if (panelRoot === undefined || title === undefined) {
|
|
158
174
|
panelRoot = createPanelRoot(panel);
|
|
175
|
+
panelRoot.background = PANEL_BACKGROUND;
|
|
159
176
|
roots.set(panel.id, panelRoot);
|
|
177
|
+
title = createUiNode({
|
|
178
|
+
text: panel.title,
|
|
179
|
+
background: TITLE_BACKGROUND,
|
|
180
|
+
tint: TITLE_TINT,
|
|
181
|
+
name: `title:${panel.id}`,
|
|
182
|
+
});
|
|
183
|
+
titles.set(panel.id, title);
|
|
160
184
|
}
|
|
185
|
+
/* The title above the panel's own space, which starts below it: a panel never learns it has
|
|
186
|
+
one, and its rows hit-test exactly as they did. */
|
|
187
|
+
title.absolute = true;
|
|
188
|
+
title.x = 0;
|
|
189
|
+
title.y = top;
|
|
190
|
+
title.width = w;
|
|
191
|
+
title.height = Math.min(PANEL_TITLE_HEIGHT, tall);
|
|
161
192
|
panelRoot.absolute = true;
|
|
162
193
|
panelRoot.x = 0;
|
|
163
|
-
panelRoot.y = top;
|
|
194
|
+
panelRoot.y = top + PANEL_TITLE_HEIGHT;
|
|
164
195
|
panelRoot.width = w;
|
|
165
|
-
panelRoot.height = tall;
|
|
196
|
+
panelRoot.height = Math.max(0, tall - PANEL_TITLE_HEIGHT);
|
|
166
197
|
panel.build(panelRoot);
|
|
167
|
-
root.children.push(panelRoot);
|
|
198
|
+
root.children.push(title, panelRoot);
|
|
168
199
|
sites.push({ id: panel.id, x: 0, y: top, w, h: tall });
|
|
169
200
|
}
|
|
170
201
|
|
|
@@ -226,12 +257,13 @@ export function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay {
|
|
|
226
257
|
if (!visible) return false;
|
|
227
258
|
|
|
228
259
|
if (event.kind === 'key') {
|
|
229
|
-
|
|
260
|
+
/* Either case: a browser reports Ctrl+Shift+Z with the key `'Z'`, shift having raised it. */
|
|
261
|
+
if (event.ctrl && (event.key === 'z' || event.key === 'Z')) {
|
|
230
262
|
const moved = event.shift ? undo.redo() : undo.undo();
|
|
231
263
|
if (moved) dirty = true;
|
|
232
264
|
return moved;
|
|
233
265
|
}
|
|
234
|
-
if (event.ctrl && event.key === 'y') {
|
|
266
|
+
if (event.ctrl && (event.key === 'y' || event.key === 'Y')) {
|
|
235
267
|
const moved = undo.redo();
|
|
236
268
|
if (moved) dirty = true;
|
|
237
269
|
return moved;
|
|
@@ -246,8 +278,16 @@ export function createToolsOverlay(options: ToolsOverlayOptions): ToolsOverlay {
|
|
|
246
278
|
if (event.kind === 'pointer' && event.phase === 'down') focused = site.id;
|
|
247
279
|
const panel = panelWith(site.id);
|
|
248
280
|
if (panel === null) return false;
|
|
281
|
+
const contentTop = site.y + PANEL_TITLE_HEIGHT;
|
|
282
|
+
/* The title row is the overlay's, not the panel's: it scrolls nothing and routes nothing. */
|
|
283
|
+
if (event.y < contentTop) return event.kind === 'wheel';
|
|
249
284
|
/* In the panel's own space: a panel never learns where on screen it is. */
|
|
250
|
-
|
|
285
|
+
const handled = take(panel.route(shift(event, -(root.rect.x + site.x), -contentTop)));
|
|
286
|
+
if (event.kind !== 'wheel') return handled;
|
|
287
|
+
/* A wheel over a panel scrolls the panel and is the overlay's whether or not it made a
|
|
288
|
+
command, or the page scrolls under the panel as well. Laid out again, for the new scroll. */
|
|
289
|
+
dirty = true;
|
|
290
|
+
return true;
|
|
251
291
|
},
|
|
252
292
|
|
|
253
293
|
invalidate(): void {
|
|
@@ -331,10 +371,12 @@ function paintNode(painter: OverlayPainter, node: UiNode): void {
|
|
|
331
371
|
if (node.clip) painter.unclip();
|
|
332
372
|
}
|
|
333
373
|
|
|
374
|
+
/** `#rrggbb`, or `#rrggbbaa` where the colour is not opaque, which the painter's contract allows. */
|
|
334
375
|
function cssColour(rgba: Float32Array | null): string {
|
|
335
376
|
if (rgba === null) return '#ffffff';
|
|
336
377
|
let out = '#';
|
|
337
|
-
|
|
378
|
+
const channels = (rgba[3] ?? 1) < 1 ? 4 : 3;
|
|
379
|
+
for (let at = 0; at < channels; at += 1) {
|
|
338
380
|
const channel = Math.round(Math.min(1, Math.max(0, rgba[at] ?? 0)) * 255);
|
|
339
381
|
out += channel.toString(16).padStart(2, '0');
|
|
340
382
|
}
|