How to negate grid like blocks without unions?

image
image

How would I negate block like parts, without using unions but instead splitting it into other parts as shown above?

1 Like

EDIT: Okay, I posted the second part that actually solves what you were asking for :sweat_smile: What a fun problem! Hope this is of some help.

EDIT: I swapped ∪ for ∩ >.<’ it’s fixed now tho

EDIT: Here's the full script, with added comments and type annotations:
function corner1(part)
	return part.Position - part.Size / 2
end

function corner2(part)
	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} --Entirity 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.
]]
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 = 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.
]]
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)
	local sCorner2 = sCorner1 * cutAxis + corner2(part) * 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)
	table.insert(slices, partFromCorners(sCorner1, sCorner2, part:Clone()))
	
	return slices
end

--[[
Given two volumes (as Parts), return the intersection of those volumes (as a Part).
Input parts must have no rotation.
]]
function volumesIntersection(block1: Part, block2: Part): Part?
	local b1c1, b1c2 = corner1(block1), corner2(block1)
	local b2c1, b2c2 = corner1(block2), corner2(block2)
	
	local xDifference = numbersDifference(b1c1.X, b1c2.X, b2c1.X, b2c2.X)
	local yDifference = numbersDifference(b1c1.Y, b1c2.Y, b2c1.Y, b2c2.Y)
	local zDifference = numbersDifference(b1c1.Z, b1c2.Z, b2c1.Z, b2c2.Z)
	
	--If any axis is non-intersecting, the intersection is empty
	if #xDifference ~= 2 and #yDifference ~= 2 and #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])

	local difference = block1:Clone()
	partFromCorners(corner1, corner2, difference)

	return difference
end

--[[
Given two volumes (as Parts), return the second volume removed from the first volume (as a list of Parts).
Input parts must have no rotation.
]]
function volumesDifference(block1: Part, block2: Part): {Part}
	local parts = {}
	
	-- 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)[A], corner2(block1)[A], corner1(block2)[A], corner2(block2)[A])
		
		axisDifferences[axis] = {points = points, toDelete = toDelete}
		if #points == 2 then --Or equivalently if toDelte == nil, i.e. block2 and block1 do not intersect.
			return { block1:Clone() }
		end
	end
	
	-- Slice along axes after verifying that none are non-interesecting
	for _, axis in pairs(Enum.Axis:GetEnumItems()) do
		local A = axis.Name
		local differences = axisDifferences[axis]
		local points: {number}, toDelete = differences.points, differences.toDelete
		print(points)
		--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(block1, axis, points)
		
		--Return all the slices except the ones that are inside block2
		for i = 1, #slices do
			if i == toDelete then continue end
			table.insert(parts, slices[i])
		end
	end
	
	return parts
end

Part 1

Problem description

First of all, I’m assuming the blocks will always be axis-aligned (i.e. not rotated).

What you want to find is A - B, where A is the grey block and B is the red one. As you’ve shown it, B fits nicely inside A, but that might not always be the case. We might as well solve the problem in a more general case where only some of B overlaps A, or even where they’re not even touching. In that case, what you want to do is remove from A the volume that is shared between A and B, which can be written as A ∩ B (a symbol from set theory meaning “intersection”). So the first step is to find A ∩ B.

Solution in 1D

It’s helpful to solve it first in 1D and then extend the solution to 3D, because 1D is much simpler and easier to think about. In 1D there are a few cases that must be handled differently.

I've illustrated all the cases below:

Case 1 (no overlap)
image

Case 2 (overlap in a point)
image

Case 3 (overlap in a segment)
image

Case 4 (A completely inside B)
image

You might wonder why there are not cases where A is to the left of B. That’s because is completely symmetric, i.e. A ∩ B = B ∩ A. For example this case…

is the same as case 1 except the colors and labels are switched, but because of that property of it’s the same as case 1.

Since we’re working in 1D the points are just numbers on the number line, so let’s create a function called numbersIntersection that finds A ∩ B where A=(p11, p12) and B=(p21, p22):

function numbersIntersection(p11, p12, p21, p22)
    -- ... something something
    -- return {point1 of A ∩ B, point 2 of A ∩ B}
end

The function will return a table containing 0, 1 or 2 points (which are numbers because 1D) that make up A ∩ B.

It's helpful to swap the parameters around to make sure they're always in the order described:
function minMax(v1, v2)
	return math.min(v1, v2), math.max(v1, v2)
end

function numbersIntersection(p11, p12, p21, p22)
    --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
   
    -- ... return something
end

-

And now it's just a matter of returning the right segment in each of the 4 cases:
function numbersIntersection(p11, p12, p21, p22)
    --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 --End of segment 1 comes before start of segment 2
        return {} --No overlap
    elseif p12 == p21 then --End of segment 1 is same as start of segment 2
        return {p12} --Overlaps in a point
    elseif p12 > p21 then --End of segment 1 inside segment 2
        if p12 < p22 then --Start of segment 1 before start of segment 2
            return {p21, p12} --Some overlapping segment
        else --Start of segment 1 inside segment 2
            return {p21, p22} --Entirity of (p21, p22) is contained in (p11, p12)
        end
    end
end

Solution in 2D

The intersection of two 1D segments was (at most) a 1D segment made up of two numbers.

Likewise, the intersection of two 2D rectangles is (at most) a 2D rectangle made up of 2 1D segments at right angles (because we know they’re not rotated), or 4 numbers (start and end on X axis, start and end on Y axis). Once again because we know the rectangles are axis aligned we don’t actually need all 4 points, we can figure out the rest if we just have 2 opposing corners.

The 2D solution illustrated:

The intersection is the rectangle made up of the two segments (projected onto the X and Y axes in this illustration). We can find those segments as the intersections of the vertical and horizontal parts of the rectangles.

Solution in 3D

The intersection of two 3D cuboids is at most a 3D cuboid made up of 3 1D segments at right angles, or 6 numbers. Once again we only need the two opposing corners. We can find those segments as the intersections of the vertical, horizontal and depth-wise parts of the cuboids.

In code that looks like this (full script):

function corner1(part)
	return part.Position - part.Size / 2
end

function corner2(part)
	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

function numbersIntersection(p11, p12, p21, p22)
	--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} --Entirity of (p21, p22) is contained in (p11, p12)
		end
	end
end

function partFromCorners(corner1, corner2, part)
	local part = part 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 = Vector3.new(mid.X, mid.Y, mid.Z)
	
	return part
end

function volumesIntersection(block1: Part, block2: Part)
	local b1c1, b1c2 = corner1(block1), corner2(block1)
	local b2c1, b2c2 = corner1(block2), corner2(block2)

	local xIntersection = numbersIntersection(b1c1.X, b1c2.X, b2c1.X, b2c2.X)
	local yIntersection = numbersIntersection(b1c1.Y, b1c2.Y, b2c1.Y, b2c2.Y)
	local zIntersection = numbersIntersection(b1c1.Z, b1c2.Z, b2c1.Z, b2c2.Z)

	if #xIntersection == 2 and #yIntersection == 2 and #zIntersection == 2  then
		local corner1 = Vector3.new(xIntersection[1], yIntersection[1], zIntersection[1])
		local corner2 = Vector3.new(xIntersection[2], yIntersection[2], zIntersection[2])
		
		local intersection= block1:Clone()
		partFromCorners(corner1, corner2, intersection)

		return intersection
	end
end

Part 1 results

Testing it I get this:

image

... with this code:
local a, b = game.Workspace.A, game.Workspace.B
local u = volumesIntersection(a, b) 
u.Name = "A∩B"
u.Color = Color3.fromRGB(255, 0, 255)
u.Parent = game.Workspace
4 Likes

Part 2

Problem description

Now that we can calculate A ∩ B, it’s not so difficult to calculate A - B.

Solution in 1D

In the case where there’s no overlap, the result of the operation is to just leave A be, because B doesn’t “take a bite” out of it.

That leaves these 3 cases:

Cases 1 and 2: A is cut into 2 segments, where either the first or last one is deleted.

Case 3: A gets cut into 3 segments where the middle one is deleted.
image

Case 4: A is entirely eaten by B so there are 0 segments left:
image

Solution in 2D

Let’s look at case 1 in 2D:

If we were to think of how we’d apply the 1D solution to figure out the 4 - 1 = 3 rectangles to create in 2D, we’d first cut A into 2 rectangles along one axis, leave one of them be and then cut the other into 2 rectangles on the other axis. This gives us 3 different parts of which 1 must be deleted.

Solution in 3D

To create the parts we need in 3D we use the same logic: cut first on one axis, then another axis, and then the final axis:

image

In this image I first cut the part on the Y axis, then one of the horizontal axes and then the other. So first I turned the blue box that was the entire part into 2 boxes, coloring the top one green. Then I cut the green in two, coloring one of the parts yellow. Then I cut the yellow box in 2 and deleted one of the yellow parts.

Examples

A similar logic applies for case 3 where the middle must be removed. Notice that the 3 cases can be mixed and matched between the 3 axes, e.g.:

Cutting a hole in a part:

image

3 blue segments, middle gets deleted. 3 green segments, middle gets deleted. 0 yellow segments because B completely ate A on that axis.

Cutting a notch into a part:

B only ate one end of A on that axis:

image

1D code

For the same reasons as in part 1, we fix the order of the parameters so they’re always right. Except we don’t swap which segment is which, because A - B ~= B - A.

Swapping
function numbersSub(p11, p12, p21, p22)
	--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)
    ...
end
Then we handle the situations where everything or nothing gets eaten:
function numbersSub(p11, p12, p21, p22)
	--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
    ...
and finally we handle the 3 cases:
function numbersSub(p11, p12, p21, p22)
	--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

3D code

Now we need a way of cutting a block into smaller blocks on any axis. Here's a function that takes a part, an axis to slice along, and points along that axis to slice at:
function slicePart(part: Part, axis: Enum.Axis, points)
	local slices = {}
	
	local cutAxis = Vector3.FromAxis(axis)
	local cutPlane = Vector3.new(1, 1, 1) - cutAxis
	
	local sCorner1 = corner1(part)
	local sCorner2 = sCorner1 * cutAxis + corner2(part) * 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)
	table.insert(slices, partFromCorners(sCorner1, sCorner2, part:Clone()))
	
	return slices
end

It keeps track of two corners to create the slices at. They both start off at one end of the part, but still opposite each other (i.e. as opposite corners on one face of the part). The slice is then “walked along” the axis, first moving sCorner2 to the next point, then creating the slice, then moving sCorner1 up to sCorner2 along the cut axis, kind of like a worm or something.

Aaaaand the logic for slicing `A` and deleting the part inside `B` looks like this:
function volumesSub(block1: Part, block2: Part)
	local b1c1, b1c2 = corner1(block1), corner2(block1)
	local b2c1, b2c2 = corner1(block2), corner2(block2)
	
	local parts = {}
	
	for _, axis in pairs(Enum.Axis:GetEnumItems()) do
		local A = axis.Name
		local points, toDelete = numbersSub(b1c1[A], b1c2[A], b2c1[A], b2c2[A])
		
		--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(block1, axis, points)
		
		--Return all the slices except the ones that are inside block2
		for i = 1, #slices do
			if i == toDelete then continue end
			table.insert(parts, slices[i])
		end
	end
	
	return parts
end

Part 2 results

Testing it like this:
local a, b, c, d = game.Workspace.A, game.Workspace.B, game.Workspace.C, game.Workspace.D
local u = volumesIntersection(a, b) 
u.Name = "A∩B"
u.Color = Color3.fromRGB(255, 0, 255)
u.Parent = game.Workspace

local s = volumesSub(c, d)
for _, part in pairs(s) do
	part.Parent = game.Workspace
	part.Transparency = 0
end
I get these results:

image

image

image

3 Likes

How would I make it so the s parts wont collide with eachother, without re cutting them?. would be highly appreciated :smiley:

image

Ah, looks like a bug :sweat_smile: I’ll take a look when I have time

1 Like

Yeah, there was a bug where it kept reusing the original block to slice each axis, when it should have been using the results of slicing from the previous axis.

Here's a fixed version:
function corner1(part)
	return part.Position - part.Size / 2
end

function corner2(part)
	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} --Entirity 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.
]]
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 = 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.
]]
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)
	local sCorner2 = sCorner1 * cutAxis + corner2(part) * 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)
	table.insert(slices, partFromCorners(sCorner1, sCorner2, part:Clone()))

	return slices
end

--[[
Given two volumes (as Parts), return the intersection of those volumes (as a Part).
Input parts must have no rotation.
]]
function volumesIntersection(block1: Part, block2: Part): Part?
	local b1c1, b1c2 = corner1(block1), corner2(block1)
	local b2c1, b2c2 = corner1(block2), corner2(block2)

	local xDifference = numbersDifference(b1c1.X, b1c2.X, b2c1.X, b2c2.X)
	local yDifference = numbersDifference(b1c1.Y, b1c2.Y, b2c1.Y, b2c2.Y)
	local zDifference = numbersDifference(b1c1.Z, b1c2.Z, b2c1.Z, b2c2.Z)

	--If any axis is non-intersecting, the intersection is empty
	if #xDifference ~= 2 and #yDifference ~= 2 and #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])

	local difference = block1:Clone()
	partFromCorners(corner1, corner2, difference)

	return difference
end

--[[
Given two volumes (as Parts), return the second volume removed from the first volume (as a list of Parts).
Input parts must have no rotation.
]]
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)[A], corner2(block1)[A], corner1(block2)[A], corner2(block2)[A])
		
		if #points == 2 then --Or equivalently if toDelte == 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

return {
	volumesIntersection = volumesIntersection,
	volumesDifference = volumesDifference
}
1 Like