@quenty/geometryutils 6.8.1 → 6.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,17 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [6.8.2](https://github.com/Quenty/NevermoreEngine/compare/@quenty/geometryutils@6.8.1...@quenty/geometryutils@6.8.2) (2025-04-05)
7
+
8
+
9
+ ### Bug Fixes
10
+
11
+ * Add types to packages ([2374fb2](https://github.com/Quenty/NevermoreEngine/commit/2374fb2b043cfbe0e9b507b3316eec46a4e353a0))
12
+
13
+
14
+
15
+
16
+
6
17
  ## [6.8.1](https://github.com/Quenty/NevermoreEngine/compare/@quenty/geometryutils@6.8.0...@quenty/geometryutils@6.8.1) (2025-03-21)
7
18
 
8
19
  **Note:** Version bump only for package @quenty/geometryutils
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/geometryutils",
3
- "version": "6.8.1",
3
+ "version": "6.8.2",
4
4
  "description": "Utility functions involving 3D and 2D geometry",
5
5
  "keywords": [
6
6
  "Roblox",
@@ -28,8 +28,8 @@
28
28
  "access": "public"
29
29
  },
30
30
  "dependencies": {
31
- "@quenty/draw": "^7.8.1",
32
- "@quenty/loader": "^10.8.0"
31
+ "@quenty/draw": "^7.8.2",
32
+ "@quenty/loader": "^10.8.1"
33
33
  },
34
- "gitHead": "6b7c3e15e60cdb185986207b574e2b5591261e7a"
34
+ "gitHead": "78c3ac0ab08dd18085b6e6e6e4f745e76ed99f68"
35
35
  }
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  @class CameraPyramidUtils
3
4
  ]=]
@@ -9,7 +10,10 @@ local Draw = require("Draw")
9
10
 
10
11
  local CameraPyramidUtils = {}
11
12
 
12
-
13
+ type InBoundIntersection = {
14
+ point: Vector3,
15
+ dist: number,
16
+ }
13
17
 
14
18
  --[=[
15
19
  Treating the camera like a pyramid, compute points on the screen that the ray intersects with the
@@ -25,23 +29,28 @@ local CameraPyramidUtils = {}
25
29
  @return Vector3? -- Screen point1
26
30
  @return Vector3? -- Screen point2
27
31
  ]=]
28
- function CameraPyramidUtils.rayIntersection(camera, rayOrigin, unitRayDirection, debugMaid)
32
+ function CameraPyramidUtils.rayIntersection(
33
+ camera: Camera,
34
+ rayOrigin: Vector3,
35
+ unitRayDirection: Vector3,
36
+ debugMaid: any?
37
+ ): (Vector3?, Vector3?)
29
38
  assert(typeof(rayOrigin) == "Vector3", "Bad rayOrigin")
30
39
  assert(typeof(unitRayDirection) == "Vector3", "Bad unitRayDirection")
31
40
 
32
- unitRayDirection = unitRayDirection.unit
41
+ unitRayDirection = unitRayDirection.Unit
33
42
  local camCFrame = camera.CFrame
34
43
  local viewportSize = camera.ViewportSize
35
- if viewportSize.x <= 0 or viewportSize.y <= 0 then
44
+ if viewportSize.X <= 0 or viewportSize.Y <= 0 then
36
45
  return nil, nil
37
46
  end
38
47
 
39
- local aspectRatio = viewportSize.x/viewportSize.y
40
- local halfVerticalFov = math.rad(camera.FieldOfView/2)
41
- local halfHorizontalFov = math.atan(math.tan(halfVerticalFov)*aspectRatio)
48
+ local aspectRatio = viewportSize.X / viewportSize.Y
49
+ local halfVerticalFov = math.rad(camera.FieldOfView / 2)
50
+ local halfHorizontalFov = math.atan(math.tan(halfVerticalFov) * aspectRatio)
42
51
 
43
52
  -- Construct pyramid with normals facing out
44
- local origin = camCFrame.p
53
+ local origin = camCFrame.Position
45
54
  local cframeTop = (camCFrame * CFrame.Angles(halfVerticalFov, 0, 0))
46
55
  local cframeBottom = (camCFrame * CFrame.Angles(-halfVerticalFov, 0, 0))
47
56
  local cframeLeft = (camCFrame * CFrame.Angles(0, halfHorizontalFov, 0))
@@ -62,42 +71,44 @@ function CameraPyramidUtils.rayIntersection(camera, rayOrigin, unitRayDirection,
62
71
  local leftInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionLeft, halfVerticalFov, false)
63
72
  local rightInBounds = CameraPyramidUtils._isInBounds(camCFrame, intersectionRight, halfVerticalFov, false)
64
73
 
65
- local inBoundsIntersections = {}
66
- if topInBounds then
74
+ local inBoundsIntersections: { InBoundIntersection } = {}
75
+ if topInBounds and intersectionTop and distTop then
67
76
  table.insert(inBoundsIntersections, {
68
- point = intersectionTop;
69
- dist = distTop;
77
+ point = intersectionTop,
78
+ dist = distTop,
70
79
  })
71
80
  end
72
- if bottomInBounds then
81
+ if bottomInBounds and intersectionBottom and distBottom then
73
82
  table.insert(inBoundsIntersections, {
74
- point = intersectionBottom;
75
- dist = distBottom;
83
+ point = intersectionBottom,
84
+ dist = distBottom,
76
85
  })
77
86
  end
78
- if leftInBounds then
87
+ if leftInBounds and intersectionLeft and distLeft then
79
88
  table.insert(inBoundsIntersections, {
80
- point = intersectionLeft;
81
- dist = distLeft;
89
+ point = intersectionLeft,
90
+ dist = distLeft,
82
91
  })
83
92
  end
84
- if rightInBounds then
93
+ if rightInBounds and intersectionRight and distRight then
85
94
  table.insert(inBoundsIntersections, {
86
- point = intersectionRight;
87
- dist = distRight;
95
+ point = intersectionRight,
96
+ dist = distRight,
88
97
  })
89
98
  end
90
99
 
91
100
  if debugMaid then
92
101
  debugMaid._top = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionTop, topInBounds)
93
- debugMaid._bottom = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionBottom, bottomInBounds)
102
+ debugMaid._bottom =
103
+ CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionBottom, bottomInBounds)
94
104
  debugMaid._left = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionLeft, leftInBounds)
95
- debugMaid._right = CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionRight, rightInBounds)
105
+ debugMaid._right =
106
+ CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersectionRight, rightInBounds)
96
107
  end
97
108
 
98
109
  if #inBoundsIntersections == 0 then
99
110
  return nil, nil
100
- elseif #inBoundsIntersections == 1 then
111
+ elseif #inBoundsIntersections == 1 then
101
112
  -- Happens when the other point fades off into the distance on this one screen such that it never ends
102
113
  local data = inBoundsIntersections[1]
103
114
  local intersection = data.point
@@ -136,44 +147,49 @@ function CameraPyramidUtils.rayIntersection(camera, rayOrigin, unitRayDirection,
136
147
  end
137
148
  end
138
149
 
139
- function CameraPyramidUtils._drawIntersection(camera, unitRayDirection, intersection, inBounds)
140
- if not inBounds then
150
+ function CameraPyramidUtils._drawIntersection(
151
+ camera: Camera,
152
+ unitRayDirection: Vector3,
153
+ intersection: Vector3?,
154
+ inBounds: boolean
155
+ ): Instance?
156
+ if not inBounds or not intersection then
141
157
  return nil
142
158
  end
143
159
 
144
- local halfVerticalFov = math.rad(camera.FieldOfView/2)
160
+ local halfVerticalFov = math.rad(camera.FieldOfView / 2)
145
161
  local viewportSize = camera.ViewportSize
146
162
  local PIXELS_DIAMETER = 40
147
- local PIXELS_OFFSET = 5 + PIXELS_DIAMETER/2
163
+ local PIXELS_OFFSET = 5 + PIXELS_DIAMETER / 2
148
164
 
149
165
  local position = camera:WorldToViewportPoint(intersection)
150
- local dist = position.z
151
- local worldHeight = 2*math.tan(halfVerticalFov)*dist
152
- local scale = worldHeight/viewportSize.y
166
+ local dist = position.Z
167
+ local worldHeight = 2 * math.tan(halfVerticalFov) * dist
168
+ local scale = worldHeight / viewportSize.Y
153
169
 
154
- local firstPoint = intersection + PIXELS_OFFSET*unitRayDirection*scale
155
- local secondPoint = intersection - PIXELS_OFFSET*unitRayDirection*scale
170
+ local firstPoint = intersection + PIXELS_OFFSET * unitRayDirection * scale
171
+ local secondPoint = intersection - PIXELS_OFFSET * unitRayDirection * scale
156
172
 
157
173
  local _, onScreen1 = camera:WorldToViewportPoint(firstPoint)
158
174
  local _, onScreen2 = camera:WorldToViewportPoint(secondPoint)
159
175
 
160
176
  local color = Color3.new(1, 1, 0)
161
177
  if onScreen1 then
162
- return Draw.point(firstPoint, color, nil, PIXELS_DIAMETER*scale)
178
+ return Draw.point(firstPoint, color, nil, PIXELS_DIAMETER * scale)
163
179
  elseif onScreen2 then
164
- return Draw.point(secondPoint, color, nil, PIXELS_DIAMETER*scale)
180
+ return Draw.point(secondPoint, color, nil, PIXELS_DIAMETER * scale)
165
181
  else
166
182
  return nil
167
183
  end
168
184
  end
169
185
 
170
- function CameraPyramidUtils._isInBounds(camCFrame, intersection, halfFov, isVertical)
186
+ function CameraPyramidUtils._isInBounds(camCFrame: CFrame, intersection: Vector3?, halfFov: number, isVertical: boolean): boolean
171
187
  if not intersection then
172
188
  return false
173
189
  end
174
190
 
175
- local relative = camCFrame:pointToObjectSpace(intersection)
176
- local dist = -relative.z
191
+ local relative = camCFrame:PointToObjectSpace(intersection)
192
+ local dist = -relative.Z
177
193
 
178
194
  if dist < 0 then
179
195
  return false
@@ -182,9 +198,9 @@ function CameraPyramidUtils._isInBounds(camCFrame, intersection, halfFov, isVert
182
198
  -- Discard the other information (we're projecting onto the flat camera plane)
183
199
  local horizontalDist
184
200
  if isVertical then
185
- horizontalDist = math.abs(relative.x)
201
+ horizontalDist = math.abs(relative.X)
186
202
  else
187
- horizontalDist = math.abs(relative.y)
203
+ horizontalDist = math.abs(relative.Y)
188
204
  end
189
205
 
190
206
  local angle = math.atan2(horizontalDist, dist)
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  @class CircleUtils
3
4
  ]=]
@@ -13,7 +14,7 @@ local CircleUtils = {}
13
14
  @param circumference number
14
15
  @return number
15
16
  ]=]
16
- function CircleUtils.updatePositionToSmallestDistOnCircle(position, target, circumference)
17
+ function CircleUtils.updatePositionToSmallestDistOnCircle(position: number, target: number, circumference: number): number
17
18
  assert(target >= 0 and target <= circumference, "Target must be between 0 and circumference")
18
19
 
19
20
  if math.abs(position - target) <= circumference/2 then
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  @class Line
3
4
  ]=]
@@ -10,25 +11,27 @@ local Line = {}
10
11
  @param r Vector3 -- Offset from a
11
12
  @param b Vector3
12
13
  @param s Vector3
14
+ @return Vector3
15
+ @return Vector3
13
16
  ]=]
14
- function Line.intersection(a, r, b, s)
15
- local q = a - b;
17
+ function Line.intersection(a: Vector3, r: Vector3, b: Vector3, s: Vector3): (Vector3, Vector3)
18
+ local q = a - b
16
19
 
17
- local dotqr = q:Dot(r); -- same as: r:Dot(q)
18
- local dotqs = q:Dot(s); -- same as: s:Dot(q)
19
- local dotrs = r:Dot(s); -- same as: s:Dot(r)
20
- local dotrr = r:Dot(r);
21
- local dotss = s:Dot(s);
20
+ local dotqr = q:Dot(r) -- same as: r:Dot(q)
21
+ local dotqs = q:Dot(s) -- same as: s:Dot(q)
22
+ local dotrs = r:Dot(s) -- same as: s:Dot(r)
23
+ local dotrr = r:Dot(r)
24
+ local dotss = s:Dot(s)
22
25
 
23
- local denom = dotrr * dotss - dotrs * dotrs;
24
- local numer = dotqs * dotrs - dotqr * dotss;
26
+ local denom = dotrr * dotss - dotrs * dotrs
27
+ local numer = dotqs * dotrs - dotqr * dotss
25
28
 
26
- local t = numer / denom;
27
- local u = (dotqs + t * dotrs) / dotss;
29
+ local t = numer / denom
30
+ local u = (dotqs + t * dotrs) / dotss
28
31
 
29
32
  -- return the two points on each line that make up the shortest line
30
- local p0, p1 = a + t * r, b + u * s;
31
- return p0, p1;
33
+ local p0, p1 = a + t * r, b + u * s
34
+ return p0, p1
32
35
  end
33
36
 
34
37
  return Line
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Utilities involving orthogonal Vector3s
3
4
  @class OrthogonalUtils
@@ -10,8 +11,8 @@ local OrthogonalUtils = {}
10
11
  @param cframe CFrame
11
12
  return { Vector3 }
12
13
  ]=]
13
- function OrthogonalUtils.decomposeCFrameToVectors(cframe)
14
- return {
14
+ function OrthogonalUtils.decomposeCFrameToVectors(cframe: CFrame): { Vector3 }
15
+ return {
15
16
  cframe.LookVector, -- front
16
17
  -cframe.LookVector,
17
18
  cframe.RightVector,
@@ -28,10 +29,10 @@ end
28
29
  @param unitVector Vector3
29
30
  return Vector3?
30
31
  ]=]
31
- function OrthogonalUtils.getClosestVector(options, unitVector)
32
+ function OrthogonalUtils.getClosestVector(options: { Vector3 }, unitVector: Vector3): Vector3?
32
33
  local best = nil
33
34
  local bestAngle = -math.huge
34
- for _, option in pairs(options) do
35
+ for _, option in options do
35
36
  local dotAngle = option:Dot(unitVector)
36
37
  if dotAngle > bestAngle then
37
38
  bestAngle = dotAngle
@@ -49,7 +50,7 @@ end
49
50
  @param snapToCFrame CFrame
50
51
  return CFrame
51
52
  ]=]
52
- function OrthogonalUtils.snapCFrameTo(cframe, snapToCFrame)
53
+ function OrthogonalUtils.snapCFrameTo(cframe: CFrame, snapToCFrame: CFrame): CFrame
53
54
  local options = OrthogonalUtils.decomposeCFrameToVectors(snapToCFrame)
54
55
  local rightVector = OrthogonalUtils.getClosestVector(options, cframe.RightVector)
55
56
  local upVector = OrthogonalUtils.getClosestVector(options, cframe.UpVector)
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Utility functions involving planes!
3
4
  @class PlaneUtils
@@ -16,13 +17,14 @@ local PlaneUtils = {}
16
17
  @param rayOrigin Vector3
17
18
  @param unitRayDirection Vector3
18
19
  @return Vector3? -- Intersection point
20
+ @return number? -- Distance to intersection point
19
21
  ]=]
20
- function PlaneUtils.rayIntersection(origin, normal, rayOrigin, unitRayDirection)
22
+ function PlaneUtils.rayIntersection(origin: Vector3, normal: Vector3, rayOrigin: Vector3, unitRayDirection: Vector3): (Vector3?, number?)
21
23
  local rpoint = rayOrigin - origin
22
24
  local dot = unitRayDirection:Dot(normal)
23
25
  if dot == 0 then
24
26
  -- Parallel
25
- return nil
27
+ return nil, nil
26
28
  end
27
29
 
28
30
  local t = -rpoint:Dot(normal) / dot
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Utility methods to scale a model
3
4
  @class ScaleModelUtils
@@ -6,8 +7,8 @@
6
7
  local ScaleModelUtils = {}
7
8
 
8
9
  local CLASS_NAME_TO_MIN_SIZE = {
9
- ["TrussPart"] = Vector3.new(2, 2, 2);
10
- ["UnionOperation"] = Vector3.zero;
10
+ ["TrussPart"] = Vector3.new(2, 2, 2),
11
+ ["UnionOperation"] = Vector3.zero,
11
12
  }
12
13
 
13
14
  local MIN_PART_SIZE = Vector3.new(0.05, 0.05, 0.05)
@@ -17,7 +18,7 @@ local MIN_PART_SIZE = Vector3.new(0.05, 0.05, 0.05)
17
18
  @param part BasePart
18
19
  @param scale number
19
20
  ]=]
20
- function ScaleModelUtils.scalePartSize(part, scale)
21
+ function ScaleModelUtils.scalePartSize(part: BasePart, scale: Vector3 | number)
21
22
  local partSize = part.Size
22
23
 
23
24
  local mesh = part:FindFirstChildWhichIsA("DataModelMesh")
@@ -33,14 +34,12 @@ function ScaleModelUtils.scalePartSize(part, scale)
33
34
 
34
35
  local minSize = CLASS_NAME_TO_MIN_SIZE[part.ClassName] or MIN_PART_SIZE
35
36
 
36
- if newPartSize.X < minSize.X
37
- or newPartSize.Y < minSize.Y
38
- or newPartSize.Z < minSize.Z then
39
-
37
+ if newPartSize.X < minSize.X or newPartSize.Y < minSize.Y or newPartSize.Z < minSize.Z then
40
38
  newPartSize = Vector3.new(
41
39
  math.max(newPartSize.X, minSize.X),
42
40
  math.max(newPartSize.Y, minSize.Y),
43
- math.max(newPartSize.Z, minSize.Z))
41
+ math.max(newPartSize.Z, minSize.Z)
42
+ )
44
43
 
45
44
  -- We need a mesh for scaling (hopefully)
46
45
  mesh = ScaleModelUtils.createMeshFromPart(part)
@@ -49,7 +48,7 @@ function ScaleModelUtils.scalePartSize(part, scale)
49
48
  part.Size = newPartSize
50
49
 
51
50
  if mesh then
52
- mesh.Scale = newRenderSize/newPartSize
51
+ mesh.Scale = newRenderSize / newPartSize
53
52
  mesh.Offset = mesh.Offset * scale
54
53
  end
55
54
  end
@@ -61,7 +60,7 @@ end
61
60
  @param scale number
62
61
  @param centroid Vector3
63
62
  ]=]
64
- function ScaleModelUtils.scalePart(part, scale, centroid)
63
+ function ScaleModelUtils.scalePart(part: BasePart, scale: Vector3 | number, centroid: Vector3)
65
64
  assert(typeof(part) == "Instance" and part:IsA("BasePart"), "Bad part")
66
65
 
67
66
  local partPosition = part.Position
@@ -80,8 +79,8 @@ end
80
79
  @param scale number -- The scale to scale by
81
80
  @param centroid Vector3 -- the center to scale by
82
81
  ]=]
83
- function ScaleModelUtils.scale(parts, scale, centroid)
84
- for _, part in pairs(parts) do
82
+ function ScaleModelUtils.scale(parts: { BasePart }, scale: number, centroid: Vector3)
83
+ for _, part in parts do
85
84
  ScaleModelUtils.scalePart(part, scale, centroid)
86
85
  end
87
86
  end
@@ -92,7 +91,7 @@ end
92
91
  @param part BasePart
93
92
  @return Mesh?
94
93
  ]=]
95
- function ScaleModelUtils.createMeshFromPart(part)
94
+ function ScaleModelUtils.createMeshFromPart(part: BasePart): FileMesh?
96
95
  if part:IsA("WedgePart") then
97
96
  local mesh = Instance.new("SpecialMesh")
98
97
  mesh.MeshType = Enum.MeshType.Wedge
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Utility functions involving spheres
3
4
  @class SphereUtils
@@ -15,9 +16,9 @@ local SphereUtils = {}
15
16
  @return boolean
16
17
  ]=]
17
18
  function SphereUtils.intersectsRay(
18
- sphereCenter, sphereRadius,
19
- rayOrigin, rayDirection
20
- )
19
+ sphereCenter: Vector3, sphereRadius: number,
20
+ rayOrigin: Vector3, rayDirection: Vector3
21
+ ): boolean
21
22
  local relOrigin = rayOrigin - sphereCenter
22
23
  local rr = relOrigin:Dot(relOrigin)
23
24
  local dr = rayDirection:Dot(relOrigin)
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Utility functions for surfaces
3
4
  @class SurfaceUtils
@@ -7,9 +8,9 @@ local SurfaceUtils = {}
7
8
 
8
9
  local UP = Vector3.new(0, 1, 0)
9
10
  local BACK = Vector3.new(0, 0, 1)
10
- local EXTRASPIN = CFrame.fromEulerAnglesXYZ(math.pi/2, 0, 0)
11
+ local EXTRASPIN = CFrame.fromEulerAnglesXYZ(math.pi / 2, 0, 0)
11
12
 
12
- local function getTranstionBetween(v1, v2, pitchAxis)
13
+ local function getTranstionBetween(v1: Vector3, v2: Vector3, pitchAxis: Vector3): CFrame
13
14
  local dot = v1:Dot(v2)
14
15
  if dot > 0.99999 then
15
16
  return CFrame.new()
@@ -30,7 +31,7 @@ end
30
31
  @param lnormal Vector3
31
32
  @return CFrame
32
33
  ]=]
33
- function SurfaceUtils.getSurfaceCFrame(part, lnormal)
34
+ function SurfaceUtils.getSurfaceCFrame(part: BasePart, lnormal: Vector3): CFrame
34
35
  local transition = getTranstionBetween(UP, lnormal, BACK)
35
36
  return part.CFrame * transition * EXTRASPIN
36
37
  end
@@ -1,3 +1,4 @@
1
+ --!strict
1
2
  --[=[
2
3
  Authored by Egomoose, modified by Quenty
3
4
 
@@ -10,16 +11,16 @@ local SwingTwistUtils = {}
10
11
  Decomposes a CFrame into a swing and a twist.
11
12
  @param cf CFrame
12
13
  @param direction Vector3
13
- @return Vector3 -- swing
14
- @return Vector3 -- twist
14
+ @return CFrame -- swing
15
+ @return CFrame -- twist
15
16
  ]=]
16
- function SwingTwistUtils.swingTwist(cf, direction)
17
+ function SwingTwistUtils.swingTwist(cf: CFrame, direction: Vector3): (CFrame, CFrame)
17
18
  local axis, theta = cf:ToAxisAngle()
18
19
  -- convert to quaternion
19
- local w, v = math.cos(theta/2), math.sin(theta/2)*axis
20
+ local w, v = math.cos(theta / 2), math.sin(theta / 2) * axis
20
21
 
21
22
  -- (v . d)*d, plug into CFrame quaternion constructor with w it will solve rest for us
22
- local proj = v:Dot(direction)*direction
23
+ local proj = v:Dot(direction) * direction
23
24
  local twist = CFrame.new(0, 0, 0, proj.X, proj.Y, proj.Z, w)
24
25
 
25
26
  -- cf = swing * twist, thus...
@@ -33,7 +34,7 @@ end
33
34
  @param direction Vector3
34
35
  @return number
35
36
  ]=]
36
- function SwingTwistUtils.twistAngle(cf, direction)
37
+ function SwingTwistUtils.twistAngle(cf: CFrame, direction: Vector3): number
37
38
  local axis, theta = cf:ToAxisAngle()
38
39
  local w, v = math.cos(theta/2), math.sin(theta/2)*axis
39
40
  local proj = v:Dot(direction)*direction