Skip to content

Commit f22fd2a

Browse files
selectgenderJang
andauthored
feat: Arc Part Command (#9)
* feat: created unfinished "Arc Part" command * feat: arcs.luau ROUGH SKETCH FINISH * feat: arcs.luau finished but not battle tested * fix: forgot to return `current` in arcs.luau instead of `instances` --------- Co-authored-by: Jang <ivy@Jangs-MacBook-Air.local>
1 parent 4ee120f commit f22fd2a

1 file changed

Lines changed: 319 additions & 0 deletions

File tree

src/extensions/geometry/arcs.luau

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
--[[
2+
arcs.luau
3+
4+
Part of the `geometry` extension. Creates smooth arcs comprised of a selected part.
5+
6+
Credit to the Archimedes plugin for the original concept and inspiration of implementation: https://devforum.roblox.com/t/introducing-archimedes-3-a-building-plugin/1610366
7+
8+
Author: Sylvia
9+
- Roblox Profile: https://www.roblox.com/users/3814464357/profile
10+
- Github Profile: https://github.com/selectgender
11+
- Discord Username: selectgender
12+
]]
13+
14+
local ChangeHistoryService = game:GetService("ChangeHistoryService")
15+
local UserInputService = game:GetService("UserInputService")
16+
17+
local Extension = require("@src/Extension")
18+
local Gizmos = require("@src/Gizmos")
19+
local ext = require(".")
20+
21+
local function BoundingBoxGizmo(cframe: CFrame, size: Vector3)
22+
local topFrontRightCorner = cframe * Vector3.new(size.X / 2, size.Y / 2, -size.Z / 2)
23+
local topFrontLeftCorner = cframe * Vector3.new(-size.X / 2, size.Y / 2, -size.Z / 2)
24+
local topBackLeftCorner = cframe * Vector3.new(-size.X / 2, size.Y / 2, size.Z / 2)
25+
local topBackRightCorner = cframe * Vector3.new(size.X / 2, size.Y / 2, size.Z / 2)
26+
local bottomFrontRightCorner = cframe * Vector3.new(size.X / 2, -size.Y / 2, -size.Z / 2)
27+
local bottomFrontLeftCorner = cframe * Vector3.new(-size.X / 2, -size.Y / 2, -size.Z / 2)
28+
local bottomBackLeftCorner = cframe * Vector3.new(-size.X / 2, -size.Y / 2, size.Z / 2)
29+
local bottomBackRightCorner = cframe * Vector3.new(size.X / 2, -size.Y / 2, size.Z / 2)
30+
31+
-- Top surface
32+
Gizmos.drawLine(topFrontRightCorner, topFrontLeftCorner)
33+
Gizmos.drawLine(topFrontLeftCorner, topBackLeftCorner)
34+
Gizmos.drawLine(topBackLeftCorner, topBackRightCorner)
35+
Gizmos.drawLine(topBackRightCorner, topFrontRightCorner)
36+
37+
-- Side edges
38+
Gizmos.drawLine(topFrontRightCorner, bottomFrontRightCorner)
39+
Gizmos.drawLine(topFrontLeftCorner, bottomFrontLeftCorner)
40+
Gizmos.drawLine(topBackLeftCorner, bottomBackLeftCorner)
41+
Gizmos.drawLine(topBackRightCorner, bottomBackRightCorner)
42+
43+
-- Buttom surface
44+
Gizmos.drawLine(bottomFrontRightCorner, bottomFrontLeftCorner)
45+
Gizmos.drawLine(bottomFrontLeftCorner, bottomBackLeftCorner)
46+
Gizmos.drawLine(bottomBackLeftCorner, bottomBackRightCorner)
47+
Gizmos.drawLine(bottomBackRightCorner, bottomFrontRightCorner)
48+
end
49+
50+
local function SurfaceSelectionGizmo(part: BasePart, surface: Enum.NormalId)
51+
local cframe = part.CFrame
52+
local size = part.Size
53+
54+
local topFrontRightCorner = cframe * Vector3.new(size.X / 2, size.Y / 2, -size.Z / 2)
55+
local topFrontLeftCorner = cframe * Vector3.new(-size.X / 2, size.Y / 2, -size.Z / 2)
56+
local topBackLeftCorner = cframe * Vector3.new(-size.X / 2, size.Y / 2, size.Z / 2)
57+
local topBackRightCorner = cframe * Vector3.new(size.X / 2, size.Y / 2, size.Z / 2)
58+
local bottomFrontRightCorner = cframe * Vector3.new(size.X / 2, -size.Y / 2, -size.Z / 2)
59+
local bottomFrontLeftCorner = cframe * Vector3.new(-size.X / 2, -size.Y / 2, -size.Z / 2)
60+
local bottomBackLeftCorner = cframe * Vector3.new(-size.X / 2, -size.Y / 2, size.Z / 2)
61+
local bottomBackRightCorner = cframe * Vector3.new(size.X / 2, -size.Y / 2, size.Z / 2)
62+
63+
if surface == Enum.NormalId.Top then
64+
Gizmos.drawLine(topFrontRightCorner, topFrontLeftCorner)
65+
Gizmos.drawLine(topFrontLeftCorner, topBackLeftCorner)
66+
Gizmos.drawLine(topBackLeftCorner, topBackRightCorner)
67+
Gizmos.drawLine(topBackRightCorner, topFrontRightCorner)
68+
elseif surface == Enum.NormalId.Bottom then
69+
Gizmos.drawLine(bottomFrontRightCorner, bottomFrontLeftCorner)
70+
Gizmos.drawLine(bottomFrontLeftCorner, bottomBackLeftCorner)
71+
Gizmos.drawLine(bottomBackLeftCorner, bottomBackRightCorner)
72+
Gizmos.drawLine(bottomBackRightCorner, bottomFrontRightCorner)
73+
elseif surface == Enum.NormalId.Left then
74+
Gizmos.drawLine(topFrontLeftCorner, topBackLeftCorner)
75+
Gizmos.drawLine(topBackLeftCorner, bottomBackLeftCorner)
76+
Gizmos.drawLine(bottomBackLeftCorner, bottomFrontLeftCorner)
77+
Gizmos.drawLine(bottomFrontLeftCorner, topFrontLeftCorner)
78+
elseif surface == Enum.NormalId.Right then
79+
Gizmos.drawLine(topFrontRightCorner, topBackRightCorner)
80+
Gizmos.drawLine(topBackRightCorner, bottomBackRightCorner)
81+
Gizmos.drawLine(bottomBackRightCorner, bottomFrontRightCorner)
82+
Gizmos.drawLine(bottomFrontRightCorner, topFrontRightCorner)
83+
elseif surface == Enum.NormalId.Front then
84+
Gizmos.drawLine(topFrontLeftCorner, topFrontRightCorner)
85+
Gizmos.drawLine(topFrontRightCorner, bottomFrontRightCorner)
86+
Gizmos.drawLine(bottomFrontRightCorner, bottomFrontLeftCorner)
87+
Gizmos.drawLine(bottomFrontLeftCorner, topFrontLeftCorner)
88+
elseif surface == Enum.NormalId.Back then
89+
Gizmos.drawLine(topBackLeftCorner, topBackRightCorner)
90+
Gizmos.drawLine(topBackRightCorner, bottomBackRightCorner)
91+
Gizmos.drawLine(bottomBackRightCorner, bottomBackLeftCorner)
92+
Gizmos.drawLine(bottomBackLeftCorner, topBackLeftCorner)
93+
end
94+
end
95+
96+
-- fattest function ive ever made. im not sure of a way to simplify all the if statements... so if youre reading this right now try to take a crack at the problem and maybe write up a quick pull request!
97+
local function getNextArcCFrame(
98+
angle: number,
99+
rotationType: string,
100+
face: Enum.NormalId,
101+
cframe: CFrame,
102+
size: Vector3
103+
): CFrame
104+
local offset: CFrame
105+
local pivot_offset: CFrame
106+
local angle_cframe: CFrame
107+
108+
if face == Enum.NormalId.Top then
109+
offset = CFrame.new(0, size.Y, 0)
110+
111+
if rotationType == "Pitch" then
112+
pivot_offset = CFrame.new(0, -size.Y / 2, math.sign(angle) * -size.Z / 2)
113+
angle_cframe = CFrame.Angles(angle, 0, 0)
114+
elseif rotationType == "Roll" then
115+
pivot_offset = CFrame.new(math.sign(angle) * size.X / 2, -size.Y / 2, 0)
116+
angle_cframe = CFrame.Angles(0, 0, angle)
117+
end
118+
elseif face == Enum.NormalId.Bottom then
119+
offset = CFrame.new(0, -size.Y, 0)
120+
121+
if rotationType == "Pitch" then
122+
pivot_offset = CFrame.new(0, size.Y / 2, math.sign(angle) * size.Z / 2)
123+
angle_cframe = CFrame.Angles(angle, 0, 0)
124+
elseif rotationType == "Roll" then
125+
pivot_offset = CFrame.new(math.sign(angle) * -size.X / 2, size.Y / 2, 0)
126+
angle_cframe = CFrame.Angles(0, 0, angle)
127+
end
128+
elseif face == Enum.NormalId.Left then
129+
offset = CFrame.new(-size.X, 0, 0)
130+
131+
if rotationType == "Pitch" then
132+
pivot_offset = CFrame.new(size.X / 2, math.sign(angle) * size.Y / 2, 0)
133+
angle_cframe = CFrame.Angles(0, 0, angle)
134+
elseif rotationType == "Yaw" then
135+
pivot_offset = CFrame.new(size.X / 2, 0, math.sign(angle) * -size.Z / 2)
136+
angle_cframe = CFrame.Angles(0, angle, 0)
137+
end
138+
elseif face == Enum.NormalId.Right then
139+
offset = CFrame.new(size.X, 0, 0)
140+
141+
if rotationType == "Pitch" then
142+
pivot_offset = CFrame.new(-size.X / 2, math.sign(angle) * -size.Y / 2, 0)
143+
angle_cframe = CFrame.Angles(0, 0, angle)
144+
elseif rotationType == "Yaw" then
145+
pivot_offset = CFrame.new(-size.X / 2, 0, math.sign(angle) * size.Z / 2)
146+
angle_cframe = CFrame.Angles(0, angle, 0)
147+
end
148+
elseif face == Enum.NormalId.Front then
149+
offset = CFrame.new(0, 0, -size.Z)
150+
151+
if rotationType == "Pitch" then
152+
pivot_offset = CFrame.new(0, math.sign(angle) * -size.Y / 2, size.Z / 2)
153+
angle_cframe = CFrame.Angles(angle, 0, 0)
154+
elseif rotationType == "Yaw" then
155+
pivot_offset = CFrame.new(math.sign(angle) * size.X / 2, 0, size.Z / 2)
156+
angle_cframe = CFrame.Angles(0, angle, 0)
157+
end
158+
elseif face == Enum.NormalId.Back then
159+
offset = CFrame.new(0, 0, size.Z)
160+
161+
if rotationType == "Pitch" then
162+
pivot_offset = CFrame.new(0, math.sign(angle) * size.Y / 2, -size.Z / 2)
163+
angle_cframe = CFrame.Angles(angle, 0, 0)
164+
elseif rotationType == "Yaw" then
165+
pivot_offset = CFrame.new(math.sign(angle) * -size.X / 2, 0, -size.Z / 2)
166+
angle_cframe = CFrame.Angles(0, angle, 0)
167+
end
168+
end
169+
170+
return cframe * offset * pivot_offset * angle_cframe * pivot_offset:Inverse()
171+
end
172+
173+
local function renderArcs(
174+
instances: { Instance },
175+
amount: number,
176+
angle: number,
177+
rotationType: string,
178+
face: Enum.NormalId
179+
)
180+
local current = instances
181+
182+
for _ = 1, amount do
183+
for index, instance in current do
184+
local isAPart = instance:IsA("BasePart")
185+
186+
if not (isAPart or instance:IsA("Model")) then
187+
continue
188+
end
189+
190+
local clone = instance:Clone()
191+
192+
if isAPart then
193+
clone = clone :: BasePart
194+
clone.CFrame = getNextArcCFrame(angle, rotationType, face, (instance :: BasePart).CFrame, clone.Size)
195+
else
196+
clone = clone :: Model
197+
clone:PivotTo(
198+
getNextArcCFrame(angle, rotationType, face, (instance :: Model):GetPivot(), clone:GetExtentsSize())
199+
)
200+
end
201+
202+
clone.Parent = instance.Parent
203+
current[index] = clone
204+
end
205+
end
206+
207+
return current
208+
end
209+
210+
ext:newCommand({
211+
id = "arc-part",
212+
title = "Arc Part",
213+
description = "Creates a smooth arc of selected part.",
214+
215+
run = function(ctx: Extension.CommandContext)
216+
ctx:recordChanges()
217+
end,
218+
219+
renderInViewport = function(ctx: Extension.CommandContext)
220+
local Iris = ctx.iris
221+
local windowPositionState = Iris.State(UserInputService:GetMouseLocation())
222+
local windowSizeState = Iris.State(Vector2.new(275, 250))
223+
local window = Iris.Window({ "Arc Settings" }, { position = windowPositionState, size = windowSizeState })
224+
225+
if window.closed() then
226+
ctx:cleanup()
227+
-- @TEMPORARY: for some reason the window would not reopen after it was closed for a first time.
228+
window.state.isOpened:set(true)
229+
Iris.End()
230+
231+
return
232+
end
233+
234+
local mouse = ctx.mouse
235+
local windowPosition = windowPositionState:get()
236+
local windowSize = windowSizeState:get()
237+
local mouseIsInWindow = mouse.X > windowPosition.X
238+
and mouse.X < windowPosition.X + windowSize.X
239+
and mouse.Y > windowPosition.Y
240+
and mouse.Y < windowPosition.Y + windowSize.Y
241+
local mouseDown = UserInputService:IsMouseButtonPressed(Enum.UserInputType.MouseButton1)
242+
local selectedFaceState = Iris.State(Enum.NormalId.Front)
243+
244+
if not mouseIsInWindow and mouse.Target and mouse.TargetSurface then
245+
SurfaceSelectionGizmo(mouse.Target, mouse.TargetSurface)
246+
247+
if mouseDown then
248+
selectedFaceState:set(mouse.TargetSurface)
249+
end
250+
end
251+
252+
local selectedFace = selectedFaceState:get()
253+
Iris.Text(`Face: {selectedFace}`)
254+
255+
local rotationTypeState = Iris.State("Pitch")
256+
local rotationType = rotationTypeState:get()
257+
local secondRotationType = if selectedFace == Enum.NormalId.Top or selectedFace == Enum.NormalId.Bottom
258+
then "Roll"
259+
else "Yaw"
260+
261+
if rotationType ~= "Pitch" and rotationType ~= secondRotationType then
262+
rotationType = secondRotationType
263+
rotationTypeState:set(secondRotationType)
264+
end
265+
266+
Iris.SameLine()
267+
Iris.RadioButton({ "Pitch", "Pitch" }, { index = rotationTypeState })
268+
Iris.RadioButton({ secondRotationType, secondRotationType }, { index = rotationTypeState })
269+
Iris.End()
270+
271+
local angleState = Iris.State(0)
272+
local angleDegrees = angleState:get()
273+
local angleRadians = math.rad(angleDegrees)
274+
Iris.SliderNum({ "Angle", 1, -180, 180 }, { number = angleState })
275+
276+
local selections = ctx:getSelection()
277+
for _, instance in selections do
278+
if instance:IsA("BasePart") then
279+
local size = instance.Size
280+
BoundingBoxGizmo(
281+
getNextArcCFrame(angleRadians, rotationType, selectedFace, instance.CFrame, size),
282+
size
283+
)
284+
elseif instance:IsA("Model") then
285+
local size = instance:GetExtentsSize()
286+
BoundingBoxGizmo(
287+
getNextArcCFrame(angleRadians, rotationType, selectedFace, instance:GetPivot(), size),
288+
size
289+
)
290+
end
291+
end
292+
293+
local amountState = Iris.State(1)
294+
Iris.InputNum({ "Amount", 1, 1 }, { number = amountState })
295+
296+
local renderAllCount = if angleDegrees == 0 then 1 else 360 // math.abs(angleDegrees) - 1
297+
if renderAllCount < amountState:get() then
298+
amountState:set(renderAllCount)
299+
end
300+
301+
Iris.SameLine()
302+
303+
if Iris.Button(`Render ({amountState:get()})`).clicked() then
304+
ctx:setSelection(
305+
renderArcs(ctx:getSelection(), amountState:get(), angleRadians, rotationType, selectedFace)
306+
)
307+
ChangeHistoryService:SetWaypoint("Rocket Arc Part Render Amount")
308+
end
309+
310+
if Iris.Button(`Render All ({renderAllCount})`).clicked() then
311+
ctx:setSelection(renderArcs(ctx:getSelection(), renderAllCount, angleRadians, rotationType, selectedFace))
312+
ChangeHistoryService:SetWaypoint("Rocket Arc Part Render All")
313+
end
314+
315+
Iris.End()
316+
317+
Iris.End()
318+
end,
319+
})

0 commit comments

Comments
 (0)