2013/terrain plugins/11 - floodfill.luau

851 lines
20 KiB
Plaintext

--!strict
while not game do
wait()
end
local ChangeHistoryService = game:GetService "ChangeHistoryService"
local ContentProvider = game:GetService "ContentProvider"
local CoreGui = game:GetService "CoreGui"
local CreateStandardButton = require "../Modules/Terrain/CreateStandardButton"
local CreateStandardLabel = require "../Modules/Terrain/CreateStandardLabel"
local News = require "../Modules/New"
local New = News.New
local Hydrate = News.Hydrate
local BaseUrl = require "../Modules/BaseUrl"
local path = BaseUrl.path
---------------
--PLUGIN SETUP-
---------------
local loaded = false
local on = false
local On, Off
local mouseDown, mouseUp
local this = PluginManager():CreatePlugin() :: Plugin
local mouse = this:GetMouse()
mouse.Button1Down:connect(function()
mouseDown(mouse)
end)
mouse.Button1Up:connect(function()
mouseUp(mouse)
end)
local toolbar = this:CreateToolbar "Terrain" :: Toolbar
local toolbarbutton =
toolbar:CreateButton("Flood Fill", "Flood Fill", "floodFill.png") :: Button
toolbarbutton.Click:connect(function()
if on then
Off()
elseif loaded then
On()
end
end)
game:WaitForChild "Workspace"
workspace:WaitForChild "Terrain"
-----------------------------
--LOCAL FUNCTION DEFINITIONS-
-----------------------------
local Terrain = workspace.Terrain
-- local WorldToCellPreferSolid = c.WorldToCellPreferSolid
local WorldToCellPreferEmpty = Terrain.WorldToCellPreferEmpty
local CellCenterToWorld = Terrain.CellCenterToWorld
local GetCell = Terrain.GetCell
local SetCell = Terrain.SetCell
-- local maxYExtents = c.MaxExtents.Max.Y
local emptyMaterial = Enum.CellMaterial.Empty
-- local waterMaterial = Enum.CellMaterial.Water
-- local floodFill
-----------------
--DEFAULT VALUES-
-----------------
local startCell
local processing = false
-- gui values
local screenGui
local dragBar, closeEvent, helpFrame --, lastCell
local progressBar, setProgressFunc, cancelEvent
local ConfirmationPopupObject
--- exposed values to user via gui
local currentMaterial = 1
-- load our libraries
local RbxGui = LoadLibrary "RbxGui"
-- local RbxUtil = LoadLibrary "RbxUtility"
ContentProvider:Preload(path "asset?id=82741829")
------------------------- OBJECT DEFINITIONS ---------------------
-- helper function for objects
local colours = {
"Bright green",
"Bright yellow",
"Bright red",
"Sand red",
"Black",
"Dark stone grey",
"Sand blue",
"Deep orange",
"Dark orange",
"Reddish brown",
"Light orange",
"Light stone grey",
"Sand green",
"Medium stone grey",
"Really red",
"Really blue",
"Bright blue",
}
local function getClosestColorToTerrainMaterial(terrainValue)
local val = BrickColor.new(colours[terrainValue])
if val then
return val
end
return BrickColor.new "Bright green"
end
-- Used to create a highlighter that follows the mouse.
-- It is a class mouse highlighter. To use, call MouseHighlighter.Create(mouse) where mouse is the mouse to track.
local MouseHighlighter = {}
MouseHighlighter.__index = MouseHighlighter
local startTween, stopTween
-- Create a mouse movement highlighter.
-- plugin - Plugin to get the mouse from.
function MouseHighlighter.Create(mouseUse)
local highlighter = {}
local mouseH = mouseUse
highlighter.OnClicked = nil
highlighter.mouseDown = false
-- Store the last point used to draw.
highlighter.lastUsedPoint = nil
-- Will hold a part the highlighter will be attached to. This will be moved where the mouse is.
highlighter.selectionPart = nil
-- Hook the mouse up to check for movement.
mouseH.Button1Down:connect(function()
highlighter.mouseDown = true
end)
mouseH.Button1Up:connect(function()
highlighter.mouseDown = false
end)
-- Create the part that the highlighter will be attached to.
highlighter.selectionPart = New "Part" {
Name = "SelectionPart",
Archivable = false,
Transparency = 0.5,
Anchored = true,
Locked = true,
CanCollide = false,
FormFactor = Enum.FormFactor.Custom,
Size = Vector3.new(4, 4, 4),
BottomSurface = 0,
TopSurface = 0,
BrickColor = getClosestColorToTerrainMaterial(currentMaterial),
Parent = workspace,
}
local billboardGui = New "BillboardGui" {
Size = UDim2.new(5, 0, 5, 0),
StudsOffset = Vector3.new(0, 2.5, 0),
Parent = highlighter.selectionPart,
}
local imageLabel = New "ImageLabel" {
BackgroundTransparency = 1,
Size = UDim2.new(1, 0, 1, 0),
Position = UDim2.new(-0.35, 0, -0.5, 0),
Image = path "asset?id=82741829",
Parent = billboardGui,
}
local lastTweenChange
startTween = function()
local tweenDown, tweenUp
while tweenUp or tweenDown do
wait(0.2)
end
lastTweenChange = tick()
delay(0, function()
local thisTweenStamp = lastTweenChange
tweenDown = function()
if
imageLabel
and imageLabel:IsDescendantOf(workspace)
and thisTweenStamp == lastTweenChange
then
imageLabel:TweenPosition(
UDim2.new(-0.35, 0, -0.5, 0),
Enum.EasingDirection.In,
Enum.EasingStyle.Quad,
0.4,
true,
tweenUp
)
end
end
tweenUp = function()
if
imageLabel
and imageLabel:IsDescendantOf(workspace)
and thisTweenStamp == lastTweenChange
then
imageLabel:TweenPosition(
UDim2.new(-0.35, 0, -0.7, 0),
Enum.EasingDirection.Out,
Enum.EasingStyle.Sine,
0.4,
true,
tweenDown
)
end
end
tweenUp()
end)
end
stopTween = function()
lastTweenChange = tick()
end
mouseH.TargetFilter = highlighter.selectionPart
setmetatable(highlighter, MouseHighlighter)
-- Update where the highlighter is displayed.
-- position - Where to display the highlighter, in world space.
local function UpdatePosition(position)
if not position then
return
elseif not mouseH.Target then
stopTween()
highlighter.selectionPart.Parent = nil
return
end
if highlighter.selectionPart.Parent ~= workspace then
highlighter.selectionPart.Parent = workspace
startTween()
end
local vectorPos = Vector3.new(position.x, position.y, position.z)
local cellPos = WorldToCellPreferEmpty(Terrain, vectorPos)
local regionToSelect
local cellMaterial = GetCell(Terrain, cellPos.x, cellPos.y, cellPos.z)
local y = cellPos.y
-- only select empty cells
while cellMaterial ~= emptyMaterial do
y += 1
cellMaterial = GetCell(Terrain, cellPos.x, y, cellPos.z)
end
cellPos = Vector3.new(cellPos.x, y, cellPos.z)
local lowVec =
CellCenterToWorld(Terrain, cellPos.x, cellPos.y - 1, cellPos.z)
local highVec =
CellCenterToWorld(Terrain, cellPos.x, cellPos.y + 1, cellPos.z)
regionToSelect = Region3.new(lowVec, highVec)
highlighter.selectionPart.CFrame = regionToSelect.CFrame
if not (highlighter.OnClicked and highlighter.mouseDown) then
return
end
if nil == highlighter.lastUsedPoint then
highlighter.lastUsedPoint = WorldToCellPreferEmpty(
Terrain,
Vector3.new(mouseH.Hit.x, mouseH.Hit.y, mouseH.Hit.z)
)
else
cellPos = WorldToCellPreferEmpty(
Terrain,
Vector3.new(mouseH.Hit.x, mouseH.Hit.y, mouseH.Hit.z)
)
-- holdChange = cellPos - highlighter.lastUsedPoint
-- Require terrain.
if 0 == GetCell(Terrain, cellPos.X, cellPos.Y, cellPos.Z).Value then
return
end
highlighter.lastUsedPoint = cellPos
end
end
-- Function to call when the mouse has moved. Updates where to display the highlighter.
mouseH.Move:connect(function()
if on and not processing then
UpdatePosition(mouseH.Hit)
end
end)
return highlighter
end
function MouseHighlighter:SetMaterial(newMaterial)
self.selectionPart.BrickColor =
getClosestColorToTerrainMaterial(newMaterial)
end
function MouseHighlighter:GetPosition()
return WorldToCellPreferEmpty(Terrain, self.selectionPart.CFrame.p)
end
-- Hide the highlighter.
function MouseHighlighter:DisablePreview()
stopTween()
self.selectionPart.Parent = nil
end
-- Show the highlighter.
function MouseHighlighter:EnablePreview()
self.selectionPart.Parent = workspace
startTween()
end
-- Create the mouse movement highlighter.
local mouseHighlighter = MouseHighlighter.Create(mouse)
mouseHighlighter:DisablePreview()
-- Used to create a highlighter.
-- A highlighter is a open, rectuangular area displayed in 3D.
local ConfirmationPopup = {}
ConfirmationPopup.__index = ConfirmationPopup
-- Create a confirmation popup.
--
-- confirmText - What to display in the popup.
-- textOffset - Offset to position text at.
-- confirmFunction - Function to run on confirmation.
-- declineFunction - Function to run when declining.
--
-- Return:
-- Value a table with confirmation gui and options.
function ConfirmationPopup.Create(
confirmText,
confirmSmallText,
confirmButtonText,
declineButtonText,
confirmFunction,
declineFunction
)
local popup = {
confirmButton = nil, -- Hold the button to confirm a choice.
declineButton = nil, -- Hold the button to decline a choice.
confirmationFrame = nil, -- Hold the conformation frame.
confirmationText = nil, -- Hold the text label to display the conformation message.
confirmationHelpText = nil, -- Hold the text label to display the conformation message help.
}
popup.confirmationFrame = New "Frame" {
Name = "ConfirmationFrame",
Size = UDim2.new(0, 280, 0, 160),
Position = UDim2.new(
0.5,
-popup.confirmationFrame.Size.X.Offset / 2,
0.5,
-popup.confirmationFrame.Size.Y.Offset / 2
),
Style = Enum.FrameStyle.RobloxRound,
Parent = screenGui,
}
popup.confirmLabel = Hydrate(
CreateStandardLabel(
"ConfirmLabel",
UDim2.new(0, 0, 0, 15),
UDim2.new(1, 0, 0, 24),
confirmText,
popup.confirmationFrame
)
) {
FontSize = Enum.FontSize.Size18,
TextXAlignment = Enum.TextXAlignment.Center,
}
popup.confirmationHelpText = Hydrate(
CreateStandardLabel(
"ConfirmSmallLabel",
UDim2.new(0, 0, 0, 40),
UDim2.new(1, 0, 0, 28),
confirmSmallText,
popup.confirmationFrame
)
) {
FontSize = Enum.FontSize.Size14,
TextWrapped = true,
Font = Enum.Font.Arial,
TextXAlignment = Enum.TextXAlignment.Center,
}
-- Confirm
popup.confirmButton = CreateStandardButton(
"ConfirmButton",
UDim2.new(0.5, -120, 1, -50),
confirmButtonText,
confirmFunction,
popup.confirmationFrame
)
-- Decline
popup.declineButton = CreateStandardButton(
"DeclineButton",
UDim2.new(0.5, 0, 1, -50),
declineButtonText,
declineFunction,
popup.confirmationFrame
)
setmetatable(popup, ConfirmationPopup)
return popup
end
-- Clear the popup, free up assets.
function ConfirmationPopup:Clear()
if self.confirmButton then
self.confirmButton.Parent = nil
end
if self.declineButton then
self.declineButton.Parent = nil
end
if self.confirmationFrame then
self.confirmationFrame.Parent = nil
end
if self.confirmLabel then
self.confirmLabel.Parent = nil
end
self.confirmButton = nil
self.declineButton = nil
self.conformationFrame = nil
self.conformText = nil
end
-----------------------
--FUNCTION DEFINITIONS-
-----------------------
-- function startLoadingFrame() end
-- Load the progress bar to display when drawing a river.
-- text - Text to display.
local function LoadProgressBar(text)
processing = true
-- Start the progress bar.
progressBar, setProgressFunc, cancelEvent = RbxGui.CreateLoadingFrame(text)
progressBar.Position = UDim2.new(0.5, -progressBar.Size.X.Offset / 2, 0, 15)
progressBar.Parent = screenGui
local loadingPercent = progressBar:FindFirstChild("LoadingPercent", true)
if loadingPercent then
loadingPercent.Parent = nil
end
local loadingBar = progressBar:FindFirstChild("LoadingBar", true)
if loadingBar then
loadingBar.Parent = nil
end
local cancelButton = progressBar:FindFirstChild("CancelButton", true)
if cancelButton then
cancelButton.Text = "Stop"
end
cancelEvent.Event:connect(function(_)
processing = false
-- spin = false
end)
local spinnerFrame = New "Frame" {
Name = "Spinner",
Size = UDim2.new(0, 80, 0, 80),
Position = UDim2.new(0.5, -40, 0.5, -55),
BackgroundTransparency = 1,
Parent = progressBar,
}
local spinnerIcons = {}
local spinnerNum = 1
while spinnerNum <= 8 do
local spinnerImage = New "ImageLabel" {
Name = "Spinner" .. spinnerNum,
Size = UDim2.new(0, 16, 0, 16),
Position = UDim2.new(
0.5 + 0.3 * math.cos(math.rad(45 * spinnerNum)),
-8,
0.5 + 0.3 * math.sin(math.rad(45 * spinnerNum)),
-8
),
BackgroundTransparency = 1,
Image = path "asset?id=45880710",
Parent = spinnerFrame,
}
spinnerIcons[spinnerNum] = spinnerImage
spinnerNum += 1
end
--Make it spin
delay(0, function()
local spinPos = 0
while processing do
local pos = 0
while pos < 8 do
spinnerIcons[pos + 1].Image = (
pos == spinPos or pos == ((spinPos + 1) % 8)
)
and path "asset?id=45880668"
or path "asset?id=45880710"
pos += 1
end
spinPos = (spinPos + 1) % 8
wait(0.2)
end
end)
end
-- Unload the progress bar.
local function UnloadProgressBar()
processing = false
if progressBar then
progressBar.Parent = nil
progressBar = nil
end
if setProgressFunc then
setProgressFunc = nil
end
if cancelEvent then
cancelEvent = nil
end
end
local function floodFill(x, y, z)
LoadProgressBar "Processing"
breadthFill(x, y, z)
UnloadProgressBar()
ChangeHistoryService:SetWaypoint "FloodFill"
end
-- Function used when we try and flood fill. Prompts the user first.
-- Will not show if disabled or terrain is being processed.
local function ConfirmFloodFill(x, y, z)
-- Only do if something isn't already being processed.
if processing then
return
end
processing = true
if ConfirmationPopupObject then
return
end
ConfirmationPopupObject = ConfirmationPopup.Create(
"Flood Fill At Selected Location?",
"This operation may take some time.",
"Fill",
"Cancel",
function()
ConfirmationPopupObject:Clear()
ConfirmationPopupObject = nil
floodFill(x, y, z)
ConfirmationPopupObject = nil
end,
function()
ConfirmationPopupObject:Clear()
ConfirmationPopupObject = nil
processing = false
end
)
end
function mouseDown(mouseD)
if on and mouseD.Target == workspace.Terrain then
startCell = mouseHighlighter:GetPosition()
end
end
function mouseUp(_)
if processing then
return
end
local upCell = mouseHighlighter:GetPosition()
if startCell == upCell then
ConfirmFloodFill(upCell.x, upCell.y, upCell.z)
end
end
local function getMaterial(x, y, z)
return GetCell(Terrain, x, y, z)
end
local function doBreadthFill(x, y, z)
if getMaterial(x, y, z) ~= emptyMaterial then
return
end
local yDepthChecks = {}
local cellsToCheck = breadthFillHelper(x, y, z, yDepthChecks)
local count = 0
while cellsToCheck and #cellsToCheck > 0 do
local cellCheckQueue = {}
for i = 1, #cellsToCheck do
if not processing then
return
end
count = count + 1
local newCellsToCheck = breadthFillHelper(
cellsToCheck[i].xPos,
cellsToCheck[i].yPos,
cellsToCheck[i].zPos,
yDepthChecks
)
if newCellsToCheck and #newCellsToCheck > 0 then
for j = 1, #newCellsToCheck do
table.insert(cellCheckQueue, {
xPos = newCellsToCheck[j].xPos,
yPos = newCellsToCheck[j].yPos,
zPos = newCellsToCheck[j].zPos,
})
end
end
if count >= 3000 then
wait()
count = 0
end
end
cellsToCheck = cellCheckQueue
end
return yDepthChecks
end
function breadthFill(x, y, z)
local yDepthChecks = doBreadthFill(x, y, z)
while yDepthChecks and #yDepthChecks > 0 do
local newYChecks = {}
for i = 1, #yDepthChecks do
local currYDepthChecks = doBreadthFill(
yDepthChecks[i].xPos,
yDepthChecks[i].yPos,
yDepthChecks[i].zPos
)
if
not (processing and currYDepthChecks and #currYDepthChecks > 0)
then
return
end
for j = 1, #currYDepthChecks do
table.insert(newYChecks, {
xPos = currYDepthChecks[j].xPos,
yPos = currYDepthChecks[j].yPos,
zPos = currYDepthChecks[j].zPos,
})
end
end
yDepthChecks = newYChecks
end
end
local function cellInTerrain(x, y, z)
if x < Terrain.MaxExtents.Min.X or x >= Terrain.MaxExtents.Max.X then
return false
end
if y < Terrain.MaxExtents.Min.Y or y >= Terrain.MaxExtents.Max.Y then
return false
end
if z < Terrain.MaxExtents.Min.Z or z >= Terrain.MaxExtents.Max.Z then
return false
end
return true
end
function breadthFillHelper(x, y, z, yDepthChecks)
-- first, lets try and fill this cell
if cellInTerrain(x, y, z) and getMaterial(x, y, z) == emptyMaterial then
SetCell(Terrain, x, y, z, currentMaterial, 0, 0)
end
local cellsToFill = {}
if
cellInTerrain(x + 1, y, z)
and getMaterial(x + 1, y, z) == emptyMaterial
then
table.insert(cellsToFill, { xPos = x + 1, yPos = y, zPos = z })
end
if
cellInTerrain(x - 1, y, z)
and getMaterial(x - 1, y, z) == emptyMaterial
then
table.insert(cellsToFill, { xPos = x - 1, yPos = y, zPos = z })
end
if
cellInTerrain(x, y, z + 1)
and getMaterial(x, y, z + 1) == emptyMaterial
then
table.insert(cellsToFill, { xPos = x, yPos = y, zPos = z + 1 })
end
if
cellInTerrain(x, y, z - 1)
and getMaterial(x, y, z - 1) == emptyMaterial
then
table.insert(cellsToFill, { xPos = x, yPos = y, zPos = z - 1 })
end
if
cellInTerrain(x, y - 1, z)
and getMaterial(x, y - 1, z) == emptyMaterial
then
table.insert(yDepthChecks, { xPos = x, yPos = y - 1, zPos = z })
end
for i = 1, #cellsToFill do
SetCell(
Terrain,
cellsToFill[i].xPos,
cellsToFill[i].yPos,
cellsToFill[i].zPos,
currentMaterial,
0,
0
)
end
if #cellsToFill <= 0 then
return nil
end
return cellsToFill
end
On = function()
if not Terrain then
return
elseif this then
this:Activate(true)
end
if toolbarbutton then
toolbarbutton:SetActive(true)
end
if dragBar then
dragBar.Visible = true
end
on = true
end
Off = function()
if toolbarbutton then
toolbarbutton:SetActive(false)
end
if mouseHighlighter then
mouseHighlighter:DisablePreview()
end
if dragBar then
dragBar.Visible = false
end
on = false
end
mouseHighlighter.OnClicked = mouseUp
------
--GUI-
------
screenGui = Instance.new "ScreenGui"
screenGui.Name = "FloodFillGui"
screenGui.Parent = CoreGui
local containerFrame
dragBar, containerFrame, helpFrame, closeEvent = RbxGui.CreatePluginFrame(
"Flood Fill",
UDim2.new(0, 163, 0, 195),
UDim2.new(0, 0, 0, 0),
false,
screenGui
)
dragBar.Visible = false
Hydrate(helpFrame) {
Size = UDim2.new(0, 200, 0, 190),
-- "This will be pdfs in 2024" - Heliodex
New "TextLabel" {
Name = "TextHelp",
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size12,
TextColor3 = Color3.new(1, 1, 1),
Size = UDim2.new(1, -6, 1, -6),
Position = UDim2.new(0, 3, 0, 3),
TextXAlignment = Enum.TextXAlignment.Left,
TextYAlignment = Enum.TextYAlignment.Top,
BackgroundTransparency = 1,
TextWrapped = true,
Text = [[
Quickly replace empty terrain cells with a selected material. Clicking the mouse will cause any empty terrain cells around the point clicked to be filled with the current material, and will also spread to surrounding empty cells (including any empty cells below, but not above).
Simply click on a different material to fill with that material. The floating paint bucket and cube indicate where filling will start.
]],
},
}
closeEvent.Event:connect(function()
Off()
end)
local terrainSelectorGui, terrainSelected, forceTerrainMaterialSelection =
RbxGui.CreateTerrainMaterialSelector(
UDim2.new(1, -10, 0, 184),
UDim2.new(0, 5, 0, 5)
)
terrainSelectorGui.Parent = containerFrame
terrainSelectorGui.BackgroundTransparency = 1
terrainSelectorGui.BorderSizePixel = 0
terrainSelected.Event:connect(function(newMaterial)
currentMaterial = newMaterial
if mouseHighlighter then
mouseHighlighter:SetMaterial(currentMaterial)
end
end)
forceTerrainMaterialSelection(Enum.CellMaterial.Water)
this.Deactivation:connect(function()
Off()
end)
--------------------------
--SUCCESSFUL LOAD MESSAGE-
--------------------------
loaded = true