i know this thread hasn’t been active for a few months, but i’m here to dump a module script that has a closest point on part function that works slightly better than the one i put out previously (points closest to slanted wedge surfaces do not have consistent results compared to when using it on block type part’s surfaces, because the previous function only focused on limiting the point to the block’s geometry) due to my laziness to isolate the functions from it.
my heart tells me there’s definitely a better way to program the code for the wedge & corner wedge part in the new function, but i couldn’t think of one. either way i’d say it’s messy but i decided to put it here because i imagine someone might need it.
--function groups
----
local Vars = {}
local MathNumber = {}
local MathVector3 = {}
local MathGeometry = {} --no idea if the functions belonging to this should be in MathVector3
local Math = {}
local Table = {}
local Parts = {}
local Graphics = {}
--module sorting
----
local module = {
["Vars"] = Vars,
["Math"] = {["Number"] = MathNumber, ["Vector3"] = MathVector3, ["Geometry"] = MathGeometry},
["Table"] = Table,
["Parts"] = Parts,
["Graphics"] = Graphics
}
--functions
----
--==variables==--
function Vars.DefaultTo(Var, Default) --intended for functions that have optional arguments. avoided arguments return nil, so this is just a simple if check.
if Var then
return Var
else
return Default
end
end
--==math (numbers)==--
function MathNumber.Average(Numbers)
local Sum = 0
for i, v in Numbers do
Sum += v
end
return Sum / #Numbers
end
function MathNumber.Lerp(a:number, b:number, x:number) --roblox strangely has a lerp function for vector3's but not numbers.
return a + (b - a) * x
end
function MathNumber.InverseLerp(a:number, b:number, x:number)
return (x - a) / (b - a)
end
--==math (vector3)==--
--[[
source: http://answers.unity.com/answers/1274648/view.html
]]
function MathVector3.InverseLerp(a:Vector3, b:Vector3, x:Vector3) --vec3 already has a lerp function, although there is no inverselerp.
local AB = b - a
local AV = x - a
return AV:Dot(AB) / AB:Dot(AB)
end
function MathVector3.Average(Vectors) --vectors should be a table containing only vectors.
local Sum = Vector3.zero
for i, v in Vectors do
Sum += v
end
return Vector3.new(Sum.X / #Vectors, Sum.Y / #Vectors, Sum.Z / #Vectors)
end
function MathVector3.UnitLength(Vec:Vector3, Unit:Vector3) --intended to get a value for a axis, using "Unit" as a representative of the axis, might be more sensible to use a dot product, honestly.
local Projection = MathVector3.ProjectToLine(Vec, Unit)
local DotProduct = Projection.Unit:Dot(Unit.Unit)
local Result = Projection.Magnitude * DotProduct
--[[
doing 0 / 0 would equal to nan. if the vec is zeroed out on all axes, it'll also give nan.
oddly enough, comparing 0/0 to the output of this func if a vector is 0 doesn't return true.
we do know that they both return nan however, so the only thing i could think of was to convert
the output into a string & compare it with a string "nan".
]]--
if tostring(Result) == "nan" then
return 0
end
return Result
end
function MathVector3.ProjectToLine(Vec:Vector3, Line:Vector3)
return Vec:Dot(Line.Unit) * Line.Unit
end
function MathVector3.ProjectToNormal(Vec:Vector3, Normal:Vector3)
return Vec - MathVector3.ProjectToLine(Vec, Normal)
end
function MathVector3.ClampMagnitude(Vec:Vector3, Limit:number)
if Vec.Magnitude > Limit then
return Vec.Unit * Limit
end
return Vec
end
--==math (geometry)==--
function MathGeometry.GetClosestPointOnLine(Point:Vector3, Start:Vector3, End:Vector3) --same deal as getting the closest point on a edge
local Line = End - Start
--to treat my obessive compulsive disorder, we'll use a center of the line as the origin by using lerp
local Center = Start:Lerp(End, 0.5)
--the position point is representing has a origin of 0,0,0. you can imagine drawing a line from 0,0,0 to point as the world space vector. to "convert it into local space", we'd change the origin to the line's center.
local LocalSpacePoint = Point - Center
--project the point onto the line & limit the length of the vector. this limits the position where the line isn't pointing at & limits the position to the line's length respectively. since the origin is at the center, the length limit should be line.magnitude/2.
return Center + MathVector3.ClampMagnitude(MathVector3.ProjectToLine(LocalSpacePoint, Line), Line.Magnitude/2)
end
--[[
this lazily uses sebastian lague's PointInTriangle() function, seen here:
https://github.com/SebLague/Gamedev-Maths/blob/master/PointInTriangle.cs
ProjectPoint is a optional argument that will project the point position to the triangle's normal if set to true, which
means regardless of the point either being off or on the triangle's surface, it'll count as if it was always on the surface.
also, if the point given is overlapping with a triangle's edge/line, it'll count as "within".
]]--
function MathGeometry.IsPointWithin3DTriangle(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3, ProjectPoint:boolean?)
--[[
==triangle legend
====
2
.
/ \
a / \ b
/_____\
1 c 3
my formatting is similar to javidx9's (seen here: https://www.youtube.com/watch?v=XgMWc6LumG4&t=285s), which orders the vertexes
and lines/edges in clockwise direction.
i'd thought about having the vertexes being automatically assigned for a clockwise order via getting a average of all vertex positions and
creating a vector that connects to the average > vertex, then sorting the vertexes by their vector's respective dot product. but i never thought
about what would be the other "static" variable for the dot product that'd be the same during the calculation & what that vector should be, because
if we just use one of the global axes, it's likely it wouldn't match the intended behaviour of the sorting because the vector would've to be within
a circle like shape on the triangle's normal (and getting the normal without the lines isn't possible because getting the normal relies on a cross product)
]]--
--==tri data==--
local TriCenter = MathVector3.Average({V1, V2, V3})
local TriLines = {
["A"] = V2 - V1,
--["B"] = V3 - V2,
["C"] = V1 - V3
}
local TriNormal = TriLines.A:Cross(TriLines.C).Unit --used to tell if the point is on the tri, doesn't really matter if it's backwards or forwards
local TriCFrame = CFrame.new(TriCenter, TriCenter + TriNormal)
--==localspace conversions==--
--[[
the cframe helps tell if the point isn't on the triangle's surface (which is known if the z axis isn't 0), because the z axis (forward &
backward) represents the normal's direction. thus, if the point's z pos isn't 0, we don't have to check if it's within the triangle.
]]
local TriLs = { --Ls > local space
["V1"] = TriCFrame:PointToObjectSpace(V1),
["V2"] = TriCFrame:PointToObjectSpace(V2),
["V3"] = TriCFrame:PointToObjectSpace(V3),
["Point"] = TriCFrame:PointToObjectSpace(Point)
}
--[[
if ProjectPoint then
TriLs.Point.Z = 0
end
]]
if TriLs.Point.Z ~= 0 and not ProjectPoint then --if the optional setting only changes this, then it's probably more compact to do this method
return false
end
--==conversion to 2d==--
--[[
honestly i don't understand what sebastian lague's code does, but it appears to be involved with algebra. his code is also intended for
triangles on 2d spaces, which is where the cframe comes in.
in lague's video, he uses letters for his vertexes instead, so here's a conversion:
V1 > A
V2 > B
V3 > C
]]
local s1 = TriLs.V3.Y - TriLs.V1.Y
local s2 = TriLs.V3.X - TriLs.V1.X
local s3 = TriLs.V2.Y - TriLs.V1.Y
local s4 = TriLs.Point.Y - TriLs.V1.Y
local w1 = (TriLs.V1.X * s1 + s4 * s2 - TriLs.Point.X * s1) / (s3 * s2 - (TriLs.V2.X - TriLs.V1.X) * s1)
local w2 = (s4- w1 * s3) / s1
return w1 >= 0 and w2 >= 0 and (w1 + w2) <= 1
end
function MathGeometry.GetClosestPointOnTriangle(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3)
--[[
==triangle legend
====
same deal as with IsPointWithin3DTriangle().
2
.
/ \
a / \ b
/_____\
1 c 3
my formatting is similar to javidx9's (seen here: https://www.youtube.com/watch?v=XgMWc6LumG4&t=285s), which orders the vertexes
and lines/edges in clockwise direction.
]]--
--==tri data==--
local TriCenter = MathVector3.Average({V1, V2, V3})
local TriLines = {
["A"] = V2 - V1,
["B"] = V3 - V2,
["C"] = V1 - V3
}
local TriNormal = TriLines.A:Cross(TriLines.C).Unit --used to tell if the point is on the tri, doesn't really matter if it's backwards or forwards
if MathGeometry.IsPointWithin3DTriangle(Point, V1, V2, V3, true) then
--[[
if the point is within the triangle, the closest point on the triangle is facing towards the given point
the same way the triangle's normal is facing.
the world point is from the origin to the point instead of the triangle's center to the point. so we're creating a version that's fit
for vector projection that start's from the triangle's center to the point instead. we can use that vector & add it up with tri center for
the closest point on the triangle.
]]
local PointFromCenter = Point - TriCenter
return TriCenter + MathVector3.ProjectToNormal(PointFromCenter, TriNormal)
end
--[[
if the point is outside the triangle, we'll just see which edge has the most closest point to the given point & return that as output.
]]
local Edges = {
["A"] = {["Start"] = V1, ["End"] = V2},
["B"] = {["Start"] = V2, ["End"] = V3},
["C"] = {["Start"] = V3, ["End"] = V1}
}
--retrieve the closest points on all edges with their distances to the given point added in
local Points = {}
for i, v in Edges do
local ClosestPoint = MathGeometry.GetClosestPointOnLine(Point, v.Start, v.End)
local DistanceToPoint = (ClosestPoint - Point).Magnitude
table.insert(Points, {["Point"] = ClosestPoint, ["Distance"] = DistanceToPoint})
end
--sort from lowest to highest
table.sort(Points, function(a,b)
return a.Distance < b.Distance
end)
--return the first listed object (should return a point with the lowest distance thanks to the sorting)
return Points[1].Point
end
--[[
this func was gonna take skewed, but symmetrical planes. it didn't go so well so i just made it suit normal square/rect planes. asymmetrical squares/rects
have a lot more variations where using the "line as coordinate axes" method wouldn't work.
if we were to go with that mind limbo, i figure that we would've done something like aligning a line perpendicular to the entire plane & calculating
where that line intersects on the start and end.
]]--
function MathGeometry.GetClosestPointOnSquarePlane(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3, V4:Vector3)
--[[
==plane legend
====
b
v2 _____ v3
| |
a | | c
|_____|
v1 d v4
as with the triangle counterpart of this function, both vertexes and lines are in clockwise order.
as we're only intending for symmetrical planes only, the average should be always at the center of the plane, so if
you were to add line a & line d, both multiplied by 0.5, to v1, we should get the same result as the average.
]]--
--==plane data==--
local PlaneCenter = MathVector3.Average({V1, V2, V3, V4})
local PlaneLines = {
["A"] = V2 - V1,
--["B"] = V3 - V2,
--["C"] = V4 - V3,
["D"] = V1 - V4
}
local PlaneNormal = PlaneLines.A:Cross(PlaneLines.D).Unit
--[[
i don't have a lot of ideas to run with for this, so i'm going to use a method that i tried on a triangle.
like a coordinate, line d & a act as x & y axes. the inverse lerp already does a projection (it uses a dot product)
so it does the heavy lifting for us
]]--
local YCoord = math.clamp(MathVector3.InverseLerp(V1, V2, Point), 0, 1)
local XCoord = math.clamp(MathVector3.InverseLerp(V1, V4, Point), 0, 1)
--[[
--this is where i tried to adapt to skewed lines.
--get the position relative to the yCoord (or moreso ratio) pos (this is because the lines can be skewed)
local XCoordStart = V1:Lerp(V2, YCoord)
local XCoordEnd = V4:Lerp(V3, YCoord)
local XCoord = math.clamp(MathVector3.InverseLerp(XCoordStart, XCoordEnd, Point), 0, 1)
]]
return V1 + (PlaneLines.A * YCoord) + (-PlaneLines.D * XCoord)
end
--==table==--
--[[
checks if a element (index or key) exists in the table. this doesn't return whatever the element is holding, because
indexes can be void. if you create a table and assign one variable in it, then assign something like thing[100] = "yeah",
everything inbetween 1 & 100 would be nil. in situations like this, indexes from 1 to 100 seems to exist, but the inbetween
elements do not hold variables.
because nil is the same as false, if the function returns a index that exists but has void/nil in it, it would return false in
a if statement, which doesn't make sense because the element clearly exists. keys however, if you assign "nil" to one, it would be
the same as not writing to that key at all.
]]--
function Table.FindElement(t, Element: number|string)
for i, v in t do
if i == Element then
return true
end
end
return false
end
--[[
this always returns a table. i'm not sure if i should make it return a string if there's only one key.
the order given is always in alphabetical order. i think this is because how tables are built. using a
for i, v loop usually gives that result.
there's no counterpart function for the indexes, because you can get the number of existing indexes with
# by slapping it to the back of a table's variable name, like so: #TableThing
]]--
function Table.GetKeys(t)
local Keys = {}
for i, v in t do
if type(i) == "string" then
table.insert(Keys, i)
end
end
return Keys
end
--[[
tries to find elements (keys & indexes) that both exist within the two tables
]]--
function Table.GetCommonElements(t1, t2)
local Output = {}
for i, v in t1 do
if Table.FindElement(t2, i) then
table.insert(Output, i)
end
end
return Output
end
--[[
opposite to GetCommonElements. only gets keys/indexes that only appears on the compare table.
]]--
function Table.GetMissingElements(Compare, On)
local Output = {}
for i, v in Compare do
if not Table.FindElement(On, i) then
table.insert(Output, i)
end
end
return Output
end
--[[
if you were fortunate enough, you'd know that writing something like local data = {something = 10} would mean you
are assigning a key to the table. that key can be accessed like a property: data.something.
in for loops, elements without keys go first, while keys go after them by alphabetical order. ex:
{10, mess = "something", schizo = "real", 2, question = "what"}
-for loop would output:-
1 > 10
2 > 2
schizo > real
mess > something
question > what
if you were to put keys with the same name, then the last one takes priority, probably due to how it overrides the old one every time.
also, keys given a nil value is pretty redundant, as it won't appear on the table after creating it & printing the table.
like searching stuff in the game from scripts, keys containing spaces would require using the "[]" brackets to get keys with
spaces. you can also do the same for assigning them.
assigning > {something = 10} or {["something bad"] = 10}
accessing> data.something or data["something bad"] (if the table is a variable named data)
for this function, if a key doesn't exist/a index is nil or nonexistant in the target table we'd create it if the key/index doesn't exist
and/or give it the same value the matching key/index has from the default table.
]]--
function Table.DefaultTo(Default, Target)
local MissingElements = Table.GetMissingElements(Default, Target)
for i, v in MissingElements do
Target[v] = Default[v]
end
return Target
end
function Table.RemoveDupes(t)
local Output = {}
for i, Var in t do
if not table.find(Output, Var) then
table.insert(Output, Var)
end
end
return Output
end
--==parts==--
--[[
originally ment to be used in conjunction with GuaranteeExistance for making creating part instances easier,
because wedge & corner wedges are seperate classes instead of shape enums.
usage: TypeTraits().X | replace "X" with Ball/Block/Cylinder/Wedge/CornerWedge.
]]--
function Parts.TypeTraits()
return {
["Ball"] = {["Class"] = "Part", ["Shape"] = Enum.PartType.Ball},
["Block"] = {["Class"] = "Part", ["Shape"] = Enum.PartType.Block},
["Cylinder"] = {["Class"] = "Part", ["Shape"] = Enum.PartType.Cylinder},
["Wedge"] = {["Class"] = "WedgePart"},
["CornerWedge"] = {["Class"] = "CornerWedgePart"}
}
end
--[[
shorthand for "if part doesn't exist, make a replacement for it", which is aimed for parts getting deleted mid-function.
had to make the replacement table include properties that are shared with parts, as a result, it's thick as a dictionary book.
obviously, not a lot of people are going to use all these details for all the properties, so we should put defaults in case they
don't specify a property.
]]--
function Parts.GuaranteeExistance(
Part: BasePart?,
Replacement:
{
--instance
Archivable:boolean?,
Locked:boolean?,
Name:string?,
Parent:userdata?,
--type
Type:{Class:string, Shape:Enum.PartType?}?,
--transform
cFrame: CFrame?, --cframe holds both pos & rot data, so why use both if you can use one?
Size: Vector3?,
--collision
CanCollide:boolean?,
CanQuery:boolean?,
CanTouch:boolean?,
CollisionGroup:string?,
--physics
Anchored:boolean?,
CustomPhysicalProperties:PhysicalProperties?,
Massless:boolean?,
AssemblyLinearVelocity:Vector3?,
AssemblyAngularVelocity:Vector3?,
Material:Enum.Material?,
MaterialVariant:string?,
RootPriority: number?,
--visuals/graphics
Color: Color3?,
Reflectance:number?,
Transparency:number?,
CastShadow:boolean?
}
)
if Part and Part.Parent ~= nil then
return Part
else
--==defaults==-- (test this: local funcs = require(game.ServerScriptService.Functions.UtilityFunctions) funcs.Part.GuaranteeExistance(nil, {}))
Replacement = Table.DefaultTo(
{
--==instance==--
["Archivable"] = true,
["Locked"] = false,
["Name"] = "Replaced",
["Parent"] = workspace,
--==type==--
["Type"] = {["Class"] = "Part", ["Shape"] = Enum.PartType.Ball},
--==transform==--
["cFrame"] = CFrame.new(),
["Size"] = Vector3.one,
--==collision==--
["CanCollide"] = false,
["CanQuery"] = false,
["CanTouch"] = false,
["CollisionGroup"] = "Default",
--==physics==--
["Anchored"] = true,
--CustomPhysicalProperties (default value is already nil, so if it's blank it'd be nil, thus kinda redundant)
["Massless"] = true,
["AssemblyLinearVelocity"] = Vector3.zero,
["AssemblyAngularVelocity"] = Vector3.zero,
["Material"] = Enum.Material.Plastic,
["MaterialVariant"] = "",
["RootPriority"] = 0,
--==visuals/graphics==--
["Color"] = Color3.new(1,1,1),
["Reflectance"] = 0,
["Transparency"] = 1,
["CastShadow"] = false,
},
Replacement
)
local ReplacementPart = Instance.new(Replacement.Type.Class)
--instance
ReplacementPart.Archivable = Replacement.Archivable
ReplacementPart.Locked = Replacement.Locked
ReplacementPart.Name = Replacement.Name
ReplacementPart.Parent = Replacement.Parent
--type (class is already involved with Instance.new(), so we just have to deal with the shape if it's a regular part.)
if Replacement.Type.Class == "Part" then
ReplacementPart.Shape = Replacement.Type.Shape
end
--transform
ReplacementPart.CFrame = Replacement.cFrame
ReplacementPart.Size = Replacement.Size
--collision
ReplacementPart.CanCollide = Replacement.CanCollide
ReplacementPart.CanQuery = Replacement.CanQuery
ReplacementPart.CanTouch = Replacement.CanTouch
ReplacementPart.CollisionGroup = Replacement.CollisionGroup
--physics
ReplacementPart.Anchored = Replacement.Anchored
ReplacementPart.CustomPhysicalProperties = Replacement.CustomPhysicalProperties
ReplacementPart.Massless = Replacement.Massless
ReplacementPart.AssemblyLinearVelocity = Replacement.AssemblyLinearVelocity
ReplacementPart.AssemblyAngularVelocity = Replacement.AssemblyAngularVelocity
ReplacementPart.Material = Replacement.Material
ReplacementPart.MaterialVariant = Replacement.MaterialVariant
ReplacementPart.RootPriority = Replacement.RootPriority
--visuals/graphics
ReplacementPart.Color = Replacement.Color
ReplacementPart.Reflectance = Replacement.Reflectance
ReplacementPart.Transparency = Replacement.Transparency
ReplacementPart.CastShadow = Replacement.CastShadow
return ReplacementPart
end
end
--[[
the result will be the same as the point if the point overlaps the part, i think it's okay because we don't have to deal with a
situation where there is more than 1 closest points which happens on a sphere/ball part if you just use it's position. this gets more complicated
with wedge parts.
]]--
function Parts.GetClosestPointOnPart(Point:Vector3, Part:BasePart) --this only works on blocks, cylinders, spheres, wedges & cornerwedges because of no mesh data access.
local ObjSpacePoint = Part.CFrame:PointToObjectSpace(Point) --convert to object space (deals with rotations)
local HalfSize = Part.Size / 2
if Part:IsA("Part") and Part.Shape == Enum.PartType.Ball then
--[[
if you were to provide a vector that has differing numbers on all axises for the part's size property when it has the shape of
a ball, it'd priortise x's number over everything else when resizing. i've got no better ideas, so i'm just gonna copy their
behaviour.
this doesn't use the conversion to local space because the rotation doesn't really matter for spheres.
]]--
local Difference = Point - Part.Position
local Radius = Part.Size.X / 2
return Part.Position + MathVector3.ClampMagnitude(Difference, Radius)
elseif Part:IsA("Part") and Part.Shape == Enum.PartType.Block then
--[[
pretty much just a spam-up of clamps.
]]--
return Part.CFrame * Vector3.new( --convert it back to worldspace
math.clamp(ObjSpacePoint.X, -HalfSize.X, HalfSize.X),
math.clamp(ObjSpacePoint.Y, -HalfSize.Y, HalfSize.Y),
math.clamp(ObjSpacePoint.Z, -HalfSize.Z, HalfSize.Z)
)
elseif Part:IsA("Part") and Part.Shape == Enum.PartType.Cylinder then
--[[
the usage of math.min is to get the right value for the cross section's/circle's radius as the smallest number from the
y or z axis actually determines the circle's radius.
the circlepos variable is attempting to get the circle position by using the object space point as a vector
then projecting it on the x axis (as thats where the circle faces). the magnitude is limited so that the circle position
doesn't overshoot the cross section on the y & z axises.
]]--
local Radius = math.min(Part.Size.Y, Part.Size.Z) / 2 --the smallest of the 2 axis determines the radius, so we have to use math.min
local CirclePos = MathVector3.ClampMagnitude(MathVector3.ProjectToNormal(ObjSpacePoint, Vector3.new(1,0,0)), Radius)
return Part.CFrame * Vector3.new(
math.clamp(ObjSpacePoint.X, -HalfSize.X, HalfSize.X),
CirclePos.Y,
CirclePos.Z
)
elseif Part:IsA("WedgePart") then
--limit point to shape
----
--[[
the function for other types of parts would give the same position as the given point if it's inside the part. for consistancy's
sake, we'll make it do the same thing.
the first thing to do is to check if the point is within the shape. we'll create a different position that is within the part's geometry
and compare it with the given point to check if it's so. if the comparision nets a match, we just return the given point. otherwise
we'll try to get the most closest point from all the faces.
y
Lz /|
front / | rear
/ |
/___|
the wedge's position always ends at the center of the sloped surface (front). this is why we're using it's size property to
locate the positions of certain points needed to find a overlap.
if you were to measure the distance from bottom to up (y), the distance would vary depending on the z axis position. if the z axis
pos was completely at the front, that distance would be zero, since the position is on a edge. completely on the rear, however,
would be the same as the wedge's y axis size. this also applies on front to rear (z).
to make this easy, we can use linear interpolation/lerp to determine the limit via position. since the slope is linear, linear
interpolation fits like a glove. lerp only takes in a value of 0 to 1, so we have to convert the respective positions to a percentage
relative to the part's size.
]]--
--==y's limit==--
--the clamp is to limit the percentage to 0% - 100%. without this, the limitation may overcompensate past the part's actual size.
local ZPosPercent = math.clamp(MathNumber.InverseLerp(-HalfSize.Z, HalfSize.Z, ObjSpacePoint.Z), 0, 1)
local YLimit = MathNumber.Lerp(-HalfSize.Y, HalfSize.Y, ZPosPercent)
--==z's limit==--
--clamp's present for the same reason y had it.
local YPosPercent = math.clamp(MathNumber.InverseLerp(-HalfSize.Y, HalfSize.Y, ObjSpacePoint.Y), 0, 1)
local ZLimit = MathNumber.Lerp(-HalfSize.Z, HalfSize.Z, YPosPercent)
--print("Zp: " .. ZPosPercent .. ", Yl: " .. YLimit .. ", Yp: " .. YPosPercent .. ", Zl: " .. ZLimit)
local ShapeLimitPos = Part.CFrame * Vector3.new(
math.clamp(ObjSpacePoint.X, -HalfSize.X, HalfSize.X),
math.clamp(ObjSpacePoint.Y, -HalfSize.Y, YLimit),
math.clamp(ObjSpacePoint.Z, -HalfSize.Z, ZLimit)
)
if Point == ShapeLimitPos then --does the point match the geometry limited version of itself?
return Point
end
--find the most closest point from all faces
----
--[[
wedges have triangle faces on their left & right (it's forward dir has the sloped face on it). we're just gonna find the
closest point for each face & return the closest one to the given point.
]]--
local Verts = {
--upper
--[[1]] Part.Position -(Part.CFrame.RightVector * HalfSize.X) +(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z), --left
--[[2]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) +(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z), --right
--lower (rear to front)
--[[3]] Part.Position -(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) +(Part.CFrame.LookVector * HalfSize.Z), --rear left
--[[4]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) +(Part.CFrame.LookVector * HalfSize.Z), --rear right
--[[5]] Part.Position -(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z), --front left
--[[6]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z) --front right
}
local ClosestPointsFromFaces = {
MathGeometry.GetClosestPointOnTriangle(Point, Verts[1], Verts[3], Verts[5]), --right face (+x)
MathGeometry.GetClosestPointOnTriangle(Point, Verts[2], Verts[4], Verts[6]), --left face (-x)
MathGeometry.GetClosestPointOnSquarePlane(Point, Verts[6], Verts[2], Verts[1], Verts[5]), --upper/front face (+y & -z)
MathGeometry.GetClosestPointOnSquarePlane(Point, Verts[6], Verts[4], Verts[3], Verts[5]), --lower face (-y)
MathGeometry.GetClosestPointOnSquarePlane(Point, Verts[3], Verts[1], Verts[2], Verts[4])--rear face (+z)
}
--retrieve the closest points on all edges with their distances to the given point added in
local Points = {}
for i, v in ClosestPointsFromFaces do
table.insert(Points, {["Point"] = v, ["Distance"] = (v - Point).Magnitude})
end
--sort from lowest to highest
table.sort(Points, function(a,b)
return a.Distance < b.Distance
end)
return Points[1].Point
elseif Part:IsA("CornerWedgePart") then
--limit point to shape
----
--[[
mostly same deal with a regular wedge.
T x | y | y
z | L z | L x
___ | |
| /| | |\ | /|
| / | | | \ | / |
|/__| | l__\ | /__|
| |
notice that on the last 2 representations, that if a point were to go from bottom to up, there be lesser
possible room on the wedge's cross section. if the point is at 100% y (at the tip), then it's position would be
the same position as the upper vertex on the corner wedge. the deal being made here is that if the point is right
on the top of the corner wedge, the position it'll be limited to is a mere point.
]]--
local YPercentage = math.clamp(MathVector3.InverseLerp(Vector3.new(0, -HalfSize.Y, 0), Vector3.new(0, HalfSize.Y, 0), ObjSpacePoint), 0, 1)
--the "max" val should be pointing towards the upper vert of the corner wedge for each axis
local XLimit = MathNumber.Lerp(-HalfSize.X, HalfSize.X, YPercentage)
local ZLimit = MathNumber.Lerp(HalfSize.Z, -HalfSize.Z, YPercentage)
--resorting to inverselerp again because of "max must be greater than or equal to min" errors regarding clamping those values
local XPercentage = math.clamp(MathNumber.InverseLerp(XLimit, HalfSize.X, ObjSpacePoint.X), 0, 1)
local ZPercentage = math.clamp(MathNumber.InverseLerp(ZLimit, -HalfSize.Z, ObjSpacePoint.Z), 0, 1)
local ShapeLimitPos = Part.CFrame * Vector3.new(
MathNumber.Lerp(XLimit, HalfSize.X, XPercentage), --math.clamp(ObjSpacePoint.X, -HalfSize.X, XLimit) --keep getting clamp errors with this config
math.clamp(ObjSpacePoint.Y, -HalfSize.Y, HalfSize.Y),
MathNumber.Lerp(ZLimit, -HalfSize.Z, ZPercentage) --math.clamp(ObjSpacePoint.Z, ZLimit, HalfSize.Z)
)
--print("hs: " .. tostring(HalfSize) .. ", slp: x=" .. ShapeLimitPos.X .. ", y=" .. ShapeLimitPos.Y .. ", z=" ..ShapeLimitPos.Z)
--print("xp:" .. XPercentage .. ", zp:" .. ZPercentage)
if Point == ShapeLimitPos then
return Point
end
--find the most closest point from all faces
----
local Verts = {
--upper
--[[1]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) +(Part.CFrame.UpVector * HalfSize.Y) +(Part.CFrame.LookVector * HalfSize.Z),
--lower
--[[2]] Part.Position -(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) +(Part.CFrame.LookVector * HalfSize.Z), --rear left
--[[3]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) +(Part.CFrame.LookVector * HalfSize.Z), --rear right
--[[4]] Part.Position -(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z), --front left
--[[5]] Part.Position +(Part.CFrame.RightVector * HalfSize.X) -(Part.CFrame.UpVector * HalfSize.Y) -(Part.CFrame.LookVector * HalfSize.Z) --front right
}
local ClosestPointsFromFaces = {
--triangles
MathGeometry.GetClosestPointOnTriangle(Point, Verts[3], Verts[1], Verts[5]), --right side triangle face (+x)
MathGeometry.GetClosestPointOnTriangle(Point, Verts[4], Verts[1], Verts[2]), --left slant triangle face (-x)
MathGeometry.GetClosestPointOnTriangle(Point, Verts[2], Verts[1], Verts[3]), --upper/rear triangle face (+y & +z)
MathGeometry.GetClosestPointOnTriangle(Point, Verts[5], Verts[1], Verts[4]), --front face (-z)
--planes/quads?
MathGeometry.GetClosestPointOnSquarePlane(Point, Verts[2], Verts[4], Verts[5], Verts[3])
}
--retrieve the closest points on all edges with their distances to the given point added in
local Points = {}
for i, v in ClosestPointsFromFaces do
table.insert(Points, {["Point"] = v, ["Distance"] = (v - Point).Magnitude})
end
--sort from lowest to highest
table.sort(Points, function(a,b)
return a.Distance < b.Distance
end)
return Points[1].Point
else--if Part:IsA("UnionOperation") then
return nil
end
end
--==Graphics==--
--[[
this is intended to modify parts that already have a beam instance in them already set up (which would be given in BeamPart &
BeamInstance), otherwise it creates a replacement/a new one for convenience.
]]
function Graphics.Ray(
BeamPart:BasePart?,
BeamInstance:Beam?,
BeamProperties:
{
--ray positions
Origin:Vector3,
End:Vector3,
--core
Enabled:boolean?,
--dimensions
FaceCamera:boolean?,
ZOffset:number?,
Width:number|{Start:number, End:number}|nil,
--color & lighting
Color:Color3|ColorSequence|nil,
Brightness:number?,
Transparency:number|NumberSequence|nil,
LightEmission:number?,
LightInfluence:number?,
--texture
Texture:string?,
TextureLength:number?,
TextureMode:Enum.TextureMode?,
TextureSpeed:number?
}
)
--defaults
----
--==properties==--
BeamProperties = Table.DefaultTo(
{
--==core==--
["Enabled"] = true,
--==dimensions==--
["FaceCamera"] = true,
["ZOffset"] = 0,
["Width"] = 0.1,
--==color & lighting==--
["Color"] = Color3.new(1,1,1),
["Transparency"] = 0,
["Brightness"] = 1,
["LightEmission"] = 0,
["LightInfluence"] = 0,
--==texture==--
["Texture"] = "",
["TextureLength"] = 1,
["TextureMode"] = Enum.TextureMode.Stretch,
["TextureSpeed"] = 1
},
BeamProperties
)
--==part==--
BeamPart = Parts.GuaranteeExistance(BeamPart, {
Type = Parts.TypeTraits().Block,
Anchored = true,
CanCollide = false,
CanQuery = false,
CanTouch = false,
CastShadow = false,
Size = Vector3.zero
})
BeamPart.CFrame = CFrame.new(BeamProperties.Origin:Lerp(BeamProperties.End, 0.5), BeamProperties.End)
BeamPart.Size = Vector3.new(0,0,(BeamProperties.End - BeamProperties.Origin).Magnitude)
--==instance==--
if not BeamInstance then --if there was no BeamInstance given (nil), search for one
BeamInstance = BeamPart:FindFirstChildOfClass("Beam")
if not BeamInstance then --if the beam can't be found, create one.
BeamInstance = Instance.new("Beam")
BeamInstance.Parent = BeamPart
end
end
--==attachments==--
--origin
if not BeamInstance.Attachment0 then
BeamInstance.Attachment0 = Instance.new("Attachment")
BeamInstance.Attachment0.Parent = BeamPart
end
BeamInstance.Attachment0.Name = "Origin"
BeamInstance.Attachment0.WorldCFrame = CFrame.new(BeamProperties.Origin)
--end
if not BeamInstance.Attachment1 then
BeamInstance.Attachment1 = Instance.new("Attachment")
BeamInstance.Attachment1.Parent = BeamPart
end
BeamInstance.Attachment1.Name = "End"
BeamInstance.Attachment1.WorldCFrame = CFrame.new(BeamProperties.End)
--update the beam's properties
----
--==core==--
BeamInstance.Enabled = BeamProperties.Enabled
--==dimensions==--
BeamInstance.FaceCamera = BeamProperties.FaceCamera
BeamInstance.ZOffset = BeamProperties.ZOffset
if typeof(BeamProperties.Width) == "number" then
BeamInstance.Width0 = BeamProperties.Width
BeamInstance.Width1 = BeamProperties.Width
end
if typeof(BeamProperties.Width) == "table" then
BeamInstance.Width0 = BeamProperties.Width[1]
BeamInstance.Width1 = BeamProperties.Width[2]
end
--==color & lighting==--
if typeof(BeamProperties.Color) == "Color3" then
BeamInstance.Color = ColorSequence.new(BeamProperties.Color)
end
if typeof(BeamProperties.Color) == "ColorSequence" then
BeamInstance.Color = BeamProperties.Color
end
if typeof(BeamProperties.Transparency) == "number" then
BeamInstance.Transparency = NumberSequence.new(BeamProperties.Transparency)
end
if typeof(BeamProperties.Transparency) == "NumberSequence" then
BeamInstance.Transparency = BeamProperties.Transparency
end
BeamInstance.LightEmission = BeamProperties.LightEmission
BeamInstance.LightInfluence = BeamProperties.LightInfluence
--==texture==--
BeamInstance.Texture = BeamProperties.Texture
BeamInstance.TextureLength = BeamProperties.TextureLength
BeamInstance.TextureMode = BeamProperties.TextureMode
BeamInstance.TextureSpeed = BeamProperties.TextureSpeed
return BeamPart, BeamInstance
end
--====--
return module
--[[
notes:
if you don't know the type of a variable, you can check it with type(). ex: type("fake") > string
function arguments can put a type requirement like so: VarName:TypeName > Health:number.
they can be also made to accept different types as well, i.e you want numbers, strings & booleans: "VarName:number|string|boolean"
this can be made optional by using "Health:number | nil" or "Health:number?"
source: https://luau-lang.org/typecheck
]]--
postscriptum edit: apparently the approach used for adapting sebastian lague’s method of detecting if a point is within a triangle into 3d doesn’t work well because attempting to point the cframe’s z axis towards the triangle’s normal direction to check if the point is off-surface involves some sort of floating point error. it works for normal wedges because the triangle faces aren’t slanted, but doesn’t for corner wedges because it has slanted triangle faces. as a result, it just gives the closest point on edges on corner wedges. i’ll come back with a fix in another edit.
fix edit: the whole point for the “IsPointWithin3DTriangle” function was to find out if i could flatten a point onto the triangle’s normal, otherwise the function for getting the closest point on a triangle resorted to searching the closest point from all the triangle’s edges. i tried thinking of just checking all of the triangle’s edges to see if the point is on the edge’s outside direction via projecting the point’s position relative to that edge’s perpendicular line. it probably sounds really silly, but it seems to work.
--==math (geometry)==--
function MathGeometry.GetClosestPointOnLine(Point:Vector3, Start:Vector3, End:Vector3) --same deal as getting the closest point on a edge
local Line = End - Start
--to treat my obessive compulsive disorder, we'll use a center of the line as the origin by using lerp
local Center = Start:Lerp(End, 0.5)
--the position point is representing has a origin of 0,0,0. you can imagine drawing a line from 0,0,0 to point as the world space vector. to "convert it into local space", we'd change the origin to the line's center.
local LocalSpacePoint = Point - Center
--project the point onto the line & limit the length of the vector. this limits the position where the line isn't pointing at & limits the position to the line's length respectively. since the origin is at the center, the length limit should be line.magnitude/2.
return Center + MathVector3.ClampMagnitude(MathVector3.ProjectToLine(LocalSpacePoint, Line), Line.Magnitude/2)
end
function MathGeometry.IsPointProjectableOn3DTriangle(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3, ProjectPoint:boolean?)
--[[
==triangle legend
====
2
.
/ \
a / \ b
/_____\
1 c 3
my formatting is similar to javidx9's (seen here: https://www.youtube.com/watch?v=XgMWc6LumG4&t=285s), which orders the vertexes
and lines/edges in clockwise direction.
i'd thought about having the vertexes being automatically assigned for a clockwise order via getting a average of all vertex positions and
creating a vector that connects to the average > vertex, then sorting the vertexes by their vector's respective dot product. but i never thought
about what would be the other "static" variable for the dot product that'd be the same during the calculation & what that vector should be, because
if we just use one of the global axes, it's likely it wouldn't match the intended behaviour of the sorting because the vector would've to be within
a circle like shape on the triangle's normal (and getting the normal without the lines isn't possible because getting the normal relies on a cross product)
]]--
--==tri data==--
local TriCenter = MathVector3.Average({V1, V2, V3})
local TriLines = {
["A"] = {["Vector"] = V2 - V1, ["Center"] = V2:Lerp(V1, 0.5)},
["B"] = {["Vector"] = V3 - V2, ["Center"] = V3:Lerp(V2, 0.5)},
["C"] = {["Vector"] = V1 - V3, ["Center"] = V1:Lerp(V3, 0.5)}
}
local TriNormal = TriLines.A.Vector:Cross(TriLines.C.Vector).Unit --used to tell if the point is on the tri, doesn't really matter if it's backwards or forwards
--[[
check if the point is on the inner side of the edge (towards center of the triangle)
if either edge has the point outwards relative to the edge, it means the point isn't projectable on the triangle.
]]
for i, Line in TriLines do
local Perpendicular = Line.Vector:Cross(TriNormal).Unit
local EdgeInbound = MathVector3.ProjectToLine(TriCenter - Line.Center, Perpendicular).Unit
if MathVector3.UnitLength(Point - Line.Center, EdgeInbound) < 0 then
return false
end
end
return true
end
function MathGeometry.GetClosestPointOnTriangle(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3)
--[[
==triangle legend
====
same deal as with IsPointProjectableOn3DTriangle().
2
.
/ \
a / \ b
/_____\
1 c 3
my formatting is similar to javidx9's (seen here: https://www.youtube.com/watch?v=XgMWc6LumG4&t=285s), which orders the vertexes
and lines/edges in clockwise direction.
]]--
--==tri data==--
local TriCenter = MathVector3.Average({V1, V2, V3})
local TriLines = {
["A"] = V2 - V1,
["B"] = V3 - V2,
["C"] = V1 - V3
}
local TriNormal = TriLines.A:Cross(TriLines.C).Unit --used to tell if the point is on the tri, doesn't really matter if it's backwards or forwards
if MathGeometry.IsPointProjectableOn3DTriangle(Point, V1, V2, V3) then
--[[
if the point is within the triangle, the closest point on the triangle is facing towards the given point
the same way the triangle's normal is facing.
the world point is from the origin to the point instead of the triangle's center to the point. so we're creating a version that's fit
for vector projection that start's from the triangle's center to the point instead. we can use that vector & add it up with tri center for
the closest point on the triangle.
]]
local PointFromCenter = Point - TriCenter
return TriCenter + MathVector3.ProjectToNormal(PointFromCenter, TriNormal)
end
--[[
if the point is outside the triangle, we'll just see which edge has the most closest point to the given point & return that as output.
]]
local Edges = {
["A"] = {["Start"] = V1, ["End"] = V2},
["B"] = {["Start"] = V2, ["End"] = V3},
["C"] = {["Start"] = V3, ["End"] = V1}
}
--retrieve the closest points on all edges with their distances to the given point added in
local Points = {}
for i, v in Edges do
local ClosestPoint = MathGeometry.GetClosestPointOnLine(Point, v.Start, v.End)
local DistanceToPoint = (ClosestPoint - Point).Magnitude
table.insert(Points, {["Point"] = ClosestPoint, ["Distance"] = DistanceToPoint})
end
--sort from lowest to highest
table.sort(Points, function(a,b)
return a.Distance < b.Distance
end)
--return the first listed object (should return a point with the lowest distance thanks to the sorting)
return Points[1].Point
end
--[[
this func was gonna take skewed, but symmetrical planes. it didn't go so well so i just made it suit normal square/rect planes. asymmetrical squares/rects
have a lot more variations where using the "line as coordinate axes" method wouldn't work.
if we were to go with that mind limbo, i figure that we would've done something like aligning a line perpendicular to the entire plane & calculating
where that line intersects on the start and end.
]]--
function MathGeometry.GetClosestPointOnSquarePlane(Point:Vector3, V1:Vector3, V2:Vector3, V3:Vector3, V4:Vector3)
--[[
==plane legend
====
b
v2 _____ v3
| |
a | | c
|_____|
v1 d v4
as with the triangle counterpart of this function, both vertexes and lines are in clockwise order.
as we're only intending for symmetrical planes only, the average should be always at the center of the plane, so if
you were to add line a & line d, both multiplied by 0.5, to v1, we should get the same result as the average.
]]--
--==plane data==--
local PlaneCenter = MathVector3.Average({V1, V2, V3, V4})
local PlaneLines = {
["A"] = V2 - V1,
--["B"] = V3 - V2,
--["C"] = V4 - V3,
["D"] = V1 - V4
}
local PlaneNormal = PlaneLines.A:Cross(PlaneLines.D).Unit
--[[
i don't have a lot of ideas to run with for this, so i'm going to use a method that i tried on a triangle.
like a coordinate, line d & a act as x & y axes. the inverse lerp already does a projection (it uses a dot product)
so it does the heavy lifting for us
]]--
local YCoord = math.clamp(MathVector3.InverseLerp(V1, V2, Point), 0, 1)
local XCoord = math.clamp(MathVector3.InverseLerp(V1, V4, Point), 0, 1)
--[[
--this is where i tried to adapt to skewed lines.
--get the position relative to the yCoord (or moreso ratio) pos (this is because the lines can be skewed)
local XCoordStart = V1:Lerp(V2, YCoord)
local XCoordEnd = V4:Lerp(V3, YCoord)
local XCoord = math.clamp(MathVector3.InverseLerp(XCoordStart, XCoordEnd, Point), 0, 1)
]]
return V1 + (PlaneLines.A * YCoord) + (-PlaneLines.D * XCoord)
end
you can just replace everything in the “–==math geometry==–” section on the previous module script with this block of code. the only change from the previous to this was “IsPointWithin3DTriangle” being replaced by “IsPointProjectableOn3DTriangle”.
another edit: just fixing a small mistake. the “ClampMagnitude” function had “Vector3” instead of “number” for the 2nd argument’s type checking.