@rafaelaugusto/jolt 0.1.0-beta.16 → 0.1.0-beta.17

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 (84) hide show
  1. package/README.md +7 -2
  2. package/dist/bottom-sheet/geometry.d.ts +18 -0
  3. package/dist/bottom-sheet/geometry.d.ts.map +1 -0
  4. package/dist/bottom-sheet/geometry.js +51 -0
  5. package/dist/bottom-sheet/geometry.js.map +1 -0
  6. package/dist/bottom-sheet/index.d.ts +56 -0
  7. package/dist/bottom-sheet/index.d.ts.map +1 -0
  8. package/dist/bottom-sheet/index.js +462 -0
  9. package/dist/bottom-sheet/index.js.map +1 -0
  10. package/dist/bottom-sheet/native.d.ts +11 -0
  11. package/dist/bottom-sheet/native.d.ts.map +1 -0
  12. package/dist/bottom-sheet/native.js +173 -0
  13. package/dist/bottom-sheet/native.js.map +1 -0
  14. package/dist/gesture-native.d.ts +1 -0
  15. package/dist/gesture-native.d.ts.map +1 -1
  16. package/dist/gesture-native.js +6 -0
  17. package/dist/gesture-native.js.map +1 -1
  18. package/dist/gestures/index.d.ts +4 -0
  19. package/dist/gestures/index.d.ts.map +1 -1
  20. package/dist/gestures/index.js +12 -1
  21. package/dist/gestures/index.js.map +1 -1
  22. package/dist/native/index.d.ts +2 -0
  23. package/dist/native/index.d.ts.map +1 -1
  24. package/dist/native/index.js +1 -0
  25. package/dist/native/index.js.map +1 -1
  26. package/dist/native-property.d.ts +2 -0
  27. package/dist/native-property.d.ts.map +1 -1
  28. package/dist/native-property.js +12 -4
  29. package/dist/native-property.js.map +1 -1
  30. package/dist/reorder/handle.d.ts +6 -0
  31. package/dist/reorder/handle.d.ts.map +1 -0
  32. package/dist/reorder/handle.js +10 -0
  33. package/dist/reorder/handle.js.map +1 -0
  34. package/dist/reorder/index.d.ts +10 -0
  35. package/dist/reorder/index.d.ts.map +1 -0
  36. package/dist/reorder/index.js +233 -0
  37. package/dist/reorder/index.js.map +1 -0
  38. package/dist/reorder/list-view.d.ts +5 -0
  39. package/dist/reorder/list-view.d.ts.map +1 -0
  40. package/dist/reorder/list-view.js +238 -0
  41. package/dist/reorder/list-view.js.map +1 -0
  42. package/dist/reorder/model.d.ts +6 -0
  43. package/dist/reorder/model.d.ts.map +1 -0
  44. package/dist/reorder/model.js +30 -0
  45. package/dist/reorder/model.js.map +1 -0
  46. package/dist/reorder/surface.d.ts +6 -0
  47. package/dist/reorder/surface.d.ts.map +1 -0
  48. package/dist/reorder/surface.js +10 -0
  49. package/dist/reorder/surface.js.map +1 -0
  50. package/dist/reorder/types.d.ts +92 -0
  51. package/dist/reorder/types.d.ts.map +1 -0
  52. package/dist/reorder/types.js +2 -0
  53. package/dist/reorder/types.js.map +1 -0
  54. package/dist/scene-shader.d.ts +18 -0
  55. package/dist/scene-shader.d.ts.map +1 -0
  56. package/dist/scene-shader.js +76 -0
  57. package/dist/scene-shader.js.map +1 -0
  58. package/package.json +9 -1
  59. package/platforms/android/java/org/rafaelaugusto/jolt/GestureSession.java +7 -2
  60. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderHandle.java +25 -0
  61. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSession.java +213 -0
  62. package/platforms/android/java/org/rafaelaugusto/jolt/ReorderSurface.java +27 -0
  63. package/platforms/android/proguard-rules.pro +5 -0
  64. package/platforms/ios/src/JoltGestureSession.h +1 -0
  65. package/platforms/ios/src/JoltGestureSession.m +7 -2
  66. package/platforms/ios/src/JoltReorderSession.h +15 -0
  67. package/platforms/ios/src/JoltReorderSession.m +205 -0
  68. package/platforms/ios/src/module.modulemap +1 -0
  69. package/src/android-plugin.d.ts +34 -0
  70. package/src/bottom-sheet/geometry.ts +84 -0
  71. package/src/bottom-sheet/index.ts +536 -0
  72. package/src/bottom-sheet/native.ts +214 -0
  73. package/src/gesture-native.ts +15 -0
  74. package/src/gestures/index.ts +16 -1
  75. package/src/ios-plugin.d.ts +32 -0
  76. package/src/native/index.ts +6 -0
  77. package/src/native-property.ts +17 -4
  78. package/src/reorder/handle.ts +9 -0
  79. package/src/reorder/index.ts +243 -0
  80. package/src/reorder/list-view.ts +262 -0
  81. package/src/reorder/model.ts +47 -0
  82. package/src/reorder/surface.ts +10 -0
  83. package/src/reorder/types.ts +87 -0
  84. package/src/scene-shader.ts +139 -0
@@ -0,0 +1,262 @@
1
+ import {
2
+ ListView,
3
+ isIOS,
4
+ type EventData,
5
+ type ItemEventData,
6
+ type View,
7
+ } from '@nativescript/core'
8
+ import { sheetActions as viewActions } from '../bottom-sheet/native.js'
9
+ import type { ReorderAdapter, ReorderKey } from './types.js'
10
+ import { ReorderHandle } from './handle.js'
11
+ import { ReorderSurface } from './surface.js'
12
+
13
+ /** Flat Core ListView only. Sectioned lists and plugin collection views need another adapter. */
14
+ export function coreListAdapter<T>(list: ListView): ReorderAdapter<T> {
15
+ if (!(list instanceof ListView)) throw new TypeError('Expected a Core ListView.')
16
+ function validate() {
17
+ if (list.sectioned || list.stickyHeader || list.showSearch)
18
+ throw new TypeError(
19
+ 'Reorder supports flat Core lists without built-in headers or search.',
20
+ )
21
+ }
22
+ validate()
23
+ return {
24
+ view: list,
25
+ items() {
26
+ validate()
27
+ const source = list.items
28
+ return Array.from({ length: source?.length ?? 0 }, (_, index) =>
29
+ Array.isArray(source) ? source[index] : source.getItem(index),
30
+ ) as T[]
31
+ },
32
+ subscribe(changed) {
33
+ let source:
34
+ | {
35
+ on?(event: string, listener: () => void): void
36
+ off?(event: string, listener: () => void): void
37
+ }
38
+ | undefined
39
+ const replaced = () => {
40
+ source?.off?.('change', changed)
41
+ source = list.items as typeof source
42
+ source?.on?.('change', changed)
43
+ changed()
44
+ }
45
+ list.on('itemsChange', replaced)
46
+ replaced()
47
+ return () => {
48
+ list.off('itemsChange', replaced)
49
+ source?.off?.('change', changed)
50
+ }
51
+ },
52
+ attach(config) {
53
+ validate()
54
+ const options = JSON.stringify({
55
+ longPress: config.longPress,
56
+ edgeSize: config.edgeSize,
57
+ edgeSpeed: config.edgeSpeed,
58
+ reducedMotion: config.reducedMotion,
59
+ handles: config.handle !== undefined,
60
+ })
61
+ let draggedKey: ReorderKey | undefined
62
+ const event = (state: number, index: number, toIndex: number) => {
63
+ if (state === 1) {
64
+ config.event({ type: 'start', index })
65
+ draggedKey = config.keyAt(index)
66
+ } else if (state === 2) {
67
+ const key = draggedKey
68
+ draggedKey = undefined
69
+ config.event({ type: 'commit', index, toIndex })
70
+ if (key !== undefined) focusKey(key, false)
71
+ } else {
72
+ draggedKey = undefined
73
+ config.event({ type: 'cancel', geometryChanged: toIndex === 1 })
74
+ }
75
+ }
76
+ let native: {
77
+ registerHandle(view: any): void
78
+ setEnabled(value: boolean): void
79
+ setReducedMotion(value: boolean): void
80
+ cancel(): void
81
+ dispose(): void
82
+ }
83
+ if (isIOS) {
84
+ if (typeof JoltReorderSession === 'undefined')
85
+ throw new Error('Rebuild the native app to use reorder().')
86
+ native = JoltReorderSession.attachOptionsOnState(
87
+ list.nativeViewProtected,
88
+ options,
89
+ event,
90
+ )
91
+ } else {
92
+ if (
93
+ !(list.parent instanceof ReorderSurface) ||
94
+ list.parent.getChildrenCount() !== 1
95
+ )
96
+ throw new TypeError(
97
+ 'On Android, make the Core ListView the only child of a ReorderSurface.',
98
+ )
99
+ if (typeof org.rafaelaugusto.jolt.ReorderSession === 'undefined')
100
+ throw new Error('Rebuild the native app to use reorder().')
101
+ native = new org.rafaelaugusto.jolt.ReorderSession(
102
+ list.parent.nativeViewProtected,
103
+ list.nativeViewProtected,
104
+ options,
105
+ new org.rafaelaugusto.jolt.ReorderSession.Listener({ onState: event }),
106
+ )
107
+ }
108
+ type Row = {
109
+ index: number
110
+ handle?: View
111
+ release(): void
112
+ timer?: ReturnType<typeof setTimeout>
113
+ }
114
+ const rows = new Map<View, Row>()
115
+ let ended = false
116
+ let focusTimer: ReturnType<typeof setTimeout> | undefined
117
+ function moveRow(row: Row, direction: -1 | 1, accessible: boolean) {
118
+ const key = config.move(row.index, direction)
119
+ if (key !== undefined) focusKey(key, accessible)
120
+ }
121
+ function reveal(index: number) {
122
+ // Run once after the app commits and Core rebinds. Estimated iOS heights
123
+ // can move the dropped row outside the viewport during reloadData.
124
+ if (isIOS) {
125
+ const table = list.nativeViewProtected as UITableView
126
+ table.layoutIfNeeded()
127
+ const path = NSIndexPath.indexPathForRowInSection(index, 0)
128
+ const rect = table.rectForRowAtIndexPath(path)
129
+ const top = table.bounds.origin.y + table.adjustedContentInset.top
130
+ const bottom =
131
+ table.bounds.origin.y +
132
+ table.bounds.size.height -
133
+ table.adjustedContentInset.bottom
134
+ // Keep the target away from an edge while UIKit resolves estimated heights.
135
+ if (rect.origin.y < top || rect.origin.y + rect.size.height > bottom)
136
+ table.scrollToRowAtIndexPathAtScrollPositionAnimated(path, 2, false)
137
+ } else {
138
+ const table = list.nativeViewProtected as android.widget.ListView
139
+ const cell = table.getChildAt(index - table.getFirstVisiblePosition())
140
+ if (!cell) list.scrollToIndex(index)
141
+ else {
142
+ const top = cell.getTop() - table.getPaddingTop()
143
+ const bottom = cell.getBottom() - table.getHeight() + table.getPaddingBottom()
144
+ if (top < 0) table.scrollListBy(top)
145
+ else if (bottom > 0) table.scrollListBy(bottom)
146
+ }
147
+ }
148
+ }
149
+ function focusKey(key: ReorderKey, accessible: boolean) {
150
+ if (ended) return
151
+ if (focusTimer !== undefined) clearTimeout(focusTimer)
152
+ let attempts = 0
153
+ const focus = () => {
154
+ focusTimer = undefined
155
+ if (ended) return
156
+ const index = config.indexOf(key)
157
+ if (index < 0) return
158
+ reveal(index)
159
+ const target = [...rows.values()].find(
160
+ (entry) => entry.index === index && entry.handle?.isLoaded,
161
+ )
162
+ if (target?.handle && list.isItemAtIndexVisible(index)) {
163
+ target.handle.focus()
164
+ if (accessible) target.handle.accessibilityScreenChanged()
165
+ } else if (attempts++ < 4) {
166
+ focusTimer = setTimeout(focus, 30)
167
+ }
168
+ }
169
+ // Core lays out recycled rows after Vue flushes its new items.
170
+ focusTimer = setTimeout(focus, 30)
171
+ }
172
+ function loaded(data: EventData) {
173
+ const args = data as ItemEventData
174
+ if (!args.view || ended) return
175
+ const view = args.view
176
+ let row = rows.get(view)
177
+ if (!row) {
178
+ row = { index: args.index, release() {} }
179
+ rows.set(view, row)
180
+ }
181
+ row.index = args.index
182
+ if (row.timer !== undefined) clearTimeout(row.timer)
183
+ const current = row
184
+ // Vue/Core finishes binding and creating native children after itemLoading.
185
+ row.timer = setTimeout(() => {
186
+ current.timer = undefined
187
+ if (ended) return
188
+ const handle =
189
+ typeof config.handle === 'function'
190
+ ? config.handle(view)
191
+ : typeof config.handle === 'string'
192
+ ? view.getViewById<View>(config.handle)
193
+ : view
194
+ if (!handle?.nativeViewProtected) return
195
+ if (current.handle !== handle) {
196
+ current.release()
197
+ current.handle = handle
198
+ const releaseActions = viewActions(handle, [
199
+ {
200
+ label: config.accessibility.moveUp,
201
+ run: () => moveRow(current, -1, true),
202
+ },
203
+ {
204
+ label: config.accessibility.moveDown,
205
+ run: () => moveRow(current, 1, true),
206
+ },
207
+ ])
208
+ let releaseKeys = () => {}
209
+ if (handle instanceof ReorderHandle) {
210
+ const move = (direction: number) =>
211
+ moveRow(current, direction < 0 ? -1 : 1, false)
212
+ if (isIOS) {
213
+ const button = handle.nativeViewProtected as JoltReorderHandle
214
+ button.onMove = move
215
+ releaseKeys = () => {
216
+ button.onMove = null
217
+ }
218
+ } else {
219
+ const button =
220
+ handle.nativeViewProtected as org.rafaelaugusto.jolt.ReorderHandle
221
+ button.setMoveListener(
222
+ new org.rafaelaugusto.jolt.ReorderHandle.MoveListener({ move }),
223
+ )
224
+ releaseKeys = () => button.setMoveListener(null)
225
+ }
226
+ }
227
+ current.release = () => {
228
+ releaseKeys()
229
+ releaseActions()
230
+ }
231
+ }
232
+ native.registerHandle(handle.nativeViewProtected)
233
+ }, 0)
234
+ }
235
+ const driver = {
236
+ cancel: () => native.cancel(),
237
+ setEnabled: (value: boolean) => native.setEnabled(value),
238
+ setReducedMotion: (value: boolean) => native.setReducedMotion(value),
239
+ dispose() {
240
+ if (ended) return
241
+ ended = true
242
+ if (focusTimer !== undefined) clearTimeout(focusTimer)
243
+ list.off('itemLoading', loaded)
244
+ for (const row of rows.values()) {
245
+ if (row.timer !== undefined) clearTimeout(row.timer)
246
+ row.release()
247
+ }
248
+ rows.clear()
249
+ native.dispose()
250
+ },
251
+ }
252
+ list.on('itemLoading', loaded)
253
+ try {
254
+ list.refresh()
255
+ } catch (error) {
256
+ driver.dispose()
257
+ throw error
258
+ }
259
+ return driver
260
+ },
261
+ }
262
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReorderKey, ReorderMove } from './types.js'
2
+
3
+ export function itemKeys<T, K extends ReorderKey>(
4
+ items: readonly T[],
5
+ key: (item: T) => K,
6
+ ): K[] {
7
+ const keys = items.map(key)
8
+ if (
9
+ keys.some(
10
+ (value) =>
11
+ (typeof value !== 'string' && typeof value !== 'number') ||
12
+ (typeof value === 'number' && !Number.isFinite(value)),
13
+ )
14
+ )
15
+ throw new TypeError('Reorder keys must be strings or finite numbers.')
16
+ if (new Set(keys).size !== keys.length)
17
+ throw new TypeError('Reorder keys must be unique and stable across recycling.')
18
+ return keys
19
+ }
20
+
21
+ export function sameKeys<K>(a: readonly K[], b: readonly K[]): boolean {
22
+ return a.length === b.length && a.every((key, index) => key === b[index])
23
+ }
24
+
25
+ /** toIndex is the final position, after removing the dragged item. */
26
+ export function moveAt<K extends ReorderKey>(
27
+ keys: readonly K[],
28
+ fromIndex: number,
29
+ toIndex: number,
30
+ ): ReorderMove<K> | undefined {
31
+ if (
32
+ !Number.isInteger(fromIndex) ||
33
+ !Number.isInteger(toIndex) ||
34
+ fromIndex < 0 ||
35
+ toIndex < 0 ||
36
+ fromIndex >= keys.length ||
37
+ toIndex >= keys.length ||
38
+ fromIndex === toIndex
39
+ )
40
+ return undefined
41
+ return {
42
+ key: keys[fromIndex]!,
43
+ beforeKey: keys[toIndex < fromIndex ? toIndex : toIndex + 1] ?? null,
44
+ fromIndex,
45
+ toIndex,
46
+ }
47
+ }
@@ -0,0 +1,10 @@
1
+ import { GridLayout, isAndroid } from '@nativescript/core'
2
+
3
+ /** Android dispatch boundary: owns the drag after the source cell is recycled. */
4
+ export class ReorderSurface extends GridLayout {
5
+ override createNativeView(): ReturnType<GridLayout['createNativeView']> {
6
+ return isAndroid
7
+ ? new org.rafaelaugusto.jolt.ReorderSurface(this._context)
8
+ : super.createNativeView()
9
+ }
10
+ }
@@ -0,0 +1,87 @@
1
+ import type { View } from '@nativescript/core'
2
+
3
+ export type ReorderKey = string | number
4
+ export type ReorderCancelReason =
5
+ | 'cancelled'
6
+ | 'unchanged'
7
+ | 'data-changed'
8
+ | 'removed'
9
+ | 'unloaded'
10
+ | 'disposed'
11
+ | 'disabled'
12
+ | 'geometry-changed'
13
+
14
+ export interface ReorderMove<K extends ReorderKey = ReorderKey> {
15
+ key: K
16
+ /** Insert before this identity; null appends. The app owns the mutation. */
17
+ beforeKey: K | null
18
+ fromIndex: number
19
+ toIndex: number
20
+ }
21
+
22
+ export interface ReorderOptions<T, K extends ReorderKey = ReorderKey> {
23
+ key(item: T): K
24
+ /** View id, or a resolver within a realized row. Omit to drag anywhere on the row. */
25
+ handle?: string | ((row: View) => View | undefined)
26
+ onCommit(move: ReorderMove<K>): void
27
+ onStart?: (key: K) => void
28
+ onCancel?: (event: { key: K; reason: ReorderCancelReason }) => void
29
+ /** Long-press activation in milliseconds; default 350. */
30
+ longPress?: number
31
+ /** Edge region in DIPs and maximum native autoscroll speed in DIPs/second. */
32
+ edge?: { size?: number; speed?: number }
33
+ accessibility?: { moveUp?: string; moveDown?: string }
34
+ }
35
+
36
+ export interface Reorder<K extends ReorderKey = ReorderKey> {
37
+ readonly activeKey: K | undefined
38
+ readonly disposed: boolean
39
+ /** A keyboard/button alternative. Returns false for a missing key or unchanged order. */
40
+ move(key: K, beforeKey: K | null): boolean
41
+ moveUp(key: K): boolean
42
+ moveDown(key: K): boolean
43
+ cancel(): void
44
+ /** Call after in-place changes to a plain array or row geometry. */
45
+ refresh(): void
46
+ pause(): void
47
+ play(): void
48
+ dispose(): void
49
+ }
50
+
51
+ export type ReorderNativeEvent =
52
+ | { type: 'start'; index: number }
53
+ | { type: 'commit'; index: number; toIndex: number }
54
+ | { type: 'cancel'; geometryChanged?: boolean }
55
+
56
+ export interface ReorderAdapterDriver {
57
+ cancel(): void
58
+ setEnabled(enabled: boolean): void
59
+ setReducedMotion(reduced: boolean): void
60
+ dispose(): void
61
+ }
62
+
63
+ /**
64
+ * Optional list-plugin integration. Indices refer to the current items until a
65
+ * change invalidates the gesture. Drivers own native input, measured variable
66
+ * heights, snapshot overlays, recycled-cell restoration and edge scrolling.
67
+ * Emit only lifecycle events; do not bridge pointer/display frames to JS.
68
+ */
69
+ export interface ReorderAdapter<T> {
70
+ readonly view: View
71
+ items(): readonly T[]
72
+ /** Notify synchronously when identities/order/content change. */
73
+ subscribe(changed: () => void): () => void
74
+ attach(config: {
75
+ handle: ReorderOptions<T>['handle']
76
+ longPress: number
77
+ edgeSize: number
78
+ edgeSpeed: number
79
+ reducedMotion: boolean
80
+ accessibility: { moveUp: string; moveDown: string }
81
+ event(event: ReorderNativeEvent): void
82
+ /** Returns the moved identity; resolve it again after the app updates data. */
83
+ move(index: number, direction: -1 | 1): ReorderKey | undefined
84
+ keyAt(index: number): ReorderKey | undefined
85
+ indexOf(key: ReorderKey): number
86
+ }): ReorderAdapterDriver
87
+ }
@@ -0,0 +1,139 @@
1
+ /// <reference types="@nativescript/types-ios/lib/ios/objc-x86_64/objc!SceneKit" preserve="true" />
2
+ import {
3
+ nativePropertyTarget,
4
+ registerNativeProperty,
5
+ type NativeProperty,
6
+ } from './native-property.js'
7
+
8
+ export type SceneUniformType = 'float' | 'float2' | 'float3' | 'float4'
9
+ export type SceneUniformValue<T extends SceneUniformType> = T extends 'float'
10
+ ? number
11
+ : T extends 'float2'
12
+ ? readonly [number, number]
13
+ : T extends 'float3'
14
+ ? readonly [number, number, number]
15
+ : readonly [number, number, number, number]
16
+ export interface SceneUniformOptions<T extends SceneUniformType> {
17
+ type: T
18
+ /** Finite inclusive limits applied to every component; spring overshoot is clamped. */
19
+ range: readonly [number, number]
20
+ /** One application-defined unit per component. Vectors have no implicit color conversion. */
21
+ units: readonly string[]
22
+ /** Spatial uniforms jump under Reduced Motion; appearance uniforms keep their timing. */
23
+ motion: 'spatial' | 'appearance'
24
+ }
25
+ function runtime() {
26
+ if (typeof JoltSceneShaderProperties === 'undefined')
27
+ throw new Error(
28
+ 'Update @rafaelaugusto/jolt-native and rebuild the native app to use SceneKit environment and shader adapters.',
29
+ )
30
+ return JoltSceneShaderProperties
31
+ }
32
+ function sink(
33
+ create: (error: interop.Reference<NSError>) => JoltPropertySink | null,
34
+ ): JoltPropertySink {
35
+ const error = new interop.Reference<NSError>()
36
+ const result = create(error)
37
+ if (!result)
38
+ throw new Error(
39
+ error.value?.localizedDescription ??
40
+ 'Invalid SceneKit property target or shader schema.',
41
+ )
42
+ return result
43
+ }
44
+
45
+ /** Scene environment multiplier, nonnegative; leaves the environment/background images untouched. */
46
+ export const sceneKitEnvironmentIntensity: (scene: SCNScene) => NativeProperty<number> =
47
+ registerNativeProperty<number>({
48
+ name: 'jolt.scenekit/environment-intensity',
49
+ units: ['environment illumination multiplier'],
50
+ platforms: ['ios'],
51
+ encode: (value) => [value],
52
+ decode: (values) => values[0]!,
53
+ validate(value) {
54
+ if (!Number.isFinite(value) || value < 0)
55
+ throw new RangeError('Environment intensity must be finite and nonnegative.')
56
+ },
57
+ interpolate: (a, b, t) => Math.max(0, a + (b - a) * t),
58
+ create: (target) =>
59
+ sink((error) => runtime().environmentError(target as NSObject, error)),
60
+ })
61
+
62
+ /** Declared Metal shaderModifiers input, initialized by the application before attachment. */
63
+ export function sceneKitUniform<T extends SceneUniformType>(
64
+ target: SCNMaterial | SCNGeometry,
65
+ name: string,
66
+ options: SceneUniformOptions<T>,
67
+ ): NativeProperty<SceneUniformValue<T>> {
68
+ if (
69
+ typeof name !== 'string' ||
70
+ !/^[A-Za-z][A-Za-z0-9_]*$/.test(name) ||
71
+ /^(u_|a_|v_|scn_)/.test(name)
72
+ )
73
+ throw new TypeError(
74
+ 'Use a declared custom uniform identifier without a reserved SceneKit prefix or KVC path.',
75
+ )
76
+ if (!options || !['float', 'float2', 'float3', 'float4'].includes(options.type))
77
+ throw new TypeError('Shader uniforms support float, float2, float3 and float4.')
78
+ const count = options.type === 'float' ? 1 : Number(options.type.slice(-1))
79
+ const range = options.range
80
+ if (
81
+ !Array.isArray(range) ||
82
+ range.length !== 2 ||
83
+ !range.every((n) => Number.isFinite(n) && Math.abs(n) <= 3.4028234663852886e38) ||
84
+ range[0] >= range[1]
85
+ )
86
+ throw new RangeError('Declare an ordered finite range for the shader components.')
87
+ if (
88
+ !Array.isArray(options.units) ||
89
+ options.units.length !== count ||
90
+ options.units.some((unit) => typeof unit !== 'string' || !unit.trim())
91
+ )
92
+ throw new TypeError('Declare one nonempty unit for each shader component.')
93
+ if (!['spatial', 'appearance'].includes(options.motion))
94
+ throw new TypeError('Declare whether the shader motion is spatial or appearance.')
95
+ // Copy the schema: caller mutation cannot change an already attached native sink.
96
+ const [low, high] = range,
97
+ type = options.type,
98
+ units = [...options.units]
99
+ const vector = (value: SceneUniformValue<T>): number[] => {
100
+ const values = type === 'float' ? [value] : value
101
+ if (
102
+ !Array.isArray(values) ||
103
+ values.length !== count ||
104
+ values.some(
105
+ (n) => typeof n !== 'number' || !Number.isFinite(n) || n < low || n > high,
106
+ )
107
+ )
108
+ throw new RangeError(
109
+ `${name} needs ${count} finite component(s) in ${low}..${high}.`,
110
+ )
111
+ return [...values] as number[]
112
+ }
113
+ return nativePropertyTarget(target, {
114
+ name: `jolt.scenekit/uniform-${name}`,
115
+ units,
116
+ platforms: ['ios'],
117
+ spatial: options.motion === 'spatial',
118
+ encode: vector,
119
+ decode: (values) =>
120
+ (type === 'float' ? values[0]! : [...values]) as SceneUniformValue<T>,
121
+ interpolate(a, b, t) {
122
+ const first = vector(a),
123
+ last = vector(b)
124
+ const result = first.map((value, i) =>
125
+ Math.max(low, Math.min(high, value + (last[i]! - value) * t)),
126
+ )
127
+ return (type === 'float' ? result[0]! : result) as SceneUniformValue<T>
128
+ },
129
+ create: (object) =>
130
+ sink((error) =>
131
+ runtime().uniformNameConfigurationError(
132
+ object as NSObject,
133
+ name,
134
+ JSON.stringify({ type, low, high }),
135
+ error,
136
+ ),
137
+ ),
138
+ })
139
+ }