This repository has been archived by the owner on Jun 11, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 25
/
TerrainTools.rbxmx
6914 lines (6570 loc) · 243 KB
/
TerrainTools.rbxmx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
<External>null</External>
<External>nil</External>
<Item class="Model" referent="RBX3CC71DDB17D842A3B313B465F8D61360">
<Properties>
<CoordinateFrame name="ModelInPrimary">
<X>0</X>
<Y>0</Y>
<Z>0</Z>
<R00>1</R00>
<R01>0</R01>
<R02>0</R02>
<R10>0</R10>
<R11>1</R11>
<R12>0</R12>
<R20>0</R20>
<R21>0</R21>
<R22>1</R22>
</CoordinateFrame>
<string name="Name">Terrain Tools</string>
<Ref name="PrimaryPart">null</Ref>
</Properties>
<Item class="LocalScript" referent="RBX24C942C33E7244998199BC2CFBC8DAC6">
<Properties>
<bool name="Disabled">false</bool>
<Content name="LinkedSource"><null></null></Content>
<string name="Name">TerrainBrushScript</string>
<ProtectedString name="Source"><![CDATA[--Made by Stickmasterluke
local Terrain = workspace:WaitForChild('Terrain', 86400) or workspace:WaitForChild('Terrain')
while not Terrain.IsSmooth do
Terrain.Changed:wait()
end
local on = false
local setup = false
local currentTool = nil
local utilityModule = require(script.Parent.Utility)(plugin)
--[[
How to add a module:
1. Add the require to the modules table below.
2. Add a plugin button for it into the pluginButtons table, with the same index as used in the modules table.
How modules work
Your ModuleScript should return a table. The table can contain the following functions
On = This function will be called when your tool is selected. Will hand in the mouse object, and a Deselect function to turn it off.
Off = This function will be called when your tool is deselected.
operation = This function will be called when this toolset's brushing functionality is used
operation(centerPoint, materialsTable, occupanciesTable, resolution, selectionSize, strength, desiredMaterial, brushShape, minBounds, maxBounds)
]]
local modules = {
['Smooth'] = require(script.Parent.SmootherModule),
['Generate'] = utilityModule:GetUsingAsPlugin() and require(script.Parent.GenerationModule) or {},
['Region Editor'] = require(script.Parent.RegionEditorModule),
}
local buttonCreator = utilityModule:GetButtonCreator()
local plugins = {
{name = 'Generate',
button = utilityModule:GetUsingAsPlugin() and buttonCreator:CreateButton(
'Generate', --button title
'Generate landscapes of terrain.', --hover text
'http://www.roblox.com/asset/?id=236006872' --button image
),
studioOnly = true,
},
{name = 'Add',
button = buttonCreator:CreateButton(
'Add',
'Click and hold to add terrain.',
'http://www.roblox.com/asset/?id=225328572'
),
usesMaterials = true,
},
{name = 'Subtract',
button = buttonCreator:CreateButton(
'Subtract',
'Click and hold to remove terrain.',
'http://www.roblox.com/asset/?id=225328818'
),
},
{name = 'Paint',
button = buttonCreator:CreateButton(
'Paint',
'Paint the material of the terrain.',
'http://www.roblox.com/asset/?id=225328954'
),
usesMaterials = true,
},
{name = 'Grow',
button = buttonCreator:CreateButton(
'Grow',
'Click and hold to grow and expand terrain.',
'http://www.roblox.com/asset/?id=225329153'
),
usesMaterials = true,
},
{name = 'Erode',
button = buttonCreator:CreateButton(
'Erode',
'Click and hold to erode and remove terrain.',
'http://www.roblox.com/asset/?id=225329301'
),
},
{name = 'Smooth',
button = buttonCreator:CreateButton(
'Smooth',
'Brush to smooth out rough or jagged terrain.',
'http://www.roblox.com/asset/?id=225329641'
),
},
{name = 'Region Editor',
button = buttonCreator:CreateButton(
'Regions',
'Manipulate regions of smooth terrain.',
'http://www.roblox.com/asset/?id=240631063'
),
},
}
for i,tool in pairs(plugins) do
if not tool.studioOnly or (utilityModule:GetUsingAsPlugin() and tool.studioOnly) then
tool.button.Click:connect(function()
if not on or (currentTool ~= nil and tool ~= currentTool) then --if off or on but current tool isn't the desired tool, then select this tool.
if not setup then --I do this so that things only get set up when this plugin is used.
FirstTimeSetUp()
end
Selected(tool)
else
Deselected()
end
end)
if not utilityModule:GetUsingAsPlugin() then
if tool.button.Deselected then
tool.button.Deselected:connect(function()
Deselected()
end)
end
end
end
end
function FirstTimeSetUp()
setup = true
local changeHistory = utilityModule:GetUsingAsPlugin() and game:GetService('ChangeHistoryService') or nil
local terrain = game.Workspace.Terrain
local coreGui = utilityModule:GetCoreGui()
local gui = script.Parent:WaitForChild('TerrainBrushGui')
local guiFrame = gui:WaitForChild('Frame')
local closeButton = guiFrame:WaitForChild('CloseButton')
local titlelabel = guiFrame:WaitForChild('TitleLabel')
local checkBox1 = guiFrame:WaitForChild('CheckBox1')
local checkBox2 = guiFrame:WaitForChild('CheckBox2')
local checkBox3 = guiFrame:WaitForChild('CheckBox3')
local checkBox4 = guiFrame:WaitForChild('CheckBox4')
local toolTip1 = guiFrame:WaitForChild('ToolTip1')
local toolTip2 = guiFrame:WaitForChild('ToolTip2')
local label4 = guiFrame:WaitForChild('Label4')
local divider2 = guiFrame:WaitForChild('Divider2')
local library = assert(LoadLibrary('RbxGui'))
local mouse = utilityModule:GetMouse()
local userInput = game:GetService('UserInputService')
local prevCameraType = game.Workspace.CurrentCamera.CameraType
--SUB SETTINGS-- (Non-userfacing Settings)
local resolution = 4 --This is the size of voxels on Roblox. Why is this a variable? ;)
local minSelectionSize = 1
local maxSelectionSize = 16
local clickThreshold = .1
local toolTipShowTime = 3.5
local materialsTable = require(script.Parent.MaterialsList)
local brushShapes = {
['Sphere'] = {
name = 'Sphere',
button = guiFrame:WaitForChild('ShapeButton1'),
image = 'http://www.roblox.com/asset/?id=225799533',
selectedImage = 'http://www.roblox.com/asset/?id=225801914',
},
['Box'] = {
name = 'Box',
button = guiFrame:WaitForChild('ShapeButton2'),
image = 'http://www.roblox.com/asset/?id=225799696',
selectedImage = 'http://www.roblox.com/asset/?id=225802254',
},
}
----------------
----SETTINGS---- (Interface Settings)
local selectionSize = 6
local strength = .5
local snapToGrid = false
local planeLock = false
local ignoreWater = true
local brushShape = 'Sphere'
local materialSelection = materialsTable[1]
local dynamicMaterial = false
----------------
----Variables----
local forcePlaneLock = false
local forceSnapToGrid = false
local forceDynamicMaterial = false
local forceDynamicMaterialTo = true
local isDynamic = false
local forceIgnoreWater = false
local forceIgnoreWaterTo = true
local isIgnoreWater = true
local forceMaterial = nil
local nearMaterial = nil
local selectionPart = nil
local selectionObject = nil
local gridLineParts = {}
local currentLoopTag = nil
local lastMainPoint = Vector3.new(0, 0, 0)
local click = false
local firstOperation = tick()
local downKeys = {}
local lastPlanePoint = Vector3.new(0, 0, 0)
local lastNormal = Vector3.new(0, 1, 0)
local lastCursorDistance = 300
local one256th = 1/256 --This should later be replaced with 0 once smooth terrain doesn't aproximate 1/256 to 0. This is causing small occupancies to become air
local toolTip1Change = nil
local toolTip2Change = nil
local materialAir = Enum.Material.Air
local materialWater = Enum.Material.Water
local ceil = math.ceil
local floor = math.floor
local abs = math.abs
local min = math.min
local max = math.max
local sqrt = math.sqrt
local sin = math.sin
local cos = math.cos
local pi = math.pi
---------------
local selectionSizeSlider, selectionSizeValue = library.CreateSlider(maxSelectionSize, 90, UDim2.new(1, -98, 0, 40))
selectionSizeSlider.Parent = guiFrame
selectionSizeValue.Changed:connect(function()
selectionSize = selectionSizeValue.Value
if selectionPart then
selectionPart.Size = Vector3.new(1, 1, 1) * selectionSize * resolution + Vector3.new(.1, .1, .1)
end
toolTip1.Visible = true
local currentToolTip1Change = {}
toolTip1Change = currentToolTip1Change
wait(toolTipShowTime)
if toolTip1Change == currentToolTip1Change then
toolTip1.Visible = false
end
end)
selectionSizeValue.Value = selectionSize
toolTip1.Visible = false
local strengthslider, strengthValue = library.CreateSlider(101, 90, UDim2.new(1, -98, 0, 65))
strengthslider.Parent = guiFrame
strengthValue.Changed:connect(function()
strength = (strengthValue.Value - 1) / 100
if selectionObject then
selectionObject.SurfaceTransparency = .95 - strength * .3
end
toolTip2.Visible = true
local currentToolTip2Change = {}
toolTip2Change = currentToolTip2Change
wait(toolTipShowTime)
if toolTip2Change == currentToolTip2Change then
toolTip2.Visible = false
end
end)
strengthValue.Value = strength * 100
toolTip2.Visible = false
function setBrushShape(newBrushShape)
brushShape = newBrushShape
for _,v in pairs(brushShapes) do
--v.button.Image = (newBrushShape == v) and v.selectedImage or v.image
v.button.ImageTransparency = (newBrushShape == v.name) and 0 or .5
v.button.ImageColor3 = (newBrushShape == v.name) and Color3.new(1,1,1) or Color3.new(.5,.5,.5)
end
clearSelection()
end
for _,v in pairs(brushShapes) do
v.button.MouseButton1Down:connect(function()
setBrushShape(v.name)
end)
end
local MakeToolTip
do
local ActiveToolTip = Instance.new("StringValue")
function MakeToolTip(Gui, Text)
local Name = tostring(Gui:GetFullName() .. Text)
local Frame = Instance.new("Frame", Gui)
Frame.BackgroundTransparency = 0.3
Frame.BackgroundColor3 = Color3.new(0, 0, 0)
Frame.Size = UDim2.new(0, 100, 0, 30)
Frame.Position = UDim2.new(0.8, 0, 0.8, 0)
Frame.SizeConstraint = "RelativeYY"
Frame.ZIndex = Gui.ZIndex + 1
Frame.Style = "DropShadow"
local TextLabel = Instance.new("TextLabel", Frame)
TextLabel.BackgroundTransparency = 1
TextLabel.TextXAlignment = "Left"
TextLabel.Text = Text
TextLabel.BorderSizePixel = 0
TextLabel.TextColor3 = Color3.new(1, 1, 1)
TextLabel.Size = UDim2.new(1, -30, 1, 0);
TextLabel.Position = UDim2.new(0, 10, 0, 0);
TextLabel.FontSize = "Size10"
TextLabel.ZIndex = Frame.ZIndex
TextLabel.TextStrokeColor3 = Color3.new(0, 0, 0)
TextLabel.TextStrokeTransparency = 0.87
Frame.Visible = false
Gui.MouseEnter:connect(function()
ActiveToolTip.Value = Name
Frame.Visible = true
Frame.Size = UDim2.new(0, math.ceil(TextLabel.TextBounds.X + 36), Frame.Size.Y.Scale, Frame.Size.Y.Offset)
end)
ActiveToolTip.Changed:connect(function()
if ActiveToolTip.Value ~= Name then
Frame.Visible = false
end
end)
Gui.MouseLeave:connect(function()
Frame.Visible = false
end)
end
end
local function setMaterialSelection(newMaterialSelection)
materialSelection = newMaterialSelection
updateUsabilityLocks()
for _, v in pairs(guiFrame:GetChildren()) do
if string.sub(v.Name,1,14) == 'MaterialButton' then
if v.Name == 'MaterialButton' .. materialSelection.enum.Name then
v.BackgroundTransparency = .1
else
v.BackgroundTransparency = 1
end
end
end
end
for i,materialSubTable in pairs(materialsTable) do
local newMaterialButton = Instance.new('ImageButton')
newMaterialButton.Name = 'MaterialButton' .. materialSubTable.enum.Name
newMaterialButton.BorderSizePixel = 2
newMaterialButton.BorderColor3 = Color3.new(.2, 1, 1)
newMaterialButton.BackgroundColor3 = Color3.new(.2, 1, 1)
newMaterialButton.BackgroundTransparency = 1
newMaterialButton.Image = materialSubTable.image
newMaterialButton.Size = UDim2.new(0, 35, 0, 35)
newMaterialButton.Position = UDim2.new(0, 5 + ((i-1) % 4) * 40, 0, 225 + ceil(i/4) * 40)
newMaterialButton.MouseButton1Down:connect(function()
setMaterialSelection(materialsTable[i])
end)
newMaterialButton.Parent = guiFrame
MakeToolTip(newMaterialButton, materialSubTable.enum.Name:gsub("([A-Z])", " %1"):gsub("^%s", "")) -- Add spaces to names
end
function resizeGuiFrame()
local materialsDynamic = dynamicMaterial
if forceDynamicMaterial then
materialsDynamic = forceDynamicMaterialTo
end
local desiredSize = UDim2.new(0, 180, 0, 240)
if currentTool and currentTool.usesMaterials then
checkBox3.Visible=true
label4.Visible=true
divider2.Visible=true
desiredSize = desiredSize + UDim2.new(0, 0, 0, 35)
if not materialsDynamic then
desiredSize = desiredSize + UDim2.new(0, 0, 0, 5 + ceil(#materialsTable / 4) * 40) --Dynamically resizes frame if we add more materials later.
end
else
checkBox3.Visible=false
label4.Visible=false
divider2.Visible=false
end
guiFrame.Size = desiredSize
--guiFrame:TweenSize(desiredSize, 'Out', 'Quad', .5) --illegal in studio
end
function updatePlaneLock()
checkBox1.Style = forcePlaneLock and Enum.ButtonStyle.RobloxRoundButton or Enum.ButtonStyle.RobloxRoundDefaultButton
checkBox1.Text = (planeLock or forcePlaneLock) and 'X' or ''
checkBox1.AutoButtonColor = not forcePlaneLock
if not (planeLock or forcePlaneLock) then
clearGrid()
end
end
checkBox1.MouseButton1Down:connect(function()
planeLock = not planeLock
updatePlaneLock()
end)
function updateSnapToGrid()
checkBox2.Style = forceSnapToGrid and Enum.ButtonStyle.RobloxRoundButton or Enum.ButtonStyle.RobloxRoundDefaultButton
checkBox2.Text = (snapToGrid or forceSnapToGrid) and 'X' or ''
checkBox2.AutoButtonColor = not forceSnapToGrid
end
checkBox2.MouseButton1Down:connect(function()
snapToGrid = not snapToGrid
updateSnapToGrid()
end)
function updateDynamicMaterial()
isDynamic = dynamicMaterial
if forceDynamicMaterial then
isDynamic = forceDynamicMaterialTo
end
checkBox3.Style = forceDynamicMaterial and Enum.ButtonStyle.RobloxRoundButton or Enum.ButtonStyle.RobloxRoundDefaultButton
checkBox3.AutoButtonColor = not forceDynamicMaterial
checkBox3.Text = isDynamic and 'X' or ''
resizeGuiFrame()
for _, v in pairs(guiFrame:GetChildren()) do
if string.sub(v.Name,1,14) == 'MaterialButton' then
v.Visible = not isDynamic
end
end
end
checkBox3.MouseButton1Down:connect(function()
dynamicMaterial = not dynamicMaterial
updateDynamicMaterial()
end)
function updateIgnoreWater()
isIgnoreWater = ignoreWater
if forceIgnoreWater then
isIgnoreWater = forceIgnoreWaterTo
end
checkBox4.Style = forceIgnoreWater and Enum.ButtonStyle.RobloxRoundButton or Enum.ButtonStyle.RobloxRoundDefaultButton
checkBox4.AutoButtonColor = not forceIgnoreWater
checkBox4.Text = isIgnoreWater and 'X' or ''
end
checkBox4.MouseButton1Down:connect(function()
ignoreWater = not ignoreWater
updateIgnoreWater()
end)
-------------
do
local runService = game:GetService('RunService').RenderStepped
function quickWait(waitTime)
if not waitTime then
runService:wait()
elseif waitTime < .033333 then
local startTick = tick()
runService:wait()
local delta = tick() - startTick
if delta <= waitTime * .5 then
quickWait(waitTime - delta)
end
else
wait(waitTime)
end
end
end
function deepCast(origin, endPoint, ignoreList, filterFunction, cubeTerrain)
local ray = Ray.new(origin, endPoint - origin)
local hit, pos, normal, material = game.Workspace:FindPartOnRayWithIgnoreList(ray, ignoreList, cubeTerrain)
if hit and filterFunction(hit) then
table.insert(ignoreList, hit)
return deepCast(pos, endPoint, ignoreList, filterFunction, cubeTerrain)
else
return hit, pos, normal, material
end
end
function clearSelection()
if selectionObject then
selectionObject:Destroy()
selectionObject = nil
end
if selectionPart then
selectionPart:Destroy()
selectionPart = nil
end
end
function clearGrid()
for i, v in pairs(gridLineParts) do
if v then
v:Destroy()
end
gridLineParts[i] = nil
end
end
function drawGrid(point, normal, transparency, color)
local transparency = transparency or .95
local color = BrickColor.new(color or 'Institutional white')--'Pastel light blue')
local gridCellSize = selectionSize * resolution
local gridSize = 10
local baseCframe = CFrame.new(point, point + normal)
local normalSpase = CFrame.new(Vector3.new(0, 0, 0), normal):pointToObjectSpace(point)
local roundedNormalOffset = (Vector3.new((normalSpase.x / gridCellSize) % 1, (normalSpase.y / gridCellSize) % 1, 0) - Vector3.new(.5, .5, 0)) * -gridCellSize
for u = 1, gridSize do
local linePart = gridLineParts[u]
if not linePart then
linePart = Instance.new('Part')
linePart.Transparency = 1
linePart.TopSurface = 'Smooth'
linePart.BottomSurface = 'Smooth'
linePart.Anchored = true
linePart.CanCollide = false
local selectionBox = Instance.new('SelectionBox')
selectionBox.Color = color
selectionBox.Transparency = transparency
selectionBox.Adornee = linePart
selectionBox.Parent = linePart
linePart.Parent = gui
gridLineParts[u] = linePart
elseif linePart.SelectionBox.Transparency ~= transparency or linePart.SelectionBox.Color ~= color then
linePart.SelectionBox.Transparency = transparency
linePart.SelectionBox.Color = color
end
local percent = (u - 1) / (gridSize - 1)
linePart.Size = Vector3.new(gridCellSize * gridSize * sin(math.acos(percent * 1.8 - .9)), 0, 0)
linePart.CFrame = baseCframe * CFrame.new(0, (percent - .5) * (gridSize - 1) * gridCellSize, 0) * CFrame.new(roundedNormalOffset)
end
for u = 1, gridSize do
local linePart = gridLineParts[gridSize + u]
if not linePart then
linePart = Instance.new('Part')
linePart.Transparency = 1
linePart.TopSurface = 'Smooth'
linePart.BottomSurface = 'Smooth'
linePart.Anchored = true
linePart.CanCollide = false
local selectionBox = Instance.new('SelectionBox')
selectionBox.Color = color
selectionBox.Transparency = transparency
selectionBox.Adornee = linePart
selectionBox.Parent = linePart
linePart.Parent = gui
gridLineParts[gridSize + u] = linePart
elseif linePart.SelectionBox.Transparency ~= transparency or linePart.SelectionBox.Color ~= color then
linePart.SelectionBox.Transparency = transparency
linePart.SelectionBox.Color = color
end
local percent = (u - 1) / (gridSize - 1)
linePart.Size = Vector3.new(0, gridCellSize * gridSize * sin(math.acos(percent * 1.8 - .9)), 0)
linePart.CFrame = baseCframe * CFrame.new((percent - .5) * (gridSize - 1) * gridCellSize, 0, 0) * CFrame.new(roundedNormalOffset)
end
end
local function getCell(list, x, y, z, materialList)
-- only include materialsList if you want to ignore water
return (materialList and materialList[x] and materialList[x][y] and materialList[x][y][z]) == materialWater and 0
or list and list[x] and list[x][y] and list[x][y][z]
end
local function getNeighborOccupancies(list, x, y, z, materialsList, includeSelf)
--only include materialsList if you want to ignore water
local fullNeighbor = false
local emptyNeighbor = false
local neighborOccupancies = includeSelf and getCell(list, x, y, z, materialsList) or 0
local totalNeighbors = includeSelf and 1 or 0
local nearMaterial = materialSelection.enum
for axis = 1, 3 do
for offset = -1, 1, 2 do
local neighbor = nil
local neighborMaterial = nil
if axis == 1 then
neighbor = list[x + offset] and list[x + offset][y][z]
elseif axis == 2 then
neighbor = list[x][y + offset] and list[x][y + offset][z]
elseif axis == 3 then
neighbor = list[x][y][z + offset]
end
if neighbor then
if materialsList then
if axis == 1 then
neighborMaterial = materialsList[x + offset] and materialsList[x + offset][y][z]
elseif axis == 2 then
neighborMaterial = materialsList[x][y + offset] and materialsList[x][y + offset][z]
elseif axis == 3 then
neighborMaterial = materialsList[x][y][z + offset]
end
if neighborMaterial == materialWater then
neighbor = 0
end
end
if neighbor >= 1 then
fullNeighbor = true
end
if neighbor <= 0 then
emptyNeighbor = true
end
totalNeighbors = totalNeighbors + 1
neighborOccupancies = neighborOccupancies + neighbor
end
end
end
return neighborOccupancies / (totalNeighbors ~= 0 and totalNeighbors or getCell(list, x, y, z, materialsList)), fullNeighbor, emptyNeighbor
end
local function round(n)
return floor(n + .5)
end
function findFace()
local cameraLookVector = game.Workspace.CurrentCamera.CoordinateFrame.lookVector
--[[local absx = abs(cameraLookVector.x) --this code is for 90 plane locking
local absy = abs(cameraLookVector.y)
local absz = abs(cameraLookVector.z)
if absy >= absx and absy >= absz then --preference towards y axis planes
return Vector3.new(0, cameraLookVector.y / absy, 0)
elseif absx >= absz then
return Vector3.new(cameraLookVector.x / absx, 0, 0)
end
return Vector3.new(0, 0, cameraLookVector.z / absz)]]
return Vector3.new(round(cameraLookVector.x), round(cameraLookVector.y), round(cameraLookVector.z)).unit --this code is for 45 degree plane locking
end
function lineToPlaneIntersection(linePoint, lineDirection, planePoint, planeNormal)
local denominator = lineDirection:Dot(planeNormal)
if denominator == 0 then
return linePoint
end
local distance = ((planePoint - linePoint):Dot(planeNormal)) / denominator
return linePoint + lineDirection * distance
end
function updateUsabilityLocks()
if currentTool then
forceSnapToGrid = currentTool.usesMaterials and materialSelection.forceSnapToGrid
updateSnapToGrid()
forcePlaneLock = currentTool.name == 'Add' or currentTool.name == 'Subtract'
updatePlaneLock()
forceDynamicMaterial = currentTool.name == 'Subtract' or currentTool.name == 'Erode' or currentTool.name == 'Paint' or currentTool.name == 'Smooth' or currentTool.name == 'Smoother'
forceDynamicMaterialTo = not (forceDynamicMaterial and currentTool.name == 'Paint')
isDynamic = dynamicMaterial
if forceDynamicMaterial then
isDynamic = forceDynamicMaterialTo
end
forceIgnoreWater = materialSelection.forceIgnoreWater and not isDynamic
if materialSelection.forceIgnoreWater then
forceIgnoreWaterTo = materialSelection.forceIgnoreWaterTo
end
isIgnoreWater = ignoreWater
if forceIgnoreWater then
isIgnoreWater = forceIgnoreWaterTo
end
updateIgnoreWater()
updateDynamicMaterial()
end
end
function operation(centerPoint)
local desiredMaterial = isDynamic and nearMaterial or materialSelection.enum
local radius = selectionSize * .5 * resolution
local minBounds = Vector3.new(
floor((centerPoint.x - radius) / resolution) * resolution,
floor((centerPoint.y - radius) / resolution) * resolution,
floor((centerPoint.z - radius) / resolution) * resolution)
local maxBounds = Vector3.new(
ceil((centerPoint.x + radius) / resolution) * resolution,
ceil((centerPoint.y + radius) / resolution) * resolution,
ceil((centerPoint.z + radius) / resolution) * resolution)
local region = Region3.new(minBounds, maxBounds)
local materials, occupancies = terrain:ReadVoxels(region, resolution)
if modules[currentTool.name] then
if modules[currentTool.name]['operation'] then
local middle = materials[ceil(#materials * .5)] --This little section of code sets nearMaterial to middle of matrix
if middle then --dig X
local middle = middle[ceil(#middle * .5)]
if middle then --dig Y
local middle = middle[ceil(#middle * .5)]
if middle and middle ~= materialAir and middle ~= materialWater then --dig Z
nearMaterial = middle
desiredMaterial = isDynamic and nearMaterial or desiredMaterial
end
end
end
modules[currentTool.name]['operation'](centerPoint, materials, occupancies, resolution, selectionSize, strength, desiredMaterial, brushShape, minBounds, maxBounds)
end
else
local airFillerMaterial = materialAir
local waterHeight = 0
if isIgnoreWater and (currentTool.name == 'Erode' or currentTool.name == 'Subtract') then
--[[local centerPointCell = Vector3.new(floor((centerPoint.x+.5)/resolution) * resolution, floor((centerPoint.y+.5)/resolution) * resolution, floor((centerPoint.z+.5)/resolution) * resolution)
local sampleRegion = Region3.new(centerPointCell - Vector3.new(resolution,resolution,resolution), centerPointCell + Vector3.new(resolution,resolution,resolution))
local sampleMaterials, sampleOccupancies = terrain:ReadVoxels(sampleRegion, resolution)]]
for ix,vx in ipairs(materials) do
for iy,vy in ipairs(vx) do
for iz, vz in ipairs(vy) do
if vz == materialWater then
airFillerMaterial = materialWater
if iy > waterHeight then
waterHeight = iy
end
end
end
end
end
end
for ix, vx in ipairs(occupancies) do
local cellVectorX = minBounds.x + (ix - .5) * resolution - centerPoint.x
for iy, vy in pairs(vx) do
local cellVectorY = minBounds.y + (iy - .5) * resolution - centerPoint.y
for iz, cellOccupancy in pairs(vy) do
local cellVectorZ = minBounds.z + (iz - .5) * resolution - centerPoint.z
local cellMaterial = materials[ix][iy][iz]
local distance = sqrt(cellVectorX * cellVectorX + cellVectorY * cellVectorY + cellVectorZ * cellVectorZ)
local magnitudePercent = 1
local brushOccupancy = 1
if brushShape == 'Sphere' then
magnitudePercent = cos(min(1, distance / (radius + resolution * .5)) * pi * .5)
brushOccupancy = max(0, min(1, (radius + .5 * resolution - distance) / resolution))
elseif brushShape == 'Box' then
if not (snapToGrid or forceSnapToGrid) then
local xOutside = 1 - max(0, abs(cellVectorX / resolution) + .5 - selectionSize * .5)
local yOutside = 1 - max(0, abs(cellVectorY / resolution) + .5 - selectionSize * .5)
local zOutside = 1 - max(0, abs(cellVectorZ / resolution) + .5 - selectionSize * .5)
brushOccupancy = xOutside * yOutside * zOutside
end
end
if cellMaterial ~= materialAir and cellMaterial ~= materialWater and cellMaterial ~= nearMaterial then
nearMaterial = cellMaterial
if isDynamic then
desiredMaterial = nearMaterial
end
end
if isIgnoreWater and cellMaterial == materialWater then
cellMaterial = materialAir
cellOccupancy = 0
end
local airFillerMaterial = waterHeight >= iy and airFillerMaterial or materialAir
if currentTool.name == 'Add' then
if selectionSize <= 2 then
if brushOccupancy >= .5 then
if cellMaterial == materialAir or cellOccupancy <= 0 then
materials[ix][iy][iz] = desiredMaterial
end
occupancies[ix][iy][iz] = 1
end
else
if brushOccupancy > cellOccupancy then
occupancies[ix][iy][iz] = brushOccupancy
end
if brushOccupancy >= .5 and cellMaterial == materialAir then
materials[ix][iy][iz] = desiredMaterial
end
end
elseif currentTool.name == 'Subtract' then
if cellMaterial ~= materialAir then
if selectionSize <= 2 then
if brushOccupancy >= .5 then
occupancies[ix][iy][iz] = airFillerMaterial == materialWater and 1 or 0
materials[ix][iy][iz] = airFillerMaterial
end
else
local desiredOccupancy = max(0,1 - brushOccupancy)
if desiredOccupancy < cellOccupancy then
if desiredOccupancy <= one256th then
occupancies[ix][iy][iz] = airFillerMaterial == materialWater and 1 or 0
materials[ix][iy][iz] = airFillerMaterial
else
occupancies[ix][iy][iz] = min(cellOccupancy, desiredOccupancy)
end
end
end
end
elseif currentTool.name == 'Grow' then
if brushOccupancy >= .5 then --working on
local desiredOccupancy = cellOccupancy
local neighborOccupancies, fullNeighbor, emptyNeighbor = getNeighborOccupancies(occupancies, ix, iy, iz, isIgnoreWater and materials)
if cellOccupancy > 0 or fullNeighbor then --problem if selection size is small.
desiredOccupancy = desiredOccupancy + neighborOccupancies * (strength + .1) * .25 * brushOccupancy * magnitudePercent
end
if cellMaterial == materialAir and desiredOccupancy > 0 then
materials[ix][iy][iz] = desiredMaterial
end
if desiredOccupancy ~= cellOccupancy then
occupancies[ix][iy][iz] = desiredOccupancy
end
end
elseif currentTool.name == 'Erode' then
if cellMaterial ~= materialAir then
local flippedBrushOccupancy = 1 - brushOccupancy
if flippedBrushOccupancy <= .5 then
local desiredOccupancy = cellOccupancy
local emptyNeighbor = false
local neighborOccupancies = 6
for axis = 1, 3 do
for offset = -1, 1, 2 do
local neighbor = nil
local neighborMaterial = nil
if axis == 1 then
neighbor = occupancies[ix + offset] and occupancies[ix + offset][iy][iz]
neighborMaterial = materials[ix + offset] and materials[ix + offset][iy][iz]
elseif axis == 2 then
neighbor = occupancies[ix][iy + offset] and occupancies[ix][iy + offset][iz]
neighborMaterial = materials[ix][iy + offset] and materials[ix][iy + offset][iz]
elseif axis == 3 then
neighbor = occupancies[ix][iy][iz + offset]
neighborMaterial = materials[ix][iy][iz + offset]
end
if neighbor then
if isIgnoreWater and neighborMaterial == materialWater then
neighbor = 0
end
if neighbor <= 0 then
emptyNeighbor = true
end
neighborOccupancies = neighborOccupancies - neighbor
end
end
end
if cellOccupancy < 1 or emptyNeighbor then
desiredOccupancy = max(0,desiredOccupancy - (neighborOccupancies / 6) * (strength + .1) * .25 * brushOccupancy * magnitudePercent)
end
if desiredOccupancy <= one256th then
occupancies[ix][iy][iz] = airFillerMaterial == materialWater and 1 or 0
materials[ix][iy][iz] = airFillerMaterial
else
occupancies[ix][iy][iz] = desiredOccupancy
end
end
end
elseif currentTool.name == 'Paint' then
if brushOccupancy > 0 and cellOccupancy > 0 then
materials[ix][iy][iz] = desiredMaterial
end
end
end
end
end
end
terrain:WriteVoxels(region, resolution, materials, occupancies)
end
function Selected(tool)
if plugin then
plugin:Activate(true)
end
if tool.button then
tool.button:SetActive(true)
lastTool = tool
end
if not userInput.MouseEnabled then
prevCameraType = game.Workspace.CurrentCamera.CameraType
game.Workspace.CurrentCamera.CameraType = Enum.CameraType.Fixed
end
on = true
currentTool = tool
updateUsabilityLocks()
if modules[tool.name] and modules[tool.name]['On'] then
modules[tool.name].On(mouse,Deselected)
end
if not modules[tool.name] or modules[tool.name]['operation'] then
resizeGuiFrame()
titlelabel.Text = tool.name
gui.Parent = coreGui
gui.Frame.Visible = true
local loopTag = {} --using table as a unique value for debouncing
currentLoopTag = loopTag
while currentLoopTag and currentLoopTag == loopTag do
local t = tick()
local radius = selectionSize * .5 * resolution
local cameraPos = mouse.Origin.p
local ignoreModel = nil
if game.Players.LocalPlayer and game.Players.LocalPlayer.Character then
ignoreModel = game.Players.LocalPlayer.Character
end
local mouseRay = Ray.new(cameraPos, mouse.UnitRay.Direction*10000)
local hitObject, mainPoint = game.Workspace:FindPartOnRay(mouseRay, ignoreModel, false, isIgnoreWater)
if tool.name == 'Add' then
mainPoint = mainPoint - mouse.UnitRay.Direction * .05
elseif tool.name == 'Subtract' or tool.name == 'Paint' or tool.name == 'Grow' then
mainPoint = mainPoint + mouse.UnitRay.Direction * .05
end
if mouse.Target == nil then --cage the cursor so that it does not fly away
mainPoint = cameraPos + mouse.UnitRay.Direction * lastCursorDistance --limits the distance of the mainPoint if the mouse is not hitting an object
end
if not mouseDown or click then
lastPlanePoint = mainPoint
lastNormal = findFace()
end
if planeLock or forcePlaneLock then
mainPoint = lineToPlaneIntersection(cameraPos, mouse.UnitRay.Direction, lastPlanePoint, lastNormal)
end
if snapToGrid or forceSnapToGrid then
local snapOffset = Vector3.new(1, 1, 1) * (radius % resolution) --in studs
local tempMainPoint = (mainPoint - snapOffset) / resolution + Vector3.new(.5, .5, .5) --in voxels
mainPoint = Vector3.new(floor(tempMainPoint.x), floor(tempMainPoint.y), floor(tempMainPoint.z)) * resolution + snapOffset
end
if mouseDown then
if click then
firstOperation = t
lastMainPoint = mainPoint
end
if click or t > firstOperation + clickThreshold then
click = false
if downKeys[Enum.KeyCode.LeftAlt] or downKeys[Enum.KeyCode.RightAlt] then
--pick color
local function filterNonTerrain(thing)
if thing and thing == terrain then
return false
end
return true
end
local hit, hitPosition, normal, foundMaterial = deepCast(cameraPos, cameraPos + mouse.UnitRay.Direction*10000, {}, filterNonTerrain, true)
if hit then
for _, materialTable in pairs(materialsTable) do
if materialTable.enum == foundMaterial then
setMaterialSelection(materialTable)
break
end
end
end
else
local difference = mainPoint - lastMainPoint
local dragDistance = (difference).magnitude
local crawlDistance = radius * .5 --Maybe adjustable setting? Considering using a different method of crawling, with a percent rather than a finite distance.
if dragDistance > crawlDistance then
local differenceVector = difference.unit
local dragDistance = min(dragDistance, crawlDistance * 2 + 20) --limiting this so that it does not attempt too many operations within a single drag.
local samples = ceil(dragDistance / crawlDistance - .1)
for i = 1, samples do
operation(lastMainPoint + differenceVector * dragDistance * (i / samples))
end
mainPoint = lastMainPoint + differenceVector * dragDistance
else
operation(mainPoint)
end
lastMainPoint = mainPoint
end
end
end
if not selectionPart then
selectionPart = Instance.new('Part')
selectionPart.Name = 'SelectionPart'
selectionPart.Transparency = 1
selectionPart.TopSurface = 'Smooth'
selectionPart.BottomSurface = 'Smooth'
selectionPart.Anchored = true
selectionPart.CanCollide = false
selectionPart.Size = Vector3.new(1, 1, 1) * selectionSize * resolution + Vector3.new(.1, .1, .1)
selectionPart.Parent = gui
end
if not selectionObject then
selectionObject = Instance.new(brushShape == 'Sphere' and 'SelectionSphere' or 'SelectionBox')
selectionObject.Name = 'SelectionObject'
selectionObject.Color = BrickColor.new('Toothpaste')
selectionObject.SurfaceTransparency = .95 - strength * .3