Files
PlaneBuilding/scripts/modules/PlacementModule.lua
T

627 lines
22 KiB
Lua

local run = game:GetService("RunService")
local inputService = game:GetService("UserInputService")
local collectionService = _G.Tags
local mouseModule = require(_G.Modules.MouseModule)
local methodModule = require(_G.Modules.MethodModule)
local partStateModule = require(_G.Modules.PartStateModule)
local selectionBox = script.SelectionBox
local adjacentBox = script.AdjacentBox
local statGui
if game.Players.LocalPlayer then
statGui = game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("StatGui")
end
local gui
local current = nil
local mode = "place"
local realMode = "place"
local rotSnap = 15
local rotSnapKey = 45.
local moveSnap = 0.5
local modelScaleSnap = 0.1
local touchModeConnect = true
local touchingBoxes = {}
local controlModifier = false -- embed
local shiftModifier = false -- lower snap
local ModeButtons = {
clone = nil,
resize = nil
}
local pointer = nil
local function temp()
local a = Instance.new("Part")
a.Parent = game.Workspace
a.Size = Vector3.new(1,1,1)
a.Anchored = true
a.CanCollide = false
a.Transparency = 0.5
a.Material = Enum.Material.Neon
return a
end
local a = temp()
local b = temp()
local modeEnd = function()
if current.axes then current.axes:Destroy() end
end
local focusAxes = function()
current.axes:ScaleTo(current.size.Magnitude*1.3)
methodModule.setItemCFrame(current.axes,current.cframe)
for _,part in pairs(current.axes:GetChildren()) do
if not current.axis then
part.Transparency = 0.5
else
part.Transparency = 1
end
end
if current.axis then current.axis.Transparency = 0 end
end
local createAxes = function(name)
current.axes = _G.Storage.Assets[name]:Clone()
current.axes.Parent = game.Workspace
current.axes.PrimaryPart.Shape = Enum.PartType.Ball
focusAxes()
end
local getBounds = function()
local cframe,size
if current.model:IsA("Model") then
cframe,size = current.model:GetBoundingBox()
else
cframe = current.model.CFrame
size = current.model.size
end
current.cframe = cframe
current.size = size
end
local styleModel = function()
local function style(part)
part.Anchored = true
part.CanCollide = false
end
if current.model:IsA("BasePart") then style(current.model) end
for _,part in pairs(current.model:GetChildren()) do
if part:IsA("BasePart") then
style(part)
end
end
end
local distanceAlong = function(o : Vector3,n : Vector3,p : Vector3) -- origin, normal, point
local k = (n.X*(p.X-o.X)+n.Y*(p.Y-o.Y)+n.Z*(p.Z-o.Z))--/(n.x*n.x+n.y*n.y+n.z*n.z) -- is 1
return (n*k+o-p).Magnitude
end
local processPointer = function(inputObject)
local position = inputObject.Position
local result,ray = mouseModule.getMouseHit() -- get whatever we're hovering
current.location = position
current.ray = ray
current.result = result
end
local function axisClick(inputObject) -- For whenever we click in a mode supporting axis
if current.axes then -- if there are axes (there should be)
processPointer(inputObject)
if current.result then
if current.result.Instance.Parent == current.axes then -- if it's an axis
current.axis = current.result.Instance -- set it
pointer = inputObject -- set the inputObject for current selection
end
end
end
end
local function round(v : Vector3,snap) -- just round a vector based on the snapping
snap = snap or moveSnap
return Vector3.new(math.round(v.X/snap)*snap,math.round(v.Y/snap)*snap,math.round(v.Z/snap)*snap)
end
local function mask(a : Vector3,b : Vector3) -- multiply each axis together
return Vector3.new(a.X*b.X,a.Y*b.Y,a.Z*b.Z)
end
local function clamp(a : Vector3, min : Vector3, max : Vector3)
return Vector3.new(math.clamp(a.X,min.X,max.X),math.clamp(a.Y,min.Y,max.Y),math.clamp(a.Z,min.Z,max.Z))
end
local function rayProjectToRay(ray,axis,root)
local n2 = ray.Direction
local o2 = ray.Origin
local n1 = (axis.Position - axis.Parent.PrimaryPart.Position).Unit
local o1 = root or current.axis.Position
-- using more vector magic to find the closest point on the axis line to the ray of the pointer
local s = n2:Dot(n1)
return o1+n1*((n1:Dot(o2-o1)+s*(n2:Dot(o1-o2)))/(1-s*s))
end
local function axisDragClick(inputObject)
axisClick(inputObject)
if current.axis then
local p = rayProjectToRay(current.ray,current.axis)
current.modelOrigin = current.touchingCFrame
current.dragOrigin = current.axes.PrimaryPart.CFrame:PointToObjectSpace(p)
current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame)
end
end
local function rayProjectToPlane(ray,axis)
-- using some vector magic to find the point of intersection between the ray of the pointer and the plane of rotation (arc handle plane)
return ray.Origin+(axis.CFrame.RightVector:Dot(axis.Position-ray.Origin))/(axis.CFrame.RightVector:Dot(ray.Direction))*ray.Direction
end
local function axisRotateClick(inputObject)
axisClick(inputObject)
if current.axis then
local p = rayProjectToPlane(current.ray,current.axis)
current.dragOrigin = current.axis.CFrame:PointToObjectSpace(p)
current.modelOrigin = current.touchingCFrame
current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame)
current.initialRotation = current.rotation
end
end
local modeStart = {}
modeStart.clone = function()
modeEnd()
end
modeStart.rotate = function()
modeEnd()
createAxes("RotateAxes")
end
modeStart.resize = function()
modeEnd()
createAxes("ResizeAxes")
for _,item in pairs(current.axes:GetChildren()) do
local axis = item.Name:match("Part(.)")
if axis then
if (current.scaleTerms or Vector3.one)[axis] == 0 then axis:Destroy() end -- if the scale terms say that this axis should not be scaled
if current.model:IsA("Model") then item.Color = Color3.fromRGB(13, 105, 172) end
end
end
end
modeStart.move = function()
modeEnd()
createAxes("MoveAxes")
end
modeStart.place = function()
modeEnd()
end
local function setMode(newMode)
for _,other_button in pairs(_G.PlaceModeFrame:GetChildren()) do
if other_button:IsA("TextButton") then
other_button.BackgroundColor3 = Color3.new(1,1,1)
if (current.disabled or {})[other_button.Name] then other_button.Visible = false else other_button.Visible = true end
end
end
realMode = newMode
mode = newMode
gui.Confirm.Text = "Place"
if not current.touching and newMode ~= "place" then
mode = "place"
gui.Confirm.Text = "Confirm"
end
_G.PlaceModeFrame[realMode].BackgroundColor3 = Color3.new(1, 0.956863, 0.345098)
_G.PlaceModeFrame[mode].BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
modeStart[mode]()
end
local function confirmPlace(inputObject,forced)
if not current then return end
if realMode ~= mode then setMode(realMode) return end
if (inputObject and inputObject.UserInputType == Enum.UserInputType.MouseButton1) or not inputObject then
current:confirm(forced)
if not current then return end
methodModule.setItemCFrame(current.model,CFrame.new())
current.touching = nil
current.touchingCFrame = CFrame.new()
setMode(realMode)
end
end
local modeSnap = {
}
local modeClick = {}
modeClick.place = function(inputObject)
confirmPlace(inputObject,false)
end
modeClick.clone = function(inputObject)
setMode("place")
end
modeClick.move = function(inputObject)
axisDragClick(inputObject)
current.moveOffset = current.newMoveOffset or Vector3.zero
end
modeClick.rotate = axisRotateClick
modeClick.resize = function(inputObject)
axisDragClick(inputObject)
current.startSize = current.size
if current.model:IsA("Model") then current.startScale = current.model:GetScale() end
end
local modeLoop = {}
modeLoop.default = function()
if current.touching and current.touching:IsA("BasePart") then
methodModule.setItemCFrame(current.model,current.touching.CFrame:ToWorldSpace(current.touchingCFrame or CFrame.identity))
end
statGui.Enabled = not not statGui.Adornee
getBounds()
end
modeLoop.clone = function()
current:clone(current.touching)
modeLoop.default()
end
modeLoop.rotate = function()
modeLoop.default()
focusAxes()
if pointer then
processPointer(pointer)
local p = rayProjectToPlane(current.ray,current.axis)
local plane = current.touching.CFrame:ToWorldSpace(current.axisOrigin)
local toDrag = plane.Rotation:PointToWorldSpace(current.dragOrigin)
local toPointer = p-plane.Position
local angle = math.round(toDrag:Angle(toPointer,plane.RightVector)/math.rad(rotSnap))*math.rad(rotSnap)
local modelCFrame = current.touching.CFrame:ToWorldSpace(current.modelOrigin) -- cframe of axes/object in the world
local newCFrame = plane.Rotation:ToWorldSpace(CFrame.Angles(angle,0,0)*plane.Rotation:ToObjectSpace(modelCFrame.Rotation)) + modelCFrame.Position -- put it back into object space
current.rotation = current.initialRotation * newCFrame:ToObjectSpace(modelCFrame):Inverse()
current.touchingCFrame = current.touching.CFrame:ToObjectSpace(newCFrame) -- need to add rotation
statGui.TextLabel.Text = tonumber(math.round(math.deg(angle)*1000)/1000)
statGui.Adornee = current.model
else
statGui.Adornee = nil
end
end
local function psumag(v)
return v.X + v.Y + v.Z
end
modeLoop.resize = function()
modeLoop.default()
focusAxes()
if pointer then
processPointer(pointer)
local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin)
local start = origin:PointToWorldSpace(current.dragOrigin) -- the start of the scale drag
local diff = (rayProjectToRay(current.ray,current.axis)-start) -- difference between start and end
local scaleChange = origin.Rotation:PointToObjectSpace(diff)
if current.model:IsA("Model") then
local before = mask(current.startSize,current.dragOrigin.Unit) -- the size in this axis before scaling
local after = psumag(mask(scaleChange,current.dragOrigin.Unit)) -- the size in this axis after scaling
local scale = math.round(((((after*math.sign(psumag(before)))/psumag(before))+1)/modelScaleSnap)*current.startScale)*modelScaleSnap
scale = math.clamp(scale,0.5,current.model:GetAttribute("MaxSize") or 3)
current.model:ScaleTo(scale)
current.model:SetAttribute("Size",Vector3.new(scale,0,0))
current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(((scale/current.startScale)-1)*before*0.5)
statGui.TextLabel.Text = tonumber(math.round(scale*1000)/1000)
else
local change = round(mask(scaleChange,current.scaleTerms or Vector3.one))
current.model.Size = clamp(current.startSize + mask(change,current.dragOrigin.Unit),Vector3.one*0.1,current.model:GetAttribute("MaxSize") or Vector3.one*20)
current.model:SetAttribute("Size",current.model.Size)
current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(change/2)
statGui.TextLabel.Text = tonumber(math.round(mask(current.model.Size,current.dragOrigin.Unit).Magnitude*1000)/1000)
end
statGui.Adornee = current.model
else
statGui.Adornee = nil
end
modeLoop.default() -- the resize system has this weird lag, i cannot be asked fixing it unfortunately
end
modeLoop.move = function()
modeLoop.default()
focusAxes()
if pointer then
processPointer(pointer)
local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin)
local start = origin:PointToWorldSpace(current.dragOrigin)
local diff = (rayProjectToRay(current.ray,current.axis,start)-start) -- difference between start and end
diff = current.touching.CFrame.Rotation:PointToObjectSpace(diff)
local offset = psumag(mask(current.moveOffset,diff.Unit))
local len = math.abs(psumag(mask(current.size,origin:ToObjectSpace(current.axis.CFrame).RightVector)))
diff = diff.Unit * math.round(math.clamp(diff.Magnitude,-len*0.5-offset,len*0.5-offset)/moveSnap) * moveSnap
current.touchingCFrame = current.modelOrigin + diff
current.newMoveOffset = current.moveOffset + diff
statGui.TextLabel.Text = tonumber(math.round(diff.Magnitude*1000)/1000)
statGui.Adornee = current.model
else
statGui.Adornee = nil
end
end
modeLoop.place = function()
local result,ray,update = mouseModule.getMouseHit()
if result and result.Instance and update then
current.touching = result.Instance
if current.touching.Locked then adjacentBox.Adornee = nil end
if not current.touching then return end
if not current.touching.Position then print(current.touching) return end
local rotation = (current.touching.CFrame - current.touching.Position)
rotation = rotation * current.rotation
local s = current.size*0.5 -- So half the size of the part, rotated correctly
local max = 0
local points = {Vector3.new(s.X,s.Y,s.Z),Vector3.new(s.X,-s.Y,s.Z),Vector3.new(-s.X,s.Y,s.Z),Vector3.new(-s.X,-s.Y,s.Z),Vector3.new(s.X,s.Y,-s.Z),Vector3.new(s.X,-s.Y,-s.Z),Vector3.new(-s.X,s.Y,-s.Z),Vector3.new(-s.X,-s.Y,-s.Z)}
for _,point in pairs(points) do
max = math.max(max,result.Normal:Dot(rotation:PointToWorldSpace(point)))
end
local push = max
local snap = moveSnap
if shiftModifier then snap = moveSnap * 0.5 end
local depth = (Ray.new(result.Position,-result.Normal):ClosestPoint(current.touching.Position)-result.Position).Magnitude
local flat = current.touching.CFrame:PointToWorldSpace(round(current.touching.CFrame:PointToObjectSpace(result.Position - result.Normal * depth),snap))
if controlModifier or current.model:HasTag("Node") then push = 0 end
local cframe = rotation + flat + result.Normal * (depth + push) -- Add the rotation, Snapped movement perpendicular to normal and the Offset/Pushback
if current.model:HasTag("Node") then
local part
if current.touching:GetAttribute("Nodes") then
part = current.touching
elseif current.touching.Parent:GetAttribute("Nodes") then
part = current.touching.Parent
end
if part then
local ideal
local current_distance = 1
for _,node in ipairs(partStateModule.get_points(part)) do
local distance = (node - result.Position).Magnitude
if distance < current_distance then
current_distance = distance
if part:IsA("Model") then
ideal = part.PrimaryPart
else
ideal = part
end
if current.model:IsA("Model") then
cframe = rotation + node
else
cframe = current.model.CFrame.Rotation + node
end
current.touching = ideal
end
end
end
end
if current.touching:HasTag("Node") then
if current.model:IsA("Model") then
cframe = rotation + current.touching.Position
else
cframe = current.model.CFrame.Rotation + current.touching.Position
end
end
methodModule.setItemCFrame(current.model,cframe)
current.touchingCFrame = current.touching.CFrame:ToObjectSpace(cframe)
current.newMoveOffset = Vector3.zero
adjacentBox.Adornee = current.touching
end
getBounds()
end
local function render()
if not current then return end
controlModifier = inputService:IsKeyDown(Enum.KeyCode.LeftControl) or inputService:IsKeyDown(Enum.KeyCode.LeftMeta)
shiftModifier = inputService:IsKeyDown(Enum.KeyCode.LeftShift)
modeLoop[mode]()
end
--[[
config:
- Can Resize
- Confirm Callback
]]
local placetouchbuttoncolor
local function toggleTouching()
touchModeConnect = not touchModeConnect
if touchModeConnect then
_G.PlaceTouchButton.BackgroundColor3 = placetouchbuttoncolor
else
_G.PlaceTouchButton.BackgroundColor3 = Color3.new(1,1,1)
end
end
function begin(model,config)
if current then current:stop() end
gui.Visible = true
current = config or {}
current.stop = function(config)
if not config then warn("No config passed to PlacementModule; current:stop(). Using current.") config = current end
if not config then return end
if config.axes then config.axes:Destroy() end
if config.stopped then config:stopped() end
current = nil
selectionBox.Adornee = nil
adjacentBox.Adornee = nil
for _,box in pairs(touchingBoxes) do
box:Destroy()
end
touchingBoxes = {}
gui.Visible = false
end
model.Parent = game.Workspace.Hidden
if model:IsA("Model") then
model.PrimaryPart.Anchored = true
else
model.Anchored = true
end
ModeButtons.resize.Visible = config.resize
if not config.resize and mode == "resize" then mode = "place" end
if not config.rotate and mode == "rotate" then mode = "place" end
_G.PlaceTouchButton.Visible = config.touch
selectionBox.Adornee = model
current.model = model
if not current.model then error("urmum") end
current.connections = current.connections or {}
current.rotation = current.model:GetAttribute("Rotation") or CFrame.identity
setMode(realMode)
styleModel()
getBounds()
modeStart[mode]()
return config
end
function init(_gui)
gui = _gui
_G:WaitFor("PlaceModeFrame") _G:WaitFor("PlaceSnapFrame") _G:WaitFor("PlaceCancelButton")
statGui.Enabled = false
placetouchbuttoncolor = _G.PlaceTouchButton.BackgroundColor3
gui.Visible = false
run:BindToRenderStep("placementmodule",Enum.RenderPriority.Last.Value,render)
local touchOverlapParams = OverlapParams.new()
touchOverlapParams.MaxParts = 3
touchOverlapParams.FilterType = Enum.RaycastFilterType.Include
touchOverlapParams.FilterDescendantsInstances = {game.Workspace.Objects}
touchOverlapParams.RespectCanCollide = true
touchOverlapParams.Tolerance = 0.05
run.Heartbeat:Connect(function() -- Touching parts loop
if current and current.model and current.touch then -- in placement
local loop_current = current
local connections = {}
if touchModeConnect then
local items
if current.model:IsA("Model") then -- Check all potential subparts
items = current.model:GetChildren()
elseif current.model:IsA("BasePart") then
items = {current.model}
end
local count = 0
for _,item in pairs(items) do
if item:IsA("BasePart") then
for _,part in pairs(workspace:GetPartsInPart(item,touchOverlapParams)) do -- Get touching parts
if current.connections[part] then connections[part] = part continue end
local object = _G.Methods.getOwnedOrNil(_G.Player,part) -- Check if this is owned
if object then
connections[object] = true -- Add to the current touching ones
current.connections[object] = { -- Add to the final list
part = object
}
if current.model ~= item then current.connections[object].name = item.Name end
--if part == current.touching then continue end
local box = object:FindFirstChild("TouchingBox")
if not box then box = script.TouchingBox:Clone() box.Parent = object end
touchingBoxes[object] = box
end
end
end
if count > 3 then wait() count = 0 else count = count + 1 end -- Dangerous! Things can change after this (wait)
if current ~= loop_current then return end -- Current is invalid now so stop!
end
elseif current.touching then
local connection = {
part = current.touching
}
current.connections[current.touching] = connection
connections[current.touching] = connection
end
for index,connection in pairs(current.connections) do -- Remove all invalid connections too
if not connections[index] then
if touchingBoxes[index] then
touchingBoxes[index]:Destroy()
touchingBoxes[index] = nil
end
current.connections[index] = nil
end
end
end
end)
_G.PlaceTouchButton.Activated:Connect(toggleTouching)
inputService.InputBegan:Connect(function(inputObject,irrelevant)
if not current then return end
if irrelevant then return end
local sign = 1
if shiftModifier then sign = -1 end
if inputObject.UserInputType == Enum.UserInputType.Keyboard then
if inputObject.KeyCode == Enum.KeyCode.R then
if controlModifier then
_G.PlaceSnapInputFrame.RotSnap:CaptureFocus()
else
current.rotation = current.rotation * CFrame.Angles(math.rad(rotSnapKey*sign),0,0)
end
elseif inputObject.KeyCode == Enum.KeyCode.T then
if controlModifier then
_G.PlaceSnapInputFrame.MoveSnap:CaptureFocus()
else
current.rotation = current.rotation * CFrame.Angles(0,math.rad(rotSnapKey*sign),0)
end
elseif inputObject.KeyCode == Enum.KeyCode.Y then
if controlModifier then
_G.PlacePolyInputFrame.PolyThicknessInputBox:CaptureFocus()
else
current.rotation = current.rotation * CFrame.Angles(0,0,math.rad(rotSnapKey*sign))
end
elseif inputObject.KeyCode == Enum.KeyCode.G and not controlModifier then
current.rotation = CFrame.identity
elseif inputObject.KeyCode == Enum.KeyCode.E then
setMode("place")
elseif inputObject.KeyCode == Enum.KeyCode.Q and current.rotate then
setMode("rotate")
elseif inputObject.KeyCode == Enum.KeyCode.Z and current.resize then
setMode("resize")
elseif inputObject.KeyCode == Enum.KeyCode.X then
setMode("move")
elseif inputObject.KeyCode == Enum.KeyCode.U and current.touch then
toggleTouching()
elseif inputObject.KeyCode == Enum.KeyCode.Backspace then
if current and current.cancel then current:cancel() end
end
elseif inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
if not shiftModifier then modeClick[mode](inputObject) else confirmPlace(inputObject,true) end
end
end)
inputService.TouchStarted:Connect(function(object)
modeClick[mode](object)
end)
inputService.InputEnded:Connect(function(inputObject,irrelevant)
if current and inputObject == pointer then
pointer = nil
current.axis = nil
end
end)
gui.Confirm.Activated:Connect(function()
confirmPlace(nil,true)
end)
_G.PlaceCancelButton.Activated:Connect(function()
if current and current.cancel then current:cancel() end
end)
for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do
if button:IsA("TextButton") then
ModeButtons[button.Name] = button
if mode == button.Name then
button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
end
button.Activated:Connect(function()
setMode(button.Name)
modeStart[mode]()
end)
end
end
local moveSnapInput = _G.PlaceSnapInputFrame.MoveSnap
local function evaluateSnapInput(text)
return text:match("([%d%.]+).-")
end
_G.PlaceSnapInputFrame.MoveSnap.FocusLost:Connect(function()
local num = evaluateSnapInput(moveSnapInput.Text)
if num then moveSnap = tonumber(num) end
moveSnapInput.Text = tostring(moveSnap).." stud"
end)
local rotSnapInput = _G.PlaceSnapInputFrame.RotSnap
_G.PlaceSnapInputFrame.RotSnap.FocusLost:Connect(function()
local num = evaluateSnapInput(rotSnapInput.Text)
if num then rotSnap = tonumber(num) end
rotSnapInput.Text = tostring(rotSnap).." deg"
end)
moveSnapInput.Text = tostring(moveSnap).." stud"
rotSnapInput.Text = tostring(rotSnap).." deg"
local snap_buttons = {}
for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do
if button:IsA("TextButton") then
table.insert(snap_buttons,button)
if mode == button.Name then
button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
end
button.Activated:Connect(function()
setMode(button.Name)
modeStart[mode]()
end)
end
end
end
return getfenv()