-- Optimized Carve Unions Plugin (spatial indexing + union-local checks)
-- Paste into a Plugin Script in Studio. Select UnionOperation(s) and click the toolbar button.
local serverStorage = game:GetService('ServerStorage')
local Selection = game:GetService("Selection")
local Workspace = game:GetService("Workspace")
-- Settings (tune)
local GRID_SIZE = 0.25 -- voxel size
local PROBE_FACTOR = 0.5
local POSITIVE_THRESHOLD = 0.35
local CORNER_EPS = 1e-4
local SAMPLE_SUBDIV = false -- 3x3x3 per voxel (slow)
local MIN_PART_SIZE = 0.25 -- do not spawn parts smaller than this on any axis
local toolbar = plugin:CreateToolbar("CSG Tools")
local btn = toolbar:CreateButton("Carve Unions (Optimized)", "Voxel-carve selected unions", "rbxassetid://123955089457539", "Carve Unions (Optimized)")
-- helpers
local function clamp(i, a, b) if i < a then return a elseif i > b then return b else return i end end
-- safe wrapper for plugin:Separate
local function safeSeparate(obj)
local ok, res = pcall(function() return plugin:Separate({obj}) end)
if not ok then return nil, res end
return res
end
-- sample points for small fallback checks (used only for heuristic negative detection)
local function samplePointsForPart(part)
if not part or not part:IsA("BasePart") then return {} end
local pts = {}
local size = part.Size
local half = size * 0.5
-- center
table.insert(pts, part.CFrame.Position)
-- face centers
local offs = {
Vector3.new( half.X, 0, 0), Vector3.new(-half.X, 0, 0),
Vector3.new(0, half.Y, 0), Vector3.new(0, -half.Y, 0),
Vector3.new(0, 0, half.Z), Vector3.new(0, 0, -half.Z),
}
for _, off in ipairs(offs) do table.insert(pts, part.CFrame:PointToWorldSpace(off)) end
-- quarter corners
local quarter = Vector3.new(half.X * 0.5, half.Y * 0.5, half.Z * 0.5)
local cornerOffsets = {
Vector3.new( quarter.X, quarter.Y, quarter.Z),
Vector3.new(-quarter.X, quarter.Y, quarter.Z),
Vector3.new( quarter.X, -quarter.Y, quarter.Z),
Vector3.new( quarter.X, quarter.Y, -quarter.Z),
}
for _, off in ipairs(cornerOffsets) do table.insert(pts, part.CFrame:PointToWorldSpace(off)) end
return pts
end
-- separate clone fully and collect basepart clones
local function separateCloneFullyAndCollect(clonedRoot, tempFolder)
local queue = { clonedRoot }
local collected = {}
while #queue > 0 do
local cur = table.remove(queue, 1)
if not cur then break end
local parts, err = safeSeparate(cur)
if not parts then
if cur:IsA("BasePart") then
local okc, clone = pcall(function() return cur:Clone() end)
if okc and clone then clone.Parent = tempFolder; table.insert(collected, clone)
else cur.Parent = tempFolder; table.insert(collected, cur) end
else
warn("Separate failed:", tostring(cur), err)
end
else
for _, p in ipairs(parts) do
if p:IsA("UnionOperation") then
local okc, clone = pcall(function() return p:Clone() end)
if okc and clone then clone.Parent = tempFolder; table.insert(queue, clone) end
elseif p:IsA("BasePart") then
local okc, clone = pcall(function() return p:Clone() end)
if okc and clone then clone.Parent = tempFolder; table.insert(collected, clone) end
end
end
end
end
return collected
end
-- geometry helpers used by core algorithm
local function voxelCornersWorldFromLocal(cframe, localMin, localMax)
local corners = {}
for ix = 0,1 do
for iy = 0,1 do
for iz = 0,1 do
local lx = (ix == 0) and localMin.X or localMax.X
local ly = (iy == 0) and localMin.Y or localMax.Y
local lz = (iz == 0) and localMin.Z or localMax.Z
table.insert(corners, cframe:PointToWorldSpace(Vector3.new(lx, ly, lz)))
end
end
end
return corners
end
local function voxelExtraSamplePointsWorld(cframe, localMin, localMax)
local pts = {}
local fx = (localMax.X + localMin.X) * 0.5
local fy = (localMax.Y + localMin.Y) * 0.5
local fz = (localMax.Z + localMin.Z) * 0.5
local faces = {
Vector3.new(localMin.X, fy, fz),
Vector3.new(localMax.X, fy, fz),
Vector3.new(fx, localMin.Y, fz),
Vector3.new(fx, localMax.Y, fz),
Vector3.new(fx, fy, localMin.Z),
Vector3.new(fx, fy, localMax.Z),
}
for _, lf in ipairs(faces) do table.insert(pts, cframe:PointToWorldSpace(lf)) end
local edges = {
Vector3.new(localMin.X, localMin.Y, fz),
Vector3.new(localMin.X, localMax.Y, fz),
Vector3.new(localMax.X, localMin.Y, fz),
Vector3.new(localMax.X, localMax.Y, fz),
Vector3.new(localMin.X, fy, localMin.Z),
Vector3.new(localMin.X, fy, localMax.Z),
Vector3.new(localMax.X, fy, localMin.Z),
Vector3.new(localMax.X, fy, localMax.Z),
Vector3.new(fx, localMin.Y, localMin.Z),
Vector3.new(fx, localMax.Y, localMin.Z),
Vector3.new(fx, localMin.Y, localMax.Z),
Vector3.new(fx, localMax.Y, localMax.Z),
}
for _, le in ipairs(edges) do table.insert(pts, cframe:PointToWorldSpace(le)) end
if SAMPLE_SUBDIV then
for sx = 0,2 do for sy = 0,2 do for sz = 0,2 do
local lx = localMin.X + (localMax.X - localMin.X) * (sx / 2)
local ly = localMin.Y + (localMax.Y - localMin.Y) * (sy / 2)
local lz = localMin.Z + (localMax.Z - localMin.Z) * (sz / 2)
table.insert(pts, cframe:PointToWorldSpace(Vector3.new(lx, ly, lz)))
end end end
end
return pts
end
local function partWorldAABB(part)
local half = part.Size * 0.5
local localMin = -half
local localMax = half
local corners = voxelCornersWorldFromLocal(part.CFrame, localMin, localMax)
local minv = Vector3.new(1e9, 1e9, 1e9)
local maxv = Vector3.new(-1e9, -1e9, -1e9)
for _, c in ipairs(corners) do
minv = Vector3.new(math.min(minv.X, c.X), math.min(minv.Y, c.Y), math.min(minv.Z, c.Z))
maxv = Vector3.new(math.max(maxv.X, c.X), math.max(maxv.Y, c.Y), math.max(maxv.Z, c.Z))
end
return minv, maxv
end
local function aabbOverlap(minA, maxA, minB, maxB)
if maxA.X < minB.X or minA.X > maxB.X then return false end
if maxA.Y < minB.Y or minA.Y > maxB.Y then return false end
if maxA.Z < minB.Z or minA.Z > maxB.Z then return false end
return true
end
local function worldPointInsidePart(worldPoint, part, eps)
eps = eps or CORNER_EPS
local localP = part.CFrame:PointToObjectSpace(worldPoint)
local hx, hy, hz = part.Size.X * 0.5 + eps, part.Size.Y * 0.5 + eps, part.Size.Z * 0.5 + eps
return math.abs(localP.X) <= hx and math.abs(localP.Y) <= hy and math.abs(localP.Z) <= hz
end
-- cheap union probe on sample points using GetPartBoundsInBox
local function voxelIntersectsUnionSamples(union, sampleWorldPoints)
if not union or not union:IsA("UnionOperation") then return false end
local uSize = union.Size or Vector3.new(1,1,1)
local probe = math.max(0.09, math.max(uSize.X, uSize.Y, uSize.Z) * PROBE_FACTOR)
for _, p in ipairs(sampleWorldPoints) do
local found = Workspace:GetPartBoundsInBox(CFrame.new(p), Vector3.new(probe, probe, probe))
for _, f in ipairs(found) do
if f == union then return true end
end
end
return false
end
-- spawn part, respects MIN_PART_SIZE
local function spawnPartFromUnionLocalBox(union, minLocal, maxLocal, parentFolder)
local size = maxLocal - minLocal
if size.X < MIN_PART_SIZE or size.Y < MIN_PART_SIZE or size.Z < MIN_PART_SIZE then return nil end
local p = Instance.new("Part")
p.Size = size
p.CFrame = union.CFrame * CFrame.new((minLocal + maxLocal) * 0.5)
if union:IsA("BasePart") then
p.Color = union.Color
p.Material = union.Material
p.Transparency = union.Transparency
end
p.Anchored = true
p.CanCollide = true
p.Name = union.Name .. "_slice"
p.Parent = parentFolder or union.Parent
return p
end
-- GREEDY MERGE (unchanged)
local function greedyMergeCells(cells)
if #cells == 0 then return {} end
local map = {}
local minX, minY, minZ = math.huge, math.huge, math.huge
local maxX, maxY, maxZ = -math.huge, -math.huge, -math.huge
for _, c in ipairs(cells) do
local key = string.format("%d_%d_%d", c.x, c.y, c.z)
map[key] = true
minX = math.min(minX, c.x); minY = math.min(minY, c.y); minZ = math.min(minZ, c.z)
maxX = math.max(maxX, c.x); maxY = math.max(maxY, c.y); maxZ = math.max(maxZ, c.z)
end
local visited = {}
local out = {}
for x = minX, maxX do
for y = minY, maxY do
for z = minZ, maxZ do
local key = string.format("%d_%d_%d", x,y,z)
if map[key] and not visited[key] then
local endX = x
while true do
local k = string.format("%d_%d_%d", endX+1, y, z)
if not map[k] or visited[k] then break end
endX = endX + 1
end
local endY = y
local okY = true
while okY do
for xx = x, endX do
local kk = string.format("%d_%d_%d", xx, endY+1, z)
if not map[kk] or visited[kk] then okY = false; break end
end
if okY then endY = endY + 1 end
end
local endZ = z
local okZ = true
while okZ do
for xx = x, endX do
for yy = y, endY do
local kk = string.format("%d_%d_%d", xx, yy, endZ+1)
if not map[kk] or visited[kk] then okZ = false; break end
end
if not okZ then break end
end
if okZ then endZ = endZ + 1 end
end
for xx = x, endX do
for yy = y, endY do
for zz = z, endZ do
visited[string.format("%d_%d_%d", xx,yy,zz)] = true
end
end
end
table.insert(out, { min = Vector3.new(x,y,z), max = Vector3.new(endX+1, endY+1, endZ+1) })
end
end
end
end
return out
end
-- KEY: carve using spatial index (neg map) and union-local AABBs
local function carveUnionGloballyOptimized(union, negatives, gridSize, outFolder)
gridSize = gridSize or GRID_SIZE
local size = union.Size
if size.X < gridSize or size.Y < gridSize or size.Z < gridSize then
spawnPartFromUnionLocalBox(union, -size/2, size/2, outFolder)
return
end
local slicesX = math.floor(size.X / gridSize)
local slicesY = math.floor(size.Y / gridSize)
local slicesZ = math.floor(size.Z / gridSize)
if slicesX <= 0 or slicesY <= 0 or slicesZ <= 0 then
spawnPartFromUnionLocalBox(union, -size/2, size/2, outFolder)
return
end
local gridExtentX = slicesX * gridSize
local gridExtentY = slicesY * gridSize
local gridExtentZ = slicesZ * gridSize
local startX = -gridExtentX / 2
local startY = -gridExtentY / 2
local startZ = -gridExtentZ / 2
-- 1) Precompute negatives in union-local coordinates and build occupancy map (voxel -> negatives list)
local negLocalAABBs = {} -- negPart -> {min = Vector3, max = Vector3}
local occupancy = {} -- keyed by "ix_iy_iz" -> {neg, neg, ...}
for _, neg in ipairs(negatives) do
if neg and neg:IsA("BasePart") then
-- compute neg corners world -> transform into union local
local corners = voxelCornersWorldFromLocal(neg.CFrame, -neg.Size*0.5, neg.Size*0.5)
local localMin = Vector3.new(1e9,1e9,1e9)
local localMax = Vector3.new(-1e9,-1e9,-1e9)
for _, wc in ipairs(corners) do
-- union CFrame: PointToObjectSpace maps world -> union-local
local lc = union.CFrame:PointToObjectSpace(wc)
localMin = Vector3.new(math.min(localMin.X, lc.X), math.min(localMin.Y, lc.Y), math.min(localMin.Z, lc.Z))
localMax = Vector3.new(math.max(localMax.X, lc.X), math.max(localMax.Y, lc.Y), math.max(localMax.Z, lc.Z))
end
negLocalAABBs[neg] = { min = localMin, max = localMax }
-- compute which voxels this negative overlaps (in union-local voxel indices)
local ixMin = math.floor((localMin.X - startX) / gridSize)
local iyMin = math.floor((localMin.Y - startY) / gridSize)
local izMin = math.floor((localMin.Z - startZ) / gridSize)
local ixMax = math.floor((localMax.X - startX) / gridSize)
local iyMax = math.floor((localMax.Y - startY) / gridSize)
local izMax = math.floor((localMax.Z - startZ) / gridSize)
ixMin = clamp(ixMin, 0, slicesX-1); iyMin = clamp(iyMin, 0, slicesY-1); izMin = clamp(izMin, 0, slicesZ-1)
ixMax = clamp(ixMax, 0, slicesX-1); iyMax = clamp(iyMax, 0, slicesY-1); izMax = clamp(izMax, 0, slicesZ-1)
for ix = ixMin, ixMax do
for iy = iyMin, iyMax do
for iz = izMin, izMax do
local key = ix.."_"..iy.."_"..iz
local list = occupancy[key]
if not list then
list = {}
occupancy[key] = list
end
table.insert(list, neg)
end
end
end
end
end
-- 2) Iterate voxels; only check negatives listed in occupancy map for that voxel.
local keepCells = {}
local smallVoxelCenterOnly = (gridSize <= 0.25) -- heuristic: small voxels use center-only sampling
for ix = 0, slicesX-1 do
for iy = 0, slicesY-1 do
for iz = 0, slicesZ-1 do
local localMin = Vector3.new(startX + ix*gridSize, startY + iy*gridSize, startZ + iz*gridSize)
local localMax = localMin + Vector3.new(gridSize, gridSize, gridSize)
-- build minimal sample set:
local samples = {}
local centerLocal = (localMin + localMax) * 0.5
table.insert(samples, union.CFrame:PointToWorldSpace(centerLocal))
if not smallVoxelCenterOnly then
-- add corners and faces/edges (fewer than before)
for _, v in ipairs(voxelCornersWorldFromLocal(union.CFrame, localMin, localMax)) do table.insert(samples, v) end
for _, v in ipairs(voxelExtraSamplePointsWorld(union.CFrame, localMin, localMax)) do table.insert(samples, v) end
end
-- quick test: does this voxel intersect the union at all?
local intersectsUnion = voxelIntersectsUnionSamples(union, samples)
if not intersectsUnion then
-- skip voxel entirely
else
-- compute occupancy key and candidate negatives
local key = ix.."_"..iy.."_"..iz
local candidates = occupancy[key] -- could be nil (fast common case)
local intersectsNeg = false
if candidates then
-- compute voxel AABB in union-local (fast)
local vmin = localMin
local vmax = localMax
-- test candidates only
for _, neg in ipairs(candidates) do
local negA = negLocalAABBs[neg]
if negA and aabbOverlap(vmin, vmax, negA.min, negA.max) then
-- fine tests: check any sample point in negative (use part.CFrame:PointToObjectSpace)
for _, sp in ipairs(samples) do
if worldPointInsidePart(sp, neg) then intersectsNeg = true; break end
end
if intersectsNeg then break end
-- also test if neg corners fall inside voxel (covers tiny negatives)
local negCorners = voxelCornersWorldFromLocal(neg.CFrame, -neg.Size * 0.5, neg.Size * 0.5)
for _, nc in ipairs(negCorners) do
-- nc is world; transform to union-local and compare to vmin..vmax
local nloc = union.CFrame:PointToObjectSpace(nc)
if nloc.X >= vmin.X and nloc.X <= vmax.X and nloc.Y >= vmin.Y and nloc.Y <= vmax.Y and nloc.Z >= vmin.Z and nloc.Z <= vmax.Z then
intersectsNeg = true; break
end
end
if intersectsNeg then break end
end
end
end
if not intersectsNeg then
table.insert(keepCells, { x = ix, y = iy, z = iz })
end
end
end
end
end
-- 3) Merge and spawn
local merged = greedyMergeCells(keepCells)
for _, m in ipairs(merged) do
local minLocal = Vector3.new(startX + m.min.X * gridSize, startY + m.min.Y * gridSize, startZ + m.min.Z * gridSize)
local maxLocal = Vector3.new(startX + m.max.X * gridSize, startY + m.max.Y * gridSize, startZ + m.max.Z * gridSize)
spawnPartFromUnionLocalBox(union, minLocal, maxLocal, outFolder)
end
-- rim slabs both sides (respect MIN_PART_SIZE)
local gridExtentX, gridExtentY, gridExtentZ = slicesX * gridSize, slicesY * gridSize, slicesZ * gridSize
local marginX = size.X - gridExtentX
local marginY = size.Y - gridExtentY
local marginZ = size.Z - gridExtentZ
-- left / right along X
if (marginX/2) >= MIN_PART_SIZE then
spawnPartFromUnionLocalBox(union, Vector3.new(-size.X/2, -size.Y/2, -size.Z/2), Vector3.new(startX, size.Y/2, size.Z/2), outFolder)
spawnPartFromUnionLocalBox(union, Vector3.new(startX + gridExtentX, -size.Y/2, -size.Z/2), Vector3.new(size.X/2, size.Y/2, size.Z/2), outFolder)
end
-- bottom / top along Y
if (marginY/2) >= MIN_PART_SIZE then
spawnPartFromUnionLocalBox(union, Vector3.new(-size.X/2, -size.Y/2, -size.Z/2), Vector3.new(size.X/2, startY, size.Z/2), outFolder)
spawnPartFromUnionLocalBox(union, Vector3.new(-size.X/2, startY + gridExtentY, -size.Z/2), Vector3.new(size.X/2, size.Y/2, size.Z/2), outFolder)
end
-- back / front along Z
if (marginZ/2) >= MIN_PART_SIZE then
spawnPartFromUnionLocalBox(union, Vector3.new(-size.X/2, -size.Y/2, -size.Z/2), Vector3.new(size.X/2, size.Y/2, startZ), outFolder)
spawnPartFromUnionLocalBox(union, Vector3.new(-size.X/2, -size.Y/2, startZ + gridExtentZ), Vector3.new(size.X/2, size.Y/2, size.Z/2), outFolder)
end
end
-- process a union: clone -> separate -> collect negatives -> carve
local function processUnion(union)
if not union or not union:IsA("UnionOperation") then return end
print("Processing union:", union:GetFullName())
local temp = Instance.new("Folder")
temp.Name = "_separate_temp_" .. union.Name .. "_" .. tostring(math.random(1,9999))
temp.Parent = Workspace
local okClone, cloneUnion = pcall(function() return union:Clone() end)
if not okClone or not cloneUnion then warn("Clone failed", union.Name); if temp.Parent then temp:Destroy() end; return end
cloneUnion.Parent = temp
local clones = separateCloneFullyAndCollect(cloneUnion, temp)
print("Separated parts count (from clone):", #clones)
-- collect negatives by name
local negatives = {}
for _, c in ipairs(clones) do
if c and c:IsA("BasePart") and c.Name == "NegativePart" then
table.insert(negatives, c)
end
end
print(("Detected %d negative parts"):format(#negatives))
-- heuristic fallback if none named negative
if #negatives == 0 then
local heur = {}
for _, piece in ipairs(clones) do
local pts = samplePointsForPart(piece)
local inside = 0
for _, sp in ipairs(pts) do
if voxelIntersectsUnionSamples(union, {sp}) then inside = inside + 1 end
end
local frac = inside / math.max(1, #pts)
if frac < POSITIVE_THRESHOLD then table.insert(heur, piece) end
end
negatives = heur
print(("Heuristic negatives found: %d"):format(#negatives))
end
local out = Instance.new("Folder")
out.Name = union.Name .. "_carved"
out.Parent = union.Parent
-- carve with optimized function
carveUnionGloballyOptimized(union, negatives, GRID_SIZE, out)
-- cleanup
if union and union.Parent then union:Destroy() end
if temp and temp.Parent then
for _, c in ipairs(temp:GetChildren()) do c:Destroy() end
temp:Destroy()
end
print("Done carving:", union.Name)
end
-- Use serverStorage float for grid size if present (your existing mechanism)
local function gridSizeCheck()
if not serverStorage:FindFirstChild('Plugins') then
local folder = Instance.new('Folder')
folder.Name = 'Plugins'
folder.Parent = serverStorage
end
if serverStorage.Plugins:FindFirstChild('GRID_SIZE_UNUNIONIFY') then
GRID_SIZE = serverStorage.Plugins.GRID_SIZE_UNUNIONIFY.Value
else
local floatValue = Instance.new('NumberValue')
floatValue.Name = 'GRID_SIZE_UNUNIONIFY'
floatValue.Parent = serverStorage.Plugins
floatValue.Value = GRID_SIZE
end
end
-- toolbar click
btn.Click:Connect(function()
gridSizeCheck()
local sel = Selection:Get()
if #sel == 0 then warn("Select union(s) in Explorer") return end
for _, obj in ipairs(sel) do
if obj and obj:IsA("UnionOperation") then
processUnion(obj)
-- small yield so Studio remains responsive for big jobs
task.wait(0.05)
else
warn("Skipping non-union:", tostring(obj))
end
end
print("All selected unions processed.")
end)