The original post linked here has code which allows the intersection or subtraction of one block from another without using the GeometryService functions. This is useful for situations like subtracting a window from a wall. However, the code assumes that the blocks have zero rotation.
This updated code uses the CFrame of the first block to do the calculations so that it can work in any rotation. The second block should be aligned with the first block but can be rotated 90 degrees in any direction. The extents of the second block are used if the second block is not aligned with the first block.
volumesIntersect(block1, block2)
returns a new block which is the intersection between the first block and the extents of the second block. The new block is a clone of the first block with its Size and Position modified.
volumesDifference(block1, block2)
returns an array of blocks which combine to form the volume of the first block with the extents of the second block removed. Each returned block is a clone of the first block with its Size and Position modified.
volumesOverlap(block1, block2)
returns true if the blocks intersect.
--
-- Adapted from code from Emma @ThanksRoBama on DevForum at
-- https://devforum.roblox.com/t/how-to-negate-grid-like-blocks-without-unions/1590850/6
--
-- @fletc3her added base parameter for the CFrame in which calculations are being made.
function corner1(part,base)
if base then
return base:PointToObjectSpace(part.Position) - GetPartExtentsSize(part,base) / 2
end
return part.Position - part.Size / 2
end
function corner2(part,base)
if base then
return base:PointToObjectSpace(part.Position) + GetPartExtentsSize(part,base) / 2
end
return part.Position + part.Size / 2
end
function minMax(v1, v2)
return math.min(v1, v2), math.max(v1, v2)
end
function avg(v1, v2)
return (v1 + v2) / 2
end
--[[
Given two number ranges (p11, p12) and (p21, p22), return the intersection of those ranges.
]]
function numbersIntersection(p11: number, p12: number, p21: number, p22: number): {number}
--Ensure segments (p11, p12) and (p21, p22) go from small to large
--Ensure p11 comes before p12
p11, p12 = minMax(p11, p12)
--Ensure p21 comes before p22
p21, p22 = minMax(p21, p22)
--Ensure (p11, p12) starts before (p21, p22)
if p11 > p21 then
--... by swapping if that's not the case
p11, p12, p21, p22 = p21, p22, p11, p12
end
if p12 < p21 then
return {} --No overlap
elseif p12 == p21 then
return {p12} --Overlaps in a point
elseif p12 > p21 then
if p12 < p22 then
return {p21, p12} --Some overlapping segment
else
return {p21, p22} --Entirety of (p21, p22) is contained in (p11, p12)
end
else
error()
end
end
--[[
Given two number ranges (p11, p12) and (p21, p22), return the second range removed from the first.
]]
function numbersDifference(p11: number, p12: number, p21: number, p22: number): {number}
--Ensure segments (p11, p12) and (p21, p22) go from small to large
--Ensure p11 comes before p12
p11, p12 = minMax(p11, p12)
--Ensure p21 comes before p22
p21, p22 = minMax(p21, p22)
--Nothing or everything gets deleted
local intersection = numbersIntersection(p11, p12, p21, p22)
if #intersection == 0 then --No overlap
return {p11, p12} --Just return first segment
elseif intersection[1] == p11 and intersection[2] == p12 then --Overlap is entire first segment
return {} --Nothing left
end
--Something but not everything gets deleted
if p21 <= p11 then -- (p21, p22) starts before (p11, p12) and ends inside
--Only 1 case because if p22 were >= p12, everything would get deleted and that's checked previously
return {p11, p22, p12}, 1 --First part gets deleted, second part stays
elseif p22 >= p12 then -- (p21, p22) starts inside (p11, p12) and ends outside then
--Only 1 case because if p21 were <= p11, everything would get deleted and that's checked previously
return {p11, p21, p12}, 2 --Second part gets deleted, first part stays then
else --Second segment is inside first segment, so cut out middle part
return {p11, p21, p22, p12}, 2 --Middle gets deleted, other parts stay
end
end
--[[
Return a non-rotated Part with Size and Position defined by two corners.
Optionally set the Size and Position of an existing Part, or use a new Part.
@fletc3her added if existingPart is specified then size and position should be in local coordinates for its CFrame.
]]
function partFromCorners(corner1: Vector3, corner2: Vector3, existingPart: Part?): Part
local part = existingPart or Instance.new("Part")
local diff = corner2 - corner1
local mid = avg(corner1, corner2)
part.Size = Vector3.new(diff.X, diff.Y, diff.Z)
part.Position += part.CFrame:VectorToWorldSpace(Vector3.new(mid.X, mid.Y, mid.Z))
return part
end
--[[
Given a non-rotated Part, an Axis and a list of points along that axis (as numbers),
return the slices of the Part on the plane perpendicular to the axis and intersecting the points
along the axis, as a list of Parts.
@fletc3her added CFrame to corner calculations to accomodate rotation
]]
function slicePart(part: Part, axis: Enum.Axis, points: {number})
local slices = {}
local cutAxis = Vector3.FromAxis(axis)
local cutPlane = Vector3.new(1, 1, 1) - cutAxis
local sCorner1 = corner1(part,part.CFrame)
local sCorner2 = sCorner1 * cutAxis + corner2(part,part.CFrame) * cutPlane --Gets zero'd on the cut axis (so it corner2 on cut axis = corner 1 on cut axis)
for i = 1, #points do
sCorner2 += points[i] * cutAxis - sCorner1 * cutAxis --Slide corner2 to point[i] on the cut axis
table.insert(slices, partFromCorners(sCorner1, sCorner2, part:Clone()))
sCorner1 += (sCorner2 - sCorner1) * cutAxis --Slide corner1 to corner2 on the cut axis
end
sCorner2 = corner2(part,part.CFrame)
table.insert(slices, partFromCorners(sCorner1, sCorner2, part:Clone()))
return slices
end
--[[
@fletc3her added utility function to check for intersection
]]
function volumesOverlap(block1: Part, block2: Part): boolean
local b1c1, b1c2 = corner1(block1,block1.CFrame), corner2(block1,block1.CFrame)
local b2c1, b2c2 = corner1(block2,block1.CFrame), corner2(block2,block1.CFrame)
--If any axis is non-intersecting, the intersection is empty
local xDifference = numbersIntersection(b1c1.X, b1c2.X, b2c1.X, b2c2.X)
if #xDifference ~= 2 then return false end
local yDifference = numbersIntersection(b1c1.Y, b1c2.Y, b2c1.Y, b2c2.Y)
if #yDifference ~= 2 then return false end
local zDifference = numbersIntersection(b1c1.Z, b1c2.Z, b2c1.Z, b2c2.Z)
if #zDifference ~= 2 then return false end
return true
end
--[[
Given two volumes (as Parts), return the intersection of those volumes (as a Part).
If the parts are not rotated similarly then the extents of the second part are used.
@fletc3her corrected a bug in this function which was preventing a return value.
]]
function volumesIntersection(block1: Part, block2: Part): Part?
local b1c1, b1c2 = corner1(block1,block1.CFrame), corner2(block1,block1.CFrame)
local b2c1, b2c2 = corner1(block2,block1.CFrame), corner2(block2,block1.CFrame)
--If any axis is non-intersecting, the intersection is empty
local xDifference = numbersIntersection(b1c1.X, b1c2.X, b2c1.X, b2c2.X)
if #xDifference ~= 2 then return nil end
local yDifference = numbersIntersection(b1c1.Y, b1c2.Y, b2c1.Y, b2c2.Y)
if #yDifference ~= 2 then return nil end
local zDifference = numbersIntersection(b1c1.Z, b1c2.Z, b2c1.Z, b2c2.Z)
if #zDifference ~= 2 then return nil end
--If all axes are intersecting, return the 3D intersection
local corner1 = Vector3.new(xDifference[1], yDifference[1], zDifference[1])
local corner2 = Vector3.new(xDifference[2], yDifference[2], zDifference[2])
return partFromCorners(corner1, corner2, block1:Clone())
end
--[[
Given two volumes (as Parts), return the second volume removed from the first volume (as a list of Parts).
If the parts are not rotated similarly then the extents of the second part are used.
]]
function volumesDifference(block1: Part, block2: Part): {Part}
-- Compute the differences on each axis *before* slicing on the axes,
-- because we might exit early if one or more axes are non-overlapping
local axisDifferences = {}
for _, axis in pairs(Enum.Axis:GetEnumItems()) do
local A = axis.Name
local points, toDelete = numbersDifference(corner1(block1,block1.CFrame)[A], corner2(block1,block1.CFrame)[A], corner1(block2,block1.CFrame)[A], corner2(block2,block1.CFrame)[A])
if #points == 2 then --Or equivalently if toDelete == nil, i.e. block2 and block1 do not intersect, so (block1 - block2) = block1
return { block1:Clone() }
end
axisDifferences[axis] = {points = points, toDelete = toDelete}
end
-- Slice along axes after verifying that none are non-interesecting
local parts = { }
local partToSlice = block1
for _, axis in pairs(Enum.Axis:GetEnumItems()) do
local A = axis.Name
local differences = axisDifferences[axis]
local points: {number}, toDelete: number = differences.points, differences.toDelete
--Slices are to be made between each point, but slicePart expects a list of points to slice AT, not two points to slice BETWEEN.
--Fix this by removing first and last points.
table.remove(points, #points)
table.remove(points, 1)
--Skip if (block1 - block2) == block1, i.e. block2 is completely outside block 1
if #points == 0 then
continue
end
--Slice block1 according to where it intersects block2
local slices = slicePart(partToSlice, axis, points)
--Return all the slices except the ones that are inside block2
for i = 1, #slices do
if i == toDelete then
partToSlice = slices[i]
else
table.insert(parts, slices[i])
end
end
end
return parts
end
-- GetPartExtentsSize by acz0r used in corner calculation to correct orientation changes
-- Original function by XAXA, original optimized by Pyseph
-- https://devforum.roblox.com/t/getextentssize-of-one-part-without-model/404945/7
function GetPartExtentsSize(part: BasePart, orientation: CFrame?): (Vector3)
orientation = orientation or CFrame.identity
local partCFrame: CFrame = part.CFrame
partCFrame = orientation:ToObjectSpace(partCFrame)
local size: Vector3 = part.Size
local sx: number, sy: number, sz: number = size.X, size.Y, size.Z
local x: number, y: number, z: number,
R00: number, R01: number, R02: number,
R10: number, R11: number, R12: number,
R20: number, R21: number, R22: number = partCFrame:GetComponents()
local wsx: number = 0.5 * (math.abs(R00) * sx + math.abs(R01) * sy + math.abs(R02) * sz)
local wsy: number = 0.5 * (math.abs(R10) * sx + math.abs(R11) * sy + math.abs(R12) * sz)
local wsz: number = 0.5 * (math.abs(R20) * sx + math.abs(R21) * sy + math.abs(R22) * sz)
return Vector3.new(x + wsx, y + wsy, z + wsz) - Vector3.new(x - wsx, y - wsy, z - wsz)
end
return {
volumesIntersection = volumesIntersection,
volumesDifference = volumesDifference,
volumesOverlap = volumesOverlap,
GetPartExtentsSize = GetPartExtentsSize,
}