From 142bef8cdeb729845c6ab2b83fe4a8b20d8d8df2 Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:39:48 -0400 Subject: [PATCH 1/8] Add Improved Clipping Support --- lua/primitive/core/construct.lua | 68 ++++++++++++++++++++++++++++++++ lua/primitive/entities/base.lua | 54 +++++++++++++++---------- 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index bb85a2e..33e9f41 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -99,6 +99,69 @@ local function util_PointMirror( point, origin, normal ) end +------------------------------- +-- Clips a convex point cloud, keeping the normal side. Every hull edge is an above/below pair, +-- so intersecting all such pairs catches every crossing; extras land inside the hull and are +-- dropped when the engine rebuilds it. Returns nil if nothing survives. +local function clipConvex( points, planePos, planeNormal ) + local above = {} + local kept = {} + + -- Sort the cloud by side, keeping what the normal points toward + for i = 1, #points do + above[i] = vec_dot( planeNormal, points[i] - planePos ) >= -1e-6 + if above[i] then kept[#kept + 1] = points[i] end + end + + if #kept == 0 then return nil end -- plane removed the convex + if #kept == #points then return points end -- plane missed it, hand back the original + + -- Add where each above/below segment crosses the plane, forming the flat cut face + for i = 1, #points do + if above[i] then + for j = 1, #points do + if not above[j] then + local xpoint = util_IntersectPlaneLineSegment( points[i], points[j], planePos, planeNormal ) + if xpoint then kept[#kept + 1] = xpoint end + end + end + end + end + + return kept +end + +-- Applies clip planes ( { pos, normal, seal } ) to a construct result, in place. A plane that +-- would erase the geometry is skipped, so a primitive is never clipped out of existence. +local function applyClips( result, clips, physics ) + for _, clip in ipairs( clips ) do + local planePos = clip.pos + local planeNormal = clip.normal + + if physics and istable( result.convexes ) then + -- Cut every collision hull, dropping the ones the plane wiped out + local clipped = {} + for i = 1, #result.convexes do + local convex = clipConvex( result.convexes[i], planePos, planeNormal ) + if convex then clipped[#clipped + 1] = convex end + end + + -- Only change if a convex got clipped + if clipped[1] then result.convexes = clipped end + end + + if CLIENT and istable( result.verts ) and istable( result.index ) then + -- Cut the render mesh, capping the hole when the clip asks to be sealed + local above = result:Bisect( { pos = planePos, normal = planeNormal }, clip.seal, false ) + if above then -- false when the plane misses the mesh, in either direction + result.verts = above.verts + result.index = above.index + end + end + end +end + + ------------------------------- addon.construct = { simpleton = {} } @@ -192,6 +255,11 @@ do return true, errorModel( 4, name ) end + -- Cut with any caller-supplied clip planes before Build triangulates, so cut faces get UVs + if istable( param.clips ) and param.clips[1] then + applyClips( result, param.clips, physics ) + end + if CLIENT then -- Bad vertex table, error model CODE 5 if not istable( result.verts ) or #result.verts < 3 then diff --git a/lua/primitive/entities/base.lua b/lua/primitive/entities/base.lua index c8d19a9..58c4cfa 100644 --- a/lua/primitive/entities/base.lua +++ b/lua/primitive/entities/base.lua @@ -54,12 +54,39 @@ function class:SetupDataTables() self:PrimitiveVar( "PrimMESHPOS", "Vector", { global = true, category = "model", title = "offset", panel = "vector", min = Vector( -500, -500, -500 ), max = Vector( 500, 500, 500 ) }, true ) self:PrimitiveVar( "PrimMESHROT", "Angle", { global = true, category = "model", title = "rotate", panel = "angle" }, true ) + -- Tell improved clipping we are responsible for maintaining the mesh and its clips. + self.ImprovedClippingExternalMesh = true + self:PrimitiveSetupDataTables() end function class:PrimitiveGetConstructSimple( name ) local keys = self:PrimitiveGetKeys() + + -- Bake any improved clips in as entity-local planes. + if ImprovedClipping then + local clips = ImprovedClipping.GetClips( self ) + + if clips[1] then + local param = {} + for k, v in pairs( keys ) do param[k] = v end + + local planes = {} + for i = 1, #clips do + local clip = clips[i] + planes[i] = { + pos = clip.Normal * clip.Distance, + normal = clip.Normal, + seal = clip.Seal ~= false, + } + end + param.clips = planes + + return Primitive.construct.get( name, param, CLIENT, param.PrimMESHPHYS ) + end + end + return Primitive.construct.get( name, keys, CLIENT, keys.PrimMESHPHYS ) end @@ -623,28 +650,13 @@ do end end) - -- Re-apply any proper_clipping physics clips after each primitive reconstruction. - -- Primitives reinitialize via PhysicsInitMultiConvex on every reconstruction, which wipes the clips - -- Bypass the race condition by preserving existing clips on rebuild - hook.Add("Primitive_PostRebuildPhysics", "Primitive_ProperClipping", function( ent ) - if not ent.Clipped or not istable( ent.ClipData ) then return end -- no clips to reapply - if not ProperClipping or not isfunction( ProperClipping.ClipPhysics ) then return end -- proper_clipping not installed - - -- Aggregate all physics clips - local norms, dists = {}, {} - local count = 0 - for _, clip in ipairs( ent.ClipData ) do - if clip.physics then - count = count + 1 - norms[count] = clip.norm - dists[count] = clip.dist - end + -- Rebuild on clip changes so the new planes get baked in ( see PrimitiveGetConstructSimple ). + -- The hook only exists when Improved Clipping is installed, so neither addon depends on the other. + hook.Add( "ImprovedClipping_ClipsChanged", "Primitive_ImprovedClipping", function( ent ) + if ent.IsPrimitive and istable( ent.primitive ) then + ent:PrimitiveReconstruct() end - - if count == 0 then return end -- no physics clips to reapply - - ProperClipping.ClipPhysics( ent, norms, dists, true ) - end) + end ) end From 99635ccfb874d38d76561eb3fb9c392514c09d1e Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Sun, 19 Jul 2026 22:19:55 -0400 Subject: [PATCH 2/8] Improve sealing agorrithm Convex hull should propperly seal off clips for clips of concave primitives such as magic cubes and tubes --- lua/primitive/core/construct.lua | 104 ++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 28 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index 2bde024..30ea957 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -93,6 +93,52 @@ local function util_IntersectPlaneLineSegment( lineStart, lineFinish, planePos, return false end +-- Orthonormal basis of the plane, right x up == normal +local function util_PlaneBasis( normal ) + local right = vec_cross( math_abs( normal.z ) < 0.9 and Vector( 0, 0, 1 ) or Vector( 1, 0, 0 ), normal ) + vec_normalize( right ) + + return right, vec_cross( normal, right ) +end + +-- Convex hull of 2D points ( { X, Y, Pos } ), counter-clockwise. Andrew's monotone chain. +-- Both tables keep stale entries past their count, so pass and read the count, never #. +local function util_ConvexHull2D( points, count ) + table.sort( points, function( a, b ) + if a.X ~= b.X then return a.X < b.X end + return a.Y < b.Y + end ) + + local function cross( o, a, b ) + return ( a.X - o.X ) * ( b.Y - o.Y ) - ( a.Y - o.Y ) * ( b.X - o.X ) + end + + local hull, n = {}, 0 + + -- Appends a point, dropping any tail that makes a non-left turn. Floor keeps the upper hull from eating the lower one. + local function append( p, floor ) + while n > floor and cross( hull[n - 1], hull[n], p ) <= 0 do + n = n - 1 + end + + n = n + 1 + hull[n] = p + end + + -- Lower hull, left to right + for i = 1, count do + append( points[i], 1 ) + end + + -- Upper hull, right to left. The rightmost point ends the lower hull, so start one short of it and keep it as the floor. + local floor = n + for i = count - 1, 1, -1 do + append( points[i], floor ) + end + + return hull, n - 1 -- The leftmost point closes the loop and repeats hull[1] +end + local function util_PointMirror( point, origin, normal ) local l = vec_dot( normal, origin - point ) return point + normal * l * 2 @@ -866,30 +912,37 @@ do end end - local function closeEdgeLoop( abovePlane, belowPlane, loopCenter, loopPoints ) - local aA - if abovePlane then - aA = abovePlane:PushVertex( loopCenter ) - end + -- Caps the hole a bisect opened up by fanning the cut points' convex hull. Sorting them by + -- angle about their centroid instead only orders a star-shaped face, so cuts came back as a sawtooth. + local function closeEdgeLoop( abovePlane, belowPlane, loopPoints, planeNormal ) + -- The above cap faces away from the plane, and the hull winds counter-clockwise about it, front facing here + local capNormal = -planeNormal + local right, up = util_PlaneBasis( capNormal ) - local bA - if belowPlane then - bA = belowPlane:PushVertex( loopCenter ) + local projected = {} + for i = 1, #loopPoints do + local p = loopPoints[i] + projected[i] = { X = vec_dot( right, p ), Y = vec_dot( up, p ), Pos = p } end - local wrap = { [#loopPoints] = 1 } + local hull, count = util_ConvexHull2D( projected, #loopPoints ) + if count < 3 then return end - for i = 1, #loopPoints do - local p0 = loopPoints[i] - local p1 = loopPoints[wrap[i] or i + 1] + -- Each corner is pushed once and fanned by index, rather than re-pushed per triangle + local aFace = abovePlane and {} + local bFace = belowPlane and {} - if aA then - abovePlane:PushTriangle( aA, abovePlane:PushVertex( p0 ), abovePlane:PushVertex( p1 ) ) - end - if bA then - belowPlane:PushTriangle( bA, belowPlane:PushVertex( p1 ), belowPlane:PushVertex( p0 ) ) - end + for i = 1, count do + local p = hull[i].Pos + + if aFace then aFace[i] = abovePlane:PushVertex( p ) end + -- Reversed, the below side's cap faces the other way + if bFace then bFace[count - i + 1] = belowPlane:PushVertex( p ) end end + + -- A convex polygon fans from any of its own vertices + if aFace then abovePlane:PushFaceTable( aFace ) end + if bFace then belowPlane:PushFaceTable( bFace ) end end --[[ Given an unordered list of line segments, return a closed boundary @@ -965,26 +1018,21 @@ do -- Check each edge of each triangle for an intersection with the plane. -- All that intersect are split into two smaller triangles. - local loopCenter = Vector() local loopPoints = ( fillAbove or fillBelow ) and {} or nil for i = 1, #self.index, 3 do local l0, l1 = intersection( self, i, planePos, planeNormal, abovePlane, belowPlane ) + -- Both ends, so a corner isn't missed when it only turns up as the far end of some triangle's cut if loopPoints and l0 and l1 then - vec_add( loopCenter, l0 ) loopPoints[#loopPoints + 1] = l0 + loopPoints[#loopPoints + 1] = l1 end end - if loopPoints then - loopCenter = loopCenter / #loopPoints - - table.sort( loopPoints, function( sa, sb ) - return vec_dot( planeNormal, vec_cross( sa - loopCenter, sb - loopCenter ) ) < 0 - end ) - - closeEdgeLoop( fillAbove and abovePlane, fillBelow and belowPlane, loopCenter, loopPoints ) + -- Fewer than three points can't bound a face + if loopPoints and loopPoints[3] then + closeEdgeLoop( fillAbove and abovePlane, fillBelow and belowPlane, loopPoints, planeNormal ) end abovePlane.key = {} From 17ce69631ffc61ee24b7871cdffda672c1da0bb5 Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:04:10 -0400 Subject: [PATCH 3/8] Fix unbounded clip expansion --- lua/primitive/core/construct.lua | 64 +++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index 30ea957..29694e2 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -146,34 +146,65 @@ end ------------------------------- --- Clips a convex point cloud, keeping the normal side. Every hull edge is an above/below pair, --- so intersecting all such pairs catches every crossing; extras land inside the hull and are --- dropped when the engine rebuilds it. Returns nil if nothing survives. +-- Clips a convex point cloud, keeping the normal side. Returns nil if nothing survives. +-- +-- Every hull edge is an above/below pair, so intersecting all such pairs catches every crossing. +-- Most pairs are chords whose crossings land inside the cut face, so the crossings are reduced to +-- their 2d hull: the face's corners alone, which keeps the point count bounded across clips. local function clipConvex( points, planePos, planeNormal ) - local above = {} - local kept = {} + local count = #points + local above, dist = {}, {} + local kept, nkept = {}, 0 -- Sort the cloud by side, keeping what the normal points toward - for i = 1, #points do - above[i] = vec_dot( planeNormal, points[i] - planePos ) >= -1e-6 - if above[i] then kept[#kept + 1] = points[i] end + for i = 1, count do + dist[i] = vec_dot( planeNormal, points[i] - planePos ) + above[i] = dist[i] >= -1e-6 + + if above[i] then + nkept = nkept + 1 + kept[nkept] = points[i] + end end - if #kept == 0 then return nil end -- plane removed the convex - if #kept == #points then return points end -- plane missed it, hand back the original + if nkept == 0 then return nil end -- plane removed the convex + if nkept == count then return points end -- plane missed it, hand back the original + + -- From the signed distances, not a segment intersection, so a grazing edge can't be missed + local right, up = util_PlaneBasis( planeNormal ) + local cuts, ncuts = {}, 0 - -- Add where each above/below segment crosses the plane, forming the flat cut face - for i = 1, #points do + for i = 1, count do if above[i] then - for j = 1, #points do + local a, da = points[i], dist[i] + + for j = 1, count do if not above[j] then - local xpoint = util_IntersectPlaneLineSegment( points[i], points[j], planePos, planeNormal ) - if xpoint then kept[#kept + 1] = xpoint end + -- da >= 0 > db, so the denominator is positive and frac lands in [0, 1] + local frac = math_clamp( da / ( da - dist[j] ), 0, 1 ) + local p = a + ( points[j] - a ) * frac + + ncuts = ncuts + 1 + cuts[ncuts] = { X = vec_dot( right, p ), Y = vec_dot( up, p ), Pos = p } end end end end + -- Fewer than three crossings can't bound a face, so there's no hull to take + if ncuts < 3 then + for i = 1, ncuts do + kept[nkept + i] = cuts[i].Pos + end + + return kept + end + + local hull, nhull = util_ConvexHull2D( cuts, ncuts ) + for i = 1, nhull do + kept[nkept + i] = hull[i].Pos + end + return kept end @@ -912,8 +943,7 @@ do end end - -- Caps the hole a bisect opened up by fanning the cut points' convex hull. Sorting them by - -- angle about their centroid instead only orders a star-shaped face, so cuts came back as a sawtooth. + -- Caps the hole a bisect opened up by fanning the cut points' convex hull local function closeEdgeLoop( abovePlane, belowPlane, loopPoints, planeNormal ) -- The above cap faces away from the plane, and the hull winds counter-clockwise about it, front facing here local capNormal = -planeNormal From 85d96105a591e990f92e2dbfbb6027eafb392bc1 Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Tue, 21 Jul 2026 02:49:27 -0400 Subject: [PATCH 4/8] Fix rebuilt parented primitive collisions Rebuilding a parented primitive would cause issues with the hitbox. Unparent and reparent --- lua/primitive/entities/base.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/primitive/entities/base.lua b/lua/primitive/entities/base.lua index 58c4cfa..0b6998b 100644 --- a/lua/primitive/entities/base.lua +++ b/lua/primitive/entities/base.lua @@ -222,6 +222,10 @@ local function rescale( self, scalar ) end function class:PrimitiveRebuildPhysics( result ) + -- Rebuilding a parented primitive would cause issues with the hitbox. Unparent and reparent + local parent = SERVER and self:GetParent() or nil + if IsValid( parent ) then self:SetParent( nil ) end + local props if SERVER then props = self:PrimitiveGetProperties() @@ -281,6 +285,8 @@ function class:PrimitiveRebuildPhysics( result ) self:PrimitiveSetProperties( props ) end + + if IsValid( parent ) then self:SetParent( parent ) end end From ae87a38719b8af9d189ede6b54c4b74cb707ca7d Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:53:30 -0400 Subject: [PATCH 5/8] Revert "Improve sealing agorrithm" This reverts commit 99635ccfb874d38d76561eb3fb9c392514c09d1e and commit 17ce69631ffc61ee24b7871cdffda672c1da0bb5, restoring the original clipping/capping logic. --- lua/primitive/core/construct.lua | 164 ++++++++----------------------- 1 file changed, 43 insertions(+), 121 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index 29694e2..2bde024 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -93,52 +93,6 @@ local function util_IntersectPlaneLineSegment( lineStart, lineFinish, planePos, return false end --- Orthonormal basis of the plane, right x up == normal -local function util_PlaneBasis( normal ) - local right = vec_cross( math_abs( normal.z ) < 0.9 and Vector( 0, 0, 1 ) or Vector( 1, 0, 0 ), normal ) - vec_normalize( right ) - - return right, vec_cross( normal, right ) -end - --- Convex hull of 2D points ( { X, Y, Pos } ), counter-clockwise. Andrew's monotone chain. --- Both tables keep stale entries past their count, so pass and read the count, never #. -local function util_ConvexHull2D( points, count ) - table.sort( points, function( a, b ) - if a.X ~= b.X then return a.X < b.X end - return a.Y < b.Y - end ) - - local function cross( o, a, b ) - return ( a.X - o.X ) * ( b.Y - o.Y ) - ( a.Y - o.Y ) * ( b.X - o.X ) - end - - local hull, n = {}, 0 - - -- Appends a point, dropping any tail that makes a non-left turn. Floor keeps the upper hull from eating the lower one. - local function append( p, floor ) - while n > floor and cross( hull[n - 1], hull[n], p ) <= 0 do - n = n - 1 - end - - n = n + 1 - hull[n] = p - end - - -- Lower hull, left to right - for i = 1, count do - append( points[i], 1 ) - end - - -- Upper hull, right to left. The rightmost point ends the lower hull, so start one short of it and keep it as the floor. - local floor = n - for i = count - 1, 1, -1 do - append( points[i], floor ) - end - - return hull, n - 1 -- The leftmost point closes the loop and repeats hull[1] -end - local function util_PointMirror( point, origin, normal ) local l = vec_dot( normal, origin - point ) return point + normal * l * 2 @@ -146,65 +100,34 @@ end ------------------------------- --- Clips a convex point cloud, keeping the normal side. Returns nil if nothing survives. --- --- Every hull edge is an above/below pair, so intersecting all such pairs catches every crossing. --- Most pairs are chords whose crossings land inside the cut face, so the crossings are reduced to --- their 2d hull: the face's corners alone, which keeps the point count bounded across clips. +-- Clips a convex point cloud, keeping the normal side. Every hull edge is an above/below pair, +-- so intersecting all such pairs catches every crossing; extras land inside the hull and are +-- dropped when the engine rebuilds it. Returns nil if nothing survives. local function clipConvex( points, planePos, planeNormal ) - local count = #points - local above, dist = {}, {} - local kept, nkept = {}, 0 + local above = {} + local kept = {} -- Sort the cloud by side, keeping what the normal points toward - for i = 1, count do - dist[i] = vec_dot( planeNormal, points[i] - planePos ) - above[i] = dist[i] >= -1e-6 - - if above[i] then - nkept = nkept + 1 - kept[nkept] = points[i] - end + for i = 1, #points do + above[i] = vec_dot( planeNormal, points[i] - planePos ) >= -1e-6 + if above[i] then kept[#kept + 1] = points[i] end end - if nkept == 0 then return nil end -- plane removed the convex - if nkept == count then return points end -- plane missed it, hand back the original - - -- From the signed distances, not a segment intersection, so a grazing edge can't be missed - local right, up = util_PlaneBasis( planeNormal ) - local cuts, ncuts = {}, 0 + if #kept == 0 then return nil end -- plane removed the convex + if #kept == #points then return points end -- plane missed it, hand back the original - for i = 1, count do + -- Add where each above/below segment crosses the plane, forming the flat cut face + for i = 1, #points do if above[i] then - local a, da = points[i], dist[i] - - for j = 1, count do + for j = 1, #points do if not above[j] then - -- da >= 0 > db, so the denominator is positive and frac lands in [0, 1] - local frac = math_clamp( da / ( da - dist[j] ), 0, 1 ) - local p = a + ( points[j] - a ) * frac - - ncuts = ncuts + 1 - cuts[ncuts] = { X = vec_dot( right, p ), Y = vec_dot( up, p ), Pos = p } + local xpoint = util_IntersectPlaneLineSegment( points[i], points[j], planePos, planeNormal ) + if xpoint then kept[#kept + 1] = xpoint end end end end end - -- Fewer than three crossings can't bound a face, so there's no hull to take - if ncuts < 3 then - for i = 1, ncuts do - kept[nkept + i] = cuts[i].Pos - end - - return kept - end - - local hull, nhull = util_ConvexHull2D( cuts, ncuts ) - for i = 1, nhull do - kept[nkept + i] = hull[i].Pos - end - return kept end @@ -943,36 +866,30 @@ do end end - -- Caps the hole a bisect opened up by fanning the cut points' convex hull - local function closeEdgeLoop( abovePlane, belowPlane, loopPoints, planeNormal ) - -- The above cap faces away from the plane, and the hull winds counter-clockwise about it, front facing here - local capNormal = -planeNormal - local right, up = util_PlaneBasis( capNormal ) - - local projected = {} - for i = 1, #loopPoints do - local p = loopPoints[i] - projected[i] = { X = vec_dot( right, p ), Y = vec_dot( up, p ), Pos = p } + local function closeEdgeLoop( abovePlane, belowPlane, loopCenter, loopPoints ) + local aA + if abovePlane then + aA = abovePlane:PushVertex( loopCenter ) end - local hull, count = util_ConvexHull2D( projected, #loopPoints ) - if count < 3 then return end + local bA + if belowPlane then + bA = belowPlane:PushVertex( loopCenter ) + end - -- Each corner is pushed once and fanned by index, rather than re-pushed per triangle - local aFace = abovePlane and {} - local bFace = belowPlane and {} + local wrap = { [#loopPoints] = 1 } - for i = 1, count do - local p = hull[i].Pos + for i = 1, #loopPoints do + local p0 = loopPoints[i] + local p1 = loopPoints[wrap[i] or i + 1] - if aFace then aFace[i] = abovePlane:PushVertex( p ) end - -- Reversed, the below side's cap faces the other way - if bFace then bFace[count - i + 1] = belowPlane:PushVertex( p ) end + if aA then + abovePlane:PushTriangle( aA, abovePlane:PushVertex( p0 ), abovePlane:PushVertex( p1 ) ) + end + if bA then + belowPlane:PushTriangle( bA, belowPlane:PushVertex( p1 ), belowPlane:PushVertex( p0 ) ) + end end - - -- A convex polygon fans from any of its own vertices - if aFace then abovePlane:PushFaceTable( aFace ) end - if bFace then belowPlane:PushFaceTable( bFace ) end end --[[ Given an unordered list of line segments, return a closed boundary @@ -1048,21 +965,26 @@ do -- Check each edge of each triangle for an intersection with the plane. -- All that intersect are split into two smaller triangles. + local loopCenter = Vector() local loopPoints = ( fillAbove or fillBelow ) and {} or nil for i = 1, #self.index, 3 do local l0, l1 = intersection( self, i, planePos, planeNormal, abovePlane, belowPlane ) - -- Both ends, so a corner isn't missed when it only turns up as the far end of some triangle's cut if loopPoints and l0 and l1 then + vec_add( loopCenter, l0 ) loopPoints[#loopPoints + 1] = l0 - loopPoints[#loopPoints + 1] = l1 end end - -- Fewer than three points can't bound a face - if loopPoints and loopPoints[3] then - closeEdgeLoop( fillAbove and abovePlane, fillBelow and belowPlane, loopPoints, planeNormal ) + if loopPoints then + loopCenter = loopCenter / #loopPoints + + table.sort( loopPoints, function( sa, sb ) + return vec_dot( planeNormal, vec_cross( sa - loopCenter, sb - loopCenter ) ) < 0 + end ) + + closeEdgeLoop( fillAbove and abovePlane, fillBelow and belowPlane, loopCenter, loopPoints ) end abovePlane.key = {} From 9b3c31b066ced58e0fc31aa46aba4b35952501fb Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:44:10 -0400 Subject: [PATCH 6/8] Reject multiconvex seals Sealing a multiconvex entity introduces a discrepancy. Clipping a concave object results in a concave hole in the physics mesh. It should not be covered up visually. --- lua/primitive/core/construct.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index 2bde024..13b9457 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -138,6 +138,10 @@ local function applyClips( result, clips, physics ) local planePos = clip.pos local planeNormal = clip.normal + -- Clipping a multi convex will always result in a concave hole in the physics mesh. + -- We shouldn't introduce a visual filling where there is a physical hole. + local seal = clip.seal and ( not istable( result.convexes ) or #result.convexes <= 1 ) + if physics and istable( result.convexes ) then -- Cut every collision hull, dropping the ones the plane wiped out local clipped = {} @@ -152,7 +156,7 @@ local function applyClips( result, clips, physics ) if CLIENT and istable( result.verts ) and istable( result.index ) then -- Cut the render mesh, capping the hole when the clip asks to be sealed - local above = result:Bisect( { pos = planePos, normal = planeNormal }, clip.seal, false ) + local above = result:Bisect( { pos = planePos, normal = planeNormal }, seal, false ) if above then -- false when the plane misses the mesh, in either direction result.verts = above.verts result.index = above.index From 8b38a3e230ea841c3f83d52f15f4be95d2369d61 Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Fri, 24 Jul 2026 02:32:44 -0400 Subject: [PATCH 7/8] Fix unbounded point expansion --- lua/primitive/core/construct.lua | 108 +++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 13 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index 13b9457..ed6fbeb 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -98,36 +98,118 @@ local function util_PointMirror( point, origin, normal ) return point + normal * l * 2 end +-- Two unit axes spanning the plane the normal defines +local function util_PlaneBasis( normal ) + local right + if math_abs( normal.z ) < 0.9 then + right = vec_getnormalized( vec_cross( normal, Vector( 0, 0, 1 ) ) ) + else + right = vec_getnormalized( vec_cross( normal, Vector( 1, 0, 0 ) ) ) + end + + return right, vec_cross( normal, right ) +end + +-- Andrew's monotone chain over { X, Y, ... } points, keeping collinear points; returns the hull and its size +local function util_ConvexHull2D( points, count ) + table.sort( points, function( a, b ) + if a.X ~= b.X then return a.X < b.X end + return a.Y < b.Y + end ) + + local hull, n = {}, 0 + + for i = 1, count do + local p = points[i] + while n >= 2 and ( hull[n].X - hull[n - 1].X ) * ( p.Y - hull[n - 1].Y ) - ( hull[n].Y - hull[n - 1].Y ) * ( p.X - hull[n - 1].X ) < 0 do + hull[n] = nil + n = n - 1 + end + n = n + 1 + hull[n] = p + end + + local lower = n + 1 + for i = count - 1, 1, -1 do + local p = points[i] + while n >= lower and ( hull[n].X - hull[n - 1].X ) * ( p.Y - hull[n - 1].Y ) - ( hull[n].Y - hull[n - 1].Y ) * ( p.X - hull[n - 1].X ) < 0 do + hull[n] = nil + n = n - 1 + end + n = n + 1 + hull[n] = p + end + + hull[n] = nil -- last point repeats the first + + return hull, n - 1 +end + ------------------------------- --- Clips a convex point cloud, keeping the normal side. Every hull edge is an above/below pair, --- so intersecting all such pairs catches every crossing; extras land inside the hull and are --- dropped when the engine rebuilds it. Returns nil if nothing survives. +-- Clips a convex point cloud, keeping the normal side; returns nil if nothing survives. +-- Crossing every above/below pair catches every cut edge, then the 2d hull drops the interior +-- chord crossings so the point count stays bounded across clips. local function clipConvex( points, planePos, planeNormal ) - local above = {} + local count = #points + local above, dist = {}, {} local kept = {} -- Sort the cloud by side, keeping what the normal points toward - for i = 1, #points do - above[i] = vec_dot( planeNormal, points[i] - planePos ) >= -1e-6 + for i = 1, count do + dist[i] = vec_dot( planeNormal, points[i] - planePos ) + above[i] = dist[i] >= -1e-6 if above[i] then kept[#kept + 1] = points[i] end end - if #kept == 0 then return nil end -- plane removed the convex - if #kept == #points then return points end -- plane missed it, hand back the original + if #kept == 0 then return nil end -- plane removed the convex + if #kept == count then return points end -- plane missed it, hand back the original + + -- Cross the plane in the plane's 2d frame, as scalars: pairs number in the tens of + -- thousands on a dense cloud, so Vectors are only built for the crossings the hull keeps + local right, up = util_PlaneBasis( planeNormal ) + local rx, uy = {}, {} + + for i = 1, count do + rx[i] = vec_dot( right, points[i] ) + uy[i] = vec_dot( up, points[i] ) + end + + local cuts = {} - -- Add where each above/below segment crosses the plane, forming the flat cut face - for i = 1, #points do + for i = 1, count do if above[i] then - for j = 1, #points do + local da, xa, ya = dist[i], rx[i], uy[i] + + for j = 1, count do if not above[j] then - local xpoint = util_IntersectPlaneLineSegment( points[i], points[j], planePos, planeNormal ) - if xpoint then kept[#kept + 1] = xpoint end + -- da >= 0 > db, so the denominator is positive and frac lands in [0, 1] + local frac = math_clamp( da / ( da - dist[j] ), 0, 1 ) + + cuts[#cuts + 1] = { + X = xa + ( rx[j] - xa ) * frac, + Y = ya + ( uy[j] - ya ) * frac, + I = i, J = j, F = frac, + } end end end end + local hull, nhull + if #cuts < 3 then + -- Fewer than three crossings can't bound a face, so there's no hull to take + hull, nhull = cuts, #cuts + else + hull, nhull = util_ConvexHull2D( cuts, #cuts ) + end + + for i = 1, nhull do + local cut = hull[i] + local a = points[cut.I] + kept[#kept + 1] = a + ( points[cut.J] - a ) * cut.F + end + return kept end From 419f803181c919aaf7c511a3bf5e5234cddaa8e5 Mon Sep 17 00:00:00 2001 From: LengthenedGradient <109800352+LengthenedGradient@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:10:37 -0400 Subject: [PATCH 8/8] Physical clipping optimizations --- lua/primitive/core/construct.lua | 65 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/lua/primitive/core/construct.lua b/lua/primitive/core/construct.lua index ed6fbeb..7105ffa 100644 --- a/lua/primitive/core/construct.lua +++ b/lua/primitive/core/construct.lua @@ -110,18 +110,19 @@ local function util_PlaneBasis( normal ) return right, vec_cross( normal, right ) end --- Andrew's monotone chain over { X, Y, ... } points, keeping collinear points; returns the hull and its size -local function util_ConvexHull2D( points, count ) - table.sort( points, function( a, b ) - if a.X ~= b.X then return a.X < b.X end - return a.Y < b.Y +-- Andrew's monotone chain over parallel coordinate arrays, returning point indices; collinear and +-- duplicate points are popped so the kept set stays minimal across clips. `order` must hold 1..count. +local function util_ConvexHull2D( px, py, count, order ) + table.sort( order, function( a, b ) + if px[a] ~= px[b] then return px[a] < px[b] end + return py[a] < py[b] end ) local hull, n = {}, 0 for i = 1, count do - local p = points[i] - while n >= 2 and ( hull[n].X - hull[n - 1].X ) * ( p.Y - hull[n - 1].Y ) - ( hull[n].Y - hull[n - 1].Y ) * ( p.X - hull[n - 1].X ) < 0 do + local p = order[i] + while n >= 2 and ( px[hull[n]] - px[hull[n - 1]] ) * ( py[p] - py[hull[n - 1]] ) - ( py[hull[n]] - py[hull[n - 1]] ) * ( px[p] - px[hull[n - 1]] ) <= 0 do hull[n] = nil n = n - 1 end @@ -131,8 +132,8 @@ local function util_ConvexHull2D( points, count ) local lower = n + 1 for i = count - 1, 1, -1 do - local p = points[i] - while n >= lower and ( hull[n].X - hull[n - 1].X ) * ( p.Y - hull[n - 1].Y ) - ( hull[n].Y - hull[n - 1].Y ) * ( p.X - hull[n - 1].X ) < 0 do + local p = order[i] + while n >= lower and ( px[hull[n]] - px[hull[n - 1]] ) * ( py[p] - py[hull[n - 1]] ) - ( py[hull[n]] - py[hull[n - 1]] ) * ( px[p] - px[hull[n - 1]] ) <= 0 do hull[n] = nil n = n - 1 end @@ -165,49 +166,59 @@ local function clipConvex( points, planePos, planeNormal ) if #kept == 0 then return nil end -- plane removed the convex if #kept == count then return points end -- plane missed it, hand back the original - -- Cross the plane in the plane's 2d frame, as scalars: pairs number in the tens of - -- thousands on a dense cloud, so Vectors are only built for the crossings the hull keeps + -- Cross the plane in the plane's 2d frame, as scalars in parallel arrays: pairs number in + -- the tens of thousands on a dense cloud, so no tables are made per crossing and Vectors + -- are only built for the crossings the hull keeps local right, up = util_PlaneBasis( planeNormal ) local rx, uy = {}, {} + local below, nbelow = {}, 0 for i = 1, count do rx[i] = vec_dot( right, points[i] ) uy[i] = vec_dot( up, points[i] ) + + if not above[i] then + nbelow = nbelow + 1 + below[nbelow] = i + end end - local cuts = {} + local cutX, cutY, cutI, cutJ, cutF, order = {}, {}, {}, {}, {}, {} + local ncuts = 0 for i = 1, count do if above[i] then local da, xa, ya = dist[i], rx[i], uy[i] - for j = 1, count do - if not above[j] then - -- da >= 0 > db, so the denominator is positive and frac lands in [0, 1] - local frac = math_clamp( da / ( da - dist[j] ), 0, 1 ) + for k = 1, nbelow do + local j = below[k] - cuts[#cuts + 1] = { - X = xa + ( rx[j] - xa ) * frac, - Y = ya + ( uy[j] - ya ) * frac, - I = i, J = j, F = frac, - } - end + -- da >= 0 > db, so the denominator is positive and frac lands in [0, 1] + local frac = math_clamp( da / ( da - dist[j] ), 0, 1 ) + + ncuts = ncuts + 1 + cutX[ncuts] = xa + ( rx[j] - xa ) * frac + cutY[ncuts] = ya + ( uy[j] - ya ) * frac + cutI[ncuts] = i + cutJ[ncuts] = j + cutF[ncuts] = frac + order[ncuts] = ncuts end end end local hull, nhull - if #cuts < 3 then + if ncuts < 3 then -- Fewer than three crossings can't bound a face, so there's no hull to take - hull, nhull = cuts, #cuts + hull, nhull = order, ncuts else - hull, nhull = util_ConvexHull2D( cuts, #cuts ) + hull, nhull = util_ConvexHull2D( cutX, cutY, ncuts, order ) end for i = 1, nhull do local cut = hull[i] - local a = points[cut.I] - kept[#kept + 1] = a + ( points[cut.J] - a ) * cut.F + local a = points[cutI[cut]] + kept[#kept + 1] = a + ( points[cutJ[cut]] - a ) * cutF[cut] end return kept