Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions lua/primitive/core/construct.lua
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,166 @@ 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 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 = 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
n = n + 1
hull[n] = p
end

local lower = n + 1
for i = count - 1, 1, -1 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
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; 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 count = #points
local above, dist = {}, {}
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 kept[#kept + 1] = points[i] end
end

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 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 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 k = 1, nbelow do
local j = below[k]

-- 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 ncuts < 3 then
-- Fewer than three crossings can't bound a face, so there's no hull to take
hull, nhull = order, ncuts
else
hull, nhull = util_ConvexHull2D( cutX, cutY, ncuts, order )
end

for i = 1, nhull do
local cut = hull[i]
local a = points[cutI[cut]]
kept[#kept + 1] = a + ( points[cutJ[cut]] - a ) * cutF[cut]
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

-- 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 = {}
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 }, 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 = {} }
Expand Down Expand Up @@ -192,6 +352,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
Expand Down
60 changes: 39 additions & 21 deletions lua/primitive/entities/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -195,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()
Expand Down Expand Up @@ -254,6 +285,8 @@ function class:PrimitiveRebuildPhysics( result )

self:PrimitiveSetProperties( props )
end

if IsValid( parent ) then self:SetParent( parent ) end
end


Expand Down Expand Up @@ -623,28 +656,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


Expand Down