@trioforge/crucible 0.2.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.
- package/maths/spline/package.json +4 -0
- package/out/internal/boot.luau +5 -0
- package/out/internal/network/claims.luau +1 -9
- package/out/internal/network/client-session.luau +3 -1
- package/out/internal/network/server-session.luau +3 -1
- package/out/maths/spline/curve.d.ts +35 -0
- package/out/maths/spline/curve.luau +1122 -0
- package/out/maths/spline/index.d.ts +2 -0
- package/out/maths/spline/init.luau +8 -0
- package/out/server/index.d.ts +1 -0
- package/out/server/init.luau +9 -1
- package/out/testing/index.d.ts +38 -0
- package/out/testing/init.luau +189 -0
- package/out/util/random/index.d.ts +24 -0
- package/out/util/random/init.luau +301 -0
- package/package.json +16 -1
- package/testing/package.json +4 -0
- package/transformer/index.js +50 -12
- package/util/random/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
|
+
}
|