· 6 years ago · Jan 03, 2020, 09:10 PM
1------------------------------------------
2-- Create references to important objects
3------------------------------------------
4
5-- Reference services
6Workspace = Game:GetService 'Workspace';
7Players = Game:GetService 'Players';
8MarketplaceService = Game:GetService 'MarketplaceService';
9ContentProvider = Game:GetService 'ContentProvider';
10SoundService = Game:GetService 'SoundService';
11UserInputService = Game:GetService 'UserInputService';
12SelectionService = Game:GetService 'Selection';
13CoreGui = Game:GetService 'CoreGui';
14HttpService = Game:GetService 'HttpService';
15ChangeHistoryService = Game:GetService 'ChangeHistoryService';
16
17-- Reference external assets
18Assets = {
19 DarkSlantedRectangle = 'http://www.roblox.com/asset/?id=127774197';
20 LightSlantedRectangle = 'http://www.roblox.com/asset/?id=127772502';
21 ActionCompletionSound = 'http://www.roblox.com/asset/?id=99666917';
22 ExpandArrow = 'http://www.roblox.com/asset/?id=134367382';
23 UndoActiveDecal = 'http://www.roblox.com/asset/?id=141741408';
24 UndoInactiveDecal = 'http://www.roblox.com/asset/?id=142074557';
25 RedoActiveDecal = 'http://www.roblox.com/asset/?id=141741327';
26 RedoInactiveDecal = 'http://www.roblox.com/asset/?id=142074553';
27 DeleteActiveDecal = 'http://www.roblox.com/asset/?id=141896298';
28 DeleteInactiveDecal = 'http://www.roblox.com/asset/?id=142074644';
29 ExportActiveDecal = 'http://www.roblox.com/asset/?id=141741337';
30 ExportInactiveDecal = 'http://www.roblox.com/asset/?id=142074569';
31 CloneActiveDecal = 'http://www.roblox.com/asset/?id=142073926';
32 CloneInactiveDecal = 'http://www.roblox.com/asset/?id=142074563';
33 PluginIcon = 'http://www.roblox.com/asset/?id=142287521';
34 GroupLockIcon = 'http://www.roblox.com/asset/?id=175396862';
35 GroupUnlockIcon = 'http://www.roblox.com/asset/?id=160408836';
36 GroupUpdateOKIcon = 'http://www.roblox.com/asset/?id=164421681';
37 GroupUpdateIcon = 'http://www.roblox.com/asset/?id=160402908';
38};
39
40-- The ID of the tool model on ROBLOX
41ToolAssetID = 142785488;
42
43Tool = script.Parent;
44Player = Players.LocalPlayer;
45Mouse = nil;
46
47-- Set tool or plugin-specific references
48if plugin then
49 ToolType = 'plugin';
50 GUIContainer = CoreGui;
51
52 -- Create the toolbar button
53 ToolbarButton = plugin:CreateToolbar( 'Building Tools by F3X' ):CreateButton( '', 'Building Tools by F3X', Assets.PluginIcon );
54
55elseif Tool:IsA 'Tool' then
56 ToolType = 'tool';
57 GUIContainer = Player:WaitForChild 'PlayerGui';
58end;
59
60
61------------------------------------------
62-- Load external dependencies
63------------------------------------------
64
65RbxUtility = LoadLibrary 'RbxUtility';
66Support = require(Tool:WaitForChild 'SupportLibrary');
67
68-- Preload external assets
69for ResourceName, ResourceUrl in pairs( Assets ) do
70 ContentProvider:Preload( ResourceUrl );
71end;
72
73repeat wait( 0 ) until _G.gloo;
74Gloo = _G.gloo;
75
76Tool:WaitForChild 'HttpInterface';
77Tool:WaitForChild 'Interfaces';
78
79
80------------------------------------------
81-- Prepare the UI
82------------------------------------------
83-- Wait for all parts of the base UI to fully replicate
84if ToolType == 'tool' then
85 local UIComponentCount = (Tool:WaitForChild 'UIComponentCount').Value;
86 repeat wait( 0.1 ) until #Support.GetAllDescendants(Tool.Interfaces) >= UIComponentCount;
87end;
88
89
90------------------------------------------
91-- Create data containers
92------------------------------------------
93
94ActiveKeys = {};
95
96CurrentTool = nil;
97
98function equipTool( NewTool )
99
100 -- If it's a different tool than the current one
101 if CurrentTool ~= NewTool then
102
103 -- Run (if existent) the old tool's `Unequipped` listener
104 if CurrentTool and CurrentTool.Listeners.Unequipped then
105 CurrentTool.Listeners.Unequipped();
106 end;
107
108 CurrentTool = NewTool;
109
110 -- Recolor the handle
111 if ToolType == 'tool' then
112 Tool.Handle.BrickColor = NewTool.Color;
113 end;
114
115 -- Highlight the right button on the dock
116 for _, Button in pairs( Dock.ToolButtons:GetChildren() ) do
117 Button.BackgroundTransparency = 1;
118 end;
119 local Button = Dock.ToolButtons:FindFirstChild( getToolName( NewTool ) .. "Button" );
120 if Button then
121 Button.BackgroundTransparency = 0;
122 end;
123
124 -- Run (if existent) the new tool's `Equipped` listener
125 if NewTool.Listeners.Equipped then
126 NewTool.Listeners.Equipped();
127 end;
128
129 end;
130end;
131
132function cloneSelection()
133 -- Clones the items in the selection
134
135 -- Make sure that there are items in the selection
136 if #Selection.Items > 0 then
137
138 local item_copies = {};
139
140 -- Make a copy of every item in the selection and add it to table `item_copies`
141 for _, Item in pairs( Selection.Items ) do
142 local ItemCopy = Item:Clone();
143 ItemCopy.Parent = Workspace;
144 table.insert( item_copies, ItemCopy );
145 end;
146
147 -- Replace the selection with the copied items
148 Selection:clear();
149 for _, Item in pairs( item_copies ) do
150 Selection:add( Item );
151 end;
152
153 local HistoryRecord = {
154 copies = item_copies;
155 unapply = function ( self )
156 for _, Copy in pairs( self.copies ) do
157 if Copy then
158 Copy.Parent = nil;
159 end;
160 end;
161 end;
162 apply = function ( self )
163 Selection:clear();
164 for _, Copy in pairs( self.copies ) do
165 if Copy then
166 Copy.Parent = Workspace;
167 Copy:MakeJoints();
168 Selection:add( Copy );
169 end;
170 end;
171 end;
172 };
173 History:add( HistoryRecord );
174
175 -- Play a confirmation sound
176 local Sound = RbxUtility.Create "Sound" {
177 Name = "BTActionCompletionSound";
178 Pitch = 1.5;
179 SoundId = Assets.ActionCompletionSound;
180 Volume = 1;
181 Parent = Player or SoundService;
182 };
183 Sound:Play();
184 Sound:Destroy();
185
186 -- Highlight the outlines of the new parts
187 coroutine.wrap( function ()
188 for transparency = 1, 0.5, -0.1 do
189 for Item, SelectionBox in pairs( SelectionBoxes ) do
190 SelectionBox.Transparency = transparency;
191 end;
192 wait( 0.1 );
193 end;
194 end )();
195
196 end;
197
198end;
199
200function deleteSelection()
201 -- Deletes the items in the selection
202
203 if #Selection.Items == 0 then
204 return;
205 end;
206
207 local selection_items = Support.CloneTable(Selection.Items);
208
209 -- Create a history record
210 local HistoryRecord = {
211 targets = selection_items;
212 parents = {};
213 apply = function ( self )
214 for _, Target in pairs( self.targets ) do
215 if Target then
216 Target.Parent = nil;
217 end;
218 end;
219 end;
220 unapply = function ( self )
221 Selection:clear();
222 for _, Target in pairs( self.targets ) do
223 if Target then
224 Target.Parent = self.parents[Target];
225 Target:MakeJoints();
226 Selection:add( Target );
227 end;
228 end;
229 end;
230 };
231
232 for _, Item in pairs( selection_items ) do
233 HistoryRecord.parents[Item] = Item.Parent;
234 Item.Parent = nil;
235 end;
236
237 History:add( HistoryRecord );
238
239end;
240
241function prismSelect()
242 -- Selects all the parts within the area of the selected parts
243
244 -- Make sure parts to define the area are present
245 if #Selection.Items == 0 then
246 return;
247 end;
248
249 local parts = {};
250
251 -- Get all the parts in workspace
252 local workspace_parts = {};
253 local workspace_children = Support.GetAllDescendants(Workspace);
254 for _, Child in pairs( workspace_children ) do
255 if Child:IsA( 'BasePart' ) and not Selection:find( Child ) then
256 table.insert( workspace_parts, Child );
257 end;
258 end;
259
260 -- Go through each part and perform area tests on each one
261 local checks = {};
262 for _, Item in pairs( workspace_parts ) do
263 checks[Item] = 0;
264 for _, SelectionItem in pairs( Selection.Items ) do
265
266 -- Calculate the position of the item in question in relation to the area-defining parts
267 local offset = SelectionItem.CFrame:toObjectSpace( Item.CFrame );
268 local extents = SelectionItem.Size / 2;
269
270 -- Check the item off if it passed this test (if it's within the range of the extents)
271 if ( math.abs( offset.x ) <= extents.x ) and ( math.abs( offset.y ) <= extents.y ) and ( math.abs( offset.z ) <= extents.z ) then
272 checks[Item] = checks[Item] + 1;
273 end;
274
275 end;
276 end;
277
278 -- Delete the parts that were used to select the area
279 local selection_items = Support.CloneTable(Selection.Items);
280 local selection_item_parents = {};
281 for _, Item in pairs( selection_items ) do
282 selection_item_parents[Item] = Item.Parent;
283 Item.Parent = nil;
284 end;
285
286 -- Select the parts that passed any area checks
287 for _, Item in pairs( workspace_parts ) do
288 if checks[Item] > 0 then
289 Selection:add( Item );
290 end;
291 end;
292
293 -- Add a history record
294 History:add( {
295 selection_parts = selection_items;
296 selection_part_parents = selection_item_parents;
297 new_selection = Support.CloneTable(Selection.Items);
298 apply = function ( self )
299 Selection:clear();
300 for _, Item in pairs( self.selection_parts ) do
301 Item.Parent = nil;
302 end;
303 for _, Item in pairs( self.new_selection ) do
304 Selection:add( Item );
305 end;
306 end;
307 unapply = function ( self )
308 Selection:clear();
309 for _, Item in pairs( self.selection_parts ) do
310 Item.Parent = self.selection_part_parents[Item];
311 Selection:add( Item );
312 end;
313 end;
314 } );
315
316end;
317
318function toggleHelp()
319
320 -- Make sure the dock is ready
321 if not Dock then
322 return;
323 end;
324
325 -- Toggle the visibility of the help tooltip
326 Dock.HelpInfo.Visible = not Dock.HelpInfo.Visible;
327
328end;
329
330function getToolName( tool )
331 -- Returns the name of `tool` as registered in `Tools`
332
333 local name_search = Support.FindTableOccurrences(Tools, tool);
334 if #name_search > 0 then
335 return name_search[1];
336 end;
337
338end;
339
340function isSelectable( Object )
341 -- Returns whether `Object` is selectable
342
343 if not Object or not Object.Parent or not Object:IsA( "BasePart" ) or Object.Locked or Selection:find( Object ) or Groups:IsPartIgnored( Object ) then
344 return false;
345 end;
346
347 -- If it passes all checks, return true
348 return true;
349end;
350
351function IsVersionOutdated()
352 -- Returns whether this version of Building Tools is out of date
353
354 -- Check the most recent version number
355 local AssetInfo = MarketplaceService:GetProductInfo( ToolAssetID, Enum.InfoType.Asset );
356 local VersionID = AssetInfo.Description:match( '%[Version: (.+)%]' );
357 local CurrentVersionID = ( Tool:WaitForChild 'Version' ).Value;
358
359 -- If the most recent version ID differs from the current tool's version ID,
360 -- this version of the tool is outdated
361 if VersionID ~= CurrentVersionID then
362 return true;
363 end;
364
365 -- If it's up-to-date, return false
366 return false;
367end;
368
369-- Provide initial HttpService availability info
370HttpAvailable, HttpAvailabilityError = Tool.HttpInterface:WaitForChild('Test'):InvokeServer();
371
372-- Keep track of the latest HttpService availability status
373-- (which is only likely to change while in Studio, using the plugin)
374if ToolType == 'plugin' then
375 HttpService.Changed:connect( function ()
376 HttpAvailable, HttpAvailabilityError = Tool.HttpInterface:WaitForChild('Test'):InvokeServer();
377 end );
378end;
379
380local StartupNotificationsShown = false;
381function ShowStartupNotifications()
382
383 -- Make sure the startup notifications are only shown once
384 if StartupNotificationsShown then
385 return;
386 end;
387 StartupNotificationsShown = true;
388
389 -- Create the main container for notifications
390 local NotificationContainer = Tool.Interfaces.BTStartupNotificationContainer:Clone();
391
392 -- Add the right notifications
393 if not HttpAvailable and HttpAvailabilityError == 'Http requests are not enabled' then
394 NotificationContainer.HttpDisabledWarning.Visible = true;
395 end;
396 if not HttpAvailable and HttpAvailabilityError == 'Http requests can only be executed by game server' then
397 NotificationContainer.SoloWarning.Visible = true;
398 end;
399 if IsVersionOutdated() then
400 if ToolType == 'tool' then
401 NotificationContainer.ToolUpdateNotification.Visible = true;
402 elseif ToolType == 'plugin' then
403 NotificationContainer.PluginUpdateNotification.Visible = true;
404 end;
405 end;
406
407 local function SetContainerSize()
408 -- A function to position the notifications in the container and
409 -- resize the container to fit all the notifications
410
411 -- Keep track of the lowest extent of each item in the container
412 local LowestPoint = 0;
413
414 local Notifications = NotificationContainer:GetChildren();
415 for NotificationIndex, Notification in pairs( Notifications ) do
416
417 -- Position each notification under the last one
418 Notification.Position = UDim2.new(
419 Notification.Position.X.Scale,
420 Notification.Position.X.Offset,
421 Notification.Position.Y.Scale,
422 ( LowestPoint == 0 ) and 0 or ( LowestPoint + 10 )
423 );
424
425 -- Calculate the lowest point of this notification
426 local VerticalEnd = Notification.Position.Y.Offset + Notification.Size.Y.Offset;
427 if Notification.Visible and VerticalEnd > LowestPoint then
428 LowestPoint = VerticalEnd;
429 end;
430
431 end;
432
433 NotificationContainer.Size = UDim2.new(
434 NotificationContainer.Size.X.Scale,
435 NotificationContainer.Size.X.Offset,
436 0,
437 LowestPoint
438 );
439 end;
440
441 SetContainerSize();
442
443 -- Have the container start from the center/bottom of the screen
444 local HCenterPos = ( UI.AbsoluteSize.x - NotificationContainer.Size.X.Offset ) / 2;
445 local VBottomPos = UI.AbsoluteSize.y + NotificationContainer.Size.Y.Offset;
446 NotificationContainer.Position = UDim2.new( 0, HCenterPos, 0, VBottomPos );
447
448 NotificationContainer.Parent = UI;
449
450 local function CenterNotificationContainer()
451 -- A function to center the notification container
452
453 -- Animate the container to slide up to the absolute center of the screen
454 local VCenterPos = ( UI.AbsoluteSize.y - NotificationContainer.Size.Y.Offset ) / 2;
455 NotificationContainer:TweenPosition(
456 UDim2.new( 0, HCenterPos, 0, VCenterPos ),
457 Enum.EasingDirection.Out,
458 Enum.EasingStyle.Quad,
459 0.2
460 );
461 end;
462
463 CenterNotificationContainer();
464
465 -- Add functionality to the notification UIs
466 for _, Notification in pairs( NotificationContainer:GetChildren() ) do
467 if Notification.Visible then
468 Notification.OKButton.MouseButton1Click:connect( function ()
469 Notification:Destroy();
470 SetContainerSize();
471 CenterNotificationContainer();
472 end );
473 Notification.HelpButton.MouseButton1Click:connect( function ()
474 Notification.HelpButton:Destroy();
475 Notification.ButtonSeparator:Destroy();
476 Notification.OKButton:TweenSize(
477 UDim2.new( 1, 0, 0, 22 ),
478 Enum.EasingDirection.Out,
479 Enum.EasingStyle.Quad,
480 0.2
481 );
482 Notification.Notice:Destroy();
483 Notification.Help.Visible = true;
484 Notification:TweenSize(
485 UDim2.new(
486 Notification.Size.X.Scale, Notification.Size.X.Offset,
487 Notification.Size.Y.Scale, Notification.Help.NotificationSize.Value
488 ),
489 Enum.EasingDirection.Out,
490 Enum.EasingStyle.Quad,
491 0.2,
492 true,
493 function ()
494 SetContainerSize();
495 CenterNotificationContainer();
496 end
497 );
498 end );
499 end;
500 end;
501
502 -- Get rid of the notifications if the user unequips the tool
503 if ToolType == 'tool' then
504 Tool.Unequipped:connect( function ()
505 if NotificationContainer.Visible then
506 NotificationContainer.Visible = false;
507 NotificationContainer:Destroy();
508 end;
509 end );
510 end;
511end;
512
513-- Keep some state data
514clicking = false;
515selecting = false;
516click_x, click_y = 0, 0;
517override_selection = false;
518
519SelectionBoxes = {};
520SelectionExistenceListeners = {};
521SelectionBoxColor = BrickColor.new( "Cyan" );
522TargetBox = nil;
523
524-- Keep a container for temporary connections
525-- from the platform
526Connections = {};
527
528-- Make sure the UI container gets placed
529UI = RbxUtility.Create "ScreenGui" {
530 Name = "Building Tools by F3X (UI)"
531};
532if ToolType == 'tool' then
533 UI.Parent = GUIContainer;
534elseif ToolType == 'plugin' then
535 UI.Parent = CoreGui;
536end;
537
538Dragger = nil;
539
540function updateSelectionBoxColor()
541 -- Updates the color of the selectionboxes
542 for _, SelectionBox in pairs( SelectionBoxes ) do
543 SelectionBox.Color = SelectionBoxColor;
544 end;
545end;
546
547Selection = {
548
549 ["Items"] = {};
550
551 -- Provide events to listen to changes in the selection
552 ["Changed"] = RbxUtility.CreateSignal();
553 ["ItemAdded"] = RbxUtility.CreateSignal();
554 ["ItemRemoved"] = RbxUtility.CreateSignal();
555 ["Cleared"] = RbxUtility.CreateSignal();
556
557 -- Provide a method to get an item's index in the selection
558 ["find"] = function ( self, Needle )
559
560 -- Look through all the selected items and return the matching item's index
561 for item_index, Item in pairs( self.Items ) do
562 if Item == Needle then
563 return item_index;
564 end;
565 end;
566
567 -- Otherwise, return `nil`
568
569 end;
570
571 -- Provide a method to add items to the selection
572 ["add"] = function ( self, NewPart )
573
574 -- Make sure `NewPart` is selectable
575 if not isSelectable( NewPart ) then
576 return false;
577 end;
578
579 -- Make sure `NewPart` isn't already in the selection
580 if #Support.FindTableOccurrences(self.Items, NewPart) > 0 then
581 return false;
582 end;
583
584 -- Insert it into the selection
585 table.insert( self.Items, NewPart );
586
587 -- Add its SelectionBox if we're in tool mode
588 if ToolType == 'tool' then
589 SelectionBoxes[NewPart] = Instance.new( "SelectionBox", UI );
590 SelectionBoxes[NewPart].Name = "BTSelectionBox";
591 SelectionBoxes[NewPart].Color = SelectionBoxColor;
592 SelectionBoxes[NewPart].Adornee = NewPart;
593 SelectionBoxes[NewPart].LineThickness = 0.05;
594 SelectionBoxes[NewPart].Transparency = 0.5;
595 end;
596
597 -- Remove any target selection box focus
598 if NewPart == TargetBox.Adornee then
599 TargetBox.Adornee = nil;
600 end;
601
602 -- Make sure to remove the item from the selection when it's deleted
603 SelectionExistenceListeners[NewPart] = NewPart.AncestryChanged:connect( function ( Object, NewParent )
604 if NewParent == nil then
605 Selection:remove( NewPart );
606 end;
607 end );
608
609 -- Provide a reference to the last item added to the selection (i.e. NewPart)
610 self:focus( NewPart );
611
612 -- Fire events
613 self.ItemAdded:fire( NewPart );
614 self.Changed:fire();
615
616 end;
617
618 -- Provide a method to remove items from the selection
619 ["remove"] = function ( self, Item, Clearing )
620
621 -- Make sure selection item `Item` exists
622 if not self:find( Item ) then
623 return false;
624 end;
625
626 -- Remove `Item`'s SelectionBox
627 local SelectionBox = SelectionBoxes[Item];
628 if SelectionBox then
629 SelectionBox:Destroy();
630 end;
631 SelectionBoxes[Item] = nil;
632
633 -- Delete the item from the selection
634 table.remove( self.Items, self:find( Item ) );
635
636 -- If it was logged as the last item, change it
637 if self.Last == Item then
638 self:focus( ( #self.Items > 0 ) and self.Items[#self.Items] or nil );
639 end;
640
641 -- Delete the existence listeners of the item
642 SelectionExistenceListeners[Item]:disconnect();
643 SelectionExistenceListeners[Item] = nil;
644
645 -- Fire events
646 self.ItemRemoved:fire( Item, Clearing );
647 self.Changed:fire();
648
649 end;
650
651 -- Provide a method to clear the selection
652 ["clear"] = function ( self )
653
654 -- Go through all the items in the selection and call `self.remove` on them
655 for _, Item in pairs(Support.CloneTable(self.Items)) do
656 self:remove( Item, true );
657 end;
658
659 -- Fire events
660 self.Cleared:fire();
661
662 end;
663
664 -- Provide a method to change the focus of the selection
665 ["focus"] = function ( self, NewFocus )
666
667 -- Change the focus
668 self.Last = NewFocus;
669
670 -- Fire events
671 self.Changed:fire();
672
673 end;
674
675};
676
677------------------------------------------
678-- WARNING: MICROOPTIMIZED CODE
679------------------------------------------
680
681-- Create shortcuts to certain things that are expensive to call constantly
682local cframe_new = CFrame.new;
683local table_insert = table.insert;
684local cframe_toWorldSpace = CFrame.new().toWorldSpace;
685local math_min = math.min;
686local math_max = math.max;
687
688function calculateExtents(Items, StaticExtents, JustExtents)
689 -- Returns the size and position of a boundary box that covers the extents
690 -- of the parts in table `Items`
691
692 -- Make sure there's actually any parts given
693 local RandomItem;
694 local ItemCount = 0;
695 for _, Item in pairs(Items) do
696 ItemCount = ItemCount + 1;
697 RandomItem = Item;
698 end;
699 if ItemCount == 0 then
700 return;
701 end;
702
703 local ComparisonBaseMin = StaticExtents and StaticExtents['Minimum'] or RandomItem['Position'];
704 local ComparisonBaseMax = StaticExtents and StaticExtents['Maximum'] or RandomItem['Position'];
705 local MinX, MinY, MinZ = ComparisonBaseMin['x'], ComparisonBaseMin['y'], ComparisonBaseMin['z'];
706 local MaxX, MaxY, MaxZ = ComparisonBaseMax['x'], ComparisonBaseMax['y'], ComparisonBaseMax['z'];
707
708 for _, Part in pairs(Items) do
709
710 if not (Part.Anchored and StaticExtents) then
711 local PartCFrame = Part['CFrame'];
712 local PartSize = Part['Size'] / 2;
713 local SizeX, SizeY, SizeZ = PartSize['x'], PartSize['y'], PartSize['z'];
714
715 local Corner;
716 local XPoints, YPoints, ZPoints = {}, {}, {};
717
718 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( SizeX, SizeY, SizeZ ) );
719 table_insert(XPoints, Corner['x']);
720 table_insert(YPoints, Corner['y']);
721 table_insert(ZPoints, Corner['z']);
722
723 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( -SizeX, SizeY, SizeZ ) );
724 table_insert(XPoints, Corner['x']);
725 table_insert(YPoints, Corner['y']);
726 table_insert(ZPoints, Corner['z']);
727
728 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( SizeX, -SizeY, SizeZ ) );
729 table_insert(XPoints, Corner['x']);
730 table_insert(YPoints, Corner['y']);
731 table_insert(ZPoints, Corner['z']);
732
733 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( SizeX, SizeY, -SizeZ ) );
734 table_insert(XPoints, Corner['x']);
735 table_insert(YPoints, Corner['y']);
736 table_insert(ZPoints, Corner['z']);
737
738 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( -SizeX, SizeY, -SizeZ ) );
739 table_insert(XPoints, Corner['x']);
740 table_insert(YPoints, Corner['y']);
741 table_insert(ZPoints, Corner['z']);
742
743 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( -SizeX, -SizeY, SizeZ ) );
744 table_insert(XPoints, Corner['x']);
745 table_insert(YPoints, Corner['y']);
746 table_insert(ZPoints, Corner['z']);
747
748 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( SizeX, -SizeY, -SizeZ ) );
749 table_insert(XPoints, Corner['x']);
750 table_insert(YPoints, Corner['y']);
751 table_insert(ZPoints, Corner['z']);
752
753 Corner = cframe_toWorldSpace( PartCFrame, cframe_new( -SizeX, -SizeY, -SizeZ ) );
754 table_insert(XPoints, Corner['x']);
755 table_insert(YPoints, Corner['y']);
756 table_insert(ZPoints, Corner['z']);
757
758 MinX = math_min(MinX, unpack(XPoints));
759 MinY = math_min(MinY, unpack(YPoints));
760 MinZ = math_min(MinZ, unpack(ZPoints));
761 MaxX = math_max(MaxX, unpack(XPoints));
762 MaxY = math_max(MaxY, unpack(YPoints));
763 MaxZ = math_max(MaxZ, unpack(ZPoints));
764
765 end;
766
767 end;
768
769 if JustExtents then
770
771 -- Return the extents information
772 return {
773 Minimum = { x = MinX, y = MinY, z = MinZ };
774 Maximum = { x = MaxX, y = MaxY, z = MaxZ };
775 };
776
777 else
778
779 -- Get the size between the extents
780 local XSize, YSize, ZSize = MaxX - MinX,
781 MaxY - MinY,
782 MaxZ - MinZ;
783
784 local Size = Vector3.new( XSize, YSize, ZSize );
785
786 -- Get the centroid of the collection of points
787 local Position = CFrame.new( MinX + ( MaxX - MinX ) / 2,
788 MinY + ( MaxY - MinY ) / 2,
789 MinZ + ( MaxZ - MinZ ) / 2 );
790
791 -- Return the size of the collection of parts
792 return Size, Position;
793
794 end;
795
796end;
797
798
799-- Keep the Studio selection up-to-date (if applicable)
800if ToolType == 'plugin' then
801 Selection.Changed:connect( function ()
802 SelectionService:Set( Selection.Items );
803 end );
804end;
805
806Tools = {};
807
808
809------------------------------------------
810-- Define other utilities needed by tools
811------------------------------------------
812
813function createDropdown()
814
815 local Frame = RbxUtility.Create "Frame" {
816 Name = "Dropdown";
817 Size = UDim2.new( 0, 20, 0, 20 );
818 BackgroundTransparency = 1;
819 BorderSizePixel = 0;
820 ClipsDescendants = true;
821 };
822
823 RbxUtility.Create "ImageLabel" {
824 Parent = Frame;
825 Name = "Arrow";
826 BackgroundTransparency = 1;
827 BorderSizePixel = 0;
828 Image = Assets.ExpandArrow;
829 Position = UDim2.new( 1, -21, 0, 3 );
830 Size = UDim2.new( 0, 20, 0, 20 );
831 ZIndex = 3;
832 };
833
834 local DropdownObject = {
835 -- Provide access to the actual frame
836 Frame = Frame;
837
838 -- Keep a list of all the options in the dropdown
839 _options = {};
840
841 -- Provide a function to add options to the dropdown
842 addOption = function ( self, option )
843
844 -- Add the option to the list
845 table.insert( self._options, option );
846
847 -- Create the GUI for the option
848 local Button = RbxUtility.Create "TextButton" {
849 Parent = self.Frame;
850 BackgroundColor3 = Color3.new( 0, 0, 0 );
851 BackgroundTransparency = 0.3;
852 BorderColor3 = Color3.new( 27 / 255, 42 / 255, 53 / 255 );
853 BorderSizePixel = 1;
854 Name = option;
855 Position = UDim2.new( math.ceil( #self._options / 9 ) - 1, 0, 0, 25 * ( ( #self._options % 9 == 0 ) and 9 or ( #self._options % 9 ) ) );
856 Size = UDim2.new( 1, 0, 0, 25 );
857 ZIndex = 3;
858 Text = "";
859 };
860 local Label = RbxUtility.Create "TextLabel" {
861 Parent = Button;
862 BackgroundTransparency = 1;
863 BorderSizePixel = 0;
864 Position = UDim2.new( 0, 6, 0, 0 );
865 Size = UDim2.new( 1, -30, 1, 0 );
866 ZIndex = 3;
867 Font = Enum.Font.ArialBold;
868 FontSize = Enum.FontSize.Size10;
869 Text = option;
870 TextColor3 = Color3.new( 1, 1, 1 );
871 TextXAlignment = Enum.TextXAlignment.Left;
872 TextYAlignment = Enum.TextYAlignment.Center;
873 };
874
875 -- Return the button object
876 return Button;
877
878 end;
879
880 selectOption = function ( self, option )
881 self.Frame.MainButton.CurrentOption.Text = option;
882 end;
883
884 open = false;
885
886 toggle = function ( self )
887
888 -- If it's open, close it
889 if self.open then
890 self.Frame.MainButton.BackgroundTransparency = 0.3;
891 self.Frame.ClipsDescendants = true;
892 self.open = false;
893
894 -- If it's not open, open it
895 else
896 self.Frame.MainButton.BackgroundTransparency = 0;
897 self.Frame.ClipsDescendants = false;
898 self.open = true;
899 end;
900
901 end;
902
903 };
904
905 -- Create the GUI for the option
906 local MainButton = RbxUtility.Create "TextButton" {
907 Parent = Frame;
908 Name = "MainButton";
909 BackgroundColor3 = Color3.new( 0, 0, 0 );
910 BackgroundTransparency = 0.3;
911 BorderColor3 = Color3.new( 27 / 255, 42 / 255, 53 / 255 );
912 BorderSizePixel = 1;
913 Position = UDim2.new( 0, 0, 0, 0 );
914 Size = UDim2.new( 1, 0, 0, 25 );
915 ZIndex = 2;
916 Text = "";
917
918 -- Toggle the dropdown when pressed
919 [RbxUtility.Create.E "MouseButton1Up"] = function ()
920 DropdownObject:toggle();
921 end;
922 };
923 RbxUtility.Create "TextLabel" {
924 Parent = MainButton;
925 Name = "CurrentOption";
926 BackgroundTransparency = 1;
927 BorderSizePixel = 0;
928 Position = UDim2.new( 0, 6, 0, 0 );
929 Size = UDim2.new( 1, -30, 1, 0 );
930 ZIndex = 3;
931 Font = Enum.Font.ArialBold;
932 FontSize = Enum.FontSize.Size10;
933 Text = "";
934 TextColor3 = Color3.new( 1, 1, 1 );
935 TextXAlignment = Enum.TextXAlignment.Left;
936 TextYAlignment = Enum.TextYAlignment.Center;
937 };
938
939 return DropdownObject;
940
941end;
942
943
944------------------------------------------
945-- Provide an interface to the 2D
946-- selection system
947------------------------------------------
948
949Select2D = {
950
951 -- Keep state data
952 ["enabled"] = false;
953
954 -- Keep objects
955 ["GUI"] = nil;
956
957 -- Keep temporary, disposable connections
958 ["Connections"] = {};
959
960 -- Provide an interface to the functions
961 ["start"] = function ( self )
962
963 if self.enabled then
964 return;
965 end;
966
967 self.enabled = true;
968
969 -- Create the GUI
970 self.GUI = RbxUtility.Create "ScreenGui" {
971 Name = "BTSelectionRectangle";
972 Parent = UI;
973 };
974
975 local Rectangle = RbxUtility.Create "Frame" {
976 Name = "Rectangle";
977 Active = false;
978 Parent = self.GUI;
979 BackgroundColor3 = Color3.new( 0, 0, 0 );
980 BackgroundTransparency = 0.5;
981 BorderSizePixel = 0;
982 Position = UDim2.new( 0, math.min( click_x, Mouse.X ), 0, math.min( click_y, Mouse.Y ) );
983 Size = UDim2.new( 0, math.max( click_x, Mouse.X ) - math.min( click_x, Mouse.X ), 0, math.max( click_y, Mouse.Y ) - math.min( click_y, Mouse.Y ) );
984 };
985
986 -- Listen for when to resize the selection
987 self.Connections.SelectionResize = Mouse.Move:connect( function ()
988 Rectangle.Position = UDim2.new( 0, math.min( click_x, Mouse.X ), 0, math.min( click_y, Mouse.Y ) );
989 Rectangle.Size = UDim2.new( 0, math.max( click_x, Mouse.X ) - math.min( click_x, Mouse.X ), 0, math.max( click_y, Mouse.Y ) - math.min( click_y, Mouse.Y ) );
990 end );
991
992 -- Listen for when the selection ends (when the left mouse button is released)
993 self.Connections.SelectionEnd = UserInputService.InputEnded:connect( function ( InputData )
994 if InputData.UserInputType == Enum.UserInputType.MouseButton1 then
995 self:select();
996 self:finish();
997 end;
998 end );
999
1000 end;
1001
1002 ["select"] = function ( self )
1003
1004 if not self.enabled then
1005 return;
1006 end;
1007
1008 for _, Object in pairs(Support.GetAllDescendants(Workspace)) do
1009
1010 -- Make sure we can select this part
1011 if isSelectable( Object ) then
1012
1013 -- Check if the part is rendered within the range of the selection area
1014 local PartPosition, InScreen = Workspace.CurrentCamera:WorldToScreenPoint(Object.Position);
1015 if PartPosition and InScreen then
1016 local left_check = PartPosition.x >= self.GUI.Rectangle.AbsolutePosition.x;
1017 local right_check = PartPosition.x <= ( self.GUI.Rectangle.AbsolutePosition.x + self.GUI.Rectangle.AbsoluteSize.x );
1018 local top_check = PartPosition.y >= self.GUI.Rectangle.AbsolutePosition.y;
1019 local bottom_check = PartPosition.y <= ( self.GUI.Rectangle.AbsolutePosition.y + self.GUI.Rectangle.AbsoluteSize.y );
1020
1021 -- If the part is within the selection area, select it
1022 if left_check and right_check and top_check and bottom_check then
1023 Selection:add( Object );
1024 end;
1025 end;
1026
1027 end;
1028
1029 end;
1030
1031 end;
1032
1033 ["finish"] = function ( self )
1034
1035 if not self.enabled then
1036 return;
1037 end;
1038
1039 -- Disconnect temporary connections
1040 for connection_index, Connection in pairs( self.Connections ) do
1041 Connection:disconnect();
1042 self.Connections[connection_index] = nil;
1043 end;
1044
1045 -- Remove temporary objects
1046 self.GUI:Destroy();
1047 self.GUI = nil;
1048
1049 self.enabled = false;
1050
1051 end;
1052
1053};
1054
1055------------------------------------------
1056-- Provide an interface to the edge
1057-- selection system
1058------------------------------------------
1059SelectEdge = {
1060
1061 -- Keep state data
1062 ["enabled"] = false;
1063 ["started"] = false;
1064
1065 -- Keep objects
1066 ["Marker"] = nil;
1067 ["MarkerOutline"] = RbxUtility.Create "SelectionBox" {
1068 Color = BrickColor.new( "Institutional white" );
1069 Parent = UI;
1070 Name = "BTEdgeSelectionMarkerOutline";
1071 };
1072
1073 -- Keep temporary, disposable connections
1074 ["Connections"] = {};
1075
1076 -- Provide an interface to the functions
1077 ["start"] = function ( self, edgeSelectionCallback )
1078
1079 if self.started then
1080 return;
1081 end;
1082
1083 -- Listen for when to engage in selection
1084 self.Connections.KeyListener = Mouse.KeyDown:connect( function ( key )
1085
1086 local key = key:lower();
1087 local key_code = key:byte();
1088
1089 if key == "t" and #Selection.Items > 0 then
1090 self:enable( edgeSelectionCallback );
1091 end;
1092
1093 end );
1094
1095 self.started = true;
1096
1097 end;
1098
1099 ["enable"] = function ( self, edgeSelectionCallback )
1100
1101 if self.enabled then
1102 return;
1103 end;
1104
1105 self.Connections.MoveListener = Mouse.Move:connect( function ()
1106
1107 -- Make sure the target can be selected
1108 if not Selection:find( Mouse.Target ) then
1109 return;
1110 end;
1111
1112 -- Calculate the proximity to each edge
1113 local Proximity = {};
1114 local edges = {};
1115
1116 -- Create shortcuts to certain things that are expensive to call constantly
1117 local table_insert = table.insert;
1118 local newCFrame = CFrame.new;
1119 local PartCFrame = Mouse.Target.CFrame;
1120 local cframe_toWorldSpace = PartCFrame.toWorldSpace;
1121 local PartSize = Mouse.Target.Size / 2;
1122 local SizeX, SizeY, SizeZ = PartSize.x, PartSize.y, PartSize.z;
1123
1124 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, SizeY, SizeZ ) ) );
1125 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, SizeY, SizeZ ) ) );
1126 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, -SizeY, SizeZ ) ) );
1127 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, SizeY, -SizeZ ) ) );
1128 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, SizeY, -SizeZ ) ) );
1129 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, -SizeY, SizeZ ) ) );
1130 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, -SizeY, -SizeZ ) ) );
1131 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, -SizeY, -SizeZ ) ) );
1132 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, SizeY, 0 ) ) );
1133 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, 0, SizeZ ) ) );
1134 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, SizeY, SizeZ ) ) );
1135 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, 0, 0 ) ) );
1136 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, SizeY, 0 ) ) );
1137 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, 0, SizeZ ) ) );
1138 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, SizeY, 0 ) ) );
1139 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, 0, SizeZ ) ) );
1140 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, -SizeY, SizeZ ) ) );
1141 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, 0, 0 ) ) );
1142 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, -SizeY, 0 ) ) );
1143 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, 0, -SizeZ ) ) );
1144 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, -SizeY, 0 ) ) );
1145 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( SizeX, 0, -SizeZ ) ) );
1146 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, SizeY, -SizeZ ) ) );
1147 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, -SizeY, 0 ) ) );
1148 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( -SizeX, 0, -SizeZ ) ) );
1149 table_insert( edges, cframe_toWorldSpace( PartCFrame, newCFrame( 0, -SizeY, -SizeZ ) ) );
1150
1151 -- Calculate the proximity of every edge to the mouse
1152 for edge_index, Edge in pairs( edges ) do
1153 Proximity[edge_index] = ( Mouse.Hit.p - Edge.p ).magnitude;
1154 end;
1155
1156 -- Get the closest edge to the mouse
1157 local highest_proximity = 1;
1158 for proximity_index, proximity in pairs( Proximity ) do
1159 if proximity < Proximity[highest_proximity] then
1160 highest_proximity = proximity_index;
1161 end;
1162 end;
1163
1164 -- Replace the current target edge (if any)
1165 local ClosestEdge = edges[highest_proximity];
1166
1167 if self.Marker then
1168 self.Marker:Destroy();
1169 end;
1170 self.Marker = RbxUtility.Create "Part" {
1171 Name = "BTEdgeSelectionMarker";
1172 Anchored = true;
1173 Locked = true;
1174 CanCollide = false;
1175 Transparency = 1;
1176 FormFactor = Enum.FormFactor.Custom;
1177 Size = Vector3.new( 0.2, 0.2, 0.2 );
1178 CFrame = ClosestEdge;
1179 };
1180
1181 self.MarkerOutline.Adornee = self.Marker;
1182
1183 end );
1184
1185 self.Connections.ClickListener = Mouse.Button1Up:connect( function ()
1186 override_selection = true;
1187 self:select( edgeSelectionCallback );
1188 end );
1189
1190 self.enabled = true;
1191
1192 end;
1193
1194 ["select"] = function ( self, callback )
1195
1196 if not self.enabled or not self.Marker then
1197 return;
1198 end;
1199
1200 self.MarkerOutline.Adornee = self.Marker;
1201
1202 callback( self.Marker );
1203
1204 -- Stop treating it like a marker
1205 self.Marker = nil;
1206
1207 self:disable();
1208
1209 end;
1210
1211 ["disable"] = function ( self )
1212
1213 if not self.enabled then
1214 return;
1215 end;
1216
1217 -- Disconnect unnecessary temporary connections
1218 if self.Connections.ClickListener then
1219 self.Connections.ClickListener:disconnect();
1220 self.Connections.ClickListener = nil;
1221 end;
1222 if self.Connections.MoveListener then
1223 self.Connections.MoveListener:disconnect();
1224 self.Connections.MoveListener = nil;
1225 end;
1226
1227 -- Remove temporary objects
1228 if self.Marker then
1229 self.Marker:Destroy();
1230 end;
1231 self.Marker = nil;
1232
1233 self.MarkerOutline.Adornee = nil;
1234 self.enabled = false;
1235
1236 end;
1237
1238 ["stop"] = function ( self )
1239
1240 if not self.started then
1241 return;
1242 end;
1243
1244 -- Cancel any ongoing selection
1245 self:disable();
1246
1247 -- Disconnect & remove all temporary connections
1248 for connection_index, Connection in pairs( self.Connections ) do
1249 Connection:disconnect();
1250 self.Connections[connection_index] = nil;
1251 end;
1252
1253 -- Remove temporary objects
1254 if self.Marker then
1255 self.Marker:Destroy();
1256 end;
1257
1258 self.started = false;
1259
1260 end;
1261
1262};
1263
1264------------------------------------------
1265-- Provide an interface to the history
1266-- system
1267------------------------------------------
1268History = {
1269
1270 -- Keep a container for the actual history data
1271 ["Data"] = {};
1272
1273 -- Keep state data
1274 ["index"] = 0;
1275
1276 -- Provide events for the platform to listen for changes
1277 ["Changed"] = RbxUtility.CreateSignal();
1278
1279 -- Provide functions to control the system
1280 ["undo"] = function ( self )
1281
1282 -- Make sure we're not getting out of boundary
1283 if self.index - 1 < 0 then
1284 return;
1285 end;
1286
1287 -- Fetch the history record & unapply it
1288 local CurrentRecord = self.Data[self.index];
1289 CurrentRecord:unapply();
1290
1291 -- Go back in the history
1292 self.index = self.index - 1;
1293
1294 -- Fire the relevant events
1295 self.Changed:fire();
1296
1297 end;
1298
1299 ["redo"] = function ( self )
1300
1301 -- Make sure we're not getting out of boundary
1302 if self.index + 1 > #self.Data then
1303 return;
1304 end;
1305
1306 -- Go forward in the history
1307 self.index = self.index + 1;
1308
1309 -- Fetch the new history record & apply it
1310 local NewRecord = self.Data[self.index];
1311 NewRecord:apply();
1312
1313 -- Fire the relevant events
1314 self.Changed:fire();
1315
1316 end;
1317
1318 ["add"] = function ( self, Record )
1319
1320 -- Place the record in its right spot
1321 self.Data[self.index + 1] = Record;
1322
1323 -- Advance the history index
1324 self.index = self.index + 1;
1325
1326 -- Clear out the following history
1327 for index = self.index + 1, #self.Data do
1328 self.Data[index] = nil;
1329 end;
1330
1331 -- Fire the relevant events
1332 self.Changed:fire();
1333
1334 end;
1335
1336};
1337
1338-- Link up to Studio's history system if this is the plugin
1339if ToolType == 'plugin' then
1340 History.Changed:connect(function ()
1341 ChangeHistoryService:SetWaypoint 'Building Tools by F3X';
1342 end);
1343end;
1344
1345
1346------------------------------------------
1347-- Provide an interface color picker
1348-- system
1349------------------------------------------
1350ColorPicker = {
1351
1352 -- Keep some state data
1353 ["enabled"] = false;
1354 ["callback"] = nil;
1355 ["track_mouse"] = nil;
1356 ["hue"] = 0;
1357 ["saturation"] = 1;
1358 ["value"] = 1;
1359
1360 -- Keep the current GUI here
1361 ["GUI"] = nil;
1362
1363 -- Keep temporary, disposable connections here
1364 ["Connections"] = {};
1365
1366 -- Provide an interface to the functions
1367 ["start"] = function ( self, callback, start_color )
1368
1369 -- Replace any existing color pickers
1370 if self.enabled then
1371 self:cancel();
1372 end;
1373 self.enabled = true;
1374
1375 -- Create the GUI
1376 self.GUI = Tool.Interfaces.BTHSVColorPicker:Clone();
1377 self.GUI.Parent = UI;
1378
1379 -- Register the callback function for when we're done here
1380 self.callback = callback;
1381
1382 -- Update the GUI
1383 local start_color = start_color or Color3.new( 1, 0, 0 );
1384 self:_changeColor(Support.RGBToHSV(start_color.r, start_color.g, start_color.b));
1385
1386 -- Add functionality to the GUI's interactive elements
1387 table.insert( self.Connections, self.GUI.HueSaturation.MouseButton1Down:connect( function ( x, y )
1388 self.track_mouse = 'hue-saturation';
1389 self:_onMouseMove( x, y );
1390 end ) );
1391
1392 table.insert( self.Connections, self.GUI.HueSaturation.MouseButton1Up:connect( function ()
1393 self.track_mouse = nil;
1394 end ) );
1395
1396 table.insert( self.Connections, self.GUI.MouseMoved:connect( function ( x, y )
1397 self:_onMouseMove( x, y );
1398 end ) );
1399
1400 table.insert( self.Connections, self.GUI.Value.MouseButton1Down:connect( function ( x, y )
1401 self.track_mouse = 'value';
1402 self:_onMouseMove( x, y );
1403 end ) );
1404
1405 table.insert( self.Connections, self.GUI.Value.MouseButton1Up:connect( function ()
1406 self.track_mouse = nil;
1407 end ) );
1408
1409 table.insert( self.Connections, self.GUI.OkButton.MouseButton1Up:connect( function ()
1410 self:finish();
1411 end ) );
1412
1413 table.insert( self.Connections, self.GUI.CancelButton.MouseButton1Up:connect( function ()
1414 self:cancel();
1415 end ) );
1416
1417 table.insert( self.Connections, self.GUI.HueOption.Input.TextButton.MouseButton1Down:connect( function ()
1418 self.GUI.HueOption.Input.TextBox:CaptureFocus();
1419 end ) );
1420 table.insert( self.Connections, self.GUI.HueOption.Input.TextBox.FocusLost:connect( function ( enter_pressed )
1421 local potential_new = tonumber( self.GUI.HueOption.Input.TextBox.Text );
1422 if potential_new then
1423 if potential_new > 360 then
1424 potential_new = 360;
1425 elseif potential_new < 0 then
1426 potential_new = 0;
1427 end;
1428 self:_changeColor( potential_new, self.saturation, self.value );
1429 else
1430 self:_updateGUI();
1431 end;
1432 end ) );
1433
1434 table.insert( self.Connections, self.GUI.SaturationOption.Input.TextButton.MouseButton1Down:connect( function ()
1435 self.GUI.SaturationOption.Input.TextBox:CaptureFocus();
1436 end ) );
1437 table.insert( self.Connections, self.GUI.SaturationOption.Input.TextBox.FocusLost:connect( function ( enter_pressed )
1438 local potential_new = tonumber( ( self.GUI.SaturationOption.Input.TextBox.Text:gsub( '%%', '' ) ) );
1439 if potential_new then
1440 if potential_new > 100 then
1441 potential_new = 100;
1442 elseif potential_new < 0 then
1443 potential_new = 0;
1444 end;
1445 self:_changeColor( self.hue, potential_new / 100, self.value );
1446 else
1447 self:_updateGUI();
1448 end;
1449 end ) );
1450
1451 table.insert( self.Connections, self.GUI.ValueOption.Input.TextButton.MouseButton1Down:connect( function ()
1452 self.GUI.ValueOption.Input.TextBox:CaptureFocus();
1453 end ) );
1454 table.insert( self.Connections, self.GUI.ValueOption.Input.TextBox.FocusLost:connect( function ( enter_pressed )
1455 local potential_new = tonumber( ( self.GUI.ValueOption.Input.TextBox.Text:gsub( '%%', '' ) ) );
1456 if potential_new then
1457 if potential_new < 0 then
1458 potential_new = 0;
1459 elseif potential_new > 100 then
1460 potential_new = 100;
1461 end;
1462 self:_changeColor( self.hue, self.saturation, potential_new / 100 );
1463 else
1464 self:_updateGUI();
1465 end;
1466 end ) );
1467
1468 end;
1469
1470 ["_onMouseMove"] = function ( self, x, y )
1471 if not self.track_mouse then
1472 return;
1473 end;
1474
1475 if self.track_mouse == 'hue-saturation' then
1476 -- Calculate the mouse position relative to the graph
1477 local graph_x, graph_y = x - self.GUI.HueSaturation.AbsolutePosition.x, y - self.GUI.HueSaturation.AbsolutePosition.y;
1478
1479 -- Make sure we're not going out of bounds
1480 if graph_x < 0 then
1481 graph_x = 0;
1482 elseif graph_x > self.GUI.HueSaturation.AbsoluteSize.x then
1483 graph_x = self.GUI.HueSaturation.AbsoluteSize.x;
1484 end;
1485 if graph_y < 0 then
1486 graph_y = 0;
1487 elseif graph_y > self.GUI.HueSaturation.AbsoluteSize.y then
1488 graph_y = self.GUI.HueSaturation.AbsoluteSize.y;
1489 end;
1490
1491 -- Calculate the new color and change it
1492 self:_changeColor( 359 * graph_x / 209, 1 - graph_y / 200, self.value );
1493
1494 elseif self.track_mouse == 'value' then
1495 -- Calculate the mouse position relative to the value bar
1496 local bar_y = y - self.GUI.Value.AbsolutePosition.y;
1497
1498 -- Make sure we're not going out of bounds
1499 if bar_y < 0 then
1500 bar_y = 0;
1501 elseif bar_y > self.GUI.Value.AbsoluteSize.y then
1502 bar_y = self.GUI.Value.AbsoluteSize.y;
1503 end;
1504
1505 -- Calculate the new color and change it
1506 self:_changeColor( self.hue, self.saturation, 1 - bar_y / 200 );
1507 end;
1508 end;
1509
1510 ["_changeColor"] = function ( self, hue, saturation, value )
1511 if hue ~= hue then
1512 hue = 359;
1513 end;
1514 self.hue = hue;
1515 self.saturation = saturation == 0 and 0.01 or saturation;
1516 self.value = value;
1517 self:_updateGUI();
1518 end;
1519
1520 ["_updateGUI"] = function ( self )
1521
1522 self.GUI.HueSaturation.Cursor.Position = UDim2.new( 0, 209 * self.hue / 360 - 8, 0, ( 1 - self.saturation ) * 200 - 8 );
1523 self.GUI.Value.Cursor.Position = UDim2.new( 0, -2, 0, ( 1 - self.value ) * 200 - 8 );
1524
1525 local color = Color3.new(Support.HSVToRGB(self.hue, self.saturation, self.value));
1526 self.GUI.ColorDisplay.BackgroundColor3 = color;
1527 self.GUI.Value.ColorBG.BackgroundColor3 = Color3.new(Support.HSVToRGB(self.hue, self.saturation, 1));
1528
1529 self.GUI.HueOption.Bar.BackgroundColor3 = color;
1530 self.GUI.SaturationOption.Bar.BackgroundColor3 = color;
1531 self.GUI.ValueOption.Bar.BackgroundColor3 = color;
1532
1533 self.GUI.HueOption.Input.TextBox.Text = math.floor( self.hue );
1534 self.GUI.SaturationOption.Input.TextBox.Text = math.floor( self.saturation * 100 ) .. "%";
1535 self.GUI.ValueOption.Input.TextBox.Text = math.floor( self.value * 100 ) .. "%";
1536
1537 end;
1538
1539 ["finish"] = function ( self )
1540
1541 if not self.enabled then
1542 return;
1543 end;
1544
1545 -- Remove the GUI
1546 if self.GUI then
1547 self.GUI:Destroy();
1548 end;
1549 self.GUI = nil;
1550 self.track_mouse = nil;
1551
1552 -- Disconnect all temporary connections
1553 for connection_index, connection in pairs( self.Connections ) do
1554 connection:disconnect();
1555 self.Connections[connection_index] = nil;
1556 end;
1557
1558 -- Call the callback function that was provided to us
1559 self.callback( self.hue, self.saturation, self.value );
1560 self.callback = nil;
1561
1562 self.enabled = false;
1563
1564 end;
1565
1566 ["cancel"] = function ( self )
1567
1568 if not self.enabled then
1569 return;
1570 end;
1571
1572 -- Remove the GUI
1573 if self.GUI then
1574 self.GUI:Destroy();
1575 end;
1576 self.GUI = nil;
1577 self.track_mouse = nil;
1578
1579 -- Disconnect all temporary connections
1580 for connection_index, connection in pairs( self.Connections ) do
1581 connection:disconnect();
1582 self.Connections[connection_index] = nil;
1583 end;
1584
1585 -- Call the callback function that was provided to us
1586 self.callback();
1587 self.callback = nil;
1588
1589 self.enabled = false;
1590
1591 end;
1592
1593};
1594
1595------------------------------------------
1596-- Provide an interface to the
1597-- import/export system
1598------------------------------------------
1599IE = {
1600
1601 ["export"] = function ()
1602
1603 -- Make sure there's actually items to export
1604 if #Selection.Items == 0 then
1605 return;
1606 end;
1607
1608 -- Create the export dialog
1609 local Dialog = Tool.Interfaces.BTExportDialog:Clone();
1610 Dialog.Loading.Size = UDim2.new( 1, 0, 0, 0 );
1611 Dialog.Parent = UI;
1612 Dialog.Loading:TweenSize( UDim2.new( 1, 0, 0, 80 ), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25 );
1613 Dialog.Loading.CloseButton.MouseButton1Up:connect( function ()
1614 Dialog:Destroy();
1615 end );
1616
1617 -- Send the export request
1618 local RequestSuccess, RequestError, ParseSuccess, ParsedData = Tool.ExportInterface.Export:InvokeServer(Selection.Items);
1619
1620 -- Handle known errors for which we have a suggestion
1621 if not RequestSuccess and (RequestError == 'Http requests are not enabled' or RequestError == 'Http requests can only be executed by game server') then
1622
1623 -- Communicate failure
1624 Dialog.Loading.TextLabel.Text = 'Upload failed, see message(s)';
1625 Dialog.Loading.CloseButton.Text = 'Okay!';
1626
1627 -- Show any warnings that might help the user understand
1628 StartupNotificationsShown = false;
1629 ShowStartupNotifications();
1630
1631 -- Handle unknown errors
1632 elseif not RequestSuccess then
1633
1634 -- Just tell them there was an unknown error
1635 Dialog.Loading.TextLabel.Text = 'Upload failed (unknown request error)';
1636 Dialog.Loading.CloseButton.Text = 'Okay :(';
1637
1638 -- Show any warnings that might help the user figure it out
1639 -- (e.g. outdated version notification)
1640 StartupNotificationsShown = false;
1641 ShowStartupNotifications();
1642
1643 -- Handle successful requests without proper responses
1644 elseif RequestSuccess and (not ParseSuccess or not ParsedData.success) then
1645
1646 -- Just tell them there was an unknown error
1647 Dialog.Loading.TextLabel.Text = 'Upload failed (unknown processing error)';
1648 Dialog.Loading.CloseButton.Text = 'Okay :(';
1649
1650 -- Show any warnings that might help the user figure it out
1651 -- (e.g. outdated version notification)
1652 StartupNotificationsShown = false;
1653 ShowStartupNotifications();
1654
1655 -- Handle completely successful requests
1656 elseif RequestSuccess and ParseSuccess then
1657
1658 print( "[Building Tools by F3X] Uploaded Export: " .. ParsedData.id );
1659
1660 -- Display the successful export GUI with the creation ID
1661 Dialog.Loading.Visible = false;
1662 Dialog.Info.Size = UDim2.new( 1, 0, 0, 0 );
1663 Dialog.Info.CreationID.Text = ParsedData.id;
1664 Dialog.Info.Visible = true;
1665 Dialog.Info:TweenSize( UDim2.new( 1, 0, 0, 75 ), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25 );
1666 Dialog.Tip.Size = UDim2.new( 1, 0, 0, 0 );
1667 Dialog.Tip.Visible = true;
1668 Dialog.Tip:TweenSize( UDim2.new( 1, 0, 0, 30 ), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25 );
1669 Dialog.Close.Size = UDim2.new( 1, 0, 0, 0 );
1670 Dialog.Close.Visible = true;
1671 Dialog.Close:TweenSize( UDim2.new( 1, 0, 0, 20 ), Enum.EasingDirection.Out, Enum.EasingStyle.Quad, 0.25 );
1672 Dialog.Close.Button.MouseButton1Up:connect( function ()
1673 Dialog:Destroy();
1674 end );
1675
1676 -- Play a confirmation sound
1677 local Sound = RbxUtility.Create "Sound" {
1678 Name = "BTActionCompletionSound";
1679 Pitch = 1.5;
1680 SoundId = Assets.ActionCompletionSound;
1681 Volume = 1;
1682 Parent = Player or SoundService;
1683 };
1684 Sound:Play();
1685 Sound:Destroy();
1686
1687 end;
1688
1689 end;
1690
1691};
1692
1693
1694------------------------------------------
1695-- Prepare the dock UI
1696------------------------------------------
1697
1698Tooltips = {};
1699
1700-- Create the main GUI
1701Dock = Tool.Interfaces.BTDockGUI:Clone();
1702Dock.Parent = UI;
1703Dock.Visible = false;
1704
1705-- Add functionality to each tool button
1706function RegisterToolButton( ToolButton )
1707 -- Provides functionality to `ToolButton`
1708
1709 -- Get the tool name and the tool
1710 local tool_name = ToolButton.Name:match( "(.+)Button" );
1711
1712 if tool_name then
1713
1714 -- Create the click connection
1715 ToolButton.MouseButton1Up:connect( function ()
1716 local Tool = Tools[tool_name];
1717 if Tool then
1718 equipTool( Tool );
1719 end;
1720 end );
1721
1722 ToolButton.MouseEnter:connect( function ()
1723 local Tooltip = Tooltips[tool_name];
1724 if Tooltip then
1725 Tooltip:focus( 'button' );
1726 end;
1727 end );
1728
1729 ToolButton.MouseLeave:connect( function ()
1730 local Tooltip = Tooltips[tool_name];
1731 if Tooltip then
1732 Tooltip:unfocus( 'button' );
1733 end;
1734 end );
1735
1736 end;
1737end;
1738for _, ToolButton in pairs( Dock.ToolButtons:GetChildren() ) do
1739 RegisterToolButton( ToolButton );
1740end;
1741
1742-- Prepare the tooltips
1743function RegisterTooltip( Tooltip )
1744 local tool_name = Tooltip.Name:match( "(.+)Info" );
1745
1746 Tooltips[tool_name] = {
1747
1748 GUI = Tooltip;
1749
1750 button_focus = false;
1751 tooltip_focus = false;
1752
1753 focus = function ( self, source )
1754 if Dock.HelpInfo.Visible then
1755 return;
1756 end;
1757 if source == 'button' then
1758 self.button_focus = true;
1759 elseif source == 'tooltip' then
1760 self.tooltip_focus = true;
1761 end;
1762 for _, Tooltip in pairs( Dock.Tooltips:GetChildren() ) do
1763 Tooltip.Visible = false;
1764 end;
1765 self.GUI.Visible = true;
1766 end;
1767
1768 unfocus = function ( self, source )
1769 if source == 'button' then
1770 self.button_focus = false;
1771 elseif source == 'tooltip' then
1772 self.tooltip_focus = false;
1773 end;
1774 if not self.button_focus and not self.tooltip_focus then
1775 self.GUI.Visible = false;
1776 end;
1777 end;
1778
1779 };
1780
1781 -- Make it disappear after it's out of mouse focus
1782 Tooltip.MouseEnter:connect( function ()
1783 Tooltips[tool_name]:focus( 'tooltip' );
1784 end );
1785 Tooltip.MouseLeave:connect( function ()
1786 Tooltips[tool_name]:unfocus( 'tooltip' );
1787 end );
1788
1789 -- Create the scrolling container
1790 local ScrollingContainer = Gloo.ScrollingContainer( true, false, 15 );
1791 ScrollingContainer.GUI.Parent = Tooltip;
1792
1793 -- Put the tooltip content in the container
1794 for _, Child in pairs( Tooltip.Content:GetChildren() ) do
1795 Child.Parent = ScrollingContainer.Container;
1796 end;
1797 ScrollingContainer.GUI.Size = Dock.Tooltips.Size;
1798 ScrollingContainer.Container.Size = Tooltip.Content.Size;
1799 ScrollingContainer.Boundary.Size = Dock.Tooltips.Size;
1800 ScrollingContainer.Boundary.BackgroundTransparency = 1;
1801 Tooltip.Content:Destroy();
1802
1803end;
1804for _, Tooltip in pairs( Dock.Tooltips:GetChildren() ) do
1805 RegisterTooltip( Tooltip );
1806end;
1807
1808-- Create the scrolling container for the help tooltip
1809local ScrollingContainer = Gloo.ScrollingContainer( true, false, 15 );
1810ScrollingContainer.GUI.Parent = Dock.HelpInfo;
1811
1812-- Put the help tooltip content in the container
1813for _, Child in pairs( Dock.HelpInfo.Content:GetChildren() ) do
1814 Child.Parent = ScrollingContainer.Container;
1815end;
1816ScrollingContainer.GUI.Size = Dock.HelpInfo.Size;
1817ScrollingContainer.Container.Size = Dock.HelpInfo.Content.Size;
1818ScrollingContainer.Boundary.Size = Dock.HelpInfo.Size;
1819ScrollingContainer.Boundary.BackgroundTransparency = 1;
1820Dock.HelpInfo.Content:Destroy();
1821
1822-- Add functionality to the other GUI buttons
1823Dock.SelectionButtons.UndoButton.MouseButton1Up:connect( function ()
1824 History:undo();
1825end );
1826Dock.SelectionButtons.RedoButton.MouseButton1Up:connect( function ()
1827 History:redo();
1828end );
1829Dock.SelectionButtons.DeleteButton.MouseButton1Up:connect( function ()
1830 deleteSelection();
1831end );
1832Dock.SelectionButtons.CloneButton.MouseButton1Up:connect( function ()
1833 cloneSelection();
1834end );
1835Dock.SelectionButtons.ExportButton.MouseButton1Up:connect( function ()
1836 IE:export();
1837end );
1838Dock.SelectionButtons.GroupsButton.MouseButton1Up:connect( function ()
1839 Groups:ToggleUI();
1840end );
1841Dock.InfoButtons.HelpButton.MouseButton1Up:connect( function ()
1842 toggleHelp();
1843end );
1844
1845-- Shade the buttons according to whether they'll function or not
1846Selection.Changed:connect( function ()
1847
1848 -- If there are items, they should be active
1849 if #Selection.Items > 0 then
1850 Dock.SelectionButtons.DeleteButton.Image = Assets.DeleteActiveDecal;
1851 Dock.SelectionButtons.CloneButton.Image = Assets.CloneActiveDecal;
1852 Dock.SelectionButtons.ExportButton.Image = Assets.ExportActiveDecal;
1853
1854 -- If there aren't items, they shouldn't be active
1855 else
1856 Dock.SelectionButtons.DeleteButton.Image = Assets.DeleteInactiveDecal;
1857 Dock.SelectionButtons.CloneButton.Image = Assets.CloneInactiveDecal;
1858 Dock.SelectionButtons.ExportButton.Image = Assets.ExportInactiveDecal;
1859 end;
1860
1861end );
1862
1863-- Make the selection/info buttons display tooltips upon hovering over them
1864for _, SelectionButton in pairs( Dock.SelectionButtons:GetChildren() ) do
1865 SelectionButton.MouseEnter:connect( function ()
1866 if SelectionButton:FindFirstChild( 'Tooltip' ) then
1867 SelectionButton.Tooltip.Visible = true;
1868 end;
1869 end );
1870 SelectionButton.MouseLeave:connect( function ()
1871 if SelectionButton:FindFirstChild( 'Tooltip' ) then
1872 SelectionButton.Tooltip.Visible = false;
1873 end;
1874 end );
1875end;
1876Dock.InfoButtons.HelpButton.MouseEnter:connect( function ()
1877 Dock.InfoButtons.HelpButton.Tooltip.Visible = true;
1878end );
1879Dock.InfoButtons.HelpButton.MouseLeave:connect( function ()
1880 Dock.InfoButtons.HelpButton.Tooltip.Visible = false;
1881end );
1882
1883History.Changed:connect( function ()
1884
1885 -- If there are any records
1886 if #History.Data > 0 then
1887
1888 -- If we're at the beginning
1889 if History.index == 0 then
1890 Dock.SelectionButtons.UndoButton.Image = Assets.UndoInactiveDecal;
1891 Dock.SelectionButtons.RedoButton.Image = Assets.RedoActiveDecal;
1892
1893 -- If we're at the end
1894 elseif History.index == #History.Data then
1895 Dock.SelectionButtons.UndoButton.Image = Assets.UndoActiveDecal;
1896 Dock.SelectionButtons.RedoButton.Image = Assets.RedoInactiveDecal;
1897
1898 -- If we're neither at the beginning or the end
1899 else
1900 Dock.SelectionButtons.UndoButton.Image = Assets.UndoActiveDecal;
1901 Dock.SelectionButtons.RedoButton.Image = Assets.RedoActiveDecal;
1902 end;
1903
1904 -- If there are no records
1905 else
1906 Dock.SelectionButtons.UndoButton.Image = Assets.UndoInactiveDecal;
1907 Dock.SelectionButtons.RedoButton.Image = Assets.RedoInactiveDecal;
1908 end;
1909
1910end );
1911
1912
1913------------------------------------------
1914-- An interface for the group system
1915------------------------------------------
1916Groups = {
1917
1918 -- A container for the groups
1919 Data = {};
1920
1921 -- Create the group manager UI
1922 UI = Tool.Interfaces.BTGroupsGUI:Clone();
1923
1924 -- Provide an event to track new groups
1925 GroupAdded = Support.CreateSignal();
1926
1927 NewGroup = function ( Groups )
1928 local Group = {
1929 Name = 'Group ' .. ( #Groups.Data + 1 );
1930 Items = {};
1931 Ignoring = false;
1932 Changed = Support.CreateSignal();
1933 Updated = Support.CreateSignal();
1934
1935 Rename = function ( Group, NewName )
1936 Group.Name = NewName;
1937 Group.Changed:Fire();
1938 end;
1939
1940 SetIgnore = function ( Group, NewIgnoringStatus )
1941 Group.Ignoring = NewIgnoringStatus;
1942 Group.Changed:Fire();
1943 end;
1944
1945 Update = function ( Group, NewItems )
1946 -- Set the new items
1947 Group.Items = Support.CloneTable(NewItems);
1948 Group.Updated:Fire();
1949 end;
1950
1951 Select = function ( Group, Multiselecting )
1952 if not Multiselecting then
1953 Selection:clear();
1954 end;
1955 for _, Item in pairs( Group.Items ) do
1956 Selection:add( Item );
1957 end;
1958 end;
1959 };
1960 table.insert( Groups.Data, Group );
1961 Groups.GroupAdded:Fire( Group );
1962 return Group;
1963 end;
1964
1965 ToggleUI = function ( Groups )
1966 Groups.UI.Visible = not Groups.UI.Visible;
1967 end;
1968
1969 IsPartIgnored = function ( Groups, Part )
1970 -- Returns whether `Part` should be ignored in selection
1971
1972 -- Check for any groups that ignore their parts and if `Part` is in any of them
1973 for _, Group in pairs( Groups.Data ) do
1974 if Group.Ignoring and #Support.FindTableOccurrences(Group.Items, Part) > 0 then
1975 return true;
1976 end;
1977 end;
1978
1979 -- If no groups come up, it's not an ignored part
1980 return false;
1981 end;
1982};
1983
1984-- Add the group manager UI to the main UI
1985Groups.UI.Visible = false;
1986Groups.UI.Parent = Dock;
1987
1988-- Prepare the functionality of the group manager UI
1989Groups.UI.Title.CreateButton.MouseButton1Click:connect( function ()
1990 local Group = Groups:NewGroup();
1991 Group:Update( Selection.Items );
1992end );
1993
1994Groups.GroupAdded:Connect( function ( Group )
1995 local GroupButton = Groups.UI.Templates.GroupButton:Clone();
1996 GroupButton.Position = UDim2.new( 0, 0, 0, 26 * #Groups.UI.GroupList:GetChildren() );
1997 GroupButton.Parent = Groups.UI.GroupList;
1998 GroupButton.GroupName.Text = Group.Name;
1999 GroupButton.GroupNamer.Text = Group.Name;
2000
2001 Groups.UI.GroupList.CanvasSize = UDim2.new( 1, -10, 0, 26 * #Groups.UI.GroupList:GetChildren() );
2002
2003 -- Adjust the tooltip caption on the ignore button
2004 GroupButton.IgnoreButton.RightTooltip.Text.Text = Group.Ignoring and 'UNIGNORE' or 'IGNORE';
2005
2006 GroupButton.GroupName.MouseButton1Click:connect( function ()
2007 Group:Select( ActiveKeys[47] or ActiveKeys[48] );
2008 end );
2009
2010 Group.Changed:Connect( function ()
2011 GroupButton.GroupName.Text = Group.Name;
2012 GroupButton.GroupNamer.Text = Group.Name;
2013 GroupButton.IgnoreButton.Image = Group.Ignoring and Assets.GroupLockIcon or Assets.GroupUnlockIcon;
2014
2015 -- Change the tooltip caption on the ignore button
2016 GroupButton.IgnoreButton.RightTooltip.Text.Text = Group.Ignoring and 'UNIGNORE' or 'IGNORE';
2017 end );
2018
2019 Group.Updated:connect( function ()
2020 GroupButton.UpdateButton.Image = Assets.GroupUpdateOKIcon;
2021 coroutine.wrap( function()
2022 wait( 1 );
2023 GroupButton.UpdateButton.Image = Assets.GroupUpdateIcon;
2024 end )();
2025 end );
2026
2027 GroupButton.EditButton.MouseButton1Click:connect( function ()
2028 GroupButton.GroupName.Visible = false;
2029 GroupButton.GroupNamer.Visible = true;
2030 GroupButton.GroupNamer:CaptureFocus();
2031 end );
2032
2033 GroupButton.GroupNamer.FocusLost:connect( function ( EnterPressed )
2034 if EnterPressed then
2035 Group:Rename( GroupButton.GroupNamer.Text );
2036 end;
2037 GroupButton.GroupNamer.Visible = false;
2038 GroupButton.GroupNamer.Text = Group.Name;
2039 GroupButton.GroupName.Visible = true;
2040 end );
2041
2042 -- Toggle ignoring when the ignore button is clicked
2043 GroupButton.IgnoreButton.MouseButton1Click:connect( function ()
2044 Group:SetIgnore( not Group.Ignoring );
2045 end );
2046
2047 GroupButton.UpdateButton.MouseButton1Click:connect( function ()
2048 Group:Update( Selection.Items );
2049 end );
2050
2051 -- Pop up tooltips when the buttons are hovered over
2052 local ButtonsWithTooltips = { GroupButton.UpdateButton, GroupButton.EditButton, GroupButton.IgnoreButton, GroupButton.GroupNameArea };
2053 for _, Button in pairs( ButtonsWithTooltips ) do
2054 local Tooltip = Button:FindFirstChild 'LeftTooltip' or Button:FindFirstChild 'RightTooltip';
2055 if Tooltip then
2056 Button.InputBegan:connect( function ( InputData )
2057 if InputData.UserInputType == Enum.UserInputType.MouseMovement then
2058 Tooltip.Visible = true;
2059 end;
2060 end );
2061 Button.InputEnded:connect( function ( InputData )
2062 if InputData.UserInputType == Enum.UserInputType.MouseMovement then
2063 Tooltip.Visible = false;
2064 end;
2065 end );
2066 end;
2067 end;
2068end );
2069
2070
2071------------------------------------------
2072-- Attach tool event listeners
2073------------------------------------------
2074
2075function equipBT( CurrentMouse )
2076
2077 Mouse = CurrentMouse;
2078
2079 -- Enable the move tool if there's no tool currently enabled
2080 if not CurrentTool then
2081 equipTool( Tools.Move );
2082 end;
2083
2084 if not TargetBox then
2085 TargetBox = Instance.new( "SelectionBox", UI );
2086 TargetBox.Name = "BTTargetBox";
2087 TargetBox.Color = BrickColor.new( "Institutional white" );
2088 TargetBox.Transparency = 0.5;
2089 end;
2090
2091 -- Enable any temporarily-disabled selection boxes
2092 for _, SelectionBox in pairs( SelectionBoxes ) do
2093 SelectionBox.Parent = UI;
2094 end;
2095
2096 -- Update the internal selection if this is a plugin
2097 if ToolType == 'plugin' then
2098 for _, Item in pairs( SelectionService:Get() ) do
2099 Selection:add( Item );
2100 end;
2101 end;
2102
2103 -- Call the `Equipped` listener of the current tool
2104 if CurrentTool and CurrentTool.Listeners.Equipped then
2105 CurrentTool.Listeners.Equipped();
2106 end;
2107
2108 -- Show the dock
2109 Dock.Visible = true;
2110
2111 -- Display any startup notifications
2112 coroutine.wrap( ShowStartupNotifications )();
2113
2114 table.insert( Connections, Mouse.KeyDown:connect( function ( key )
2115
2116 local key = key:lower();
2117 local key_code = key:byte();
2118
2119 -- Provide the abiltiy to delete via the shift + X key combination
2120 if ActiveKeys[47] or ActiveKeys[48] and key == "x" then
2121 deleteSelection();
2122 return;
2123 end;
2124
2125 -- Provide the ability to clone via the shift + C key combination
2126 if ActiveKeys[47] or ActiveKeys[48] and key == "c" then
2127 cloneSelection();
2128 return;
2129 end;
2130
2131 -- Undo if shift+z is pressed
2132 if key == "z" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2133 History:undo();
2134 return;
2135
2136 -- Redo if shift+y is pressed
2137 elseif key == "y" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2138 History:redo();
2139 return;
2140 end;
2141
2142 -- Serialize and dump selection to logs if shift+p is pressed
2143 if key == "p" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2144 IE:export();
2145 return;
2146 end;
2147
2148 -- Perform a prism selection if shift + k is pressed
2149 if key == "k" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2150 prismSelect();
2151 return;
2152 end;
2153
2154 -- Clear the selection if shift + r is pressed
2155 if key == "r" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2156 Selection:clear();
2157 return;
2158 end;
2159
2160 -- Show the groups GUI when shift + g is pressed
2161 if key == "g" and ( ActiveKeys[47] or ActiveKeys[48] ) then
2162 Groups:ToggleUI();
2163 return;
2164 end;
2165
2166 -- Select all parts within the parent of the focused part
2167 -- when [ is pressed
2168 if key == "[" then
2169
2170 -- Make sure we have a part that's focused
2171 local FocusedPart = Selection.Last;
2172 if not FocusedPart then
2173 return;
2174 end;
2175
2176 -- Make sure the part isn't a child of Workspace,
2177 -- since that would cause us to select everything
2178 if FocusedPart.Parent == Workspace then
2179 return;
2180 end;
2181
2182 -- Clear the selection (or not), depending on whether
2183 -- it's part of a multiselection
2184 if not (ActiveKeys[47] or ActiveKeys[48]) then
2185 Selection:clear();
2186 end;
2187
2188 -- Select all the parts within the parent of the focused part
2189 local SearchField = Support.GetAllDescendants(FocusedPart.Parent);
2190 for _, Item in pairs(SearchField) do
2191 Selection:add(Item);
2192 end;
2193
2194 -- Select the part itself
2195 Selection:add(FocusedPart);
2196
2197 return;
2198 end;
2199
2200 if key == "z" then
2201 equipTool( Tools.Move );
2202
2203 elseif key == "x" then
2204 equipTool( Tools.Resize );
2205
2206 elseif key == "c" then
2207 equipTool( Tools.Rotate );
2208
2209 elseif key == "v" then
2210 equipTool( Tools.Paint );
2211
2212 elseif key == "b" then
2213 equipTool( Tools.Surface );
2214
2215 elseif key == "n" then
2216 equipTool( Tools.Material );
2217
2218 elseif key == "m" then
2219 equipTool( Tools.Anchor );
2220
2221 elseif key == "k" then
2222 equipTool( Tools.Collision );
2223
2224 elseif key == "j" then
2225 equipTool( Tools.NewPart );
2226
2227 elseif key == "h" then
2228 equipTool( Tools.Mesh );
2229
2230 elseif key == "g" then
2231 equipTool( Tools.Texture );
2232
2233 elseif key == "f" then
2234 equipTool( Tools.Weld );
2235
2236 elseif key == "u" then
2237 equipTool( Tools.Lighting );
2238
2239 elseif key == "p" then
2240 equipTool( Tools.Decorate );
2241
2242 end;
2243
2244 ActiveKeys[key_code] = key_code;
2245 ActiveKeys[key] = key;
2246
2247 -- If it's now in multiselection mode, update `selecting`
2248 -- (these are the left/right ctrl & shift keys)
2249 if ActiveKeys[47] or ActiveKeys[48] or ActiveKeys[49] or ActiveKeys[50] then
2250 selecting = ActiveKeys[47] or ActiveKeys[48] or ActiveKeys[49] or ActiveKeys[50];
2251 end;
2252
2253 end ) );
2254
2255 table.insert( Connections, Mouse.KeyUp:connect( function ( key )
2256
2257 local key = key:lower();
2258 local key_code = key:byte();
2259
2260 ActiveKeys[key_code] = nil;
2261 ActiveKeys[key] = nil;
2262
2263 -- If it's no longer in multiselection mode, update `selecting` & related values
2264 if selecting and not ActiveKeys[selecting] then
2265 selecting = false;
2266 if Select2D.enabled then
2267 Select2D:select();
2268 Select2D:finish();
2269 end;
2270 end;
2271
2272 -- Fire tool listeners
2273 if CurrentTool and CurrentTool.Listeners.KeyUp then
2274 CurrentTool.Listeners.KeyUp( key );
2275 end;
2276
2277 end ) );
2278
2279 table.insert( Connections, UserInputService.InputEnded:connect( function ( InputData )
2280
2281 if InputData.UserInputType == Enum.UserInputType.MouseButton1 then
2282 clicking = false;
2283
2284 -- Finish any ongoing 2D selection wherever the left mouse button is released
2285 if Select2D.enabled then
2286 Select2D:select();
2287 Select2D:finish();
2288 end;
2289 end;
2290
2291 end ) );
2292
2293 table.insert( Connections, Mouse.Button1Down:connect( function ()
2294
2295 clicking = true;
2296 click_x, click_y = Mouse.X, Mouse.Y;
2297
2298 -- If multiselection is, just add to the selection
2299 if selecting then
2300 return;
2301 end;
2302
2303 -- Fire tool listeners
2304 if CurrentTool and CurrentTool.Listeners.Button1Down then
2305 CurrentTool.Listeners.Button1Down();
2306 end;
2307
2308 end ) );
2309
2310 table.insert( Connections, Mouse.Move:connect( function ()
2311
2312 -- If the mouse has moved since it was clicked, start 2D selection mode
2313 if not override_selection and not Select2D.enabled and clicking and selecting and ( click_x ~= Mouse.X or click_y ~= Mouse.Y ) then
2314 Select2D:start();
2315 end;
2316
2317 -- If the target has changed, update the selectionbox appropriately
2318 if not override_selection and isSelectable( Mouse.Target ) and TargetBox.Adornee ~= Mouse.Target then
2319 TargetBox.Adornee = Mouse.Target;
2320 end;
2321
2322 -- When aiming at something invalid, don't highlight any targets
2323 if not override_selection and not isSelectable( Mouse.Target ) then
2324 TargetBox.Adornee = nil;
2325 end;
2326
2327 -- Fire tool listeners
2328 if CurrentTool and CurrentTool.Listeners.Move then
2329 CurrentTool.Listeners.Move();
2330 end;
2331
2332 if override_selection then
2333 override_selection = false;
2334 end;
2335
2336 end ) );
2337
2338 table.insert( Connections, Mouse.Button1Up:connect( function ()
2339
2340 clicking = false;
2341
2342 -- Make sure the person didn't accidentally miss a handle or something
2343 if not Select2D.enabled and ( Mouse.X ~= click_x or Mouse.Y ~= click_y ) then
2344 override_selection = true;
2345 end;
2346
2347 -- If the target when clicking was invalid then clear the selection (unless we're multi-selecting)
2348 if not override_selection and not selecting and not isSelectable( Mouse.Target ) then
2349 Selection:clear();
2350 end;
2351
2352 -- If multi-selecting, add to/remove from the selection
2353 if not override_selection and selecting then
2354
2355 -- If the item isn't already selected, add it to the selection
2356 if not Selection:find( Mouse.Target ) then
2357 if isSelectable( Mouse.Target ) then
2358 Selection:add( Mouse.Target );
2359 end;
2360
2361 -- If the item _is_ already selected, remove it from the selection
2362 else
2363 if ( Mouse.X == click_x and Mouse.Y == click_y ) and Selection:find( Mouse.Target ) then
2364 Selection:remove( Mouse.Target );
2365 end;
2366 end;
2367
2368 -- If not multi-selecting, replace the selection
2369 else
2370 if not override_selection and isSelectable( Mouse.Target ) then
2371 Selection:clear();
2372 Selection:add( Mouse.Target );
2373 end;
2374 end;
2375
2376 -- Fire tool listeners
2377 if CurrentTool and CurrentTool.Listeners.Button1Up then
2378 CurrentTool.Listeners.Button1Up();
2379 end;
2380
2381 if override_selection then
2382 override_selection = false;
2383 end;
2384
2385 end ) );
2386
2387 table.insert( Connections, Mouse.Button2Down:connect( function ()
2388 -- Fire tool listeners
2389 if CurrentTool and CurrentTool.Listeners.Button2Down then
2390 CurrentTool.Listeners.Button2Down();
2391 end;
2392 end ) );
2393
2394 table.insert( Connections, Mouse.Button2Up:connect( function ()
2395 -- Fire tool listeners
2396 if CurrentTool and CurrentTool.Listeners.Button2Up then
2397 CurrentTool.Listeners.Button2Up();
2398 end;
2399 end ) );
2400
2401end;
2402
2403function unequipBT()
2404
2405 Mouse = nil;
2406
2407 -- Remove the mouse target SelectionBox from `Player`
2408 if TargetBox then
2409 TargetBox:Destroy();
2410 TargetBox = nil;
2411 end;
2412
2413 -- Disable all the selection boxes temporarily
2414 for _, SelectionBox in pairs( SelectionBoxes ) do
2415 SelectionBox.Parent = nil;
2416 end;
2417
2418 -- Hide the dock
2419 Dock.Visible = false;
2420
2421 -- Disconnect temporary platform-related connections
2422 for connection_index, Connection in pairs( Connections ) do
2423 Connection:disconnect();
2424 Connections[connection_index] = nil;
2425 end;
2426
2427 -- Call the `Unequipped` listener of the current tool
2428 if CurrentTool and CurrentTool.Listeners.Unequipped then
2429 CurrentTool.Listeners.Unequipped();
2430 end;
2431
2432end;
2433
2434
2435------------------------------------------
2436-- Provide the platform's environment for
2437-- other tool scripts to extend upon
2438------------------------------------------
2439
2440local tool_list = {
2441 "Anchor",
2442 "Collision",
2443 "Material",
2444 "Mesh",
2445 "Move",
2446 "NewPart",
2447 "Paint",
2448 "Resize",
2449 "Rotate",
2450 "Surface",
2451 "Texture",
2452 "Weld",
2453 "Lighting",
2454 "Decorate"
2455};
2456
2457-- Make sure all the tool scripts are in the tool & deactivate them
2458for _, tool_name in pairs( tool_list ) do
2459 local script_name = "BT" .. tool_name .. "Tool";
2460 repeat wait() until script:FindFirstChild( script_name );
2461 script[script_name].Disabled = true;
2462end;
2463
2464-- Load the platform
2465if not _G.BTCoreEnv then
2466 _G.BTCoreEnv = {};
2467end;
2468_G.BTCoreEnv[Tool] = getfenv( 0 );
2469CoreReady = true;
2470
2471-- Reload the tool scripts
2472for _, tool_name in pairs( tool_list ) do
2473 local script_name = "BT" .. tool_name .. "Tool";
2474 script[script_name].Disabled = false;
2475end;
2476
2477-- Wait for all the tools to load
2478for _, tool_name in pairs( tool_list ) do
2479 if not Tools[tool_name] then
2480 repeat wait() until Tools[tool_name];
2481 end;
2482 repeat wait() until Tools[tool_name].Loaded;
2483end;
2484
2485-- Activate the plugin and tool connections
2486if ToolType == 'plugin' then
2487 local plugin_active = false;
2488 ToolbarButton.Click:connect( function ()
2489 if plugin_active then
2490 plugin_active = false;
2491 unequipBT();
2492 else
2493 plugin_active = true;
2494 plugin:Activate( true );
2495 equipBT( plugin:GetMouse() );
2496 end;
2497 end );
2498 plugin.Deactivation:connect( unequipBT );
2499
2500elseif ToolType == 'tool' then
2501 Tool.Equipped:connect( equipBT );
2502 Tool.Unequipped:connect( unequipBT );
2503end;
2504
2505
2506-- Provide a remote function allowing server-side code to
2507-- make the tool select the parts in a given model
2508(Tool:WaitForChild 'SelectModel').OnClientInvoke = function (Model)
2509
2510 -- Clear the existing selection
2511 Selection:clear();
2512
2513 -- Select all the parts within `Model` (filtered by Selection:add)
2514 local Descendants = Support.GetAllDescendants(Model);
2515 for _, Descendant in pairs(Descendants) do
2516 Selection:add(Descendant);
2517 end;
2518
2519end;