@el4cteo/rbx-studio-mcp 0.4.5 → 0.5.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/README.md +46 -4
- package/dist/bridge/api.js +22 -3
- package/dist/bridge/api.js.map +1 -1
- package/dist/bridge/console.js +279 -0
- package/dist/bridge/console.js.map +1 -0
- package/dist/bridge/harness.js +513 -0
- package/dist/bridge/harness.js.map +1 -0
- package/dist/bridge/remote.js +9 -1
- package/dist/bridge/remote.js.map +1 -1
- package/dist/bridge/rpc.js +69 -4
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/bridge/server.js +54 -6
- package/dist/bridge/server.js.map +1 -1
- package/dist/doctor.js +13 -1
- package/dist/doctor.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/protocol.js.map +1 -1
- package/dist/tools/screenshot.js +7 -6
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/terrain.js +116 -0
- package/dist/tools/terrain.js.map +1 -0
- package/package.json +4 -4
- package/plugin/src/Commands.luau +524 -0
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +282 -49
- package/plugin/src/Format.luau +114 -0
- package/plugin/src/Prompt.luau +436 -0
- package/plugin/src/ScriptEdit.luau +48 -0
- package/plugin/src/Visuals.luau +1409 -1332
- package/plugin/src/handlers/Capture.luau +68 -20
- package/plugin/src/handlers/Terrain.luau +371 -0
- package/plugin/src/init.server.luau +829 -725
- package/scripts/test-console.mjs +293 -0
- package/scripts/test-failover.mjs +47 -0
- package/scripts/test-plugin.mjs +85 -82
package/plugin/src/Visuals.luau
CHANGED
|
@@ -1,1332 +1,1409 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
The console's activity band: a themed cell beside a live latency trace.
|
|
4
|
-
|
|
5
|
-
Watching an agent work is mostly waiting, and the log answers "what is it
|
|
6
|
-
doing" precisely while answering "is it still alive" badly -- a stalled
|
|
7
|
-
session and a thinking one produce the same still screen.
|
|
8
|
-
|
|
9
|
-
This began as a full-panel overlay and that was wrong. It sat on top of the
|
|
10
|
-
log, so the thing you actually read was covered and, being slightly
|
|
11
|
-
transparent, softened underneath. Decoration that costs legibility is a bad
|
|
12
|
-
trade however good it looks. So it is a band now: a fixed strip above the
|
|
13
|
-
log, which keeps every line at full contrast and never moves under the
|
|
14
|
-
reader. The log shrinks by the band's height and nothing overlaps.
|
|
15
|
-
|
|
16
|
-
The strip earns its space twice over. The cell on the left says the session
|
|
17
|
-
is live; the trace beside it is the last forty calls, each drawn as tall as
|
|
18
|
-
it was slow and coloured by whether it worked. That turns "is it alive" and
|
|
19
|
-
"is it healthy" into one glance, which is more than the log gives and more
|
|
20
|
-
than an animation alone would.
|
|
21
|
-
|
|
22
|
-
WHAT THIS FILE OWNS, since it used to own everything: the strip's Instances,
|
|
23
|
-
the pointer handling on the trace, and the simulation -- energy, activity,
|
|
24
|
-
flare, shake, the scale spring, the quiet timer and the blended tint. What
|
|
25
|
-
it does NOT own is any drawing inside the cell. That belongs to whichever
|
|
26
|
-
preset is active (see Themes/), which is handed the simulation each frame
|
|
27
|
-
and paints whatever it likes with it. Eight presets are therefore eight
|
|
28
|
-
painters over one set of physics rather than eight animation systems, and
|
|
29
|
-
only one of them is ever mounted.
|
|
30
|
-
]]
|
|
31
|
-
|
|
32
|
-
local RunService = game:GetService("RunService")
|
|
33
|
-
|
|
34
|
-
local Themes = require(script.Parent.Themes)
|
|
35
|
-
|
|
36
|
-
local Visuals = {}
|
|
37
|
-
|
|
38
|
-
-- How long the session must go without a command before the cell winds down.
|
|
39
|
-
-- Matches the console's idle notice, so the picture and the log agree.
|
|
40
|
-
local QUIET_AFTER = 20
|
|
41
|
-
|
|
42
|
-
--[[
|
|
43
|
-
Spring constants for the scale. Stiff enough that a call reads as a snap
|
|
44
|
-
rather than a swell, damped just under critical so a reply overshoots once
|
|
45
|
-
and settles instead of ringing.
|
|
46
|
-
]]
|
|
47
|
-
local SPRING = 90
|
|
48
|
-
local DAMPING = 14
|
|
49
|
-
|
|
50
|
-
-- How many calls the trace remembers. Forty is about a screen's width of bars
|
|
51
|
-
-- at five pixels each, and about as far back as anyone reads a trend.
|
|
52
|
-
local TRACE = 40
|
|
53
|
-
|
|
54
|
-
-- Bars are scaled against this, so a normal call is a low bar and a slow one is
|
|
55
|
-
-- obvious. Anything past it clamps to full height rather than flattening the
|
|
56
|
-
-- rest of the trace into invisibility.
|
|
57
|
-
local SLOW_MS = 400
|
|
58
|
-
|
|
59
|
-
-- Pixels from one bar to the next. Also the width of a bar's hit target and of
|
|
60
|
-
-- the hover highlight, so pointing between two bars still selects one of them
|
|
61
|
-
-- rather than falling into a dead gap.
|
|
62
|
-
local BAR_STRIDE = 5
|
|
63
|
-
|
|
64
|
-
-- Height of the trace, and so of every bar's hit column. The taller band spends
|
|
65
|
-
-- its extra pixels here: a 19px column was a small thing to aim a pointer at.
|
|
66
|
-
local TRACE_HEIGHT = 26
|
|
67
|
-
|
|
68
|
-
--[[
|
|
69
|
-
The connecting wave: what the trace does while there is no session to plot.
|
|
70
|
-
|
|
71
|
-
A connect or a reconnect is the one moment the trace has nothing true to
|
|
72
|
-
say. No calls have happened yet, so it is a bare baseline -- which looks
|
|
73
|
-
exactly like a panel that has died, at the one time the user is most likely
|
|
74
|
-
to be watching it to find out whether it has.
|
|
75
|
-
|
|
76
|
-
So the columns wave instead: a swell travelling along the trace for as long
|
|
77
|
-
as the transport is reaching for the bridge, and gone the moment it either
|
|
78
|
-
arrives or gives up.
|
|
79
|
-
|
|
80
|
-
It is drawn THROUGH THE ACTIVE PRESET's slot painter rather than painted
|
|
81
|
-
here, for the same reason history is. Eight presets already know how to draw
|
|
82
|
-
a column of a given height and freshness, so routing the wave through them
|
|
83
|
-
means it arrives in all eight without eight implementations of it -- a theme
|
|
84
|
-
that draws history as rising points draws the wave as an arc of points, and
|
|
85
|
-
nobody had to tell it to.
|
|
86
|
-
]]
|
|
87
|
-
--[[
|
|
88
|
-
ONE GRID, shared with the history. This is the whole trick.
|
|
89
|
-
|
|
90
|
-
The wave used to sit on its own grid -- a fixed count of columns spread by
|
|
91
|
-
scale across the trace -- and history sits on a stride grid anchored to the
|
|
92
|
-
right edge. Two grids in one strip is two instruments in one strip: on a
|
|
93
|
-
reconnect the wave's columns landed between and behind the bars at a
|
|
94
|
-
different spacing, which read as interference rather than as one surface.
|
|
95
|
-
|
|
96
|
-
So the trace is now a single row of BAR_STRIDE-spaced slots, numbered 1 at
|
|
97
|
-
the right edge and rising leftwards. History fills slots 1..n. The wave
|
|
98
|
-
fills every slot LEFT of those and never one of them, so the two can no
|
|
99
|
-
longer overlap by construction rather than by draw order. And because they
|
|
100
|
-
share the numbering, the same swell that lifts a wave column also lifts the
|
|
101
|
-
bars it rolls over -- the history rides the wave instead of standing still
|
|
102
|
-
in the middle of it.
|
|
103
|
-
]]
|
|
104
|
-
-- The most slots the wave will ever fill, which bounds the pool a very wide
|
|
105
|
-
-- panel can ask for.
|
|
106
|
-
local WAVE_MAX = 160
|
|
107
|
-
-- Seconds for one swell to cross the trace.
|
|
108
|
-
local WAVE_PERIOD = 1.8
|
|
109
|
-
-- The swell's width as a fraction of the trace. Narrow enough to read as a
|
|
110
|
-
-- pulse travelling along it rather than as the whole trace breathing.
|
|
111
|
-
local WAVE_SPREAD = 0.2
|
|
112
|
-
--[[
|
|
113
|
-
The fine ripple riding on the swell, as SLOTS PER CYCLE rather than cycles
|
|
114
|
-
per trace.
|
|
115
|
-
|
|
116
|
-
Fixed cycles made the texture a function of the panel's width: the same wave
|
|
117
|
-
was a coarse comb in a narrow dock and a smooth featureless hump in a wide
|
|
118
|
-
one. Per-slot keeps a ripple the same physical size at any width, which is
|
|
119
|
-
what makes it read as strips moving at all.
|
|
120
|
-
]]
|
|
121
|
-
local WAVE_RIPPLE_STRIDE = 16
|
|
122
|
-
--[[
|
|
123
|
-
How far the swell lifts a history bar, against the headroom that bar has
|
|
124
|
-
left.
|
|
125
|
-
|
|
126
|
-
Additive and proportional, not a replacement. A bar already near the top of
|
|
127
|
-
the trace barely moves and a stub moves nearly the full amount, so the wave
|
|
128
|
-
is visible across the history without flattening the profile that history is
|
|
129
|
-
there to show. The hover readout reports the recorded milliseconds either
|
|
130
|
-
way -- what the wave changes is the drawing, never the reading.
|
|
131
|
-
]]
|
|
132
|
-
local WAVE_LIFT = 0.8
|
|
133
|
-
-- Fade rates, in and out. Out is the slower of the two: an attempt that fails
|
|
134
|
-
-- immediately should ebb rather than blink off.
|
|
135
|
-
local WAVE_IN = 7
|
|
136
|
-
local WAVE_OUT = 2.5
|
|
137
|
-
--[[
|
|
138
|
-
The shortest time the wave stays up once it has started.
|
|
139
|
-
|
|
140
|
-
A handshake against a port with nothing behind it fails in milliseconds, and
|
|
141
|
-
the transport reports "connecting" before every one of those attempts. Without
|
|
142
|
-
a floor the wave would be a single-frame flash per retry, which reads as a
|
|
143
|
-
glitch rather than as an attempt being made.
|
|
144
|
-
]]
|
|
145
|
-
local WAVE_HOLD = 0.7
|
|
146
|
-
-- Where the columns rest when the wave is not lifting them. Low enough to read
|
|
147
|
-
-- as an instrument waiting rather than as invented data, high enough to be
|
|
148
|
-
-- there at all.
|
|
149
|
-
local WAVE_FLOOR = 0.05
|
|
150
|
-
|
|
151
|
-
--[[
|
|
152
|
-
The arrival: what the trace does the moment a session actually exists.
|
|
153
|
-
|
|
154
|
-
The connecting wave is a searching motion and says so -- the same swell over
|
|
155
|
-
and over, going nowhere. Landing deserves a different shape, once, or the
|
|
156
|
-
only thing that marks the event is a word in the header going green.
|
|
157
|
-
|
|
158
|
-
So the columns are struck rather than swept. A bright front races along the
|
|
159
|
-
trace and every column it reaches leaps and rings down behind it, which
|
|
160
|
-
reads as a thing arriving rather than a thing looking. It plays once per
|
|
161
|
-
connection and cannot repeat on its own -- the status line only fires it on
|
|
162
|
-
a real transition into `connected`, so a theme switch or a port change
|
|
163
|
-
replaying the same status does not replay the celebration.
|
|
164
|
-
]]
|
|
165
|
-
-- How long the whole flourish lasts.
|
|
166
|
-
local CHEER_TIME = 1.4
|
|
167
|
-
-- The fraction of that time the front takes to cross the trace. Well under
|
|
168
|
-
-- half: the sweep should outrun the ringing rather than drag it along.
|
|
169
|
-
local CHEER_SWEEP = 0.38
|
|
170
|
-
-- How fast a struck column settles. Against the sweep above this leaves about
|
|
171
|
-
-- two visible bounces before it is back on the baseline.
|
|
172
|
-
local CHEER_DECAY = 3.2
|
|
173
|
-
-- Bounces per column, over the flourish's own length.
|
|
174
|
-
local CHEER_RING = 2.5
|
|
175
|
-
--[[
|
|
176
|
-
How abruptly the flourish fades.
|
|
177
|
-
|
|
178
|
-
Opacity holds at full for the first stretch and only gives way at the end,
|
|
179
|
-
so the celebration reads as ringing out rather than as being dimmed while it
|
|
180
|
-
is still moving. `1 / CHEER_FADE` is the fraction of the tail spent fading.
|
|
181
|
-
]]
|
|
182
|
-
local CHEER_FADE = 2.5
|
|
183
|
-
|
|
184
|
-
--[[
|
|
185
|
-
The strip's height, and the geometry around the cell.
|
|
186
|
-
|
|
187
|
-
The cell was originally as tall as the band and read as looming; it crowded
|
|
188
|
-
the caption and made the strip feel like a panel. It is an indicator, not the
|
|
189
|
-
subject, so it sits small with air around it.
|
|
190
|
-
|
|
191
|
-
INSET and GUTTER both grew after the cell stopped being a wireframe and
|
|
192
|
-
started being eight different things. At six pixels from the panel edge the
|
|
193
|
-
cell was pressed into the corner, which reads as cramped for a prism and
|
|
194
|
-
simply wrong for a starfield -- a sky needs a horizon around it, not a
|
|
195
|
-
border. The band grew with them so the extra margin comes out of empty space
|
|
196
|
-
rather than out of the trace, whose bars are what people try to point at.
|
|
197
|
-
]]
|
|
198
|
-
Visuals.BAND_HEIGHT = 58
|
|
199
|
-
local CELL = 40
|
|
200
|
-
local INSET = 12
|
|
201
|
-
local GUTTER = 22
|
|
202
|
-
|
|
203
|
-
type Bar = {
|
|
204
|
-
frame: Frame,
|
|
205
|
-
--[[
|
|
206
|
-
An invisible column over the bar, full height of the trace, wide enough to
|
|
207
|
-
hit. The bar itself is three pixels wide and usually one pixel tall -- a
|
|
208
|
-
fast call is a stub on the baseline -- so hovering the bar is not something
|
|
209
|
-
a person can reliably do. The hit target is the whole column above it, so
|
|
210
|
-
pointing anywhere near a bar selects it.
|
|
211
|
-
]]
|
|
212
|
-
hit: TextButton,
|
|
213
|
-
milliseconds: number,
|
|
214
|
-
ok: boolean,
|
|
215
|
-
--[[
|
|
216
|
-
What ran, in the same words the log uses.
|
|
217
|
-
|
|
218
|
-
The trace plotted durations with nothing to attach them to, so a tall bar
|
|
219
|
-
raised the question it could not answer: forty numbers and no way to tell
|
|
220
|
-
which of them was the script edit. Carried per bar rather than looked up,
|
|
221
|
-
because the bar outlives whatever produced it.
|
|
222
|
-
]]
|
|
223
|
-
title: string,
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
type Runtime = {
|
|
227
|
-
band: Frame?,
|
|
228
|
-
cell: Frame?,
|
|
229
|
-
trace: Frame?,
|
|
230
|
-
divider: Frame?,
|
|
231
|
-
baseline: Frame?,
|
|
232
|
-
highlight: Frame?,
|
|
233
|
-
readout: TextLabel?,
|
|
234
|
-
caption: TextLabel?,
|
|
235
|
-
--[[
|
|
236
|
-
What the caption said before a note took it over.
|
|
237
|
-
|
|
238
|
-
Nil means no note is showing, which is why this is the flag as well as
|
|
239
|
-
the storage: a second `showNote` while one is up must not overwrite the
|
|
240
|
-
text being held, or clearing would restore a note instead of the
|
|
241
|
-
caption.
|
|
242
|
-
]]
|
|
243
|
-
noted: string?,
|
|
244
|
-
bars: { Bar },
|
|
245
|
-
connection: RBXScriptConnection?,
|
|
246
|
-
|
|
247
|
-
spin: Vector3,
|
|
248
|
-
angle: Vector3,
|
|
249
|
-
|
|
250
|
-
energy: number,
|
|
251
|
-
shake: number,
|
|
252
|
-
--[[
|
|
253
|
-
The cell's size, as a spring rather than a value.
|
|
254
|
-
|
|
255
|
-
Scale was previously a small function of energy -- a 24% range that
|
|
256
|
-
nobody could see. Driving it as a spring means each event can push it and
|
|
257
|
-
let physics do the rest: a dispatch pulls it in, a reply throws it out
|
|
258
|
-
past its resting size, and it settles on its own.
|
|
259
|
-
]]
|
|
260
|
-
scale: number,
|
|
261
|
-
scaleVelocity: number,
|
|
262
|
-
-- Spikes to 1 the instant a reply lands, gone in a third of a second.
|
|
263
|
-
flare: number,
|
|
264
|
-
-- Seconds since anything last ran. Drives the wind-down.
|
|
265
|
-
quiet: number,
|
|
266
|
-
tint: Color3,
|
|
267
|
-
targetTint: Color3,
|
|
268
|
-
baseTint: Color3,
|
|
269
|
-
activeTint: Color3,
|
|
270
|
-
activity: number,
|
|
271
|
-
lastTitle: string?,
|
|
272
|
-
-- Whether the active preset has been given its Instances yet.
|
|
273
|
-
mounted: boolean,
|
|
274
|
-
|
|
275
|
-
-- The wave's columns, pooled at mount and hidden until a connect needs them.
|
|
276
|
-
wave: { Frame },
|
|
277
|
-
-- What the transport last said: true while it is trying to reach the bridge.
|
|
278
|
-
waveWanted: boolean,
|
|
279
|
-
-- The ramp, 0 to 1. Separate from `waveWanted` so the wave fades rather than
|
|
280
|
-
-- appearing and vanishing with the status line.
|
|
281
|
-
waveLevel: number,
|
|
282
|
-
-- Seconds the wave is held up regardless of `waveWanted`. See WAVE_HOLD.
|
|
283
|
-
waveHold: number,
|
|
284
|
-
-- Whether the columns are currently on screen. Kept rather than derived, so
|
|
285
|
-
-- a settled wave hides them once instead of every frame afterwards.
|
|
286
|
-
waveShown: boolean,
|
|
287
|
-
-- The arrival flourish: 1 the instant a session connects, down to 0 over
|
|
288
|
-
-- CHEER_TIME. Overrides the connecting wave's shape while it runs.
|
|
289
|
-
cheer: number,
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
local
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
if runtime.
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
local
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
A
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
--[[
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
local
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
--[[
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
]]
|
|
776
|
-
local
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
--
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
for the same reason
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
end
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
--
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
--
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
runtime.
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
--
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
if
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
local
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
cell
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
runtime.
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
local
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
The console's activity band: a themed cell beside a live latency trace.
|
|
4
|
+
|
|
5
|
+
Watching an agent work is mostly waiting, and the log answers "what is it
|
|
6
|
+
doing" precisely while answering "is it still alive" badly -- a stalled
|
|
7
|
+
session and a thinking one produce the same still screen.
|
|
8
|
+
|
|
9
|
+
This began as a full-panel overlay and that was wrong. It sat on top of the
|
|
10
|
+
log, so the thing you actually read was covered and, being slightly
|
|
11
|
+
transparent, softened underneath. Decoration that costs legibility is a bad
|
|
12
|
+
trade however good it looks. So it is a band now: a fixed strip above the
|
|
13
|
+
log, which keeps every line at full contrast and never moves under the
|
|
14
|
+
reader. The log shrinks by the band's height and nothing overlaps.
|
|
15
|
+
|
|
16
|
+
The strip earns its space twice over. The cell on the left says the session
|
|
17
|
+
is live; the trace beside it is the last forty calls, each drawn as tall as
|
|
18
|
+
it was slow and coloured by whether it worked. That turns "is it alive" and
|
|
19
|
+
"is it healthy" into one glance, which is more than the log gives and more
|
|
20
|
+
than an animation alone would.
|
|
21
|
+
|
|
22
|
+
WHAT THIS FILE OWNS, since it used to own everything: the strip's Instances,
|
|
23
|
+
the pointer handling on the trace, and the simulation -- energy, activity,
|
|
24
|
+
flare, shake, the scale spring, the quiet timer and the blended tint. What
|
|
25
|
+
it does NOT own is any drawing inside the cell. That belongs to whichever
|
|
26
|
+
preset is active (see Themes/), which is handed the simulation each frame
|
|
27
|
+
and paints whatever it likes with it. Eight presets are therefore eight
|
|
28
|
+
painters over one set of physics rather than eight animation systems, and
|
|
29
|
+
only one of them is ever mounted.
|
|
30
|
+
]]
|
|
31
|
+
|
|
32
|
+
local RunService = game:GetService("RunService")
|
|
33
|
+
|
|
34
|
+
local Themes = require(script.Parent.Themes)
|
|
35
|
+
|
|
36
|
+
local Visuals = {}
|
|
37
|
+
|
|
38
|
+
-- How long the session must go without a command before the cell winds down.
|
|
39
|
+
-- Matches the console's idle notice, so the picture and the log agree.
|
|
40
|
+
local QUIET_AFTER = 20
|
|
41
|
+
|
|
42
|
+
--[[
|
|
43
|
+
Spring constants for the scale. Stiff enough that a call reads as a snap
|
|
44
|
+
rather than a swell, damped just under critical so a reply overshoots once
|
|
45
|
+
and settles instead of ringing.
|
|
46
|
+
]]
|
|
47
|
+
local SPRING = 90
|
|
48
|
+
local DAMPING = 14
|
|
49
|
+
|
|
50
|
+
-- How many calls the trace remembers. Forty is about a screen's width of bars
|
|
51
|
+
-- at five pixels each, and about as far back as anyone reads a trend.
|
|
52
|
+
local TRACE = 40
|
|
53
|
+
|
|
54
|
+
-- Bars are scaled against this, so a normal call is a low bar and a slow one is
|
|
55
|
+
-- obvious. Anything past it clamps to full height rather than flattening the
|
|
56
|
+
-- rest of the trace into invisibility.
|
|
57
|
+
local SLOW_MS = 400
|
|
58
|
+
|
|
59
|
+
-- Pixels from one bar to the next. Also the width of a bar's hit target and of
|
|
60
|
+
-- the hover highlight, so pointing between two bars still selects one of them
|
|
61
|
+
-- rather than falling into a dead gap.
|
|
62
|
+
local BAR_STRIDE = 5
|
|
63
|
+
|
|
64
|
+
-- Height of the trace, and so of every bar's hit column. The taller band spends
|
|
65
|
+
-- its extra pixels here: a 19px column was a small thing to aim a pointer at.
|
|
66
|
+
local TRACE_HEIGHT = 26
|
|
67
|
+
|
|
68
|
+
--[[
|
|
69
|
+
The connecting wave: what the trace does while there is no session to plot.
|
|
70
|
+
|
|
71
|
+
A connect or a reconnect is the one moment the trace has nothing true to
|
|
72
|
+
say. No calls have happened yet, so it is a bare baseline -- which looks
|
|
73
|
+
exactly like a panel that has died, at the one time the user is most likely
|
|
74
|
+
to be watching it to find out whether it has.
|
|
75
|
+
|
|
76
|
+
So the columns wave instead: a swell travelling along the trace for as long
|
|
77
|
+
as the transport is reaching for the bridge, and gone the moment it either
|
|
78
|
+
arrives or gives up.
|
|
79
|
+
|
|
80
|
+
It is drawn THROUGH THE ACTIVE PRESET's slot painter rather than painted
|
|
81
|
+
here, for the same reason history is. Eight presets already know how to draw
|
|
82
|
+
a column of a given height and freshness, so routing the wave through them
|
|
83
|
+
means it arrives in all eight without eight implementations of it -- a theme
|
|
84
|
+
that draws history as rising points draws the wave as an arc of points, and
|
|
85
|
+
nobody had to tell it to.
|
|
86
|
+
]]
|
|
87
|
+
--[[
|
|
88
|
+
ONE GRID, shared with the history. This is the whole trick.
|
|
89
|
+
|
|
90
|
+
The wave used to sit on its own grid -- a fixed count of columns spread by
|
|
91
|
+
scale across the trace -- and history sits on a stride grid anchored to the
|
|
92
|
+
right edge. Two grids in one strip is two instruments in one strip: on a
|
|
93
|
+
reconnect the wave's columns landed between and behind the bars at a
|
|
94
|
+
different spacing, which read as interference rather than as one surface.
|
|
95
|
+
|
|
96
|
+
So the trace is now a single row of BAR_STRIDE-spaced slots, numbered 1 at
|
|
97
|
+
the right edge and rising leftwards. History fills slots 1..n. The wave
|
|
98
|
+
fills every slot LEFT of those and never one of them, so the two can no
|
|
99
|
+
longer overlap by construction rather than by draw order. And because they
|
|
100
|
+
share the numbering, the same swell that lifts a wave column also lifts the
|
|
101
|
+
bars it rolls over -- the history rides the wave instead of standing still
|
|
102
|
+
in the middle of it.
|
|
103
|
+
]]
|
|
104
|
+
-- The most slots the wave will ever fill, which bounds the pool a very wide
|
|
105
|
+
-- panel can ask for.
|
|
106
|
+
local WAVE_MAX = 160
|
|
107
|
+
-- Seconds for one swell to cross the trace.
|
|
108
|
+
local WAVE_PERIOD = 1.8
|
|
109
|
+
-- The swell's width as a fraction of the trace. Narrow enough to read as a
|
|
110
|
+
-- pulse travelling along it rather than as the whole trace breathing.
|
|
111
|
+
local WAVE_SPREAD = 0.2
|
|
112
|
+
--[[
|
|
113
|
+
The fine ripple riding on the swell, as SLOTS PER CYCLE rather than cycles
|
|
114
|
+
per trace.
|
|
115
|
+
|
|
116
|
+
Fixed cycles made the texture a function of the panel's width: the same wave
|
|
117
|
+
was a coarse comb in a narrow dock and a smooth featureless hump in a wide
|
|
118
|
+
one. Per-slot keeps a ripple the same physical size at any width, which is
|
|
119
|
+
what makes it read as strips moving at all.
|
|
120
|
+
]]
|
|
121
|
+
local WAVE_RIPPLE_STRIDE = 16
|
|
122
|
+
--[[
|
|
123
|
+
How far the swell lifts a history bar, against the headroom that bar has
|
|
124
|
+
left.
|
|
125
|
+
|
|
126
|
+
Additive and proportional, not a replacement. A bar already near the top of
|
|
127
|
+
the trace barely moves and a stub moves nearly the full amount, so the wave
|
|
128
|
+
is visible across the history without flattening the profile that history is
|
|
129
|
+
there to show. The hover readout reports the recorded milliseconds either
|
|
130
|
+
way -- what the wave changes is the drawing, never the reading.
|
|
131
|
+
]]
|
|
132
|
+
local WAVE_LIFT = 0.8
|
|
133
|
+
-- Fade rates, in and out. Out is the slower of the two: an attempt that fails
|
|
134
|
+
-- immediately should ebb rather than blink off.
|
|
135
|
+
local WAVE_IN = 7
|
|
136
|
+
local WAVE_OUT = 2.5
|
|
137
|
+
--[[
|
|
138
|
+
The shortest time the wave stays up once it has started.
|
|
139
|
+
|
|
140
|
+
A handshake against a port with nothing behind it fails in milliseconds, and
|
|
141
|
+
the transport reports "connecting" before every one of those attempts. Without
|
|
142
|
+
a floor the wave would be a single-frame flash per retry, which reads as a
|
|
143
|
+
glitch rather than as an attempt being made.
|
|
144
|
+
]]
|
|
145
|
+
local WAVE_HOLD = 0.7
|
|
146
|
+
-- Where the columns rest when the wave is not lifting them. Low enough to read
|
|
147
|
+
-- as an instrument waiting rather than as invented data, high enough to be
|
|
148
|
+
-- there at all.
|
|
149
|
+
local WAVE_FLOOR = 0.05
|
|
150
|
+
|
|
151
|
+
--[[
|
|
152
|
+
The arrival: what the trace does the moment a session actually exists.
|
|
153
|
+
|
|
154
|
+
The connecting wave is a searching motion and says so -- the same swell over
|
|
155
|
+
and over, going nowhere. Landing deserves a different shape, once, or the
|
|
156
|
+
only thing that marks the event is a word in the header going green.
|
|
157
|
+
|
|
158
|
+
So the columns are struck rather than swept. A bright front races along the
|
|
159
|
+
trace and every column it reaches leaps and rings down behind it, which
|
|
160
|
+
reads as a thing arriving rather than a thing looking. It plays once per
|
|
161
|
+
connection and cannot repeat on its own -- the status line only fires it on
|
|
162
|
+
a real transition into `connected`, so a theme switch or a port change
|
|
163
|
+
replaying the same status does not replay the celebration.
|
|
164
|
+
]]
|
|
165
|
+
-- How long the whole flourish lasts.
|
|
166
|
+
local CHEER_TIME = 1.4
|
|
167
|
+
-- The fraction of that time the front takes to cross the trace. Well under
|
|
168
|
+
-- half: the sweep should outrun the ringing rather than drag it along.
|
|
169
|
+
local CHEER_SWEEP = 0.38
|
|
170
|
+
-- How fast a struck column settles. Against the sweep above this leaves about
|
|
171
|
+
-- two visible bounces before it is back on the baseline.
|
|
172
|
+
local CHEER_DECAY = 3.2
|
|
173
|
+
-- Bounces per column, over the flourish's own length.
|
|
174
|
+
local CHEER_RING = 2.5
|
|
175
|
+
--[[
|
|
176
|
+
How abruptly the flourish fades.
|
|
177
|
+
|
|
178
|
+
Opacity holds at full for the first stretch and only gives way at the end,
|
|
179
|
+
so the celebration reads as ringing out rather than as being dimmed while it
|
|
180
|
+
is still moving. `1 / CHEER_FADE` is the fraction of the tail spent fading.
|
|
181
|
+
]]
|
|
182
|
+
local CHEER_FADE = 2.5
|
|
183
|
+
|
|
184
|
+
--[[
|
|
185
|
+
The strip's height, and the geometry around the cell.
|
|
186
|
+
|
|
187
|
+
The cell was originally as tall as the band and read as looming; it crowded
|
|
188
|
+
the caption and made the strip feel like a panel. It is an indicator, not the
|
|
189
|
+
subject, so it sits small with air around it.
|
|
190
|
+
|
|
191
|
+
INSET and GUTTER both grew after the cell stopped being a wireframe and
|
|
192
|
+
started being eight different things. At six pixels from the panel edge the
|
|
193
|
+
cell was pressed into the corner, which reads as cramped for a prism and
|
|
194
|
+
simply wrong for a starfield -- a sky needs a horizon around it, not a
|
|
195
|
+
border. The band grew with them so the extra margin comes out of empty space
|
|
196
|
+
rather than out of the trace, whose bars are what people try to point at.
|
|
197
|
+
]]
|
|
198
|
+
Visuals.BAND_HEIGHT = 58
|
|
199
|
+
local CELL = 40
|
|
200
|
+
local INSET = 12
|
|
201
|
+
local GUTTER = 22
|
|
202
|
+
|
|
203
|
+
type Bar = {
|
|
204
|
+
frame: Frame,
|
|
205
|
+
--[[
|
|
206
|
+
An invisible column over the bar, full height of the trace, wide enough to
|
|
207
|
+
hit. The bar itself is three pixels wide and usually one pixel tall -- a
|
|
208
|
+
fast call is a stub on the baseline -- so hovering the bar is not something
|
|
209
|
+
a person can reliably do. The hit target is the whole column above it, so
|
|
210
|
+
pointing anywhere near a bar selects it.
|
|
211
|
+
]]
|
|
212
|
+
hit: TextButton,
|
|
213
|
+
milliseconds: number,
|
|
214
|
+
ok: boolean,
|
|
215
|
+
--[[
|
|
216
|
+
What ran, in the same words the log uses.
|
|
217
|
+
|
|
218
|
+
The trace plotted durations with nothing to attach them to, so a tall bar
|
|
219
|
+
raised the question it could not answer: forty numbers and no way to tell
|
|
220
|
+
which of them was the script edit. Carried per bar rather than looked up,
|
|
221
|
+
because the bar outlives whatever produced it.
|
|
222
|
+
]]
|
|
223
|
+
title: string,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type Runtime = {
|
|
227
|
+
band: Frame?,
|
|
228
|
+
cell: Frame?,
|
|
229
|
+
trace: Frame?,
|
|
230
|
+
divider: Frame?,
|
|
231
|
+
baseline: Frame?,
|
|
232
|
+
highlight: Frame?,
|
|
233
|
+
readout: TextLabel?,
|
|
234
|
+
caption: TextLabel?,
|
|
235
|
+
--[[
|
|
236
|
+
What the caption said before a note took it over.
|
|
237
|
+
|
|
238
|
+
Nil means no note is showing, which is why this is the flag as well as
|
|
239
|
+
the storage: a second `showNote` while one is up must not overwrite the
|
|
240
|
+
text being held, or clearing would restore a note instead of the
|
|
241
|
+
caption.
|
|
242
|
+
]]
|
|
243
|
+
noted: string?,
|
|
244
|
+
bars: { Bar },
|
|
245
|
+
connection: RBXScriptConnection?,
|
|
246
|
+
|
|
247
|
+
spin: Vector3,
|
|
248
|
+
angle: Vector3,
|
|
249
|
+
|
|
250
|
+
energy: number,
|
|
251
|
+
shake: number,
|
|
252
|
+
--[[
|
|
253
|
+
The cell's size, as a spring rather than a value.
|
|
254
|
+
|
|
255
|
+
Scale was previously a small function of energy -- a 24% range that
|
|
256
|
+
nobody could see. Driving it as a spring means each event can push it and
|
|
257
|
+
let physics do the rest: a dispatch pulls it in, a reply throws it out
|
|
258
|
+
past its resting size, and it settles on its own.
|
|
259
|
+
]]
|
|
260
|
+
scale: number,
|
|
261
|
+
scaleVelocity: number,
|
|
262
|
+
-- Spikes to 1 the instant a reply lands, gone in a third of a second.
|
|
263
|
+
flare: number,
|
|
264
|
+
-- Seconds since anything last ran. Drives the wind-down.
|
|
265
|
+
quiet: number,
|
|
266
|
+
tint: Color3,
|
|
267
|
+
targetTint: Color3,
|
|
268
|
+
baseTint: Color3,
|
|
269
|
+
activeTint: Color3,
|
|
270
|
+
activity: number,
|
|
271
|
+
lastTitle: string?,
|
|
272
|
+
-- Whether the active preset has been given its Instances yet.
|
|
273
|
+
mounted: boolean,
|
|
274
|
+
|
|
275
|
+
-- The wave's columns, pooled at mount and hidden until a connect needs them.
|
|
276
|
+
wave: { Frame },
|
|
277
|
+
-- What the transport last said: true while it is trying to reach the bridge.
|
|
278
|
+
waveWanted: boolean,
|
|
279
|
+
-- The ramp, 0 to 1. Separate from `waveWanted` so the wave fades rather than
|
|
280
|
+
-- appearing and vanishing with the status line.
|
|
281
|
+
waveLevel: number,
|
|
282
|
+
-- Seconds the wave is held up regardless of `waveWanted`. See WAVE_HOLD.
|
|
283
|
+
waveHold: number,
|
|
284
|
+
-- Whether the columns are currently on screen. Kept rather than derived, so
|
|
285
|
+
-- a settled wave hides them once instead of every frame afterwards.
|
|
286
|
+
waveShown: boolean,
|
|
287
|
+
-- The arrival flourish: 1 the instant a session connects, down to 0 over
|
|
288
|
+
-- CHEER_TIME. Overrides the connecting wave's shape while it runs.
|
|
289
|
+
cheer: number,
|
|
290
|
+
|
|
291
|
+
--[[
|
|
292
|
+
True while an agent the panel started is working.
|
|
293
|
+
|
|
294
|
+
Everything else here is driven by events: a tool call arrives, the cell
|
|
295
|
+
is struck, and the strike decays. That is the right model for an agent
|
|
296
|
+
being driven from a terminal, where the gaps between calls are the user
|
|
297
|
+
reading. It is the wrong one for an agent this panel started, because the
|
|
298
|
+
longest gaps are the model thinking -- which is the part that takes the
|
|
299
|
+
time, produces no call, and left the strip winding down to "stopped" in
|
|
300
|
+
the middle of the work it was supposed to be reporting.
|
|
301
|
+
|
|
302
|
+
So this is the one piece of state that is a CONDITION rather than an
|
|
303
|
+
event, and the only thing it does is hold a floor under the decay. A real
|
|
304
|
+
call still spikes above it and still falls back; it just falls back to
|
|
305
|
+
"working" instead of to "nothing is happening".
|
|
306
|
+
]]
|
|
307
|
+
thinking: boolean,
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
local runtime: Runtime = {
|
|
311
|
+
band = nil,
|
|
312
|
+
cell = nil,
|
|
313
|
+
trace = nil,
|
|
314
|
+
divider = nil,
|
|
315
|
+
baseline = nil,
|
|
316
|
+
highlight = nil,
|
|
317
|
+
readout = nil,
|
|
318
|
+
caption = nil,
|
|
319
|
+
noted = nil,
|
|
320
|
+
bars = {},
|
|
321
|
+
connection = nil,
|
|
322
|
+
spin = Vector3.new(0.18, 0.27, 0.11),
|
|
323
|
+
angle = Vector3.zero,
|
|
324
|
+
energy = 0,
|
|
325
|
+
shake = 0,
|
|
326
|
+
scale = 1,
|
|
327
|
+
scaleVelocity = 0,
|
|
328
|
+
flare = 0,
|
|
329
|
+
quiet = 0,
|
|
330
|
+
tint = Color3.fromRGB(167, 139, 250),
|
|
331
|
+
targetTint = Color3.fromRGB(167, 139, 250),
|
|
332
|
+
baseTint = Color3.fromRGB(167, 139, 250),
|
|
333
|
+
activeTint = Color3.fromRGB(167, 139, 250),
|
|
334
|
+
activity = 0,
|
|
335
|
+
lastTitle = nil,
|
|
336
|
+
mounted = false,
|
|
337
|
+
wave = {},
|
|
338
|
+
waveWanted = false,
|
|
339
|
+
waveLevel = 0,
|
|
340
|
+
waveHold = 0,
|
|
341
|
+
waveShown = false,
|
|
342
|
+
cheer = 0,
|
|
343
|
+
thinking = false,
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
--[[
|
|
347
|
+
The simulation, packaged for whichever preset is drawing it.
|
|
348
|
+
|
|
349
|
+
Rebuilt every frame rather than mutated in place. A preset holding onto the
|
|
350
|
+
table between frames would see values change under it, and the one rule that
|
|
351
|
+
keeps eight renderers honest is that they read this and draw -- they do not
|
|
352
|
+
own any of it.
|
|
353
|
+
]]
|
|
354
|
+
local function context(): Themes.Context
|
|
355
|
+
return {
|
|
356
|
+
palette = Themes.palette(),
|
|
357
|
+
tint = runtime.tint,
|
|
358
|
+
energy = runtime.energy,
|
|
359
|
+
activity = runtime.activity,
|
|
360
|
+
flare = runtime.flare,
|
|
361
|
+
shake = runtime.shake,
|
|
362
|
+
scale = runtime.scale,
|
|
363
|
+
quiet = runtime.quiet,
|
|
364
|
+
angle = runtime.angle,
|
|
365
|
+
clock = os.clock(),
|
|
366
|
+
size = CELL,
|
|
367
|
+
centre = Vector2.new(CELL / 2, CELL / 2),
|
|
368
|
+
}
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
-- Declared here and defined below, because it draws through the same slot
|
|
372
|
+
-- painter history does and that helper is defined after this function.
|
|
373
|
+
local stepWave: (delta: number) -> ()
|
|
374
|
+
|
|
375
|
+
local function step(delta: number)
|
|
376
|
+
local band = runtime.band
|
|
377
|
+
local cell = runtime.cell
|
|
378
|
+
if band == nil or cell == nil or not band.Visible then
|
|
379
|
+
return
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
runtime.energy = math.max(0, runtime.energy - delta * 0.9)
|
|
383
|
+
runtime.shake = math.max(0, runtime.shake - delta * 2.4)
|
|
384
|
+
--[[
|
|
385
|
+
Slower than it was. The cell is meant to climb as the session works, and
|
|
386
|
+
at the old rates a call had to arrive every three seconds just to hold the
|
|
387
|
+
middle of the range -- so the top of it was, in practice, unreachable. A
|
|
388
|
+
burst of four calls now gets there, and it unwinds over about ten seconds
|
|
389
|
+
afterwards.
|
|
390
|
+
]]
|
|
391
|
+
runtime.activity = math.max(0, runtime.activity - delta * 0.28)
|
|
392
|
+
runtime.flare = math.max(0, runtime.flare - delta * 3)
|
|
393
|
+
runtime.quiet += delta
|
|
394
|
+
|
|
395
|
+
--[[
|
|
396
|
+
The floor an agent's thinking holds under all of that.
|
|
397
|
+
|
|
398
|
+
A pulse rather than a constant, and the difference is the whole point: a
|
|
399
|
+
fixed floor produces a cell frozen mid-swell, which reads as a hung
|
|
400
|
+
panel rather than a working one. The sine is slow -- about a three second
|
|
401
|
+
period -- so it is unmistakably a breath and never competes with the
|
|
402
|
+
sharp strike of a real call landing on top of it.
|
|
403
|
+
|
|
404
|
+
Applied after the decay and before anything reads these values, so the
|
|
405
|
+
ordinary event path above is untouched: it still decays exactly as it
|
|
406
|
+
did, it simply cannot fall below this while an agent is running. `quiet`
|
|
407
|
+
is pinned too, or the wind-down at QUIET_AFTER would draw the cell in
|
|
408
|
+
and slow its spin during the longest thinking pauses -- the precise
|
|
409
|
+
moment this exists to cover.
|
|
410
|
+
]]
|
|
411
|
+
if runtime.thinking then
|
|
412
|
+
local pulse = 0.34 + math.sin(os.clock() * 2.1) * 0.13
|
|
413
|
+
runtime.energy = math.max(runtime.energy, pulse)
|
|
414
|
+
runtime.activity = math.max(runtime.activity, 0.4)
|
|
415
|
+
runtime.quiet = 0
|
|
416
|
+
end
|
|
417
|
+
--[[
|
|
418
|
+
Two colours, blended by how recently something happened.
|
|
419
|
+
|
|
420
|
+
`baseTint` is the connection state, which is what the panel should read
|
|
421
|
+
as when nothing is going on. `activeTint` is the kind of work last done
|
|
422
|
+
-- reading, writing, running, debugging -- and it takes over while that
|
|
423
|
+
work is fresh, then fades back. So the strip is not one fixed colour: it
|
|
424
|
+
leans toward whatever the session is actually doing and returns to
|
|
425
|
+
resting on its own.
|
|
426
|
+
]]
|
|
427
|
+
local blend = math.clamp(runtime.energy, 0, 1)
|
|
428
|
+
runtime.targetTint = runtime.baseTint:Lerp(runtime.activeTint, blend)
|
|
429
|
+
runtime.tint = runtime.tint:Lerp(runtime.targetTint, math.min(1, delta * 5))
|
|
430
|
+
|
|
431
|
+
-- Turning faster while busy makes the rate itself readable: a glance says
|
|
432
|
+
-- whether anything is happening without reading a word.
|
|
433
|
+
--
|
|
434
|
+
-- And nearly stopping once the session has gone quiet, for the same reason in
|
|
435
|
+
-- reverse: a strip that keeps moving at its working rate hours after the
|
|
436
|
+
-- last command claims an activity that is not happening.
|
|
437
|
+
local pace = if runtime.quiet > QUIET_AFTER then 0.25 else 1
|
|
438
|
+
runtime.angle += runtime.spin * delta * (1 + runtime.energy * 2.2) * pace
|
|
439
|
+
|
|
440
|
+
--[[
|
|
441
|
+
Where the cell wants to be, before the spring gets a say.
|
|
442
|
+
|
|
443
|
+
Three things move it and they are deliberately ordered. Energy -- how
|
|
444
|
+
recently work happened -- swells it while a session is busy. The breathe
|
|
445
|
+
is a slow sine that only survives while energy is near zero, so an idle
|
|
446
|
+
panel is visibly alive without a resting animation competing with the
|
|
447
|
+
reaction to a real call. And past QUIET_AFTER seconds of nothing, it draws
|
|
448
|
+
in and stays there: the picture of a session that has stopped.
|
|
449
|
+
]]
|
|
450
|
+
local calm = 1 - math.clamp(runtime.energy, 0, 1)
|
|
451
|
+
local breathe = math.sin(os.clock() * 0.9) * 0.04 * calm
|
|
452
|
+
local target = 1 + math.clamp(runtime.energy, 0, 1.6) * 0.18 + breathe
|
|
453
|
+
if runtime.quiet > QUIET_AFTER then
|
|
454
|
+
target = 0.82
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
--[[
|
|
458
|
+
A damped spring rather than a lerp, because a lerp cannot overshoot and
|
|
459
|
+
overshoot is the whole point: a reply that pushes the cell past its
|
|
460
|
+
resting size and lets it fall back reads as a thing being struck, where
|
|
461
|
+
easing toward a value reads as a slider being dragged.
|
|
462
|
+
|
|
463
|
+
Integrated semi-implicitly -- velocity first, then position from the new
|
|
464
|
+
velocity -- which stays stable at the frame times Studio actually hands us
|
|
465
|
+
rather than only at small ones.
|
|
466
|
+
]]
|
|
467
|
+
--[[
|
|
468
|
+
Substepped rather than truncated.
|
|
469
|
+
|
|
470
|
+
`math.min(delta, 1/30)` kept the integration stable, but it also THREW
|
|
471
|
+
AWAY the rest of a long frame: at 20fps a third of every frame's motion
|
|
472
|
+
simply did not happen, so the spring ran slower in wall-clock time
|
|
473
|
+
exactly when Studio was busiest -- which is during a tool call, which is
|
|
474
|
+
when the spring is reacting to something. Stepping the leftover instead
|
|
475
|
+
keeps the reaction the same length whatever the frame rate, and the cap
|
|
476
|
+
on the number of steps stops a two-second hitch from paying it all back
|
|
477
|
+
in one frame.
|
|
478
|
+
]]
|
|
479
|
+
local remaining = math.min(delta, 0.25)
|
|
480
|
+
for _ = 1, 8 do
|
|
481
|
+
if remaining <= 0 then
|
|
482
|
+
break
|
|
483
|
+
end
|
|
484
|
+
local integration = math.min(remaining, 1 / 60)
|
|
485
|
+
remaining -= integration
|
|
486
|
+
|
|
487
|
+
runtime.scaleVelocity += (target - runtime.scale) * SPRING * integration
|
|
488
|
+
runtime.scaleVelocity -= runtime.scaleVelocity * DAMPING * integration
|
|
489
|
+
runtime.scale += runtime.scaleVelocity * integration
|
|
490
|
+
|
|
491
|
+
--[[
|
|
492
|
+
Clamping the position without killing the velocity is what made a
|
|
493
|
+
reply read as a jolt rather than a bounce. The reply impulse is
|
|
494
|
+
`scaleVelocity += 9`, which crosses the whole 0.55..1.7 range in
|
|
495
|
+
about two frames, so the scale pinned itself against the ceiling
|
|
496
|
+
while still carrying all that speed -- and then sat there, motionless
|
|
497
|
+
but not at rest, until the spring had spent the stored velocity. The
|
|
498
|
+
eye reads that as a snap, a stall, and a second snap.
|
|
499
|
+
|
|
500
|
+
Zeroing the component pushing into the rail turns it into a stop.
|
|
501
|
+
]]
|
|
502
|
+
if runtime.scale <= 0.55 then
|
|
503
|
+
runtime.scale = 0.55
|
|
504
|
+
runtime.scaleVelocity = math.max(runtime.scaleVelocity, 0)
|
|
505
|
+
elseif runtime.scale >= 1.7 then
|
|
506
|
+
runtime.scale = 1.7
|
|
507
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity, 0)
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
-- Stepped before the cell is handed off, so a preset that errors and
|
|
512
|
+
-- disables the strip does not also take the connect animation with it.
|
|
513
|
+
stepWave(delta)
|
|
514
|
+
|
|
515
|
+
--[[
|
|
516
|
+
Handed off. A preset that throws must not take the console's status
|
|
517
|
+
display down with it, so a failure here disables the strip and says so
|
|
518
|
+
once rather than erroring sixty times a second.
|
|
519
|
+
]]
|
|
520
|
+
local ok, err = pcall(Themes.active().paint, context(), delta)
|
|
521
|
+
if not ok then
|
|
522
|
+
band.Visible = false
|
|
523
|
+
warn(string.format("[rbx-studio] theme %q failed to paint: %s", Themes.activeId(), tostring(err)))
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
--[[
|
|
528
|
+
Puts the highlight behind one bar and prints its timing.
|
|
529
|
+
|
|
530
|
+
The trace is only legible if you already know what it plots, and nothing on
|
|
531
|
+
screen said so. Hovering answers it directly: the column lights up, and the
|
|
532
|
+
number that made that bar its height appears in words. One reading at a time,
|
|
533
|
+
so a single highlight and a single label are moved around rather than one of
|
|
534
|
+
each being built per bar.
|
|
535
|
+
]]
|
|
536
|
+
local function showReading(entry: Bar)
|
|
537
|
+
local highlight = runtime.highlight
|
|
538
|
+
local readout = runtime.readout
|
|
539
|
+
if highlight == nil or readout == nil then
|
|
540
|
+
return
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
--[[
|
|
544
|
+
The Y here has to be 1, and was 0.
|
|
545
|
+
|
|
546
|
+
The highlight is anchored to its own bottom edge, so a Y scale of 0 put
|
|
547
|
+
that edge on the trace's top line and drew the whole 26px column above it
|
|
548
|
+
-- outside a frame that clips its descendants. The hover was therefore
|
|
549
|
+
never visible to anyone: the hit targets fired, the readout appeared, and
|
|
550
|
+
the highlight they were meant to explain was off screen every time.
|
|
551
|
+
]]
|
|
552
|
+
highlight.Position = UDim2.new(entry.hit.Position.X.Scale, entry.hit.Position.X.Offset, 1, 0)
|
|
553
|
+
highlight.Visible = true
|
|
554
|
+
|
|
555
|
+
--[[
|
|
556
|
+
Named, not just timed.
|
|
557
|
+
|
|
558
|
+
A duration with nothing attached to it raises the question it cannot
|
|
559
|
+
answer -- forty bars, one of them tall, and no way to tell whether that
|
|
560
|
+
was a script edit or a screenshot. The title is the same phrase the log
|
|
561
|
+
row uses, so pointing at a bar and reading the log agree with each other.
|
|
562
|
+
]]
|
|
563
|
+
local palette = Themes.palette()
|
|
564
|
+
readout.Text = if entry.ok
|
|
565
|
+
then string.format("%s: %d ms", entry.title, math.round(entry.milliseconds))
|
|
566
|
+
else string.format("%s: failed after %d ms", entry.title, math.round(entry.milliseconds))
|
|
567
|
+
readout.TextColor3 = if entry.ok then palette.text else palette.red
|
|
568
|
+
readout.Visible = true
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
local function clearReading()
|
|
572
|
+
if runtime.highlight then
|
|
573
|
+
runtime.highlight.Visible = false
|
|
574
|
+
end
|
|
575
|
+
if runtime.readout then
|
|
576
|
+
runtime.readout.Visible = false
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
--[[
|
|
581
|
+
Bars, for a preset that does not want to draw its own history.
|
|
582
|
+
|
|
583
|
+
Height is duration and colour is outcome, so a slow call and a failed one are
|
|
584
|
+
distinguishable at a glance -- which is the pair of questions that actually
|
|
585
|
+
get asked of a log this size.
|
|
586
|
+
]]
|
|
587
|
+
local function defaultSlot(slot: Themes.Slot, ctx: Themes.Context)
|
|
588
|
+
local frame = slot.frame
|
|
589
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
590
|
+
frame.Size = UDim2.new(0, 3, math.max(slot.weight, 0.06), 0)
|
|
591
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
592
|
+
frame.BackgroundColor3 = if slot.ok then ctx.palette.violet else ctx.palette.red
|
|
593
|
+
frame.BackgroundTransparency = 0.15 + slot.age * 0.6
|
|
594
|
+
frame.Rotation = 0
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
--[[
|
|
598
|
+
Whether the transport is currently reaching for the bridge.
|
|
599
|
+
|
|
600
|
+
Told rather than inferred, because "connecting" is the transport's word and
|
|
601
|
+
the trace must never disagree with the status dot about what the session is
|
|
602
|
+
doing.
|
|
603
|
+
]]
|
|
604
|
+
function Visuals.setConnecting(active: boolean)
|
|
605
|
+
if active and not runtime.waveWanted then
|
|
606
|
+
runtime.waveHold = WAVE_HOLD
|
|
607
|
+
end
|
|
608
|
+
runtime.waveWanted = active
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
--[[
|
|
612
|
+
Plays the arrival flourish, once.
|
|
613
|
+
|
|
614
|
+
Edge-triggered by the caller rather than latched here: only the status line
|
|
615
|
+
knows whether this is a session actually landing or the same `connected`
|
|
616
|
+
state being repeated after a theme switch, and a celebration that replays
|
|
617
|
+
itself every time the port is re-read stops meaning anything.
|
|
618
|
+
]]
|
|
619
|
+
function Visuals.celebrate()
|
|
620
|
+
runtime.cheer = 1
|
|
621
|
+
-- The search is over. Nothing should be left holding the connecting wave up
|
|
622
|
+
-- underneath a flourish that has replaced it.
|
|
623
|
+
runtime.waveWanted = false
|
|
624
|
+
runtime.waveHold = 0
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
--[[
|
|
628
|
+
Re-places every bar and hands each to the active preset to draw.
|
|
629
|
+
|
|
630
|
+
Laid out right to left so the newest bar is always at the same edge and the
|
|
631
|
+
trace reads as scrolling rather than reshuffling. The horizontal position is
|
|
632
|
+
set here and the preset is expected to keep it -- everything else about the
|
|
633
|
+
bar is the preset's to decide.
|
|
634
|
+
]]
|
|
635
|
+
--[[
|
|
636
|
+
Where slot `number` sits, counting 1 at the right edge and rising leftwards.
|
|
637
|
+
|
|
638
|
+
The one place the trace's spacing is decided. Both the history and the wave
|
|
639
|
+
call it, which is what guarantees they land on the same grid rather than on
|
|
640
|
+
two that merely look similar.
|
|
641
|
+
]]
|
|
642
|
+
local function slotOffset(number: number): number
|
|
643
|
+
return -(number - 0.5) * BAR_STRIDE
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
--[[
|
|
647
|
+
Re-places every bar and hands each to the active preset to draw.
|
|
648
|
+
|
|
649
|
+
Laid out right to left so the newest bar is always at the same edge and the
|
|
650
|
+
trace reads as scrolling rather than reshuffling. The horizontal position is
|
|
651
|
+
set here and the preset is expected to keep it -- everything else about the
|
|
652
|
+
bar is the preset's to decide.
|
|
653
|
+
|
|
654
|
+
`modulate` is the wave, when one is running. It is handed a bar's slot
|
|
655
|
+
number and its true height and freshness, and returns what to draw instead,
|
|
656
|
+
so the history is lifted by the same swell that lifts the empty slots beside
|
|
657
|
+
it. Absent, every bar is drawn from its own recorded timing, which is what
|
|
658
|
+
restores the true trace the moment a wave ends.
|
|
659
|
+
]]
|
|
660
|
+
local function relayout(modulate: ((number, number, number) -> (number, number))?)
|
|
661
|
+
local ctx = context()
|
|
662
|
+
local painter = Themes.active().paintSlot or defaultSlot
|
|
663
|
+
local total = #runtime.bars
|
|
664
|
+
|
|
665
|
+
for index, item in runtime.bars do
|
|
666
|
+
local fromRight = total - index
|
|
667
|
+
local number = fromRight + 1
|
|
668
|
+
local offset = slotOffset(number)
|
|
669
|
+
|
|
670
|
+
item.frame.Position = UDim2.new(1, offset, 1, 0)
|
|
671
|
+
item.hit.Position = UDim2.new(1, offset, 1, 0)
|
|
672
|
+
|
|
673
|
+
-- 0 for the newest, rising toward 1 for the oldest still shown.
|
|
674
|
+
local age = if TRACE > 1 then fromRight / (TRACE - 1) else 0
|
|
675
|
+
local weight = math.clamp(item.milliseconds / SLOW_MS, 0.06, 1)
|
|
676
|
+
if modulate ~= nil then
|
|
677
|
+
weight, age = modulate(number, weight, age)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
local slot: Themes.Slot = {
|
|
681
|
+
frame = item.frame,
|
|
682
|
+
milliseconds = item.milliseconds,
|
|
683
|
+
ok = item.ok,
|
|
684
|
+
title = item.title,
|
|
685
|
+
age = age,
|
|
686
|
+
weight = weight,
|
|
687
|
+
}
|
|
688
|
+
local ok, err = pcall(painter, slot, ctx)
|
|
689
|
+
if not ok then
|
|
690
|
+
-- One bad slot must not leave the other thirty-nine unplaced.
|
|
691
|
+
defaultSlot(slot, ctx)
|
|
692
|
+
--[[
|
|
693
|
+
Silent under a wave. This runs sixty times a second while one is
|
|
694
|
+
playing, and a preset that throws would fill the log faster than
|
|
695
|
+
anyone could read it -- the unmodulated path below still says so
|
|
696
|
+
once the next call lands.
|
|
697
|
+
]]
|
|
698
|
+
if modulate == nil then
|
|
699
|
+
warn(string.format("[rbx-studio] theme %q failed on a slot: %s", Themes.activeId(), tostring(err)))
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
--[[
|
|
706
|
+
How many slots the trace has room for, at the shared stride.
|
|
707
|
+
|
|
708
|
+
Read from the live width rather than fixed, because the dock is resizable and
|
|
709
|
+
a wave that stops halfway across a widened panel is worse than no wave.
|
|
710
|
+
]]
|
|
711
|
+
local function slotsAcross(): number
|
|
712
|
+
local trace = runtime.trace
|
|
713
|
+
if trace == nil then
|
|
714
|
+
return 0
|
|
715
|
+
end
|
|
716
|
+
return math.clamp(math.floor(trace.AbsoluteSize.X / BAR_STRIDE), 0, WAVE_MAX)
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
--[[
|
|
720
|
+
Makes sure the wave has a column for every slot up to `count`.
|
|
721
|
+
|
|
722
|
+
Grown on demand and never shrunk. The pool only ever reaches the width the
|
|
723
|
+
panel actually had, it is built the first time a connection is attempted
|
|
724
|
+
rather than at mount, and a column costs nothing while it is hidden.
|
|
725
|
+
]]
|
|
726
|
+
local function growWave(count: number)
|
|
727
|
+
local trace = runtime.trace
|
|
728
|
+
if trace == nil then
|
|
729
|
+
return
|
|
730
|
+
end
|
|
731
|
+
for _ = #runtime.wave + 1, count do
|
|
732
|
+
local column = Instance.new("Frame")
|
|
733
|
+
column.Name = "Wave"
|
|
734
|
+
column.AnchorPoint = Vector2.new(0.5, 1)
|
|
735
|
+
column.Size = UDim2.fromOffset(2, 1)
|
|
736
|
+
column.BackgroundColor3 = Themes.palette().violet
|
|
737
|
+
column.BorderSizePixel = 0
|
|
738
|
+
column.Visible = false
|
|
739
|
+
-- Under the bars and the hover highlight. They never share a slot, but
|
|
740
|
+
-- depth should not be the thing standing between them if that changes.
|
|
741
|
+
column.ZIndex = 1
|
|
742
|
+
column.Parent = trace
|
|
743
|
+
|
|
744
|
+
-- Presets draw points as well as bars, same as the history slots.
|
|
745
|
+
local round = Instance.new("UICorner")
|
|
746
|
+
round.CornerRadius = UDim.new(0, 1)
|
|
747
|
+
round.Parent = column
|
|
748
|
+
|
|
749
|
+
table.insert(runtime.wave, column)
|
|
750
|
+
end
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
--[[
|
|
754
|
+
Runs the connecting wave for one frame.
|
|
755
|
+
|
|
756
|
+
Cheap when nothing is connecting: the ramp settles at zero, the columns are
|
|
757
|
+
hidden once, the true trace is restored once, and every later frame leaves
|
|
758
|
+
after two comparisons.
|
|
759
|
+
]]
|
|
760
|
+
function stepWave(delta: number)
|
|
761
|
+
runtime.cheer = math.max(0, runtime.cheer - delta / CHEER_TIME)
|
|
762
|
+
|
|
763
|
+
runtime.waveHold = math.max(0, runtime.waveHold - delta)
|
|
764
|
+
local target = if runtime.waveWanted or runtime.waveHold > 0 then 1 else 0
|
|
765
|
+
local rate = if target > runtime.waveLevel then WAVE_IN else WAVE_OUT
|
|
766
|
+
runtime.waveLevel += (target - runtime.waveLevel) * math.min(1, delta * rate)
|
|
767
|
+
|
|
768
|
+
--[[
|
|
769
|
+
Two sources, one opacity.
|
|
770
|
+
|
|
771
|
+
They overlap for a moment at every connection -- the status goes green,
|
|
772
|
+
which drops `waveWanted`, while the flourish is only just starting -- and
|
|
773
|
+
taking the louder of the two is what carries the handover without a dip
|
|
774
|
+
between the search ending and the arrival beginning.
|
|
775
|
+
]]
|
|
776
|
+
local celebration = math.min(1, runtime.cheer * CHEER_FADE)
|
|
777
|
+
local strength = math.max(runtime.waveLevel, celebration)
|
|
778
|
+
|
|
779
|
+
if strength <= 0.01 then
|
|
780
|
+
if runtime.waveShown then
|
|
781
|
+
runtime.waveShown = false
|
|
782
|
+
runtime.waveLevel = 0
|
|
783
|
+
for _, column in runtime.wave do
|
|
784
|
+
column.Visible = false
|
|
785
|
+
end
|
|
786
|
+
-- The bars were being drawn lifted. Put the real timings back, once,
|
|
787
|
+
-- rather than leaving the history frozen mid-swell.
|
|
788
|
+
relayout()
|
|
789
|
+
end
|
|
790
|
+
return
|
|
791
|
+
end
|
|
792
|
+
runtime.waveShown = true
|
|
793
|
+
|
|
794
|
+
local slots = slotsAcross()
|
|
795
|
+
if slots == 0 then
|
|
796
|
+
return
|
|
797
|
+
end
|
|
798
|
+
growWave(slots)
|
|
799
|
+
|
|
800
|
+
local ctx = context()
|
|
801
|
+
local painter = Themes.active().paintSlot or defaultSlot
|
|
802
|
+
local phase = ctx.clock / WAVE_PERIOD
|
|
803
|
+
local crest = phase % 1
|
|
804
|
+
local ripple = slots / WAVE_RIPPLE_STRIDE
|
|
805
|
+
-- The flourish runs on its own progress rather than on the clock, so it is
|
|
806
|
+
-- the same length however long the connection took to land.
|
|
807
|
+
local arriving = runtime.cheer > 0
|
|
808
|
+
local progress = 1 - runtime.cheer
|
|
809
|
+
|
|
810
|
+
--[[
|
|
811
|
+
The wave itself, as a pure function of where a slot is.
|
|
812
|
+
|
|
813
|
+
Taking a slot number rather than a column is what lets the history use it
|
|
814
|
+
too: a bar at slot 6 and an empty column at slot 7 are handed the same
|
|
815
|
+
swell, so the surface is continuous across the join instead of stopping
|
|
816
|
+
where the data starts.
|
|
817
|
+
]]
|
|
818
|
+
local function shape(number: number): (number, number)
|
|
819
|
+
-- 0 at the left edge of the trace, 1 at the right.
|
|
820
|
+
local x = 1 - (number - 0.5) / slots
|
|
821
|
+
|
|
822
|
+
if arriving then
|
|
823
|
+
--[[
|
|
824
|
+
Struck, not swept. `since` is how long ago the front passed this
|
|
825
|
+
slot: negative means it has not arrived, and the column waits
|
|
826
|
+
flat and dark rather than anticipating it.
|
|
827
|
+
]]
|
|
828
|
+
local since = progress - x * CHEER_SWEEP
|
|
829
|
+
if since < 0 then
|
|
830
|
+
return WAVE_FLOOR, 1
|
|
831
|
+
end
|
|
832
|
+
-- One envelope for both height and brightness, so a column is at its
|
|
833
|
+
-- tallest exactly when it is at its brightest.
|
|
834
|
+
local settle = math.exp(-since * CHEER_DECAY)
|
|
835
|
+
local bounce = 0.5 + 0.5 * math.cos(since * CHEER_RING * math.pi * 2)
|
|
836
|
+
return math.clamp(WAVE_FLOOR + settle * bounce * 0.95, WAVE_FLOOR, 1),
|
|
837
|
+
math.clamp(1 - settle, 0, 1)
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
--[[
|
|
841
|
+
Distance to the crest, wrapped around the ends, so the swell leaves
|
|
842
|
+
the right edge and re-enters at the left instead of jumping back
|
|
843
|
+
across a trace it has just crossed.
|
|
844
|
+
]]
|
|
845
|
+
local distance = math.abs(x - crest)
|
|
846
|
+
distance = math.min(distance, 1 - distance)
|
|
847
|
+
local swell = math.exp(-(distance * distance) / (2 * WAVE_SPREAD * WAVE_SPREAD))
|
|
848
|
+
local texture = 0.5 + 0.5 * math.sin((x * ripple - phase * 2) * math.pi * 2)
|
|
849
|
+
|
|
850
|
+
--[[
|
|
851
|
+
A floor everywhere and a hump at the crest. Away from the swell the
|
|
852
|
+
columns sit just off the baseline, which keeps the trace reading as
|
|
853
|
+
an instrument at rest rather than as a screenful of invented data.
|
|
854
|
+
]]
|
|
855
|
+
return math.clamp(WAVE_FLOOR + swell * (0.55 + 0.45 * texture) * 0.9, WAVE_FLOOR, 1),
|
|
856
|
+
-- Freshness follows the swell rather than position. Every preset
|
|
857
|
+
-- fades a slot by how recent it is, so handing the crest an age of
|
|
858
|
+
-- zero lights it and leaves the rest of the trace resting.
|
|
859
|
+
math.clamp(1 - swell, 0, 1)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
--[[
|
|
863
|
+
The history, riding the same swell.
|
|
864
|
+
|
|
865
|
+
Lifted against its own headroom and brightened to whichever of the two is
|
|
866
|
+
fresher, so a bar keeps its shape and its colour and simply moves with
|
|
867
|
+
the wave rolling under it.
|
|
868
|
+
]]
|
|
869
|
+
local barCount = #runtime.bars
|
|
870
|
+
if barCount > 0 then
|
|
871
|
+
relayout(function(number: number, weight: number, age: number): (number, number)
|
|
872
|
+
local lift, lightness = shape(number)
|
|
873
|
+
--[[
|
|
874
|
+
Both channels scale with the ramp, which is what makes the wave
|
|
875
|
+
let go of the history rather than drop it. At full strength a bar
|
|
876
|
+
is lifted and lit by the swell; as the ramp falls it slides back
|
|
877
|
+
onto its own timing and its own age, and the last frame of the
|
|
878
|
+
wave and the first frame without one are the same picture.
|
|
879
|
+
]]
|
|
880
|
+
local raised = weight + (lift - WAVE_FLOOR) * (1 - weight) * WAVE_LIFT * strength
|
|
881
|
+
local lit = age + (math.min(age, lightness) - age) * strength
|
|
882
|
+
return math.clamp(raised, weight, 1), lit
|
|
883
|
+
end)
|
|
884
|
+
end
|
|
885
|
+
|
|
886
|
+
for number, column in runtime.wave do
|
|
887
|
+
if number > slots or number <= barCount then
|
|
888
|
+
--[[
|
|
889
|
+
Past the panel's edge, or a slot the history owns. Either way the
|
|
890
|
+
wave has no business drawing here -- this is what keeps the two
|
|
891
|
+
from ever landing on the same five pixels.
|
|
892
|
+
]]
|
|
893
|
+
column.Visible = false
|
|
894
|
+
continue
|
|
895
|
+
end
|
|
896
|
+
|
|
897
|
+
local weight, age = shape(number)
|
|
898
|
+
local slot: Themes.Slot = {
|
|
899
|
+
frame = column,
|
|
900
|
+
-- Kept agreeing with the height, since that is the pair every
|
|
901
|
+
-- painter is written against, even though none of them reads it.
|
|
902
|
+
milliseconds = weight * SLOW_MS,
|
|
903
|
+
ok = true,
|
|
904
|
+
title = if arriving then "connected" else "connecting",
|
|
905
|
+
age = age,
|
|
906
|
+
weight = weight,
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
-- Set before the painter runs, off the same helper the bars use: every
|
|
910
|
+
-- painter preserves the X it is given and decides the rest.
|
|
911
|
+
column.Position = UDim2.new(1, slotOffset(number), 1, 0)
|
|
912
|
+
if not pcall(painter, slot, ctx) then
|
|
913
|
+
-- Silent, for the same reason the modulated history path is: a
|
|
914
|
+
-- preset that throws here would throw sixty times a second.
|
|
915
|
+
defaultSlot(slot, ctx)
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
--[[
|
|
919
|
+
Faded as one thing, after whoever drew it. The ramp belongs to the
|
|
920
|
+
wave and not to any preset, and applying it to the transparency the
|
|
921
|
+
painter chose keeps each theme's own weighting intact.
|
|
922
|
+
]]
|
|
923
|
+
column.BackgroundTransparency = 1 - (1 - column.BackgroundTransparency) * strength
|
|
924
|
+
column.Visible = true
|
|
925
|
+
end
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
--[[
|
|
929
|
+
Records one finished call and re-draws the trace.
|
|
930
|
+
]]
|
|
931
|
+
function Visuals.recordCall(milliseconds: number, ok: boolean, title: string)
|
|
932
|
+
runtime.energy = math.min(1.6, runtime.energy + (if ok then 0.3 else 0.8))
|
|
933
|
+
runtime.activity = math.min(1, runtime.activity + 0.34)
|
|
934
|
+
runtime.quiet = 0
|
|
935
|
+
runtime.flare = 1
|
|
936
|
+
|
|
937
|
+
--[[
|
|
938
|
+
A shove rather than a new target.
|
|
939
|
+
|
|
940
|
+
Setting the scale outright would snap; giving the spring velocity lets it
|
|
941
|
+
carry past its resting size and fall back, which is the difference between
|
|
942
|
+
a value changing and something being struck. A failure shoves the other
|
|
943
|
+
way and knocks the axis with it.
|
|
944
|
+
]]
|
|
945
|
+
if ok then
|
|
946
|
+
--[[
|
|
947
|
+
Sized to peak just under the clamp rather than against it.
|
|
948
|
+
|
|
949
|
+
At +9 the shove carried the scale into the 1.7 ceiling within about a
|
|
950
|
+
tenth of a second, so what should have been an arc became a rise, a
|
|
951
|
+
flat spot, and a drop -- and a burst of calls, each adding another 9
|
|
952
|
+
to a velocity that was already railed, held it pinned there for as
|
|
953
|
+
long as the burst lasted. +6 tops out around 1.55, which reads as the
|
|
954
|
+
same strike and stays on the curve. The accumulated case is capped
|
|
955
|
+
for the same reason: several calls at once should look busy, not
|
|
956
|
+
stuck.
|
|
957
|
+
]]
|
|
958
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity + 6, 8)
|
|
959
|
+
else
|
|
960
|
+
runtime.scale = 0.8
|
|
961
|
+
runtime.scaleVelocity = -2
|
|
962
|
+
runtime.shake = 1
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
local trace = runtime.trace
|
|
966
|
+
if trace == nil then
|
|
967
|
+
return
|
|
968
|
+
end
|
|
969
|
+
|
|
970
|
+
local bar = Instance.new("Frame")
|
|
971
|
+
bar.AnchorPoint = Vector2.new(0.5, 1)
|
|
972
|
+
bar.BorderSizePixel = 0
|
|
973
|
+
-- Above the wave's columns and the hover highlight. A plugin widget draws
|
|
974
|
+
-- with ZIndexBehavior.Global, so depth is stated rather than inherited from
|
|
975
|
+
-- the order things happened to be built in.
|
|
976
|
+
bar.ZIndex = 2
|
|
977
|
+
bar.Parent = trace
|
|
978
|
+
-- Presets draw points as well as bars, and a square point is not a point.
|
|
979
|
+
local corner = Instance.new("UICorner")
|
|
980
|
+
corner.CornerRadius = UDim.new(0, 1)
|
|
981
|
+
corner.Parent = bar
|
|
982
|
+
|
|
983
|
+
-- A TextButton rather than a Frame: buttons take mouse events reliably in a
|
|
984
|
+
-- plugin widget, and with no text and no background it is purely a target.
|
|
985
|
+
local hit = Instance.new("TextButton")
|
|
986
|
+
hit.AnchorPoint = Vector2.new(0.5, 1)
|
|
987
|
+
hit.BackgroundTransparency = 1
|
|
988
|
+
hit.Text = ""
|
|
989
|
+
hit.AutoButtonColor = false
|
|
990
|
+
hit.BorderSizePixel = 0
|
|
991
|
+
hit.Size = UDim2.new(0, BAR_STRIDE, 1, 0)
|
|
992
|
+
hit.Parent = trace
|
|
993
|
+
|
|
994
|
+
local entry: Bar = {
|
|
995
|
+
frame = bar,
|
|
996
|
+
hit = hit,
|
|
997
|
+
milliseconds = milliseconds,
|
|
998
|
+
ok = ok,
|
|
999
|
+
title = if title ~= "" then title else "call",
|
|
1000
|
+
}
|
|
1001
|
+
table.insert(runtime.bars, entry)
|
|
1002
|
+
|
|
1003
|
+
hit.MouseEnter:Connect(function()
|
|
1004
|
+
showReading(entry)
|
|
1005
|
+
end)
|
|
1006
|
+
hit.MouseLeave:Connect(function()
|
|
1007
|
+
clearReading()
|
|
1008
|
+
end)
|
|
1009
|
+
|
|
1010
|
+
while #runtime.bars > TRACE do
|
|
1011
|
+
local oldest = runtime.bars[1]
|
|
1012
|
+
oldest.frame:Destroy()
|
|
1013
|
+
oldest.hit:Destroy()
|
|
1014
|
+
table.remove(runtime.bars, 1)
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
relayout()
|
|
1018
|
+
|
|
1019
|
+
-- The bars have all moved, so a reading still on screen now names the wrong
|
|
1020
|
+
-- one. Cheaper and more honest to drop it than to work out which bar the
|
|
1021
|
+
-- pointer has ended up over.
|
|
1022
|
+
clearReading()
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
--[[
|
|
1026
|
+
The resting colour, which is the connection state.
|
|
1027
|
+
]]
|
|
1028
|
+
function Visuals.setTint(color: Color3)
|
|
1029
|
+
runtime.baseTint = color
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
--[[
|
|
1033
|
+
The colour and pace of the work now running.
|
|
1034
|
+
|
|
1035
|
+
Both are set from the same call because they describe the same thing: what
|
|
1036
|
+
kind of command this is. Reads are quick and cool, writes are slower and
|
|
1037
|
+
warm, so the strip's colour and its rate agree with each other instead of
|
|
1038
|
+
moving independently.
|
|
1039
|
+
]]
|
|
1040
|
+
function Visuals.setKind(color: Color3, urgency: number)
|
|
1041
|
+
runtime.activeTint = color
|
|
1042
|
+
runtime.energy = math.min(1.6, math.max(runtime.energy, urgency))
|
|
1043
|
+
runtime.quiet = 0
|
|
1044
|
+
--[[
|
|
1045
|
+
Drawn in as the command goes out, so the pair reads as one gesture: the
|
|
1046
|
+
cell contracts while the request is in flight and springs open when the
|
|
1047
|
+
answer arrives. On a fast call the two are almost one motion, which is
|
|
1048
|
+
itself the report -- a slow call visibly holds its breath.
|
|
1049
|
+
]]
|
|
1050
|
+
runtime.scale = math.min(runtime.scale, 0.85)
|
|
1051
|
+
runtime.scaleVelocity = math.min(runtime.scaleVelocity, 0)
|
|
1052
|
+
-- Re-aimed per kind so successive commands of different types visibly
|
|
1053
|
+
-- change the axis rather than continuing the same turn.
|
|
1054
|
+
runtime.spin = Vector3.new(
|
|
1055
|
+
0.12 + math.random() * 0.16,
|
|
1056
|
+
0.18 + math.random() * 0.22,
|
|
1057
|
+
0.06 + math.random() * 0.12
|
|
1058
|
+
)
|
|
1059
|
+
end
|
|
1060
|
+
|
|
1061
|
+
--[[
|
|
1062
|
+
What is running, in the same words the log uses. The motion says something is
|
|
1063
|
+
happening; this says what, and neither answers the other's question.
|
|
1064
|
+
]]
|
|
1065
|
+
function Visuals.setCaption(title: string)
|
|
1066
|
+
runtime.lastTitle = title
|
|
1067
|
+
-- A note is holding the caption's real text for later. Update what will be
|
|
1068
|
+
-- restored, not what is on screen, or the note is wiped by the next command
|
|
1069
|
+
-- and clearing it would put back a line that is already out of date.
|
|
1070
|
+
if runtime.noted ~= nil then
|
|
1071
|
+
runtime.noted = title
|
|
1072
|
+
return
|
|
1073
|
+
end
|
|
1074
|
+
if runtime.caption then
|
|
1075
|
+
runtime.caption.Text = title
|
|
1076
|
+
end
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
--[[
|
|
1080
|
+
Borrows the caption to answer something the user is pointing at.
|
|
1081
|
+
|
|
1082
|
+
The caption is the band's one line of prose, so a note goes there rather
|
|
1083
|
+
than into a floating panel: there is exactly one place on this widget that
|
|
1084
|
+
explains what you are looking at, and two would be one too many. Whatever
|
|
1085
|
+
the caption was saying is put back by `clearNote`, so a note never costs the
|
|
1086
|
+
user the line they were reading.
|
|
1087
|
+
]]
|
|
1088
|
+
function Visuals.showNote(text: string)
|
|
1089
|
+
if runtime.caption == nil or text == "" then
|
|
1090
|
+
return
|
|
1091
|
+
end
|
|
1092
|
+
if runtime.noted == nil then
|
|
1093
|
+
runtime.noted = runtime.caption.Text
|
|
1094
|
+
end
|
|
1095
|
+
runtime.caption.Text = text
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1098
|
+
function Visuals.clearNote()
|
|
1099
|
+
local held = runtime.noted
|
|
1100
|
+
if held == nil or runtime.caption == nil then
|
|
1101
|
+
return
|
|
1102
|
+
end
|
|
1103
|
+
runtime.noted = nil
|
|
1104
|
+
runtime.caption.Text = held
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
--[[
|
|
1108
|
+
Falls back to the last thing that ran once a command finishes.
|
|
1109
|
+
|
|
1110
|
+
The footer already reports totals, so repeating "idle" here would say the
|
|
1111
|
+
same word twice on one screen. What has just happened is more useful and is
|
|
1112
|
+
not shown anywhere else.
|
|
1113
|
+
]]
|
|
1114
|
+
function Visuals.setIdle()
|
|
1115
|
+
if runtime.caption == nil then
|
|
1116
|
+
return
|
|
1117
|
+
end
|
|
1118
|
+
local text = if runtime.lastTitle ~= nil
|
|
1119
|
+
then "last: " .. runtime.lastTitle
|
|
1120
|
+
else "waiting for a command"
|
|
1121
|
+
-- Same rule as setCaption: a note owns the line until it is cleared.
|
|
1122
|
+
if runtime.noted ~= nil then
|
|
1123
|
+
runtime.noted = text
|
|
1124
|
+
return
|
|
1125
|
+
end
|
|
1126
|
+
runtime.caption.Text = text
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
--[[
|
|
1130
|
+
Says whether an agent started from the prompt is currently working.
|
|
1131
|
+
|
|
1132
|
+
One call on each edge rather than a stream of keepalives: the state is a
|
|
1133
|
+
condition with two ends, and a heartbeat that has to keep arriving is a
|
|
1134
|
+
heartbeat that eventually does not -- leaving the strip pulsing at a
|
|
1135
|
+
conversation that finished.
|
|
1136
|
+
|
|
1137
|
+
Starting re-aims the spin, so the turn visibly changes axis at the moment the
|
|
1138
|
+
agent picks the work up. Stopping does nothing but drop the floor: what is
|
|
1139
|
+
left decays on its own, which is the honest picture of work that has just
|
|
1140
|
+
ended rather than work that was cut off.
|
|
1141
|
+
]]
|
|
1142
|
+
function Visuals.setThinking(thinking: boolean)
|
|
1143
|
+
if runtime.thinking == thinking then
|
|
1144
|
+
return
|
|
1145
|
+
end
|
|
1146
|
+
runtime.thinking = thinking
|
|
1147
|
+
if thinking then
|
|
1148
|
+
runtime.quiet = 0
|
|
1149
|
+
runtime.spin = Vector3.new(
|
|
1150
|
+
0.10 + math.random() * 0.14,
|
|
1151
|
+
0.16 + math.random() * 0.20,
|
|
1152
|
+
0.05 + math.random() * 0.10
|
|
1153
|
+
)
|
|
1154
|
+
end
|
|
1155
|
+
end
|
|
1156
|
+
|
|
1157
|
+
--[[
|
|
1158
|
+
Winds the cell down, because the session has stopped rather than paused.
|
|
1159
|
+
|
|
1160
|
+
The strip already decays on its own, but decay bottoms out at "idle and
|
|
1161
|
+
moving", which looks the same after twenty seconds as after two hours. This
|
|
1162
|
+
is the console telling the picture what it has just told the log, so the two
|
|
1163
|
+
agree.
|
|
1164
|
+
]]
|
|
1165
|
+
function Visuals.setQuiet()
|
|
1166
|
+
-- Not while an agent is mid-run. This is called when a client disconnects,
|
|
1167
|
+
-- and an agent that finishes one turn and is about to be asked another
|
|
1168
|
+
-- would otherwise wind the strip fully down between two halves of the same
|
|
1169
|
+
-- conversation.
|
|
1170
|
+
if runtime.thinking then
|
|
1171
|
+
return
|
|
1172
|
+
end
|
|
1173
|
+
runtime.quiet = QUIET_AFTER + 1
|
|
1174
|
+
runtime.activity = 0
|
|
1175
|
+
end
|
|
1176
|
+
|
|
1177
|
+
--[[
|
|
1178
|
+
Empties the trace.
|
|
1179
|
+
|
|
1180
|
+
Paired with the console's clear button. The bars used to survive it while the
|
|
1181
|
+
footer counters reset, so "clear" wiped the log, wiped the statistics, and
|
|
1182
|
+
left forty timings from the session it had just erased sitting on screen.
|
|
1183
|
+
]]
|
|
1184
|
+
function Visuals.clearTrace()
|
|
1185
|
+
for _, entry in runtime.bars do
|
|
1186
|
+
entry.frame:Destroy()
|
|
1187
|
+
entry.hit:Destroy()
|
|
1188
|
+
end
|
|
1189
|
+
table.clear(runtime.bars)
|
|
1190
|
+
clearReading()
|
|
1191
|
+
end
|
|
1192
|
+
|
|
1193
|
+
function Visuals.setVisible(visible: boolean)
|
|
1194
|
+
local band = runtime.band
|
|
1195
|
+
if band == nil then
|
|
1196
|
+
return
|
|
1197
|
+
end
|
|
1198
|
+
band.Visible = visible
|
|
1199
|
+
|
|
1200
|
+
-- Connected only while on screen: a hidden strip that keeps projecting
|
|
1201
|
+
-- geometry every frame is a battery complaint waiting to happen.
|
|
1202
|
+
if visible and runtime.connection == nil then
|
|
1203
|
+
runtime.connection = RunService.Heartbeat:Connect(step)
|
|
1204
|
+
elseif not visible and runtime.connection ~= nil then
|
|
1205
|
+
runtime.connection:Disconnect()
|
|
1206
|
+
runtime.connection = nil
|
|
1207
|
+
end
|
|
1208
|
+
end
|
|
1209
|
+
|
|
1210
|
+
function Visuals.isVisible(): boolean
|
|
1211
|
+
local band = runtime.band
|
|
1212
|
+
return band ~= nil and band.Visible
|
|
1213
|
+
end
|
|
1214
|
+
|
|
1215
|
+
--[[
|
|
1216
|
+
Swaps the cell's contents and recolours the strip's own chrome.
|
|
1217
|
+
|
|
1218
|
+
The order matters. The outgoing preset is unmounted before the cell is
|
|
1219
|
+
emptied, so a preset that keeps references cannot be handed destroyed
|
|
1220
|
+
Instances; the cell is then cleared outright rather than trusted to be
|
|
1221
|
+
clean, because a preset that errored midway through `mount` will have left
|
|
1222
|
+
something behind.
|
|
1223
|
+
]]
|
|
1224
|
+
function Visuals.applyTheme()
|
|
1225
|
+
local cell = runtime.cell
|
|
1226
|
+
local band = runtime.band
|
|
1227
|
+
if cell == nil or band == nil then
|
|
1228
|
+
return
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
if runtime.mounted then
|
|
1232
|
+
pcall(function()
|
|
1233
|
+
Themes.active().unmount()
|
|
1234
|
+
end)
|
|
1235
|
+
end
|
|
1236
|
+
for _, child in cell:GetChildren() do
|
|
1237
|
+
child:Destroy()
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
local palette = Themes.palette()
|
|
1241
|
+
band.BackgroundColor3 = palette.surface
|
|
1242
|
+
if runtime.divider then
|
|
1243
|
+
(runtime.divider :: Frame).BackgroundColor3 = palette.dim
|
|
1244
|
+
end
|
|
1245
|
+
if runtime.baseline then
|
|
1246
|
+
(runtime.baseline :: Frame).BackgroundColor3 = palette.dim
|
|
1247
|
+
end
|
|
1248
|
+
if runtime.highlight then
|
|
1249
|
+
(runtime.highlight :: Frame).BackgroundColor3 = palette.text
|
|
1250
|
+
end
|
|
1251
|
+
if runtime.caption then
|
|
1252
|
+
(runtime.caption :: TextLabel).TextColor3 = palette.text
|
|
1253
|
+
end
|
|
1254
|
+
if runtime.readout then
|
|
1255
|
+
(runtime.readout :: TextLabel).BackgroundColor3 = palette.background
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
local ok, err = pcall(function()
|
|
1259
|
+
Themes.active().mount(cell)
|
|
1260
|
+
end)
|
|
1261
|
+
runtime.mounted = ok
|
|
1262
|
+
if not ok then
|
|
1263
|
+
warn(string.format("[rbx-studio] theme %q failed to mount: %s", Themes.activeId(), tostring(err)))
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
relayout()
|
|
1267
|
+
-- A preset that failed to paint disabled the band; a new one deserves the
|
|
1268
|
+
-- chance to prove it works.
|
|
1269
|
+
band.Visible = true
|
|
1270
|
+
end
|
|
1271
|
+
|
|
1272
|
+
--[[
|
|
1273
|
+
Builds the strip. Everything except the cell's contents is created here and
|
|
1274
|
+
never again, which is what keeps the per-frame path allocation-free.
|
|
1275
|
+
]]
|
|
1276
|
+
function Visuals.mount(parent: Instance)
|
|
1277
|
+
local palette = Themes.palette()
|
|
1278
|
+
|
|
1279
|
+
local band = Instance.new("Frame")
|
|
1280
|
+
band.Name = "ActivityBand"
|
|
1281
|
+
band.BackgroundColor3 = palette.surface
|
|
1282
|
+
band.BorderSizePixel = 0
|
|
1283
|
+
band.Visible = false
|
|
1284
|
+
band.ClipsDescendants = true
|
|
1285
|
+
band.Parent = parent
|
|
1286
|
+
runtime.band = band
|
|
1287
|
+
|
|
1288
|
+
local cell = Instance.new("Frame")
|
|
1289
|
+
cell.Name = "Cell"
|
|
1290
|
+
cell.Position = UDim2.fromOffset(INSET, (Visuals.BAND_HEIGHT - CELL) / 2)
|
|
1291
|
+
cell.Size = UDim2.fromOffset(CELL, CELL)
|
|
1292
|
+
cell.BackgroundTransparency = 1
|
|
1293
|
+
cell.ClipsDescendants = true
|
|
1294
|
+
cell.Parent = band
|
|
1295
|
+
runtime.cell = cell
|
|
1296
|
+
|
|
1297
|
+
-- A hairline between the cell and the trace, so the strip reads as two
|
|
1298
|
+
-- instruments rather than one busy rectangle. Centred in the gutter, which
|
|
1299
|
+
-- is what keeps the extra margin as air on both sides rather than as a gap
|
|
1300
|
+
-- on one.
|
|
1301
|
+
local divider = Instance.new("Frame")
|
|
1302
|
+
divider.Name = "Divider"
|
|
1303
|
+
divider.Position = UDim2.fromOffset(INSET + CELL + GUTTER / 2, 10)
|
|
1304
|
+
divider.Size = UDim2.new(0, 1, 1, -20)
|
|
1305
|
+
divider.BackgroundColor3 = palette.dim
|
|
1306
|
+
divider.BackgroundTransparency = 0.78
|
|
1307
|
+
divider.BorderSizePixel = 0
|
|
1308
|
+
divider.Parent = band
|
|
1309
|
+
runtime.divider = divider
|
|
1310
|
+
|
|
1311
|
+
local right = INSET + CELL + GUTTER
|
|
1312
|
+
|
|
1313
|
+
local caption = Instance.new("TextLabel")
|
|
1314
|
+
caption.Name = "Caption"
|
|
1315
|
+
caption.Position = UDim2.new(0, right, 0, 6)
|
|
1316
|
+
caption.Size = UDim2.new(1, -right - 12, 0, 14)
|
|
1317
|
+
caption.BackgroundTransparency = 1
|
|
1318
|
+
caption.Font = Enum.Font.Code
|
|
1319
|
+
caption.TextSize = 11
|
|
1320
|
+
caption.TextColor3 = palette.text
|
|
1321
|
+
caption.TextXAlignment = Enum.TextXAlignment.Left
|
|
1322
|
+
caption.TextTruncate = Enum.TextTruncate.AtEnd
|
|
1323
|
+
caption.Text = "idle"
|
|
1324
|
+
caption.Parent = band
|
|
1325
|
+
runtime.caption = caption
|
|
1326
|
+
|
|
1327
|
+
local trace = Instance.new("Frame")
|
|
1328
|
+
trace.Name = "Trace"
|
|
1329
|
+
trace.Position = UDim2.new(0, right, 0, 24)
|
|
1330
|
+
trace.Size = UDim2.new(1, -right - 12, 0, TRACE_HEIGHT)
|
|
1331
|
+
trace.BackgroundTransparency = 1
|
|
1332
|
+
trace.ClipsDescendants = true
|
|
1333
|
+
trace.Parent = band
|
|
1334
|
+
runtime.trace = trace
|
|
1335
|
+
|
|
1336
|
+
-- A baseline under the bars, so an empty trace still reads as an instrument
|
|
1337
|
+
-- waiting for data rather than as a blank gap.
|
|
1338
|
+
local baseline = Instance.new("Frame")
|
|
1339
|
+
baseline.Name = "Baseline"
|
|
1340
|
+
baseline.AnchorPoint = Vector2.new(0, 1)
|
|
1341
|
+
baseline.Position = UDim2.fromScale(0, 1)
|
|
1342
|
+
baseline.Size = UDim2.new(1, 0, 0, 1)
|
|
1343
|
+
baseline.BackgroundColor3 = palette.dim
|
|
1344
|
+
baseline.BackgroundTransparency = 0.75
|
|
1345
|
+
baseline.BorderSizePixel = 0
|
|
1346
|
+
baseline.Parent = trace
|
|
1347
|
+
runtime.baseline = baseline
|
|
1348
|
+
|
|
1349
|
+
--[[
|
|
1350
|
+
Built once and moved, and created BEFORE the bars exist so it sits under
|
|
1351
|
+
them in draw order -- a highlight drawn over a one-pixel bar would hide the
|
|
1352
|
+
very thing it is pointing at.
|
|
1353
|
+
]]
|
|
1354
|
+
local highlight = Instance.new("Frame")
|
|
1355
|
+
highlight.Name = "Highlight"
|
|
1356
|
+
highlight.AnchorPoint = Vector2.new(0.5, 1)
|
|
1357
|
+
highlight.Position = UDim2.new(1, 0, 1, 0)
|
|
1358
|
+
highlight.Size = UDim2.new(0, BAR_STRIDE, 1, 0)
|
|
1359
|
+
highlight.BackgroundColor3 = palette.text
|
|
1360
|
+
-- Faint enough not to hide the bar it sits behind, solid enough to be seen
|
|
1361
|
+
-- at all.
|
|
1362
|
+
highlight.BackgroundTransparency = 0.7
|
|
1363
|
+
highlight.BorderSizePixel = 0
|
|
1364
|
+
highlight.Visible = false
|
|
1365
|
+
highlight.Parent = trace
|
|
1366
|
+
runtime.highlight = highlight
|
|
1367
|
+
|
|
1368
|
+
--[[
|
|
1369
|
+
The reading sits at the left end of the trace rather than beside the bar it
|
|
1370
|
+
describes. Following the pointer would put it off the right edge for the
|
|
1371
|
+
newest bars, which are the ones most often asked about, and the left end is
|
|
1372
|
+
empty until forty calls have accumulated.
|
|
1373
|
+
]]
|
|
1374
|
+
local readout = Instance.new("TextLabel")
|
|
1375
|
+
readout.Name = "Readout"
|
|
1376
|
+
readout.AnchorPoint = Vector2.new(0, 0.5)
|
|
1377
|
+
readout.Position = UDim2.new(0, 0, 0.5, 0)
|
|
1378
|
+
-- Sized by its text now that it carries a name as well as a number. A fixed
|
|
1379
|
+
-- 110px was enough for "18 ms" and truncates anything with a phrase in front
|
|
1380
|
+
-- of it, which is the half worth reading.
|
|
1381
|
+
readout.AutomaticSize = Enum.AutomaticSize.X
|
|
1382
|
+
readout.Size = UDim2.new(0, 0, 0, 14)
|
|
1383
|
+
readout.BackgroundColor3 = palette.background
|
|
1384
|
+
readout.BackgroundTransparency = 0.15
|
|
1385
|
+
readout.BorderSizePixel = 0
|
|
1386
|
+
readout.Font = Enum.Font.Code
|
|
1387
|
+
readout.TextSize = 11
|
|
1388
|
+
readout.TextXAlignment = Enum.TextXAlignment.Left
|
|
1389
|
+
readout.Text = ""
|
|
1390
|
+
readout.Visible = false
|
|
1391
|
+
readout.ZIndex = 3
|
|
1392
|
+
readout.Parent = trace
|
|
1393
|
+
runtime.readout = readout
|
|
1394
|
+
|
|
1395
|
+
local readoutPadding = Instance.new("UIPadding")
|
|
1396
|
+
readoutPadding.PaddingLeft = UDim.new(0, 4)
|
|
1397
|
+
readoutPadding.PaddingRight = UDim.new(0, 4)
|
|
1398
|
+
readoutPadding.Parent = readout
|
|
1399
|
+
|
|
1400
|
+
local ok, err = pcall(function()
|
|
1401
|
+
Themes.active().mount(cell)
|
|
1402
|
+
end)
|
|
1403
|
+
runtime.mounted = ok
|
|
1404
|
+
if not ok then
|
|
1405
|
+
warn(string.format("[rbx-studio] theme %q failed to mount: %s", Themes.activeId(), tostring(err)))
|
|
1406
|
+
end
|
|
1407
|
+
end
|
|
1408
|
+
|
|
1409
|
+
return Visuals
|