--- lua-tikz3dtools-geometry.lua --- Computational geometry utilities for the lua-tikz3dtools package. --- Intersection tests, occlusion sorting, partitioning, and spatial indexing. local Vector local Matrix local Geometry = {} --- Set class references (called after all modules are loaded). function Geometry._set_classes(V, M) Vector = V Matrix = M end -- ================================================================ -- Point-level geometry (attached to Vector via methods below) -- ================================================================ --- Check if two homogeneous vectors intersect (are the same point) --- @param self Vector --- @param other Vector --- @return boolean function Geometry.hpoint_point_intersecting(self, other) return self:hdistance(other) < 1e-12 end --- Check if a homogeneous point is in the triangular prism defined by triangle T --- @param self Vector --- @param T Matrix --- @return boolean function Geometry.hpoint_in_triangular_prism(self, T) local A = Vector:_new(T[1]) local B = Vector:_new(T[2]) local C = Vector:_new(T[3]) local v0 = C:hsub(A) local v1 = B:hsub(A) local v2 = self:hsub(A) local d00 = v0:hinner(v0) local d01 = v0:hinner(v1) local d11 = v1:hinner(v1) local d20 = v2:hinner(v0) local d21 = v2:hinner(v1) local denom = d00 * d11 - d01 * d01 local v = (d11 * d20 - d01 * d21) / denom local w = (d00 * d21 - d01 * d20) / denom local u = 1 - v - w local eps = 1e-12 return u >= -3*eps and v >= -3*eps and w >= -3*eps end --- Check if a homogeneous point intersects a homogeneous triangle --- @param self Vector --- @param T Matrix --- @return boolean function Geometry.hpoint_triangle_intersecting(self, T) local Tbasis = T:deep_copy():hto_basis() local S = self:horthogonal_projection_onto_plane(Tbasis) return Geometry.hpoint_in_triangular_prism(S, T) and Geometry.hpoint_point_intersecting(self, S) end --- Check occlusion sort of a homogeneous point with a homogeneous point --- @param self Vector --- @param P Vector --- @return boolean|nil function Geometry.hpoint_point_occlusion_sort(self, P) if not Geometry.hpoint_point_intersecting(self, P) then if Geometry.hpoint_point_intersecting( Vector:_new{self[1], self[2], 0, 1}, Vector:_new{P[1], P[2], 0, 1} ) then return self[3] < P[3] end end return nil end --- Check collinearity of two homogeneous vectors --- @param self Vector --- @param other Vector --- @return boolean function Geometry.hcollinear(self, other) local cross = self:hhypercross(other) return cross:hnorm() < 1e-12 end --- Check opposite direction of two homogeneous vectors --- @param self Vector --- @param other Vector --- @return boolean function Geometry.hoppositely_directed(self, other) return self:hinner(other) < 0 end --- Check occlusion sort of a homogeneous point with a homogeneous line segment --- @param self Vector --- @param L Matrix --- @return boolean|nil function Geometry.hpoint_line_segment_occlusion_sort(self, L) local LA = L:hto_basis() local LO = Vector:_new(LA[1]) local OP = self:hsub(LO) local LU = Vector:_new(LA[2]) local proj = OP:hproject_onto(LU) local true_proj = LO:hadd(proj) local F = Vector:_new{self[1], self[2], 0, 1} local G = Vector:_new{true_proj[1], true_proj[2], 0, 1} if Geometry.hpoint_point_intersecting(F, G) then local temp = 1 if Geometry.hoppositely_directed(proj, LU) then temp = -1 end local test = temp * proj:hnorm() / LU:hnorm() local eps = 1e-12 if (eps < test and test < 1 - eps) then return Geometry.hpoint_point_occlusion_sort(self, true_proj) end end return nil end --- Check occlusion sort of a homogeneous point with a homogeneous triangle --- @param self Vector --- @param T Matrix --- @return boolean|nil function Geometry.hpoint_triangle_occlusion_sort(self, T) local P1 = Vector:_new{self[1], self[2], 0, 1} local T1 = Vector:_new{T[1][1], T[1][2], 0, 1} local T2 = Vector:_new{T[2][1], T[2][2], 0, 1} local T3 = Vector:_new{T[3][1], T[3][2], 0, 1} local TP = Matrix:_new{ {T1[1], T1[2], 0, 1}, {T2[1], T2[2], 0, 1}, {T3[1], T3[2], 0, 1} } local Ta = Vector:_new(T[1]) local Tb = Vector:_new(T[2]) local Tc = Vector:_new(T[3]) if Geometry.hpoint_point_intersecting(self, Ta) then return nil end if Geometry.hpoint_point_intersecting(self, Tb) then return nil end if Geometry.hpoint_point_intersecting(self, Tc) then return nil end local eps = 1e-12 if Geometry.hpoint_in_triangular_prism(P1, TP) then local vu = Tb:hsub(Ta) local vv = Tc:hsub(Ta) local sol = Matrix:_new{ {vu[1], vu[2]}, {vv[1], vv[2]}, {P1:hsub(Ta)[1], P1:hsub(Ta)[2]} }:column_reduction() local t, s if sol then t, s = sol[1], sol[2] else return nil end if ( -eps < t and t < 1 + eps and -eps < s and s < 1 + eps ) then local a = Ta:hadd(vu:hscale(t)):hadd(vv:hscale(s)) return Geometry.hpoint_point_occlusion_sort(self, a) else return nil end end return nil end -- ================================================================ -- Line-segment level geometry (attached to Matrix via methods below) -- ================================================================ --- Unified line-segment / point intersection test. --- @param self Matrix 2-row line segment --- @param point Vector The point to test --- @param boundary_inclusive boolean If true, endpoints count as intersecting --- @return boolean function Geometry.hline_segment_point_intersecting(self, point, boundary_inclusive) local LA = self:hto_basis() local LO = Vector:_new(LA[1]) local LU = Vector:_new(LA[2]) local LOP = point:hsub(LO) local rhs = LOP:hproject_onto(LU) local orth = LO:hadd(rhs) if not Geometry.hpoint_point_intersecting(orth, point) then return false end local sol = Matrix:_new{ {LU[1], LU[2], LU[3]}, {0, 0, 0}, {0, 0, 0}, {rhs[1], rhs[2], rhs[3]} }:column_reduction() if sol == nil then return false end local t = sol[1] local eps = 1e-12 if boundary_inclusive then return -eps < t and t < 1 + eps else return eps < t and t < 1 - eps end end --- Partition a line segment by a point --- @param self Matrix --- @param point Vector --- @return table|nil function Geometry.hpartition_line_segment_by_point(self, point) if Geometry.hline_segment_point_intersecting(self, point, false) then return { Matrix:_new{self[1], point:to_table()}, Matrix:_new{point:to_table(), self[2]} } end return nil end --- Compute the intersection of two lines (basis form) --- @param self Matrix --- @param line Matrix --- @return Vector|nil function Geometry.hline_line_intersection(self, line) local O1 = Vector:_new(self[1]) local U1 = Vector:_new(self[2]) local O2 = Vector:_new(line[1]) local U2 = Vector:_new(line[2]) local rhs = O2:hsub(O1) local sol = Matrix:_new{ {U1[1], U1[2], U1[3]}, {-U2[1], -U2[2], -U2[3]}, {0, 0, 0}, {rhs[1], rhs[2], rhs[3]} }:column_reduction() if sol == nil then return nil end return O1:hadd(U1:hscale(sol[1])) end --- Compute the intersection of two line segments --- @param self Matrix --- @param line Matrix --- @return Vector|nil function Geometry.hline_segment_line_segment_intersection(self, line) local L1A = self:hto_basis() local L2A = line:hto_basis() local I = Geometry.hline_line_intersection(L1A, L2A) if I == nil then return nil end if Geometry.hline_segment_point_intersecting(self, I, false) and Geometry.hline_segment_point_intersecting(line, I, false) then return I end return nil end --- Compute the intersection of a line and a plane (both in basis form) --- @param self Matrix line basis --- @param plane Matrix plane basis --- @return Vector|nil function Geometry.hline_plane_intersection(self, plane) local LO = Vector:_new(self[1]) local LU = Vector:_new(self[2]) local TO = Vector:_new(plane[1]) local TU = Vector:_new(plane[2]) local TV = Vector:_new(plane[3]) local rhs = TO:hsub(LO) local sol = Matrix:_new{ {LU[1], LU[2], LU[3]}, {-TU[1], -TU[2], -TU[3]}, {-TV[1], -TV[2], -TV[3]}, {rhs[1], rhs[2], rhs[3]} }:column_reduction() if sol == nil then return nil end return LO:hadd(LU:hscale(sol[1])) end --- Compute the intersection of a line segment and a triangle --- @param self Matrix line segment (2 rows) --- @param tri Matrix triangle (3 rows) --- @return Vector|nil function Geometry.hline_segment_triangle_intersection(self, tri) -- skip if any endpoints coincide with triangle vertices for _, P1 in ipairs(self) do for _, P2 in ipairs(tri) do if Geometry.hpoint_point_intersecting(Vector:_new(P1), Vector:_new(P2)) then return nil end end end local I = Geometry.hline_plane_intersection(self:hto_basis(), tri:hto_basis()) if I == nil then return nil end if not Geometry.hline_segment_point_intersecting(self, I, true) then return nil end if not Geometry.hpoint_triangle_intersecting(I, tri) then return nil end return I end --- Compute the intersection of two triangles --- @param self Matrix --- @param tri Matrix --- @return Matrix|nil 2-row intersection segment, or nil function Geometry.htriangle_triangle_intersections(self, tri) local edges1 = { Matrix:_new{self[1], self[2]}, Matrix:_new{self[2], self[3]}, Matrix:_new{self[3], self[1]} } local edges2 = { Matrix:_new{tri[1], tri[2]}, Matrix:_new{tri[2], tri[3]}, Matrix:_new{tri[3], tri[1]} } local points = {} local function add_unique(point) for _, p in ipairs(points) do if Geometry.hpoint_point_intersecting(point, p) then return nil end end table.insert(points, point) end for _, edge in ipairs(edges1) do local I = Geometry.hline_segment_triangle_intersection(edge, tri) if I ~= nil then add_unique(I) end end for _, edge in ipairs(edges2) do local I = Geometry.hline_segment_triangle_intersection(edge, self) if I ~= nil then add_unique(I) end end if #points ~= 2 then return nil end local ans = {} for _, p in ipairs(points) do table.insert(ans, p:to_table()) end return Matrix:_new(ans) end --- Partition a line segment by another line segment --- @param self Matrix --- @param line Matrix --- @return table|nil function Geometry.hpartition_line_segment_by_line_segment(self, line) local I = Geometry.hline_segment_line_segment_intersection(self, line) if I ~= nil then return Geometry.hpartition_line_segment_by_point(self, I) end return nil end --- Partition a line segment by a triangle --- @param self Matrix --- @param tri Matrix --- @return table|nil function Geometry.hpartition_line_segment_by_triangle(self, tri) local I = Geometry.hline_segment_triangle_intersection(self, tri) if I ~= nil then return Geometry.hpartition_line_segment_by_point(self, I) end return nil end --- Partition a triangle by another triangle --- @param self Matrix --- @param tri Matrix --- @return table|nil list of sub-triangle Matrices function Geometry.hpartition_triangle_by_triangle(self, tri) local I = Geometry.htriangle_triangle_intersections(self, tri) if I == nil then return nil end local IA = I:hto_basis() local edges1 = { Matrix:_new{self[1], self[2]}, Matrix:_new{self[2], self[3]}, Matrix:_new{self[3], self[1]} } local bases1 = {} for i, edge in ipairs(edges1) do bases1[i] = edge:hto_basis() end local points = {} local function add_point(point) for _, p in ipairs(points) do if Geometry.hpoint_point_intersecting(point, p) then return nil end end table.insert(points, point) end local non_intersecting = nil for i, edge_basis in ipairs(bases1) do local int = Geometry.hline_line_intersection(IA, edge_basis) if int ~= nil and Geometry.hline_segment_point_intersecting(edges1[i], int, false) then add_point(int) else non_intersecting = i end end if #points ~= 2 then return nil end local quad = {} local tri1 local A, B = points[1], points[2] table.insert(quad, A) table.insert(quad, B) if non_intersecting == 1 then table.insert(quad, edges1[1][1]) table.insert(quad, edges1[1][2]) tri1 = Matrix:_new{A:to_table(), B:to_table(), edges1[2][2]} elseif non_intersecting == 2 then table.insert(quad, edges1[2][1]) table.insert(quad, edges1[2][2]) tri1 = Matrix:_new{A:to_table(), B:to_table(), edges1[3][2]} elseif non_intersecting == 3 then table.insert(quad, edges1[3][1]) table.insert(quad, edges1[3][2]) tri1 = Matrix:_new{A:to_table(), B:to_table(), edges1[1][2]} else return nil end quad = Matrix:_new(quad):hcentroid_sort() local Q1 = Vector:_new(quad[1]) local Q2 = Vector:_new(quad[2]) local Q3 = Vector:_new(quad[3]) local Q4 = Vector:_new(quad[4]) if Q1:hdistance(Q3) > Q2:hdistance(Q4) then return { tri1, Matrix:_new{Q2:to_table(), Q1:to_table(), Q4:to_table()}, Matrix:_new{Q2:to_table(), Q3:to_table(), Q4:to_table()} } else return { tri1, Matrix:_new{Q1:to_table(), Q2:to_table(), Q3:to_table()}, Matrix:_new{Q3:to_table(), Q4:to_table(), Q1:to_table()} } end end --- Partition a triangle by vertical edge-planes of another triangle. --- @param self Matrix --- @param tri Matrix --- @return table|nil function Geometry.hpartition_triangle_by_edge_planes(self, tri) local z_min = math.min(self[1][3], self[2][3], self[3][3]) local z_max = math.max(self[1][3], self[2][3], self[3][3]) local z_margin = math.max((z_max - z_min) * 0.1, 1e-6) z_min = z_min - z_margin z_max = z_max + z_margin local cutting_tris = {} local edges = { {tri[1], tri[2]}, {tri[2], tri[3]}, {tri[3], tri[1]} } for _, edge in ipairs(edges) do local A = edge[1] local B = edge[2] if math.abs(A[1] - B[1]) < 1e-12 and math.abs(A[2] - B[2]) < 1e-12 then goto continue_edge end local BL = {A[1], A[2], z_min, 1} local BR = {B[1], B[2], z_min, 1} local TR = {B[1], B[2], z_max, 1} local TL = {A[1], A[2], z_max, 1} table.insert(cutting_tris, Matrix:_new{BL, BR, TR}) table.insert(cutting_tris, Matrix:_new{BL, TR, TL}) ::continue_edge:: end local pieces = {self:deep_copy()} for _, cutter in ipairs(cutting_tris) do local new_pieces = {} for _, piece in ipairs(pieces) do local parts = Geometry.hpartition_triangle_by_triangle(piece, cutter) if parts ~= nil then for _, part in ipairs(parts) do table.insert(new_pieces, part) end else table.insert(new_pieces, piece) end end pieces = new_pieces end if #pieces > 1 then return pieces end return nil end -- ================================================================ -- Occlusion sorting between simplices -- ================================================================ --- line-segment vs line-segment occlusion function Geometry.hline_segment_line_segment_occlusion_sort(self, L) local L1A = Vector:_new{self[1][1], self[1][2], 0, 1} local L1B = Vector:_new{self[2][1], self[2][2], 0, 1} local L2A = Vector:_new{L[1][1], L[1][2], 0, 1} local L2B = Vector:_new{L[2][1], L[2][2], 0, 1} local L1_dir = L1B:hsub(L1A) local L2_dir = L2B:hsub(L2A) local RL1A = Vector:_new(self[1]) local RL1B = Vector:_new(self[2]) local RL2A = Vector:_new(L[1]) local RL2B = Vector:_new(L[2]) local eps = 1e-12 if not Geometry.hcollinear(L1_dir, L2_dir) then local sol = Matrix:_new{ {L1_dir[1], L1_dir[2]}, {-L2_dir[1], -L2_dir[2]}, {L2A[1] - L1A[1], L2A[2] - L1A[2]} }:column_reduction() local t, s if sol ~= nil then t, s = sol[1], sol[2] else return nil end if (eps < t and t < 1 - eps and eps < s and s < 1 - eps) then local RL1I = RL1A:hadd((RL1B:hsub(RL1A)):hscale(t)) local RL2I = RL2A:hadd((RL2B:hsub(RL2A)):hscale(s)) return Geometry.hpoint_point_occlusion_sort(RL1I, RL2I) end else local M1 = Geometry.hpoint_line_segment_occlusion_sort(RL1A, L) if M1 ~= nil then return M1 end local M2 = Geometry.hpoint_line_segment_occlusion_sort(RL1B, L) if M2 ~= nil then return M2 end local M3 = Geometry.hpoint_line_segment_occlusion_sort(RL2A, self) if M3 ~= nil then return not M3 end local M4 = Geometry.hpoint_line_segment_occlusion_sort(RL2B, self) if M4 ~= nil then return not M4 end end return nil end --- line-segment vs triangle occlusion function Geometry.hline_segment_triangle_occlusion_sort(self, T) local points1 = { Vector:_new(self[1]), Vector:_new(self[2]) } for _, p1 in ipairs(points1) do local A = Geometry.hpoint_triangle_occlusion_sort(p1, T) if A ~= nil then return A end end local edges2 = { Matrix:_new{T[1], T[2]}, Matrix:_new{T[2], T[3]}, Matrix:_new{T[3], T[1]} } for _, e2 in ipairs(edges2) do local A = Geometry.hline_segment_line_segment_occlusion_sort(self, e2) if A ~= nil then return A end end return nil end --- triangle vs triangle occlusion function Geometry.htriangle_triangle_occlusion_sort(self, T) local edges1 = { Matrix:_new{self[1], self[2]}, Matrix:_new{self[2], self[3]}, Matrix:_new{self[3], self[1]} } for _, e1 in ipairs(edges1) do local A = Geometry.hline_segment_triangle_occlusion_sort(e1, T) if A ~= nil then return A end end local points1 = { Vector:_new(self[1]), Vector:_new(self[2]), Vector:_new(self[3]) } for _, p1 in ipairs(points1) do local A = Geometry.hpoint_triangle_occlusion_sort(p1, T) if A ~= nil then return A end end local points2 = { Vector:_new(T[1]), Vector:_new(T[2]), Vector:_new(T[3]) } for _, p2 in ipairs(points2) do local A = Geometry.hpoint_triangle_occlusion_sort(p2, self) if A ~= nil then return not A end end return nil end --- Top-level occlusion sort dispatcher --- @param S1 table simplex record --- @param S2 table simplex record --- @return boolean|nil function Geometry.occlusion_sort_simplices(S1, S2) if S1.type == "point" and S2.type == "point" then return Geometry.hpoint_point_occlusion_sort(S1.simplex, S2.simplex) elseif S1.type == "point" and S2.type == "line segment" then return Geometry.hpoint_line_segment_occlusion_sort(S1.simplex, S2.simplex) elseif S1.type == "line segment" and S2.type == "point" then local A = Geometry.hpoint_line_segment_occlusion_sort(S2.simplex, S1.simplex) if A ~= nil then return not A end elseif S1.type == "point" and S2.type == "triangle" then return Geometry.hpoint_triangle_occlusion_sort(S1.simplex, S2.simplex) elseif S1.type == "triangle" and S2.type == "point" then local A = Geometry.hpoint_triangle_occlusion_sort(S2.simplex, S1.simplex) if A ~= nil then return not A end elseif S1.type == "line segment" and S2.type == "line segment" then return Geometry.hline_segment_line_segment_occlusion_sort(S1.simplex, S2.simplex) elseif S1.type == "line segment" and S2.type == "triangle" then local A = Geometry.hline_segment_triangle_occlusion_sort(S1.simplex, S2.simplex) if A == nil then return nil else return A end elseif S1.type == "triangle" and S2.type == "line segment" then local A = Geometry.hline_segment_triangle_occlusion_sort(S2.simplex, S1.simplex) if A == nil then return nil else return not A end elseif S1.type == "triangle" and S2.type == "triangle" then return Geometry.htriangle_triangle_occlusion_sort(S1.simplex, S2.simplex) end end -- ================================================================ -- 2D grid spatial index for bbox overlap queries -- ================================================================ --- Build a uniform 2D grid index from a list of simplex records. --- Each record must have a .bbox2 field (pre-computed). --- @param simplices table list of simplex records --- @param cell_size number grid cell width/height --- @return table grid { cells = {}, cell_size = N, ... } function Geometry.build_grid(simplices, cell_size) local cells = {} local function key(cx, cy) return cx .. "," .. cy end for i, s in ipairs(simplices) do local bb = s.bbox2 if bb then local cx0 = math.floor(bb.min[1] / cell_size) local cy0 = math.floor(bb.min[2] / cell_size) local cx1 = math.floor(bb.max[1] / cell_size) local cy1 = math.floor(bb.max[2] / cell_size) for cx = cx0, cx1 do for cy = cy0, cy1 do local k = key(cx, cy) if not cells[k] then cells[k] = {} end table.insert(cells[k], i) end end end end return { cells = cells, cell_size = cell_size, key = key } end --- Iterate over candidate pairs that share at least one grid cell. --- Returns a set of {i,j} pairs (i < j) as keys in a table. --- @param grid table --- @return table set of "i,j" keys function Geometry.grid_candidate_pairs(grid) local pairs_seen = {} for _, bucket in pairs(grid.cells) do for a = 1, #bucket do for b = a + 1, #bucket do local i, j = bucket[a], bucket[b] if i > j then i, j = j, i end local k = i .. "," .. j pairs_seen[k] = { i, j } end end end return pairs_seen end -- ================================================================ -- SCC-based occlusion sort with spatial indexing -- ================================================================ --- Strongly connected components sort for simplices based on occlusion. --- Uses a 2D grid to reduce pair comparisons, Tarjan's SCC to detect cycles, --- and edge-plane partitioning to break them. --- @param simplices table list of simplex records (each must have .bbox2) --- @param depth number|nil --- @return table sorted list function Geometry.scc(simplices, depth) depth = depth or 0 local max_depth = 8 local n = #simplices -- Estimate cell size from average bbox diagonal local total_diag = 0 local diag_count = 0 for _, s in ipairs(simplices) do if s.bbox2 then local dx = s.bbox2.max[1] - s.bbox2.min[1] local dy = s.bbox2.max[2] - s.bbox2.min[2] total_diag = total_diag + math.sqrt(dx*dx + dy*dy) diag_count = diag_count + 1 end end local cell_size = (diag_count > 0) and (total_diag / diag_count * 2) or 1.0 if cell_size < 1e-6 then cell_size = 1.0 end local grid = Geometry.build_grid(simplices, cell_size) local candidate_pairs = Geometry.grid_candidate_pairs(grid) local adj = {} for i = 1, n do adj[i] = {} end for _, pair in pairs(candidate_pairs) do local i, j = pair[1], pair[2] local si, sj = simplices[i], simplices[j] if si.type ~= "label" and sj.type ~= "label" and si.type ~= "point" and sj.type ~= "point" then -- Use cached bboxes for the overlap check if si.simplex:bboxes_overlap2(sj.simplex, si.bbox2, sj.bbox2) then local cmp = Geometry.occlusion_sort_simplices(si, sj) if cmp == true then table.insert(adj[i], j) elseif cmp == false then table.insert(adj[j], i) end end end end -- Tarjan's SCC local index_counter = 0 local stack = {} local on_stack = {} local indices = {} local lowlinks = {} local components = {} local function strongconnect(v) indices[v] = index_counter lowlinks[v] = index_counter index_counter = index_counter + 1 table.insert(stack, v) on_stack[v] = true for _, w in ipairs(adj[v]) do if indices[w] == nil then strongconnect(w) lowlinks[v] = math.min(lowlinks[v], lowlinks[w]) elseif on_stack[w] then lowlinks[v] = math.min(lowlinks[v], indices[w]) end end if lowlinks[v] == indices[v] then local component = {} while true do local w = table.remove(stack) on_stack[w] = false table.insert(component, w) if w == v then break end end table.insert(components, component) end end for i = 1, n do if indices[i] == nil then strongconnect(i) end end -- Cycle resolution local has_cycle = false for _, comp in ipairs(components) do if #comp > 1 then has_cycle = true; break end end if has_cycle and depth < max_depth then local to_remove = {} local to_add = {} for _, comp in ipairs(components) do if #comp > 1 then local split_done = false for ci = 1, #comp do if split_done then break end local idx = comp[ci] if simplices[idx].type == "triangle" and not to_remove[idx] then for cj = 1, #comp do if cj ~= ci then local other_idx = comp[cj] if simplices[other_idx].type == "triangle" then local parts = Geometry.hpartition_triangle_by_edge_planes( simplices[idx].simplex, simplices[other_idx].simplex) if parts ~= nil then to_remove[idx] = true local meta = {} for k, v in pairs(simplices[idx]) do if k ~= "simplex" and k ~= "type" and k ~= "bbox2" then meta[k] = v end end for _, part in ipairs(parts) do local ns = { simplex = part, type = "triangle", bbox2 = part:get_bbox2() } for k, v in pairs(meta) do ns[k] = v end table.insert(to_add, ns) end split_done = true break end end end end end end end end if next(to_remove) then local new_simplices = {} for i, s in ipairs(simplices) do if not to_remove[i] then table.insert(new_simplices, s) end end for _, s in ipairs(to_add) do table.insert(new_simplices, s) end return Geometry.scc(new_simplices, depth + 1) end end -- Topological sort (DFS) local visited = {} local sorted = {} local function visit(u) if visited[u] then return end visited[u] = true for _, v in ipairs(adj[u]) do visit(v) end table.insert(sorted, 1, simplices[u]) end for i = 1, n do visit(i) end return sorted end -- ================================================================ -- Partition simplices by parents (with screen-space overlap check) -- ================================================================ --- Partition simplices by their parents recursively, retaining all terminal pieces. --- Only tests pairs that overlap in screen-space (bbox2). --- Skips self-partitioning. --- @param simplices table --- @param parents table --- @return table function Geometry.partition_simplices_by_parents(simplices, parents) -- Pre-compute parent bboxes local parent_bboxes = {} for pi, parent in ipairs(parents) do if parent.type ~= "point" and parent.type ~= "label" then if parent.bbox2 then parent_bboxes[pi] = parent.bbox2 else if getmetatable(parent.simplex) == Matrix then parent_bboxes[pi] = parent.simplex:get_bbox2() end end end end local result = {} local function partition_recursive(piece, piece_bbox, start_parent_index) local meta = {} for k, v in pairs(piece) do if k ~= "simplex" and k ~= "type" and k ~= "bbox2" then meta[k] = v end end local split_occurred = false start_parent_index = start_parent_index or 1 for pi = start_parent_index, #parents do local parent = parents[pi] -- Skip self (same simplex table reference) if parent.simplex == piece.simplex then goto continue_parent end if parent.type == "point" or parent.type == "label" then goto continue_parent end if piece.type == "point" or piece.type == "label" then goto continue_parent end local pb = parent_bboxes[pi] if not pb then goto continue_parent end -- Screen-space overlap check if not piece.simplex:bboxes_overlap2(parent.simplex, piece_bbox, pb) then goto continue_parent end -- 3D overlap check if not piece.simplex:bboxes_overlap3(parent.simplex) then goto continue_parent end local parts = nil local piece_type = piece.type local parent_type = parent.type if parent_type == "point" and piece_type == "line segment" then parts = Geometry.hpartition_line_segment_by_point(piece.simplex, parent.simplex) elseif parent_type == "line segment" and piece_type == "line segment" then parts = Geometry.hpartition_line_segment_by_line_segment(piece.simplex, parent.simplex) elseif parent_type == "triangle" and piece_type == "line segment" then parts = Geometry.hpartition_line_segment_by_triangle(piece.simplex, parent.simplex) elseif parent_type == "triangle" and piece_type == "triangle" then parts = Geometry.hpartition_triangle_by_triangle(piece.simplex, parent.simplex) end if parts ~= nil then for _, part in ipairs(parts) do local new_piece = { simplex = part, type = piece_type, bbox2 = part:get_bbox2() } for k, v in pairs(meta) do new_piece[k] = v end partition_recursive(new_piece, new_piece.bbox2, pi + 1) end split_occurred = true break end ::continue_parent:: end if not split_occurred then table.insert(result, piece) end end for _, simplex in ipairs(simplices) do local bb = simplex.bbox2 if not bb and getmetatable(simplex.simplex) == Matrix then bb = simplex.simplex:get_bbox2() end partition_recursive(simplex, bb, 1) end return result end -- ================================================================ -- Install methods on Vector/Matrix metatables for backward compatibility -- These delegate to the Geometry module functions. -- ================================================================ function Geometry.install_methods() -- Vector methods Vector.hpoint_point_intersecting = Geometry.hpoint_point_intersecting Vector.hpoint_in_triangular_prism = Geometry.hpoint_in_triangular_prism Vector.hpoint_triangle_intersecting = Geometry.hpoint_triangle_intersecting Vector.hpoint_point_occlusion_sort = Geometry.hpoint_point_occlusion_sort Vector.hcollinear = Geometry.hcollinear Vector.hoppositely_directed = Geometry.hoppositely_directed Vector.hpoint_line_segment_occlusion_sort = Geometry.hpoint_line_segment_occlusion_sort Vector.hpoint_triangle_occlusion_sort = Geometry.hpoint_triangle_occlusion_sort -- Matrix methods function Matrix:hline_segment_point_intersecting(point) return Geometry.hline_segment_point_intersecting(self, point, false) end function Matrix:hline_segment_point_intersecting2(point) return Geometry.hline_segment_point_intersecting(self, point, true) end Matrix.hpartition_line_segment_by_point = Geometry.hpartition_line_segment_by_point Matrix.hline_line_intersection = Geometry.hline_line_intersection Matrix.hline_segment_line_segment_intersection = Geometry.hline_segment_line_segment_intersection Matrix.hline_plane_intersection = Geometry.hline_plane_intersection Matrix.hline_segment_triangle_intersection = Geometry.hline_segment_triangle_intersection Matrix.htriangle_triangle_intersections = Geometry.htriangle_triangle_intersections Matrix.hpartition_line_segment_by_line_segment = Geometry.hpartition_line_segment_by_line_segment Matrix.hpartition_line_segment_by_triangle = Geometry.hpartition_line_segment_by_triangle Matrix.hpartition_triangle_by_triangle = Geometry.hpartition_triangle_by_triangle Matrix.hpartition_triangle_by_edge_planes = Geometry.hpartition_triangle_by_edge_planes Matrix.hline_segment_line_segment_occlusion_sort = Geometry.hline_segment_line_segment_occlusion_sort Matrix.hline_segment_triangle_occlusion_sort = Geometry.hline_segment_triangle_occlusion_sort Matrix.htriangle_triangle_occlusion_sort = Geometry.htriangle_triangle_occlusion_sort end return Geometry