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