@px-petals/roku.motion-web 1.0.0 → 1.1.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 (77) hide show
  1. package/bands/bands.js +3 -1
  2. package/bundle/bundle.js +56 -0
  3. package/list/list.js +12 -1
  4. package/lottie/lottie.layers.js +64 -11
  5. package/lottie/lottie.paint.js +30 -0
  6. package/lottie/lottie.paths.js +19 -4
  7. package/lottie/lottie.props.js +18 -8
  8. package/lottie/lottie.shapes.js +2 -1
  9. package/mixer/mixer.js +31 -8
  10. package/mixer/mixer.mix.js +17 -0
  11. package/motion/motion.convert.js +32 -1
  12. package/motion/motion.flow.js +20 -2
  13. package/motion/motion.machine.js +28 -13
  14. package/motion/motion.values.js +13 -9
  15. package/motion.js +196 -20
  16. package/package.json +10 -16
  17. package/player/player.js +25 -5
  18. package/readme.md +78 -3
  19. package/script/luau/lib/base/base.js +202 -0
  20. package/script/luau/lib/base/base.table.js +189 -0
  21. package/script/luau/lib/lib.args.js +152 -0
  22. package/script/luau/lib/lib.js +23 -0
  23. package/script/luau/lib/math/math.bit32.js +144 -0
  24. package/script/luau/lib/math/math.js +288 -0
  25. package/script/luau/lib/math/math.vector.js +125 -0
  26. package/script/luau/lib/string/string.format.js +143 -0
  27. package/script/luau/lib/string/string.js +160 -0
  28. package/script/luau/lib/string/string.pattern.js +505 -0
  29. package/script/luau/load/load.js +362 -0
  30. package/script/luau/load/load.ops.js +79 -0
  31. package/script/luau/luau.convert.js +259 -0
  32. package/script/luau/luau.js +132 -0
  33. package/script/luau/luau.md +111 -0
  34. package/script/luau/vm/vm.js +167 -0
  35. package/script/luau/vm/vm.meta.js +401 -0
  36. package/script/luau/vm/vm.num.js +236 -0
  37. package/script/luau/vm/vm.run.js +842 -0
  38. package/script/luau/vm/vm.value.js +406 -0
  39. package/script/play/play.js +199 -0
  40. package/script/play/play.objects.js +295 -0
  41. package/script/play/play.scope.js +91 -0
  42. package/script/rive/api/api.statics.js +31 -0
  43. package/script/rive/api/api.type.js +188 -0
  44. package/script/rive/api/artboard/artboard.js +89 -0
  45. package/script/rive/api/assets/assets.js +72 -0
  46. package/script/rive/api/data/data.value.js +99 -0
  47. package/script/rive/api/draw/draw.contours.js +163 -0
  48. package/script/rive/api/draw/draw.measure.js +196 -0
  49. package/script/rive/api/draw/draw.paint.js +235 -0
  50. package/script/rive/api/draw/draw.path.js +153 -0
  51. package/script/rive/api/draw/draw.renderer.js +117 -0
  52. package/script/rive/api/math/math.color.js +96 -0
  53. package/script/rive/api/math/math.mat2d.js +180 -0
  54. package/script/rive/api/math/math.vector.js +121 -0
  55. package/script/rive/api/media/media.audio.js +113 -0
  56. package/script/rive/api/media/media.draw.js +73 -0
  57. package/script/rive/api/media/media.image.js +61 -0
  58. package/script/rive/api/media/media.mesh.js +50 -0
  59. package/script/rive/api/pointer/pointer.event.js +64 -0
  60. package/script/rive/api/pointer/pointer.route.js +69 -0
  61. package/script/rive/api/text/text.font.js +129 -0
  62. package/script/rive/api/text/text.js +278 -0
  63. package/script/rive/api/text/text.layout.js +259 -0
  64. package/script/rive/context/context.js +76 -0
  65. package/script/rive/context/context.memory.js +196 -0
  66. package/script/rive/context/context.viewmodel.js +302 -0
  67. package/script/rive/context/lists/lists.js +160 -0
  68. package/script/rive/context/lists/lists.values.js +281 -0
  69. package/script/rive/host/host.globals.js +44 -0
  70. package/script/rive/host/host.js +533 -0
  71. package/script/rive/host/host.node.js +45 -0
  72. package/script/rive/rive.js +23 -0
  73. package/script/rive/rive.md +180 -0
  74. package/script/script.js +11 -0
  75. package/skin/skin.deform.js +514 -0
  76. package/skin/skin.ik.js +260 -0
  77. package/skin/skin.js +297 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Pointer events into scripted nodes. `routePointer` takes one pointer event
3
+ * in the player's space and offers it to the nodes that listen, top-most
4
+ * first, until one claims it with `event:hit()`; a node that is not hit but
5
+ * was under the pointer before gets a `pointerExit`.
6
+ *
7
+ * ```js
8
+ * const nodes = [ { host, world: [1, 0, 0, 1, 40, 20], contains: (x, y) => hits.test('7', x, y) } ]
9
+ * routePointer(nodes, 'down', x, y, id, seconds)
10
+ * ```
11
+ *
12
+ * The player's pointer module (`pointer/`) calls this where it hit-tests its
13
+ * listeners; `contains` is its hit test of the node's shape.
14
+ */
15
+
16
+ /**
17
+ * @typedef {object} PointerNode
18
+ * @property {{ pointer: (type: string, x: number, y: number, id?: number, time?: number) => { hit: boolean, translucent: boolean } }} host the script's host
19
+ * @property {number[]} [world] the node's world matrix [xx, xy, yx, yy, tx, ty]; identity when absent
20
+ * @property {(x: number, y: number) => boolean} [contains] whether the point is on the node (in the player's space); absent counts as always
21
+ * @property {boolean} [inside] kept by the router
22
+ */
23
+
24
+ /**
25
+ * @param {number[] | undefined} m
26
+ * @param {number} x
27
+ * @param {number} y
28
+ * @returns {number[]} [x, y] in the node's space
29
+ */
30
+ function local(m, x, y) {
31
+ if (!m) return [ x, y ]
32
+ const det = m[0] * m[3] - m[1] * m[2]
33
+ if (det === 0) return [ x, y ]
34
+ const dx = x - m[4]
35
+ const dy = y - m[5]
36
+ return [ (m[3] * dx - m[2] * dy) / det, (m[0] * dy - m[1] * dx) / det ]
37
+ }
38
+
39
+ /**
40
+ * @param {PointerNode[]} nodes in draw order, top-most last
41
+ * @param {'down' | 'move' | 'up' | 'exit'} type
42
+ * @param {number} x
43
+ * @param {number} y
44
+ * @param {number} [id]
45
+ * @param {number} [time]
46
+ * @returns {boolean} whether a node claimed the event
47
+ */
48
+ export function routePointer(nodes, type, x, y, id, time) {
49
+ let claimed = false
50
+ for (let n = nodes.length - 1; n >= 0; n--) {
51
+ const node = nodes[n]
52
+ const on = type !== 'exit' && !claimed && (!node.contains || node.contains(x, y))
53
+ if (!on) {
54
+ if (node.inside) {
55
+ node.inside = false
56
+ const p = local(node.world, x, y)
57
+ node.host.pointer('exit', p[0], p[1], id, time)
58
+ }
59
+ continue
60
+ }
61
+ const p = local(node.world, x, y)
62
+ const result = node.host.pointer(type, p[0], p[1], id, time)
63
+ node.inside = type !== 'up' || node.inside
64
+ if (result.hit) {
65
+ if (!result.translucent) claimed = true
66
+ }
67
+ }
68
+ return claimed
69
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * `Font`: a font a script measures and draws with. The host holds no font
3
+ * data: the player gives it a `FontInfo`, the metrics and advances of a font
4
+ * its bundle carries (from `bundle/bundle.fonts.js`' registered face measured
5
+ * on a canvas, or from opentype.js at conversion), and the host lays text out
6
+ * with them. Without one, text is laid out with a fixed fallback.
7
+ */
8
+
9
+ import { LuaTable } from '../../../luau/vm/vm.value.js'
10
+ import { decodeUtf8 } from '../../../luau/luau.convert.js'
11
+ import { defineType, num, text } from '../api.type.js'
12
+ import { statics } from '../api.statics.js'
13
+ import { newPath } from '../draw/draw.path.js'
14
+
15
+ /**
16
+ * @typedef {object} FontInfo
17
+ * @property {string} name
18
+ * @property {number} [ascent] per 1 point of size, negative up (default -0.8)
19
+ * @property {number} [descent] positive down (default 0.2)
20
+ * @property {number} [capHeight] negative up
21
+ * @property {number} [xHeight] negative up
22
+ * @property {number} [weight] 1 to 1000, 400 regular
23
+ * @property {boolean} [isItalic]
24
+ * @property {(codepoint: number) => number} [advance] the advance of a character in em (default 0.5)
25
+ * @property {(codepoint: number) => boolean} [hasGlyph]
26
+ * @property {(codepoint: number) => number} [glyphId] default the codepoint itself
27
+ * @property {(glyphId: number) => any[]} [glyphPath] the outline as Path commands `{ t, p }` in 1 point units
28
+ * @property {Record<string, number>} [axes]
29
+ * @property {string[]} [features]
30
+ */
31
+
32
+ /**
33
+ * @param {FontInfo} info
34
+ * @returns {Required<Pick<FontInfo, 'name' | 'ascent' | 'descent' | 'capHeight' | 'xHeight' | 'weight' | 'isItalic'>> & FontInfo}
35
+ */
36
+ export function completeFont(info) {
37
+ return /** @type {any} */ ({
38
+ ascent: -0.8,
39
+ descent: 0.2,
40
+ capHeight: -0.7,
41
+ xHeight: -0.5,
42
+ weight: 400,
43
+ isItalic: false,
44
+ ...info,
45
+ })
46
+ }
47
+
48
+ /**
49
+ * A `FontInfo` whose advances are measured by a function of one character,
50
+ * such as `(c) => ctx.measureText(c).width / ctx.font size`.
51
+ *
52
+ * @param {string} name
53
+ * @param {(character: string) => number} measure the character's advance in em
54
+ * @param {Partial<FontInfo>} [metrics]
55
+ * @returns {FontInfo}
56
+ */
57
+ export function measuredFont(name, measure, metrics) {
58
+ return { name, ...metrics, advance: (cp) => measure(String.fromCharCode(cp)) }
59
+ }
60
+
61
+ export const Font = defineType('Font', {
62
+ fields: {
63
+ ascent: { get: (f) => f.ascent },
64
+ descent: { get: (f) => f.descent },
65
+ capHeight: { get: (f) => f.capHeight },
66
+ xHeight: { get: (f) => f.xHeight },
67
+ weight: { get: (f) => f.weight },
68
+ isItalic: { get: (f) => f.isItalic },
69
+ axes: { get: (f) => tableOf(f.axes ? Object.keys(f.axes) : []) },
70
+ features: { get: (f) => tableOf(f.features || []) },
71
+ },
72
+ methods: {
73
+ lineHeight: (f, args) => (f.descent - f.ascent) * num(args, 1, 'lineHeight'),
74
+ hasGlyph: (f, args) => (f.hasGlyph ? f.hasGlyph(num(args, 1, 'hasGlyph')) : true),
75
+ axisValue: (f, args) => (f.axes ? f.axes[text(args, 1, 'axisValue')] || 0 : 0),
76
+ withOptions: (f, args) => {
77
+ const axes = args[1] instanceof LuaTable ? merged(f.axes, plain(args[1])) : f.axes
78
+ const features = args[2] instanceof LuaTable ? Object.keys(plain(args[2])) : f.features
79
+ return Font.wrap({ ...f, axes, features })
80
+ },
81
+ glyphPath: (f, args) => newPath(f.glyphPath ? f.glyphPath(num(args, 1, 'glyphPath')) : []),
82
+ },
83
+ })
84
+
85
+ /**
86
+ * @param {Record<string, number> | undefined} base
87
+ * @param {Record<string, number>} over
88
+ * @returns {Record<string, number>}
89
+ */
90
+ function merged(base, over) {
91
+ /** @type {Record<string, number>} */
92
+ const out = {}
93
+ for (const k in base) out[k] = base[k]
94
+ for (const k in over) out[k] = over[k]
95
+ return out
96
+ }
97
+
98
+ /**
99
+ * @param {string[]} names
100
+ * @returns {LuaTable}
101
+ */
102
+ function tableOf(names) {
103
+ const t = new LuaTable()
104
+ t.setRange(1, names, 0, names.length)
105
+ return t
106
+ }
107
+
108
+ /**
109
+ * @param {LuaTable} t
110
+ * @returns {Record<string, number>} its string keys with number values
111
+ */
112
+ function plain(t) {
113
+ /** @type {Record<string, number>} */
114
+ const out = {}
115
+ for (let at = t.advance(0); at !== null; at = t.advance(at)) {
116
+ if (typeof t.ck === 'string' && typeof t.cv === 'number') out[decodeUtf8(t.ck)] = t.cv
117
+ }
118
+ return out
119
+ }
120
+
121
+ /**
122
+ * Installs the `Font` global.
123
+ *
124
+ * @param {import('../../../luau/vm/vm.js').VM} vm
125
+ */
126
+ export function installFont(vm) {
127
+ // there is no decoder here: a font a script brings as bytes is nil, as for bytes that are not a font
128
+ vm.globals.set('Font', statics('Font', { decode: () => undefined }))
129
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * `Text`: styled runs a script lays out and draws. `Text.new()`, `append`,
3
+ * `clear`, `draw`, `bounds`, `line`, `lines`, `hitTest`, `caret`,
4
+ * `selectionRects`, and the layout fields (`sizing`, `overflow`, `align`,
5
+ * `wrap`, `wordBreak`, `origin`, `direction`, `maxWidth`, `maxHeight`,
6
+ * `paragraphSpacing`). Layout is `text.layout.js`, with the metrics of the
7
+ * fonts the player gave the host; the draw is recorded as
8
+ *
9
+ * `{ kind: 'text', runs, lines, text, transform, clips, opacity, color, paint, ...layout }`
10
+ *
11
+ * where each run is `{ text, font, size, lineHeight, letterSpacing, paint, foregroundColor }`
12
+ * (`font` an asset name, `paint` a snapshot or null) and each line
13
+ * `{ x, top, baseline, bottom, width, firstIndex, lastIndex, text }`. A run
14
+ * with no paint draws only when `Text:draw` is given one.
15
+ */
16
+
17
+ import { LuaTable, Vector } from '../../../luau/vm/vm.value.js'
18
+ import { decodeUtf8 } from '../../../luau/luau.convert.js'
19
+ import { badArg, defineType, num, text, wrapped } from '../api.type.js'
20
+ import { statics } from '../api.statics.js'
21
+ import { Mat2D } from '../math/math.mat2d.js'
22
+ import { Paint, snapshotPaint } from '../draw/draw.paint.js'
23
+ import { newPath } from '../draw/draw.path.js'
24
+ import { Renderer } from '../draw/draw.renderer.js'
25
+ import { stamp } from '../media/media.draw.js'
26
+ import { Font } from './text.font.js'
27
+ import { layoutText } from './text.layout.js'
28
+
29
+ /** @import { Layout, LayoutOptions, Run } from './text.layout.js' */
30
+
31
+ /**
32
+ * @typedef {object} TextData
33
+ * @property {{ run: Run, paint: any, foreground: number }[]} runs
34
+ * @property {LayoutOptions} o
35
+ * @property {Layout | null} laid
36
+ * @property {() => any} fallback the font for a run with none
37
+ */
38
+
39
+ /** @type {Record<string, string>} the fields that hold a name */
40
+ const NAMES = { sizing: 'autoWidth', overflow: 'visible', align: 'left', wrap: 'wrap', wordBreak: 'normal', origin: 'top', direction: 'ltr' }
41
+ /** @type {string[]} */
42
+ const NUMBERS = [ 'maxWidth', 'maxHeight', 'paragraphSpacing' ]
43
+
44
+ /**
45
+ * @param {TextData} h
46
+ * @returns {Layout}
47
+ */
48
+ function laid(h) {
49
+ if (!h.laid) {
50
+ const runs = []
51
+ for (let n = 0; n < h.runs.length; n++) runs.push(h.runs[n].run)
52
+ h.laid = layoutText(runs, h.o)
53
+ }
54
+ return h.laid
55
+ }
56
+
57
+ /** @type {Record<string, { get: (h: any) => any, set: (h: any, v: any) => void }>} */
58
+ const fields = {
59
+ isEmpty: { get: (h) => laid(h).length === 0, set: () => {} },
60
+ lineCount: { get: (h) => laid(h).lines.length, set: () => {} },
61
+ length: { get: (h) => laid(h).length, set: () => {} },
62
+ }
63
+ for (const name in NAMES) {
64
+ fields[name] = {
65
+ get: (h) => h.o[name],
66
+ set: (h, v) => {
67
+ if (typeof v !== 'string') throw badArg(0, name, 'string', [ v ])
68
+ h.o[name] = decodeUtf8(v)
69
+ h.laid = null
70
+ },
71
+ }
72
+ }
73
+ for (let n = 0; n < NUMBERS.length; n++) {
74
+ const name = NUMBERS[n]
75
+ fields[name] = {
76
+ get: (h) => h.o[name],
77
+ set: (h, v) => {
78
+ if (typeof v !== 'number') throw badArg(0, name, 'number', [ v ])
79
+ h.o[name] = v
80
+ h.laid = null
81
+ },
82
+ }
83
+ }
84
+
85
+ /**
86
+ * @param {any} style a Lua table
87
+ * @param {TextData} h
88
+ * @returns {{ run: Run, paint: any, foreground: number }}
89
+ */
90
+ function runOf(style, h) {
91
+ const t = style instanceof LuaTable ? style : new LuaTable()
92
+ const f = t.getStr('font')
93
+ const size = t.getStr('size')
94
+ const lineHeight = t.getStr('lineHeight')
95
+ const letterSpacing = t.getStr('letterSpacing')
96
+ const fg = t.getStr('foregroundColor')
97
+ return {
98
+ run: {
99
+ text: '',
100
+ font: Font.is(f) ? Font.unwrap(f) : h.fallback(),
101
+ size: typeof size === 'number' ? size : 16,
102
+ lineHeight: typeof lineHeight === 'number' ? lineHeight : 0,
103
+ letterSpacing: typeof letterSpacing === 'number' ? letterSpacing : 0,
104
+ },
105
+ paint: Paint.unwrap(t.getStr('paint')) || null,
106
+ foreground: typeof fg === 'number' ? fg : 0xff000000,
107
+ }
108
+ }
109
+
110
+ /**
111
+ * @param {TextData} h
112
+ * @param {number} index 1-based codepoint index
113
+ * @returns {{ line: import('./text.layout.js').Line, x: number } | null}
114
+ */
115
+ function locate(h, index) {
116
+ const lines = laid(h).lines
117
+ for (let n = 0; n < lines.length; n++) {
118
+ const l = lines[n]
119
+ if (index >= l.firstIndex && index <= l.lastIndex + 1) {
120
+ for (let k = 0; k < l.glyphs.length; k++) if (l.glyphs[k].textIndex === index) return { line: l, x: l.glyphs[k].x }
121
+ return { line: l, x: l.x + l.width }
122
+ }
123
+ }
124
+ return null
125
+ }
126
+
127
+ export const Glyph = defineType('Glyph', {
128
+ fields: {
129
+ id: { get: (g) => g.id },
130
+ font: { get: (g) => Font.wrap(g.run.font) },
131
+ size: { get: (g) => g.run.size },
132
+ x: { get: (g) => g.x },
133
+ y: { get: (g) => g.y },
134
+ advance: { get: (g) => g.advance },
135
+ textIndex: { get: (g) => g.textIndex },
136
+ direction: { get: () => 'ltr' },
137
+ isColor: { get: () => false },
138
+ transform: { get: (g) => Mat2D.wrap([ g.run.size, 0, 0, g.run.size, g.x, g.y ]) },
139
+ },
140
+ methods: {
141
+ path: (g) => {
142
+ const f = g.run.font
143
+ const cmds = f.glyphPath ? f.glyphPath(g.id) : []
144
+ return newPath(cmds.map((/** @type {any} */ c) => ({ t: c.t, p: c.p.map((/** @type {number} */ v, /** @type {number} */ i) => v * g.run.size + (i % 2 ? g.y : g.x)) })))
145
+ },
146
+ },
147
+ })
148
+
149
+ export const TextLine = defineType('TextLine', {
150
+ fields: {
151
+ index: { get: (l) => l.index },
152
+ x: { get: (l) => l.x },
153
+ top: { get: (l) => l.top },
154
+ baseline: { get: (l) => l.baseline },
155
+ bottom: { get: (l) => l.bottom },
156
+ width: { get: (l) => l.width },
157
+ direction: { get: () => 'ltr' },
158
+ firstIndex: { get: (l) => l.firstIndex },
159
+ lastIndex: { get: (l) => l.lastIndex },
160
+ glyphCount: { get: (l) => l.glyphs.length },
161
+ },
162
+ methods: {
163
+ glyph: (l, args) => {
164
+ const g = l.glyphs[num(args, 1, 'glyph') - 1]
165
+ return g ? Glyph.wrap(g) : undefined
166
+ },
167
+ glyphs: (l) => list(l.glyphs.map((/** @type {any} */ g) => Glyph.wrap(g))),
168
+ },
169
+ })
170
+
171
+ /**
172
+ * @param {any[]} items
173
+ * @returns {LuaTable}
174
+ */
175
+ function list(items) {
176
+ const t = new LuaTable()
177
+ t.setRange(1, items, 0, items.length)
178
+ return t
179
+ }
180
+
181
+ export const Text = defineType('Text', {
182
+ fields,
183
+ methods: {
184
+ append: (h, args) => {
185
+ const r = runOf(args[2], h)
186
+ r.run.text = text(args, 1, 'append')
187
+ h.runs.push(r)
188
+ h.laid = null
189
+ },
190
+ clear: (h) => {
191
+ h.runs.length = 0
192
+ h.laid = null
193
+ },
194
+ draw: (h, args) => {
195
+ const rec = wrapped(args, 1, Renderer, 'draw')
196
+ const given = args[2] === undefined ? null : wrapped(args, 2, Paint, 'draw')
197
+ const l = laid(h)
198
+ const out = /** @type {any} */ (stamp(rec, 'text'))
199
+ const color = rec.state.color
200
+ out.runs = h.runs.map((/** @type {any} */ r) => {
201
+ const p = given || r.paint
202
+ return {
203
+ text: r.run.text,
204
+ font: r.run.font.name,
205
+ size: r.run.size,
206
+ lineHeight: r.run.lineHeight,
207
+ letterSpacing: r.run.letterSpacing,
208
+ paint: p ? snapshotPaint(p, color) : null,
209
+ foregroundColor: r.foreground,
210
+ }
211
+ })
212
+ if (!out.runs.some((/** @type {any} */ r) => r.paint)) return
213
+ out.paint = given ? snapshotPaint(given, color) : null
214
+ out.lines = l.lines.map((n) => ({ x: n.x, top: n.top, baseline: n.baseline, bottom: n.bottom, width: n.width, firstIndex: n.firstIndex, lastIndex: n.lastIndex, text: n.text }))
215
+ out.text = out.lines.map((/** @type {any} */ n) => n.text).join('\n')
216
+ out.width = l.width
217
+ out.height = l.height
218
+ for (const k in h.o) out[k] = h.o[/** @type {keyof LayoutOptions} */ (k)]
219
+ rec.drawings.push(out)
220
+ },
221
+ bounds: (h) => {
222
+ const l = laid(h)
223
+ const top = l.lines.length ? l.lines[0].top : 0
224
+ return [ new Vector(0, top, 0), new Vector(l.width, top + l.height, 0) ]
225
+ },
226
+ line: (h, args) => {
227
+ const l = laid(h).lines[num(args, 1, 'line') - 1]
228
+ return l ? TextLine.wrap(l) : undefined
229
+ },
230
+ lines: (h) => list(laid(h).lines.map((l) => TextLine.wrap(l))),
231
+ hitTest: (h, args) => {
232
+ const p = args[1]
233
+ if (!(p instanceof Vector)) throw badArg(1, 'hitTest', 'Vector', args)
234
+ const lines = laid(h).lines
235
+ let line = lines[0]
236
+ for (let n = 0; n < lines.length; n++) if (p.y >= lines[n].top) line = lines[n]
237
+ if (!line) return 1
238
+ for (let k = 0; k < line.glyphs.length; k++) {
239
+ const g = line.glyphs[k]
240
+ if (p.x < g.x + g.advance / 2) return g.textIndex
241
+ }
242
+ return line.lastIndex + 1
243
+ },
244
+ caret: (h, args) => {
245
+ const at = locate(h, num(args, 1, 'caret'))
246
+ return at ? [ at.x, at.line.top, at.line.bottom ] : undefined
247
+ },
248
+ selectionRects: (h, args) => {
249
+ const from = num(args, 1, 'selectionRects')
250
+ const to = num(args, 2, 'selectionRects')
251
+ const out = []
252
+ const lines = laid(h).lines
253
+ for (let n = 0; n < lines.length; n++) {
254
+ for (let k = 0; k < lines[n].glyphs.length; k++) {
255
+ const g = lines[n].glyphs[k]
256
+ if (g.textIndex < from || g.textIndex >= to) continue
257
+ const r = new LuaTable()
258
+ r.set('min', new Vector(g.x, lines[n].top, 0))
259
+ r.set('max', new Vector(g.x + g.advance, lines[n].bottom, 0))
260
+ out.push(r)
261
+ }
262
+ }
263
+ return list(out)
264
+ },
265
+ },
266
+ })
267
+
268
+ /**
269
+ * Installs the `Text` global. A run with no font uses `fallback()`'s.
270
+ *
271
+ * @param {import('../../../luau/vm/vm.js').VM} vm
272
+ * @param {() => any} fallback a completed FontInfo
273
+ */
274
+ export function installText(vm, fallback) {
275
+ vm.globals.set('Text', statics('Text', {
276
+ new: () => Text.wrap({ runs: [], o: { ...NAMES, maxWidth: 0, maxHeight: 0, paragraphSpacing: 0 }, laid: null, fallback }),
277
+ }))
278
+ }