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()