react-native-magnetkit 0.0.0-stage → 0.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.
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/android/build.gradle +18 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetDipole.kt +76 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetModel.kt +189 -0
- package/android/src/main/java/dev/libraries/magnetkit/MagnetSource.kt +598 -0
- package/android/src/main/java/dev/libraries/magnetkit/MfxlNet.kt +92 -0
- package/android/src/main/java/dev/libraries/magnetkit/Vec3.kt +30 -0
- package/android/src/main/java/dev/libraries/magnetkit/rn/MagnetKitModule.kt +172 -0
- package/app.plugin.js +1 -0
- package/build/MagnetCalibration.d.ts +13 -0
- package/build/MagnetCalibration.js +58 -0
- package/build/MagnetKitModule.d.ts +52 -0
- package/build/MagnetKitModule.js +2 -0
- package/build/MagneticView.d.ts +15 -0
- package/build/MagneticView.js +129 -0
- package/build/core/calibration-core.d.ts +43 -0
- package/build/core/calibration-core.js +84 -0
- package/build/core/field.d.ts +64 -0
- package/build/core/field.js +341 -0
- package/build/core/fit.d.ts +13 -0
- package/build/core/fit.js +50 -0
- package/build/core/types.d.ts +37 -0
- package/build/core/types.js +2 -0
- package/build/hooks.d.ts +7 -0
- package/build/hooks.js +29 -0
- package/build/index.d.ts +9 -0
- package/build/index.js +7 -0
- package/build/magnet.d.ts +29 -0
- package/build/magnet.js +66 -0
- package/expo-module.config.json +9 -0
- package/ios/MagnetKit/MFXLNet.swift +89 -0
- package/ios/MagnetKit/MagnetLocator.swift +252 -0
- package/ios/MagnetKit/MagnetModel.swift +56 -0
- package/ios/MagnetKit/MagnetPresence.swift +81 -0
- package/ios/MagnetKit/MagnetSource.swift +967 -0
- package/ios/MagnetKit/MagnetTap.swift +53 -0
- package/ios/MagnetKitModule.swift +147 -0
- package/ios/MagnetKitRN.podspec +26 -0
- package/package.json +54 -4
- package/plugin/withMagnetKit.js +60 -0
- package/src/MagnetCalibration.tsx +87 -0
- package/src/MagnetKitModule.ts +42 -0
- package/src/MagneticView.tsx +153 -0
- package/src/core/calibration-core.ts +112 -0
- package/src/core/field.ts +295 -0
- package/src/core/fit.ts +55 -0
- package/src/core/types.ts +32 -0
- package/src/hooks.ts +32 -0
- package/src/index.ts +9 -0
- package/src/magnet.ts +77 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jakub Antalik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-magnetkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Magnetic UI for React Native: a real magnet beside the phone pulls your
|
|
4
|
+
interface. An Expo module around MagnetKit, the on-device magnet model for
|
|
5
|
+
iOS (Swift) and Android (Kotlin), with React hooks, a `MagneticView` that the
|
|
6
|
+
magnet pulls, and a ready-made calibration screen.
|
|
7
|
+
|
|
8
|
+
Docs and the model: https://libraries.dev/magnetic
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npx expo install react-native-magnetkit
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The model (`Magnet.mfxl`, 2 MB) is not in this package. Download it from
|
|
17
|
+
https://libraries.dev/magnetic (Model tab), put it in your app's `assets/`,
|
|
18
|
+
and point the config plugin at it:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"expo": {
|
|
23
|
+
"plugins": [["react-native-magnetkit", { "model": "./assets/Magnet.mfxl" }]]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The plugin copies it into the iOS app target and Android's assets at
|
|
29
|
+
prebuild. It ships native code, so use a development build
|
|
30
|
+
(`npx expo run:ios`, `npx expo run:android` or EAS), not Expo Go.
|
|
31
|
+
|
|
32
|
+
Bare React Native: run `npx install-expo-modules`, then add `Magnet.mfxl` to
|
|
33
|
+
the iOS target's Copy Bundle Resources and to `android/app/src/main/assets`.
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Magnet, MagnetCalibration, MagneticView, useMagnet, useMagnetGestures } from "react-native-magnetkit";
|
|
39
|
+
|
|
40
|
+
// Views the magnet pulls. They collide, and spring back when it leaves.
|
|
41
|
+
<MagneticView style={styles.chip}><Text>Home</Text></MagneticView>
|
|
42
|
+
|
|
43
|
+
// Once per phone and magnet: five rings, about 10 s, kept across launches.
|
|
44
|
+
<MagnetCalibration visible={calibrating} onClose={(ok) => setCalibrating(false)} />
|
|
45
|
+
|
|
46
|
+
// Readings and gestures.
|
|
47
|
+
const f = useMagnet(); // f.pos (window points), f.presence, f.pull, f.strength…
|
|
48
|
+
useMagnetGestures((g) => { if (g.g === "tap") openMenu(g.at); });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `useMagnet()` re-renders once per frame; `useMagnetFrame(cb)` gets every
|
|
52
|
+
reading without rendering.
|
|
53
|
+
- `Magnet.hasModel()`, `Magnet.zero()`, `Magnet.getMagnet()`,
|
|
54
|
+
`Magnet.clearMagnet()`, `Magnet.addFrameListener()`.
|
|
55
|
+
- `getMagneticField()` tunes the physics (stiffness, damping, force, reach).
|
|
56
|
+
- `calibrate({ onUpdate })` runs a calibration headless, to draw your own rings.
|
|
57
|
+
|
|
58
|
+
The sensor runs only while something listens. Keep the magnet 2 cm or more
|
|
59
|
+
from the sensor (`f.tooClose`).
|
|
60
|
+
|
|
61
|
+
## Repository layout
|
|
62
|
+
|
|
63
|
+
The Swift and Kotlin sources come from `../../ios/MagnetKit` and
|
|
64
|
+
`../../android/magnetkit`, and the shared TypeScript (readings, calibration
|
|
65
|
+
fit, Magnetic UI physics) from `packages/magnetkit-web`. `npm run sync`
|
|
66
|
+
copies them in (`prepare` and `build` run it); the copies are not committed.
|
|
67
|
+
`../magnet-example` is a working app.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
The code is MIT. The model is under the Libraries.dev Model License: free for
|
|
72
|
+
development, prototypes and non-commercial apps; a commercial app needs
|
|
73
|
+
Libraries.dev Pro or Business.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
id 'com.android.library'
|
|
3
|
+
id 'expo-module-gradle-plugin'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
group = 'dev.libraries.magnetkit.rn'
|
|
7
|
+
version = '0.1.0'
|
|
8
|
+
|
|
9
|
+
android {
|
|
10
|
+
namespace "dev.libraries.magnetkit.rn"
|
|
11
|
+
defaultConfig {
|
|
12
|
+
versionCode 1
|
|
13
|
+
versionName "0.1.0"
|
|
14
|
+
}
|
|
15
|
+
lintOptions {
|
|
16
|
+
abortOnError false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
package dev.libraries.magnetkit
|
|
2
|
+
|
|
3
|
+
import kotlin.math.atan2
|
|
4
|
+
import kotlin.math.cos
|
|
5
|
+
import kotlin.math.max
|
|
6
|
+
import kotlin.math.min
|
|
7
|
+
import kotlin.math.pow
|
|
8
|
+
import kotlin.math.sin
|
|
9
|
+
import kotlin.math.sqrt
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where an upright magnet is, worked out directly from the physics (a dipole
|
|
13
|
+
* whose axis points roughly at the screen). Needs the magnet's face; the
|
|
14
|
+
* strength only sets the distance. The same maths as MagnetKit's MagnetDipole.
|
|
15
|
+
*/
|
|
16
|
+
object MagnetDipole {
|
|
17
|
+
/** cos²α from the field's vertical share q = face·Bz/|B| (closed form). */
|
|
18
|
+
fun cos2(q0: Double): Double {
|
|
19
|
+
val q = min(1.0, max(-1.0, q0))
|
|
20
|
+
val u = (q + sqrt(q * q + 8)) / 2
|
|
21
|
+
return min(1.0, max(0.0, (u * u - 1) / 3))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The magnet relative to the sensor (cm, device axes) and how much of the field lies in the screen's plane. */
|
|
25
|
+
fun locate(b: Vec3, face: Int, strength: Double, maxHeight: Double? = null): Pair<Vec3, Double>? {
|
|
26
|
+
val nb = b.length
|
|
27
|
+
if (nb <= 1e-6 || strength <= 0) return null
|
|
28
|
+
val f = if (face < 0) -1.0 else 1.0
|
|
29
|
+
val c2 = cos2(f * b.z / nb)
|
|
30
|
+
val c = sqrt(c2); val s = sqrt(max(0.0, 1 - c2))
|
|
31
|
+
var phi = atan2(b.y, b.x)
|
|
32
|
+
if (f < 0) phi += Math.PI
|
|
33
|
+
val r = (strength * sqrt(1 + 3 * c2) / nb).pow(1.0 / 3.0)
|
|
34
|
+
val inPlane = sqrt(b.x * b.x + b.y * b.y) / nb
|
|
35
|
+
var h = r * c; var lateral = r * s
|
|
36
|
+
if (maxHeight != null && h > maxHeight) { h = maxHeight; lateral = sqrt(max(0.0, r * r - h * h)) }
|
|
37
|
+
return Pair(Vec3(lateral * cos(phi), lateral * sin(phi), h), inPlane)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The face and strength from one reading with the magnet held over a known
|
|
42
|
+
* spot (`offsetX`, `offsetY`: from the sensor, cm, device axes): the face
|
|
43
|
+
* and height (0.6–3.6 cm) whose predicted field direction matches best.
|
|
44
|
+
*/
|
|
45
|
+
fun learn(b: Vec3, offsetX: Double, offsetY: Double): Pair<Int, Double>? {
|
|
46
|
+
val nb = b.length
|
|
47
|
+
if (nb <= 1e-6 || sqrt(offsetX * offsetX + offsetY * offsetY) <= 0.5) return null
|
|
48
|
+
val u = b / nb
|
|
49
|
+
var best: Triple<Double, Int, Double>? = null
|
|
50
|
+
for (face in intArrayOf(1, -1)) {
|
|
51
|
+
val m = Vec3(0.0, 0.0, face.toDouble())
|
|
52
|
+
var h = 0.6
|
|
53
|
+
while (h <= 3.6001) {
|
|
54
|
+
val p = Vec3(offsetX, offsetY, h)
|
|
55
|
+
val r = p.length
|
|
56
|
+
val rh = -p / r
|
|
57
|
+
val dir = rh * (3 * m.dot(rh)) - m
|
|
58
|
+
val score = u.dot(dir / dir.length)
|
|
59
|
+
if (best == null || score > best.first) best = Triple(score, face, nb * r * r * r / dir.length)
|
|
60
|
+
h += 0.2
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
val b2 = best ?: return null
|
|
64
|
+
return if (b2.first > 0.6) Pair(b2.second, b2.third) else null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The strength from a magnet held over a spot far from the sensor, at roughly `height` cm above it. */
|
|
68
|
+
fun strength(b: Vec3, offsetX: Double, offsetY: Double, height: Double = 2.4): Double? {
|
|
69
|
+
val nb = b.length
|
|
70
|
+
val lateral = sqrt(offsetX * offsetX + offsetY * offsetY)
|
|
71
|
+
if (nb <= 1e-6 || lateral <= 0.5) return null
|
|
72
|
+
val r = sqrt(lateral * lateral + height * height)
|
|
73
|
+
val c = height / r
|
|
74
|
+
return nb * r * r * r / sqrt(1 + 3 * c * c)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package dev.libraries.magnetkit
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import kotlin.math.exp
|
|
5
|
+
import kotlin.math.hypot
|
|
6
|
+
import kotlin.math.ln
|
|
7
|
+
import kotlin.math.ln1p
|
|
8
|
+
import kotlin.math.max
|
|
9
|
+
import kotlin.math.min
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The magnet model: one on-device model that reads the phone's magnetometer
|
|
13
|
+
* and answers three things — where the magnet is, whether it is really there
|
|
14
|
+
* (or the phone's own magnetised parts are lying), and air taps. The same
|
|
15
|
+
* file as MagnetKit on iOS (Magnet.mfxl, ~2 MB, half precision).
|
|
16
|
+
* `MagnetSource` runs it every frame.
|
|
17
|
+
*
|
|
18
|
+
* The model is not part of this MIT library: it is licensed separately under
|
|
19
|
+
* the Libraries.dev Model License (LICENSE-MODEL.md) and downloaded from
|
|
20
|
+
* libraries.dev. Put Magnet.mfxl in your app's src/main/assets and
|
|
21
|
+
* `bundled` finds it.
|
|
22
|
+
*/
|
|
23
|
+
class MagnetModel private constructor(
|
|
24
|
+
val version: String,
|
|
25
|
+
val locator: MagnetLocator,
|
|
26
|
+
val presence: MagnetPresence,
|
|
27
|
+
val tap: MagnetTap,
|
|
28
|
+
) {
|
|
29
|
+
companion object {
|
|
30
|
+
@Volatile private var cached: MagnetModel? = null
|
|
31
|
+
|
|
32
|
+
/** The model in the app's assets (assets/Magnet.mfxl), or null if missing. */
|
|
33
|
+
fun bundled(context: Context): MagnetModel? = cached ?: synchronized(this) {
|
|
34
|
+
cached ?: runCatching { context.assets.open("Magnet.mfxl").use { it.readBytes() } }
|
|
35
|
+
.getOrNull()?.let { parse(it) }?.also { cached = it }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** "MFXL", header (kind "magnet", version, parts with offsets and lengths), then each part. */
|
|
39
|
+
fun parse(data: ByteArray): MagnetModel? {
|
|
40
|
+
val header = MfxlNet.header(data) ?: return null
|
|
41
|
+
if (header.optString("kind") != "magnet") return null
|
|
42
|
+
val parts = header.optJSONArray("parts") ?: return null
|
|
43
|
+
val start = 8 + MfxlNet.headerLength(data)
|
|
44
|
+
fun part(name: String): ByteArray? {
|
|
45
|
+
for (i in 0 until parts.length()) {
|
|
46
|
+
val p = parts.getJSONObject(i)
|
|
47
|
+
if (p.optString("name") != name) continue
|
|
48
|
+
val off = start + p.getInt("offset"); val len = p.getInt("length")
|
|
49
|
+
if (off + len > data.size) return null
|
|
50
|
+
return data.copyOfRange(off, off + len)
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
val l = part("locator")?.let { MagnetLocator.parse(it) } ?: return null
|
|
55
|
+
val p = part("presence")?.let { MagnetPresence.parse(it) } ?: return null
|
|
56
|
+
val t = part("tap")?.let { MagnetTap.parse(it) } ?: return null
|
|
57
|
+
return MagnetModel(header.optString("version", "1.0"), l, p, t)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Where the magnet is, from one reading: position relative to the sensor (cm, device axes) and its 1-σ spread, cm. */
|
|
63
|
+
class MagnetLocator private constructor(
|
|
64
|
+
private val net: MfxlNet,
|
|
65
|
+
private val logbMid: Float,
|
|
66
|
+
private val logkMid: Float,
|
|
67
|
+
private val logrMid: Float,
|
|
68
|
+
private val lvMin: Float,
|
|
69
|
+
private val lvMax: Float,
|
|
70
|
+
val takesFace: Boolean,
|
|
71
|
+
) {
|
|
72
|
+
fun locate(field: Vec3, strength: Double?, face: Int?): Pair<Vec3, Double>? {
|
|
73
|
+
val nb = field.length
|
|
74
|
+
if (nb <= 1e-6) return null
|
|
75
|
+
val knownFace = if (takesFace) face?.let { if (it < 0) -1.0 else 1.0 } else null
|
|
76
|
+
// Known face: turn the field to face +1. Unknown: a flipped magnet
|
|
77
|
+
// gives −B at the same spot, so the network sees one face (Bz ≥ 0).
|
|
78
|
+
val f = knownFace?.let { field * it } ?: if (field.z < 0) -field else field
|
|
79
|
+
val input = ArrayList<Float>(7)
|
|
80
|
+
input += (f.x / nb).toFloat(); input += (f.y / nb).toFloat(); input += (f.z / nb).toFloat()
|
|
81
|
+
input += (ln(nb).toFloat() - logbMid) / 2f
|
|
82
|
+
input += strength?.let { (ln(max(1.0, it)).toFloat() - logkMid) / 1.5f } ?: 0f
|
|
83
|
+
input += if (strength == null) 0f else 1f
|
|
84
|
+
if (takesFace) input += if (knownFace == null) 0f else 1f
|
|
85
|
+
val o = net.forward(input.toFloatArray())
|
|
86
|
+
val d = Vec3(o[0].toDouble(), o[1].toDouble(), o[2].toDouble())
|
|
87
|
+
val dl = d.length
|
|
88
|
+
if (dl <= 1e-9) return null
|
|
89
|
+
val r = exp((o[3] + logrMid).toDouble())
|
|
90
|
+
fun sd(v: Float) = exp(0.5 * min(lvMax, max(lvMin, v)))
|
|
91
|
+
val spread = r * hypot(sd(o[4]), sd(o[5]))
|
|
92
|
+
return Pair(d / dl * r, spread)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
companion object {
|
|
96
|
+
fun parse(data: ByteArray): MagnetLocator? {
|
|
97
|
+
val net = MfxlNet.parse(data, "locator") ?: return null
|
|
98
|
+
val h = net.header
|
|
99
|
+
val version = h.optInt("version", 0)
|
|
100
|
+
if (version != 2 && version != 3) return null
|
|
101
|
+
val clamp = h.optJSONArray("logvarClamp")
|
|
102
|
+
if (net.inputs != (if (version == 3) 7 else 6) || net.outputs != 6) return null
|
|
103
|
+
return MagnetLocator(
|
|
104
|
+
net,
|
|
105
|
+
h.optDouble("logbMid", ln(40.0)).toFloat(), h.optDouble("logkMid", ln(10000.0)).toFloat(),
|
|
106
|
+
h.optDouble("logrMid", ln(5.0)).toFloat(),
|
|
107
|
+
(clamp?.optDouble(0, -10.0) ?: -10.0).toFloat(), (clamp?.optDouble(1, 4.0) ?: 4.0).toFloat(),
|
|
108
|
+
version == 3,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Is a magnet really there, or is the phone's own offset lying — from the last 15 s. */
|
|
115
|
+
class MagnetPresence private constructor(
|
|
116
|
+
private val net: MfxlNet,
|
|
117
|
+
val hz: Double, val short: Int, val longN: Int, val longStep: Int, val window: Double, val contextInputs: Int,
|
|
118
|
+
) {
|
|
119
|
+
/**
|
|
120
|
+
* @param history the field change (µT, against the current zero) at [hz], oldest first, ending now.
|
|
121
|
+
* @param overloadedFor seconds since an unzeroed overload, told only right after the field drops; null: no.
|
|
122
|
+
* @return presence (0–1) and the non-magnet part of the reading now, µT.
|
|
123
|
+
*/
|
|
124
|
+
fun evaluate(history: List<Vec3>, overloadedFor: Double? = null): Pair<Double, Vec3> {
|
|
125
|
+
val n = history.size
|
|
126
|
+
if (n < max(short, longN * longStep)) return Pair(1.0, Vec3.ZERO)
|
|
127
|
+
val last = history[n - 1]
|
|
128
|
+
val s = max(10.0, last.length)
|
|
129
|
+
val x = FloatArray(short * 3 + longN + 4 + contextInputs)
|
|
130
|
+
var k = 0
|
|
131
|
+
for (i in n - short until n) {
|
|
132
|
+
val v = history[i] / s
|
|
133
|
+
x[k++] = v.x.coerceIn(-6.0, 6.0).toFloat(); x[k++] = v.y.coerceIn(-6.0, 6.0).toFloat(); x[k++] = v.z.coerceIn(-6.0, 6.0).toFloat()
|
|
134
|
+
}
|
|
135
|
+
var j = n - longN * longStep
|
|
136
|
+
repeat(longN) { x[k++] = (ln1p(history[j].length / 10) / 6).toFloat(); j += longStep }
|
|
137
|
+
var peak = 0.0; var peakAt = 0
|
|
138
|
+
for ((i, v) in history.withIndex()) { val m = v.length; if (m > peak) { peak = m; peakAt = i } }
|
|
139
|
+
x[k++] = (ln1p(peak / 100) / 4).toFloat()
|
|
140
|
+
x[k++] = ((n - 1 - peakAt) / hz / 15).toFloat()
|
|
141
|
+
x[k++] = (ln1p(s / 10) / 6).toFloat()
|
|
142
|
+
x[k++] = if (peak > 2000) 1f else 0f
|
|
143
|
+
if (contextInputs >= 2) {
|
|
144
|
+
x[k++] = if (overloadedFor == null) 0f else 1f
|
|
145
|
+
x[k++] = overloadedFor?.let { min(1.6, ln1p(it / 10) / ln1p(60.0)).toFloat() } ?: 0f
|
|
146
|
+
}
|
|
147
|
+
val o = net.forward(x)
|
|
148
|
+
val p = 1 / (1 + exp(-o[0].toDouble()))
|
|
149
|
+
return Pair(p, Vec3(o[1].toDouble(), o[2].toDouble(), o[3].toDouble()) * s)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
companion object {
|
|
153
|
+
fun parse(data: ByteArray): MagnetPresence? {
|
|
154
|
+
val net = MfxlNet.parse(data, "presence") ?: return null
|
|
155
|
+
val h = net.header
|
|
156
|
+
val short = h.optInt("short", 64); val longN = h.optInt("longN", 40); val ctx = h.optInt("context", 0)
|
|
157
|
+
if (net.inputs != short * 3 + longN + 4 + ctx || net.outputs != 4) return null
|
|
158
|
+
return MagnetPresence(net, h.optDouble("hz", 50.0), short, longN, h.optInt("longStep", 5), h.optDouble("window", 15.0), ctx)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Did an air tap — the hovering magnet dipped toward the glass and back — just finish, from the last 1.28 s. */
|
|
164
|
+
class MagnetTap private constructor(private val net: MfxlNet, val hz: Double, val window: Int) {
|
|
165
|
+
fun evaluate(history: List<Vec3>): Double {
|
|
166
|
+
if (history.size < window) return 0.0
|
|
167
|
+
val w = history.subList(history.size - window, history.size)
|
|
168
|
+
val mags = w.map { it.length }
|
|
169
|
+
val s = max(10.0, mags.sorted()[(window - 1) / 2]) // the lower median, as torch's
|
|
170
|
+
val x = FloatArray(window * 4 + 1)
|
|
171
|
+
var k = 0
|
|
172
|
+
for (v in w) {
|
|
173
|
+
x[k++] = (v.x / s).coerceIn(-8.0, 8.0).toFloat(); x[k++] = (v.y / s).coerceIn(-8.0, 8.0).toFloat(); x[k++] = (v.z / s).coerceIn(-8.0, 8.0).toFloat()
|
|
174
|
+
}
|
|
175
|
+
for (m in mags) x[k++] = ln(max(m, 1.0) / s).coerceIn(-4.0, 4.0).toFloat()
|
|
176
|
+
x[k] = (ln1p(s / 10) / 6).toFloat()
|
|
177
|
+
val o = net.forward(x)
|
|
178
|
+
return 1 / (1 + exp(-o[0].toDouble()))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
companion object {
|
|
182
|
+
fun parse(data: ByteArray): MagnetTap? {
|
|
183
|
+
val net = MfxlNet.parse(data, "tap") ?: return null
|
|
184
|
+
val window = net.header.optInt("window", 64)
|
|
185
|
+
if (net.inputs != window * 4 + 1 || net.outputs != 1) return null
|
|
186
|
+
return MagnetTap(net, net.header.optDouble("hz", 50.0), window)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|