local t = {} function waitForChild(instance, name) while not instance:FindFirstChild(name) do instance.ChildAdded:wait() end end -- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0) -- -- vectorPos - End point of the line. -- -- Return: -- 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. function PlaneIntersection(vectorPos) local hit = false local currCamera = game:GetService("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 = game:GetService("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. 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 = game:GetService("Workspace").Terrain:WorldToCellPreferSolid(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z)) local planeLoc = nil -- If nothing was hit, do the plane intersection. if 0 == game:GetService("Workspace").Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then cell = nil 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(.3, .3, .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 index,child in pairs(object:GetChildren()) do collectParts(child, baseParts, scripts, decals) end end local function clusterPartsInRegion(startVector, endVector) local cluster = game:GetService("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 end if 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 else return false end 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 else return 3 end elseif math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot) then if yDot > 0 then return 1 else return 4 end else if zDot > 0 then return 2 else return 5 end end end local function positionPartsAtCFrame3(aCFrame, currentParts) local insertCFrame = nil 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) < .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 = nil local modelSize = nil 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 justify = justifyValue.Value two = Vector3.new(2, 2, 2) actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01) containingGridBox = Vector3.new(4 * math.ceil(actualBox.x/4), 4 * math.ceil(actualBox.y/4), 4 * math.ceil(actualBox.z/4)) adjustment = containingGridBox - actualBox minVec = minVec - 0.5 * adjustment * justify maxVec = 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 vec1 = partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size) 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 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 vec1 = partOrModel:GetModelCFrame():pointToWorldSpace(-0.5 * partOrModel:GetModelSize()) 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") ~= nil then return getBoundingBox2(targetPart.Parent) else return getBoundingBox2(targetPart) end end local function getMouseTargetCFrame(targetPart) if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then if targetPart.Parent:IsA("Tool") then return targetPart.Parent.Handle.CFrame else return targetPart.Parent:GetModelCFrame() end else return targetPart.CFrame end end local function isBlocker(part) -- returns whether or not we want to cancel the stamp because we're blocked by this part if not part then return false end if not part.Parent then return false end if part:FindFirstChild("Humanoid") then return false end if part:FindFirstChild("RobloxStamper") or part:FindFirstChild("RobloxModel") then return true end if part:IsA("Part") and not part.CanCollide then return false end if part == game:GetService("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, newTorsoY, stampData) local partsAboveChar = game:GetService("Workspace"):FindPartsInRegion3( Region3.new(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75), Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)), charTorso.Parent, 100) for j = 1, #partsAboveChar do if partsAboveChar[j].CanCollide and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts) then return false end end if clusterPartsInRegion(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75), Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)) then return false end return true end local function findConfigAtMouseTarget(Mouse, stampData) -- *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. if not Mouse then return nil end -- This can happen sometimes, return if so if not stampData then error("findConfigAtMouseTarget: stampData is nil") return nil end if not stampData["CurrentParts"] then return nil 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 then if stampData.CurrentParts:IsA("Tool") then Mouse.TargetFilter = stampData.CurrentParts.Handle else Mouse.TargetFilter = stampData.CurrentParts end end local hitPlane = false local targetPart = nil local success = pcall(function() targetPart = Mouse.Target end) if not success 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 = nil -- 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 else targetPart = game:GetService("Workspace").Terrain hitPlane = true -- Take into account error that will occur. cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z) mouseHitInWorld = game:GetService("Workspace").Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z) end 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 = game:GetService("Workspace"):FindFirstChild("Terrain") end local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld) if hitPlane then cellID = cellPos end targetCFrame = CFrame.new(game:GetService("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, game.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 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 = targetRefPointInTarget * (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget) insertRefPointInInsert = 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 = deltaClamped * clampToSurface local targetTouchInTarget = deltaClamped + targetRefPointInTarget local TargetTouchRelToWorld = targetCFrame:pointToWorldSpace(targetTouchInTarget) local InsertTouchInWorld = insertCFrame:vectorToWorldSpace(insertRefPointInInsert) local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld local x, y, z, 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 > .382683432 then -- between 22.5 and 45 degrees, so truncate to 45-degree tilt return .707106781 * hypotenuse * bigSign, .707106781 * hypotenuse * littleSign else -- between 0 and 22.5 degrees, so truncate to 0-degree tilt return hypotenuse * bigSign, 0 end 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 t.CanEditRegion = function(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 end if 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 t.GetStampModel = function(assetId, terrainShape, useAssetVersionId) if assetId == 0 then return nil, "No Asset" end if assetId < 0 then return nil, "Negative Asset" end local function UnlockInstances(object) if object:IsA("BasePart") then object.Locked = false end for index,child in pairs(object:GetChildren()) do UnlockInstances(child) end end local function getClosestColorToTerrainMaterial(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") else return BrickColor.new("Bright green") end end local function setupFakeTerrainPart(cellMat, cellType, cellOrient) local newTerrainPiece = nil if (cellType == 1 or cellType == 4) then newTerrainPiece = Instance.new("WedgePart") newTerrainPiece.formFactor = "Custom" elseif (cellType == 2) then newTerrainPiece = Instance.new("CornerWedgePart") else newTerrainPiece = Instance.new("Part") newTerrainPiece.formFactor = "Custom" end newTerrainPiece.Name = "MegaClusterCube" newTerrainPiece.Size = Vector3.new(4, 4, 4) newTerrainPiece.BottomSurface = "Smooth" newTerrainPiece.TopSurface = "Smooth" -- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors newTerrainPiece.BrickColor = getClosestColorToTerrainMaterial(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 local inverseCornerWedgeMesh = Instance.new("SpecialMesh") inverseCornerWedgeMesh.MeshType = "FileMesh" inverseCornerWedgeMesh.MeshId = "https://assetdelivery.roblox.com/v1/asset?id=66832495" inverseCornerWedgeMesh.Scale = Vector3.new(2, 2, 2) inverseCornerWedgeMesh.Parent = newTerrainPiece end local materialTag = Instance.new("Vector3Value") materialTag.Value = Vector3.new(cellMat, cellType, cellOrient) materialTag.Name = "ClusterMaterial" materialTag.Parent = newTerrainPiece return newTerrainPiece 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 loading = true if useAssetVersionId then loader = coroutine.create(function() root = game:GetService("InsertService"):LoadAssetVersion(assetId) loading = false end) coroutine.resume(loader) else loader = coroutine.create(function() root = game:GetService("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 = totalTime + tick() - lastGameTime end loading = false if totalTime >= maxWait then return nil, "Load Time Fail" end if root == nil then return nil, "Load Asset Fail" end if 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 pos, 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 index,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", root) stringTag.Name = "RobloxModel" if root:FindFirstChild("RobloxStamper") == nil then local stringTag2 = Instance.new("BoolValue", root) stringTag2.Name = "RobloxStamper" end end if terrainShape then if 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 end return root end t.SetupStamperDragger = function(modelToStamp, Mouse, StampInModel, AllowedStampRegion, StampFailedFunc) if not modelToStamp then error("SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model") return nil end if not modelToStamp:IsA("Model") and not modelToStamp:IsA("BasePart") then error("SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!") return nil end if not Mouse then error("SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object") return nil end if not Mouse:IsA("Mouse") then error("SetupStamperDragger: Mouse (second arg) is not of type Mouse!") return nil end local stampInModel = nil local allowedStampRegion = nil local stampFailedFunc = nil 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 = nil local mouseTarget = nil local errorBox = Instance.new("SelectionBox") errorBox.Color = BrickColor.new("Bright red") errorBox.Transparency = 0 errorBox.Archivable = false -- for megacluster MEGA STAMPING local adornPart = Instance.new("Part") adornPart.Parent = nil adornPart.formFactor = "Custom" adornPart.Size = Vector3.new(4, 4, 4) adornPart.CFrame = CFrame.new() adornPart.Archivable = false local adorn = Instance.new("SelectionBox") adorn.Color = BrickColor.new("Toothpaste") adorn.Adornee = adornPart adorn.Visible = true adorn.Transparency = 0 adorn.Name = "HighScalabilityStamperLine" adorn.Archivable = false local HighScalabilityLine = {} HighScalabilityLine.Start = nil HighScalabilityLine.End = nil HighScalabilityLine.Adorn = adorn HighScalabilityLine.AdornPart = adornPart HighScalabilityLine.InternalLine = nil HighScalabilityLine.NewHint = true HighScalabilityLine.MorePoints = {nil, nil} HighScalabilityLine.MoreLines = {nil, nil} HighScalabilityLine.Dimensions = 1 local control = {} local movingLock = false local stampUpLock = false local unstampableSurface = false local mouseCons = {} local keyCon = nil local stamped = Instance.new("BoolValue") stamped.Archivable = false stamped.Value = false local lastTarget = {} lastTarget.TerrainOrientation = 0 lastTarget.CFrame = 0 local cellInfo = {} cellInfo.Material = 1 cellInfo.clusterType = 0 cellInfo.clusterOrientation = 0 local function isMegaClusterPart() if not stampData then return false end if not stampData.CurrentParts then return false end return ( stampData.CurrentParts:FindFirstChild("ClusterMaterial",true) or (stampData.CurrentParts.Name == "MegaClusterCube") ) end local function DoHighScalabilityRegionSelect() local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube") if not megaCube then if not stampData.CurrentParts.Name == "MegaClusterCube" then return else megaCube = stampData.CurrentParts end end HighScalabilityLine.End = megaCube.CFrame.p local line = nil 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 = line2 - line.unit*line.unit:Dot(line2) 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 = line2 - xAxis * xComp else line2 = 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 = line3 - line.unit * line.unit:Dot(line3) line3 = line3 - line2.unit * line2.unit:Dot(line3) HighScalabilityLine.InternalLine = line3 end -- resize the "line" graphic to be the correct size and orientation 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 = nil if game:GetService("Players")["LocalPlayer"] then gui = game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") if gui and gui:IsA("PlayerGui") then if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough HighScalabilityLine.Adorn.Parent = gui elseif HighScalabilityLine.Dimensions > 1 then HighScalabilityLine.Adorn.Parent = gui end end end if gui == nil then -- we are in studio gui = game:GetService("CoreGui") if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough HighScalabilityLine.Adorn.Parent = gui elseif HighScalabilityLine.Dimensions > 1 then HighScalabilityLine.Adorn.Parent = gui end end end local function DoStamperMouseMove(Mouse) if not Mouse then error("Error: RbxStamper.DoStamperMouseMove: Mouse is nil") return end if 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 then local cellPos = GetTerrainForMouse(Mouse) if nil == cellPos then return end end if 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 = 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 = nil if stampData.CurrentParts:IsA("Model") then currModelCFrame = stampData.CurrentParts:GetModelCFrame() else currModelCFrame = stampData.CurrentParts.CFrame end minBB = minBB + targetCFrame.p - currModelCFrame.p maxBB = maxBB + targetCFrame.p - currModelCFrame.p -- don't drag into terrain if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then if lastTarget.CFrame then if (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 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() then local cellToStamp = game:GetService("Workspace").Terrain:WorldToCell(targetCFrame.p) local newCFramePosition = game:GetService("Workspace").Terrain:CellCenterToWorld(cellToStamp.X, cellToStamp.Y, cellToStamp.Z) local x, y, z, 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 local 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, game:GetService("Workspace").CurrentCamera.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 game.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, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p) -- are we stamping WITH an unstampable surface? for bf in string.gmatch(myBreakingFaces, "[^,]+") do if hitFace == tonumber(bf) then unstampableSurface = true game.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 game.JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts) -- most common mouse inactive error occurs here, so check mouse active one more time in a pcall if not pcall(function() if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then return else return end end) then error("Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check") game.JointsService:ClearJoinAfterMoveJoints() Mouse = nil return end if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then game.JointsService:SetJoinAfterMoveTarget(Mouse.Target) else game.JointsService:SetJoinAfterMoveTarget(nil) end game.JointsService:ShowPermissibleJoints() -- here we allow for a line of high-scalability parts if isMegaClusterPart() and HighScalabilityLine and HighScalabilityLine.Start then DoHighScalabilityRegionSelect() 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 = 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' then -- try to expand our high scalability dragger dimension if HighScalabilityLine.InternalLine and HighScalabilityLine.InternalLine.magnitude > 0 and HighScalabilityLine.Dimensions < 3 then HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] = HighScalabilityLine.End HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] = HighScalabilityLine.InternalLine HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1 HighScalabilityLine.NewHint = true end end end keyCon = Mouse.KeyDown:connect(function(key) -- init key connection (keeping code close to func) setupKeyListener(key, Mouse) end) local function resetHighScalabilityLine() if HighScalabilityLine then HighScalabilityLine.Start = nil HighScalabilityLine.End = nil HighScalabilityLine.InternalLine = nil HighScalabilityLine.NewHint = true end end local function flashRedBox() local gui = game:GetService("CoreGui") if game:GetService("Players") then if game:GetService("Players")["LocalPlayer"] then if game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") then gui = game:GetService("Players").LocalPlayer.PlayerGui end end 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 i = 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 DoStamperMouseDown(Mouse) if not Mouse then error("Error: RbxStamper.DoStamperMouseDown: Mouse is nil") return end if not Mouse:IsA("Mouse") then error("Error: RbxStamper.DoStamperMouseDown: Mouse is of type", Mouse.className,"should be of type Mouse") return end if not stampData then return end if isMegaClusterPart() then if Mouse and HighScalabilityLine then local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube", true) local terrain = game:GetService("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 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 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 = {} stampData.DisabledScripts = {} stampData.TransparencyTable = {} stampData.MaterialTable = {} stampData.CanCollideTable = {} stampData.AnchoredTable = {} stampData.ArchivableTable = {} stampData.DecalTransparencyTable = {} stampData.SurfaceTypeTable = {} collectParts(clone, parts, scripts, decals) if #parts <= 0 then return nil, "no parts found in modelToStamp" end for index,script in pairs(scripts) do if not(script.Disabled) then script.Disabled = true stampData.DisabledScripts[#stampData.DisabledScripts + 1] = script end end for index, 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") then if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then part.Transparency = gDesiredTrans + (1 - gDesiredTrans) * stampData.TransparencyTable[part] end end end) end for index, 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 local configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData) if configFound then stampData.CurrentParts = positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts) end -- to show joints during the mouse move game.JointsService:SetJoinAfterMoveInstance(stampData.CurrentParts) return clone, parts end local function checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp) local cellCenterToWorld = game:GetService("Workspace").Terrain.CellCenterToWorld local cellCenter = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z) local cellBlockingParts = game:GetService("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 local cellPos = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z) if cellPos.X + 2 > allowedStampRegion.CFrame.p.X + allowedStampRegion.Size.X/2 then canSetCell = false elseif cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X/2 then canSetCell = false elseif cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y/2 then canSetCell = false elseif cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y/2 then canSetCell = false elseif cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z/2 then canSetCell = false elseif 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(checkHighScalabilityStamp) local cellSet = false local cluser = game:GetService("Workspace").Terrain local line = HighScalabilityLine.InternalLine local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max local cMin = game:GetService("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 = game:GetService("Workspace").Terrain:WorldToCell(HighScalabilityLine.Start) local xInc = {0,0,0} local yInc = {0,0,0} local zInc = {0,0,0} local cluster = game:GetService("Workspace").Terrain 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+.5), math.floor(lines[i].Y+.5), math.floor(lines[i].Z+.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 < .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(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 game:GetService("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] = stepVect[1] + incrementVect[1] end if incrementVect[2] then while innerStepVectIndex < 4 and worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) == 0 do innerStepVectIndex = innerStepVectIndex + 1 end if innerStepVectIndex < 4 then stepVect[2] = stepVect[2] + worldAxes[innerStepVectIndex] * worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) end innerStepVectIndex = 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 = outerStepVectIndex + 1 end if outerStepVectIndex < 4 then stepVect[3] = stepVect[3] + worldAxes[outerStepVectIndex] * worldAxes[outerStepVectIndex]:Dot(incrementVect[3]) end outerStepVectIndex = 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() game:GetService("ChangeHistoryService"): SetWaypoint("StamperMulti") end) end return cellSet 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 = nil if stampInModel then local canStamp = nil local isHSLPart = isMegaClusterPart() 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 = t.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() return false end -- recheck if we can stamp, as we just moved part canStamp, checkHighScalabilityStamp = t.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() return false end local blockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector), stampData.CurrentParts, 100) for b = 1, #blockingParts do if isBlocker(blockingParts[b]) then flashRedBox() 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() 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") then if game:GetService("Players")["LocalPlayer"] then if game:GetService("Players").LocalPlayer["Character"] then local localChar = game:GetService("Players").LocalPlayer.Character local stampTracker = localChar:FindFirstChild("StampTracker") if stampTracker and not stampTracker.Value then stampTracker.Value = true end end end end -- if we drew a line of mega parts, stamp them out if HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent and isMegaClusterPart() then if ResolveMegaClusterStamp(checkHighScalabilityStamp) or checkHighScalabilityStamp then -- kill the ghost part stampData.CurrentParts.Parent = nil return true end end -- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff] HighScalabilityLine.Start = nil HighScalabilityLine.Adorn.Parent = nil local cluster = game:GetService("Workspace").Terrain -- if target point is in cluster, just use cluster:SetCell if isMegaClusterPart() 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 = game:GetService("Workspace").Terrain.MaxExtents.Max local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min if checkTerrainBlockCollisions(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 game:GetService("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() game:GetService("ChangeHistoryService"):SetWaypoint("StamperSingle") end) return true end else -- you tried to stamp a HSL-single part where one does not belong! flashRedBox() return false end end local function getPlayer() if game:FindFirstChild("Players") then if game:GetService("Players")["LocalPlayer"] then return game:GetService("Players").LocalPlayer end 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) playerIdTag = stampData.CurrentParts:FindFirstChild("PlayerIdTag") playerNameTag = stampData.CurrentParts:FindFirstChild("PlayerNameTag") if playerIdTag ~= nil then tempPlayerValue = getPlayer() if tempPlayerValue ~= nil then playerIdTag.Value = tempPlayerValue.userId end end if playerNameTag ~= nil then if game:FindFirstChild("Players") and game:GetService("Players")["LocalPlayer"] then tempPlayerValue = game:GetService("Players").LocalPlayer if tempPlayerValue ~= nil then playerNameTag.Value = tempPlayerValue.Name end end end -- ...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", stampData.CurrentParts) stringTag.Name = "RobloxModel" if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts) stringTag2.Name = "RobloxStamper" end end else stampData.CurrentParts:BreakJoints() if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts) stringTag2.Name = "RobloxStamper" end end -- make sure all the joints are activated before restoring anchor states if not createJoints then game.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() 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", stampData.CurrentParts) stringTag.Name = "RobloxModel" 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 index,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 index,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() game:GetService("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() game.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 = isPositive * -1 whichSurface = 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() 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 if stampInModel then clone.Parent = stampInModel else clone.Parent = game:GetService("Workspace") end if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector 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 game.JointsService:SetJoinAfterMoveTarget(mouseTarget) else game.JointsService:SetJoinAfterMoveTarget(nil) end game.JointsService:ShowPermissibleJoints() for index, 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 control.LoadNewModel = function(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) end control.ReloadModel = function() -- will automatically set stamper to get a new model of current model and start stamping with new model resetStamperState() end control.Pause = function() -- 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 control.Resume = function() -- 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 control.ResetRotation = function() -- 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 control.Destroy = function() -- 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 game.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 t.Help = function(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 == t.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" end if funcNameOrFunc == "SetupStamperDragger" or funcNameOrFunc == t.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 end return t