3000 lines
82 KiB
Plaintext
3000 lines
82 KiB
Plaintext
-- RbxStamper
|
|
print "[Mercury]: Loaded corescript 73157242"
|
|
|
|
local ChangeHistoryService = game:GetService "ChangeHistoryService"
|
|
local InsertService = game:GetService "InsertService"
|
|
local JointsService = game:GetService "JointsService"
|
|
local Lighting = game:GetService "Lighting"
|
|
local Players = game:GetService "Players"
|
|
|
|
local News = require "../Modules/New"
|
|
local New = News.New
|
|
local Hydrate = News.Hydrate
|
|
local BaseUrl = require "../Modules/BaseUrl"
|
|
local path = BaseUrl.path
|
|
|
|
local RbxStamper = {}
|
|
|
|
-- 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:
|
|
-- cellPos - The terrain cell intersection point if there is one, vectorPos if there isn't.
|
|
-- hit - Whether there was a plane intersection. Value is true if there was, false if not.
|
|
local function PlaneIntersection(vectorPos)
|
|
local hit = false
|
|
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
|
|
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 = workspace.Terrain:WorldToCell(intersection)
|
|
hit = true
|
|
end
|
|
end
|
|
|
|
return cellPos, hit
|
|
end
|
|
|
|
-- Purpose:
|
|
-- Checks for terrain touched by the mouse hit.
|
|
-- Will do a plane intersection if no terrain is touched.
|
|
--
|
|
-- mouse - Mouse to check the .hit for.
|
|
--
|
|
-- Return:
|
|
-- cellPos - Cell position hit. Nil if none.
|
|
local function GetTerrainForMouse(mouse)
|
|
-- There was no target, so all it could be is a plane intersection.
|
|
-- Check for a plane intersection. If there isn't one then nothing will get hit.
|
|
local cell = workspace.Terrain:WorldToCellPreferSolid(
|
|
Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)
|
|
)
|
|
-- If nothing was hit, do the plane intersection.
|
|
if 0 == workspace.Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then
|
|
cell = nil
|
|
local planeLoc, hit = PlaneIntersection(
|
|
Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)
|
|
)
|
|
if hit then
|
|
cell = planeLoc
|
|
end
|
|
end
|
|
return cell
|
|
end
|
|
|
|
-- setup helper functions
|
|
local insertBoundingBoxOverlapVector = Vector3.new(0.3, 0.3, 0.3) -- we can still stamp if our character extrudes into the target stamping space by .3 or fewer units
|
|
|
|
-- rotates a model by yAngle radians about the global y-axis
|
|
local function rotatePartAndChildren(part, rotCF, offsetFromOrigin)
|
|
-- rotate this thing, if it's a part
|
|
if part:IsA "BasePart" then
|
|
part.CFrame = (rotCF * (part.CFrame - offsetFromOrigin))
|
|
+ offsetFromOrigin
|
|
end
|
|
|
|
-- recursively do the same to all children
|
|
local partChildren = part:GetChildren()
|
|
for c = 1, #partChildren do
|
|
rotatePartAndChildren(partChildren[c], rotCF, offsetFromOrigin)
|
|
end
|
|
end
|
|
|
|
local function modelRotate(model, yAngle)
|
|
local rotCF = CFrame.Angles(0, yAngle, 0)
|
|
local offsetFromOrigin = model:GetModelCFrame().p
|
|
|
|
rotatePartAndChildren(model, rotCF, offsetFromOrigin)
|
|
end
|
|
|
|
local function collectParts(object, baseParts, scripts, decals)
|
|
if object:IsA "BasePart" then
|
|
baseParts[#baseParts + 1] = object
|
|
elseif object:IsA "Script" then
|
|
scripts[#scripts + 1] = object
|
|
elseif object:IsA "Decal" then
|
|
decals[#decals + 1] = object
|
|
end
|
|
|
|
for _, child in pairs(object:GetChildren()) do
|
|
collectParts(child, baseParts, scripts, decals)
|
|
end
|
|
end
|
|
|
|
local cluster
|
|
|
|
local function clusterPartsInRegion(startVector, endVector)
|
|
cluster = workspace:FindFirstChild "Terrain"
|
|
|
|
local startCell = cluster:WorldToCell(startVector)
|
|
local endCell = cluster:WorldToCell(endVector)
|
|
|
|
local startX = startCell.X
|
|
local startY = startCell.Y
|
|
local startZ = startCell.Z
|
|
|
|
local endX = endCell.X
|
|
local endY = endCell.Y
|
|
local endZ = endCell.Z
|
|
|
|
if startX < cluster.MaxExtents.Min.X then
|
|
startX = cluster.MaxExtents.Min.X
|
|
end
|
|
if startY < cluster.MaxExtents.Min.Y then
|
|
startY = cluster.MaxExtents.Min.Y
|
|
end
|
|
if startZ < cluster.MaxExtents.Min.Z then
|
|
startZ = cluster.MaxExtents.Min.Z
|
|
end
|
|
|
|
if endX > cluster.MaxExtents.Max.X then
|
|
endX = cluster.MaxExtents.Max.X
|
|
end
|
|
if endY > cluster.MaxExtents.Max.Y then
|
|
endY = cluster.MaxExtents.Max.Y
|
|
end
|
|
if endZ > cluster.MaxExtents.Max.Z then
|
|
endZ = cluster.MaxExtents.Max.Z
|
|
end
|
|
|
|
for x = startX, endX do
|
|
for y = startY, endY do
|
|
for z = startZ, endZ do
|
|
if cluster:GetCell(x, y, z).Value > 0 then
|
|
return true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
local function findSeatsInModel(parent, seatTable)
|
|
if not parent then
|
|
return
|
|
elseif parent.className == "Seat" or parent.className == "VehicleSeat" then
|
|
table.insert(seatTable, parent)
|
|
end
|
|
local myChildren = parent:GetChildren()
|
|
for j = 1, #myChildren do
|
|
findSeatsInModel(myChildren[j], seatTable)
|
|
end
|
|
end
|
|
|
|
local function setSeatEnabledStatus(model, isEnabled)
|
|
local seatList = {}
|
|
findSeatsInModel(model, seatList)
|
|
|
|
if isEnabled then
|
|
-- remove any welds called "SeatWeld" in seats
|
|
for i = 1, #seatList do
|
|
local nextSeat = seatList[i]:FindFirstChild "SeatWeld"
|
|
while nextSeat do
|
|
nextSeat:Remove()
|
|
nextSeat = seatList[i]:FindFirstChild "SeatWeld"
|
|
end
|
|
end
|
|
else
|
|
-- put a weld called "SeatWeld" in every seat
|
|
-- this tricks it into thinking there's already someone sitting there, and it won't make you sit XD
|
|
for i = 1, #seatList do
|
|
local fakeWeld = Instance.new "Weld"
|
|
fakeWeld.Name = "SeatWeld"
|
|
fakeWeld.Parent = seatList[i]
|
|
end
|
|
end
|
|
end
|
|
|
|
local function autoAlignToFace(parts)
|
|
local aatf = parts:FindFirstChild "AutoAlignToFace"
|
|
if aatf then
|
|
return aatf.Value
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function getClosestAlignedWorldDirection(aVector3InWorld)
|
|
local xDir = Vector3.new(1, 0, 0)
|
|
local yDir = Vector3.new(0, 1, 0)
|
|
local zDir = Vector3.new(0, 0, 1)
|
|
local xDot = aVector3InWorld.x * xDir.x
|
|
+ aVector3InWorld.y * xDir.y
|
|
+ aVector3InWorld.z * xDir.z
|
|
local yDot = aVector3InWorld.x * yDir.x
|
|
+ aVector3InWorld.y * yDir.y
|
|
+ aVector3InWorld.z * yDir.z
|
|
local zDot = aVector3InWorld.x * zDir.x
|
|
+ aVector3InWorld.y * zDir.y
|
|
+ aVector3InWorld.z * zDir.z
|
|
|
|
if math.abs(xDot) > math.abs(yDot) and math.abs(xDot) > math.abs(zDot) then
|
|
if xDot > 0 then
|
|
return 0
|
|
end
|
|
return 3
|
|
elseif
|
|
math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot)
|
|
then
|
|
if yDot > 0 then
|
|
return 1
|
|
end
|
|
return 4
|
|
end
|
|
if zDot > 0 then
|
|
return 2
|
|
end
|
|
return 5
|
|
end
|
|
|
|
local function positionPartsAtCFrame3(aCFrame, currentParts)
|
|
local insertCFrame
|
|
if not currentParts then
|
|
return currentParts
|
|
end
|
|
if
|
|
currentParts and (currentParts:IsA "Model" or currentParts:IsA "Tool")
|
|
then
|
|
insertCFrame = currentParts:GetModelCFrame()
|
|
currentParts:TranslateBy(aCFrame.p - insertCFrame.p)
|
|
else
|
|
currentParts.CFrame = aCFrame
|
|
end
|
|
return currentParts
|
|
end
|
|
|
|
local function calcRayHitTime(rayStart, raySlope, intersectionPlane)
|
|
if math.abs(raySlope) < 0.01 then
|
|
return 0
|
|
end -- 0 slope --> we just say intersection time is 0, and sidestep this dimension
|
|
return (intersectionPlane - rayStart) / raySlope
|
|
end
|
|
|
|
local function modelTargetSurface(partOrModel, rayStart, rayEnd)
|
|
if not partOrModel then
|
|
return 0
|
|
end
|
|
|
|
local modelCFrame
|
|
local modelSize
|
|
if partOrModel:IsA "Model" then
|
|
modelCFrame = partOrModel:GetModelCFrame()
|
|
modelSize = partOrModel:GetModelSize()
|
|
else
|
|
modelCFrame = partOrModel.CFrame
|
|
modelSize = partOrModel.Size
|
|
end
|
|
|
|
local mouseRayStart = modelCFrame:pointToObjectSpace(rayStart)
|
|
local mouseRayEnd = modelCFrame:pointToObjectSpace(rayEnd)
|
|
local mouseSlope = mouseRayEnd - mouseRayStart
|
|
|
|
local xPositive = 1
|
|
local yPositive = 1
|
|
local zPositive = 1
|
|
if mouseSlope.X > 0 then
|
|
xPositive = -1
|
|
end
|
|
if mouseSlope.Y > 0 then
|
|
yPositive = -1
|
|
end
|
|
if mouseSlope.Z > 0 then
|
|
zPositive = -1
|
|
end
|
|
|
|
-- find which surface the transformed mouse ray hits (using modelSize):
|
|
local xHitTime = calcRayHitTime(
|
|
mouseRayStart.X,
|
|
mouseSlope.X,
|
|
modelSize.X / 2 * xPositive
|
|
)
|
|
local yHitTime = calcRayHitTime(
|
|
mouseRayStart.Y,
|
|
mouseSlope.Y,
|
|
modelSize.Y / 2 * yPositive
|
|
)
|
|
local zHitTime = calcRayHitTime(
|
|
mouseRayStart.Z,
|
|
mouseSlope.Z,
|
|
modelSize.Z / 2 * zPositive
|
|
)
|
|
|
|
local hitFace = 0
|
|
|
|
--if xHitTime >= 0 and yHitTime >= 0 and zHitTime >= 0 then
|
|
if xHitTime > yHitTime then
|
|
if xHitTime > zHitTime then
|
|
-- xFace is hit
|
|
hitFace = 1 * xPositive
|
|
else
|
|
-- zFace is hit
|
|
hitFace = 3 * zPositive
|
|
end
|
|
else
|
|
if yHitTime > zHitTime then
|
|
-- yFace is hit
|
|
hitFace = 2 * yPositive
|
|
else
|
|
-- zFace is hit
|
|
hitFace = 3 * zPositive
|
|
end
|
|
end
|
|
|
|
return hitFace
|
|
end
|
|
|
|
local function getBoundingBox2(partOrModel)
|
|
-- for models, the bounding box is defined as the minimum and maximum individual part bounding boxes
|
|
-- relative to the first part's coordinate frame.
|
|
local minVec = Vector3.new(math.huge, math.huge, math.huge)
|
|
local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
|
|
|
|
if partOrModel:IsA "Terrain" then
|
|
minVec = Vector3.new(-2, -2, -2)
|
|
maxVec = Vector3.new(2, 2, 2)
|
|
elseif partOrModel:IsA "BasePart" then
|
|
minVec = -0.5 * partOrModel.Size
|
|
maxVec = -minVec
|
|
else
|
|
maxVec = partOrModel:GetModelSize() * 0.5
|
|
minVec = -maxVec
|
|
end
|
|
|
|
-- Adjust bounding box to reflect what the model or part author wants in terms of justification
|
|
local justifyValue = partOrModel:FindFirstChild "Justification"
|
|
if justifyValue ~= nil then
|
|
-- find the multiple of 4 that contains the model
|
|
local justify = justifyValue.Value
|
|
local two = Vector3.new(2, 2, 2)
|
|
local actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01)
|
|
local containingGridBox = Vector3.new(
|
|
4 * math.ceil(actualBox.x / 4),
|
|
4 * math.ceil(actualBox.y / 4),
|
|
4 * math.ceil(actualBox.z / 4)
|
|
)
|
|
local adjustment = containingGridBox - actualBox
|
|
minVec -= 0.5 * adjustment * justify
|
|
maxVec += 0.5 * adjustment * (two - justify)
|
|
end
|
|
|
|
return minVec, maxVec
|
|
end
|
|
|
|
local function getBoundingBoxInWorldCoordinates(partOrModel)
|
|
local minVec = Vector3.new(math.huge, math.huge, math.huge)
|
|
local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
|
|
|
|
if partOrModel:IsA "BasePart" and not partOrModel:IsA "Terrain" then
|
|
local vec1 =
|
|
partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size)
|
|
local vec2 =
|
|
partOrModel.CFrame:pointToWorldSpace(0.5 * partOrModel.Size)
|
|
minVec = Vector3.new(
|
|
math.min(vec1.X, vec2.X),
|
|
math.min(vec1.Y, vec2.Y),
|
|
math.min(vec1.Z, vec2.Z)
|
|
)
|
|
maxVec = Vector3.new(
|
|
math.max(vec1.X, vec2.X),
|
|
math.max(vec1.Y, vec2.Y),
|
|
math.max(vec1.Z, vec2.Z)
|
|
)
|
|
elseif not partOrModel:IsA "Terrain" then
|
|
-- we shouldn't have to deal with this case
|
|
--minVec = Vector3.new(-2, -2, -2)
|
|
--maxVec = Vector3.new(2, 2, 2)
|
|
-- else
|
|
local vec1 = partOrModel
|
|
:GetModelCFrame()
|
|
:pointToWorldSpace(-0.5 * partOrModel:GetModelSize())
|
|
local vec2 = partOrModel
|
|
:GetModelCFrame()
|
|
:pointToWorldSpace(0.5 * partOrModel:GetModelSize())
|
|
minVec = Vector3.new(
|
|
math.min(vec1.X, vec2.X),
|
|
math.min(vec1.Y, vec2.Y),
|
|
math.min(vec1.Z, vec2.Z)
|
|
)
|
|
maxVec = Vector3.new(
|
|
math.max(vec1.X, vec2.X),
|
|
math.max(vec1.Y, vec2.Y),
|
|
math.max(vec1.Z, vec2.Z)
|
|
)
|
|
end
|
|
|
|
return minVec, maxVec
|
|
end
|
|
|
|
local function getTargetPartBoundingBox(targetPart)
|
|
if targetPart.Parent:FindFirstChild "RobloxModel" then
|
|
return getBoundingBox2(targetPart.Parent)
|
|
end
|
|
return getBoundingBox2(targetPart)
|
|
end
|
|
|
|
local function getMouseTargetCFrame(targetPart)
|
|
if not targetPart.Parent:FindFirstChild "RobloxModel" then
|
|
return targetPart.CFrame
|
|
elseif targetPart.Parent:IsA "Tool" then
|
|
return targetPart.Parent.Handle.CFrame
|
|
end
|
|
return targetPart.Parent:GetModelCFrame()
|
|
end
|
|
|
|
local function isBlocker(part: Instance) -- returns whether or not we want to cancel the stamp because we're blocked by this part
|
|
if not part or not part.Parent or part:FindFirstChild "Humanoid" then
|
|
return false
|
|
elseif
|
|
part:FindFirstChild "RobloxStamper"
|
|
or part:FindFirstChild "RobloxModel"
|
|
then
|
|
return true
|
|
elseif (part:IsA "Part" and not part.CanCollide) or (part == Lighting) then
|
|
return false
|
|
end
|
|
return isBlocker(part.Parent)
|
|
end
|
|
|
|
-- helper function to determine if a character can be pushed upwards by a certain amount
|
|
-- character is 5 studs tall, we'll check a 1.5 x 1.5 x 4.5 box around char, with center .5 studs below torsocenter
|
|
local function spaceAboveCharacter(
|
|
charTorso: BasePart,
|
|
newTorsoY,
|
|
stampData: Instance
|
|
)
|
|
local partsAboveChar = workspace:FindPartsInRegion3(
|
|
Region3.new(
|
|
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
|
|
- Vector3.new(0.75, 2.75, 0.75),
|
|
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
|
|
+ Vector3.new(0.75, 1.75, 0.75)
|
|
),
|
|
charTorso.Parent :: Instance,
|
|
100
|
|
)
|
|
|
|
for j = 1, #partsAboveChar do
|
|
if
|
|
partsAboveChar[j].CanCollide
|
|
and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts)
|
|
then
|
|
return false
|
|
end
|
|
end
|
|
|
|
return not clusterPartsInRegion(
|
|
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
|
|
- Vector3.new(0.75, 2.75, 0.75),
|
|
Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z)
|
|
+ Vector3.new(0.75, 1.75, 0.75)
|
|
)
|
|
end
|
|
|
|
local function findConfigAtMouseTarget(
|
|
Mouse: Mouse,
|
|
stampData: Instance & {
|
|
CurrentParts: Instance,
|
|
}
|
|
)
|
|
-- *Critical Assumption* :
|
|
-- This function assumes the target CF axes are orthogonal with the target bounding box faces
|
|
-- And, it assumes the insert CF axes are orthongonal with the insert bounding box faces
|
|
-- Therefore, insertion will not work with angled faces on wedges or other "non-block" parts, nor
|
|
-- will it work for parts in a model that are not orthogonally aligned with the model's CF.
|
|
|
|
-- This can happen sometimes, return if so
|
|
if not Mouse then
|
|
return
|
|
elseif not stampData then
|
|
error "findConfigAtMouseTarget: stampData is nil"
|
|
return
|
|
elseif not stampData.CurrentParts then
|
|
return
|
|
end
|
|
|
|
local grid = 4.0
|
|
local admissibleConfig = false
|
|
local targetConfig = CFrame.new(0, 0, 0)
|
|
|
|
local minBB, maxBB = getBoundingBox2(stampData.CurrentParts)
|
|
local diagBB = maxBB - minBB
|
|
|
|
local insertCFrame
|
|
if
|
|
stampData.CurrentParts:IsA "Model" or stampData.CurrentParts:IsA "Tool"
|
|
then
|
|
insertCFrame = stampData.CurrentParts:GetModelCFrame()
|
|
else
|
|
insertCFrame = stampData.CurrentParts.CFrame
|
|
end
|
|
|
|
if Mouse and stampData.CurrentParts:IsA "Tool" then
|
|
Mouse.TargetFilter = stampData.CurrentParts.Handle
|
|
else
|
|
Mouse.TargetFilter = stampData.CurrentParts
|
|
end
|
|
|
|
local hitPlane = false
|
|
local targetPart
|
|
local ok = pcall(function()
|
|
targetPart = Mouse.Target
|
|
end)
|
|
|
|
if not ok then -- or targetPart == nil then
|
|
return admissibleConfig, targetConfig
|
|
end
|
|
|
|
local mouseHitInWorld = Vector3.new(0, 0, 0)
|
|
if Mouse then
|
|
mouseHitInWorld = Vector3.new(Mouse.Hit.x, Mouse.Hit.y, Mouse.Hit.z)
|
|
end
|
|
|
|
local cellPos
|
|
|
|
-- Nothing was hit, so check for the default plane.
|
|
if nil == targetPart then
|
|
cellPos = GetTerrainForMouse(Mouse)
|
|
if nil == cellPos then
|
|
hitPlane = false
|
|
return admissibleConfig, targetConfig
|
|
end
|
|
targetPart = workspace.Terrain
|
|
hitPlane = true
|
|
-- Take into account error that will occur.
|
|
cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z)
|
|
mouseHitInWorld =
|
|
workspace.Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z)
|
|
end
|
|
|
|
-- test mouse hit location
|
|
local minBBTarget, maxBBTarget = getTargetPartBoundingBox(targetPart)
|
|
local diagBBTarget = maxBBTarget - minBBTarget
|
|
local targetCFrame = getMouseTargetCFrame(targetPart)
|
|
|
|
if targetPart:IsA "Terrain" then
|
|
if not cluster then
|
|
cluster = workspace:FindFirstChild "Terrain" :: Terrain
|
|
end
|
|
local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld)
|
|
if hitPlane then
|
|
cellID = cellPos
|
|
end
|
|
|
|
targetCFrame = CFrame.new(
|
|
workspace.Terrain:CellCenterToWorld(cellID.x, cellID.y, cellID.z)
|
|
)
|
|
end
|
|
|
|
local mouseHitInTarget = targetCFrame:pointToObjectSpace(mouseHitInWorld)
|
|
local targetVectorInWorld = Vector3.new(0, 0, 0)
|
|
if Mouse then
|
|
-- DON'T WANT THIS IN TERMS OF THE MODEL CFRAME! (.TargetSurface is in terms of the part CFrame, so this would break, right? [HotThoth])
|
|
-- (ideally, we would want to make the Mouse.TargetSurface a model-targetsurface instead, but for testing will be using the converse)
|
|
--targetVectorInWorld = targetCFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface))
|
|
targetVectorInWorld = targetPart.CFrame:vectorToWorldSpace(
|
|
Vector3.FromNormalId(Mouse.TargetSurface)
|
|
) -- better, but model cframe would be best
|
|
--[[
|
|
if targetPart.Parent:IsA "Model" then
|
|
local hitFace = modelTargetSurface(
|
|
targetPart.Parent,
|
|
Mouse.Hit.p,
|
|
workspace.CurrentCamera.CoordinateFrame.p
|
|
) -- best, if you get it right
|
|
local WORLD_AXES = {
|
|
Vector3.new(1, 0, 0),
|
|
Vector3.new(0, 1, 0),
|
|
Vector3.new(0, 0, 1),
|
|
}
|
|
if hitFace > 0 then
|
|
targetVectorInWorld =
|
|
targetCFrame:vectorToWorldSpace(WORLD_AXES[hitFace])
|
|
elseif hitFace < 0 then
|
|
targetVectorInWorld =
|
|
targetCFrame:vectorToWorldSpace(-WORLD_AXES[-hitFace])
|
|
end
|
|
end
|
|
]]
|
|
end
|
|
|
|
local targetRefPointInTarget, insertRefPointInInsert
|
|
local clampToSurface
|
|
|
|
if getClosestAlignedWorldDirection(targetVectorInWorld) == 0 then
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
|
|
clampToSurface = Vector3.new(0, 1, 1)
|
|
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 3 then
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
|
|
clampToSurface = Vector3.new(0, 1, 1)
|
|
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 1 then
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
|
|
clampToSurface = Vector3.new(1, 0, 1)
|
|
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 4 then
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
|
|
clampToSurface = Vector3.new(1, 0, 1)
|
|
elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 2 then
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
|
|
clampToSurface = Vector3.new(1, 1, 0)
|
|
else
|
|
targetRefPointInTarget =
|
|
targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
|
|
insertRefPointInInsert =
|
|
insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
|
|
clampToSurface = Vector3.new(1, 1, 0)
|
|
end
|
|
|
|
targetRefPointInTarget *= (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget)
|
|
insertRefPointInInsert *= (0.5 * diagBB) + 0.5 * (maxBB + minBB)
|
|
|
|
-- To Do: For cases that are not aligned to the world grid, account for the minimal rotation
|
|
-- needed to bring the Insert part(s) into alignment with the Target Part
|
|
-- Apply the rotation here
|
|
|
|
local delta = mouseHitInTarget - targetRefPointInTarget
|
|
local deltaClamped = Vector3.new(
|
|
grid * math.modf(delta.x / grid),
|
|
grid * math.modf(delta.y / grid),
|
|
grid * math.modf(delta.z / grid)
|
|
)
|
|
deltaClamped *= clampToSurface
|
|
local targetTouchInTarget = deltaClamped + targetRefPointInTarget
|
|
|
|
local TargetTouchRelToWorld =
|
|
targetCFrame:pointToWorldSpace(targetTouchInTarget)
|
|
local InsertTouchInWorld =
|
|
insertCFrame:vectorToWorldSpace(insertRefPointInInsert)
|
|
local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld
|
|
|
|
local _, _, _, R00, R01, R02, R10, R11, R12, R20, R21, R22 =
|
|
insertCFrame:components()
|
|
targetConfig = CFrame.new(
|
|
posInsertOriginInWorld.x,
|
|
posInsertOriginInWorld.y,
|
|
posInsertOriginInWorld.z,
|
|
R00,
|
|
R01,
|
|
R02,
|
|
R10,
|
|
R11,
|
|
R12,
|
|
R20,
|
|
R21,
|
|
R22
|
|
)
|
|
admissibleConfig = true
|
|
|
|
return admissibleConfig,
|
|
targetConfig,
|
|
getClosestAlignedWorldDirection(targetVectorInWorld)
|
|
end
|
|
|
|
local function truncateToCircleEighth(bigValue, littleValue)
|
|
local big = math.abs(bigValue)
|
|
local little = math.abs(littleValue)
|
|
local hypotenuse = math.sqrt(big * big + little * little)
|
|
local frac = little / hypotenuse
|
|
|
|
local bigSign = 1
|
|
local littleSign = 1
|
|
if bigValue < 0 then
|
|
bigSign = -1
|
|
end
|
|
if littleValue < 0 then
|
|
littleSign = -1
|
|
end
|
|
|
|
if frac > 0.382683432 then
|
|
-- between 22.5 and 45 degrees, so truncate to 45-degree tilt
|
|
return 0.707106781 * hypotenuse * bigSign,
|
|
0.707106781 * hypotenuse * littleSign
|
|
end
|
|
-- between 0 and 22.5 degrees, so truncate to 0-degree tilt
|
|
return hypotenuse * bigSign, 0
|
|
end
|
|
|
|
local function saveTheWelds(object, manualWeldTable, manualWeldParentTable)
|
|
if object:IsA "ManualWeld" or object:IsA "Rotate" then
|
|
table.insert(manualWeldTable, object)
|
|
table.insert(manualWeldParentTable, object.Parent)
|
|
else
|
|
local children = object:GetChildren()
|
|
for i = 1, #children do
|
|
saveTheWelds(children[i], manualWeldTable, manualWeldParentTable)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function restoreTheWelds(manualWeldTable, manualWeldParentTable)
|
|
for i = 1, #manualWeldTable do
|
|
manualWeldTable[i].Parent = manualWeldParentTable[i]
|
|
end
|
|
end
|
|
|
|
function RbxStamper.CanEditRegion(partOrModel, EditRegion) -- todo: use model and stamper metadata
|
|
if not EditRegion then
|
|
return true, false
|
|
end
|
|
|
|
local minBB, maxBB = getBoundingBoxInWorldCoordinates(partOrModel)
|
|
|
|
if
|
|
minBB.X < EditRegion.CFrame.p.X - EditRegion.Size.X / 2
|
|
or minBB.Y < EditRegion.CFrame.p.Y - EditRegion.Size.Y / 2
|
|
or minBB.Z < EditRegion.CFrame.p.Z - EditRegion.Size.Z / 2
|
|
then
|
|
return false, false
|
|
elseif
|
|
maxBB.X > EditRegion.CFrame.p.X + EditRegion.Size.X / 2
|
|
or maxBB.Y > EditRegion.CFrame.p.Y + EditRegion.Size.Y / 2
|
|
or maxBB.Z > EditRegion.CFrame.p.Z + EditRegion.Size.Z / 2
|
|
then
|
|
return false, false
|
|
end
|
|
|
|
return true, false
|
|
end
|
|
|
|
local function UnlockInstances(object)
|
|
if object:IsA "BasePart" then
|
|
object.Locked = false
|
|
end
|
|
for _, child in pairs(object:GetChildren()) do
|
|
UnlockInstances(child)
|
|
end
|
|
end
|
|
|
|
local function closestTerrainMaterialColour(terrainValue)
|
|
if terrainValue == 1 then
|
|
return BrickColor.new "Bright green"
|
|
elseif terrainValue == 2 then
|
|
return BrickColor.new "Bright yellow"
|
|
elseif terrainValue == 3 then
|
|
return BrickColor.new "Bright red"
|
|
elseif terrainValue == 4 then
|
|
return BrickColor.new "Sand red"
|
|
elseif terrainValue == 5 then
|
|
return BrickColor.new "Black"
|
|
elseif terrainValue == 6 then
|
|
return BrickColor.new "Dark stone grey"
|
|
elseif terrainValue == 7 then
|
|
return BrickColor.new "Sand blue"
|
|
elseif terrainValue == 8 then
|
|
return BrickColor.new "Deep orange"
|
|
elseif terrainValue == 9 then
|
|
return BrickColor.new "Dark orange"
|
|
elseif terrainValue == 10 then
|
|
return BrickColor.new "Reddish brown"
|
|
elseif terrainValue == 11 then
|
|
return BrickColor.new "Light orange"
|
|
elseif terrainValue == 12 then
|
|
return BrickColor.new "Light stone grey"
|
|
elseif terrainValue == 13 then
|
|
return BrickColor.new "Sand green"
|
|
elseif terrainValue == 14 then
|
|
return BrickColor.new "Medium stone grey"
|
|
elseif terrainValue == 15 then
|
|
return BrickColor.new "Really red"
|
|
elseif terrainValue == 16 then
|
|
return BrickColor.new "Really blue"
|
|
elseif terrainValue == 17 then
|
|
return BrickColor.new "Bright blue"
|
|
end
|
|
return BrickColor.new "Bright green"
|
|
end
|
|
|
|
local function setupFakeTerrainPart(cellMat, cellType, cellOrient)
|
|
local newTerrainPiece
|
|
if cellType == 1 or cellType == 4 then
|
|
newTerrainPiece = New "WedgePart" {
|
|
formFactor = "Custom",
|
|
}
|
|
elseif cellType == 2 then
|
|
newTerrainPiece = Instance.new "CornerWedgePart"
|
|
else
|
|
newTerrainPiece = New "Part" {
|
|
formFactor = "Custom",
|
|
}
|
|
end
|
|
|
|
Hydrate(newTerrainPiece) {
|
|
Name = "MegaClusterCube",
|
|
Size = Vector3.new(4, 4, 4),
|
|
BottomSurface = "Smooth",
|
|
TopSurface = "Smooth",
|
|
}
|
|
|
|
-- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors
|
|
newTerrainPiece.BrickColor = closestTerrainMaterialColour(cellMat)
|
|
|
|
local sideways = 0
|
|
local flipped = math.pi
|
|
if cellType == 4 then
|
|
sideways = -math.pi / 2
|
|
end
|
|
if cellType == 2 or cellType == 3 then
|
|
flipped = 0
|
|
end
|
|
newTerrainPiece.CFrame =
|
|
CFrame.Angles(0, math.pi / 2 * cellOrient + flipped, sideways)
|
|
|
|
if cellType == 3 then
|
|
New "SpecialMesh" {
|
|
MeshType = "FileMesh",
|
|
MeshId = path "asset?id=66832495",
|
|
Scale = Vector3.new(2, 2, 2),
|
|
Parent = newTerrainPiece,
|
|
}
|
|
end
|
|
|
|
New "Vector3Value" {
|
|
Value = Vector3.new(cellMat, cellType, cellOrient),
|
|
Name = "ClusterMaterial",
|
|
Parent = newTerrainPiece,
|
|
}
|
|
|
|
return newTerrainPiece
|
|
end
|
|
|
|
function RbxStamper.GetStampModel(assetId, terrainShape, useAssetVersionId)
|
|
if assetId == 0 then
|
|
return nil, "No Asset"
|
|
elseif assetId < 0 then
|
|
return nil, "Negative Asset"
|
|
end
|
|
|
|
-- This call will cause a "wait" until the data comes back
|
|
-- below we wait a max of 8 seconds before deciding to bail out on loading
|
|
local root
|
|
local loader
|
|
local loading = true
|
|
if useAssetVersionId then
|
|
loader = coroutine.create(function()
|
|
root = InsertService:LoadAssetVersion(assetId)
|
|
loading = false
|
|
end)
|
|
coroutine.resume(loader)
|
|
else
|
|
loader = coroutine.create(function()
|
|
root = InsertService:LoadAsset(assetId)
|
|
loading = false
|
|
end)
|
|
coroutine.resume(loader)
|
|
end
|
|
|
|
local lastGameTime = 0
|
|
local totalTime = 0
|
|
local maxWait = 8
|
|
while loading and totalTime < maxWait do
|
|
lastGameTime = tick()
|
|
wait(1)
|
|
totalTime += tick() - lastGameTime
|
|
end
|
|
loading = false
|
|
|
|
if totalTime >= maxWait then
|
|
return nil, "Load Time Fail"
|
|
elseif root == nil then
|
|
return nil, "Load Asset Fail"
|
|
elseif not root:IsA "Model" then
|
|
return nil, "Load Type Fail"
|
|
end
|
|
|
|
local instances = root:GetChildren()
|
|
if #instances == 0 then
|
|
return nil, "Empty Model Fail"
|
|
end
|
|
|
|
--Unlock all parts that are inserted, to make sure they are editable
|
|
UnlockInstances(root)
|
|
|
|
--Continue the insert process
|
|
root = root:GetChildren()[1]
|
|
|
|
--Examine the contents and decide what it looks like
|
|
for _, instance in pairs(instances) do
|
|
if instance:IsA "Team" then
|
|
instance.Parent = game:GetService "Teams"
|
|
elseif instance:IsA "Sky" then
|
|
local lightingService = game:GetService "Lighting"
|
|
for _, child in pairs(lightingService:GetChildren()) do
|
|
if child:IsA "Sky" then
|
|
child:Remove()
|
|
end
|
|
end
|
|
instance.Parent = lightingService
|
|
return
|
|
end
|
|
end
|
|
|
|
-- ...and tag all inserted models for subsequent origin identification
|
|
-- if no RobloxModel tag already exists, then add it.
|
|
if root:FindFirstChild "RobloxModel" == nil then
|
|
local stringTag = Instance.new "BoolValue"
|
|
stringTag.Name = "RobloxModel"
|
|
stringTag.Parent = root
|
|
|
|
if root:FindFirstChild "RobloxStamper" == nil then
|
|
local stringTag2 = Instance.new "BoolValue"
|
|
stringTag2.Name = "RobloxStamper"
|
|
stringTag2.Parent = root
|
|
end
|
|
end
|
|
|
|
if terrainShape and root.Name == "MegaClusterCube" then
|
|
if terrainShape == 6 then -- insert an autowedging tag
|
|
local autowedgeTag = Instance.new "BoolValue"
|
|
autowedgeTag.Name = "AutoWedge"
|
|
autowedgeTag.Parent = root
|
|
else
|
|
local clusterTag = root:FindFirstChild "ClusterMaterial"
|
|
if clusterTag then
|
|
if clusterTag:IsA "Vector3Value" then
|
|
root = setupFakeTerrainPart(
|
|
clusterTag.Value.X,
|
|
terrainShape,
|
|
clusterTag.Value.Z
|
|
)
|
|
else
|
|
root =
|
|
setupFakeTerrainPart(clusterTag.Value, terrainShape, 0)
|
|
end
|
|
else
|
|
root = setupFakeTerrainPart(1, terrainShape, 0)
|
|
end
|
|
end
|
|
end
|
|
|
|
return root
|
|
end
|
|
|
|
local function isMegaClusterPart(stampData: {
|
|
CurrentParts: BasePart,
|
|
})
|
|
if not (stampData and stampData.CurrentParts) then
|
|
return false
|
|
end
|
|
|
|
return (
|
|
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
|
|
or (stampData.CurrentParts.Name == "MegaClusterCube")
|
|
)
|
|
end
|
|
|
|
type HighScalabilityLine = {
|
|
Adorn: Instance,
|
|
Start: Vector3,
|
|
End: Vector3,
|
|
Dimensions: number,
|
|
MoreLines: { Vector3 },
|
|
MorePoints: { Vector3 },
|
|
InternalLine: Vector3,
|
|
AdornPart: BasePart,
|
|
}
|
|
|
|
local function DoHighScalabilityRegionSelect(
|
|
stampData: {
|
|
CurrentParts: BasePart,
|
|
},
|
|
HighScalabilityLine: HighScalabilityLine
|
|
)
|
|
local megaCube = stampData.CurrentParts:FindFirstChild "MegaClusterCube"
|
|
if not megaCube then
|
|
if stampData.CurrentParts.Name ~= "MegaClusterCube" then
|
|
return
|
|
end
|
|
megaCube = stampData.CurrentParts
|
|
end
|
|
|
|
HighScalabilityLine.End = megaCube.CFrame.p
|
|
local line
|
|
local line2 = Vector3.new(0, 0, 0)
|
|
local line3 = Vector3.new(0, 0, 0)
|
|
|
|
if HighScalabilityLine.Dimensions == 1 then
|
|
-- extract the line from these positions and limit to a 2D plane made from 2 of the world axes
|
|
-- then use dominating axis to limit line to be at 45-degree intervals
|
|
-- will use this internal representation of the line for the actual stamping
|
|
line = (HighScalabilityLine.End - HighScalabilityLine.Start)
|
|
|
|
if math.abs(line.X) < math.abs(line.Y) then
|
|
if math.abs(line.X) < math.abs(line.Z) then
|
|
-- limit to Y/Z plane, domination unknown
|
|
local newY, newZ
|
|
if math.abs(line.Y) > math.abs(line.Z) then
|
|
newY, newZ = truncateToCircleEighth(line.Y, line.Z)
|
|
else
|
|
newZ, newY = truncateToCircleEighth(line.Z, line.Y)
|
|
end
|
|
line = Vector3.new(0, newY, newZ)
|
|
else
|
|
-- limit to X/Y plane, with Y dominating
|
|
local newY, newX = truncateToCircleEighth(line.Y, line.X)
|
|
line = Vector3.new(newX, newY, 0)
|
|
end
|
|
else
|
|
if math.abs(line.Y) < math.abs(line.Z) then
|
|
-- limit to X/Z plane, domination unknown
|
|
local newX, newZ
|
|
if math.abs(line.X) > math.abs(line.Z) then
|
|
newX, newZ = truncateToCircleEighth(line.X, line.Z)
|
|
else
|
|
newZ, newX = truncateToCircleEighth(line.Z, line.X)
|
|
end
|
|
line = Vector3.new(newX, 0, newZ)
|
|
else
|
|
-- limit to X/Y plane, with X dominating
|
|
local newX, newY = truncateToCircleEighth(line.X, line.Y)
|
|
line = Vector3.new(newX, newY, 0)
|
|
end
|
|
end
|
|
HighScalabilityLine.InternalLine = line
|
|
elseif HighScalabilityLine.Dimensions == 2 then
|
|
line = HighScalabilityLine.MoreLines[1]
|
|
line2 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[1]
|
|
|
|
-- take out any component of line2 along line1, so you get perpendicular to line1 component
|
|
line2 -= line.unit * line.unit:Dot(line2)
|
|
|
|
local tempCFrame = CFrame.new(
|
|
HighScalabilityLine.Start,
|
|
HighScalabilityLine.Start + line
|
|
)
|
|
|
|
-- then zero out whichever is the smaller component
|
|
local yAxis = tempCFrame:vectorToWorldSpace(Vector3.new(0, 1, 0))
|
|
local xAxis = tempCFrame:vectorToWorldSpace(Vector3.new(1, 0, 0))
|
|
|
|
local xComp = xAxis:Dot(line2)
|
|
local yComp = yAxis:Dot(line2)
|
|
|
|
if math.abs(yComp) > math.abs(xComp) then
|
|
line2 -= xAxis * xComp
|
|
else
|
|
line2 -= yAxis * yComp
|
|
end
|
|
|
|
HighScalabilityLine.InternalLine = line2
|
|
elseif HighScalabilityLine.Dimensions == 3 then
|
|
line = HighScalabilityLine.MoreLines[1]
|
|
line2 = HighScalabilityLine.MoreLines[2]
|
|
line3 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[2]
|
|
|
|
-- zero out all components of previous lines
|
|
line3 -= line.unit * line.unit:Dot(line3)
|
|
line3 -= line2.unit * line2.unit:Dot(line3)
|
|
|
|
HighScalabilityLine.InternalLine = line3
|
|
end
|
|
|
|
-- resize the "line" graphic to be the correct size and orientation
|
|
local tempCFrame =
|
|
CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line)
|
|
|
|
if HighScalabilityLine.Dimensions == 1 then -- faster calculation for line
|
|
HighScalabilityLine.AdornPart.Size =
|
|
Vector3.new(4, 4, line.magnitude + 4)
|
|
HighScalabilityLine.AdornPart.CFrame = tempCFrame
|
|
+ tempCFrame:vectorToWorldSpace(
|
|
Vector3.new(2, 2, 2) - HighScalabilityLine.AdornPart.Size / 2
|
|
)
|
|
else
|
|
local boxSize = tempCFrame:vectorToObjectSpace(line + line2 + line3)
|
|
HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, 4)
|
|
+ Vector3.new(
|
|
math.abs(boxSize.X),
|
|
math.abs(boxSize.Y),
|
|
math.abs(boxSize.Z)
|
|
)
|
|
HighScalabilityLine.AdornPart.CFrame = tempCFrame
|
|
+ tempCFrame:vectorToWorldSpace(boxSize / 2)
|
|
end
|
|
|
|
-- make player able to see this ish
|
|
|
|
local gui
|
|
if Players.LocalPlayer then
|
|
gui = Players.LocalPlayer:FindFirstChild "PlayerGui"
|
|
if
|
|
gui
|
|
and gui:IsA "PlayerGui"
|
|
and (
|
|
(HighScalabilityLine.Dimensions == 1 and line.magnitude > 3)
|
|
or HighScalabilityLine.Dimensions > 1
|
|
)
|
|
then -- don't show if mouse hasn't moved enough
|
|
HighScalabilityLine.Adorn.Parent = gui
|
|
end
|
|
end
|
|
|
|
if not gui then -- we are in studio
|
|
gui = game:GetService "CoreGui"
|
|
if
|
|
(HighScalabilityLine.Dimensions == 1 and line.magnitude > 3)
|
|
or HighScalabilityLine.Dimensions > 1
|
|
then -- don't show if mouse hasn't moved enough
|
|
HighScalabilityLine.Adorn.Parent = gui
|
|
end
|
|
end
|
|
end
|
|
|
|
local function flashRedBox(stampData: {
|
|
CurrentParts: BasePart,
|
|
ErrorBox: SelectionBox,
|
|
})
|
|
local gui = game.CoreGui
|
|
if
|
|
game:FindFirstChild "Players"
|
|
and Players.LocalPlayer
|
|
and Players.LocalPlayer:FindFirstChild "PlayerGui"
|
|
then
|
|
gui = Players.LocalPlayer.PlayerGui
|
|
end
|
|
if not stampData.ErrorBox then
|
|
return
|
|
end
|
|
|
|
stampData.ErrorBox.Parent = gui
|
|
if stampData.CurrentParts:IsA "Tool" then
|
|
stampData.ErrorBox.Adornee = stampData.CurrentParts.Handle
|
|
else
|
|
stampData.ErrorBox.Adornee = stampData.CurrentParts
|
|
end
|
|
|
|
delay(0, function()
|
|
for _ = 1, 3 do
|
|
if stampData.ErrorBox then
|
|
stampData.ErrorBox.Visible = true
|
|
end
|
|
wait(0.13)
|
|
if stampData.ErrorBox then
|
|
stampData.ErrorBox.Visible = false
|
|
end
|
|
wait(0.13)
|
|
end
|
|
if stampData.ErrorBox then
|
|
stampData.ErrorBox.Adornee = nil
|
|
stampData.ErrorBox.Parent = Tool -- ?
|
|
end
|
|
end)
|
|
end
|
|
|
|
local function loadSurfaceTypes(part, surfaces)
|
|
part.TopSurface = surfaces[1]
|
|
part.BottomSurface = surfaces[2]
|
|
part.LeftSurface = surfaces[3]
|
|
part.RightSurface = surfaces[4]
|
|
part.FrontSurface = surfaces[5]
|
|
part.BackSurface = surfaces[6]
|
|
end
|
|
|
|
local function saveSurfaceTypes(part, myTable)
|
|
local tempTable = {}
|
|
tempTable[1] = part.TopSurface
|
|
tempTable[2] = part.BottomSurface
|
|
tempTable[3] = part.LeftSurface
|
|
tempTable[4] = part.RightSurface
|
|
tempTable[5] = part.FrontSurface
|
|
tempTable[6] = part.BackSurface
|
|
|
|
myTable[part] = tempTable
|
|
end
|
|
|
|
local function checkTerrainBlockCollisions(
|
|
stampData,
|
|
allowedStampRegion,
|
|
cellPos,
|
|
checkHighScalabilityStamp
|
|
)
|
|
local cellCenterToWorld = workspace.Terrain.CellCenterToWorld
|
|
local cellCenter =
|
|
cellCenterToWorld(workspace.Terrain, cellPos.X, cellPos.Y, cellPos.Z)
|
|
local cellBlockingParts = workspace:FindPartsInRegion3(
|
|
Region3.new(
|
|
cellCenter - Vector3.new(2, 2, 2) + insertBoundingBoxOverlapVector,
|
|
cellCenter + Vector3.new(2, 2, 2) - insertBoundingBoxOverlapVector
|
|
),
|
|
stampData.CurrentParts,
|
|
100
|
|
)
|
|
|
|
local skipThisCell = false
|
|
|
|
for b = 1, #cellBlockingParts do
|
|
if isBlocker(cellBlockingParts[b]) then
|
|
skipThisCell = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not skipThisCell then
|
|
-- pop players up above any set cells
|
|
local alreadyPushedUp = {}
|
|
-- if no blocking model below, then see if stamping on top of a character
|
|
for b = 1, #cellBlockingParts do
|
|
if
|
|
cellBlockingParts[b].Parent
|
|
and not alreadyPushedUp[cellBlockingParts[b].Parent]
|
|
and cellBlockingParts[b].Parent:FindFirstChild "Humanoid"
|
|
and cellBlockingParts[b].Parent
|
|
:FindFirstChild("Humanoid")
|
|
:IsA "Humanoid"
|
|
then
|
|
-----------------------------------------------------------------------------------
|
|
local blockingPersonTorso =
|
|
cellBlockingParts[b].Parent:FindFirstChild "Torso"
|
|
alreadyPushedUp[cellBlockingParts[b].Parent] = true
|
|
|
|
if blockingPersonTorso then
|
|
-- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them)
|
|
local newY = cellCenter.Y + 5
|
|
if
|
|
spaceAboveCharacter(
|
|
blockingPersonTorso,
|
|
newY,
|
|
stampData
|
|
)
|
|
then
|
|
blockingPersonTorso.CFrame = blockingPersonTorso.CFrame
|
|
+ Vector3.new(
|
|
0,
|
|
newY - blockingPersonTorso.CFrame.p.Y,
|
|
0
|
|
)
|
|
else
|
|
-- if no space, we just skip this one
|
|
skipThisCell = true
|
|
break
|
|
end
|
|
end
|
|
-----------------------------------------------------------------------------------
|
|
end
|
|
end
|
|
end
|
|
|
|
if not skipThisCell then -- if we STILL aren't skipping... then we're good to go!
|
|
local canSetCell = true
|
|
|
|
if checkHighScalabilityStamp then -- check to see if cell is in region, if not we'll skip set
|
|
if allowedStampRegion then
|
|
cellPos = cellCenterToWorld(
|
|
workspace.Terrain,
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z
|
|
)
|
|
if
|
|
(
|
|
cellPos.X + 2
|
|
> allowedStampRegion.CFrame.p.X
|
|
+ allowedStampRegion.Size.X / 2
|
|
)
|
|
or (cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X / 2)
|
|
or (cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y / 2)
|
|
or (cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y / 2)
|
|
or (cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z / 2)
|
|
or (
|
|
cellPos.Z - 2
|
|
< allowedStampRegion.CFrame.p.Z
|
|
- allowedStampRegion.Size.Z / 2
|
|
)
|
|
then
|
|
canSetCell = false
|
|
end
|
|
end
|
|
end
|
|
|
|
return canSetCell
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function ResolveMegaClusterStamp(
|
|
stampData: {
|
|
CurrentParts: BasePart,
|
|
},
|
|
HighScalabilityLine: HighScalabilityLine,
|
|
checkHighScalabilityStamp,
|
|
allowedStampRegion
|
|
)
|
|
local cellSet = false
|
|
|
|
cluster = workspace.Terrain
|
|
|
|
local line = HighScalabilityLine.InternalLine
|
|
local cMax = workspace.Terrain.MaxExtents.Max
|
|
local cMin = workspace.Terrain.MaxExtents.Min
|
|
|
|
local clusterMaterial = 1 -- default is grass
|
|
local clusterType = 0 -- default is brick
|
|
local clusterOrientation = 0 -- default is 0 rotation
|
|
|
|
local autoWedgeClusterParts = false
|
|
if stampData.CurrentParts:FindFirstChild "AutoWedge" then
|
|
autoWedgeClusterParts = true
|
|
end
|
|
|
|
if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
|
|
clusterMaterial =
|
|
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
|
|
if clusterMaterial:IsA "Vector3Value" then
|
|
clusterType = clusterMaterial.Value.Y
|
|
clusterOrientation = clusterMaterial.Value.Z
|
|
clusterMaterial = clusterMaterial.Value.X
|
|
elseif clusterMaterial:IsA "IntValue" then
|
|
clusterMaterial = clusterMaterial.Value
|
|
end
|
|
end
|
|
|
|
if
|
|
HighScalabilityLine.Adorn.Parent
|
|
and HighScalabilityLine.Start
|
|
and (
|
|
(HighScalabilityLine.Dimensions > 1)
|
|
or (line and line.magnitude > 0)
|
|
)
|
|
then
|
|
local startCell =
|
|
workspace.Terrain:WorldToCell(HighScalabilityLine.Start)
|
|
local xInc = { 0, 0, 0 }
|
|
local yInc = { 0, 0, 0 }
|
|
local zInc = { 0, 0, 0 }
|
|
|
|
local incrementVect = { nil, nil, nil }
|
|
local stepVect = {
|
|
Vector3.new(0, 0, 0),
|
|
Vector3.new(0, 0, 0),
|
|
Vector3.new(0, 0, 0),
|
|
}
|
|
|
|
local worldAxes = {
|
|
Vector3.new(1, 0, 0),
|
|
Vector3.new(0, 1, 0),
|
|
Vector3.new(0, 0, 1),
|
|
}
|
|
|
|
local lines = {}
|
|
if HighScalabilityLine.Dimensions > 1 then
|
|
table.insert(lines, HighScalabilityLine.MoreLines[1])
|
|
end
|
|
if line and line.magnitude > 0 then
|
|
table.insert(lines, line)
|
|
end
|
|
if HighScalabilityLine.Dimensions > 2 then
|
|
table.insert(lines, HighScalabilityLine.MoreLines[2])
|
|
end
|
|
|
|
for i = 1, #lines do
|
|
lines[i] = Vector3.new(
|
|
math.floor(lines[i].X + 0.5),
|
|
math.floor(lines[i].Y + 0.5),
|
|
math.floor(lines[i].Z + 0.5)
|
|
) -- round to integers
|
|
|
|
if lines[i].X > 0 then
|
|
xInc[i] = 1
|
|
elseif lines[i].X < 0 then
|
|
xInc[i] = -1
|
|
end
|
|
if lines[i].Y > 0 then
|
|
yInc[i] = 1
|
|
elseif lines[i].Y < 0 then
|
|
yInc[i] = -1
|
|
end
|
|
if lines[i].Z > 0 then
|
|
zInc[i] = 1
|
|
elseif lines[i].Z < 0 then
|
|
zInc[i] = -1
|
|
end
|
|
|
|
incrementVect[i] = Vector3.new(xInc[i], yInc[i], zInc[i])
|
|
if incrementVect[i].magnitude < 0.9 then
|
|
incrementVect[i] = nil
|
|
end
|
|
end
|
|
|
|
if not lines[2] then
|
|
lines[2] = Vector3.new(0, 0, 0)
|
|
end
|
|
if not lines[3] then
|
|
lines[3] = Vector3.new(0, 0, 0)
|
|
end
|
|
|
|
local waterForceTag =
|
|
stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
|
|
local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild(
|
|
"WaterForceDirectionTag",
|
|
true
|
|
)
|
|
|
|
while stepVect[3].magnitude * 4 <= lines[3].magnitude do
|
|
local outerStepVectIndex = 1
|
|
while outerStepVectIndex < 4 do
|
|
stepVect[2] = Vector3.new(0, 0, 0)
|
|
while stepVect[2].magnitude * 4 <= lines[2].magnitude do
|
|
local innerStepVectIndex = 1
|
|
while innerStepVectIndex < 4 do
|
|
stepVect[1] = Vector3.new(0, 0, 0)
|
|
while stepVect[1].magnitude * 4 <= lines[1].magnitude do
|
|
local stepVectSum = stepVect[1]
|
|
+ stepVect[2]
|
|
+ stepVect[3]
|
|
local cellPos = Vector3int16.new(
|
|
startCell.X + stepVectSum.X,
|
|
startCell.Y + stepVectSum.Y,
|
|
startCell.Z + stepVectSum.Z
|
|
)
|
|
if
|
|
cellPos.X >= cMin.X
|
|
and cellPos.Y >= cMin.Y
|
|
and cellPos.Z >= cMin.Z
|
|
and cellPos.X < cMax.X
|
|
and cellPos.Y < cMax.Y
|
|
and cellPos.Z < cMax.Z
|
|
then
|
|
-- check if overlaps player or part
|
|
local okToStampTerrainBlock =
|
|
checkTerrainBlockCollisions(
|
|
stampData,
|
|
allowedStampRegion,
|
|
cellPos,
|
|
checkHighScalabilityStamp
|
|
)
|
|
|
|
if okToStampTerrainBlock then
|
|
if waterForceTag then
|
|
cluster:SetWaterCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
Enum.WaterForce[waterForceTag.Value],
|
|
Enum.WaterDirection[waterForceDirectionTag.Value]
|
|
)
|
|
else
|
|
cluster:SetCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
clusterMaterial,
|
|
clusterType,
|
|
clusterOrientation
|
|
)
|
|
end
|
|
cellSet = true
|
|
|
|
-- auto-wedge it?
|
|
if autoWedgeClusterParts then
|
|
workspace.Terrain:AutowedgeCells(
|
|
Region3int16.new(
|
|
Vector3int16.new(
|
|
cellPos.x - 1,
|
|
cellPos.y - 1,
|
|
cellPos.z - 1
|
|
),
|
|
Vector3int16.new(
|
|
cellPos.x + 1,
|
|
cellPos.y + 1,
|
|
cellPos.z + 1
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
stepVect[1] += incrementVect[1]
|
|
end
|
|
if incrementVect[2] then
|
|
while
|
|
innerStepVectIndex < 4
|
|
and worldAxes[innerStepVectIndex]:Dot(
|
|
incrementVect[2]
|
|
)
|
|
== 0
|
|
do
|
|
innerStepVectIndex += 1
|
|
end
|
|
if innerStepVectIndex < 4 then
|
|
stepVect[2] = stepVect[2]
|
|
+ worldAxes[innerStepVectIndex]
|
|
* worldAxes[innerStepVectIndex]:Dot(
|
|
incrementVect[2]
|
|
)
|
|
end
|
|
innerStepVectIndex += 1
|
|
else
|
|
stepVect[2] = Vector3.new(1, 0, 0)
|
|
innerStepVectIndex = 4 -- skip all remaining loops
|
|
end
|
|
if stepVect[2].magnitude * 4 > lines[2].magnitude then
|
|
innerStepVectIndex = 4
|
|
end
|
|
end
|
|
end
|
|
if incrementVect[3] then
|
|
while
|
|
outerStepVectIndex < 4
|
|
and worldAxes[outerStepVectIndex]:Dot(
|
|
incrementVect[3]
|
|
)
|
|
== 0
|
|
do
|
|
outerStepVectIndex += 1
|
|
end
|
|
if outerStepVectIndex < 4 then
|
|
stepVect[3] = stepVect[3]
|
|
+ worldAxes[outerStepVectIndex]
|
|
* worldAxes[outerStepVectIndex]:Dot(
|
|
incrementVect[3]
|
|
)
|
|
end
|
|
outerStepVectIndex += 1
|
|
else -- skip all remaining loops
|
|
stepVect[3] = Vector3.new(1, 0, 0)
|
|
outerStepVectIndex = 4
|
|
end
|
|
if stepVect[3].magnitude * 4 > lines[3].magnitude then
|
|
outerStepVectIndex = 4
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- and also get rid of any HighScalabilityLine stuff if it's there
|
|
HighScalabilityLine.Start = nil
|
|
HighScalabilityLine.Adorn.Parent = nil
|
|
|
|
-- Mark for undo.
|
|
if cellSet then
|
|
stampData.CurrentParts.Parent = nil
|
|
pcall(function()
|
|
ChangeHistoryService:SetWaypoint "StamperMulti"
|
|
end)
|
|
end
|
|
|
|
return cellSet
|
|
end
|
|
|
|
function RbxStamper.SetupStamperDragger(
|
|
modelToStamp: Model | BasePart,
|
|
Mouse: Mouse,
|
|
StampInModel: Model?,
|
|
AllowedStampRegion,
|
|
StampFailedFunc: () -> ()?
|
|
)
|
|
if not modelToStamp then
|
|
error "SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model"
|
|
return nil
|
|
elseif not modelToStamp:IsA "Model" and not modelToStamp:IsA "BasePart" then
|
|
error "SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!"
|
|
return nil
|
|
elseif not Mouse then
|
|
error "SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object"
|
|
return nil
|
|
elseif not Mouse:IsA "Mouse" then
|
|
error "SetupStamperDragger: Mouse (second arg) is not of type Mouse!"
|
|
return nil
|
|
end
|
|
|
|
local configFound, targetCFrame, targetSurface
|
|
|
|
local stampInModel
|
|
local allowedStampRegion
|
|
local stampFailedFunc
|
|
if StampInModel then
|
|
if not StampInModel:IsA "Model" then
|
|
error "SetupStamperDragger: StampInModel (optional third arg) is not of type 'Model'"
|
|
return nil
|
|
end
|
|
if not AllowedStampRegion then
|
|
error "SetupStamperDragger: AllowedStampRegion (optional fourth arg) is nil when StampInModel (optional third arg) is defined"
|
|
return nil
|
|
end
|
|
stampFailedFunc = StampFailedFunc
|
|
stampInModel = StampInModel
|
|
allowedStampRegion = AllowedStampRegion
|
|
end
|
|
|
|
-- Init all state variables
|
|
local gInitial90DegreeRotations = 0
|
|
local stampData: {
|
|
DisabledScripts: { LuaSourceContainer }?,
|
|
TransparencyTable: {}?,
|
|
MaterialTable: {}?,
|
|
CanCollideTable: {}?,
|
|
AnchoredTable: {}?,
|
|
ArchivableTable: {}?,
|
|
DecalTransparencyTable: {}?,
|
|
SurfaceTypeTable: {}?,
|
|
CurrentParts: BasePart & {
|
|
ErrorBox: SelectionBox?,
|
|
},
|
|
}
|
|
local mouseTarget
|
|
|
|
local errorBox = New "SelectionBox" {
|
|
Color = BrickColor.new "Bright red",
|
|
Transparency = 0,
|
|
Archivable = false,
|
|
}
|
|
|
|
-- for megacluster MEGA STAMPING
|
|
local adornPart = New "Part" {
|
|
formFactor = "Custom",
|
|
Size = Vector3.new(4, 4, 4),
|
|
CFrame = CFrame.new(),
|
|
Archivable = false,
|
|
}
|
|
|
|
local adorn = New "SelectionBox" {
|
|
Color = BrickColor.new "Toothpaste",
|
|
Adornee = adornPart,
|
|
Visible = true,
|
|
Transparency = 0,
|
|
Name = "HighScalabilityStamperLine",
|
|
Archivable = false,
|
|
}
|
|
|
|
local HighScalabilityLine: HighScalabilityLine = {
|
|
Start = nil,
|
|
End = nil,
|
|
Adorn = adorn,
|
|
AdornPart = adornPart,
|
|
InternalLine = nil,
|
|
NewHint = true,
|
|
MorePoints = { nil, nil },
|
|
MoreLines = { nil, nil },
|
|
Dimensions = 1,
|
|
}
|
|
|
|
local control = {}
|
|
local movingLock = false
|
|
local stampUpLock = false
|
|
local unstampableSurface = false
|
|
local mouseCons = {}
|
|
local keyCon
|
|
|
|
local stamped = New "BoolValue" {
|
|
Archivable = false,
|
|
Value = false,
|
|
}
|
|
|
|
local lastTarget = {
|
|
TerrainOrientation = 0,
|
|
CFrame = 0,
|
|
}
|
|
|
|
local cellInfo = {
|
|
Material = 1,
|
|
clusterType = 0,
|
|
clusterOrientation = 0,
|
|
}
|
|
|
|
local function DoStamperMouseMove(mouse)
|
|
if not mouse then
|
|
error "Error: RbxStamper.DoStamperMouseMove: Mouse is nil"
|
|
return
|
|
elseif not mouse:IsA "Mouse" then
|
|
error(
|
|
`Error: RbxStamper.DoStamperMouseMove: Mouse is of type {mouse.className} should be of type Mouse`
|
|
)
|
|
return
|
|
end
|
|
|
|
-- There wasn't a target (no part or terrain), so check for plane intersection.
|
|
if not (mouse.Target or GetTerrainForMouse(mouse)) or not stampData then
|
|
return
|
|
end
|
|
|
|
-- don't move with dragger - will move in one step on mouse down
|
|
-- draw ghost at acceptable positions
|
|
configFound, targetCFrame, targetSurface =
|
|
findConfigAtMouseTarget(mouse, stampData)
|
|
if not configFound then
|
|
error "RbxStamper.DoStamperMouseMove No configFound, returning"
|
|
return
|
|
end
|
|
|
|
local numRotations = 0 -- update this according to how many rotations you need to get it to target surface
|
|
if
|
|
autoAlignToFace(stampData.CurrentParts)
|
|
and targetSurface ~= 1
|
|
and targetSurface ~= 4
|
|
then -- pre-rotate the flag or portrait so it's aligned correctly
|
|
if targetSurface == 3 then
|
|
numRotations = 0
|
|
- gInitial90DegreeRotations
|
|
+ autoAlignToFace(stampData.CurrentParts)
|
|
elseif targetSurface == 0 then
|
|
numRotations = 2
|
|
- gInitial90DegreeRotations
|
|
+ autoAlignToFace(stampData.CurrentParts)
|
|
elseif targetSurface == 5 then
|
|
numRotations = 3
|
|
- gInitial90DegreeRotations
|
|
+ autoAlignToFace(stampData.CurrentParts)
|
|
elseif targetSurface == 2 then
|
|
numRotations = 1
|
|
- gInitial90DegreeRotations
|
|
+ autoAlignToFace(stampData.CurrentParts)
|
|
end
|
|
end
|
|
|
|
local ry = math.pi / 2
|
|
gInitial90DegreeRotations += numRotations
|
|
if
|
|
stampData.CurrentParts:IsA "Model"
|
|
or stampData.CurrentParts:IsA "Tool"
|
|
then
|
|
--stampData.CurrentParts:Rotate(0, ry*numRotations, 0)
|
|
modelRotate(stampData.CurrentParts, ry * numRotations)
|
|
else
|
|
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(
|
|
0,
|
|
ry * numRotations,
|
|
0
|
|
) * stampData.CurrentParts.CFrame
|
|
end
|
|
|
|
-- CODE TO CHECK FOR DRAGGING GHOST PART INTO A COLLIDING STATE
|
|
local minBB, maxBB =
|
|
getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
|
|
|
|
-- need to offset by distance to be dragged
|
|
local currModelCFrame
|
|
if stampData.CurrentParts:IsA "Model" then
|
|
currModelCFrame = stampData.CurrentParts:GetModelCFrame()
|
|
else
|
|
currModelCFrame = stampData.CurrentParts.CFrame
|
|
end
|
|
|
|
minBB += targetCFrame.p - currModelCFrame.p
|
|
maxBB += targetCFrame.p - currModelCFrame.p
|
|
|
|
local clusterMat: Instance
|
|
|
|
-- don't drag into terrain
|
|
if
|
|
clusterPartsInRegion(
|
|
minBB + insertBoundingBoxOverlapVector,
|
|
maxBB - insertBoundingBoxOverlapVector
|
|
)
|
|
then
|
|
if
|
|
lastTarget.CFrame
|
|
and stampData.CurrentParts:FindFirstChild(
|
|
"ClusterMaterial",
|
|
true
|
|
)
|
|
then
|
|
local theClusterMaterial =
|
|
stampData.CurrentParts:FindFirstChild(
|
|
"ClusterMaterial",
|
|
true
|
|
)
|
|
if theClusterMaterial:IsA "Vector3Value" then
|
|
local stampClusterMaterial =
|
|
stampData.CurrentParts:FindFirstChild(
|
|
"ClusterMaterial",
|
|
true
|
|
)
|
|
if stampClusterMaterial then
|
|
stampClusterMaterial = clusterMat
|
|
end
|
|
end
|
|
end
|
|
return
|
|
end
|
|
|
|
-- if we are stamping a terrain part, make sure it goes on the grid! Otherwise preview block could be placed off grid, but stamped on grid
|
|
if isMegaClusterPart(stampData) then
|
|
local cellToStamp = workspace.Terrain:WorldToCell(targetCFrame.p)
|
|
local newCFramePosition = workspace.Terrain:CellCenterToWorld(
|
|
cellToStamp.X,
|
|
cellToStamp.Y,
|
|
cellToStamp.Z
|
|
)
|
|
local _, _, _, R00, R01, R02, R10, R11, R12, R20, R21, R22 =
|
|
targetCFrame:components()
|
|
targetCFrame = CFrame.new(
|
|
newCFramePosition.X,
|
|
newCFramePosition.Y,
|
|
newCFramePosition.Z,
|
|
R00,
|
|
R01,
|
|
R02,
|
|
R10,
|
|
R11,
|
|
R12,
|
|
R20,
|
|
R21,
|
|
R22
|
|
)
|
|
end
|
|
|
|
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
|
|
lastTarget.CFrame = targetCFrame -- successful positioning, so update 'dat cframe
|
|
if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
|
|
clusterMat =
|
|
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
|
|
if clusterMat:IsA "Vector3Value" then
|
|
lastTarget.TerrainOrientation = clusterMat.Value.Z
|
|
end
|
|
end
|
|
|
|
-- auto break joints code
|
|
if mouse and mouse.Target and mouse.Target.Parent then
|
|
local modelInfo = mouse.Target:FindFirstChild "RobloxModel"
|
|
if not modelInfo then
|
|
modelInfo = mouse.Target.Parent:FindFirstChild "RobloxModel"
|
|
end
|
|
|
|
local myModelInfo =
|
|
stampData.CurrentParts:FindFirstChild "UnstampableFaces"
|
|
|
|
--if (modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces")) or (modelInfo and myModelInfo) then -- need better targetSurface calcs
|
|
if true then
|
|
local breakingFaces = ""
|
|
local myBreakingFaces = ""
|
|
if
|
|
modelInfo
|
|
and modelInfo.Parent:FindFirstChild "UnstampableFaces"
|
|
then
|
|
breakingFaces = modelInfo.Parent.UnstampableFaces.Value
|
|
end
|
|
if myModelInfo then
|
|
myBreakingFaces = myModelInfo.Value
|
|
end
|
|
local hitFace = 0
|
|
|
|
if modelInfo then
|
|
hitFace = modelTargetSurface(
|
|
modelInfo.Parent,
|
|
(workspace.CurrentCamera :: Camera).CoordinateFrame.p,
|
|
mouse.Hit.p
|
|
)
|
|
end
|
|
|
|
-- are we stamping TO an unstampable surface?
|
|
for bf in string.gmatch(breakingFaces, "[^,]+") do
|
|
if hitFace == tonumber(bf) then
|
|
-- return before we hit the JointsService code below!
|
|
unstampableSurface = true
|
|
JointsService:ClearJoinAfterMoveJoints() -- clear the JointsService cache
|
|
return
|
|
end
|
|
end
|
|
|
|
-- now we have to cast the ray back in the other direction to find the surface we're stamping FROM
|
|
hitFace = modelTargetSurface(
|
|
stampData.CurrentParts,
|
|
mouse.Hit.p,
|
|
(workspace.CurrentCamera :: Camera).CoordinateFrame.p
|
|
)
|
|
|
|
-- are we stamping WITH an unstampable surface?
|
|
for bf in string.gmatch(myBreakingFaces, "[^,]+") do
|
|
if hitFace == tonumber(bf) then
|
|
unstampableSurface = true
|
|
JointsService:ClearJoinAfterMoveJoints() -- clear the JointsService cache
|
|
return
|
|
end
|
|
end
|
|
|
|
-- just need to match breakingFace against targetSurface using rotation supplied by modelCFrame
|
|
-- targetSurface: 1 is top, 4 is bottom,
|
|
end
|
|
end
|
|
|
|
-- to show joints during the mouse move
|
|
unstampableSurface = false
|
|
JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
|
|
|
|
-- most common mouse inactive error occurs here, so check mouse active one more time in a pcall
|
|
local ok = pcall(function()
|
|
return mouse -- ? - Heliodex
|
|
and mouse.Target
|
|
and not mouse.Target.Parent:FindFirstChild "RobloxModel"
|
|
end)
|
|
if not ok then
|
|
JointsService:ClearJoinAfterMoveJoints()
|
|
mouse = nil
|
|
error "Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check"
|
|
return
|
|
end
|
|
|
|
if
|
|
mouse
|
|
and mouse.Target
|
|
and mouse.Target.Parent:FindFirstChild "RobloxModel" == nil
|
|
then
|
|
JointsService:SetJoinAfterMoveTarget(mouse.Target)
|
|
else
|
|
JointsService:SetJoinAfterMoveTarget(nil)
|
|
end
|
|
JointsService:ShowPermissibleJoints()
|
|
|
|
-- here we allow for a line of high-scalability parts
|
|
if
|
|
isMegaClusterPart(stampData)
|
|
and HighScalabilityLine
|
|
and HighScalabilityLine.Start
|
|
then
|
|
DoHighScalabilityRegionSelect(stampData, HighScalabilityLine)
|
|
end
|
|
end
|
|
|
|
local function setupKeyListener(key, mouse)
|
|
if control and control.Paused then
|
|
return
|
|
end -- don't do this if we have no stamp
|
|
|
|
key = string.lower(key)
|
|
if key == "r" and not autoAlignToFace(stampData.CurrentParts) then -- rotate the model
|
|
gInitial90DegreeRotations += 1
|
|
|
|
-- Update orientation value if this is a fake terrain part
|
|
local clusterValues =
|
|
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
|
|
if clusterValues and clusterValues:IsA "Vector3Value" then
|
|
clusterValues.Value = Vector3.new(
|
|
clusterValues.Value.X,
|
|
clusterValues.Value.Y,
|
|
(clusterValues.Value.Z + 1) % 4
|
|
)
|
|
end
|
|
|
|
-- Rotate the parts or all the parts in the model
|
|
local ry = math.pi / 2
|
|
if
|
|
stampData.CurrentParts:IsA "Model"
|
|
or stampData.CurrentParts:IsA "Tool"
|
|
then
|
|
--stampData.CurrentParts:Rotate(0, ry, 0)
|
|
modelRotate(stampData.CurrentParts, ry)
|
|
else
|
|
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(
|
|
0,
|
|
ry,
|
|
0
|
|
) * stampData.CurrentParts.CFrame
|
|
end
|
|
|
|
-- After rotating, update the position
|
|
configFound, targetCFrame =
|
|
findConfigAtMouseTarget(mouse, stampData)
|
|
if configFound then
|
|
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
|
|
|
|
-- update everything else in MouseMove
|
|
DoStamperMouseMove(mouse)
|
|
end
|
|
elseif
|
|
key == "c"
|
|
and HighScalabilityLine.InternalLine
|
|
and HighScalabilityLine.InternalLine.magnitude > 0
|
|
and HighScalabilityLine.Dimensions < 3
|
|
then -- try to expand our high scalability dragger dimension
|
|
HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] =
|
|
HighScalabilityLine.End
|
|
HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] =
|
|
HighScalabilityLine.InternalLine
|
|
HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1
|
|
HighScalabilityLine.NewHint = true
|
|
end
|
|
end
|
|
|
|
keyCon = Mouse.KeyDown:connect(
|
|
function(key) -- init key connection (keeping code close to func)
|
|
setupKeyListener(key, Mouse)
|
|
end
|
|
)
|
|
|
|
local function resetHighScalabilityLine()
|
|
if not HighScalabilityLine then
|
|
return
|
|
end
|
|
HighScalabilityLine.Start = nil
|
|
HighScalabilityLine.End = nil
|
|
HighScalabilityLine.InternalLine = nil
|
|
HighScalabilityLine.NewHint = true
|
|
end
|
|
|
|
local function DoStamperMouseDown(mouse)
|
|
if not mouse then
|
|
error "Error: RbxStamper.DoStamperMouseDown: Mouse is nil"
|
|
return
|
|
elseif not mouse:IsA "Mouse" then
|
|
error(
|
|
`Error: RbxStamper.DoStamperMouseDown: Mouse is of type {mouse.className}, should be of type Mouse`
|
|
)
|
|
return
|
|
elseif
|
|
not (
|
|
stampData
|
|
and isMegaClusterPart(stampData)
|
|
and mouse
|
|
and HighScalabilityLine
|
|
)
|
|
then
|
|
return
|
|
end
|
|
|
|
local megaCube =
|
|
stampData.CurrentParts:FindFirstChild("MegaClusterCube", true)
|
|
local terrain = workspace.Terrain
|
|
if megaCube then
|
|
HighScalabilityLine.Dimensions = 1
|
|
local tempCell = terrain:WorldToCell(megaCube.CFrame.p)
|
|
HighScalabilityLine.Start =
|
|
terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
|
|
return
|
|
else
|
|
HighScalabilityLine.Dimensions = 1
|
|
local tempCell =
|
|
terrain:WorldToCell(stampData.CurrentParts.CFrame.p)
|
|
HighScalabilityLine.Start =
|
|
terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
|
|
return
|
|
end
|
|
end
|
|
|
|
-- local function makeSurfaceUnjoinable(part, surface)
|
|
-- -- TODO: FILL OUT!
|
|
-- end
|
|
|
|
local function prepareModel(model)
|
|
if not model then
|
|
return nil
|
|
end
|
|
|
|
local gDesiredTrans = 0.7
|
|
local gStaticTrans = 1
|
|
|
|
local clone = model:Clone()
|
|
local scripts = {}
|
|
local parts = {}
|
|
local decals = {}
|
|
|
|
stampData = {
|
|
DisabledScripts = {},
|
|
TransparencyTable = {},
|
|
MaterialTable = {},
|
|
CanCollideTable = {},
|
|
AnchoredTable = {},
|
|
ArchivableTable = {},
|
|
DecalTransparencyTable = {},
|
|
SurfaceTypeTable = {},
|
|
}
|
|
|
|
collectParts(clone, parts, scripts, decals)
|
|
|
|
if #parts <= 0 then
|
|
return nil, "no parts found in modelToStamp"
|
|
end
|
|
|
|
for _, script in pairs(scripts) do
|
|
if not script.Disabled then
|
|
script.Disabled = true
|
|
stampData.DisabledScripts[#stampData.DisabledScripts + 1] =
|
|
script
|
|
end
|
|
end
|
|
for _, part in pairs(parts) do
|
|
stampData.TransparencyTable[part] = part.Transparency
|
|
part.Transparency = gStaticTrans
|
|
+ (1 - gStaticTrans) * part.Transparency
|
|
stampData.MaterialTable[part] = part.Material
|
|
part.Material = Enum.Material.Plastic
|
|
stampData.CanCollideTable[part] = part.CanCollide
|
|
part.CanCollide = false
|
|
stampData.AnchoredTable[part] = part.Anchored
|
|
part.Anchored = true
|
|
stampData.ArchivableTable[part] = part.Archivable
|
|
part.Archivable = false
|
|
|
|
saveSurfaceTypes(part, stampData.SurfaceTypeTable)
|
|
|
|
local fadeInDelayTime = 0.5
|
|
local transFadeInTime = 0.5
|
|
delay(0, function()
|
|
wait(fadeInDelayTime) -- give it some time to be completely transparent
|
|
|
|
local begTime = tick()
|
|
local currTime = begTime
|
|
while
|
|
(currTime - begTime) < transFadeInTime
|
|
and part
|
|
and part:IsA "BasePart"
|
|
and part.Transparency > gDesiredTrans
|
|
do
|
|
local newTrans = 1
|
|
- (
|
|
((currTime - begTime) / transFadeInTime)
|
|
* (gStaticTrans - gDesiredTrans)
|
|
)
|
|
if
|
|
stampData.TransparencyTable
|
|
and stampData.TransparencyTable[part]
|
|
then
|
|
part.Transparency = newTrans
|
|
+ (1 - newTrans)
|
|
* stampData.TransparencyTable[part]
|
|
end
|
|
wait(0.03)
|
|
currTime = tick()
|
|
end
|
|
if
|
|
part
|
|
and part:IsA "BasePart"
|
|
and stampData.TransparencyTable
|
|
and stampData.TransparencyTable[part]
|
|
then
|
|
part.Transparency = gDesiredTrans
|
|
+ (1 - gDesiredTrans)
|
|
* stampData.TransparencyTable[part]
|
|
end
|
|
end)
|
|
end
|
|
|
|
for _, decal in pairs(decals) do
|
|
stampData.DecalTransparencyTable[decal] = decal.Transparency
|
|
decal.Transparency = gDesiredTrans
|
|
+ (1 - gDesiredTrans) * decal.Transparency
|
|
end
|
|
|
|
-- disable all seats
|
|
setSeatEnabledStatus(clone, true)
|
|
setSeatEnabledStatus(clone, false)
|
|
|
|
stampData.CurrentParts = clone
|
|
|
|
-- if auto-alignable, we enforce a pre-rotation to the canonical "0-frame"
|
|
if autoAlignToFace(clone) then
|
|
stampData.CurrentParts:ResetOrientationToIdentity()
|
|
gInitial90DegreeRotations = 0
|
|
else -- pre-rotate if necessary
|
|
local ry = gInitial90DegreeRotations * math.pi / 2
|
|
if
|
|
stampData.CurrentParts:IsA "Model"
|
|
or stampData.CurrentParts:IsA "Tool"
|
|
then
|
|
--stampData.CurrentParts:Rotate(0, ry, 0)
|
|
modelRotate(stampData.CurrentParts, ry)
|
|
else
|
|
stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(
|
|
0,
|
|
ry,
|
|
0
|
|
) * stampData.CurrentParts.CFrame
|
|
end
|
|
end
|
|
|
|
-- since we're cloning the old model instead of the new one, we will need to update the orientation based on the original value AND how many more
|
|
-- rotations we expect since then [either that or we need to store the just-stamped clusterMaterial.Value.Z somewhere]. This should fix the terrain rotation
|
|
-- issue (fingers crossed) [HotThoth]
|
|
|
|
local clusterMaterial =
|
|
stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
|
|
if clusterMaterial and clusterMaterial:IsA "Vector3Value" then
|
|
clusterMaterial.Value = Vector3.new(
|
|
clusterMaterial.Value.X,
|
|
clusterMaterial.Value.Y,
|
|
(clusterMaterial.Value.Z + gInitial90DegreeRotations) % 4
|
|
)
|
|
end
|
|
|
|
-- After rotating, update the position
|
|
configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
|
|
if configFound then
|
|
stampData.CurrentParts =
|
|
positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
|
|
end
|
|
|
|
-- to show joints during the mouse move
|
|
JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts)
|
|
|
|
return clone, parts
|
|
end
|
|
|
|
local function DoStamperMouseUp(mouse)
|
|
if not mouse then
|
|
error "Error: RbxStamper.DoStamperMouseUp: Mouse is nil"
|
|
return false
|
|
end
|
|
if not mouse:IsA "Mouse" then
|
|
error(
|
|
`Error: RbxStamper.DoStamperMouseUp: Mouse is of type {mouse.className}, should be of type Mouse`
|
|
)
|
|
return false
|
|
end
|
|
|
|
if not stampData.Dragger then
|
|
error "Error: RbxStamper.DoStamperMouseUp: stampData.Dragger is nil"
|
|
return false
|
|
end
|
|
|
|
if not HighScalabilityLine then
|
|
return false
|
|
end
|
|
|
|
local checkHighScalabilityStamp
|
|
if stampInModel then
|
|
local canStamp
|
|
local isHSLPart = isMegaClusterPart(stampData)
|
|
|
|
if
|
|
isHSLPart
|
|
and HighScalabilityLine
|
|
and HighScalabilityLine.Start
|
|
and HighScalabilityLine.InternalLine
|
|
and HighScalabilityLine.InternalLine.magnitude > 0
|
|
then -- we have an HSL line, test later
|
|
canStamp = true
|
|
checkHighScalabilityStamp = true
|
|
else
|
|
canStamp, checkHighScalabilityStamp = RbxStamper.CanEditRegion(
|
|
stampData.CurrentParts,
|
|
allowedStampRegion
|
|
)
|
|
end
|
|
|
|
if not canStamp then
|
|
if stampFailedFunc then
|
|
stampFailedFunc()
|
|
end
|
|
return false
|
|
end
|
|
end
|
|
|
|
-- if unstampable face, then don't let us stamp there!
|
|
if unstampableSurface then
|
|
flashRedBox(stampData)
|
|
return false
|
|
end
|
|
|
|
-- recheck if we can stamp, as we just moved part
|
|
local canStamp
|
|
canStamp, checkHighScalabilityStamp =
|
|
RbxStamper.CanEditRegion(stampData.CurrentParts, allowedStampRegion)
|
|
if not canStamp then
|
|
if stampFailedFunc then
|
|
stampFailedFunc()
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Prevent part from being stamped on top of a player
|
|
|
|
local minBB, maxBB =
|
|
getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
|
|
|
|
-- HotThoth's note: Now that above CurrentParts positioning has been commented out, to be truly correct, we would need to use the
|
|
-- value of configFound from the previous onStamperMouseMove call which moved the CurrentParts
|
|
-- Shouldn't this be true when lastTargetCFrame has been set and false otherwise?
|
|
configFound, targetCFrame = findConfigAtMouseTarget(mouse, stampData)
|
|
|
|
if configFound and not HighScalabilityLine.Adorn.Parent then
|
|
if
|
|
clusterPartsInRegion(
|
|
minBB + insertBoundingBoxOverlapVector,
|
|
maxBB - insertBoundingBoxOverlapVector
|
|
)
|
|
then
|
|
flashRedBox(stampData)
|
|
return false
|
|
end
|
|
|
|
local blockingParts = workspace:FindPartsInRegion3(
|
|
Region3.new(
|
|
minBB + insertBoundingBoxOverlapVector,
|
|
maxBB - insertBoundingBoxOverlapVector
|
|
),
|
|
stampData.CurrentParts,
|
|
100
|
|
)
|
|
|
|
for b = 1, #blockingParts do
|
|
if isBlocker(blockingParts[b]) then
|
|
flashRedBox(stampData)
|
|
return false
|
|
end
|
|
end
|
|
|
|
local alreadyPushedUp = {}
|
|
-- if no blocking model below, then see if stamping on top of a character
|
|
for b = 1, #blockingParts do
|
|
if
|
|
blockingParts[b].Parent
|
|
and not alreadyPushedUp[blockingParts[b].Parent]
|
|
and blockingParts[b].Parent:FindFirstChild "Humanoid"
|
|
and blockingParts[b].Parent
|
|
:FindFirstChild("Humanoid")
|
|
:IsA "Humanoid"
|
|
then
|
|
---------------------------------------------------------------------------
|
|
local blockingPersonTorso =
|
|
blockingParts[b].Parent:FindFirstChild "Torso"
|
|
alreadyPushedUp[blockingParts[b].Parent] = true
|
|
|
|
if blockingPersonTorso then
|
|
-- if so, let's push the person upwards so they pop on top of the stamped model/part (but only if there's space above them)
|
|
local newY = maxBB.Y + 3
|
|
if
|
|
spaceAboveCharacter(
|
|
blockingPersonTorso,
|
|
newY,
|
|
stampData
|
|
)
|
|
then
|
|
blockingPersonTorso.CFrame = blockingPersonTorso.CFrame
|
|
+ Vector3.new(
|
|
0,
|
|
newY - blockingPersonTorso.CFrame.p.Y,
|
|
0
|
|
)
|
|
else
|
|
-- if no space, we just error
|
|
flashRedBox(stampData)
|
|
return false
|
|
end
|
|
end
|
|
---------------------------------------------------------------------------
|
|
end
|
|
end
|
|
elseif
|
|
not configFound
|
|
and not (
|
|
HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent
|
|
)
|
|
then -- if no config then only stamp if it's a real HSL!
|
|
resetHighScalabilityLine()
|
|
return false
|
|
end
|
|
|
|
-- something will be stamped! so set the "StampedSomething" toggle to true
|
|
if
|
|
game:FindFirstChild "Players"
|
|
and Players.LocalPlayer
|
|
and Players.LocalPlayer.Character
|
|
then
|
|
local stampTracker =
|
|
Players.LocalPlayer.Character:FindFirstChild "StampTracker"
|
|
if stampTracker and not stampTracker.Value then
|
|
stampTracker.Value = true
|
|
end
|
|
end
|
|
|
|
-- if we drew a line of mega parts, stamp them out
|
|
if
|
|
HighScalabilityLine.Start
|
|
and HighScalabilityLine.Adorn.Parent
|
|
and isMegaClusterPart(stampData)
|
|
and (
|
|
ResolveMegaClusterStamp(
|
|
stampData,
|
|
HighScalabilityLine,
|
|
checkHighScalabilityStamp,
|
|
allowedStampRegion
|
|
) or checkHighScalabilityStamp
|
|
)
|
|
then
|
|
-- kill the ghost part
|
|
stampData.CurrentParts.Parent = nil
|
|
return true
|
|
end
|
|
|
|
-- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff]
|
|
HighScalabilityLine.Start = nil
|
|
HighScalabilityLine.Adorn.Parent = nil
|
|
|
|
cluster = workspace.Terrain
|
|
|
|
-- if target point is in cluster, just use cluster:SetCell
|
|
if isMegaClusterPart(stampData) then
|
|
-- if targetCFrame is inside cluster, just set that cell to 1 and return
|
|
--local cellPos = cluster:WorldToCell(targetCFrame.p)
|
|
|
|
local cellPos
|
|
if stampData.CurrentParts:IsA "Model" then
|
|
cellPos = cluster:WorldToCell(
|
|
stampData.CurrentParts:GetModelCFrame().p
|
|
)
|
|
else
|
|
cellPos = cluster:WorldToCell(stampData.CurrentParts.CFrame.p)
|
|
end
|
|
|
|
local cMax = workspace.Terrain.MaxExtents.Max
|
|
local cMin = workspace.Terrain.MaxExtents.Min
|
|
|
|
if
|
|
checkTerrainBlockCollisions(
|
|
stampData,
|
|
allowedStampRegion,
|
|
cellPos,
|
|
false
|
|
)
|
|
then
|
|
local clusterValues = stampData.CurrentParts:FindFirstChild(
|
|
"ClusterMaterial",
|
|
true
|
|
)
|
|
local waterForceTag =
|
|
stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
|
|
local waterForceDirectionTag =
|
|
stampData.CurrentParts:FindFirstChild(
|
|
"WaterForceDirectionTag",
|
|
true
|
|
)
|
|
|
|
if
|
|
cellPos.X >= cMin.X
|
|
and cellPos.Y >= cMin.Y
|
|
and cellPos.Z >= cMin.Z
|
|
and cellPos.X < cMax.X
|
|
and cellPos.Y < cMax.Y
|
|
and cellPos.Z < cMax.Z
|
|
then
|
|
if waterForceTag then
|
|
cluster:SetWaterCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
Enum.WaterForce[waterForceTag.Value],
|
|
Enum.WaterDirection[waterForceDirectionTag.Value]
|
|
)
|
|
elseif not clusterValues then
|
|
cluster:SetCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
cellInfo.Material,
|
|
cellInfo.clusterType,
|
|
gInitial90DegreeRotations % 4
|
|
)
|
|
elseif clusterValues:IsA "Vector3Value" then
|
|
cluster:SetCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
clusterValues.Value.X,
|
|
clusterValues.Value.Y,
|
|
clusterValues.Value.Z
|
|
)
|
|
else
|
|
cluster:SetCell(
|
|
cellPos.X,
|
|
cellPos.Y,
|
|
cellPos.Z,
|
|
clusterValues.Value,
|
|
0,
|
|
0
|
|
)
|
|
end
|
|
|
|
local autoWedgeClusterParts = false
|
|
if stampData.CurrentParts:FindFirstChild "AutoWedge" then
|
|
autoWedgeClusterParts = true
|
|
end
|
|
|
|
-- auto-wedge it
|
|
if autoWedgeClusterParts then
|
|
workspace.Terrain:AutowedgeCells(
|
|
Region3int16.new(
|
|
Vector3int16.new(
|
|
cellPos.x - 1,
|
|
cellPos.y - 1,
|
|
cellPos.z - 1
|
|
),
|
|
Vector3int16.new(
|
|
cellPos.x + 1,
|
|
cellPos.y + 1,
|
|
cellPos.z + 1
|
|
)
|
|
)
|
|
)
|
|
end
|
|
|
|
-- kill the ghost part
|
|
stampData.CurrentParts.Parent = nil
|
|
|
|
-- Mark for undo. It has to happen here or the selection display will come back also.
|
|
pcall(function()
|
|
ChangeHistoryService:SetWaypoint "StamperSingle"
|
|
end)
|
|
return true
|
|
end
|
|
else
|
|
-- you tried to stamp a HSL-single part where one does not belong!
|
|
flashRedBox(stampData)
|
|
return false
|
|
end
|
|
end
|
|
|
|
local function getPlayer()
|
|
if game:FindFirstChild "Players" and Players.LocalPlayer then
|
|
return Players.LocalPlayer
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Post process: after positioning the part or model, restore transparency, material, anchored and collide states and create joints
|
|
if
|
|
stampData.CurrentParts:IsA "Model"
|
|
or stampData.CurrentParts:IsA "Tool"
|
|
then
|
|
if stampData.CurrentParts:IsA "Model" then
|
|
-- Tyler's magical hack-code for allowing/preserving clones of both Surface and Manual Welds... just don't ask X<
|
|
local manualWeldTable = {}
|
|
local manualWeldParentTable = {}
|
|
saveTheWelds(
|
|
stampData.CurrentParts,
|
|
manualWeldTable,
|
|
manualWeldParentTable
|
|
)
|
|
stampData.CurrentParts:BreakJoints()
|
|
stampData.CurrentParts:MakeJoints()
|
|
restoreTheWelds(manualWeldTable, manualWeldParentTable)
|
|
end
|
|
|
|
-- if it's a model, we also want to fill in the playerID and playerName tags, if it has those (e.g. for the friend-only door)
|
|
local playerIdTag =
|
|
stampData.CurrentParts:FindFirstChild "PlayerIdTag"
|
|
local playerNameTag =
|
|
stampData.CurrentParts:FindFirstChild "PlayerNameTag"
|
|
|
|
local tempPlayerValue
|
|
if playerIdTag then
|
|
tempPlayerValue = getPlayer()
|
|
if tempPlayerValue then
|
|
playerIdTag.Value = tempPlayerValue.userId
|
|
end
|
|
end
|
|
if
|
|
playerNameTag
|
|
and game:FindFirstChild "Players"
|
|
and Players.LocalPlayer
|
|
then
|
|
tempPlayerValue = Players.LocalPlayer
|
|
if tempPlayerValue then
|
|
playerNameTag.Value = tempPlayerValue.Name
|
|
end
|
|
end
|
|
-- ...and tag all inserted models for subsequent origin identification
|
|
-- if no RobloxModel tag already exists, then add it.
|
|
if not stampData.CurrentParts:FindFirstChild "RobloxModel" then
|
|
New "BoolValue" {
|
|
Name = "RobloxModel",
|
|
Parent = stampData.CurrentParts,
|
|
}
|
|
|
|
if
|
|
not stampData.CurrentParts:FindFirstChild "RobloxStamper"
|
|
then
|
|
New "BoolValue" {
|
|
Name = "RobloxStamper",
|
|
Parent = stampData.CurrentParts,
|
|
}
|
|
end
|
|
end
|
|
else
|
|
stampData.CurrentParts:BreakJoints()
|
|
if stampData.CurrentParts:FindFirstChild "RobloxStamper" == nil then
|
|
New "BoolValue" {
|
|
Name = "RobloxStamper",
|
|
Parent = stampData.CurrentParts,
|
|
}
|
|
end
|
|
end
|
|
|
|
-- make sure all the joints are activated before restoring anchor states
|
|
-- if not createJoints then
|
|
JointsService:CreateJoinAfterMoveJoints()
|
|
-- end
|
|
|
|
-- Restore the original properties for all parts being stamped
|
|
for part, transparency in pairs(stampData.TransparencyTable) do
|
|
part.Transparency = transparency
|
|
end
|
|
for part, archivable in pairs(stampData.ArchivableTable) do
|
|
part.Archivable = archivable
|
|
end
|
|
for part, material in pairs(stampData.MaterialTable) do
|
|
part.Material = material
|
|
end
|
|
for part, collide in pairs(stampData.CanCollideTable) do
|
|
part.CanCollide = collide
|
|
end
|
|
for part, anchored in pairs(stampData.AnchoredTable) do
|
|
part.Anchored = anchored
|
|
end
|
|
for decal, transparency in pairs(stampData.DecalTransparencyTable) do
|
|
decal.Transparency = transparency
|
|
end
|
|
|
|
for part, surfaces in pairs(stampData.SurfaceTypeTable) do
|
|
loadSurfaceTypes(part, surfaces)
|
|
end
|
|
|
|
if isMegaClusterPart(stampData) then
|
|
stampData.CurrentParts.Transparency = 0
|
|
end
|
|
|
|
-- re-enable all seats
|
|
setSeatEnabledStatus(stampData.CurrentParts, true)
|
|
|
|
stampData.TransparencyTable = nil
|
|
stampData.ArchivableTable = nil
|
|
stampData.MaterialTable = nil
|
|
stampData.CanCollideTable = nil
|
|
stampData.AnchoredTable = nil
|
|
stampData.SurfaceTypeTable = nil
|
|
|
|
-- ...and tag all inserted models for subsequent origin identification
|
|
-- if no RobloxModel tag already exists, then add it.
|
|
if stampData.CurrentParts:FindFirstChild "RobloxModel" == nil then
|
|
local stringTag = Instance.new "BoolValue"
|
|
stringTag.Name = "RobloxModel"
|
|
stringTag.Parent = stampData.CurrentParts
|
|
end
|
|
|
|
-- and make sure we don't delete it, now that it's not a ghost part
|
|
-- if ghostRemovalScript then
|
|
-- ghostRemovalScript.Parent = nil
|
|
-- end
|
|
|
|
-- Re-enable the scripts
|
|
for _, script in pairs(stampData.DisabledScripts) do
|
|
script.Disabled = false
|
|
end
|
|
|
|
-- Now that they are all marked enabled, reinsert them into the world so they start running
|
|
for _, script in pairs(stampData.DisabledScripts) do
|
|
local oldParent = script.Parent
|
|
script.Parent = nil
|
|
script:Clone().Parent = oldParent
|
|
end
|
|
|
|
-- clear out more data
|
|
stampData.DisabledScripts = nil
|
|
stampData.Dragger = nil
|
|
stampData.CurrentParts = nil
|
|
|
|
pcall(function()
|
|
ChangeHistoryService:SetWaypoint "StampedObject"
|
|
end)
|
|
return true
|
|
end
|
|
|
|
local function pauseStamper()
|
|
for i = 1, #mouseCons do -- stop the mouse from doing anything
|
|
mouseCons[i]:disconnect()
|
|
mouseCons[i] = nil
|
|
end
|
|
mouseCons = {}
|
|
|
|
if stampData and stampData.CurrentParts then -- remove our ghost part
|
|
stampData.CurrentParts.Parent = nil
|
|
stampData.CurrentParts:Remove()
|
|
end
|
|
|
|
resetHighScalabilityLine()
|
|
|
|
JointsService:ClearJoinAfterMoveJoints()
|
|
end
|
|
|
|
local function prepareUnjoinableSurfaces(modelCFrame, parts, whichSurface)
|
|
local AXIS_VECTORS =
|
|
{ Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1) } -- maybe last one is negative? TODO: check this!
|
|
local isPositive = 1
|
|
if whichSurface < 0 then
|
|
isPositive *= -1
|
|
whichSurface *= -1
|
|
end
|
|
local surfaceNormal = isPositive
|
|
* modelCFrame:vectorToWorldSpace(AXIS_VECTORS[whichSurface])
|
|
|
|
for i = 1, #parts do
|
|
local currPart = parts[i]
|
|
|
|
-- now just need to find which surface of currPart most closely match surfaceNormal and then set that to Unjoinable
|
|
local surfaceNormalInLocalCoords =
|
|
currPart.CFrame:vectorToObjectSpace(surfaceNormal)
|
|
if
|
|
math.abs(surfaceNormalInLocalCoords.X)
|
|
> math.abs(surfaceNormalInLocalCoords.Y)
|
|
then
|
|
if
|
|
math.abs(surfaceNormalInLocalCoords.X)
|
|
> math.abs(surfaceNormalInLocalCoords.Z)
|
|
then
|
|
if surfaceNormalInLocalCoords.X > 0 then
|
|
currPart.RightSurface = "Unjoinable"
|
|
else
|
|
currPart.LeftSurface = "Unjoinable"
|
|
end
|
|
else
|
|
if surfaceNormalInLocalCoords.Z > 0 then
|
|
currPart.BackSurface = "Unjoinable"
|
|
else
|
|
currPart.FrontSurface = "Unjoinable"
|
|
end
|
|
end
|
|
else
|
|
if
|
|
math.abs(surfaceNormalInLocalCoords.Y)
|
|
> math.abs(surfaceNormalInLocalCoords.Z)
|
|
then
|
|
if surfaceNormalInLocalCoords.Y > 0 then
|
|
currPart.TopSurface = "Unjoinable"
|
|
else
|
|
currPart.BottomSurface = "Unjoinable"
|
|
end
|
|
else
|
|
if surfaceNormalInLocalCoords.Z > 0 then
|
|
currPart.BackSurface = "Unjoinable"
|
|
else
|
|
currPart.FrontSurface = "Unjoinable"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function resumeStamper()
|
|
local clone, parts = prepareModel(modelToStamp)
|
|
|
|
if not clone or not parts then
|
|
return
|
|
end
|
|
|
|
-- if we have unjoinable faces, then we want to change those surfaces to be Unjoinable
|
|
local unjoinableTag = clone:FindFirstChild("UnjoinableFaces", true)
|
|
if unjoinableTag then
|
|
for unjoinableSurface in string.gmatch(unjoinableTag.Value, "[^,]*") do
|
|
if tonumber(unjoinableSurface) then
|
|
if clone:IsA "Model" then
|
|
prepareUnjoinableSurfaces(
|
|
clone:GetModelCFrame(),
|
|
parts,
|
|
tonumber(unjoinableSurface)
|
|
)
|
|
else
|
|
prepareUnjoinableSurfaces(
|
|
clone.CFrame,
|
|
parts,
|
|
tonumber(unjoinableSurface)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
stampData.ErrorBox = errorBox
|
|
clone.Parent = stampInModel or workspace
|
|
|
|
if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector
|
|
local clusterMaterial =
|
|
clone:FindFirstChild("ClusterMaterial", true)
|
|
if clusterMaterial:IsA "Vector3Value" then
|
|
cellInfo.Material = clusterMaterial.Value.X
|
|
cellInfo.clusterType = clusterMaterial.Value.Y
|
|
cellInfo.clusterOrientation = clusterMaterial.Value.Z
|
|
elseif clusterMaterial:IsA "IntValue" then
|
|
cellInfo.Material = clusterMaterial.Value
|
|
end
|
|
end
|
|
|
|
pcall(function()
|
|
mouseTarget = Mouse.Target
|
|
end)
|
|
|
|
if
|
|
mouseTarget
|
|
and mouseTarget.Parent:FindFirstChild "RobloxModel" == nil
|
|
then
|
|
JointsService:SetJoinAfterMoveTarget(mouseTarget)
|
|
else
|
|
JointsService:SetJoinAfterMoveTarget(nil)
|
|
end
|
|
JointsService:ShowPermissibleJoints()
|
|
|
|
for _, object in pairs(stampData.DisabledScripts) do
|
|
if object.Name == "GhostRemovalScript" then
|
|
object.Parent = stampData.CurrentParts
|
|
end
|
|
end
|
|
|
|
stampData.Dragger = Instance.new "Dragger"
|
|
|
|
--Begin a movement by faking a MouseDown signal
|
|
stampData.Dragger:MouseDown(parts[1], Vector3.new(0, 0, 0), parts)
|
|
stampData.Dragger:MouseUp()
|
|
|
|
DoStamperMouseMove(Mouse)
|
|
|
|
table.insert(
|
|
mouseCons,
|
|
Mouse.Move:connect(function()
|
|
if movingLock or stampUpLock then
|
|
return
|
|
end
|
|
movingLock = true
|
|
DoStamperMouseMove(Mouse)
|
|
movingLock = false
|
|
end)
|
|
)
|
|
|
|
table.insert(
|
|
mouseCons,
|
|
Mouse.Button1Down:connect(function()
|
|
DoStamperMouseDown(Mouse)
|
|
end)
|
|
)
|
|
|
|
table.insert(
|
|
mouseCons,
|
|
Mouse.Button1Up:connect(function()
|
|
stampUpLock = true
|
|
while movingLock do
|
|
wait()
|
|
end
|
|
stamped.Value = DoStamperMouseUp(Mouse)
|
|
resetHighScalabilityLine()
|
|
stampUpLock = false
|
|
end)
|
|
)
|
|
|
|
stamped.Value = false
|
|
end
|
|
|
|
local function resetStamperState(newModelToStamp)
|
|
-- if we have a new model, swap it out
|
|
if newModelToStamp then
|
|
if
|
|
not newModelToStamp:IsA "Model"
|
|
and not newModelToStamp:IsA "BasePart"
|
|
then
|
|
error "resetStamperState: newModelToStamp (first arg) is not nil, but not a model or part!"
|
|
end
|
|
modelToStamp = newModelToStamp
|
|
end
|
|
|
|
-- first clear our state
|
|
pauseStamper()
|
|
-- now lets load in the new model
|
|
resumeStamper()
|
|
end
|
|
|
|
-- load the model initially
|
|
resetStamperState()
|
|
|
|
-- setup the control table we pass back to the user
|
|
control.Stamped = stamped -- BoolValue that fires when user stamps
|
|
control.Paused = false
|
|
|
|
function control.LoadNewModel(newStampModel) -- allows us to specify a new stamper model to be used with this stamper
|
|
if
|
|
newStampModel
|
|
and not newStampModel:IsA "Model"
|
|
and not newStampModel:IsA "BasePart"
|
|
then
|
|
error "Control.LoadNewModel: newStampModel (first arg) is not a Model or Part!"
|
|
return nil
|
|
end
|
|
resetStamperState(newStampModel)
|
|
return -- bruh
|
|
end
|
|
|
|
function control.ReloadModel() -- will automatically set stamper to get a new model of current model and start stamping with new model
|
|
resetStamperState()
|
|
end
|
|
|
|
function control.Pause() -- temporarily stops stamping, use resume to start up again
|
|
if not control.Paused then
|
|
pauseStamper()
|
|
control.Paused = true
|
|
else
|
|
print "RbxStamper Warning: Tried to call Control.Pause() when already paused"
|
|
end
|
|
end
|
|
|
|
function control.Resume() -- resumes stamping, if currently paused
|
|
if control.Paused then
|
|
resumeStamper()
|
|
control.Paused = false
|
|
else
|
|
print "RbxStamper Warning: Tried to call Control.Resume() without Pausing First"
|
|
end
|
|
end
|
|
|
|
function control.ResetRotation() -- resets the model rotation so new models are at default orientation
|
|
-- gInitial90DegreeRotations = 0
|
|
-- Note: This function will not always work quite the way we want it to; we will have to build this out further so it works with
|
|
-- High-Scalability and with the new model orientation setting methods (model:ResetOrientationToIdentity()) [HotThoth]
|
|
end
|
|
|
|
function control.Destroy() -- Stops current Stamp operation and destroys control construct
|
|
for i = 1, #mouseCons do
|
|
mouseCons[i]:disconnect()
|
|
mouseCons[i] = nil
|
|
end
|
|
|
|
if keyCon then
|
|
keyCon:disconnect()
|
|
end
|
|
|
|
JointsService:ClearJoinAfterMoveJoints()
|
|
|
|
if adorn then
|
|
adorn:Destroy()
|
|
end
|
|
if adornPart then
|
|
adornPart:Destroy()
|
|
end
|
|
if errorBox then
|
|
errorBox:Destroy()
|
|
end
|
|
if stampData then
|
|
if stampData.Dragger then
|
|
stampData.Dragger:Destroy()
|
|
end
|
|
if stampData.CurrentParts then
|
|
stampData.CurrentParts:Destroy()
|
|
end
|
|
end
|
|
if control and control.Stamped then
|
|
control.Stamped:Destroy()
|
|
end
|
|
control = nil
|
|
end
|
|
|
|
return control
|
|
end
|
|
|
|
function RbxStamper.Help(funcNameOrFunc)
|
|
--input argument can be a string or a function. Should return a description (of arguments and expected side effects)
|
|
if
|
|
funcNameOrFunc == "GetStampModel"
|
|
or funcNameOrFunc == RbxStamper.GetStampModel
|
|
then
|
|
return [[Function GetStampModel.
|
|
Arguments: assetId, useAssetVersionId. assetId is the asset to load in, define useAssetVersionId as true if assetId is a version id instead of a relative assetId. Side effect: returns a model of the assetId, or a string with error message if something fails]]
|
|
elseif
|
|
funcNameOrFunc == "SetupStamperDragger"
|
|
or funcNameOrFunc == RbxStamper.SetupStamperDragger
|
|
then
|
|
return [[Function SetupStamperDragger.
|
|
Side Effect: Creates 4x4 stamping mechanism for building out parts quickly.
|
|
Arguments: ModelToStamp, Mouse, LegalStampCheckFunction. ModelToStamp should be a Model or Part, preferrably loaded from RbxStamper.GetStampModel and should have extents that are multiples of 4. Mouse should be a mouse object (obtained from things such as Tool.OnEquipped), used to drag parts around 'stamp' them out. LegalStampCheckFunction is optional, used as a callback with a table argument (table is full of instances about to be stamped). Function should return either true or false, false stopping the stamp action.]]
|
|
end
|
|
return "No help available for this function"
|
|
end
|
|
|
|
return RbxStamper
|