2013/terrain plugins/04 - brush.luau

689 lines
17 KiB
Plaintext

while not game do
wait()
end
local ChangeHistoryService = game:GetService "ChangeHistoryService"
local CoreGui = game:GetService "CoreGui"
local New = require("../Modules/New").New
-- local CreateStandardLabel = require "../Modules/Terrain/CreateStandardLabel"
---------------
--PLUGIN SETUP-
---------------
local loaded = false
local on = false
local On, Off
local this = PluginManager():CreatePlugin() :: Plugin
local toolbar = this:CreateToolbar "Terrain" :: Toolbar
local toolbarbutton =
toolbar:CreateButton("Brush", "Brush", "brush.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 c = workspace.Terrain
local GetCell = c.GetCell
local SetCells = c.SetCells
local AutowedgeCells = c.AutowedgeCells
local WorldToCellPreferSolid = c.WorldToCellPreferSolid
local CellCenterToWorld = c.CellCenterToWorld
local buildTerrainMode = "Add"
local removeTerrainMode = "Remove"
-----------------
--DEFAULT VALUES-
-----------------
local radius = 5
local depth = 0
local mousedown = false
local mousemoving = false
local brushheight
local material = 1
local lastMousePos = Vector2.new(-1, -1)
local lastCellFillTime = 0
local maxYExtents = math.min(c.MaxExtents.Max.Y, 512)
-- Which mode (build/remove) it is.
local mode = buildTerrainMode
-- Height and depth to use for the different modes.
local buildTerrainHeight = 5
local removeTerrainDepth = -5
local mouse = this:GetMouse()
mouse.Button1Down:connect(function()
buttonDown()
end)
mouse.Button1Up:connect(function()
mousedown = false
brushheight = nil
enablePreview()
updatePreviewSelection(mouse.Hit)
ChangeHistoryService:SetWaypoint "Brush"
end)
mouse.Move:connect(function()
mouseMoved()
end)
local selectionPart = New "Part" {
Name = "SelectionPart",
Archivable = false,
Transparency = 1,
Anchored = true,
Locked = true,
CanCollide = false,
FormFactor = Enum.FormFactor.Custom,
}
local selectionBox = New "SelectionBox" {
Archivable = false,
Color = BrickColor.new "Lime green",
Adornee = selectionPart,
}
mouse.TargetFilter = selectionPart
function disablePreview()
selectionBox.Parent = nil
end
function enablePreview()
selectionBox.Parent = workspace
end
-----------------------
--FUNCTION DEFINITIONS-
-----------------------
-- searches the y depth of a particular cell position to find the lowest y that is empty
function findLowestEmptyCell(x, y, z)
local cellMat = GetCell(c, x, y, z)
local lowestY = y
while cellMat == Enum.CellMaterial.Empty do
lowestY = y
if y > 0 then
y = y - 1
cellMat = GetCell(c, x, y, z)
else
lowestY = 0
cellMat = nil
end
end
return lowestY
end
-- finds the lowest cell that is not filled in the radius that is currently specified
local function findLowPoint(x, y, z)
local lowestPoint = maxYExtents + 1
for i = -radius, radius do
local zPos = z + i
for _ = -radius, radius do
local xPos = x + i
local cellMat = GetCell(c, xPos, y, zPos)
if cellMat == Enum.CellMaterial.Empty then
local emptyDepth = findLowestEmptyCell(xPos, y, zPos)
if emptyDepth < lowestPoint then
lowestPoint = emptyDepth
end
end
end
end
return lowestPoint
end
-- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0)
--
-- vectorPos - End point of the line.
--
-- Return:
-- success - Value is true if there was a plane intersection, false if not.
-- intersection - Value is the terrain cell intersection point if there is one, vectorPos if there isn't.
local function PlaneIntersection(vectorPos)
local currCamera = workspace.CurrentCamera
local startPos = Vector3.new(
currCamera.CoordinateFrame.p.X,
currCamera.CoordinateFrame.p.Y,
currCamera.CoordinateFrame.p.Z
)
local endPos = Vector3.new(vectorPos.X, vectorPos.Y, vectorPos.Z)
local normal = Vector3.new(0, 1, 0)
local p3 = Vector3.new(0, 0, 0)
local startEndDot = normal:Dot(endPos - startPos)
local cellPos = vectorPos
local success = false
if startEndDot ~= 0 then
local t = normal:Dot(p3 - startPos) / startEndDot
if t >= 0 and t <= 1 then
local intersection = ((endPos - startPos) * t) + startPos
cellPos = c:WorldToCell(intersection)
success = true
end
end
return success, cellPos
end
-- brushes terrain at point (x, y, z) in cluster c
local function brush(x, y, z)
if depth == 0 then
return
elseif depth > 0 then
local findY = findLowPoint(x, y + depth, z)
local yWithDepth = y + depth
local lowY
if findY < yWithDepth then
lowY = findY
else
lowY = yWithDepth
end
local lowVec = Vector3int16.new(x - radius, lowY, z - radius)
local highVec = Vector3int16.new(x + radius, yWithDepth, z + radius)
local regionToFill = Region3int16.new(lowVec, highVec)
SetCells(c, regionToFill, material, 0, 0)
AutowedgeCells(c, regionToFill)
else
local lowVec = Vector3int16.new(x - radius, y + depth + 1, z - radius)
local highVec = Vector3int16.new(x + radius, maxYExtents, z + radius)
local regionToEmpty = Region3int16.new(lowVec, highVec)
SetCells(c, regionToEmpty, Enum.CellMaterial.Empty, 0, 0)
end
end
function updatePreviewSelection(position)
if not position then
return
elseif depth == 0 then -- or not mouse.Target
disablePreview()
return
end
local vectorPos = Vector3.new(position.x, position.y, position.z)
local cellPos = WorldToCellPreferSolid(c, vectorPos)
local solidCell = WorldToCellPreferSolid(c, vectorPos)
-- If nothing was hit, do the plane intersection.
if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then
local success = false
success, cellPos = PlaneIntersection(vectorPos)
if not success then
if mouse.Target then
cellPos = solidCell
else
return
end
end
end
local regionToSelect
if depth > 0 then
local yWithDepth
if brushheight then
yWithDepth = brushheight + depth
else
yWithDepth = cellPos.y + depth
end
local lowY
if brushheight then
lowY = brushheight + 1
else
local findY = findLowPoint(cellPos.x, yWithDepth, cellPos.z)
if findY < yWithDepth then
lowY = findY
else
lowY = yWithDepth
end
end
local lowVec = CellCenterToWorld(
c,
cellPos.x - radius,
lowY - 1,
cellPos.z - radius
)
local highVec = CellCenterToWorld(
c,
cellPos.x + radius,
yWithDepth + 1,
cellPos.z + radius
)
selectionBox.Color = BrickColor.new "Lime green"
regionToSelect = Region3.new(lowVec, highVec)
else
local yPos = cellPos.y + depth
if brushheight then
yPos = brushheight + depth
end
local lowVec =
CellCenterToWorld(c, cellPos.x - radius, yPos, cellPos.z - radius)
local highVec = CellCenterToWorld(
c,
cellPos.x + radius,
maxYExtents,
cellPos.z + radius
)
selectionBox.Color = BrickColor.new "Really red"
regionToSelect = Region3.new(lowVec, highVec)
end
selectionPart.Size = regionToSelect.Size - Vector3.new(-4, 4, -4)
selectionPart.CFrame = regionToSelect.CFrame
enablePreview()
end
function doFillCells(position, mouseDrag, needsCellPos)
if mouseDrag then
local timeBetweenFills = tick() - lastCellFillTime
local totalDragPixels = math.abs(mouseDrag.x) + math.abs(mouseDrag.y)
local editDistance = (
(workspace.CurrentCamera :: Camera).CoordinateFrame.p
- Vector3.new(position.x, position.y, position.z)
).Magnitude
if timeBetweenFills <= 0.05 then
if editDistance * totalDragPixels < 450 then
lastCellFillTime = tick()
return
end
end
end
local x = position.x
local y = position.y
local z = position.z
if needsCellPos then
local cellPos = WorldToCellPreferSolid(c, Vector3.new(x, y, z))
local solidCell = WorldToCellPreferSolid(c, Vector3.new(x, y, z))
-- If nothing was hit, do the plane intersection.
if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then
local success = false
success, cellPos = PlaneIntersection(Vector3.new(x, y, z))
if not success then
if mouse.Target then
cellPos = solidCell
else
return
end
end
end
x = cellPos.x
y = cellPos.y
z = cellPos.z
end
if brushheight == nil then
brushheight = y
end
brush(x, brushheight, z)
lastCellFillTime = tick()
end
function mouseMoved()
if not on then
return
elseif mousedown == true then
if mousemoving then
return
end
mousemoving = true
local currMousePos = Vector2.new(mouse.X, mouse.Y)
local mouseDrag = currMousePos - lastMousePos
doFillCells(mouse.Hit, mouseDrag, true)
lastMousePos = currMousePos
mousemoving = false
end
updatePreviewSelection(mouse.Hit)
end
function buttonDown()
if on then
local firstCellPos = WorldToCellPreferSolid(
c,
Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z)
)
local solidCell = WorldToCellPreferSolid(
c,
Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z)
)
-- If nothing was hit, do the plane intersection.
if 0 == GetCell(c, solidCell.X, solidCell.Y, solidCell.Z).Value then
local success = false
success, firstCellPos = PlaneIntersection(
Vector3.new(mouse.Hit.x, mouse.Hit.y, mouse.Hit.z)
)
if not success then
if not mouse.Target then
return
end
firstCellPos = solidCell
end
end
local celMat =
GetCell(c, firstCellPos.x, firstCellPos.y, firstCellPos.z)
if celMat.Value > 0 then
material = celMat.Value
else
if 0 == material then
-- It was nothing, give it a default type and the plane intersection.
material = 1
end
end
brushheight = firstCellPos.y
lastMousePos = Vector2.new(mouse.X, mouse.Y)
doFillCells(firstCellPos)
mousedown = true
end
end
local brushDragBar
function On()
if not c then
return
elseif this then
this:Activate(true)
end
if toolbarbutton then
toolbarbutton:SetActive(true)
end
if enablePreview then
enablePreview()
end
if brushDragBar then
brushDragBar.Visible = true
end
on = true
end
function Off()
if toolbarbutton then
toolbarbutton:SetActive(false)
end
if disablePreview then
disablePreview()
end
if brushDragBar then
brushDragBar.Visible = false
end
on = false
end
------
--GUI-
------
--load library for with sliders
local RbxGui = LoadLibrary "RbxGui"
-- Create a standard dropdown. Use this for all dropdowns in the popup so it is easy to standardize.
-- name - What to set the text label name as.
-- pos - Where to position the label. Should be of type UDim2.
-- values - A table of the values that will be in the dropbox, in the order they are to be shown.
-- initValue - Initial value the dropdown should be set to.
-- funcOnChange - Function to run when a dropdown selection is made.
-- parent - What to set the parent as.
-- Return:
-- dropdown - The dropdown gui.
-- updateSelection - Object to use to change the current dropdown.
function CreateStandardDropdown(
name,
pos,
values,
initValue,
funcOnChange,
parent
)
-- Create a dropdown selection for the modes to fill in a river
local dropdown, updateSelection =
RbxGui.CreateDropDownMenu(values, funcOnChange)
dropdown.Name = name
dropdown.Position = pos
dropdown.Active = true
dropdown.Size = UDim2.new(0, 150, 0, 32)
dropdown.Parent = parent
updateSelection(initValue)
return dropdown, updateSelection
end
-- Create a standardized slider.
-- name - Name to use for the slider.
-- pos - Position to display the slider at.
-- steps - How many steps there are in the slider.
-- funcOnChange - Function to call when the slider changes.
-- initValue - Initial value to set the slider to. If nil the slider stays at the default.
-- parent - What to set as the parent.
-- Return:
-- sliderGui - Slider gui object.
-- sliderPosition - Object that can set the slider value.
-- function CreateStandardSlider(
-- name,
-- pos,
-- lengthBarPos,
-- steps,
-- funcOnChange,
-- initValue,
-- parent
-- )
-- local sliderGui, sliderPosition =
-- RbxGui.CreateSlider(steps, 0, UDim2.new(0, 0, 0, 0))
-- sliderGui.Name = name
-- sliderGui.Parent = parent
-- sliderGui.Position = pos
-- sliderGui.Size = UDim2.new(0, 160, 0, 20)
-- local lengthBar = sliderGui:FindFirstChild "Bar"
-- lengthBar.Size = UDim2.new(1, -20, 0, 5)
-- lengthBar.Position = lengthBarPos
-- if nil ~= funcOnChange then
-- sliderPosition.Changed:connect(function()
-- funcOnChange(sliderPosition)
-- end)
-- end
-- if nil ~= initValue then
-- sliderPosition.Value = initValue
-- end
-- return sliderGui, sliderPosition
-- end
--screengui
local g = New "ScreenGui" {
Name = "TerrainBrushGui",
Parent = CoreGui,
}
local elevationFrame, elevationHelpFrame, elevationCloseEvent
brushDragBar, elevationFrame, elevationHelpFrame, elevationCloseEvent =
RbxGui.CreatePluginFrame(
"Terrain Brush",
UDim2.new(0, 151, 0, 160),
UDim2.new(0, 0, 0, 0),
false,
g
)
brushDragBar.Visible = false
elevationCloseEvent.Event:connect(function()
Off()
end)
elevationHelpFrame.Size = UDim2.new(0, 200, 0, 210)
local helpText =
[[Drag the mouse by holding the left mouse button to either create or destroy terrain defined by the selection box.
Radius:
Half of the width of the selection box to be used.
Height:
How tall to make terrain from the mouse location. If this value is negative, the brush will remove terrain instead of creating terrain (indicated by the red selection box).
]]
New "TextLabel" {
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size12,
TextColor3 = Color3.new(1, 1, 1),
BackgroundTransparency = 1,
TextWrapped = true,
Size = UDim2.new(1, -10, 1, -10),
Position = UDim2.new(0, 5, 0, 5),
TextXAlignment = Enum.TextXAlignment.Left,
Text = helpText,
Parent = elevationHelpFrame,
}
-- current radius display label
local radl = New "TextLabel" {
Position = UDim2.new(0, 0, 0, 70),
Size = UDim2.new(1, 0, 0, 14),
Text = "",
BackgroundColor3 = Color3.new(0.4, 0.4, 0.4),
TextColor3 = Color3.new(0.95, 0.95, 0.95),
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size14,
TextXAlignment = Enum.TextXAlignment.Left,
BorderColor3 = Color3.new(0, 0, 0),
BackgroundTransparency = 1,
Parent = elevationFrame,
}
--radius slider
local radSliderGui, radSliderPosition =
RbxGui.CreateSlider(5, 0, UDim2.new(0, 10, 0, 92))
radSliderGui.Parent = elevationFrame
local radBar = radSliderGui:FindFirstChild "Bar"
radBar.Size = UDim2.new(1, -20, 0, 5)
radSliderPosition.Changed:connect(function()
radius = radSliderPosition.Value + 1
radl.Text = " Radius: " .. radius
end)
radSliderPosition.Value = radius - 1
--current depth factor display label
local dfl = New "TextLabel" {
Position = UDim2.new(0, 0, 0, 110),
Size = UDim2.new(1, 0, 0, 14),
Text = "",
BackgroundColor3 = Color3.new(0.4, 0.4, 0.4),
TextColor3 = Color3.new(0.95, 0.95, 0.95),
Font = Enum.Font.ArialBold,
FontSize = Enum.FontSize.Size14,
BorderColor3 = Color3.new(0, 0, 0),
TextXAlignment = Enum.TextXAlignment.Left,
BackgroundTransparency = 1,
Parent = elevationFrame,
}
--depth factor slider
local addSliderGui, addSliderPosition =
RbxGui.CreateSlider(31, 0, UDim2.new(0, 10, 0, 132))
addSliderGui.Parent = elevationFrame
local dfBar = addSliderGui:FindFirstChild "Bar"
dfBar.Size = UDim2.new(1, -20, 0, 5)
addSliderPosition.Changed:connect(function()
depth = addSliderPosition.Value
dfl.Text = " Height: " .. depth
end)
addSliderPosition.Value = buildTerrainHeight
--depth factor slider
local removeSliderGui, removeSliderPosition =
RbxGui.CreateSlider(31, 0, UDim2.new(0, 10, 0, 132))
removeSliderGui.Parent = elevationFrame
dfBar = removeSliderGui:FindFirstChild "Bar"
dfBar.Size = UDim2.new(1, -20, 0, 5)
removeSliderPosition.Changed:connect(function()
depth = -removeSliderPosition.Value
dfl.Text = " Height: " .. depth
end)
removeSliderPosition.Value = removeTerrainDepth
-- Set which mode is to be used. Show/hide as needed.
--
-- mode - Which build mode to run.
function SetMode(setmode)
if setmode == buildTerrainMode then
addSliderGui.Visible = true
local hold = addSliderPosition.Value
addSliderPosition.Value = 0
addSliderPosition.Value = hold
removeSliderGui.Visible = false
elseif setmode == removeTerrainMode then
addSliderGui.Visible = false
removeSliderGui.Visible = true
local hold = removeSliderPosition.Value
removeSliderPosition.Value = 0
removeSliderPosition.Value = hold
end
end
-- Create/Remove mode
-- Create the build mode gui.
-- local buildModeLabel = CreateStandardLabel(
-- "BuildModeLabel",
-- UDim2.new(0, 8, 0, 10),
-- UDim2.new(0, 67, 0, 14),
-- "Build Mode:",
-- elevationFrame
-- )
local _, buildModeSet = CreateStandardDropdown(
"BuildModeDropdown",
UDim2.new(0, 1, 0, 26),
{ buildTerrainMode, removeTerrainMode },
buildMode,
function(value)
if "Add" == value then
SetMode(buildTerrainMode)
elseif "Remove" == value then
SetMode(removeTerrainMode)
end
end,
elevationFrame
)
--[[
buildModeSet(buildTerrainMode)
buildModeSet(removeTerrainMode)
--]]
buildModeSet(mode)
SetMode(mode)
this.Deactivation:connect(function()
Off()
end)
--------------------------
--SUCCESSFUL LOAD MESSAGE-
--------------------------
loaded = true