@el4cteo/rbx-studio-mcp 0.3.0 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -26
- package/dist/index.js +1 -1
- package/dist/lib/format.js +18 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/pluginbuild.js +23 -13
- package/dist/lib/pluginbuild.js.map +1 -1
- package/dist/tools/api.js +20 -1
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/debug.js +18 -8
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/discover.js +5 -0
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +11 -1
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +27 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/session.js +16 -0
- package/dist/tools/session.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +257 -116
- package/plugin/src/Paths.luau +138 -69
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +147 -0
- package/plugin/src/Themes/Blueprint.luau +213 -0
- package/plugin/src/Themes/Draw.luau +177 -0
- package/plugin/src/Themes/Lattice.luau +304 -0
- package/plugin/src/Themes/Nebula.luau +182 -0
- package/plugin/src/Themes/Observatory.luau +200 -0
- package/plugin/src/Themes/Orbit.luau +268 -0
- package/plugin/src/Themes/Phosphor.luau +200 -0
- package/plugin/src/Themes/Theme.luau +121 -0
- package/plugin/src/Themes/Void.luau +230 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Visuals.luau +788 -907
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/handlers/Scripts.luau +58 -2
- package/plugin/src/init.server.luau +386 -354
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Blueprint -- the only preset with no perspective.
|
|
4
|
+
|
|
5
|
+
An orthographic cube on a drafting grid, with tick marks on two edges. Every
|
|
6
|
+
other theme leans on depth to make the cell interesting; this one refuses it
|
|
7
|
+
on purpose, because a drawing that says "this is a measurement, not a
|
|
8
|
+
picture" is a genuinely different thing to look at, and it is the one preset
|
|
9
|
+
that reads as an engineering instrument rather than as an ornament.
|
|
10
|
+
|
|
11
|
+
Orthographic here means the projection divide is simply skipped: the cube
|
|
12
|
+
turns, but a far edge is exactly as long as a near one. That looks wrong for
|
|
13
|
+
a second and then reads as a plan view, which is the effect.
|
|
14
|
+
|
|
15
|
+
It is also the lightest preset -- pale lines on navy, no glow, no bloom. On
|
|
16
|
+
a bright monitor in a bright room it is the only one of the eight that is
|
|
17
|
+
comfortable to look at all day.
|
|
18
|
+
]]
|
|
19
|
+
|
|
20
|
+
local Draw = require(script.Parent.Draw)
|
|
21
|
+
local Theme = require(script.Parent.Theme)
|
|
22
|
+
|
|
23
|
+
local Blueprint = {}
|
|
24
|
+
|
|
25
|
+
Blueprint.id = "blueprint"
|
|
26
|
+
Blueprint.label = "Blueprint"
|
|
27
|
+
Blueprint.blurb = "Drafting grid"
|
|
28
|
+
|
|
29
|
+
Blueprint.palette = {
|
|
30
|
+
background = Color3.fromRGB(12, 22, 42),
|
|
31
|
+
surface = Color3.fromRGB(18, 32, 58),
|
|
32
|
+
rule = Color3.fromRGB(126, 196, 240),
|
|
33
|
+
text = Color3.fromRGB(206, 226, 246),
|
|
34
|
+
dim = Color3.fromRGB(86, 122, 164),
|
|
35
|
+
green = Color3.fromRGB(128, 226, 190),
|
|
36
|
+
red = Color3.fromRGB(248, 138, 138),
|
|
37
|
+
amber = Color3.fromRGB(244, 202, 130),
|
|
38
|
+
cyan = Color3.fromRGB(140, 210, 246),
|
|
39
|
+
violet = Color3.fromRGB(170, 176, 240),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
local CUBE = {
|
|
43
|
+
Vector3.new(-1, -1, -1), Vector3.new(1, -1, -1),
|
|
44
|
+
Vector3.new(1, 1, -1), Vector3.new(-1, 1, -1),
|
|
45
|
+
Vector3.new(-1, -1, 1), Vector3.new(1, -1, 1),
|
|
46
|
+
Vector3.new(1, 1, 1), Vector3.new(-1, 1, 1),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
local EDGES = {
|
|
50
|
+
{ 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 1 },
|
|
51
|
+
{ 5, 6 }, { 6, 7 }, { 7, 8 }, { 8, 5 },
|
|
52
|
+
{ 1, 5 }, { 2, 6 }, { 3, 7 }, { 4, 8 },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
local edges: { Frame } = {}
|
|
56
|
+
local corners: { Frame } = {}
|
|
57
|
+
local grid: { Frame } = {}
|
|
58
|
+
local ticks: { Frame } = {}
|
|
59
|
+
|
|
60
|
+
--[[
|
|
61
|
+
Builds the grid the drawing sits on.
|
|
62
|
+
|
|
63
|
+
Drawn once and never touched again -- it does not react to anything, which
|
|
64
|
+
is the point of graph paper. Kept out of `paint` entirely so the per-frame
|
|
65
|
+
loop stays proportional to the cube.
|
|
66
|
+
]]
|
|
67
|
+
local function drawGrid(cell: Frame, palette: Theme.Palette, size: number)
|
|
68
|
+
local spacing = 8
|
|
69
|
+
local lines: { Frame } = {}
|
|
70
|
+
for offset = spacing, size - 1, spacing do
|
|
71
|
+
local vertical = Instance.new("Frame")
|
|
72
|
+
vertical.BorderSizePixel = 0
|
|
73
|
+
vertical.BackgroundColor3 = palette.dim
|
|
74
|
+
vertical.BackgroundTransparency = 0.86
|
|
75
|
+
vertical.Size = UDim2.new(0, 1, 1, 0)
|
|
76
|
+
vertical.Position = UDim2.fromOffset(offset, 0)
|
|
77
|
+
vertical.Parent = cell
|
|
78
|
+
table.insert(lines, vertical)
|
|
79
|
+
|
|
80
|
+
local horizontal = Instance.new("Frame")
|
|
81
|
+
horizontal.BorderSizePixel = 0
|
|
82
|
+
horizontal.BackgroundColor3 = palette.dim
|
|
83
|
+
horizontal.BackgroundTransparency = 0.86
|
|
84
|
+
horizontal.Size = UDim2.new(1, 0, 0, 1)
|
|
85
|
+
horizontal.Position = UDim2.fromOffset(0, offset)
|
|
86
|
+
horizontal.Parent = cell
|
|
87
|
+
table.insert(lines, horizontal)
|
|
88
|
+
end
|
|
89
|
+
return lines
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
function Blueprint.mount(cell: Frame)
|
|
93
|
+
local size = cell.AbsoluteSize.X
|
|
94
|
+
if size < 8 then
|
|
95
|
+
size = 40
|
|
96
|
+
end
|
|
97
|
+
grid = drawGrid(cell, Blueprint.palette, size)
|
|
98
|
+
ticks = Draw.pool(cell, 8, "Tick", false)
|
|
99
|
+
edges = Draw.pool(cell, #EDGES, "Edge", false)
|
|
100
|
+
corners = Draw.pool(cell, #CUBE, "Corner", false)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
function Blueprint.unmount()
|
|
104
|
+
edges = {}
|
|
105
|
+
corners = {}
|
|
106
|
+
grid = {}
|
|
107
|
+
ticks = {}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
function Blueprint.paint(ctx: Theme.Context, _delta: number)
|
|
111
|
+
local radius = ctx.size * 0.2 * ctx.scale
|
|
112
|
+
-- Slower than Lattice and on two axes rather than three. A plan view that
|
|
113
|
+
-- tumbles is not a plan view.
|
|
114
|
+
local angle = Vector3.new(ctx.angle.X * 0.5, ctx.angle.Y * 0.5, 0)
|
|
115
|
+
|
|
116
|
+
local projected: { Vector2 } = {}
|
|
117
|
+
for index, vertex in CUBE do
|
|
118
|
+
local turned = Draw.rotate(vertex, angle)
|
|
119
|
+
-- No perspective divide. This is the whole theme.
|
|
120
|
+
projected[index] = ctx.centre + Vector2.new(turned.X * radius, turned.Y * radius)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
for index, edge in EDGES do
|
|
124
|
+
local frame = edges[index]
|
|
125
|
+
if frame == nil then
|
|
126
|
+
continue
|
|
127
|
+
end
|
|
128
|
+
Draw.edge(
|
|
129
|
+
frame,
|
|
130
|
+
projected[edge[1]],
|
|
131
|
+
projected[edge[2]],
|
|
132
|
+
1,
|
|
133
|
+
ctx.tint:Lerp(Color3.new(1, 1, 1), ctx.flare * 0.5),
|
|
134
|
+
math.clamp(0.55 - ctx.flare * 0.35, 0, 1)
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
--[[
|
|
139
|
+
Corners as small open squares rather than filled dots -- a draughtsman's
|
|
140
|
+
node. Uniform weight, because in an orthographic drawing no corner is
|
|
141
|
+
nearer than another and shading one would be a lie the rest of the theme
|
|
142
|
+
is built on refusing.
|
|
143
|
+
]]
|
|
144
|
+
for index = 1, #CUBE do
|
|
145
|
+
local frame = corners[index]
|
|
146
|
+
if frame == nil then
|
|
147
|
+
continue
|
|
148
|
+
end
|
|
149
|
+
Draw.dot(frame, projected[index], 2, ctx.palette.cyan, 0.35)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
--[[
|
|
153
|
+
Dimension ticks along the bottom and left, marching with the rotation.
|
|
154
|
+
|
|
155
|
+
They carry no data. They are there because a technical drawing has them,
|
|
156
|
+
and their slow travel is what gives an idle Blueprint something moving
|
|
157
|
+
to look at now that nothing is glowing or blooming.
|
|
158
|
+
]]
|
|
159
|
+
for index = 1, 8 do
|
|
160
|
+
local frame = ticks[index]
|
|
161
|
+
if frame == nil then
|
|
162
|
+
continue
|
|
163
|
+
end
|
|
164
|
+
local along = ((index / 8) + ctx.angle.Y * 0.05) % 1
|
|
165
|
+
if index <= 4 then
|
|
166
|
+
local x = along * ctx.size
|
|
167
|
+
Draw.edge(
|
|
168
|
+
frame,
|
|
169
|
+
Vector2.new(x, ctx.size - 4),
|
|
170
|
+
Vector2.new(x, ctx.size - 1),
|
|
171
|
+
1,
|
|
172
|
+
ctx.palette.dim,
|
|
173
|
+
0.5
|
|
174
|
+
)
|
|
175
|
+
else
|
|
176
|
+
local y = along * ctx.size
|
|
177
|
+
Draw.edge(frame, Vector2.new(1, y), Vector2.new(4, y), 1, ctx.palette.dim, 0.5)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
--[[
|
|
183
|
+
History as a section drawing: hairline columns with a tick at the top of
|
|
184
|
+
each, so a bar's height is marked as well as drawn.
|
|
185
|
+
]]
|
|
186
|
+
function Blueprint.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
187
|
+
local frame = slot.frame
|
|
188
|
+
local recency = 1 - slot.age
|
|
189
|
+
local height = math.max(1, math.floor(1 + slot.weight * 21))
|
|
190
|
+
|
|
191
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
192
|
+
frame.Size = UDim2.fromOffset(1, height)
|
|
193
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
194
|
+
frame.BackgroundColor3 = if slot.ok then ctx.palette.cyan else ctx.palette.red
|
|
195
|
+
frame.BackgroundTransparency = math.clamp(0.6 - recency * 0.5, 0, 1)
|
|
196
|
+
frame.Rotation = 0
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
function Blueprint.swatch(frame: Frame, palette: Theme.Palette)
|
|
200
|
+
drawGrid(frame, palette, 24)
|
|
201
|
+
local centre = Vector2.new(20, 12)
|
|
202
|
+
local pool = Draw.pool(frame, #EDGES, "Edge", false)
|
|
203
|
+
local projected: { Vector2 } = {}
|
|
204
|
+
for index, vertex in CUBE do
|
|
205
|
+
local turned = Draw.rotate(vertex, Vector3.new(0.45, 0.6, 0))
|
|
206
|
+
projected[index] = centre + Vector2.new(turned.X * 6, turned.Y * 6)
|
|
207
|
+
end
|
|
208
|
+
for index, edge in EDGES do
|
|
209
|
+
Draw.edge(pool[index], projected[edge[1]], projected[edge[2]], 1, palette.cyan, 0.4)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
return Blueprint
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Drawing primitives shared by the presets.
|
|
4
|
+
|
|
5
|
+
Roblox has no line primitive and no point primitive, so both are Frames --
|
|
6
|
+
a segment is a thin rectangle turned about its midpoint, a point is a small
|
|
7
|
+
square with a full corner radius. Every preset that draws either would
|
|
8
|
+
otherwise carry its own copy, and they did not stay identical: the pooling
|
|
9
|
+
rule (build once in `mount`, never allocate in `paint`) is easy to state and
|
|
10
|
+
easy to forget, so it lives here where forgetting it is not an option.
|
|
11
|
+
]]
|
|
12
|
+
|
|
13
|
+
local Draw = {}
|
|
14
|
+
|
|
15
|
+
--[[
|
|
16
|
+
Turns a point about all three axes.
|
|
17
|
+
|
|
18
|
+
Applied X, then Y, then Z. The order is arbitrary but must not change: the
|
|
19
|
+
presets were tuned against this one, and swapping it silently re-aims every
|
|
20
|
+
spin axis in the project.
|
|
21
|
+
]]
|
|
22
|
+
function Draw.rotate(point: Vector3, angle: Vector3): Vector3
|
|
23
|
+
local sinX, cosX = math.sin(angle.X), math.cos(angle.X)
|
|
24
|
+
local sinY, cosY = math.sin(angle.Y), math.cos(angle.Y)
|
|
25
|
+
local sinZ, cosZ = math.sin(angle.Z), math.cos(angle.Z)
|
|
26
|
+
|
|
27
|
+
local y1 = point.Y * cosX - point.Z * sinX
|
|
28
|
+
local z1 = point.Y * sinX + point.Z * cosX
|
|
29
|
+
local x2 = point.X * cosY + z1 * sinY
|
|
30
|
+
local z2 = -point.X * sinY + z1 * cosY
|
|
31
|
+
local x3 = x2 * cosZ - y1 * sinZ
|
|
32
|
+
local y3 = x2 * sinZ + y1 * cosZ
|
|
33
|
+
|
|
34
|
+
return Vector3.new(x3, y3, z2)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
--[[
|
|
38
|
+
A perspective divide. Returns the screen point and the depth it came from,
|
|
39
|
+
because depth is what makes a wireframe read as solid -- near edges are
|
|
40
|
+
brighter and thicker because they genuinely are nearer.
|
|
41
|
+
]]
|
|
42
|
+
function Draw.project(
|
|
43
|
+
turned: Vector3,
|
|
44
|
+
focal: number,
|
|
45
|
+
radius: number,
|
|
46
|
+
centre: Vector2
|
|
47
|
+
): (Vector2, number)
|
|
48
|
+
local scale = focal / (focal - turned.Z)
|
|
49
|
+
return centre + Vector2.new(turned.X * scale * radius, turned.Y * scale * radius), turned.Z
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
--[[
|
|
53
|
+
One segment as a rectangle turned about its midpoint. `Rotation` pivots a
|
|
54
|
+
GuiObject around its centre, which is exactly what a segment needs.
|
|
55
|
+
]]
|
|
56
|
+
function Draw.edge(
|
|
57
|
+
frame: Frame,
|
|
58
|
+
a: Vector2,
|
|
59
|
+
b: Vector2,
|
|
60
|
+
thickness: number,
|
|
61
|
+
color: Color3,
|
|
62
|
+
transparency: number
|
|
63
|
+
)
|
|
64
|
+
local delta = b - a
|
|
65
|
+
local length = delta.Magnitude
|
|
66
|
+
if length < 0.5 then
|
|
67
|
+
frame.Visible = false
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
frame.Visible = true
|
|
71
|
+
frame.Position = UDim2.fromOffset((a.X + b.X) / 2, (a.Y + b.Y) / 2)
|
|
72
|
+
frame.Size = UDim2.fromOffset(math.ceil(length), thickness)
|
|
73
|
+
frame.Rotation = math.deg(math.atan2(delta.Y, delta.X))
|
|
74
|
+
frame.BackgroundColor3 = color
|
|
75
|
+
frame.BackgroundTransparency = transparency
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
--[[
|
|
79
|
+
A point. Square frames with a full corner radius, so `size` is a diameter.
|
|
80
|
+
]]
|
|
81
|
+
function Draw.dot(frame: Frame, at: Vector2, size: number, color: Color3, transparency: number)
|
|
82
|
+
frame.Visible = true
|
|
83
|
+
frame.Position = UDim2.fromOffset(at.X, at.Y)
|
|
84
|
+
frame.Size = UDim2.fromOffset(size, size)
|
|
85
|
+
frame.BackgroundColor3 = color
|
|
86
|
+
frame.BackgroundTransparency = transparency
|
|
87
|
+
frame.Rotation = 0
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
--[[
|
|
91
|
+
Builds a pool of centred frames. `round` gives them a full corner radius,
|
|
92
|
+
which is the difference between a point and a tiny square.
|
|
93
|
+
]]
|
|
94
|
+
function Draw.pool(parent: Instance, count: number, name: string, round: boolean): { Frame }
|
|
95
|
+
local frames: { Frame } = {}
|
|
96
|
+
for index = 1, count do
|
|
97
|
+
local frame = Instance.new("Frame")
|
|
98
|
+
frame.Name = string.format("%s%02d", name, index)
|
|
99
|
+
frame.AnchorPoint = Vector2.new(0.5, 0.5)
|
|
100
|
+
frame.BorderSizePixel = 0
|
|
101
|
+
frame.Visible = false
|
|
102
|
+
frame.Parent = parent
|
|
103
|
+
if round then
|
|
104
|
+
local corner = Instance.new("UICorner")
|
|
105
|
+
corner.CornerRadius = UDim.new(1, 0)
|
|
106
|
+
corner.Parent = frame
|
|
107
|
+
end
|
|
108
|
+
frames[index] = frame
|
|
109
|
+
end
|
|
110
|
+
return frames
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
--[[
|
|
114
|
+
Empties a cell between presets.
|
|
115
|
+
|
|
116
|
+
Themes are singletons -- only one is ever mounted -- so `unmount` could just
|
|
117
|
+
destroy the frames it made. It goes through the cell instead so a preset
|
|
118
|
+
that returns early on an error still leaves nothing behind for the next one
|
|
119
|
+
to draw on top of.
|
|
120
|
+
]]
|
|
121
|
+
function Draw.clear(cell: Frame)
|
|
122
|
+
for _, child in cell:GetChildren() do
|
|
123
|
+
child:Destroy()
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
local function fract(value: number): number
|
|
128
|
+
return value - math.floor(value)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
--[[
|
|
132
|
+
Deterministic noise for a given index.
|
|
133
|
+
|
|
134
|
+
Presets that scatter things -- stars, dust, motes -- must scatter them the
|
|
135
|
+
same way every frame, and `math.random` in a paint loop would make the scene
|
|
136
|
+
boil. Seeding per element and reading a fixed sequence gives a stable
|
|
137
|
+
arrangement that still looks unplanned.
|
|
138
|
+
|
|
139
|
+
This was `fract(sin(index * 12.9898 + salt * 78.233) * 43758.5453)`, the
|
|
140
|
+
usual shader one-liner, and it is wrong for the way it is used here. The
|
|
141
|
+
salt only shifts the PHASE of a single sequence, so two draws for the same
|
|
142
|
+
index -- a longitude and a latitude, say -- stay correlated: the points fall
|
|
143
|
+
near a curve instead of filling the space, and at twenty-odd samples two of
|
|
144
|
+
them land almost exactly on top of each other. Measured over 26 points, the
|
|
145
|
+
closest pair sat 0.017 apart on a unit sphere where an even spread gives
|
|
146
|
+
0.6.
|
|
147
|
+
|
|
148
|
+
Hoskins' hash mixes the two inputs through each other instead of adding
|
|
149
|
+
them, which decorrelates the draws.
|
|
150
|
+
]]
|
|
151
|
+
function Draw.hash(index: number, salt: number): number
|
|
152
|
+
local x = fract(index * 0.1031)
|
|
153
|
+
local y = fract(salt * 0.1031)
|
|
154
|
+
local z = fract(index * 0.0973)
|
|
155
|
+
local mixed = x * (y + 33.33) + y * (z + 33.33) + z * (x + 33.33)
|
|
156
|
+
return fract((x + mixed + y + mixed) * (z + mixed))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
--[[
|
|
160
|
+
The `index`-th of `count` points spread evenly over a unit sphere.
|
|
161
|
+
|
|
162
|
+
For a starfield, "unplanned" is not the requirement -- "no visible clumps
|
|
163
|
+
and no visible gaps" is, and no hash gives that. The Fibonacci lattice does:
|
|
164
|
+
walking the golden angle while stepping evenly down the z axis puts the
|
|
165
|
+
points at a measured centroid offset of 0.004 from the origin, against 0.23
|
|
166
|
+
for the hash it replaced, which is the difference between a sky and a smear
|
|
167
|
+
in one corner.
|
|
168
|
+
]]
|
|
169
|
+
function Draw.sphere(index: number, count: number): Vector3
|
|
170
|
+
local z = 1 - (2 * index - 1) / count
|
|
171
|
+
local radius = math.sqrt(math.max(0, 1 - z * z))
|
|
172
|
+
-- The golden angle. Any other turn per step lines the points up into arms.
|
|
173
|
+
local turn = (index - 1) * math.pi * (3 - math.sqrt(5))
|
|
174
|
+
return Vector3.new(radius * math.cos(turn), radius * math.sin(turn), z)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
return Draw
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Lattice -- the default.
|
|
4
|
+
|
|
5
|
+
A turning wireframe solid with a counter-rotating core, over a bar trace of
|
|
6
|
+
the last forty calls. This is the console's original look, and it stays the
|
|
7
|
+
default because it is the most legible of the eight: hard edges, one accent
|
|
8
|
+
colour, nothing moving that is not reporting something.
|
|
9
|
+
|
|
10
|
+
The solid is a real projection rather than a canned loop. Vertices rotate in
|
|
11
|
+
three dimensions and pass through a perspective divide every frame, so near
|
|
12
|
+
edges are brighter and thicker because they genuinely are nearer, and the
|
|
13
|
+
shape climbs a ladder -- tetrahedron, octahedron, cube, icosahedron -- as the
|
|
14
|
+
session gets busier. An idle plugin shows four vertices; a working one shows
|
|
15
|
+
twelve. The shape itself is the load meter.
|
|
16
|
+
]]
|
|
17
|
+
|
|
18
|
+
local Draw = require(script.Parent.Draw)
|
|
19
|
+
local Theme = require(script.Parent.Theme)
|
|
20
|
+
|
|
21
|
+
local PHI = (1 + math.sqrt(5)) / 2
|
|
22
|
+
|
|
23
|
+
type Shape = {
|
|
24
|
+
name: string,
|
|
25
|
+
vertices: { Vector3 },
|
|
26
|
+
edges: { { number } },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
--[[
|
|
30
|
+
Ordered by how busy they look. The session climbs this list as it works, so
|
|
31
|
+
the shape itself reports load: an idle plugin shows a tetrahedron, a busy one
|
|
32
|
+
an icosahedron.
|
|
33
|
+
]]
|
|
34
|
+
local SHAPES: { Shape } = {
|
|
35
|
+
{
|
|
36
|
+
name = "tetrahedron",
|
|
37
|
+
vertices = {
|
|
38
|
+
Vector3.new(1, 1, 1),
|
|
39
|
+
Vector3.new(1, -1, -1),
|
|
40
|
+
Vector3.new(-1, 1, -1),
|
|
41
|
+
Vector3.new(-1, -1, 1),
|
|
42
|
+
},
|
|
43
|
+
edges = { { 1, 2 }, { 1, 3 }, { 1, 4 }, { 2, 3 }, { 2, 4 }, { 3, 4 } },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name = "octahedron",
|
|
47
|
+
vertices = {
|
|
48
|
+
Vector3.new(1, 0, 0),
|
|
49
|
+
Vector3.new(-1, 0, 0),
|
|
50
|
+
Vector3.new(0, 1, 0),
|
|
51
|
+
Vector3.new(0, -1, 0),
|
|
52
|
+
Vector3.new(0, 0, 1),
|
|
53
|
+
Vector3.new(0, 0, -1),
|
|
54
|
+
},
|
|
55
|
+
edges = {
|
|
56
|
+
{ 1, 3 }, { 1, 4 }, { 1, 5 }, { 1, 6 },
|
|
57
|
+
{ 2, 3 }, { 2, 4 }, { 2, 5 }, { 2, 6 },
|
|
58
|
+
{ 3, 5 }, { 5, 4 }, { 4, 6 }, { 6, 3 },
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name = "cube",
|
|
63
|
+
vertices = {
|
|
64
|
+
Vector3.new(-1, -1, -1), Vector3.new(1, -1, -1),
|
|
65
|
+
Vector3.new(1, 1, -1), Vector3.new(-1, 1, -1),
|
|
66
|
+
Vector3.new(-1, -1, 1), Vector3.new(1, -1, 1),
|
|
67
|
+
Vector3.new(1, 1, 1), Vector3.new(-1, 1, 1),
|
|
68
|
+
},
|
|
69
|
+
edges = {
|
|
70
|
+
{ 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 1 },
|
|
71
|
+
{ 5, 6 }, { 6, 7 }, { 7, 8 }, { 8, 5 },
|
|
72
|
+
{ 1, 5 }, { 2, 6 }, { 3, 7 }, { 4, 8 },
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name = "icosahedron",
|
|
77
|
+
vertices = {
|
|
78
|
+
Vector3.new(0, 1, PHI), Vector3.new(0, -1, PHI),
|
|
79
|
+
Vector3.new(0, 1, -PHI), Vector3.new(0, -1, -PHI),
|
|
80
|
+
Vector3.new(1, PHI, 0), Vector3.new(-1, PHI, 0),
|
|
81
|
+
Vector3.new(1, -PHI, 0), Vector3.new(-1, -PHI, 0),
|
|
82
|
+
Vector3.new(PHI, 0, 1), Vector3.new(PHI, 0, -1),
|
|
83
|
+
Vector3.new(-PHI, 0, 1), Vector3.new(-PHI, 0, -1),
|
|
84
|
+
},
|
|
85
|
+
edges = {
|
|
86
|
+
{ 1, 2 }, { 1, 5 }, { 1, 6 }, { 1, 9 }, { 1, 11 },
|
|
87
|
+
{ 2, 7 }, { 2, 8 }, { 2, 9 }, { 2, 11 },
|
|
88
|
+
{ 3, 4 }, { 3, 5 }, { 3, 6 }, { 3, 10 }, { 3, 12 },
|
|
89
|
+
{ 4, 7 }, { 4, 8 }, { 4, 10 }, { 4, 12 },
|
|
90
|
+
{ 5, 6 }, { 5, 9 }, { 5, 10 },
|
|
91
|
+
{ 6, 11 }, { 6, 12 },
|
|
92
|
+
{ 7, 8 }, { 7, 9 }, { 7, 10 },
|
|
93
|
+
{ 8, 11 }, { 8, 12 },
|
|
94
|
+
{ 9, 10 }, { 11, 12 },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
local MAX_EDGES = 30
|
|
100
|
+
local MAX_VERTICES = 12
|
|
101
|
+
local FOCAL = 3.4
|
|
102
|
+
|
|
103
|
+
-- The inner solid, counter-rotating inside the outer one. An octahedron
|
|
104
|
+
-- regardless of what is outside it: it reads as a core rather than as a second
|
|
105
|
+
-- object competing for attention, and its 12 edges are cheap.
|
|
106
|
+
local INNER_SHAPE = 2
|
|
107
|
+
local INNER_SCALE = 0.42
|
|
108
|
+
|
|
109
|
+
-- The solid's resting radius as a fraction of the cell. At full overshoot the
|
|
110
|
+
-- perspective divide pushes a near vertex to roughly 15px, which still clears
|
|
111
|
+
-- the cell's half-width.
|
|
112
|
+
local BASE_RADIUS = 0.185
|
|
113
|
+
|
|
114
|
+
local Lattice = {}
|
|
115
|
+
|
|
116
|
+
Lattice.id = "lattice"
|
|
117
|
+
Lattice.label = "Lattice"
|
|
118
|
+
Lattice.blurb = "Wireframe solid"
|
|
119
|
+
|
|
120
|
+
Lattice.palette = {
|
|
121
|
+
-- Violet-tinted near-black. The cast is slight but it stops the panel
|
|
122
|
+
-- reading as the same grey as every other Studio dock.
|
|
123
|
+
background = Color3.fromRGB(11, 12, 20),
|
|
124
|
+
surface = Color3.fromRGB(18, 20, 31),
|
|
125
|
+
rule = Color3.fromRGB(167, 139, 250),
|
|
126
|
+
text = Color3.fromRGB(206, 212, 226),
|
|
127
|
+
dim = Color3.fromRGB(90, 97, 120),
|
|
128
|
+
green = Color3.fromRGB(110, 231, 168),
|
|
129
|
+
red = Color3.fromRGB(251, 113, 133),
|
|
130
|
+
amber = Color3.fromRGB(251, 191, 36),
|
|
131
|
+
cyan = Color3.fromRGB(103, 232, 249),
|
|
132
|
+
violet = Color3.fromRGB(167, 139, 250),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
local edges: { Frame } = {}
|
|
136
|
+
local innerEdges: { Frame } = {}
|
|
137
|
+
local dots: { Frame } = {}
|
|
138
|
+
|
|
139
|
+
function Lattice.mount(cell: Frame)
|
|
140
|
+
edges = Draw.pool(cell, MAX_EDGES, "Edge", false)
|
|
141
|
+
innerEdges = Draw.pool(cell, #SHAPES[INNER_SHAPE].edges, "Inner", false)
|
|
142
|
+
-- Built after the edges so the lit corners sit over the strokes they join.
|
|
143
|
+
dots = Draw.pool(cell, MAX_VERTICES, "Vertex", true)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
function Lattice.unmount()
|
|
147
|
+
edges = {}
|
|
148
|
+
innerEdges = {}
|
|
149
|
+
dots = {}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
function Lattice.paint(ctx: Theme.Context, _delta: number)
|
|
153
|
+
local shapeIndex = math.clamp(1 + math.floor(ctx.activity * (#SHAPES - 0.001)), 1, #SHAPES)
|
|
154
|
+
local shape = SHAPES[shapeIndex]
|
|
155
|
+
local radius = ctx.size * BASE_RADIUS * ctx.scale
|
|
156
|
+
|
|
157
|
+
-- Wobble the axis rather than the panel: shaking the strip would read as a
|
|
158
|
+
-- glitch, tilting the solid reads as it being knocked.
|
|
159
|
+
local wobble = if ctx.shake > 0
|
|
160
|
+
then Vector3.new(
|
|
161
|
+
math.sin(ctx.clock * 37) * ctx.shake * 0.12,
|
|
162
|
+
math.cos(ctx.clock * 29) * ctx.shake * 0.12,
|
|
163
|
+
0
|
|
164
|
+
)
|
|
165
|
+
else Vector3.zero
|
|
166
|
+
|
|
167
|
+
local projected: { Vector2 } = {}
|
|
168
|
+
local depths: { number } = {}
|
|
169
|
+
for index, vertex in shape.vertices do
|
|
170
|
+
local point, depth = Draw.project(
|
|
171
|
+
Draw.rotate(vertex, ctx.angle + wobble),
|
|
172
|
+
FOCAL,
|
|
173
|
+
radius,
|
|
174
|
+
ctx.centre
|
|
175
|
+
)
|
|
176
|
+
projected[index] = point
|
|
177
|
+
depths[index] = depth
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
for index = 1, MAX_EDGES do
|
|
181
|
+
local frame = edges[index]
|
|
182
|
+
if frame == nil then
|
|
183
|
+
continue
|
|
184
|
+
end
|
|
185
|
+
local edge = shape.edges[index]
|
|
186
|
+
if edge == nil then
|
|
187
|
+
frame.Visible = false
|
|
188
|
+
continue
|
|
189
|
+
end
|
|
190
|
+
local a, b = edge[1], edge[2]
|
|
191
|
+
local depth = math.clamp(((depths[a] + depths[b]) / 2 + 2) / 4, 0, 1)
|
|
192
|
+
Draw.edge(
|
|
193
|
+
frame,
|
|
194
|
+
projected[a],
|
|
195
|
+
projected[b],
|
|
196
|
+
math.max(1, math.floor(1 + depth * 1.6 + ctx.flare * 0.8)),
|
|
197
|
+
ctx.tint:Lerp(Color3.new(1, 1, 1), depth * 0.35 + ctx.flare * 0.3),
|
|
198
|
+
math.max(0, 0.84 - depth * 0.74 - ctx.flare * 0.3)
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
--[[
|
|
203
|
+
Vertices as points, brighter than the edges they join.
|
|
204
|
+
|
|
205
|
+
Wireframes read as flat when every stroke is the same weight. Lighting
|
|
206
|
+
the corners gives the eye something to track as the solid turns, which is
|
|
207
|
+
what makes it read as rotating rather than merely flickering.
|
|
208
|
+
]]
|
|
209
|
+
for index = 1, MAX_VERTICES do
|
|
210
|
+
local dot = dots[index]
|
|
211
|
+
if dot == nil then
|
|
212
|
+
continue
|
|
213
|
+
end
|
|
214
|
+
local point = projected[index]
|
|
215
|
+
if point == nil then
|
|
216
|
+
dot.Visible = false
|
|
217
|
+
continue
|
|
218
|
+
end
|
|
219
|
+
local depth = math.clamp((depths[index] + 2) / 4, 0, 1)
|
|
220
|
+
-- The corners carry the flare. A pulse of light travelling through the
|
|
221
|
+
-- vertices is what makes a completed call land as an event rather than
|
|
222
|
+
-- as a bar quietly appearing somewhere off to the right.
|
|
223
|
+
Draw.dot(
|
|
224
|
+
dot,
|
|
225
|
+
point,
|
|
226
|
+
math.max(2, math.floor(1.5 + depth * 2.5 + ctx.flare * 2.5)),
|
|
227
|
+
ctx.tint:Lerp(Color3.new(1, 1, 1), 0.25 + depth * 0.5),
|
|
228
|
+
math.max(0, 0.55 - depth * 0.5 - ctx.flare * 0.45)
|
|
229
|
+
)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
--[[
|
|
233
|
+
A second solid inside the first, turning the other way.
|
|
234
|
+
|
|
235
|
+
One rotating outline is a loading spinner. Two, at different rates and
|
|
236
|
+
opposite directions, read as a mechanism -- and the counter-rotation
|
|
237
|
+
makes the outer solid's direction obvious, which a lone wireframe hides
|
|
238
|
+
whenever it passes through a symmetrical angle.
|
|
239
|
+
]]
|
|
240
|
+
local inner = SHAPES[INNER_SHAPE]
|
|
241
|
+
local innerAngle = -ctx.angle * 1.6
|
|
242
|
+
local innerProjected: { Vector2 } = {}
|
|
243
|
+
local innerDepths: { number } = {}
|
|
244
|
+
for index, vertex in inner.vertices do
|
|
245
|
+
local point, depth = Draw.project(
|
|
246
|
+
Draw.rotate(vertex, innerAngle),
|
|
247
|
+
FOCAL,
|
|
248
|
+
radius * INNER_SCALE,
|
|
249
|
+
ctx.centre
|
|
250
|
+
)
|
|
251
|
+
innerProjected[index] = point
|
|
252
|
+
innerDepths[index] = depth
|
|
253
|
+
end
|
|
254
|
+
for index, frame in innerEdges do
|
|
255
|
+
local edge = inner.edges[index]
|
|
256
|
+
if edge == nil then
|
|
257
|
+
frame.Visible = false
|
|
258
|
+
continue
|
|
259
|
+
end
|
|
260
|
+
local a, b = edge[1], edge[2]
|
|
261
|
+
local depth = math.clamp(((innerDepths[a] + innerDepths[b]) / 2 + 2) / 4, 0, 1)
|
|
262
|
+
Draw.edge(
|
|
263
|
+
frame,
|
|
264
|
+
innerProjected[a],
|
|
265
|
+
innerProjected[b],
|
|
266
|
+
1,
|
|
267
|
+
ctx.tint:Lerp(Color3.new(1, 1, 1), 0.5),
|
|
268
|
+
0.9 - depth * 0.35
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
--[[
|
|
274
|
+
A still cube, which is the shape people recognise the console by, drawn at a
|
|
275
|
+
three-quarter angle so all three dimensions are visible at once.
|
|
276
|
+
]]
|
|
277
|
+
function Lattice.swatch(frame: Frame, palette: Theme.Palette)
|
|
278
|
+
local centre = Vector2.new(20, 12)
|
|
279
|
+
local pool = Draw.pool(frame, 12, "Edge", false)
|
|
280
|
+
local shape = SHAPES[3]
|
|
281
|
+
local angle = Vector3.new(0.5, 0.7, 0)
|
|
282
|
+
|
|
283
|
+
local projected: { Vector2 } = {}
|
|
284
|
+
local depths: { number } = {}
|
|
285
|
+
for index, vertex in shape.vertices do
|
|
286
|
+
local point, depth = Draw.project(Draw.rotate(vertex, angle), FOCAL, 5.5, centre)
|
|
287
|
+
projected[index] = point
|
|
288
|
+
depths[index] = depth
|
|
289
|
+
end
|
|
290
|
+
for index, edge in shape.edges do
|
|
291
|
+
local a, b = edge[1], edge[2]
|
|
292
|
+
local depth = math.clamp(((depths[a] + depths[b]) / 2 + 2) / 4, 0, 1)
|
|
293
|
+
Draw.edge(
|
|
294
|
+
pool[index],
|
|
295
|
+
projected[a],
|
|
296
|
+
projected[b],
|
|
297
|
+
1,
|
|
298
|
+
palette.violet:Lerp(Color3.new(1, 1, 1), depth * 0.4),
|
|
299
|
+
0.75 - depth * 0.6
|
|
300
|
+
)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
return Lattice
|