Updated Negate grid like blocks without unions

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,
}
1 Like