@el4cteo/rbx-studio-mcp 0.3.1 → 0.3.6
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/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/input.js +38 -5
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +9 -8
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +10 -1
- package/dist/tools/scripts.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +301 -117
- package/plugin/src/Mirror.luau +126 -0
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +161 -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 +266 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Undo.luau +29 -0
- package/plugin/src/Visuals.luau +838 -907
- package/plugin/src/handlers/Device.luau +21 -1
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Input.luau +583 -493
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/init.server.luau +209 -11
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Aurora -- load as an amplitude.
|
|
4
|
+
|
|
5
|
+
A curtain of vertical rays whose height follows a travelling wave. The wave
|
|
6
|
+
is always there; what the session changes is how violent it is. Idle, it is
|
|
7
|
+
a shallow ripple you have to look at to notice. Under load it becomes a
|
|
8
|
+
curtain that fills the cell. Nothing here counts or ranks anything, which is
|
|
9
|
+
the point: it answers "how hard is this working" continuously, where Lattice
|
|
10
|
+
answers it in four steps and Orbit in six.
|
|
11
|
+
|
|
12
|
+
Two waves at different rates are summed rather than one, because a single
|
|
13
|
+
sine is a loading bar lying on its side -- the interference is what makes it
|
|
14
|
+
read as something physical rather than as an animation on a loop.
|
|
15
|
+
|
|
16
|
+
Colour runs green at the base to violet at the tips, which is the actual
|
|
17
|
+
emission order of a real aurora and, more usefully here, means the height of
|
|
18
|
+
a ray is legible from its colour alone at four pixels wide.
|
|
19
|
+
]]
|
|
20
|
+
|
|
21
|
+
local Draw = require(script.Parent.Draw)
|
|
22
|
+
local Theme = require(script.Parent.Theme)
|
|
23
|
+
|
|
24
|
+
local RAYS = 15
|
|
25
|
+
|
|
26
|
+
local Aurora = {}
|
|
27
|
+
|
|
28
|
+
Aurora.id = "aurora"
|
|
29
|
+
Aurora.label = "Aurora"
|
|
30
|
+
Aurora.blurb = "Rising curtain"
|
|
31
|
+
|
|
32
|
+
Aurora.palette = {
|
|
33
|
+
background = Color3.fromRGB(9, 14, 28),
|
|
34
|
+
surface = Color3.fromRGB(16, 23, 44),
|
|
35
|
+
rule = Color3.fromRGB(108, 226, 178),
|
|
36
|
+
text = Color3.fromRGB(206, 220, 238),
|
|
37
|
+
dim = Color3.fromRGB(86, 100, 130),
|
|
38
|
+
green = Color3.fromRGB(108, 232, 172),
|
|
39
|
+
red = Color3.fromRGB(246, 126, 146),
|
|
40
|
+
amber = Color3.fromRGB(244, 196, 128),
|
|
41
|
+
cyan = Color3.fromRGB(114, 214, 236),
|
|
42
|
+
violet = Color3.fromRGB(162, 146, 240),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
local rays: { Frame } = {}
|
|
46
|
+
|
|
47
|
+
--[[
|
|
48
|
+
Accumulated wave phase, rather than wall time times a rate.
|
|
49
|
+
|
|
50
|
+
`ctx.clock * 1.6 * pace` looks harmless and is not: `pace` drops to 0.35 the
|
|
51
|
+
moment the session goes quiet, and multiplying a clock in the thousands by a
|
|
52
|
+
rate that changes RESHAPES the whole curtain in one frame -- the same class
|
|
53
|
+
of fault as Void's disc, just rarer, because this rate only moves twice per
|
|
54
|
+
idle cycle instead of every frame. Integrating it means going quiet slows the
|
|
55
|
+
curtain, which is what was meant.
|
|
56
|
+
]]
|
|
57
|
+
local phase = 0
|
|
58
|
+
|
|
59
|
+
function Aurora.mount(cell: Frame)
|
|
60
|
+
rays = {}
|
|
61
|
+
for index = 1, RAYS do
|
|
62
|
+
local ray = Instance.new("Frame")
|
|
63
|
+
ray.Name = string.format("Ray%02d", index)
|
|
64
|
+
-- Anchored at the bottom so height grows upward from a fixed baseline,
|
|
65
|
+
-- which is what makes the curtain rise rather than expand about its
|
|
66
|
+
-- middle.
|
|
67
|
+
ray.AnchorPoint = Vector2.new(0.5, 1)
|
|
68
|
+
ray.BorderSizePixel = 0
|
|
69
|
+
ray.Parent = cell
|
|
70
|
+
local corner = Instance.new("UICorner")
|
|
71
|
+
corner.CornerRadius = UDim.new(0, 1)
|
|
72
|
+
corner.Parent = ray
|
|
73
|
+
rays[index] = ray
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
function Aurora.unmount()
|
|
78
|
+
phase = 0
|
|
79
|
+
rays = {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
function Aurora.paint(ctx: Theme.Context, delta: number)
|
|
83
|
+
local width = ctx.size
|
|
84
|
+
local spacing = width / (RAYS + 1)
|
|
85
|
+
|
|
86
|
+
--[[
|
|
87
|
+
The floor is what keeps an idle panel alive.
|
|
88
|
+
|
|
89
|
+
Amplitude driven purely by activity would flatten the curtain to nothing
|
|
90
|
+
between commands, and a flat line is indistinguishable from a plugin
|
|
91
|
+
that has crashed. A tenth of the range is always moving.
|
|
92
|
+
]]
|
|
93
|
+
local amplitude = 0.1 + math.clamp(ctx.activity, 0, 1) * 0.62 + ctx.energy * 0.16
|
|
94
|
+
local pace = if ctx.quiet > 20 then 0.35 else 1
|
|
95
|
+
phase += delta * pace
|
|
96
|
+
|
|
97
|
+
for index = 1, RAYS do
|
|
98
|
+
local ray = rays[index]
|
|
99
|
+
if ray == nil then
|
|
100
|
+
continue
|
|
101
|
+
end
|
|
102
|
+
local across = index / RAYS
|
|
103
|
+
|
|
104
|
+
-- Two travelling waves, incommensurate rates. Their beat is slow enough
|
|
105
|
+
-- that the curtain visibly reshapes over several seconds instead of
|
|
106
|
+
-- repeating on a cycle the eye can learn.
|
|
107
|
+
local wave = math.sin(across * 5.2 - phase * 1.6)
|
|
108
|
+
+ 0.6 * math.sin(across * 2.7 + phase * 0.9)
|
|
109
|
+
local normalised = (wave + 1.6) / 3.2
|
|
110
|
+
|
|
111
|
+
local height = math.max(2, math.floor(width * (0.12 + normalised * amplitude) * ctx.scale))
|
|
112
|
+
local reach = math.clamp(height / width, 0, 1)
|
|
113
|
+
|
|
114
|
+
ray.Size = UDim2.fromOffset(3, height)
|
|
115
|
+
ray.Position = UDim2.fromOffset(spacing * index, width - 2)
|
|
116
|
+
-- Green low, violet high: the ray's own height is readable from its
|
|
117
|
+
-- colour, which a three-pixel column badly needs.
|
|
118
|
+
ray.BackgroundColor3 = ctx.palette.green
|
|
119
|
+
:Lerp(ctx.palette.violet, reach)
|
|
120
|
+
:Lerp(Color3.new(1, 1, 1), ctx.flare * 0.45)
|
|
121
|
+
ray.BackgroundTransparency = math.clamp(0.74 - reach * 0.45 - ctx.flare * 0.3, 0, 1)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
--[[
|
|
126
|
+
The trace as a continuation of the curtain: same rays, same colour rule, but
|
|
127
|
+
their heights are history rather than a wave.
|
|
128
|
+
]]
|
|
129
|
+
function Aurora.paintSlot(slot: Theme.Slot, ctx: Theme.Context)
|
|
130
|
+
local frame = slot.frame
|
|
131
|
+
local recency = 1 - slot.age
|
|
132
|
+
local height = math.max(2, math.floor(2 + slot.weight * 21))
|
|
133
|
+
|
|
134
|
+
frame.AnchorPoint = Vector2.new(0.5, 1)
|
|
135
|
+
frame.Size = UDim2.fromOffset(3, height)
|
|
136
|
+
frame.Position = UDim2.new(frame.Position.X.Scale, frame.Position.X.Offset, 1, 0)
|
|
137
|
+
frame.BackgroundColor3 = if slot.ok
|
|
138
|
+
then ctx.palette.green:Lerp(ctx.palette.violet, slot.weight)
|
|
139
|
+
else ctx.palette.red
|
|
140
|
+
frame.BackgroundTransparency = math.clamp(0.66 - recency * 0.56, 0, 1)
|
|
141
|
+
frame.Rotation = 0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
function Aurora.swatch(frame: Frame, palette: Theme.Palette)
|
|
145
|
+
for index = 1, 12 do
|
|
146
|
+
local ray = Instance.new("Frame")
|
|
147
|
+
ray.AnchorPoint = Vector2.new(0.5, 1)
|
|
148
|
+
ray.BorderSizePixel = 0
|
|
149
|
+
local across = index / 12
|
|
150
|
+
local wave = math.sin(across * 5.2) + 0.6 * math.sin(across * 2.7)
|
|
151
|
+
local reach = (wave + 1.6) / 3.2
|
|
152
|
+
local height = math.max(2, math.floor(4 + reach * 17))
|
|
153
|
+
ray.Size = UDim2.fromOffset(2, height)
|
|
154
|
+
ray.Position = UDim2.fromOffset(3 + index * 3, 23)
|
|
155
|
+
ray.BackgroundColor3 = palette.green:Lerp(palette.violet, reach)
|
|
156
|
+
ray.BackgroundTransparency = 0.35
|
|
157
|
+
ray.Parent = frame
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return Aurora
|
|
@@ -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
|