@trioforge/crucible 0.1.0 → 0.3.0

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.
Files changed (145) hide show
  1. package/maths/spline/package.json +4 -0
  2. package/out/client/index.d.ts +7 -0
  3. package/out/client/init.luau +36 -1
  4. package/out/configs.d.ts +37 -0
  5. package/out/configs.luau +46 -0
  6. package/out/data.d.ts +224 -0
  7. package/out/data.luau +97 -0
  8. package/out/emit/configs.d.ts +4 -0
  9. package/out/emit/configs.luau +22 -0
  10. package/out/emit/data.d.ts +6 -0
  11. package/out/emit/data.luau +19 -0
  12. package/out/emit/index.d.ts +5 -1
  13. package/out/emit/init.luau +23 -7
  14. package/out/emit/network.d.ts +6 -0
  15. package/out/emit/network.luau +36 -0
  16. package/out/emit/replicated.d.ts +4 -0
  17. package/out/emit/replicated.luau +13 -0
  18. package/out/index.d.ts +8 -0
  19. package/out/init.luau +23 -0
  20. package/out/internal/binding.luau +5 -0
  21. package/out/internal/boot.luau +112 -8
  22. package/out/internal/configs/limits.d.ts +10 -0
  23. package/out/internal/configs/limits.luau +25 -0
  24. package/out/internal/configs/port.d.ts +29 -0
  25. package/out/internal/configs/port.luau +99 -0
  26. package/out/internal/configs/runtime.d.ts +21 -0
  27. package/out/internal/configs/runtime.luau +981 -0
  28. package/out/internal/data/admission.d.ts +1 -0
  29. package/out/internal/data/admission.luau +673 -0
  30. package/out/internal/data/declarations.d.ts +1 -0
  31. package/out/internal/data/declarations.luau +384 -0
  32. package/out/internal/data/envelope.d.ts +1 -0
  33. package/out/internal/data/envelope.luau +318 -0
  34. package/out/internal/data/handles.d.ts +1 -0
  35. package/out/internal/data/handles.luau +255 -0
  36. package/out/internal/data/inbox-transforms.d.ts +1 -0
  37. package/out/internal/data/inbox-transforms.luau +458 -0
  38. package/out/internal/data/inbox.d.ts +1 -0
  39. package/out/internal/data/inbox.luau +437 -0
  40. package/out/internal/data/lease.d.ts +1 -0
  41. package/out/internal/data/lease.luau +304 -0
  42. package/out/internal/data/limits.d.ts +1 -0
  43. package/out/internal/data/limits.luau +234 -0
  44. package/out/internal/data/messages.d.ts +1 -0
  45. package/out/internal/data/messages.luau +69 -0
  46. package/out/internal/data/operation-id.d.ts +1 -0
  47. package/out/internal/data/operation-id.luau +61 -0
  48. package/out/internal/data/paths.d.ts +2 -0
  49. package/out/internal/data/paths.luau +323 -0
  50. package/out/internal/data/port.d.ts +1 -0
  51. package/out/internal/data/port.luau +290 -0
  52. package/out/internal/data/profiles.d.ts +1 -0
  53. package/out/internal/data/profiles.luau +862 -0
  54. package/out/internal/data/receipts.d.ts +1 -0
  55. package/out/internal/data/receipts.luau +190 -0
  56. package/out/internal/data/record-bindings.d.ts +1 -0
  57. package/out/internal/data/record-bindings.luau +92 -0
  58. package/out/internal/data/record-declarations.d.ts +1 -0
  59. package/out/internal/data/record-declarations.luau +140 -0
  60. package/out/internal/data/records.d.ts +1 -0
  61. package/out/internal/data/records.luau +400 -0
  62. package/out/internal/data/runtime.d.ts +1 -0
  63. package/out/internal/data/runtime.luau +918 -0
  64. package/out/internal/data/scheduler.d.ts +1 -0
  65. package/out/internal/data/scheduler.luau +507 -0
  66. package/out/internal/data/session.d.ts +1 -0
  67. package/out/internal/data/session.luau +234 -0
  68. package/out/internal/data/shutdown.d.ts +1 -0
  69. package/out/internal/data/shutdown.luau +91 -0
  70. package/out/internal/data/waiters.d.ts +1 -0
  71. package/out/internal/data/waiters.luau +65 -0
  72. package/out/internal/lifecycle.luau +45 -0
  73. package/out/internal/network/catalog.d.ts +26 -0
  74. package/out/internal/network/catalog.luau +182 -0
  75. package/out/internal/network/claims.d.ts +1 -0
  76. package/out/internal/network/claims.luau +146 -0
  77. package/out/internal/network/client-session.d.ts +1 -0
  78. package/out/internal/network/client-session.luau +405 -0
  79. package/out/internal/network/codec.d.ts +63 -0
  80. package/out/internal/network/codec.luau +1183 -0
  81. package/out/internal/network/envelope.d.ts +1 -0
  82. package/out/internal/network/envelope.luau +247 -0
  83. package/out/internal/network/limits.d.ts +26 -0
  84. package/out/internal/network/limits.luau +55 -0
  85. package/out/internal/network/port.d.ts +1 -0
  86. package/out/internal/network/port.luau +19 -0
  87. package/out/internal/network/roblox-network-port.d.ts +1 -0
  88. package/out/internal/network/roblox-network-port.luau +105 -0
  89. package/out/internal/network/schema.d.ts +72 -0
  90. package/out/internal/network/schema.luau +386 -0
  91. package/out/internal/network/server-session.d.ts +1 -0
  92. package/out/internal/network/server-session.luau +395 -0
  93. package/out/internal/network/session.d.ts +1 -0
  94. package/out/internal/network/session.luau +266 -0
  95. package/out/internal/reactive/context.d.ts +1 -0
  96. package/out/internal/reactive/context.luau +149 -0
  97. package/out/internal/reactive/flush.d.ts +1 -0
  98. package/out/internal/reactive/flush.luau +355 -0
  99. package/out/internal/reactive/graph.d.ts +1 -0
  100. package/out/internal/reactive/graph.luau +626 -0
  101. package/out/internal/reactive/limits.d.ts +1 -0
  102. package/out/internal/reactive/limits.luau +54 -0
  103. package/out/internal/reactive/ownership.d.ts +1 -0
  104. package/out/internal/reactive/ownership.luau +626 -0
  105. package/out/internal/registry.luau +10 -1
  106. package/out/internal/sync/client-sync.d.ts +1 -0
  107. package/out/internal/sync/client-sync.luau +157 -0
  108. package/out/internal/sync/declarations.d.ts +1 -0
  109. package/out/internal/sync/declarations.luau +276 -0
  110. package/out/internal/sync/frame.d.ts +1 -0
  111. package/out/internal/sync/frame.luau +91 -0
  112. package/out/internal/sync/server-sync.d.ts +1 -0
  113. package/out/internal/sync/server-sync.luau +502 -0
  114. package/out/internal/sync/values.d.ts +1 -0
  115. package/out/internal/sync/values.luau +930 -0
  116. package/out/maths/spline/curve.d.ts +35 -0
  117. package/out/maths/spline/curve.luau +1122 -0
  118. package/out/maths/spline/index.d.ts +2 -0
  119. package/out/maths/spline/init.luau +8 -0
  120. package/out/network.d.ts +66 -0
  121. package/out/network.luau +24 -0
  122. package/out/reactive.d.ts +76 -0
  123. package/out/reactive.luau +136 -0
  124. package/out/replicated.d.ts +38 -0
  125. package/out/replicated.luau +45 -0
  126. package/out/server/data.d.ts +54 -0
  127. package/out/server/data.luau +44 -0
  128. package/out/server/index.d.ts +17 -0
  129. package/out/server/init.luau +58 -4
  130. package/out/server/publish.d.ts +20 -0
  131. package/out/server/publish.luau +24 -0
  132. package/out/testing/index.d.ts +38 -0
  133. package/out/testing/init.luau +189 -0
  134. package/out/util/random/index.d.ts +24 -0
  135. package/out/util/random/init.luau +301 -0
  136. package/out/vide/index.d.ts +7 -0
  137. package/out/vide/init.luau +50 -0
  138. package/package.json +28 -2
  139. package/testing/package.json +4 -0
  140. package/transformer/data.cjs +72 -0
  141. package/transformer/declarations.cjs +153 -0
  142. package/transformer/index.js +87 -23
  143. package/transformer/network.cjs +961 -0
  144. package/util/random/package.json +4 -0
  145. package/vide/package.json +4 -0
@@ -0,0 +1,1122 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ -- Arrow properties compile to dot calls, so Luau callers write curve.pointAt(t) and can detach them.
3
+ local build, catmullRomSpans
4
+ local function catmullRom(points, options)
5
+ if options == nil then
6
+ options = {}
7
+ end
8
+ local _condition = options.alpha
9
+ if _condition == nil then
10
+ _condition = 0.5
11
+ end
12
+ local alpha = _condition
13
+ if not (alpha >= 0 and alpha <= 1) then
14
+ error(`a Catmull-Rom alpha must be from 0 to 1, got {alpha}`)
15
+ end
16
+ return build(points, options, function(lifted, closed)
17
+ return catmullRomSpans(lifted, closed, alpha)
18
+ end)
19
+ end
20
+ local bezierSpans
21
+ local function cubicBezier(points, options)
22
+ if options == nil then
23
+ options = {}
24
+ end
25
+ return build(points, options, bezierSpans)
26
+ end
27
+ local bSplineSpans
28
+ local function uniformBSpline(points, options)
29
+ if options == nil then
30
+ options = {}
31
+ end
32
+ return build(points, options, bSplineSpans)
33
+ end
34
+ -- A span's squared speed as a polynomial in u, highest power first.
35
+ local UP = Vector3.new(0, 1, 0)
36
+ local AXES = { Vector3.new(1, 0, 0), UP, Vector3.new(0, 0, 1) }
37
+ local LENGTH_DEPTH = 40
38
+ local FRAME_DEPTH = 10
39
+ local FRAME_TURN = math.cos(math.rad(5))
40
+ local SEARCH_BUDGET = 20000
41
+ local GAUSS_INNER = math.sqrt(5 - 2 * math.sqrt(10 / 7)) / 3
42
+ local GAUSS_OUTER = math.sqrt(5 + 2 * math.sqrt(10 / 7)) / 3
43
+ local GAUSS = { { 0, 128 / 225 }, { GAUSS_INNER, (322 + 13 * math.sqrt(70)) / 900 }, { -GAUSS_INNER, (322 + 13 * math.sqrt(70)) / 900 }, { GAUSS_OUTER, (322 - 13 * math.sqrt(70)) / 900 }, { -GAUSS_OUTER, (322 - 13 * math.sqrt(70)) / 900 } }
44
+ local function finite(value)
45
+ return math.abs(value) < math.huge
46
+ end
47
+ local function liftPlanar(point)
48
+ local _point = point
49
+ if not (typeof(_point) == "Vector2") then
50
+ error("a Vector2 curve takes only Vector2 points")
51
+ end
52
+ if not finite(point.X) or not finite(point.Y) then
53
+ error("curve points must be finite")
54
+ end
55
+ return Vector3.new(point.X, point.Y, 0)
56
+ end
57
+ local function lowerPlanar(point)
58
+ return Vector2.new(point.X, point.Y)
59
+ end
60
+ local function liftSpatial(point)
61
+ local _point = point
62
+ if not (typeof(_point) == "Vector3") then
63
+ error("a Vector3 curve takes only Vector3 points")
64
+ end
65
+ if not finite(point.X) or not finite(point.Y) or not finite(point.Z) then
66
+ error("curve points must be finite")
67
+ end
68
+ return point
69
+ end
70
+ local function keepSpatial(point)
71
+ return point
72
+ end
73
+ local createCurve, shape
74
+ function build(points, options, toSpans)
75
+ if #points == 0 then
76
+ error("a curve needs at least one point")
77
+ end
78
+ local _condition = options.closed
79
+ if _condition == nil then
80
+ _condition = false
81
+ end
82
+ local closed = _condition
83
+ local initialNormal = options.initialNormal
84
+ local _arg0 = points[1]
85
+ if typeof(_arg0) == "Vector2" then
86
+ if initialNormal ~= nil then
87
+ error("initialNormal applies only to Vector3 curves")
88
+ end
89
+ local _fn = toSpans
90
+ -- ▼ ReadonlyArray.map ▼
91
+ local _newValue = table.create(#points)
92
+ for _k, _v in points do
93
+ _newValue[_k] = liftPlanar(_v, _k - 1, points)
94
+ end
95
+ -- ▲ ReadonlyArray.map ▲
96
+ return createCurve(shape(_fn(_newValue, closed), closed, true), liftPlanar, lowerPlanar)
97
+ end
98
+ local seed = if initialNormal == nil then nil else liftSpatial(initialNormal)
99
+ local _fn = toSpans
100
+ -- ▼ ReadonlyArray.map ▼
101
+ local _newValue = table.create(#points)
102
+ for _k, _v in points do
103
+ _newValue[_k] = liftSpatial(_v, _k - 1, points)
104
+ end
105
+ -- ▲ ReadonlyArray.map ▲
106
+ return createCurve(shape(_fn(_newValue, closed), closed, false, seed), liftSpatial, keepSpatial)
107
+ end
108
+ function shape(spans, closed, planar, initialNormal)
109
+ local low = spans[1][1]
110
+ local high = low
111
+ for _, span in spans do
112
+ for _1, control in span do
113
+ if not finite(control.X) or not finite(control.Y) or not finite(control.Z) then
114
+ error("the curve's controls overflow")
115
+ end
116
+ low = low:Min(control)
117
+ high = high:Max(control)
118
+ end
119
+ end
120
+ local _object = {
121
+ spans = spans,
122
+ closed = closed,
123
+ planar = planar,
124
+ initialNormal = initialNormal,
125
+ }
126
+ local _left = "tolerance"
127
+ local _high = high
128
+ local _low = low
129
+ _object[_left] = math.max(1e-4, 1e-5 * (_high - _low).Magnitude)
130
+ return _object
131
+ end
132
+ -- One axis of a span's velocity as a polynomial in u, constant term first.
133
+ local function hodograph(a, b, c, d)
134
+ return 3 * (b - a), 6 * (c - 2 * b + a), 3 * (d - 3 * c + 3 * b - a)
135
+ end
136
+ -- Expanded from components in doubles, so arc length integrates without Vector3 arithmetic.
137
+ local function squaredSpeed(_param)
138
+ local a = _param[1]
139
+ local b = _param[2]
140
+ local c = _param[3]
141
+ local d = _param[4]
142
+ local x0, x1, x2 = hodograph(a.X, b.X, c.X, d.X)
143
+ local y0, y1, y2 = hodograph(a.Y, b.Y, c.Y, d.Y)
144
+ local z0, z1, z2 = hodograph(a.Z, b.Z, c.Z, d.Z)
145
+ return { x2 * x2 + y2 * y2 + z2 * z2, 2 * (x2 * x1 + y2 * y1 + z2 * z1), x1 * x1 + y1 * y1 + z1 * z1 + 2 * (x2 * x0 + y2 * y0 + z2 * z0), 2 * (x1 * x0 + y1 * y0 + z1 * z0), x0 * x0 + y0 * y0 + z0 * z0 }
146
+ end
147
+ function catmullRomSpans(input, closed, alpha)
148
+ -- ▼ ReadonlyArray.filter ▼
149
+ local _newValue = {}
150
+ local _callback = function(point, index)
151
+ return index == 0 or point ~= input[index]
152
+ end
153
+ local _length = 0
154
+ for _k, _v in input do
155
+ if _callback(_v, _k - 1, input) == true then
156
+ _length += 1
157
+ _newValue[_length] = _v
158
+ end
159
+ end
160
+ -- ▲ ReadonlyArray.filter ▲
161
+ local points = _newValue
162
+ if closed and #points > 1 and points[#points] == points[1] then
163
+ points[#points] = nil
164
+ end
165
+ local count = #points
166
+ if count == 1 then
167
+ return { { points[1], points[1], points[1], points[1] } }
168
+ end
169
+ if closed and count < 3 then
170
+ error("a closed Catmull-Rom curve needs three distinct points")
171
+ end
172
+ local at = function(index)
173
+ if closed then
174
+ return points[(index + count) % count + 1]
175
+ end
176
+ if index < 0 then
177
+ local _exp = points[1] * 2
178
+ local _arg0 = points[2]
179
+ return _exp - _arg0
180
+ end
181
+ if index >= count then
182
+ local _exp = points[count] * 2
183
+ local _arg0 = points[count - 1]
184
+ return _exp - _arg0
185
+ end
186
+ return points[index + 1]
187
+ end
188
+ local last = if closed then count - 1 else count - 2
189
+ -- gaps[k] spaces at(k - 1) and at(k). Neighboring spans share them.
190
+ local gaps = {}
191
+ for index = -1, last + 1 do
192
+ local _exp = at(index + 1)
193
+ local _arg0 = at(index)
194
+ local _arg0_1 = (_exp - _arg0).Magnitude ^ alpha
195
+ table.insert(gaps, _arg0_1)
196
+ end
197
+ local span = function(index)
198
+ local p0, p1, p2, p3 = at(index - 1), at(index), at(index + 1), at(index + 2)
199
+ local d01, d12, d23 = gaps[index + 1], gaps[index + 2], gaps[index + 3]
200
+ local _exp = (p1 - p0) / d01
201
+ local _exp_1 = p2 - p0
202
+ local _arg0 = d01 + d12
203
+ local _arg0_1 = (p2 - p1) / d12
204
+ local m1 = _exp - (_exp_1 / _arg0) + _arg0_1
205
+ local _exp_2 = (p2 - p1) / d12
206
+ local _exp_3 = p3 - p1
207
+ local _arg0_2 = d12 + d23
208
+ local _arg0_3 = (p3 - p2) / d23
209
+ local m2 = _exp_2 - (_exp_3 / _arg0_2) + _arg0_3
210
+ local _arg0_4 = d12 / 3
211
+ local _arg0_5 = m1 * _arg0_4
212
+ local _exp_4 = p1 + _arg0_5
213
+ local _arg0_6 = d12 / 3
214
+ local _arg0_7 = m2 * _arg0_6
215
+ return { p1, _exp_4, p2 - _arg0_7, p2 }
216
+ end
217
+ local spans = { span(0) }
218
+ for index = 1, last do
219
+ local _arg0 = span(index)
220
+ table.insert(spans, _arg0)
221
+ end
222
+ return spans
223
+ end
224
+ function bezierSpans(points, closed)
225
+ local count = #points
226
+ if if closed then count % 3 ~= 0 else count < 4 or count % 3 ~= 1 then
227
+ error(`a {if closed then "closed cubic Bezier needs 3n" else "cubic Bezier needs 3n + 1"} points, got {count}`)
228
+ end
229
+ local span = function(start)
230
+ return { points[start + 1], points[start + 2], points[start + 3], points[(start + 3) % count + 1] }
231
+ end
232
+ local spans = { span(0) }
233
+ for index = 1, math.floor(count / 3) - 1 do
234
+ local _arg0 = span(index * 3)
235
+ table.insert(spans, _arg0)
236
+ end
237
+ return spans
238
+ end
239
+ function bSplineSpans(points, closed)
240
+ local count = #points
241
+ local least = if closed then 3 else 4
242
+ if count < least then
243
+ error(`a {if closed then "closed " else ""}uniform B-spline needs {least} points, got {count}`)
244
+ end
245
+ local at = function(index)
246
+ return points[(if closed then (index + count - 1) % count else index) + 1]
247
+ end
248
+ local thirdOrSame = function(near, far)
249
+ local _result
250
+ if near == far then
251
+ _result = near
252
+ else
253
+ local _exp = near * 2
254
+ local _far = far
255
+ _result = (_exp + _far) / 3
256
+ end
257
+ return _result
258
+ end
259
+ local jointOrSame = function(a, b, c)
260
+ local _result
261
+ if a == b and b == c then
262
+ _result = b
263
+ else
264
+ local _a = a
265
+ local _arg0 = b * 4
266
+ local _c = c
267
+ _result = (_a + _arg0 + _c) / 6
268
+ end
269
+ return _result
270
+ end
271
+ local span = function(index)
272
+ local q0, q1, q2, q3 = at(index), at(index + 1), at(index + 2), at(index + 3)
273
+ return { jointOrSame(q0, q1, q2), thirdOrSame(q1, q2), thirdOrSame(q2, q1), jointOrSame(q1, q2, q3) }
274
+ end
275
+ local spans = { span(0) }
276
+ for index = 1, (if closed then count else count - 3) - 1 do
277
+ local _arg0 = span(index)
278
+ table.insert(spans, _arg0)
279
+ end
280
+ return spans
281
+ end
282
+ local function locate(geometry, parameter)
283
+ local count = #geometry.spans
284
+ local wrapped = if geometry.closed then parameter - math.floor(parameter) else math.clamp(parameter, 0, 1)
285
+ local scaled = wrapped * count
286
+ local index = math.min(math.floor(scaled), count - 1)
287
+ return index, scaled - index
288
+ end
289
+ local function bernstein(_param, u)
290
+ local a = _param[1]
291
+ local b = _param[2]
292
+ local c = _param[3]
293
+ local d = _param[4]
294
+ local v = 1 - u
295
+ local _arg0 = v * v * v
296
+ local _exp = a * _arg0
297
+ local _arg0_1 = 3 * v * v * u
298
+ local _arg0_2 = b * _arg0_1
299
+ local _arg0_3 = 3 * v * u * u
300
+ local _arg0_4 = c * _arg0_3
301
+ local _arg0_5 = u * u * u
302
+ local _arg0_6 = d * _arg0_5
303
+ return _exp + _arg0_2 + _arg0_4 + _arg0_6
304
+ end
305
+ local function velocity(_param, u)
306
+ local a = _param[1]
307
+ local b = _param[2]
308
+ local c = _param[3]
309
+ local d = _param[4]
310
+ local v = 1 - u
311
+ local _exp = b - a
312
+ local _arg0 = 3 * v * v
313
+ local _exp_1 = c - b
314
+ local _arg0_1 = 6 * v * u
315
+ local _exp_2 = d - c
316
+ local _arg0_2 = 3 * u * u
317
+ return _exp * _arg0 + (_exp_1 * _arg0_1) + (_exp_2 * _arg0_2)
318
+ end
319
+ local function acceleration(_param, u)
320
+ local a = _param[1]
321
+ local b = _param[2]
322
+ local c = _param[3]
323
+ local d = _param[4]
324
+ local _arg0 = b * 2
325
+ local _exp = c - _arg0 + a
326
+ local _arg0_1 = 6 * (1 - u)
327
+ local _arg0_2 = c * 2
328
+ local _exp_1 = d - _arg0_2 + b
329
+ local _arg0_3 = 6 * u
330
+ return _exp * _arg0_1 + (_exp_1 * _arg0_3)
331
+ end
332
+ local function direction(vector)
333
+ local size = vector.Magnitude
334
+ return if size > 0 then vector / size else nil
335
+ end
336
+ -- Rounding can take a near-zero square slightly negative.
337
+ local function speed(_param, u)
338
+ local u4 = _param[1]
339
+ local u3 = _param[2]
340
+ local u2 = _param[3]
341
+ local u1 = _param[4]
342
+ local u0 = _param[5]
343
+ return math.sqrt(math.max((((u4 * u + u3) * u + u2) * u + u1) * u + u0, 0))
344
+ end
345
+ local function travel(_param, from, to)
346
+ local u4 = _param[1]
347
+ local u3 = _param[2]
348
+ local u2 = _param[3]
349
+ local u1 = _param[4]
350
+ local u0 = _param[5]
351
+ local half = (to - from) / 2
352
+ local middle = (from + to) / 2
353
+ local sum = 0
354
+ for _, _binding in GAUSS do
355
+ local node = _binding[1]
356
+ local weight = _binding[2]
357
+ local u = middle + half * node
358
+ sum += weight * math.sqrt(math.max((((u4 * u + u3) * u + u2) * u + u1) * u + u0, 0))
359
+ end
360
+ return sum * half
361
+ end
362
+ local function measure(geometry)
363
+ local count = #geometry.spans
364
+ local length = 0
365
+ local _exp = geometry.spans
366
+ -- ▼ ReadonlyArray.map ▼
367
+ local _newValue = table.create(#_exp)
368
+ for _k, _v in _exp do
369
+ _newValue[_k] = squaredSpeed(_v, _k - 1, _exp)
370
+ end
371
+ -- ▲ ReadonlyArray.map ▲
372
+ local quartics = _newValue
373
+ -- ▼ ReadonlyArray.map ▼
374
+ local _newValue_1 = table.create(#quartics)
375
+ local _callback = function(quartic)
376
+ local stops = { 0 }
377
+ local distances = { length }
378
+ local speeds = { speed(quartic, 0) }
379
+ local visit
380
+ visit = function(from, to, whole, depth)
381
+ local middle = (from + to) / 2
382
+ local left = travel(quartic, from, middle)
383
+ local right = travel(quartic, middle, to)
384
+ local allowed = math.max((geometry.tolerance * (to - from)) / count, 1e-6 * whole)
385
+ if depth >= 2 and math.abs(left + right - whole) <= allowed then
386
+ length += left + right
387
+ local _to = to
388
+ table.insert(stops, _to)
389
+ local _length = length
390
+ table.insert(distances, _length)
391
+ local _arg0 = speed(quartic, to)
392
+ table.insert(speeds, _arg0)
393
+ return nil
394
+ end
395
+ if depth >= LENGTH_DEPTH then
396
+ error("a curve span is too irregular to measure")
397
+ end
398
+ visit(from, middle, left, depth + 1)
399
+ visit(middle, to, right, depth + 1)
400
+ end
401
+ visit(0, 1, travel(quartic, 0, 1), 0)
402
+ return {
403
+ stops = stops,
404
+ distances = distances,
405
+ speeds = speeds,
406
+ }
407
+ end
408
+ for _k, _v in quartics do
409
+ _newValue_1[_k] = _callback(_v, _k - 1, quartics)
410
+ end
411
+ -- ▲ ReadonlyArray.map ▲
412
+ local spans = _newValue_1
413
+ local _object = {
414
+ quartics = quartics,
415
+ spans = spans,
416
+ }
417
+ local _left = "ends"
418
+ -- ▼ ReadonlyArray.map ▼
419
+ local _newValue_2 = table.create(#spans)
420
+ local _callback_1 = function(_param)
421
+ local distances = _param.distances
422
+ return distances[#distances]
423
+ end
424
+ for _k, _v in spans do
425
+ _newValue_2[_k] = _callback_1(_v, _k - 1, spans)
426
+ end
427
+ -- ▲ ReadonlyArray.map ▲
428
+ _object[_left] = _newValue_2
429
+ _object.length = length
430
+ return _object
431
+ end
432
+ local function lastAtMost(values, value)
433
+ local low = 0
434
+ local high = #values - 1
435
+ while low < high do
436
+ local middle = math.ceil((low + high) / 2)
437
+ if values[middle + 1] <= value then
438
+ low = middle
439
+ else
440
+ high = middle - 1
441
+ end
442
+ end
443
+ return low
444
+ end
445
+ local function firstAtLeast(values, value, from)
446
+ if from == nil then
447
+ from = 0
448
+ end
449
+ local low = from
450
+ local high = #values - 1
451
+ while low < high do
452
+ local middle = math.floor((low + high) / 2)
453
+ if values[middle + 1] >= value then
454
+ high = middle
455
+ else
456
+ low = middle + 1
457
+ end
458
+ end
459
+ return low
460
+ end
461
+ local function distanceAt(arc, index, u)
462
+ local _binding = arc.spans[index + 1]
463
+ local stops = _binding.stops
464
+ local distances = _binding.distances
465
+ local station = lastAtMost(stops, u)
466
+ if stops[station + 1] == u then
467
+ return distances[station + 1]
468
+ end
469
+ return distances[station + 1] + travel(arc.quartics[index + 1], stops[station + 1], u)
470
+ end
471
+ -- Inverts distance over one stop with a cubic Hermite in the stop's share of distance, using du/ds = 1 / speed.
472
+ local function hermiteGuess(low, high, run, share, startSpeed, endSpeed)
473
+ local linear = low + (high - low) * share
474
+ if not (startSpeed > 0 and endSpeed > 0) then
475
+ return linear
476
+ end
477
+ local square = share * share
478
+ local cube = square * share
479
+ local guess = low * (2 * cube - 3 * square + 1) + (run / startSpeed) * (cube - 2 * square + share) + high * (3 * square - 2 * cube) + (run / endSpeed) * (cube - square)
480
+ return if guess > low and guess < high then guess else linear
481
+ end
482
+ local function parameterAt(geometry, arc, distance)
483
+ local total = arc.length
484
+ if geometry.closed then
485
+ distance -= math.floor(distance / total) * total
486
+ end
487
+ if not (distance > 0 and distance < total) then
488
+ return if geometry.closed or distance <= 0 then 0 else 1
489
+ end
490
+ local count = #geometry.spans
491
+ local index = firstAtLeast(arc.ends, distance)
492
+ local quartic = arc.quartics[index + 1]
493
+ local _binding = arc.spans[index + 1]
494
+ local stops = _binding.stops
495
+ local distances = _binding.distances
496
+ local speeds = _binding.speeds
497
+ local station = firstAtLeast(distances, distance, 1) - 1
498
+ local from = stops[station + 1]
499
+ local start = distances[station + 1]
500
+ local run = distances[station + 2] - start
501
+ local low = from
502
+ local high = stops[station + 2]
503
+ local u = if run > 0 then hermiteGuess(low, high, run, (distance - start) / run, speeds[station + 1], speeds[station + 2]) else low
504
+ do
505
+ local step = 0
506
+ local _shouldIncrement = false
507
+ while true do
508
+ if _shouldIncrement then
509
+ step += 1
510
+ else
511
+ _shouldIncrement = true
512
+ end
513
+ if not (step < 40 and high - low > 1e-12) then
514
+ break
515
+ end
516
+ local miss = start + travel(quartic, from, u) - distance
517
+ if math.abs(miss) <= geometry.tolerance * 1e-2 then
518
+ break
519
+ end
520
+ if miss > 0 then
521
+ high = u
522
+ else
523
+ low = u
524
+ end
525
+ local pace = speed(quartic, u)
526
+ local stepped = if pace > 0 then u - miss / pace else low
527
+ u = if stepped > low and stepped < high then stepped else (low + high) / 2
528
+ end
529
+ end
530
+ return (index + u) / count
531
+ end
532
+ local function perpendicular(vector, tangent)
533
+ local _vector = vector
534
+ local _tangent = tangent
535
+ local _arg0 = tangent:Dot(vector)
536
+ local flat = _vector - (_tangent * _arg0)
537
+ local size = flat.Magnitude
538
+ return if size > 1e-4 * vector.Magnitude then flat / size else nil
539
+ end
540
+ local function seed(tangent, preferred)
541
+ if preferred == nil then
542
+ preferred = UP
543
+ end
544
+ local normal = perpendicular(preferred, tangent)
545
+ if normal ~= nil then
546
+ return normal
547
+ end
548
+ local axis = AXES[1]
549
+ for _, candidate in AXES do
550
+ if math.abs(candidate:Dot(tangent)) < math.abs(axis:Dot(tangent)) then
551
+ axis = candidate
552
+ end
553
+ end
554
+ local _axis = axis
555
+ local _tangent = tangent
556
+ local _arg0 = tangent:Dot(axis)
557
+ return (_axis - (_tangent * _arg0)).Unit
558
+ end
559
+ local function minimalRotation(from, to, normal)
560
+ local _from = from
561
+ local _to = to
562
+ local sum = _from + _to
563
+ local size = sum:Dot(sum)
564
+ if size <= 1e-12 then
565
+ return normal
566
+ end
567
+ local _normal = normal
568
+ local _from_1 = from
569
+ local _arg0 = 2 * from:Dot(normal)
570
+ local flipped = _normal - (_from_1 * _arg0)
571
+ local _arg0_1 = (2 * sum:Dot(flipped)) / size
572
+ local _arg0_2 = sum * _arg0_1
573
+ return flipped - _arg0_2
574
+ end
575
+ local function transport(from, point, tangent, tolerance)
576
+ local _point = point
577
+ local _point_1 = from.point
578
+ local chord = _point - _point_1
579
+ local c1 = chord:Dot(chord)
580
+ local normal
581
+ if c1 <= (tolerance * 1e-3) ^ 2 then
582
+ normal = minimalRotation(from.tangent, tangent, from.normal)
583
+ else
584
+ local _normal = from.normal
585
+ local _arg0 = (2 * chord:Dot(from.normal)) / c1
586
+ local _arg0_1 = chord * _arg0
587
+ local reflectedNormal = _normal - _arg0_1
588
+ local _tangent = from.tangent
589
+ local _arg0_2 = (2 * chord:Dot(from.tangent)) / c1
590
+ local _arg0_3 = chord * _arg0_2
591
+ local reflectedTangent = _tangent - _arg0_3
592
+ local second = tangent - reflectedTangent
593
+ local c2 = second:Dot(second)
594
+ local _result
595
+ if c2 > 0 then
596
+ local _arg0_4 = (2 * second:Dot(reflectedNormal)) / c2
597
+ local _arg0_5 = second * _arg0_4
598
+ _result = reflectedNormal - _arg0_5
599
+ else
600
+ _result = reflectedNormal
601
+ end
602
+ normal = _result
603
+ end
604
+ return perpendicular(normal, tangent) or seed(tangent, from.normal)
605
+ end
606
+ -- Returns the stops that have a tangent, with that tangent, which the frame table reuses.
607
+ local function frameStops(span)
608
+ local stops = {}
609
+ local tangents = {}
610
+ local keep = function(u, tangent)
611
+ if tangent == nil then
612
+ return nil
613
+ end
614
+ local _u = u
615
+ table.insert(stops, _u)
616
+ local _tangent = tangent
617
+ table.insert(tangents, _tangent)
618
+ end
619
+ local start = direction(velocity(span, 0))
620
+ keep(0, start)
621
+ local turns = function(a, b)
622
+ return if a == nil or b == nil then a ~= b else a:Dot(b) < FRAME_TURN
623
+ end
624
+ local visit
625
+ visit = function(from, fromTangent, to, toTangent, depth)
626
+ local middle = (from + to) / 2
627
+ local middleTangent = direction(velocity(span, middle))
628
+ if depth < FRAME_DEPTH and (depth < 2 or turns(fromTangent, middleTangent) or turns(middleTangent, toTangent)) then
629
+ visit(from, fromTangent, middle, middleTangent, depth + 1)
630
+ visit(middle, middleTangent, to, toTangent, depth + 1)
631
+ else
632
+ keep(to, toTangent)
633
+ end
634
+ end
635
+ visit(0, start, 1, direction(velocity(span, 1)), 0)
636
+ return stops, tangents
637
+ end
638
+ local function travelBetween(quartics, from, fromU, to, toU)
639
+ if from == to then
640
+ return travel(quartics[from + 1], fromU, toU)
641
+ end
642
+ local sum = travel(quartics[from + 1], fromU, 1) + travel(quartics[to + 1], 0, toU)
643
+ for index = from + 1, to - 1 do
644
+ sum += travel(quartics[index + 1], 0, 1)
645
+ end
646
+ return sum
647
+ end
648
+ local function transportFrames(geometry)
649
+ local positions = {}
650
+ local stations = {}
651
+ local _exp = geometry.spans
652
+ -- ▼ ReadonlyArray.forEach ▼
653
+ local _callback = function(span, index)
654
+ local stops, tangents = frameStops(span)
655
+ -- ▼ ReadonlyArray.forEach ▼
656
+ local _callback_1 = function(u, position)
657
+ local tangent = tangents[position + 1]
658
+ local point = bernstein(span, u)
659
+ local normal = if #stations == 0 then seed(tangent, geometry.initialNormal) else transport(stations[#stations], point, tangent, geometry.tolerance)
660
+ local _arg0 = index + u
661
+ table.insert(positions, _arg0)
662
+ local _arg0_1 = {
663
+ index = index,
664
+ u = u,
665
+ point = point,
666
+ tangent = tangent,
667
+ normal = normal,
668
+ }
669
+ table.insert(stations, _arg0_1)
670
+ end
671
+ for _k, _v in stops do
672
+ _callback_1(_v, _k - 1, stops)
673
+ end
674
+ -- ▲ ReadonlyArray.forEach ▲
675
+ end
676
+ for _k, _v in _exp do
677
+ _callback(_v, _k - 1, _exp)
678
+ end
679
+ -- ▲ ReadonlyArray.forEach ▲
680
+ local count = #geometry.spans
681
+ local first = stations[1]
682
+ local last = stations[#stations]
683
+ if not geometry.closed or #stations == 0 or positions[1] ~= 0 or positions[#positions] ~= count then
684
+ return {
685
+ positions = positions,
686
+ stations = stations,
687
+ seamRoll = 0,
688
+ quartics = {},
689
+ distances = {},
690
+ }
691
+ end
692
+ local arrived = minimalRotation(last.tangent, first.tangent, last.normal)
693
+ local seamRoll = math.atan2(first.tangent:Dot(arrived:Cross(first.normal)), arrived:Dot(first.normal))
694
+ local _exp_1 = geometry.spans
695
+ -- ▼ ReadonlyArray.map ▼
696
+ local _newValue = table.create(#_exp_1)
697
+ for _k, _v in _exp_1 do
698
+ _newValue[_k] = squaredSpeed(_v, _k - 1, _exp_1)
699
+ end
700
+ -- ▲ ReadonlyArray.map ▲
701
+ local quartics = _newValue
702
+ local distances = { 0 }
703
+ for index = 1, #stations - 1 do
704
+ local from, to = stations[index], stations[index + 1]
705
+ local _arg0 = distances[index] + travelBetween(quartics, from.index, from.u, to.index, to.u)
706
+ table.insert(distances, _arg0)
707
+ end
708
+ return {
709
+ positions = positions,
710
+ stations = stations,
711
+ seamRoll = seamRoll,
712
+ quartics = quartics,
713
+ distances = distances,
714
+ }
715
+ end
716
+ local roll
717
+ local function spatialNormal(geometry, frames, index, u, point, tangent)
718
+ if #frames.stations == 0 then
719
+ return seed(tangent, geometry.initialNormal)
720
+ end
721
+ local found = lastAtMost(frames.positions, index + u)
722
+ local station = frames.stations[found + 1]
723
+ local normal = transport(station, point, tangent, geometry.tolerance)
724
+ if frames.seamRoll == 0 then
725
+ return normal
726
+ end
727
+ -- The seam correction spreads by distance from the first station to the last.
728
+ local _binding = frames
729
+ local quartics = _binding.quartics
730
+ local distances = _binding.distances
731
+ local distance = distances[found + 1] + travelBetween(quartics, station.index, station.u, index, u)
732
+ return roll(normal, tangent, (frames.seamRoll * distance) / distances[#distances])
733
+ end
734
+ function roll(normal, axis, angle)
735
+ local _normal = normal
736
+ local _arg0 = math.cos(angle)
737
+ local _exp = axis:Cross(normal)
738
+ local _arg0_1 = math.sin(angle)
739
+ return _normal * _arg0 + (_exp * _arg0_1)
740
+ end
741
+ local function halve(_param)
742
+ local a = _param[1]
743
+ local b = _param[2]
744
+ local c = _param[3]
745
+ local d = _param[4]
746
+ local ab = (a + b) * 0.5
747
+ local bc = (b + c) * 0.5
748
+ local cd = (c + d) * 0.5
749
+ local abc = (ab + bc) * 0.5
750
+ local bcd = (bc + cd) * 0.5
751
+ local middle = (abc + bcd) * 0.5
752
+ return { a, ab, abc, middle }, { middle, bcd, cd, d }
753
+ end
754
+ local function segmentGap(point, a, d)
755
+ local _d = d
756
+ local _a = a
757
+ local chord = _d - _a
758
+ local _point = point
759
+ local _a_1 = a
760
+ local offset = _point - _a_1
761
+ local along = chord:Dot(chord)
762
+ if along <= 0 then
763
+ return offset.Magnitude
764
+ end
765
+ local _arg0 = math.clamp(offset:Dot(chord) / along, 0, 1)
766
+ local _arg0_1 = chord * _arg0
767
+ return (offset - _arg0_1).Magnitude
768
+ end
769
+ local function boxGap(low, high, query)
770
+ local _query = query
771
+ local _arg0 = query:Max(low):Min(high)
772
+ return (_query - _arg0).Magnitude
773
+ end
774
+ -- Both bounds hold by the convex hull property. The chord bound shrinks with the square of a piece's size,
775
+ -- the box bound only linearly, but the box is tighter on wide pieces such as a loop's whole span.
776
+ local function gapBound(controls, query)
777
+ local _binding = controls
778
+ local a = _binding[1]
779
+ local b = _binding[2]
780
+ local c = _binding[3]
781
+ local d = _binding[4]
782
+ local reach = math.max(segmentGap(b, a, d), segmentGap(c, a, d))
783
+ return math.max(boxGap(a:Min(b, c, d), a:Max(b, c, d), query), segmentGap(query, a, d) - reach)
784
+ end
785
+ -- Boxes over runs of neighboring spans, so a query skips spans far from it.
786
+ local function cluster(spans, first, last)
787
+ if first == last then
788
+ local _binding = spans[first + 1]
789
+ local a = _binding[1]
790
+ local b = _binding[2]
791
+ local c = _binding[3]
792
+ local d = _binding[4]
793
+ return {
794
+ low = a:Min(b, c, d),
795
+ high = a:Max(b, c, d),
796
+ index = first,
797
+ }
798
+ end
799
+ local middle = math.floor((first + last) / 2)
800
+ local left = cluster(spans, first, middle)
801
+ local right = cluster(spans, middle + 1, last)
802
+ return {
803
+ low = left.low:Min(right.low),
804
+ high = left.high:Max(right.high),
805
+ index = first,
806
+ children = { left, right },
807
+ }
808
+ end
809
+ local function pushEntry(heap, entry)
810
+ local position = #heap
811
+ local _heap = heap
812
+ local _entry = entry
813
+ table.insert(_heap, _entry)
814
+ while position > 0 do
815
+ local parent = math.floor((position - 1) / 2)
816
+ if heap[parent + 1].bound <= entry.bound then
817
+ break
818
+ end
819
+ heap[position + 1] = heap[parent + 1]
820
+ position = parent
821
+ end
822
+ heap[position + 1] = entry
823
+ end
824
+ local function popEntry(heap)
825
+ local top = heap[1]
826
+ -- ▼ Array.pop ▼
827
+ local _length = #heap
828
+ local _result = heap[_length]
829
+ heap[_length] = nil
830
+ -- ▲ Array.pop ▲
831
+ local last = _result
832
+ local size = #heap
833
+ if last == nil or size == 0 then
834
+ return last
835
+ end
836
+ local position = 0
837
+ do
838
+ local _child = 1
839
+ local _shouldIncrement = false
840
+ while true do
841
+ local child = _child
842
+ if _shouldIncrement then
843
+ child = 2 * position + 1
844
+ else
845
+ _shouldIncrement = true
846
+ end
847
+ if not (child < size) then
848
+ break
849
+ end
850
+ if child + 1 < size and heap[child + 2].bound < heap[child + 1].bound then
851
+ child += 1
852
+ end
853
+ if heap[child + 1].bound >= last.bound then
854
+ break
855
+ end
856
+ heap[position + 1] = heap[child + 1]
857
+ position = child
858
+ _child = child
859
+ end
860
+ end
861
+ heap[position + 1] = last
862
+ return top
863
+ end
864
+ local polish
865
+ local function nearest(geometry, tree, query)
866
+ local tolerance = geometry.tolerance
867
+ local _object = {
868
+ index = 0,
869
+ u = 0,
870
+ }
871
+ local _left = "separation"
872
+ local _exp = geometry.spans[1][1]
873
+ local _query = query
874
+ _object[_left] = (_exp - _query).Magnitude
875
+ local best = _object
876
+ local offer = function(index, u, point)
877
+ local _point = point
878
+ local _query_1 = query
879
+ local separation = (_point - _query_1).Magnitude
880
+ if separation < best.separation or (separation == best.separation and index + u < best.index + best.u) then
881
+ best = {
882
+ index = index,
883
+ u = u,
884
+ separation = separation,
885
+ }
886
+ end
887
+ end
888
+ local heap = { {
889
+ bound = boxGap(tree.low, tree.high, query),
890
+ cluster = tree,
891
+ } }
892
+ do
893
+ local step = 0
894
+ local _shouldIncrement = false
895
+ while true do
896
+ if _shouldIncrement then
897
+ step += 1
898
+ else
899
+ _shouldIncrement = true
900
+ end
901
+ local entry = popEntry(heap)
902
+ if entry == nil or entry.bound >= best.separation - tolerance then
903
+ break
904
+ end
905
+ if step >= SEARCH_BUDGET then
906
+ error("the closest point search did not converge")
907
+ end
908
+ if entry.cluster ~= nil then
909
+ local _binding = entry.cluster
910
+ local index = _binding.index
911
+ local children = _binding.children
912
+ if children ~= nil then
913
+ for _, child in children do
914
+ local bound = boxGap(child.low, child.high, query)
915
+ if bound < best.separation - tolerance then
916
+ pushEntry(heap, {
917
+ bound = bound,
918
+ cluster = child,
919
+ })
920
+ end
921
+ end
922
+ continue
923
+ end
924
+ local span = geometry.spans[index + 1]
925
+ offer(index, 0, span[1])
926
+ offer(index, 1, span[4])
927
+ local bound = gapBound(span, query)
928
+ if bound < best.separation - tolerance then
929
+ pushEntry(heap, {
930
+ index = index,
931
+ controls = span,
932
+ from = 0,
933
+ to = 1,
934
+ bound = bound,
935
+ })
936
+ end
937
+ continue
938
+ end
939
+ local _binding = entry
940
+ local index = _binding.index
941
+ local controls = _binding.controls
942
+ local from = _binding.from
943
+ local to = _binding.to
944
+ local _binding_1 = controls
945
+ local a = _binding_1[1]
946
+ local d = _binding_1[4]
947
+ -- The chord's foot lands near the piece's closest point, so the best gap tightens quickly.
948
+ local chord = d - a
949
+ local along = chord:Dot(chord)
950
+ if along > 0 then
951
+ local u = from + (to - from) * math.clamp((query - a):Dot(chord) / along, 0, 1)
952
+ offer(index, u, bernstein(geometry.spans[index + 1], u))
953
+ end
954
+ local middle = (from + to) / 2
955
+ local left, right = halve(controls)
956
+ offer(index, middle, left[4])
957
+ local limit = best.separation - tolerance
958
+ local leftBound = gapBound(left, query)
959
+ if leftBound < limit then
960
+ pushEntry(heap, {
961
+ index = index,
962
+ controls = left,
963
+ from = from,
964
+ to = middle,
965
+ bound = leftBound,
966
+ })
967
+ end
968
+ local rightBound = gapBound(right, query)
969
+ if rightBound < limit then
970
+ pushEntry(heap, {
971
+ index = index,
972
+ controls = right,
973
+ from = middle,
974
+ to = to,
975
+ bound = rightBound,
976
+ })
977
+ end
978
+ end
979
+ end
980
+ return polish(geometry, query, best)
981
+ end
982
+ -- The search stops within tolerance, which can leave the best point on a joint while the true closest point
983
+ -- lies just inside the neighboring span, where Newton's clamp to one span can't reach.
984
+ local refineClosest
985
+ function polish(geometry, query, found)
986
+ local best = refineClosest(geometry.spans[found.index + 1], query, found)
987
+ local count = #geometry.spans
988
+ local step = if best.u == 1 then 1 elseif best.u == 0 then -1 else 0
989
+ local neighbor = best.index + step
990
+ if step == 0 or (not geometry.closed and (neighbor < 0 or neighbor >= count)) then
991
+ return best
992
+ end
993
+ local index = (neighbor + count) % count
994
+ local across = refineClosest(geometry.spans[index + 1], query, {
995
+ index = index,
996
+ u = if step > 0 then 0 else 1,
997
+ separation = best.separation,
998
+ })
999
+ return if across.separation < best.separation then across else best
1000
+ end
1001
+ function refineClosest(span, query, found)
1002
+ local best = found
1003
+ local u = best.u
1004
+ for step = 0, 7 do
1005
+ local _exp = bernstein(span, u)
1006
+ local _query = query
1007
+ local offset = _exp - _query
1008
+ local motion = velocity(span, u)
1009
+ local slope = motion:Dot(motion) + offset:Dot(acceleration(span, u))
1010
+ if not (slope > 0) then
1011
+ break
1012
+ end
1013
+ u = math.clamp(u - offset:Dot(motion) / slope, 0, 1)
1014
+ local _exp_1 = bernstein(span, u)
1015
+ local _query_1 = query
1016
+ local separation = (_exp_1 - _query_1).Magnitude
1017
+ if not (separation < best.separation) then
1018
+ break
1019
+ end
1020
+ best = {
1021
+ index = best.index,
1022
+ u = u,
1023
+ separation = separation,
1024
+ }
1025
+ end
1026
+ return best
1027
+ end
1028
+ function createCurve(geometry, lift, lower)
1029
+ local arc
1030
+ local frames
1031
+ local tree
1032
+ local measured = function()
1033
+ if arc == nil then
1034
+ arc = measure(geometry)
1035
+ end
1036
+ return arc
1037
+ end
1038
+ local framed = function()
1039
+ if frames == nil then
1040
+ frames = transportFrames(geometry)
1041
+ end
1042
+ return frames
1043
+ end
1044
+ local clustered = function()
1045
+ if tree == nil then
1046
+ tree = cluster(geometry.spans, 0, #geometry.spans - 1)
1047
+ end
1048
+ return tree
1049
+ end
1050
+ local locateChecked = function(parameter)
1051
+ if not finite(parameter) then
1052
+ error("curve parameters must be finite")
1053
+ end
1054
+ return locate(geometry, parameter)
1055
+ end
1056
+ return {
1057
+ pointAt = function(parameter)
1058
+ local index, u = locateChecked(parameter)
1059
+ return lower(bernstein(geometry.spans[index + 1], u))
1060
+ end,
1061
+ tangentAt = function(parameter)
1062
+ local index, u = locateChecked(parameter)
1063
+ local tangent = direction(velocity(geometry.spans[index + 1], u))
1064
+ return if tangent == nil then nil else lower(tangent)
1065
+ end,
1066
+ frameAt = function(parameter)
1067
+ local index, u = locateChecked(parameter)
1068
+ local span = geometry.spans[index + 1]
1069
+ local tangent = direction(velocity(span, u))
1070
+ if tangent == nil then
1071
+ return nil
1072
+ end
1073
+ local point = bernstein(span, u)
1074
+ local normal = if geometry.planar then Vector3.new(-tangent.Y, tangent.X, 0) else spatialNormal(geometry, framed(), index, u, point, tangent)
1075
+ return {
1076
+ point = lower(point),
1077
+ tangent = lower(tangent),
1078
+ normal = lower(normal),
1079
+ }
1080
+ end,
1081
+ length = function()
1082
+ return measured().length
1083
+ end,
1084
+ parameterAtDistance = function(distance)
1085
+ if not finite(distance) then
1086
+ error("curve distances must be finite")
1087
+ end
1088
+ local lengths = measured()
1089
+ return if lengths.length > 0 then parameterAt(geometry, lengths, distance) else 0
1090
+ end,
1091
+ distanceAtParameter = function(parameter)
1092
+ local index, u = locateChecked(parameter)
1093
+ return distanceAt(measured(), index, u)
1094
+ end,
1095
+ closest = function(query)
1096
+ local target = lift(query)
1097
+ local found = nearest(geometry, clustered(), target)
1098
+ local count = #geometry.spans
1099
+ local parameter = if geometry.closed and found.index + found.u >= count then 0 else (found.index + found.u) / count
1100
+ local index, u = locate(geometry, parameter)
1101
+ local point = bernstein(geometry.spans[index + 1], u)
1102
+ return {
1103
+ parameter = parameter,
1104
+ point = lower(point),
1105
+ distance = distanceAt(measured(), index, u),
1106
+ separation = (point - target).Magnitude,
1107
+ }
1108
+ end,
1109
+ prepare = function()
1110
+ measured()
1111
+ clustered()
1112
+ if not geometry.planar then
1113
+ framed()
1114
+ end
1115
+ end,
1116
+ }
1117
+ end
1118
+ return {
1119
+ catmullRom = catmullRom,
1120
+ cubicBezier = cubicBezier,
1121
+ uniformBSpline = uniformBSpline,
1122
+ }