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