· 5 years ago · Jul 01, 2020, 03:24 AM
1local t = {}
2
3-- Do a line/plane intersection. The line starts at the camera. The plane is at y == 0, normal(0, 1, 0)
4--
5-- vectorPos - End point of the line.
6--
7-- Return:
8-- cellPos - The terrain cell intersection point if there is one, vectorPos if there isn't.
9-- hit - Whether there was a plane intersection. Value is true if there was, false if not.
10function PlaneIntersection(vectorPos)
11 local hit = false
12 local currCamera = game:GetService("Workspace").CurrentCamera
13 local startPos = Vector3.new(currCamera.CoordinateFrame.p.X, currCamera.CoordinateFrame.p.Y, currCamera.CoordinateFrame.p.Z)
14 local endPos = Vector3.new(vectorPos.X, vectorPos.Y, vectorPos.Z)
15 local normal = Vector3.new(0, 1, 0)
16 local p3 = Vector3.new(0, 0, 0)
17 local startEndDot = normal:Dot(endPos - startPos)
18 local cellPos = vectorPos
19 if startEndDot ~= 0 then
20 local t = normal:Dot(p3 - startPos) / startEndDot
21 if(t >=0 and t <=1) then
22 local intersection = ((endPos - startPos) * t) + startPos
23 cellPos = game:GetService("Workspace").Terrain:WorldToCell(intersection)
24 hit = true
25 end
26 end
27
28 return cellPos, hit
29end
30
31
32-- Purpose:
33-- Checks for terrain touched by the mouse hit.
34-- Will do a plane intersection if no terrain is touched.
35--
36-- mouse - Mouse to check the .hit for.
37--
38-- Return:
39-- cellPos - Cell position hit. Nil if none.
40function GetTerrainForMouse(mouse)
41 -- There was no target, so all it could be is a plane intersection.
42 -- Check for a plane intersection. If there isn't one then nothing will get hit.
43 local cell = game:GetService("Workspace").Terrain:WorldToCellPreferSolid(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z))
44 local planeLoc = nil
45 local hit = nil
46 -- If nothing was hit, do the plane intersection.
47 if 0 == game:GetService("Workspace").Terrain:GetCell(cell.X, cell.Y, cell.Z).Value then
48 cell = nil
49 planeLoc, hit = PlaneIntersection(Vector3.new(mouse.hit.x, mouse.hit.y, mouse.hit.z))
50 if hit then
51 cell = planeLoc
52 end
53 end
54 return cell
55end
56
57-- setup helper functions
58local insertBoundingBoxOverlapVector = Vector3.new(.3, .3, .3) -- we can still stamp if our character extrudes into the target stamping space by .3 or fewer units
59
60-- rotates a model by yAngle radians about the global y-axis
61local function rotatePartAndChildren(part, rotCF, offsetFromOrigin)
62 -- rotate this thing, if it's a part
63 if part:IsA("BasePart") then
64 part.CFrame = (rotCF * (part.CFrame - offsetFromOrigin)) + offsetFromOrigin
65 end
66
67 -- recursively do the same to all children
68 local partChildren = part:GetChildren()
69 for c = 1, #partChildren do rotatePartAndChildren(partChildren[c], rotCF, offsetFromOrigin) end
70end
71
72local function modelRotate(model, yAngle)
73 local rotCF = CFrame.Angles(0, yAngle, 0)
74 local offsetFromOrigin = model:GetModelCFrame().p
75
76 rotatePartAndChildren(model, rotCF, offsetFromOrigin)
77end
78
79
80local function collectParts(object, baseParts, scripts, decals)
81 if object:IsA("BasePart") then
82 baseParts[#baseParts+1] = object
83 elseif object:IsA("Script") then
84 scripts[#scripts+1] = object
85 elseif object:IsA("Decal") then
86 decals[#decals+1] = object
87 end
88
89 for index,child in pairs(object:GetChildren()) do
90 collectParts(child, baseParts, scripts, decals)
91 end
92end
93
94local function clusterPartsInRegion(startVector, endVector)
95 local cluster = game:GetService("Workspace"):FindFirstChild("Terrain")
96
97 local startCell = cluster:WorldToCell(startVector)
98 local endCell = cluster:WorldToCell(endVector)
99
100 local startX = startCell.X
101 local startY = startCell.Y
102 local startZ = startCell.Z
103
104 local endX = endCell.X
105 local endY = endCell.Y
106 local endZ = endCell.Z
107
108 if startX < cluster.MaxExtents.Min.X then startX = cluster.MaxExtents.Min.X end
109 if startY < cluster.MaxExtents.Min.Y then startY = cluster.MaxExtents.Min.Y end
110 if startZ < cluster.MaxExtents.Min.Z then startZ = cluster.MaxExtents.Min.Z end
111
112 if endX > cluster.MaxExtents.Max.X then endX = cluster.MaxExtents.Max.X end
113 if endY > cluster.MaxExtents.Max.Y then endY = cluster.MaxExtents.Max.Y end
114 if endZ > cluster.MaxExtents.Max.Z then endZ = cluster.MaxExtents.Max.Z end
115
116 for x = startX, endX do
117 for y = startY, endY do
118 for z = startZ, endZ do
119 if (cluster:GetCell(x, y, z).Value) > 0 then return true end
120 end
121 end
122 end
123
124 return false
125end
126
127local function findSeatsInModel(parent, seatTable)
128 if not parent then return end
129
130 if parent.className == "Seat" or parent.className == "VehicleSeat" then
131 table.insert(seatTable, parent)
132 end
133 local myChildren = parent:GetChildren()
134 for j = 1, #myChildren do
135 findSeatsInModel(myChildren[j], seatTable)
136 end
137end
138
139local function setSeatEnabledStatus(model, isEnabled)
140 local seatList = {}
141 findSeatsInModel(model, seatList)
142
143 if isEnabled then
144 -- remove any welds called "SeatWeld" in seats
145 for i = 1, #seatList do
146 local nextSeat = seatList[i]:FindFirstChild("SeatWeld")
147 while nextSeat do nextSeat:Remove() nextSeat = seatList[i]:FindFirstChild("SeatWeld") end
148 end
149 else
150 -- put a weld called "SeatWeld" in every seat
151 -- this tricks it into thinking there's already someone sitting there, and it won't make you sit XD
152 for i = 1, #seatList do
153 local fakeWeld = Instance.new("Weld")
154 fakeWeld.Name = "SeatWeld"
155 fakeWeld.Parent = seatList[i]
156 end
157 end
158end
159
160local function autoAlignToFace(parts)
161 local aatf = parts:FindFirstChild("AutoAlignToFace")
162 if aatf then return aatf.Value else return false end
163end
164
165local function getClosestAlignedWorldDirection(aVector3InWorld)
166 local xDir = Vector3.new(1,0,0)
167 local yDir = Vector3.new(0,1,0)
168 local zDir = Vector3.new(0,0,1)
169 local xDot = aVector3InWorld.x * xDir.x + aVector3InWorld.y * xDir.y + aVector3InWorld.z * xDir.z
170 local yDot = aVector3InWorld.x * yDir.x + aVector3InWorld.y * yDir.y + aVector3InWorld.z * yDir.z
171 local zDot = aVector3InWorld.x * zDir.x + aVector3InWorld.y * zDir.y + aVector3InWorld.z * zDir.z
172
173 if math.abs(xDot) > math.abs(yDot) and math.abs(xDot) > math.abs(zDot) then
174 if xDot > 0 then
175 return 0
176 else
177 return 3
178 end
179 elseif math.abs(yDot) > math.abs(xDot) and math.abs(yDot) > math.abs(zDot) then
180 if yDot > 0 then
181 return 1
182 else
183 return 4
184 end
185 else
186 if zDot > 0 then
187 return 2
188 else
189 return 5
190 end
191 end
192end
193
194local function positionPartsAtCFrame3(aCFrame, currentParts)
195 local insertCFrame = nil
196 if not currentParts then return currentParts end
197 if currentParts and (currentParts:IsA("Model") or currentParts:IsA("Tool")) then
198 insertCFrame = currentParts:GetModelCFrame()
199 currentParts:TranslateBy(aCFrame.p - insertCFrame.p)
200 else
201 currentParts.CFrame = aCFrame
202 end
203 return currentParts
204end
205
206local function calcRayHitTime(rayStart, raySlope, intersectionPlane)
207 if math.abs(raySlope) < .01 then return 0 end -- 0 slope --> we just say intersection time is 0, and sidestep this dimension
208 return (intersectionPlane - rayStart) / raySlope
209end
210
211local function modelTargetSurface(partOrModel, rayStart, rayEnd)
212 if not partOrModel then
213 return 0
214 end
215
216 local modelCFrame = nil
217 local modelSize = nil
218 if partOrModel:IsA("Model") then
219 modelCFrame = partOrModel:GetModelCFrame()
220 modelSize = partOrModel:GetModelSize()
221 else
222 modelCFrame = partOrModel.CFrame
223 modelSize = partOrModel.Size
224 end
225
226 local mouseRayStart = modelCFrame:pointToObjectSpace(rayStart)
227 local mouseRayEnd = modelCFrame:pointToObjectSpace(rayEnd)
228 local mouseSlope = mouseRayEnd - mouseRayStart
229
230 local xPositive = 1
231 local yPositive = 1
232 local zPositive = 1
233 if mouseSlope.X > 0 then xPositive = -1 end
234 if mouseSlope.Y > 0 then yPositive = -1 end
235 if mouseSlope.Z > 0 then zPositive = -1 end
236
237 -- find which surface the transformed mouse ray hits (using modelSize):
238 local xHitTime = calcRayHitTime(mouseRayStart.X, mouseSlope.X, modelSize.X/2 * xPositive)
239 local yHitTime = calcRayHitTime(mouseRayStart.Y, mouseSlope.Y, modelSize.Y/2 * yPositive)
240 local zHitTime = calcRayHitTime(mouseRayStart.Z, mouseSlope.Z, modelSize.Z/2 * zPositive)
241
242 local hitFace = 0
243
244 --if xHitTime >= 0 and yHitTime >= 0 and zHitTime >= 0 then
245 if xHitTime > yHitTime then
246 if xHitTime > zHitTime then
247 -- xFace is hit
248 hitFace = 1*xPositive
249 else
250 -- zFace is hit
251 hitFace = 3*zPositive
252 end
253 else
254 if yHitTime > zHitTime then
255 -- yFace is hit
256 hitFace = 2*yPositive
257 else
258 -- zFace is hit
259 hitFace = 3*zPositive
260 end
261 end
262
263 return hitFace
264end
265
266local function getBoundingBox2(partOrModel)
267
268 -- for models, the bounding box is defined as the minimum and maximum individual part bounding boxes
269 -- relative to the first part's coordinate frame.
270 local minVec = Vector3.new(math.huge, math.huge, math.huge)
271 local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
272
273 if partOrModel:IsA("Terrain") then
274 minVec = Vector3.new(-2, -2, -2)
275 maxVec = Vector3.new(2, 2, 2)
276 elseif partOrModel:IsA("BasePart") then
277 minVec = -0.5 * partOrModel.Size
278 maxVec = -minVec
279 else
280 maxVec = partOrModel:GetModelSize()*0.5
281 minVec = -maxVec
282 end
283
284 -- Adjust bounding box to reflect what the model or part author wants in terms of justification
285 local justifyValue = partOrModel:FindFirstChild("Justification")
286 if justifyValue ~= nil then
287 -- find the multiple of 4 that contains the model
288 local justify = justifyValue.Value
289 local two = Vector3.new(2, 2, 2)
290 local actualBox = maxVec - minVec - Vector3.new(0.01, 0.01, 0.01)
291 local containingGridBox = Vector3.new(4 * math.ceil(actualBox.x/4), 4 * math.ceil(actualBox.y/4), 4 * math.ceil(actualBox.z/4))
292 local adjustment = containingGridBox - actualBox
293 minVec = minVec - 0.5 * adjustment * justify
294 maxVec = maxVec + 0.5 * adjustment * (two - justify)
295 end
296
297 return minVec, maxVec
298end
299
300local function getBoundingBoxInWorldCoordinates(partOrModel)
301 local minVec = Vector3.new(math.huge, math.huge, math.huge)
302 local maxVec = Vector3.new(-math.huge, -math.huge, -math.huge)
303
304 if partOrModel:IsA("BasePart") and not partOrModel:IsA("Terrain") then
305 local vec1 = partOrModel.CFrame:pointToWorldSpace(-0.5 * partOrModel.Size)
306 local vec2 = partOrModel.CFrame:pointToWorldSpace(0.5 * partOrModel.Size)
307 minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z))
308 maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z))
309 elseif partOrModel:IsA("Terrain") then
310 -- we shouldn't have to deal with this case
311 --minVec = Vector3.new(-2, -2, -2)
312 --maxVec = Vector3.new(2, 2, 2)
313 else
314 local vec1 = partOrModel:GetModelCFrame():pointToWorldSpace(-0.5 * partOrModel:GetModelSize())
315 local vec2 = partOrModel:GetModelCFrame():pointToWorldSpace(0.5 * partOrModel:GetModelSize())
316 minVec = Vector3.new(math.min(vec1.X, vec2.X), math.min(vec1.Y, vec2.Y), math.min(vec1.Z, vec2.Z))
317 maxVec = Vector3.new(math.max(vec1.X, vec2.X), math.max(vec1.Y, vec2.Y), math.max(vec1.Z, vec2.Z))
318 end
319
320 return minVec, maxVec
321end
322
323local function getTargetPartBoundingBox(targetPart)
324 if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then
325 return getBoundingBox2(targetPart.Parent)
326 else
327 return getBoundingBox2(targetPart)
328 end
329end
330
331local function getMouseTargetCFrame(targetPart)
332 if targetPart.Parent:FindFirstChild("RobloxModel") ~= nil then
333 if targetPart.Parent:IsA("Tool") then return targetPart.Parent.Handle.CFrame
334 else return targetPart.Parent:GetModelCFrame() end
335 else
336 return targetPart.CFrame
337 end
338end
339
340local function isBlocker(part) -- returns whether or not we want to cancel the stamp because we're blocked by this part
341 if not part then return false end
342 if not part.Parent then return false end
343 if part:FindFirstChild("Humanoid") then return false end
344 if part:FindFirstChild("RobloxStamper") or part:FindFirstChild("RobloxModel") then return true end
345 if part:IsA("Part") and not part.CanCollide then return false end
346 if part == game:GetService("Lighting") then return false end
347 return isBlocker(part.Parent)
348end
349
350-- helper function to determine if a character can be pushed upwards by a certain amount
351-- 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
352local function spaceAboveCharacter(charTorso, newTorsoY, stampData)
353 local partsAboveChar = game:GetService("Workspace"):FindPartsInRegion3(
354 Region3.new(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75),
355 Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)),
356 charTorso.Parent,
357 100)
358
359 for j = 1, #partsAboveChar do
360 if partsAboveChar[j].CanCollide and not partsAboveChar[j]:IsDescendantOf(stampData.CurrentParts) then return false end
361 end
362
363 if clusterPartsInRegion(Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) - Vector3.new(.75, 2.75, .75),
364 Vector3.new(charTorso.Position.X, newTorsoY, charTorso.Position.Z) + Vector3.new(.75, 1.75, .75)) then
365 return false
366 end
367
368 return true
369end
370
371
372local function findConfigAtMouseTarget(Mouse, stampData)
373 -- *Critical Assumption* :
374 -- This function assumes the target CF axes are orthogonal with the target bounding box faces
375 -- And, it assumes the insert CF axes are orthongonal with the insert bounding box faces
376 -- Therefore, insertion will not work with angled faces on wedges or other "non-block" parts, nor
377 -- will it work for parts in a model that are not orthogonally aligned with the model's CF.
378
379 if not Mouse then return nil end -- This can happen sometimes, return if so
380 if not stampData then error("findConfigAtMouseTarget: stampData is nil") return nil end
381 if not stampData["CurrentParts"] then return nil end
382
383 local grid = 4.0
384 local admissibleConfig = false
385 local targetConfig = CFrame.new(0,0,0)
386
387 local minBB, maxBB = getBoundingBox2(stampData.CurrentParts)
388 local diagBB = maxBB - minBB
389
390 local insertCFrame
391 if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
392 insertCFrame = stampData.CurrentParts:GetModelCFrame()
393 else
394 insertCFrame = stampData.CurrentParts.CFrame
395 end
396
397 if Mouse then
398 if stampData.CurrentParts:IsA("Tool") then
399 Mouse.TargetFilter = stampData.CurrentParts.Handle
400 else
401 Mouse.TargetFilter = stampData.CurrentParts
402 end
403 end
404
405 local hitPlane = false
406 local targetPart = nil
407 local success = pcall(function() targetPart = Mouse.Target end)
408
409 if not success then-- or targetPart == nil then
410 return admissibleConfig, targetConfig
411 end
412
413 local mouseHitInWorld = Vector3.new(0, 0, 0)
414 if Mouse then
415 mouseHitInWorld = Vector3.new(Mouse.Hit.x, Mouse.Hit.y, Mouse.Hit.z)
416 end
417
418 local cellPos = nil
419
420 -- Nothing was hit, so check for the default plane.
421 if nil == targetPart then
422 cellPos = GetTerrainForMouse(Mouse)
423 if nil == cellPos then
424 hitPlane = false
425 return admissibleConfig, targetConfig
426 else
427 targetPart = game:GetService("Workspace").Terrain
428 hitPlane = true
429 -- Take into account error that will occur.
430 cellPos = Vector3.new(cellPos.X - 1, cellPos.Y, cellPos.Z)
431 mouseHitInWorld = game:GetService("Workspace").Terrain:CellCenterToWorld(cellPos.x, cellPos.y, cellPos.z)
432 end
433 end
434
435 -- test mouse hit location
436 local minBBTarget, maxBBTarget = getTargetPartBoundingBox(targetPart)
437 local diagBBTarget = maxBBTarget - minBBTarget
438 local targetCFrame = getMouseTargetCFrame(targetPart)
439
440 if targetPart:IsA("Terrain") then
441 local cluster = game:GetService("Workspace"):FindFirstChild("Terrain")
442 local cellID = cluster:WorldToCellPreferSolid(mouseHitInWorld)
443 if hitPlane then
444 cellID = cellPos
445 end
446
447 targetCFrame = CFrame.new(game:GetService("Workspace").Terrain:CellCenterToWorld(cellID.x, cellID.y, cellID.z))
448 end
449
450 local mouseHitInTarget = targetCFrame:pointToObjectSpace(mouseHitInWorld)
451 local targetVectorInWorld = Vector3.new(0,0,0)
452 if Mouse then
453 -- DON'T WANT THIS IN TERMS OF THE MODEL CFRAME! (.TargetSurface is in terms of the part CFrame, so this would break, right? [HotThoth])
454 -- (ideally, we would want to make the Mouse.TargetSurface a model-targetsurface instead, but for testing will be using the converse)
455 --targetVectorInWorld = targetCFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface))
456 targetVectorInWorld = targetPart.CFrame:vectorToWorldSpace(Vector3.FromNormalId(Mouse.TargetSurface)) -- better, but model cframe would be best
457 --[[if targetPart.Parent:IsA("Model") then
458 local hitFace = modelTargetSurface(targetPart.Parent, Mouse.Hit.p, game.Workspace.CurrentCamera.CoordinateFrame.p) -- best, if you get it right
459 local WORLD_AXES = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)}
460 if hitFace > 0 then
461 targetVectorInWorld = targetCFrame:vectorToWorldSpace(WORLD_AXES[hitFace])
462 elseif hitFace < 0 then
463 targetVectorInWorld = targetCFrame:vectorToWorldSpace(-WORLD_AXES[-hitFace])
464 end
465 end]]
466 end
467
468 local targetRefPointInTarget
469 local clampToSurface
470 local insertRefPointInInsert
471
472 if getClosestAlignedWorldDirection(targetVectorInWorld) == 0 then
473 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
474 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
475 clampToSurface = Vector3.new(0,1,1)
476 elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 3 then
477 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
478 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
479 clampToSurface = Vector3.new(0,1,1)
480 elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 1 then
481 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
482 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
483 clampToSurface = Vector3.new(1,0,1)
484 elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 4 then
485 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
486 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, 1, 1))
487 clampToSurface = Vector3.new(1,0,1)
488 elseif getClosestAlignedWorldDirection(targetVectorInWorld) == 2 then
489 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(-1, -1, 1))
490 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(-1, -1, -1))
491 clampToSurface = Vector3.new(1,1,0)
492 else
493 targetRefPointInTarget = targetCFrame:vectorToObjectSpace(Vector3.new(1, -1, -1))
494 insertRefPointInInsert = insertCFrame:vectorToObjectSpace(Vector3.new(1, -1, 1))
495 clampToSurface = Vector3.new(1,1,0)
496 end
497
498 targetRefPointInTarget = targetRefPointInTarget * (0.5 * diagBBTarget) + 0.5 * (maxBBTarget + minBBTarget)
499 insertRefPointInInsert = insertRefPointInInsert * (0.5 * diagBB) + 0.5 * (maxBB + minBB)
500
501 -- To Do: For cases that are not aligned to the world grid, account for the minimal rotation
502 -- needed to bring the Insert part(s) into alignment with the Target Part
503 -- Apply the rotation here
504
505 local delta = mouseHitInTarget - targetRefPointInTarget
506 local deltaClamped = Vector3.new(grid * math.modf(delta.x/grid), grid * math.modf(delta.y/grid), grid * math.modf(delta.z/grid))
507 deltaClamped = deltaClamped * clampToSurface
508 local targetTouchInTarget = deltaClamped + targetRefPointInTarget
509
510 local TargetTouchRelToWorld = targetCFrame:pointToWorldSpace(targetTouchInTarget)
511 local InsertTouchInWorld = insertCFrame:vectorToWorldSpace(insertRefPointInInsert)
512 local posInsertOriginInWorld = TargetTouchRelToWorld - InsertTouchInWorld
513
514 local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = insertCFrame:components()
515 targetConfig = CFrame.new(posInsertOriginInWorld.x, posInsertOriginInWorld.y, posInsertOriginInWorld.z, R00, R01, R02, R10, R11, R12, R20, R21, R22)
516 admissibleConfig = true
517
518 return admissibleConfig, targetConfig, getClosestAlignedWorldDirection(targetVectorInWorld)
519end
520
521local function truncateToCircleEighth(bigValue, littleValue)
522 local big = math.abs(bigValue)
523 local little = math.abs(littleValue)
524 local hypotenuse = math.sqrt(big*big + little*little)
525 local frac = little / hypotenuse
526
527 local bigSign = 1
528 local littleSign = 1
529 if bigValue < 0 then bigSign = -1 end
530 if littleValue < 0 then littleSign = -1 end
531
532 if frac > .382683432 then
533 -- between 22.5 and 45 degrees, so truncate to 45-degree tilt
534 return .707106781 * hypotenuse * bigSign, .707106781 * hypotenuse * littleSign
535 else
536 -- between 0 and 22.5 degrees, so truncate to 0-degree tilt
537 return hypotenuse * bigSign, 0
538 end
539end
540
541
542local function saveTheWelds(object, manualWeldTable, manualWeldParentTable)
543 if object:IsA("ManualWeld") or object:IsA("Rotate") then
544 table.insert(manualWeldTable, object)
545 table.insert(manualWeldParentTable, object.Parent)
546 else
547 local children = object:GetChildren()
548 for i = 1, #children do
549 saveTheWelds(children[i], manualWeldTable, manualWeldParentTable)
550 end
551 end
552end
553
554local function restoreTheWelds(manualWeldTable, manualWeldParentTable)
555 for i = 1, #manualWeldTable do
556 manualWeldTable[i].Parent = manualWeldParentTable[i]
557 end
558end
559
560t.CanEditRegion = function(partOrModel, EditRegion) -- todo: use model and stamper metadata
561 if not EditRegion then return true, false end
562
563 local minBB, maxBB = getBoundingBoxInWorldCoordinates(partOrModel)
564
565 if minBB.X < EditRegion.CFrame.p.X - EditRegion.Size.X/2 or
566 minBB.Y < EditRegion.CFrame.p.Y - EditRegion.Size.Y/2 or
567 minBB.Z < EditRegion.CFrame.p.Z - EditRegion.Size.Z/2 then
568 return false, false
569 end
570
571 if maxBB.X > EditRegion.CFrame.p.X + EditRegion.Size.X/2 or
572 maxBB.Y > EditRegion.CFrame.p.Y + EditRegion.Size.Y/2 or
573 maxBB.Z > EditRegion.CFrame.p.Z + EditRegion.Size.Z/2 then
574 return false, false
575 end
576
577 return true, false
578end
579
580t.GetStampModel = function(assetId, terrainShape, useAssetVersionId)
581 if assetId == 0 then
582 return nil, "No Asset"
583 end
584 if assetId < 0 then
585 return nil, "Negative Asset"
586 end
587
588 local function UnlockInstances(object)
589 if object:IsA("BasePart") then
590 object.Locked = false
591 end
592 for index,child in pairs(object:GetChildren()) do
593 UnlockInstances(child)
594 end
595 end
596
597 local function getClosestColorToTerrainMaterial(terrainValue)
598 if terrainValue == 1 then
599 return BrickColor.new("Bright green")
600 elseif terrainValue == 2 then
601 return BrickColor.new("Bright yellow")
602 elseif terrainValue == 3 then
603 return BrickColor.new("Bright red")
604 elseif terrainValue == 4 then
605 return BrickColor.new("Sand red")
606 elseif terrainValue == 5 then
607 return BrickColor.new("Black")
608 elseif terrainValue == 6 then
609 return BrickColor.new("Dark stone grey")
610 elseif terrainValue == 7 then
611 return BrickColor.new("Sand blue")
612 elseif terrainValue == 8 then
613 return BrickColor.new("Deep orange")
614 elseif terrainValue == 9 then
615 return BrickColor.new("Dark orange")
616 elseif terrainValue == 10 then
617 return BrickColor.new("Reddish brown")
618 elseif terrainValue == 11 then
619 return BrickColor.new("Light orange")
620 elseif terrainValue == 12 then
621 return BrickColor.new("Light stone grey")
622 elseif terrainValue == 13 then
623 return BrickColor.new("Sand green")
624 elseif terrainValue == 14 then
625 return BrickColor.new("Medium stone grey")
626 elseif terrainValue == 15 then
627 return BrickColor.new("Really red")
628 elseif terrainValue == 16 then
629 return BrickColor.new("Really blue")
630 elseif terrainValue == 17 then
631 return BrickColor.new("Bright blue")
632 else
633 return BrickColor.new("Bright green")
634 end
635 end
636
637 local function setupFakeTerrainPart(cellMat, cellType, cellOrient)
638 local newTerrainPiece = nil
639 if (cellType == 1 or cellType == 4) then newTerrainPiece = Instance.new("WedgePart")
640 elseif (cellType == 2) then newTerrainPiece = Instance.new("CornerWedgePart")
641 else newTerrainPiece = Instance.new("Part") end
642 newTerrainPiece.Name = "MegaClusterCube"
643 newTerrainPiece.Size = Vector3.new(4, 4, 4)
644 newTerrainPiece.BottomSurface = "Smooth"
645 newTerrainPiece.TopSurface = "Smooth"
646
647 -- can add decals or textures here if feeling particularly adventurous... for now, can make a table of look-up colors
648 newTerrainPiece.BrickColor = getClosestColorToTerrainMaterial(cellMat)
649
650 local sideways = 0
651 local flipped = math.pi
652 if cellType == 4 then sideways = -math.pi/2 end
653 if cellType == 2 or cellType == 3 then flipped = 0 end
654 newTerrainPiece.CFrame = CFrame.Angles(0, math.pi/2*cellOrient + flipped, sideways)
655
656 if cellType == 3 then
657 local inverseCornerWedgeMesh = Instance.new("SpecialMesh")
658 inverseCornerWedgeMesh.MeshType = "FileMesh"
659 inverseCornerWedgeMesh.MeshId = "https://www.roblox.com/asset/?id=66832495"
660 inverseCornerWedgeMesh.Scale = Vector3.new(2, 2, 2)
661 inverseCornerWedgeMesh.Parent = newTerrainPiece
662 end
663
664 local materialTag = Instance.new("Vector3Value")
665 materialTag.Value = Vector3.new(cellMat, cellType, cellOrient)
666 materialTag.Name = "ClusterMaterial"
667 materialTag.Parent = newTerrainPiece
668
669 return newTerrainPiece
670 end
671
672 -- This call will cause a "wait" until the data comes back
673 -- below we wait a max of 8 seconds before deciding to bail out on loading
674 local root
675 local loader
676 loading = true
677 if useAssetVersionId then
678 loader = coroutine.create(function()
679 root = game:GetService("InsertService"):LoadAssetVersion(assetId)
680 loading = false
681 end)
682 coroutine.resume(loader)
683 else
684 loader = coroutine.create(function()
685 root = game:GetService("InsertService"):LoadAsset(assetId)
686 loading = false
687 end)
688 coroutine.resume(loader)
689 end
690
691 local lastGameTime = 0
692 local totalTime = 0
693 local maxWait = 8
694 while loading and totalTime < maxWait do
695 lastGameTime = tick()
696 wait(1)
697 totalTime = totalTime + tick() - lastGameTime
698 end
699 loading = false
700
701 if totalTime >= maxWait then
702 return nil, "Load Time Fail"
703 end
704
705
706 if root == nil then
707 return nil, "Load Asset Fail"
708 end
709
710 if not root:IsA("Model") then
711 return nil, "Load Type Fail"
712 end
713
714 local instances = root:GetChildren()
715 if #instances == 0 then
716 return nil, "Empty Model Fail"
717 end
718
719 --Unlock all parts that are inserted, to make sure they are editable
720 UnlockInstances(root)
721
722 --Continue the insert process
723 root = root:GetChildren()[1]
724
725 --Examine the contents and decide what it looks like
726 for pos, instance in pairs(instances) do
727 if instance:IsA("Team") then
728 instance.Parent = game:GetService("Teams")
729 elseif instance:IsA("Sky") then
730 local lightingService = game:GetService("Lighting")
731 for index,child in pairs(lightingService:GetChildren()) do
732 if child:IsA("Sky") then
733 child:Remove();
734 end
735 end
736 instance.Parent = lightingService
737 return
738 end
739 end
740
741 -- ...and tag all inserted models for subsequent origin identification
742 -- if no RobloxModel tag already exists, then add it.
743 if root:FindFirstChild("RobloxModel") == nil then
744 local stringTag = Instance.new("BoolValue", root)
745 stringTag.Name = "RobloxModel"
746
747 if root:FindFirstChild("RobloxStamper") == nil then
748 local stringTag2 = Instance.new("BoolValue", root)
749 stringTag2.Name = "RobloxStamper"
750 end
751 end
752
753 if terrainShape then
754 if root.Name == "MegaClusterCube" then
755 if (terrainShape == 6) then -- insert an autowedging tag
756 local autowedgeTag = Instance.new("BoolValue")
757 autowedgeTag.Name = "AutoWedge"
758 autowedgeTag.Parent = root
759 else
760 local clusterTag = root:FindFirstChild("ClusterMaterial")
761 if clusterTag then
762 if clusterTag:IsA("Vector3Value") then
763 root = setupFakeTerrainPart(clusterTag.Value.X, terrainShape, clusterTag.Value.Z)
764 else
765 root = setupFakeTerrainPart(clusterTag.Value, terrainShape, 0)
766 end
767 else
768 root = setupFakeTerrainPart(1, terrainShape, 0)
769 end
770 end
771 end
772 end
773
774 return root
775end
776
777
778
779t.SetupStamperDragger = function(modelToStamp, Mouse, StampInModel, AllowedStampRegion, StampFailedFunc)
780 if not modelToStamp then
781 error("SetupStamperDragger: modelToStamp (first arg) is nil! Should be a stamper model")
782 return nil
783 end
784 if not modelToStamp:IsA("Model") and not modelToStamp:IsA("BasePart") then
785 error("SetupStamperDragger: modelToStamp (first arg) is neither a Model or Part!")
786 return nil
787 end
788 if not Mouse then
789 error("SetupStamperDragger: Mouse (second arg) is nil! Should be a mouse object")
790 return nil
791 end
792 if not Mouse:IsA("Mouse") then
793 error("SetupStamperDragger: Mouse (second arg) is not of type Mouse!")
794 return nil
795 end
796
797 local stampInModel = nil
798 local allowedStampRegion = nil
799 local stampFailedFunc = nil
800 if StampInModel then
801 if not StampInModel:IsA("Model") then
802 error("SetupStamperDragger: StampInModel (optional third arg) is not of type 'Model'")
803 return nil
804 end
805 if not AllowedStampRegion then
806 error("SetupStamperDragger: AllowedStampRegion (optional fourth arg) is nil when StampInModel (optional third arg) is defined")
807 return nil
808 end
809 stampFailedFunc = StampFailedFunc
810 stampInModel = StampInModel
811 allowedStampRegion = AllowedStampRegion
812 end
813
814 -- Init all state variables
815 local gInitial90DegreeRotations = 0
816 local stampData = nil
817 local mouseTarget = nil
818
819 local errorBox = Instance.new("SelectionBox")
820 errorBox.Color = BrickColor.new("Bright red")
821 errorBox.Transparency = 0
822 errorBox.Archivable = false
823
824 -- for megacluster MEGA STAMPING
825 local adornPart = Instance.new("Part")
826 adornPart.Parent = nil
827 adornPart.Size = Vector3.new(4, 4, 4)
828 adornPart.CFrame = CFrame.new()
829 adornPart.Archivable = false
830
831 local adorn = Instance.new("SelectionBox")
832 adorn.Color = BrickColor.new("Toothpaste")
833 adorn.Adornee = adornPart
834 adorn.Visible = true
835 adorn.Transparency = 0
836 adorn.Name = "HighScalabilityStamperLine"
837 adorn.Archivable = false
838
839 local HighScalabilityLine = {}
840 HighScalabilityLine.Start = nil
841 HighScalabilityLine.End = nil
842 HighScalabilityLine.Adorn = adorn
843 HighScalabilityLine.AdornPart = adornPart
844 HighScalabilityLine.InternalLine = nil
845 HighScalabilityLine.NewHint = true
846
847 HighScalabilityLine.MorePoints = {nil, nil}
848 HighScalabilityLine.MoreLines = {nil, nil}
849 HighScalabilityLine.Dimensions = 1
850
851 local control = {}
852 local movingLock = false
853 local stampUpLock = false
854 local unstampableSurface = false
855 local mouseCons = {}
856 local keyCon = nil
857
858 local stamped = Instance.new("BoolValue")
859 stamped.Archivable = false
860 stamped.Value = false
861
862 local lastTarget = {}
863 lastTarget.TerrainOrientation = 0
864 lastTarget.CFrame = 0
865
866 local cellInfo = {}
867 cellInfo.Material = 1
868 cellInfo.clusterType = 0
869 cellInfo.clusterOrientation = 0
870
871 local function isMegaClusterPart()
872 if not stampData then return false end
873 if not stampData.CurrentParts then return false end
874
875 return ( stampData.CurrentParts:FindFirstChild("ClusterMaterial",true) or (stampData.CurrentParts.Name == "MegaClusterCube") )
876 end
877
878 local function DoHighScalabilityRegionSelect()
879 local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube")
880 if not megaCube then
881 if not stampData.CurrentParts.Name == "MegaClusterCube" then
882 return
883 else
884 megaCube = stampData.CurrentParts
885 end
886 end
887
888 HighScalabilityLine.End = megaCube.CFrame.p
889 local line = nil
890 local line2 = Vector3.new(0, 0, 0)
891 local line3 = Vector3.new(0, 0, 0)
892
893 if HighScalabilityLine.Dimensions == 1 then
894 -- extract the line from these positions and limit to a 2D plane made from 2 of the world axes
895 -- then use dominating axis to limit line to be at 45-degree intervals
896 -- will use this internal representation of the line for the actual stamping
897 line = (HighScalabilityLine.End - HighScalabilityLine.Start)
898
899 if math.abs(line.X) < math.abs(line.Y) then
900 if math.abs(line.X) < math.abs(line.Z) then
901 -- limit to Y/Z plane, domination unknown
902 local newY, newZ
903 if (math.abs(line.Y) > math.abs(line.Z)) then
904 newY, newZ = truncateToCircleEighth(line.Y, line.Z)
905 else
906 newZ, newY = truncateToCircleEighth(line.Z, line.Y)
907 end
908 line = Vector3.new(0, newY, newZ)
909 else
910 -- limit to X/Y plane, with Y dominating
911 local newY, newX = truncateToCircleEighth(line.Y, line.X)
912 line = Vector3.new(newX, newY, 0)
913 end
914 else
915 if math.abs(line.Y) < math.abs(line.Z) then
916 -- limit to X/Z plane, domination unknown
917 local newX, newZ
918 if math.abs(line.X) > math.abs(line.Z) then
919 newX, newZ = truncateToCircleEighth(line.X, line.Z)
920 else
921 newZ, newX = truncateToCircleEighth(line.Z, line.X)
922 end
923 line = Vector3.new(newX, 0, newZ)
924 else
925 -- limit to X/Y plane, with X dominating
926 local newX, newY = truncateToCircleEighth(line.X, line.Y)
927 line = Vector3.new(newX, newY, 0)
928 end
929 end
930 HighScalabilityLine.InternalLine = line
931
932 elseif HighScalabilityLine.Dimensions == 2 then
933 line = HighScalabilityLine.MoreLines[1]
934 line2 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[1]
935
936 -- take out any component of line2 along line1, so you get perpendicular to line1 component
937 line2 = line2 - line.unit*line.unit:Dot(line2)
938
939 local tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line)
940
941 -- then zero out whichever is the smaller component
942 local yAxis = tempCFrame:vectorToWorldSpace(Vector3.new(0, 1, 0))
943 local xAxis = tempCFrame:vectorToWorldSpace(Vector3.new(1, 0, 0))
944
945 local xComp = xAxis:Dot(line2)
946 local yComp = yAxis:Dot(line2)
947
948 if math.abs(yComp) > math.abs(xComp) then
949 line2 = line2 - xAxis * xComp
950 else
951 line2 = line2 - yAxis * yComp
952 end
953
954 HighScalabilityLine.InternalLine = line2
955
956 elseif HighScalabilityLine.Dimensions == 3 then
957 line = HighScalabilityLine.MoreLines[1]
958 line2 = HighScalabilityLine.MoreLines[2]
959 line3 = HighScalabilityLine.End - HighScalabilityLine.MorePoints[2]
960
961 -- zero out all components of previous lines
962 line3 = line3 - line.unit * line.unit:Dot(line3)
963 line3 = line3 - line2.unit * line2.unit:Dot(line3)
964
965 HighScalabilityLine.InternalLine = line3
966 end
967
968 -- resize the "line" graphic to be the correct size and orientation
969 local tempCFrame = CFrame.new(HighScalabilityLine.Start, HighScalabilityLine.Start + line)
970
971 if HighScalabilityLine.Dimensions == 1 then -- faster calculation for line
972 HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, line.magnitude + 4)
973 HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(Vector3.new(2, 2, 2) - HighScalabilityLine.AdornPart.Size/2)
974 else
975 local boxSize = tempCFrame:vectorToObjectSpace(line + line2 + line3)
976 HighScalabilityLine.AdornPart.Size = Vector3.new(4, 4, 4) + Vector3.new(math.abs(boxSize.X), math.abs(boxSize.Y), math.abs(boxSize.Z))
977 HighScalabilityLine.AdornPart.CFrame = tempCFrame + tempCFrame:vectorToWorldSpace(boxSize/2)
978 end
979
980 -- make player able to see this ish
981
982 local gui = nil
983 if game:GetService("Players")["LocalPlayer"] then
984 gui = game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui")
985 if gui and gui:IsA("PlayerGui") then
986 if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough
987 HighScalabilityLine.Adorn.Parent = gui
988 elseif HighScalabilityLine.Dimensions > 1 then
989 HighScalabilityLine.Adorn.Parent = gui
990 end
991 end
992 end
993
994 if gui == nil then -- we are in studio
995 gui = game:GetService("CoreGui")
996 if HighScalabilityLine.Dimensions == 1 and line.magnitude > 3 then -- don't show if mouse hasn't moved enough
997 HighScalabilityLine.Adorn.Parent = gui
998 elseif HighScalabilityLine.Dimensions > 1 then
999 HighScalabilityLine.Adorn.Parent = gui
1000 end
1001 end
1002 end
1003
1004
1005 local function DoStamperMouseMove(Mouse)
1006 if not Mouse then
1007 error("Error: RbxStamper.DoStamperMouseMove: Mouse is nil")
1008 return
1009 end
1010 if not Mouse:IsA("Mouse") then
1011 error("Error: RbxStamper.DoStamperMouseMove: Mouse is of type", Mouse.className,"should be of type Mouse")
1012 return
1013 end
1014
1015 -- There wasn't a target (no part or terrain), so check for plane intersection.
1016 if not Mouse.Target then
1017 local cellPos = GetTerrainForMouse(Mouse)
1018 if nil == cellPos then
1019 return
1020 end
1021 end
1022
1023 if not stampData then
1024 return
1025 end
1026
1027 -- don't move with dragger - will move in one step on mouse down
1028 -- draw ghost at acceptable positions
1029 local configFound, targetCFrame, targetSurface = findConfigAtMouseTarget(Mouse, stampData)
1030 if not configFound then
1031 error("RbxStamper.DoStamperMouseMove No configFound, returning")
1032 return
1033 end
1034
1035 local numRotations = 0 -- update this according to how many rotations you need to get it to target surface
1036 if autoAlignToFace(stampData.CurrentParts) and targetSurface ~= 1 and targetSurface ~= 4 then -- pre-rotate the flag or portrait so it's aligned correctly
1037 if targetSurface == 3 then numRotations = 0 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
1038 elseif targetSurface == 0 then numRotations = 2 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
1039 elseif targetSurface == 5 then numRotations = 3 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
1040 elseif targetSurface == 2 then numRotations = 1 - gInitial90DegreeRotations + autoAlignToFace(stampData.CurrentParts)
1041 end
1042 end
1043
1044 local ry = math.pi/2
1045 gInitial90DegreeRotations = gInitial90DegreeRotations + numRotations
1046 if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
1047 --stampData.CurrentParts:Rotate(0, ry*numRotations, 0)
1048 modelRotate(stampData.CurrentParts, ry*numRotations)
1049 else
1050 stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry*numRotations, 0) * stampData.CurrentParts.CFrame
1051 end
1052
1053 -- CODE TO CHECK FOR DRAGGING GHOST PART INTO A COLLIDING STATE
1054 local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
1055
1056 -- need to offset by distance to be dragged
1057 local currModelCFrame = nil
1058 if stampData.CurrentParts:IsA("Model") then
1059 currModelCFrame = stampData.CurrentParts:GetModelCFrame()
1060 else
1061 currModelCFrame = stampData.CurrentParts.CFrame
1062 end
1063
1064 minBB = minBB + targetCFrame.p - currModelCFrame.p
1065 maxBB = maxBB + targetCFrame.p - currModelCFrame.p
1066
1067 -- don't drag into terrain
1068 if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then
1069 if lastTarget.CFrame then
1070 if (stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)) then
1071 local theClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1072 if theClusterMaterial:IsA("Vector3Value") then
1073 local stampClusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1074 if stampClusterMaterial then
1075 stampClusterMaterial = theClusterMaterial
1076 end
1077 end
1078 end
1079 end
1080 return
1081 end
1082
1083 -- 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
1084 if isMegaClusterPart() then
1085 local cellToStamp = game:GetService("Workspace").Terrain:WorldToCell(targetCFrame.p)
1086 local newCFramePosition = game:GetService("Workspace").Terrain:CellCenterToWorld(cellToStamp.X, cellToStamp.Y, cellToStamp.Z)
1087 local x, y, z, R00, R01, R02, R10, R11, R12, R20, R21, R22 = targetCFrame:components()
1088 targetCFrame = CFrame.new(newCFramePosition.X,newCFramePosition.Y,newCFramePosition.Z,R00, R01, R02, R10, R11, R12, R20, R21, R22)
1089 end
1090
1091 positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
1092 lastTarget.CFrame = targetCFrame -- successful positioning, so update 'dat cframe
1093 if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
1094 local clusterMat = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1095 if clusterMat:IsA("Vector3Value") then
1096 lastTarget.TerrainOrientation = clusterMat.Value.Z
1097 end
1098 end
1099
1100
1101 -- auto break joints code
1102 if Mouse and Mouse.Target and Mouse.Target.Parent then
1103 local modelInfo = Mouse.Target:FindFirstChild("RobloxModel")
1104 if not modelInfo then modelInfo = Mouse.Target.Parent:FindFirstChild("RobloxModel") end
1105
1106 local myModelInfo = stampData.CurrentParts:FindFirstChild("UnstampableFaces")
1107
1108 --if (modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces")) or (modelInfo and myModelInfo) then -- need better targetSurface calcs
1109 if (true) then
1110 local breakingFaces = ""
1111 local myBreakingFaces = ""
1112 if modelInfo and modelInfo.Parent:FindFirstChild("UnstampableFaces") then breakingFaces = modelInfo.Parent.UnstampableFaces.Value end
1113 if myModelInfo then myBreakingFaces = myModelInfo.Value end
1114 local hitFace = 0
1115
1116 if modelInfo then hitFace = modelTargetSurface(modelInfo.Parent, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p, Mouse.Hit.p) end
1117
1118 -- are we stamping TO an unstampable surface?
1119 for bf in string.gmatch(breakingFaces, "[^,]+") do
1120 if hitFace == tonumber(bf) then
1121 -- return before we hit the JointsService code below!
1122 unstampableSurface = true
1123 game:GetService("JointsService"):ClearJoinAfterMoveJoints() -- clear the JointsService cache
1124 return
1125 end
1126 end
1127
1128 -- now we have to cast the ray back in the other direction to find the surface we're stamping FROM
1129 hitFace = modelTargetSurface(stampData.CurrentParts, Mouse.Hit.p, game:GetService("Workspace").CurrentCamera.CoordinateFrame.p)
1130
1131 -- are we stamping WITH an unstampable surface?
1132 for bf in string.gmatch(myBreakingFaces, "[^,]+") do
1133 if hitFace == tonumber(bf) then
1134 unstampableSurface = true
1135 game:GetService("JointsService"):ClearJoinAfterMoveJoints() -- clear the JointsService cache
1136 return
1137 end
1138 end
1139
1140 -- just need to match breakingFace against targetSurface using rotation supplied by modelCFrame
1141 -- targetSurface: 1 is top, 4 is bottom,
1142 end
1143 end
1144
1145 -- to show joints during the mouse move
1146 unstampableSurface = false
1147 game:GetService("JointsService"):SetJoinAfterMoveInstance(stampData.CurrentParts)
1148
1149 -- most common mouse inactive error occurs here, so check mouse active one more time in a pcall
1150 if not pcall(function()
1151 if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then
1152 return
1153 else
1154 return
1155 end
1156 end)
1157 then
1158 error("Error: RbxStamper.DoStamperMouseMove Mouse is nil on second check")
1159 game:GetService("JointsService"):ClearJoinAfterMoveJoints()
1160 Mouse = nil
1161 return
1162 end
1163
1164 if Mouse and Mouse.Target and Mouse.Target.Parent:FindFirstChild("RobloxModel") == nil then
1165 game:GetService("JointsService"):SetJoinAfterMoveTarget(Mouse.Target)
1166 else
1167 game:GetService("JointsService"):SetJoinAfterMoveTarget(nil)
1168 end
1169 game:GetService("JointsService"):ShowPermissibleJoints()
1170
1171 -- here we allow for a line of high-scalability parts
1172 if isMegaClusterPart() and HighScalabilityLine and HighScalabilityLine.Start then
1173 DoHighScalabilityRegionSelect()
1174 end
1175 end
1176
1177 local function setupKeyListener(key, Mouse)
1178 if control and control["Paused"] then return end -- don't do this if we have no stamp
1179
1180 key = string.lower(key)
1181 if key == 'r' and not autoAlignToFace(stampData.CurrentParts) then -- rotate the model
1182 gInitial90DegreeRotations = gInitial90DegreeRotations + 1
1183
1184 -- Update orientation value if this is a fake terrain part
1185 local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1186 if clusterValues and clusterValues:IsA("Vector3Value") then
1187 clusterValues.Value = Vector3.new(clusterValues.Value.X, clusterValues.Value.Y, (clusterValues.Value.Z + 1) % 4)
1188 end
1189
1190 -- Rotate the parts or all the parts in the model
1191 local ry = math.pi/2
1192 if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
1193 --stampData.CurrentParts:Rotate(0, ry, 0)
1194 modelRotate(stampData.CurrentParts, ry)
1195 else
1196 stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame
1197 end
1198
1199 -- After rotating, update the position
1200 configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
1201 if configFound then
1202 positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
1203
1204 -- update everything else in MouseMove
1205 DoStamperMouseMove(Mouse)
1206 end
1207 elseif key == 'c' then -- try to expand our high scalability dragger dimension
1208 if HighScalabilityLine.InternalLine and HighScalabilityLine.InternalLine.magnitude > 0 and HighScalabilityLine.Dimensions < 3 then
1209 HighScalabilityLine.MorePoints[HighScalabilityLine.Dimensions] = HighScalabilityLine.End
1210 HighScalabilityLine.MoreLines[HighScalabilityLine.Dimensions] = HighScalabilityLine.InternalLine
1211 HighScalabilityLine.Dimensions = HighScalabilityLine.Dimensions + 1
1212 HighScalabilityLine.NewHint = true
1213 end
1214 end
1215 end
1216
1217 keyCon = Mouse.KeyDown:connect(function(key) -- init key connection (keeping code close to func)
1218 setupKeyListener(key, Mouse)
1219 end)
1220
1221 local function resetHighScalabilityLine()
1222 if HighScalabilityLine then
1223 HighScalabilityLine.Start = nil
1224 HighScalabilityLine.End = nil
1225 HighScalabilityLine.InternalLine = nil
1226 HighScalabilityLine.NewHint = true
1227 end
1228 end
1229
1230 local function flashRedBox()
1231 local gui = game:GetService("CoreGui")
1232 if game:GetService("Players") then
1233 if game:GetService("Players")["LocalPlayer"] then
1234 if game:GetService("Players").LocalPlayer:FindFirstChild("PlayerGui") then
1235 gui = game:GetService("Players").LocalPlayer.PlayerGui
1236 end
1237 end
1238 end
1239 if not stampData["ErrorBox"] then return end
1240
1241 stampData.ErrorBox.Parent = gui
1242 if stampData.CurrentParts:IsA("Tool") then
1243 stampData.ErrorBox.Adornee = stampData.CurrentParts.Handle
1244 else
1245 stampData.ErrorBox.Adornee = stampData.CurrentParts
1246 end
1247
1248 delay(0,function()
1249 for i = 1, 3 do
1250 if stampData["ErrorBox"] then stampData.ErrorBox.Visible = true end
1251 wait(0.13)
1252 if stampData["ErrorBox"] then stampData.ErrorBox.Visible = false end
1253 wait(0.13)
1254 end
1255 if stampData["ErrorBox"] then
1256 stampData.ErrorBox.Adornee = nil
1257 stampData.ErrorBox.Parent = nil
1258 end
1259 end)
1260 end
1261
1262 local function DoStamperMouseDown(Mouse)
1263 if not Mouse then
1264 error("Error: RbxStamper.DoStamperMouseDown: Mouse is nil")
1265 return
1266 end
1267 if not Mouse:IsA("Mouse") then
1268 error("Error: RbxStamper.DoStamperMouseDown: Mouse is of type", Mouse.className,"should be of type Mouse")
1269 return
1270 end
1271 if not stampData then
1272 return
1273 end
1274
1275 if isMegaClusterPart() then
1276 if Mouse and HighScalabilityLine then
1277 local megaCube = stampData.CurrentParts:FindFirstChild("MegaClusterCube", true)
1278 local terrain = game:GetService("Workspace").Terrain
1279 if megaCube then
1280 HighScalabilityLine.Dimensions = 1
1281 local tempCell = terrain:WorldToCell(megaCube.CFrame.p)
1282 HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
1283 return
1284 else
1285 HighScalabilityLine.Dimensions = 1
1286 local tempCell = terrain:WorldToCell(stampData.CurrentParts.CFrame.p)
1287 HighScalabilityLine.Start = terrain:CellCenterToWorld(tempCell.X, tempCell.Y, tempCell.Z)
1288 return
1289 end
1290 end
1291 end
1292 end
1293
1294 local function loadSurfaceTypes(part, surfaces)
1295 part.TopSurface = surfaces[1]
1296 part.BottomSurface = surfaces[2]
1297 part.LeftSurface = surfaces[3]
1298 part.RightSurface = surfaces[4]
1299 part.FrontSurface = surfaces[5]
1300 part.BackSurface = surfaces[6]
1301 end
1302
1303 local function saveSurfaceTypes(part, myTable)
1304 local tempTable = {}
1305 tempTable[1] = part.TopSurface
1306 tempTable[2] = part.BottomSurface
1307 tempTable[3] = part.LeftSurface
1308 tempTable[4] = part.RightSurface
1309 tempTable[5] = part.FrontSurface
1310 tempTable[6] = part.BackSurface
1311
1312 myTable[part] = tempTable
1313 end
1314
1315 local function makeSurfaceUnjoinable(part, surface)
1316 -- TODO: FILL OUT!
1317 end
1318
1319 local function prepareModel(model)
1320 if not model then return nil end
1321
1322 local gDesiredTrans = 0.7
1323 local gStaticTrans = 1
1324
1325 local clone = model:Clone()
1326 local scripts = {}
1327 local parts = {}
1328 local decals = {}
1329
1330 stampData = {}
1331 stampData.DisabledScripts = {}
1332 stampData.TransparencyTable = {}
1333 stampData.MaterialTable = {}
1334 stampData.CanCollideTable = {}
1335 stampData.AnchoredTable = {}
1336 stampData.ArchivableTable = {}
1337 stampData.DecalTransparencyTable = {}
1338 stampData.SurfaceTypeTable = {}
1339
1340 collectParts(clone, parts, scripts, decals)
1341
1342 if #parts <= 0 then return nil, "no parts found in modelToStamp" end
1343
1344 for index,script in pairs(scripts) do
1345 if not(script.Disabled) then
1346 script.Disabled = true
1347 stampData.DisabledScripts[#stampData.DisabledScripts + 1] = script
1348 end
1349 end
1350 for index, part in pairs(parts) do
1351 stampData.TransparencyTable[part] = part.Transparency
1352 part.Transparency = gStaticTrans + (1 - gStaticTrans) * part.Transparency
1353 stampData.MaterialTable[part] = part.Material
1354 part.Material = Enum.Material.Plastic
1355 stampData.CanCollideTable[part] = part.CanCollide
1356 part.CanCollide = false
1357 stampData.AnchoredTable[part] = part.Anchored
1358 part.Anchored = true
1359 stampData.ArchivableTable[part] = part.Archivable
1360 part.Archivable = false
1361
1362 saveSurfaceTypes(part, stampData.SurfaceTypeTable)
1363
1364 local fadeInDelayTime = 0.5
1365 local transFadeInTime = 0.5
1366 delay(0,function()
1367 wait(fadeInDelayTime) -- give it some time to be completely transparent
1368
1369 local begTime = tick()
1370 local currTime = begTime
1371 while (currTime - begTime) < transFadeInTime and part and part:IsA("BasePart") and part.Transparency > gDesiredTrans do
1372 local newTrans = 1 - (((currTime - begTime)/transFadeInTime) * (gStaticTrans - gDesiredTrans))
1373 if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then
1374 part.Transparency = newTrans + (1 - newTrans) * stampData.TransparencyTable[part]
1375 end
1376 wait(0.03)
1377 currTime = tick()
1378 end
1379 if part and part:IsA("BasePart") then
1380 if stampData["TransparencyTable"] and stampData.TransparencyTable[part] then
1381 part.Transparency = gDesiredTrans + (1 - gDesiredTrans) * stampData.TransparencyTable[part]
1382 end
1383 end
1384 end)
1385 end
1386
1387 for index, decal in pairs(decals) do
1388 stampData.DecalTransparencyTable[decal] = decal.Transparency
1389 decal.Transparency = gDesiredTrans + (1 - gDesiredTrans) * decal.Transparency
1390 end
1391
1392 -- disable all seats
1393 setSeatEnabledStatus(clone, true)
1394 setSeatEnabledStatus(clone, false)
1395
1396 stampData.CurrentParts = clone
1397
1398 -- if auto-alignable, we enforce a pre-rotation to the canonical "0-frame"
1399 if autoAlignToFace(clone) then
1400 stampData.CurrentParts:ResetOrientationToIdentity()
1401 gInitial90DegreeRotations = 0
1402 else -- pre-rotate if necessary
1403 local ry = gInitial90DegreeRotations * math.pi/2
1404 if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
1405 --stampData.CurrentParts:Rotate(0, ry, 0)
1406 modelRotate(stampData.CurrentParts, ry)
1407 else
1408 stampData.CurrentParts.CFrame = CFrame.fromEulerAnglesXYZ(0, ry, 0) * stampData.CurrentParts.CFrame
1409 end
1410 end
1411
1412 -- 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
1413 -- 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
1414 -- issue (fingers crossed) [HotThoth]
1415
1416 local clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1417 if clusterMaterial and clusterMaterial:IsA("Vector3Value") then
1418 clusterMaterial.Value = Vector3.new(clusterMaterial.Value.X, clusterMaterial.Value.Y, (clusterMaterial.Value.Z + gInitial90DegreeRotations) % 4)
1419 end
1420
1421 -- After rotating, update the position
1422 local configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
1423 if configFound then
1424 stampData.CurrentParts = positionPartsAtCFrame3(targetCFrame, stampData.CurrentParts)
1425 end
1426
1427 -- to show joints during the mouse move
1428 game:GetService("JointsService"):SetJoinAfterMoveInstance(stampData.CurrentParts)
1429
1430 return clone, parts
1431 end
1432
1433 local function checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp)
1434 local cellCenterToWorld = game:GetService("Workspace").Terrain.CellCenterToWorld
1435 local cellCenter = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z)
1436 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)
1437
1438 local skipThisCell = false
1439
1440 for b = 1, #cellBlockingParts do
1441 if isBlocker(cellBlockingParts[b]) then skipThisCell = true break end
1442 end
1443
1444 if not skipThisCell then
1445 -- pop players up above any set cells
1446 local alreadyPushedUp = {}
1447 -- if no blocking model below, then see if stamping on top of a character
1448 for b = 1, #cellBlockingParts do
1449 if cellBlockingParts[b].Parent and
1450 not alreadyPushedUp[cellBlockingParts[b].Parent] and
1451 cellBlockingParts[b].Parent:FindFirstChild("Humanoid") and
1452 cellBlockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then
1453 -----------------------------------------------------------------------------------
1454 local blockingPersonTorso = cellBlockingParts[b].Parent:FindFirstChild("Torso")
1455 alreadyPushedUp[cellBlockingParts[b].Parent] = true
1456
1457 if blockingPersonTorso then
1458 -- 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)
1459 local newY = cellCenter.Y + 5
1460 if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then
1461 blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0)
1462 else
1463 -- if no space, we just skip this one
1464 skipThisCell = true
1465 break
1466 end
1467 end
1468 -----------------------------------------------------------------------------------
1469 end
1470 end
1471 end
1472
1473 if not skipThisCell then -- if we STILL aren't skipping... then we're good to go!
1474 local canSetCell = true
1475
1476 if checkHighScalabilityStamp then -- check to see if cell is in region, if not we'll skip set
1477 if allowedStampRegion then
1478 local cellPos = cellCenterToWorld(game:GetService("Workspace").Terrain, cellPos.X, cellPos.Y, cellPos.Z)
1479 if cellPos.X + 2 > allowedStampRegion.CFrame.p.X + allowedStampRegion.Size.X/2 then
1480 canSetCell = false
1481 elseif cellPos.X - 2 < allowedStampRegion.CFrame.p.X - allowedStampRegion.Size.X/2 then
1482 canSetCell = false
1483 elseif cellPos.Y + 2 > allowedStampRegion.CFrame.p.Y + allowedStampRegion.Size.Y/2 then
1484 canSetCell = false
1485 elseif cellPos.Y - 2 < allowedStampRegion.CFrame.p.Y - allowedStampRegion.Size.Y/2 then
1486 canSetCell = false
1487 elseif cellPos.Z + 2 > allowedStampRegion.CFrame.p.Z + allowedStampRegion.Size.Z/2 then
1488 canSetCell = false
1489 elseif cellPos.Z - 2 < allowedStampRegion.CFrame.p.Z - allowedStampRegion.Size.Z/2 then
1490 canSetCell = false
1491 end
1492 end
1493 end
1494
1495 return canSetCell
1496 end
1497 return false
1498 end
1499
1500
1501 local function ResolveMegaClusterStamp(checkHighScalabilityStamp)
1502 local cellSet = false
1503
1504 local cluser = game:GetService("Workspace").Terrain
1505
1506 local line = HighScalabilityLine.InternalLine
1507 local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max
1508 local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min
1509
1510 local clusterMaterial = 1 -- default is grass
1511 local clusterType = 0 -- default is brick
1512 local clusterOrientation = 0 -- default is 0 rotation
1513
1514 local autoWedgeClusterParts = false
1515 if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end
1516
1517 if stampData.CurrentParts:FindFirstChild("ClusterMaterial", true) then
1518 clusterMaterial = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1519 if clusterMaterial:IsA("Vector3Value") then
1520 clusterType = clusterMaterial.Value.Y
1521 clusterOrientation = clusterMaterial.Value.Z
1522 clusterMaterial = clusterMaterial.Value.X
1523 elseif clusterMaterial:IsA("IntValue") then
1524 clusterMaterial = clusterMaterial.Value
1525 end
1526 end
1527
1528 if HighScalabilityLine.Adorn.Parent and HighScalabilityLine.Start and ((HighScalabilityLine.Dimensions > 1) or (line and line.magnitude > 0)) then
1529 local startCell = game:GetService("Workspace").Terrain:WorldToCell(HighScalabilityLine.Start)
1530 local xInc = {0,0,0}
1531 local yInc = {0,0,0}
1532 local zInc = {0,0,0}
1533
1534 local cluster = game:GetService("Workspace").Terrain
1535
1536 local incrementVect = {nil, nil, nil}
1537 local stepVect = {Vector3.new(0, 0, 0), Vector3.new(0, 0, 0), Vector3.new(0, 0, 0)}
1538
1539 local worldAxes = {Vector3.new(1, 0, 0), Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)}
1540
1541 local lines = {}
1542 if HighScalabilityLine.Dimensions > 1 then table.insert(lines, HighScalabilityLine.MoreLines[1]) end
1543 if line and line.magnitude > 0 then table.insert(lines, line) end
1544 if HighScalabilityLine.Dimensions > 2 then table.insert(lines, HighScalabilityLine.MoreLines[2]) end
1545
1546 for i = 1, #lines do
1547 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
1548
1549 if lines[i].X > 0 then xInc[i] = 1 elseif lines[i].X < 0 then xInc[i] = -1 end
1550 if lines[i].Y > 0 then yInc[i] = 1 elseif lines[i].Y < 0 then yInc[i] = -1 end
1551 if lines[i].Z > 0 then zInc[i] = 1 elseif lines[i].Z < 0 then zInc[i] = -1 end
1552
1553 incrementVect[i] = Vector3.new(xInc[i], yInc[i], zInc[i])
1554 if incrementVect[i].magnitude < .9 then incrementVect[i] = nil end
1555 end
1556
1557
1558 if not lines[2] then lines[2] = Vector3.new(0, 0, 0) end
1559 if not lines[3] then lines[3] = Vector3.new(0, 0, 0) end
1560
1561 local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
1562 local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true)
1563
1564 while (stepVect[3].magnitude*4 <= lines[3].magnitude) do
1565 local outerStepVectIndex = 1
1566 while outerStepVectIndex < 4 do
1567 stepVect[2] = Vector3.new(0, 0, 0)
1568 while (stepVect[2].magnitude*4 <= lines[2].magnitude) do
1569 local innerStepVectIndex = 1
1570 while innerStepVectIndex < 4 do
1571 stepVect[1] = Vector3.new(0, 0, 0)
1572 while (stepVect[1].magnitude*4 <= lines[1].magnitude) do
1573 local stepVectSum = stepVect[1] + stepVect[2] + stepVect[3]
1574 local cellPos = Vector3int16.new(startCell.X + stepVectSum.X, startCell.Y + stepVectSum.Y, startCell.Z + stepVectSum.Z)
1575 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
1576 -- check if overlaps player or part
1577 local okToStampTerrainBlock = checkTerrainBlockCollisions(cellPos, checkHighScalabilityStamp)
1578
1579 if okToStampTerrainBlock then
1580 if waterForceTag then
1581 cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value])
1582 else
1583 cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterMaterial, clusterType, clusterOrientation)
1584 end
1585 cellSet = true
1586
1587 -- auto-wedge it?
1588 if (autoWedgeClusterParts) then
1589 game:GetService("Workspace").Terrain:AutowedgeCells(Region3int16.new(Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1),
1590 Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1)))
1591 end
1592 end
1593 end
1594 stepVect[1] = stepVect[1] + incrementVect[1]
1595 end
1596 if incrementVect[2] then
1597 while innerStepVectIndex < 4 and worldAxes[innerStepVectIndex]:Dot(incrementVect[2]) == 0 do
1598 innerStepVectIndex = innerStepVectIndex + 1
1599 end
1600 if innerStepVectIndex < 4 then
1601 stepVect[2] = stepVect[2] + worldAxes[innerStepVectIndex] * worldAxes[innerStepVectIndex]:Dot(incrementVect[2])
1602 end
1603 innerStepVectIndex = innerStepVectIndex + 1
1604 else
1605 stepVect[2] = Vector3.new(1, 0, 0)
1606 innerStepVectIndex = 4 -- skip all remaining loops
1607 end
1608 if (stepVect[2].magnitude*4 > lines[2].magnitude) then innerStepVectIndex = 4 end
1609 end
1610 end
1611 if incrementVect[3] then
1612 while outerStepVectIndex < 4 and worldAxes[outerStepVectIndex]:Dot(incrementVect[3]) == 0 do
1613 outerStepVectIndex = outerStepVectIndex + 1
1614 end
1615 if outerStepVectIndex < 4 then
1616 stepVect[3] = stepVect[3] + worldAxes[outerStepVectIndex] * worldAxes[outerStepVectIndex]:Dot(incrementVect[3])
1617 end
1618 outerStepVectIndex = outerStepVectIndex + 1
1619 else -- skip all remaining loops
1620 stepVect[3] = Vector3.new(1, 0, 0) outerStepVectIndex = 4
1621 end
1622 if (stepVect[3].magnitude*4 > lines[3].magnitude) then outerStepVectIndex = 4 end
1623 end
1624 end
1625 end
1626
1627 -- and also get rid of any HighScalabilityLine stuff if it's there
1628 HighScalabilityLine.Start = nil
1629 HighScalabilityLine.Adorn.Parent = nil
1630
1631 -- Mark for undo.
1632 if cellSet then
1633 stampData.CurrentParts.Parent = nil
1634 pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StamperMulti") end)
1635 end
1636
1637 return cellSet
1638 end
1639
1640 local function DoStamperMouseUp(Mouse)
1641 if not Mouse then
1642 error("Error: RbxStamper.DoStamperMouseUp: Mouse is nil")
1643 return false
1644 end
1645 if not Mouse:IsA("Mouse") then
1646 error("Error: RbxStamper.DoStamperMouseUp: Mouse is of type", Mouse.className,"should be of type Mouse")
1647 return false
1648 end
1649
1650 if not stampData.Dragger then
1651 error("Error: RbxStamper.DoStamperMouseUp: stampData.Dragger is nil")
1652 return false
1653 end
1654
1655 if not HighScalabilityLine then
1656 return false
1657 end
1658
1659 local checkHighScalabilityStamp = nil
1660 if stampInModel then
1661 local canStamp = nil
1662 local isHSLPart = isMegaClusterPart()
1663
1664 if isHSLPart and
1665 HighScalabilityLine and
1666 HighScalabilityLine.Start and
1667 HighScalabilityLine.InternalLine and
1668 HighScalabilityLine.InternalLine.magnitude > 0 then -- we have an HSL line, test later
1669 canStamp = true
1670 checkHighScalabilityStamp = true
1671 else
1672 canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion)
1673 end
1674
1675 if not canStamp then
1676 if stampFailedFunc then
1677 stampFailedFunc()
1678 end
1679 return false
1680 end
1681 end
1682
1683 -- if unstampable face, then don't let us stamp there!
1684 if unstampableSurface then
1685 flashRedBox()
1686 return false
1687 end
1688
1689 -- recheck if we can stamp, as we just moved part
1690 local canStamp, checkHighScalabilityStamp = t.CanEditRegion(stampData.CurrentParts, allowedStampRegion)
1691 if not canStamp then
1692 if stampFailedFunc then
1693 stampFailedFunc()
1694 end
1695 return false
1696 end
1697
1698 -- Prevent part from being stamped on top of a player
1699
1700 local minBB, maxBB = getBoundingBoxInWorldCoordinates(stampData.CurrentParts)
1701
1702 -- HotThoth's note: Now that above CurrentParts positioning has been commented out, to be truly correct, we would need to use the
1703 -- value of configFound from the previous onStamperMouseMove call which moved the CurrentParts
1704 -- Shouldn't this be true when lastTargetCFrame has been set and false otherwise?
1705 configFound, targetCFrame = findConfigAtMouseTarget(Mouse, stampData)
1706
1707 if configFound and not HighScalabilityLine.Adorn.Parent then
1708 if clusterPartsInRegion(minBB + insertBoundingBoxOverlapVector, maxBB - insertBoundingBoxOverlapVector) then
1709 flashRedBox()
1710 return false
1711 end
1712
1713 local blockingParts = game:GetService("Workspace"):FindPartsInRegion3(Region3.new(minBB + insertBoundingBoxOverlapVector,
1714 maxBB - insertBoundingBoxOverlapVector),
1715 stampData.CurrentParts,
1716 100)
1717
1718
1719 for b = 1, #blockingParts do
1720 if isBlocker(blockingParts[b]) then
1721 flashRedBox()
1722 return false
1723 end
1724 end
1725
1726 local alreadyPushedUp = {}
1727 -- if no blocking model below, then see if stamping on top of a character
1728 for b = 1, #blockingParts do
1729 if blockingParts[b].Parent and
1730 not alreadyPushedUp[blockingParts[b].Parent] and
1731 blockingParts[b].Parent:FindFirstChild("Humanoid") and
1732 blockingParts[b].Parent:FindFirstChild("Humanoid"):IsA("Humanoid") then
1733 ---------------------------------------------------------------------------
1734 local blockingPersonTorso = blockingParts[b].Parent:FindFirstChild("Torso")
1735 alreadyPushedUp[blockingParts[b].Parent] = true
1736
1737 if blockingPersonTorso then
1738 -- 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)
1739 local newY = maxBB.Y + 3
1740 if spaceAboveCharacter(blockingPersonTorso, newY, stampData) then
1741 blockingPersonTorso.CFrame = blockingPersonTorso.CFrame + Vector3.new(0, newY - blockingPersonTorso.CFrame.p.Y, 0)
1742 else
1743 -- if no space, we just error
1744 flashRedBox()
1745 return false
1746 end
1747 end
1748 ---------------------------------------------------------------------------
1749 end
1750 end
1751
1752 elseif (not configFound) and not (HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent) then -- if no config then only stamp if it's a real HSL!
1753 resetHighScalabilityLine()
1754 return false
1755 end
1756
1757 -- something will be stamped! so set the "StampedSomething" toggle to true
1758 if game:GetService("Players")["LocalPlayer"] then
1759 if game:GetService("Players").LocalPlayer["Character"] then
1760 local localChar = game:GetService("Players").LocalPlayer.Character
1761 local stampTracker = localChar:FindFirstChild("StampTracker")
1762 if stampTracker and not stampTracker.Value then
1763 stampTracker.Value = true
1764 end
1765 end
1766 end
1767
1768 -- if we drew a line of mega parts, stamp them out
1769 if HighScalabilityLine.Start and HighScalabilityLine.Adorn.Parent and isMegaClusterPart() then
1770 if ResolveMegaClusterStamp(checkHighScalabilityStamp) or checkHighScalabilityStamp then
1771 -- kill the ghost part
1772 stampData.CurrentParts.Parent = nil
1773 return true
1774 end
1775 end
1776
1777 -- not High-Scalability-Line-Based, so behave normally [and get rid of any HSL stuff]
1778 HighScalabilityLine.Start = nil
1779 HighScalabilityLine.Adorn.Parent = nil
1780
1781 local cluster = game:GetService("Workspace").Terrain
1782
1783 -- if target point is in cluster, just use cluster:SetCell
1784 if isMegaClusterPart() then
1785 -- if targetCFrame is inside cluster, just set that cell to 1 and return
1786 --local cellPos = cluster:WorldToCell(targetCFrame.p)
1787
1788 local cellPos
1789 if stampData.CurrentParts:IsA("Model") then cellPos = cluster:WorldToCell(stampData.CurrentParts:GetModelCFrame().p)
1790 else cellPos = cluster:WorldToCell(stampData.CurrentParts.CFrame.p) end
1791
1792 local cMax = game:GetService("Workspace").Terrain.MaxExtents.Max
1793 local cMin = game:GetService("Workspace").Terrain.MaxExtents.Min
1794
1795 if checkTerrainBlockCollisions(cellPos, false) then
1796
1797 local clusterValues = stampData.CurrentParts:FindFirstChild("ClusterMaterial", true)
1798 local waterForceTag = stampData.CurrentParts:FindFirstChild("WaterForceTag", true)
1799 local waterForceDirectionTag = stampData.CurrentParts:FindFirstChild("WaterForceDirectionTag", true)
1800
1801 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
1802
1803 if waterForceTag then
1804 cluster:SetWaterCell(cellPos.X, cellPos.Y, cellPos.Z, Enum.WaterForce[waterForceTag.Value], Enum.WaterDirection[waterForceDirectionTag.Value])
1805 elseif not clusterValues then
1806 cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, cellInfo.Material, cellInfo.clusterType, gInitial90DegreeRotations % 4)
1807 elseif clusterValues:IsA("Vector3Value") then
1808 cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value.X, clusterValues.Value.Y, clusterValues.Value.Z)
1809 else
1810 cluster:SetCell(cellPos.X, cellPos.Y, cellPos.Z, clusterValues.Value, 0, 0)
1811 end
1812
1813 local autoWedgeClusterParts = false
1814 if stampData.CurrentParts:FindFirstChild("AutoWedge") then autoWedgeClusterParts = true end
1815
1816 -- auto-wedge it
1817 if (autoWedgeClusterParts) then
1818 game:GetService("Workspace").Terrain:AutowedgeCells(
1819 Region3int16.new(
1820 Vector3int16.new(cellPos.x - 1, cellPos.y - 1, cellPos.z - 1),
1821 Vector3int16.new(cellPos.x + 1, cellPos.y + 1, cellPos.z + 1)
1822 )
1823 )
1824 end
1825
1826 -- kill the ghost part
1827 stampData.CurrentParts.Parent = nil
1828
1829 -- Mark for undo. It has to happen here or the selection display will come back also.
1830 pcall(function() game:GetService("ChangeHistoryService"):SetWaypoint("StamperSingle") end)
1831 return true
1832 end
1833 else
1834 -- you tried to stamp a HSL-single part where one does not belong!
1835 flashRedBox()
1836 return false
1837 end
1838 end
1839
1840 local function getPlayer()
1841 if game:GetService("Players")["LocalPlayer"] then
1842 return game:GetService("Players").LocalPlayer
1843 end
1844 return nil
1845 end
1846
1847
1848 -- Post process: after positioning the part or model, restore transparency, material, anchored and collide states and create joints
1849 if stampData.CurrentParts:IsA("Model") or stampData.CurrentParts:IsA("Tool") then
1850 if stampData.CurrentParts:IsA("Model") then
1851 -- Tyler's magical hack-code for allowing/preserving clones of both Surface and Manual Welds... just don't ask X<
1852 local manualWeldTable = {}
1853 local manualWeldParentTable = {}
1854 saveTheWelds(stampData.CurrentParts, manualWeldTable, manualWeldParentTable)
1855 stampData.CurrentParts:BreakJoints()
1856 stampData.CurrentParts:MakeJoints()
1857 restoreTheWelds(manualWeldTable, manualWeldParentTable)
1858 end
1859
1860 -- 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)
1861 local playerIdTag = stampData.CurrentParts:FindFirstChild("PlayerIdTag")
1862 local playerNameTag = stampData.CurrentParts:FindFirstChild("PlayerNameTag")
1863 if playerIdTag ~= nil then
1864 local tempPlayerValue = getPlayer()
1865 if tempPlayerValue ~= nil then playerIdTag.Value = tempPlayerValue.UserId end
1866 end
1867 if playerNameTag ~= nil then
1868 if game:GetService("Players")["LocalPlayer"] then
1869 local tempPlayerValue = game:GetService("Players").LocalPlayer
1870 if tempPlayerValue ~= nil then playerNameTag.Value = tempPlayerValue.Name end
1871 end
1872 end
1873 -- ...and tag all inserted models for subsequent origin identification
1874 -- if no RobloxModel tag already exists, then add it.
1875 if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then
1876 local stringTag = Instance.new("BoolValue", stampData.CurrentParts)
1877 stringTag.Name = "RobloxModel"
1878
1879 if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then
1880 local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts)
1881 stringTag2.Name = "RobloxStamper"
1882 end
1883 end
1884
1885 else
1886 stampData.CurrentParts:BreakJoints()
1887 if stampData.CurrentParts:FindFirstChild("RobloxStamper") == nil then
1888 local stringTag2 = Instance.new("BoolValue", stampData.CurrentParts)
1889 stringTag2.Name = "RobloxStamper"
1890 end
1891 end
1892
1893 -- make sure all the joints are activated before restoring anchor states
1894 game:GetService("JointsService"):CreateJoinAfterMoveJoints()
1895
1896 -- Restore the original properties for all parts being stamped
1897 for part, transparency in pairs(stampData.TransparencyTable) do
1898 part.Transparency = transparency
1899 end
1900 for part, archivable in pairs(stampData.ArchivableTable) do
1901 part.Archivable = archivable
1902 end
1903 for part, material in pairs(stampData.MaterialTable) do
1904 part.Material = material
1905 end
1906 for part, collide in pairs(stampData.CanCollideTable) do
1907 part.CanCollide = collide
1908 end
1909 for part, anchored in pairs(stampData.AnchoredTable) do
1910 part.Anchored = anchored
1911 end
1912 for decal, transparency in pairs(stampData.DecalTransparencyTable) do
1913 decal.Transparency = transparency
1914 end
1915
1916 for part, surfaces in pairs(stampData.SurfaceTypeTable) do
1917 loadSurfaceTypes(part, surfaces)
1918 end
1919
1920 if isMegaClusterPart() then
1921 stampData.CurrentParts.Transparency = 0
1922 end
1923
1924 -- re-enable all seats
1925 setSeatEnabledStatus(stampData.CurrentParts, true)
1926
1927 stampData.TransparencyTable = nil
1928 stampData.ArchivableTable = nil
1929 stampData.MaterialTable = nil
1930 stampData.CanCollideTable = nil
1931 stampData.AnchoredTable = nil
1932 stampData.SurfaceTypeTable = nil
1933
1934 -- ...and tag all inserted models for subsequent origin identification
1935 -- if no RobloxModel tag already exists, then add it.
1936 if stampData.CurrentParts:FindFirstChild("RobloxModel") == nil then
1937 local stringTag = Instance.new("BoolValue", stampData.CurrentParts)
1938 stringTag.Name = "RobloxModel"
1939 end
1940
1941 --Re-enable the scripts
1942 for index,script in pairs(stampData.DisabledScripts) do
1943 script.Disabled = false
1944 end
1945
1946 --Now that they are all marked enabled, reinsert them into the world so they start running
1947 for index,script in pairs(stampData.DisabledScripts) do
1948 local oldParent = script.Parent
1949 script.Parent = nil
1950 script:Clone().Parent = oldParent
1951 end
1952
1953 -- clear out more data
1954 stampData.DisabledScripts = nil
1955 stampData.Dragger = nil
1956 stampData.CurrentParts = nil
1957
1958 pcall(function() game:GetService("ChangeHistoryService"): SetWaypoint("StampedObject") end)
1959 return true
1960 end
1961
1962 local function pauseStamper()
1963 for i = 1, #mouseCons do -- stop the mouse from doing anything
1964 mouseCons[i]:disconnect()
1965 mouseCons[i] = nil
1966 end
1967 mouseCons = {}
1968
1969 if stampData and stampData.CurrentParts then -- remove our ghost part
1970 stampData.CurrentParts.Parent = nil
1971 stampData.CurrentParts:Remove()
1972 end
1973
1974 resetHighScalabilityLine()
1975
1976 game:GetService("JointsService"):ClearJoinAfterMoveJoints()
1977 end
1978
1979
1980 local function prepareUnjoinableSurfaces(modelCFrame, parts, whichSurface)
1981 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!
1982 local isPositive = 1
1983 if whichSurface < 0 then isPositive = isPositive * -1 whichSurface = whichSurface*-1 end
1984 local surfaceNormal = isPositive * modelCFrame:vectorToWorldSpace(AXIS_VECTORS[whichSurface])
1985
1986 for i = 1, #parts do
1987 local currPart = parts[i]
1988
1989 -- now just need to find which surface of currPart most closely match surfaceNormal and then set that to Unjoinable
1990 local surfaceNormalInLocalCoords = currPart.CFrame:vectorToObjectSpace(surfaceNormal)
1991 if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Y) then
1992 if math.abs(surfaceNormalInLocalCoords.X) > math.abs(surfaceNormalInLocalCoords.Z) then
1993 if surfaceNormalInLocalCoords.X > 0 then currPart.RightSurface = "Unjoinable" else currPart.LeftSurface = "Unjoinable" end
1994 else
1995 if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end
1996 end
1997 else
1998 if math.abs(surfaceNormalInLocalCoords.Y) > math.abs(surfaceNormalInLocalCoords.Z) then
1999 if surfaceNormalInLocalCoords.Y > 0 then currPart.TopSurface = "Unjoinable" else currPart.BottomSurface = "Unjoinable" end
2000 else
2001 if surfaceNormalInLocalCoords.Z > 0 then currPart.BackSurface = "Unjoinable" else currPart.FrontSurface = "Unjoinable" end
2002 end
2003 end
2004 end
2005 end
2006
2007 local function resumeStamper()
2008 local clone, parts = prepareModel(modelToStamp)
2009
2010 if not clone or not parts then
2011 return
2012 end
2013
2014 -- if we have unjoinable faces, then we want to change those surfaces to be Unjoinable
2015 local unjoinableTag = clone:FindFirstChild("UnjoinableFaces", true)
2016 if unjoinableTag then
2017 for unjoinableSurface in string.gmatch(unjoinableTag.Value, "[^,]*") do
2018 if tonumber(unjoinableSurface) then
2019 if clone:IsA("Model") then
2020 prepareUnjoinableSurfaces(clone:GetModelCFrame(), parts, tonumber(unjoinableSurface))
2021 else
2022 prepareUnjoinableSurfaces(clone.CFrame, parts, tonumber(unjoinableSurface))
2023 end
2024 end
2025 end
2026 end
2027
2028 stampData.ErrorBox = errorBox
2029 if stampInModel then
2030 clone.Parent = stampInModel
2031 else
2032 clone.Parent = game:GetService("Workspace")
2033 end
2034
2035 if clone:FindFirstChild("ClusterMaterial", true) then -- extract all info from vector
2036 local clusterMaterial = clone:FindFirstChild("ClusterMaterial", true)
2037 if (clusterMaterial:IsA("Vector3Value")) then
2038 cellInfo.Material = clusterMaterial.Value.X
2039 cellInfo.clusterType = clusterMaterial.Value.Y
2040 cellInfo.clusterOrientation = clusterMaterial.Value.Z
2041 elseif clusterMaterial:IsA("IntValue") then
2042 cellInfo.Material = clusterMaterial.Value
2043 end
2044 end
2045
2046 pcall(function() mouseTarget = Mouse.Target end)
2047
2048 if mouseTarget and mouseTarget.Parent:FindFirstChild("RobloxModel") == nil then
2049 game:GetService("JointsService"):SetJoinAfterMoveTarget(mouseTarget)
2050 else
2051 game:GetService("JointsService"):SetJoinAfterMoveTarget(nil)
2052 end
2053 game:GetService("JointsService"):ShowPermissibleJoints()
2054
2055 for index, object in pairs(stampData.DisabledScripts) do
2056 if object.Name == "GhostRemovalScript" then
2057 object.Parent = stampData.CurrentParts
2058 end
2059 end
2060
2061 stampData.Dragger = Instance.new("Dragger")
2062
2063 --Begin a movement by faking a MouseDown signal
2064 stampData.Dragger:MouseDown(parts[1], Vector3.new(0,0,0), parts)
2065 stampData.Dragger:MouseUp()
2066
2067 DoStamperMouseMove(Mouse)
2068
2069 table.insert(mouseCons,Mouse.Move:connect(function()
2070 if movingLock or stampUpLock then return end
2071 movingLock = true
2072 DoStamperMouseMove(Mouse)
2073 movingLock = false
2074 end))
2075
2076 table.insert(mouseCons,Mouse.Button1Down:connect(function()
2077 DoStamperMouseDown(Mouse)
2078 end))
2079
2080 table.insert(mouseCons,Mouse.Button1Up:connect(function()
2081 stampUpLock = true
2082 while movingLock do wait() end
2083 stamped.Value = DoStamperMouseUp(Mouse)
2084 resetHighScalabilityLine()
2085 stampUpLock = false
2086 end))
2087
2088 stamped.Value = false
2089 end
2090
2091 local function resetStamperState(newModelToStamp)
2092
2093 -- if we have a new model, swap it out
2094 if newModelToStamp then
2095 if not newModelToStamp:IsA("Model") and not newModelToStamp:IsA("BasePart") then
2096 error("resetStamperState: newModelToStamp (first arg) is not nil, but not a model or part!")
2097 end
2098 modelToStamp = newModelToStamp
2099 end
2100
2101 -- first clear our state
2102 pauseStamper()
2103 -- now lets load in the new model
2104 resumeStamper()
2105
2106 end
2107
2108 -- load the model initially
2109 resetStamperState()
2110
2111
2112 -- setup the control table we pass back to the user
2113 control.Stamped = stamped -- BoolValue that fires when user stamps
2114 control.Paused = false
2115
2116 control.LoadNewModel = function(newStampModel) -- allows us to specify a new stamper model to be used with this stamper
2117 if newStampModel and not newStampModel:IsA("Model") and not newStampModel:IsA("BasePart") then
2118 error("Control.LoadNewModel: newStampModel (first arg) is not a Model or Part!")
2119 return nil
2120 end
2121 resetStamperState(newStampModel)
2122 end
2123
2124 control.ReloadModel = function() -- will automatically set stamper to get a new model of current model and start stamping with new model
2125 resetStamperState()
2126 end
2127
2128 control.Pause = function() -- temporarily stops stamping, use resume to start up again
2129 if not control.Paused then
2130 pauseStamper()
2131 control.Paused = true
2132 else
2133 print("RbxStamper Warning: Tried to call Control.Pause() when already paused")
2134 end
2135 end
2136
2137 control.Resume = function() -- resumes stamping, if currently paused
2138 if control.Paused then
2139 resumeStamper()
2140 control.Paused = false
2141 else
2142 print("RbxStamper Warning: Tried to call Control.Resume() without Pausing First")
2143 end
2144 end
2145
2146 control.ResetRotation = function() -- resets the model rotation so new models are at default orientation
2147 -- gInitial90DegreeRotations = 0
2148 -- 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
2149 -- High-Scalability and with the new model orientation setting methods (model:ResetOrientationToIdentity()) [HotThoth]
2150 end
2151
2152 control.Destroy = function() -- Stops current Stamp operation and destroys control construct
2153 for i = 1, #mouseCons do
2154 mouseCons[i]:disconnect()
2155 mouseCons[i] = nil
2156 end
2157
2158 if keyCon then
2159 keyCon:disconnect()
2160 end
2161
2162 game:GetService("JointsService"):ClearJoinAfterMoveJoints()
2163
2164 if adorn then adorn:Destroy() end
2165 if adornPart then adornPart:Destroy() end
2166 if errorBox then errorBox:Destroy() end
2167 if stampData then
2168 if stampData["Dragger"] then
2169 stampData.Dragger:Destroy()
2170 end
2171 if stampData.CurrentParts then
2172 stampData.CurrentParts:Destroy()
2173 end
2174 end
2175 if control and control["Stamped"] then
2176 control.Stamped:Destroy()
2177 end
2178 control = nil
2179 end
2180
2181 return control
2182end
2183
2184t.Help =
2185 function(funcNameOrFunc)
2186 --input argument can be a string or a function. Should return a description (of arguments and expected side effects)
2187 if funcNameOrFunc == "GetStampModel" or funcNameOrFunc == t.GetStampModel then
2188 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"
2189 end
2190 if funcNameOrFunc == "SetupStamperDragger" or funcNameOrFunc == t.SetupStamperDragger then
2191 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."
2192 end
2193 end
2194
2195return t