├── LICENSE ├── README.md └── gizmo.lua /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brad Sharp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About 2 | 3 | _gizmo_ is a visual debug library designed for the Roblox game engine. 4 | 5 | # Usage 6 | 7 | ## API Reference 8 | 9 | ### Types 10 | 11 | The following types are supported: 12 | - point: Position 13 | - box: Orientation, Size 14 | - wirebox: Orientation, Size 15 | - sphere: Position, Radius 16 | - wiresphere: Position, Radius 17 | - line: Position, Position 18 | - arrow: Position, Position 19 | - ray: Position, Direction 20 | 21 | ### gizmo.\.draw(...) -> void 22 | 23 | Renders the gizmo for a single frame 24 | 25 | ### gizmo.\.create(...) -> object 26 | 27 | Creates a new object which can be rendered over multiple frames 28 | 29 | #### object:enable() -> void 30 | 31 | Starts rendering the gizmo 32 | 33 | #### object:disable() -> void 34 | 35 | Stops rendering the gizmo 36 | 37 | #### object:update(...) -> void 38 | 39 | Updates the gizmos appearance 40 | 41 | #### object.style 42 | 43 | Controls the 'style' of the gizmo 44 | 45 | ### gizmo.style 46 | 47 | The global style used by default when creating or drawing a gizmo 48 | 49 | ## Examples 50 | 51 | ### Drawing an array frame-by-frame 52 | 53 | ```lua 54 | RunService.PostSimulation:Connect(function () 55 | gizmo.arrow.draw(start, finish) 56 | end) 57 | ``` 58 | 59 | ### Drawing an array with an object 60 | 61 | 62 | ```lua 63 | local arrow = gizmo.arrow.create(start, finish) 64 | arrow:enable() 65 | 66 | RunService.PostSimulation:Connect(function () 67 | arrow:update(start, finish) 68 | end) 69 | ``` 70 | -------------------------------------------------------------------------------- /gizmo.lua: -------------------------------------------------------------------------------- 1 | --!nocheck 2 | -- Name: gizmo.lua 3 | -- Version: 2.0 (02/07/23) 4 | -- Author: Brad Sharp 5 | -- 6 | -- Repository: https://github.com/BradSharp/Roblox-Miscellaneous/tree/master/Gizmo 7 | -- License: MIT 8 | -- 9 | -- Copyright (c) 2021-2023 Brad Sharp 10 | ------------------------------------------------------------------------------------------------------------------------ 11 | 12 | local GLOBAL_ATTRIBUTE = "EnableGizmos" 13 | 14 | local DEFAULT_SCALE = 0.1 15 | local DEFAULT_COLOR = Color3.fromRGB(255, 255, 0) 16 | 17 | local RunService = game:GetService("RunService") 18 | 19 | ------------------------------------------------------------------------------------------------------------------------ 20 | -- Type Definitions 21 | ------------------------------------------------------------------------------------------------------------------------ 22 | 23 | type Style = { 24 | color: Color3, 25 | layer: number, 26 | transparency: number, 27 | scale: number, 28 | } 29 | 30 | type Gizmo = { 31 | __properties: {[number]: any, n: number}, 32 | style: Style 33 | } 34 | 35 | ------------------------------------------------------------------------------------------------------------------------ 36 | -- Internal Variables 37 | ------------------------------------------------------------------------------------------------------------------------ 38 | 39 | local moduleId = script:GetFullName() 40 | local active = false 41 | 42 | local scheduledObjects = {} 43 | local renderQueue = {} 44 | local instanceCache = {} 45 | local container = Instance.new("Folder", workspace) 46 | 47 | container.Name = "Gizmos" 48 | container.Archivable = false 49 | 50 | local globalStyle: Style = { 51 | color = DEFAULT_COLOR, 52 | layer = 1, 53 | transparency = 0, 54 | scale = DEFAULT_SCALE 55 | } 56 | 57 | ------------------------------------------------------------------------------------------------------------------------ 58 | -- Update Instance Visibility 59 | ------------------------------------------------------------------------------------------------------------------------ 60 | 61 | local function show(instance) 62 | if instance:IsA("PVAdornment") then 63 | instance.Visible = true 64 | else 65 | instance.Enabled = true 66 | end 67 | end 68 | 69 | local function hide(instance) 70 | if instance:IsA("PVAdornment") then 71 | instance.Visible = false 72 | else 73 | instance.Enabled = false 74 | end 75 | end 76 | 77 | ------------------------------------------------------------------------------------------------------------------------ 78 | -- Instance Caching 79 | ------------------------------------------------------------------------------------------------------------------------ 80 | 81 | local function get(class) 82 | local classCache = instanceCache[class] 83 | if not classCache then 84 | classCache = {} 85 | instanceCache[class] = classCache 86 | end 87 | local instance = table.remove(classCache) 88 | if not instance then 89 | instance = Instance.new(class, container) 90 | hide(instance) 91 | end 92 | return instance 93 | end 94 | 95 | local function release(instance) 96 | local class = instance.ClassName 97 | local classCache = instanceCache[class] 98 | if not classCache then 99 | classCache = {} 100 | instanceCache[class] = classCache 101 | end 102 | hide(instance) 103 | table.insert(classCache, instance) 104 | end 105 | 106 | ------------------------------------------------------------------------------------------------------------------------ 107 | -- Style Instances 108 | ------------------------------------------------------------------------------------------------------------------------ 109 | 110 | local function applyStyleToAdornment(style : Style, adornment) 111 | adornment.Color3 = style.color 112 | adornment.Transparency = style.transparency 113 | adornment.ZIndex = style.layer 114 | adornment.Adornee = workspace 115 | adornment.AlwaysOnTop = true 116 | end 117 | 118 | local function applyStyleToHighlight(style : Style, highlight) 119 | highlight.FillColor = style.color 120 | highlight.OutlineColor = style.color 121 | highlight.DepthMode = Enum.HighlightDepthMode.AlwaysOnTop 122 | end 123 | 124 | ------------------------------------------------------------------------------------------------------------------------ 125 | -- Render Instances 126 | ------------------------------------------------------------------------------------------------------------------------ 127 | 128 | local function renderPoint(style, position: Vector3) 129 | local adornment = get("SphereHandleAdornment") 130 | adornment.Radius = style.scale * 0.5 131 | adornment.CFrame = CFrame.new(position) 132 | applyStyleToAdornment(style, adornment) 133 | table.insert(renderQueue, adornment) 134 | end 135 | 136 | local function renderBox(style, orientation: CFrame, size: Vector3) 137 | local adornment = get("BoxHandleAdornment") 138 | adornment.Size = size 139 | adornment.CFrame = orientation 140 | applyStyleToAdornment(style, adornment) 141 | table.insert(renderQueue, adornment) 142 | end 143 | 144 | -- If anyone has a better way to do this which is just as performant please let me know 145 | local function renderWireBox(style, orientation: CFrame, size: Vector3) 146 | local x, y, z = size.X / 2, size.Y / 2, size.Z / 2 147 | local lineWidth = style.scale 148 | local sizeX = Vector3.new(size.X + lineWidth, lineWidth, lineWidth) 149 | local sizeY = Vector3.new(lineWidth, size.Y + lineWidth, lineWidth) 150 | local sizeZ = Vector3.new(lineWidth, lineWidth, size.Z + lineWidth) 151 | local relativeOrientation = orientation 152 | local adornmentX1 = get("BoxHandleAdornment") 153 | local adornmentX2 = get("BoxHandleAdornment") 154 | local adornmentX3 = get("BoxHandleAdornment") 155 | local adornmentX4 = get("BoxHandleAdornment") 156 | local adornmentY1 = get("BoxHandleAdornment") 157 | local adornmentY2 = get("BoxHandleAdornment") 158 | local adornmentY3 = get("BoxHandleAdornment") 159 | local adornmentY4 = get("BoxHandleAdornment") 160 | local adornmentZ1 = get("BoxHandleAdornment") 161 | local adornmentZ2 = get("BoxHandleAdornment") 162 | local adornmentZ3 = get("BoxHandleAdornment") 163 | local adornmentZ4 = get("BoxHandleAdornment") 164 | adornmentX1.Size = sizeX 165 | adornmentX1.CFrame = relativeOrientation * CFrame.new(0, y, z) 166 | adornmentX2.Size = sizeX 167 | adornmentX2.CFrame = relativeOrientation * CFrame.new(0, -y, z) 168 | adornmentX3.Size = sizeX 169 | adornmentX3.CFrame = relativeOrientation * CFrame.new(0, y, -z) 170 | adornmentX4.Size = sizeX 171 | adornmentX4.CFrame = relativeOrientation * CFrame.new(0, -y, -z) 172 | applyStyleToAdornment(style, adornmentX1) 173 | applyStyleToAdornment(style, adornmentX2) 174 | applyStyleToAdornment(style, adornmentX3) 175 | applyStyleToAdornment(style, adornmentX4) 176 | table.insert(renderQueue, adornmentX1) 177 | table.insert(renderQueue, adornmentX2) 178 | table.insert(renderQueue, adornmentX3) 179 | table.insert(renderQueue, adornmentX4) 180 | adornmentY1.Size = sizeY 181 | adornmentY1.CFrame = relativeOrientation * CFrame.new(x, 0, z) 182 | adornmentY2.Size = sizeY 183 | adornmentY2.CFrame = relativeOrientation * CFrame.new(-x, 0, z) 184 | adornmentY3.Size = sizeY 185 | adornmentY3.CFrame = relativeOrientation * CFrame.new(x, 0, -z) 186 | adornmentY4.Size = sizeY 187 | adornmentY4.CFrame = relativeOrientation * CFrame.new(-x, 0, -z) 188 | applyStyleToAdornment(style, adornmentY1) 189 | applyStyleToAdornment(style, adornmentY2) 190 | applyStyleToAdornment(style, adornmentY3) 191 | applyStyleToAdornment(style, adornmentY4) 192 | table.insert(renderQueue, adornmentY1) 193 | table.insert(renderQueue, adornmentY2) 194 | table.insert(renderQueue, adornmentY3) 195 | table.insert(renderQueue, adornmentY4) 196 | adornmentZ1.Size = sizeZ 197 | adornmentZ1.CFrame = relativeOrientation * CFrame.new(x, y, 0) 198 | adornmentZ2.Size = sizeZ 199 | adornmentZ2.CFrame = relativeOrientation * CFrame.new(-x, y, 0) 200 | adornmentZ3.Size = sizeZ 201 | adornmentZ3.CFrame = relativeOrientation * CFrame.new(x, -y, 0) 202 | adornmentZ4.Size = sizeZ 203 | adornmentZ4.CFrame = relativeOrientation * CFrame.new(-x, -y, 0) 204 | applyStyleToAdornment(style, adornmentZ1) 205 | applyStyleToAdornment(style, adornmentZ2) 206 | applyStyleToAdornment(style, adornmentZ3) 207 | applyStyleToAdornment(style, adornmentZ4) 208 | table.insert(renderQueue, adornmentZ1) 209 | table.insert(renderQueue, adornmentZ2) 210 | table.insert(renderQueue, adornmentZ3) 211 | table.insert(renderQueue, adornmentZ4) 212 | end 213 | 214 | local function renderSphere(style, position: Vector3, radius: number) 215 | local adornment = get("SphereHandleAdornment") 216 | adornment.Radius = radius 217 | adornment.CFrame = CFrame.new(position) 218 | applyStyleToAdornment(style, adornment) 219 | table.insert(renderQueue, adornment) 220 | end 221 | 222 | local function renderWireSphere(style, position: Vector3, radius: number) 223 | local offset = style.scale * 0.5 224 | local outerRadius, innerRadius = radius + offset, radius - offset 225 | local relativeOrientation = CFrame.new(position) 226 | local adornmentX = get("CylinderHandleAdornment") 227 | local adornmentY = get("CylinderHandleAdornment") 228 | local adornmentZ = get("CylinderHandleAdornment") 229 | adornmentX.Radius = outerRadius 230 | adornmentX.InnerRadius = innerRadius 231 | adornmentX.Height = style.scale 232 | adornmentX.CFrame = relativeOrientation 233 | applyStyleToAdornment(style, adornmentX) 234 | table.insert(renderQueue, adornmentX) 235 | adornmentY.Radius = outerRadius 236 | adornmentY.InnerRadius = innerRadius 237 | adornmentY.Height = style.scale 238 | adornmentY.CFrame = relativeOrientation * CFrame.Angles(math.pi * 0.5, 0, 0) 239 | applyStyleToAdornment(style, adornmentY) 240 | table.insert(renderQueue, adornmentY) 241 | adornmentZ.Radius = outerRadius 242 | adornmentZ.InnerRadius = innerRadius 243 | adornmentZ.Height = style.scale 244 | adornmentZ.CFrame = relativeOrientation * CFrame.Angles(0, math.pi * 0.5, 0) 245 | applyStyleToAdornment(style, adornmentZ) 246 | table.insert(renderQueue, adornmentZ) 247 | end 248 | 249 | local function renderLine(style, from: Vector3, to: Vector3) 250 | local distance = (to - from).Magnitude 251 | local adornment = get("CylinderHandleAdornment") 252 | adornment.Radius = style.scale * 0.5 253 | adornment.InnerRadius = 0 254 | adornment.Height = distance 255 | adornment.CFrame = CFrame.lookAt(from, to) * CFrame.new(0, 0, -distance * 0.5) 256 | applyStyleToAdornment(style, adornment) 257 | table.insert(renderQueue, adornment) 258 | end 259 | 260 | local function renderArrow(style, from: Vector3, to: Vector3) 261 | local coneHeight = style.scale * 3 262 | local distance = math.abs((to - from).Magnitude - coneHeight) 263 | local orientation = CFrame.lookAt(from, to) 264 | local adornmentLine = get("CylinderHandleAdornment") 265 | local adornmentCone = get("ConeHandleAdornment") 266 | adornmentLine.Radius = style.scale * 0.5 267 | adornmentLine.InnerRadius = 0 268 | adornmentLine.Height = distance 269 | adornmentLine.CFrame = orientation * CFrame.new(0, 0, -distance * 0.5) 270 | applyStyleToAdornment(style, adornmentLine) 271 | adornmentCone.Height = coneHeight 272 | adornmentCone.Radius = coneHeight * 0.5 273 | adornmentCone.CFrame = orientation * CFrame.new(0, 0, -distance) 274 | applyStyleToAdornment(style, adornmentCone) 275 | table.insert(renderQueue, adornmentLine) 276 | table.insert(renderQueue, adornmentCone) 277 | end 278 | 279 | local function renderRay(style, from: Vector3, direction: Vector3) 280 | return renderArrow(style, from, from + direction) 281 | end 282 | 283 | ------------------------------------------------------------------------------------------------------------------------ 284 | -- Gizmo Class Wrapper 285 | ------------------------------------------------------------------------------------------------------------------------ 286 | 287 | local function createGizmo(render: (Style, T...) -> ()) 288 | 289 | local class = {__index={}} 290 | 291 | function class.draw(... : T...) 292 | if active then 293 | render(globalStyle, ...) 294 | end 295 | end 296 | 297 | type Object = typeof(setmetatable({} :: Gizmo, class)) 298 | 299 | function class.create(... : T...) : Object 300 | return setmetatable({ 301 | __properties = table.pack(...), 302 | style = table.clone(globalStyle) 303 | }, class) 304 | end 305 | 306 | function class.__index:enable() 307 | scheduledObjects[self] = true 308 | end 309 | 310 | function class.__index:disable() 311 | scheduledObjects[self] = nil 312 | end 313 | 314 | function class.__index:update(... : T...) 315 | self.__properties = table.pack(...) 316 | end 317 | 318 | function class.__index:__render() 319 | render(self.style, table.unpack(self.__properties)) 320 | end 321 | 322 | return table.freeze(class) 323 | end 324 | 325 | ------------------------------------------------------------------------------------------------------------------------ 326 | -- Render Update 327 | ------------------------------------------------------------------------------------------------------------------------ 328 | 329 | local function update() 330 | -- All gizmos created with 'create' need to be queued for render 331 | for object in pairs(scheduledObjects) do 332 | object:__render() 333 | end 334 | -- Clone the queue and render all instances in it 335 | local queue = table.clone(renderQueue) 336 | for _, instance in ipairs(queue) do 337 | instance.Visible = true 338 | end 339 | table.clear(renderQueue) 340 | task.wait() 341 | for _, instance in ipairs(queue) do 342 | release(instance) 343 | end 344 | end 345 | 346 | ------------------------------------------------------------------------------------------------------------------------ 347 | -- State Management 348 | ------------------------------------------------------------------------------------------------------------------------ 349 | 350 | local function enable() 351 | active = true 352 | RunService:BindToRenderStep(moduleId, Enum.RenderPriority.Last.Value + 1, update) 353 | end 354 | 355 | local function disable() 356 | active = false 357 | RunService:UnbindFromRenderStep(moduleId) 358 | end 359 | 360 | workspace:GetAttributeChangedSignal(GLOBAL_ATTRIBUTE):Connect(function () 361 | if workspace:GetAttribute(GLOBAL_ATTRIBUTE) then 362 | enable() 363 | else 364 | disable() 365 | end 366 | end) 367 | 368 | if workspace:GetAttribute(GLOBAL_ATTRIBUTE) then 369 | enable() 370 | end 371 | 372 | ------------------------------------------------------------------------------------------------------------------------ 373 | -- Exports 374 | ------------------------------------------------------------------------------------------------------------------------ 375 | 376 | return table.freeze { 377 | 378 | -- Globals 379 | style = globalStyle, 380 | 381 | -- Gizmos 382 | point = createGizmo(renderPoint), 383 | box = createGizmo(renderBox), 384 | wirebox = createGizmo(renderWireBox), 385 | sphere = createGizmo(renderSphere), 386 | wiresphere = createGizmo(renderWireSphere), 387 | line = createGizmo(renderLine), 388 | arrow = createGizmo(renderArrow), 389 | ray = createGizmo(renderRay), 390 | 391 | } 392 | --------------------------------------------------------------------------------