mudra-skills 1.2.0 → 3.0.2
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/README.md +0 -1
- package/package.json +2 -2
- package/skills/mudra-master/SKILL.md +1 -7
- package/skills/mudra-master/mudra-preview/SKILL.md +8 -9
- package/skills/mudra-master/mudra-preview/references/agent_protocol.json +0 -12
- package/skills/mudra-master/mudra-preview/references/{promt.md → prompt.md} +11 -94
- package/skills/mudra-master/mudra-xr/SKILL.md +6 -6
- package/skills/mudra-master/mudra-xr/references/agent_protocol.json +1 -14
- package/skills/mudra-master/mudra-xr/references/{promt.md → prompt.md} +329 -240
- package/skills/mudra-preview/SKILL.md +34 -26
- package/skills/mudra-preview/assets/mudra-ultimate-template.html +997 -891
- package/skills/mudra-preview/references/agent_protocol.json +191 -136
- package/skills/mudra-preview/references/{promt.md → prompt.md} +173 -271
- package/skills/mudra-xr/SKILL.md +6 -6
- package/skills/mudra-xr/references/agent_protocol.json +1 -14
- package/skills/mudra-xr/references/{promt.md → prompt.md} +329 -240
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mudra-skills",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "Mudra Band skills for Claude Code, Codex, and Antigravity — 2D, 3D/XR, or auto-classified",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
"webxr"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"mudra-skills": "^
|
|
29
|
+
"mudra-skills": "^3.0.0"
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mudra-master
|
|
3
|
-
version:
|
|
3
|
+
version: 3.0.0
|
|
4
4
|
description: Master router for Mudra Band app builds. Use when the user describes an app, experience, prototype, timer, counter, or tool to build with the Mudra Band but has not made clear whether they want a 2D (flat/screen) or 3D (spatial/XR/VR/AR) version. This skill classifies the prompt deterministically and hands off the original prompt verbatim to either mudra-preview (2D) or mudra-xr (3D), asking exactly one 2D-or-3D disambiguation question only when the dimension cannot be inferred. Do not use for pure code edits, repo questions, or any request that is not a Mudra-app build.
|
|
5
5
|
---
|
|
6
6
|
# Mudra Master Skill
|
|
@@ -16,12 +16,6 @@ Activate when the user:
|
|
|
16
16
|
- Describes something they want to **build / make / create / prototyp e** with the Mudra Band — an app, experience, tool, timer, counter, demo, game, dashboard, visualizer, panel, or scene — AND
|
|
17
17
|
- Has not made clear whether they want a 2D (screen-based) or 3D (spatial / XR / VR / AR) version.
|
|
18
18
|
|
|
19
|
-
I have 2 sklills and I want to build other skill master that can use both skill, how it can be done ?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
and what the structure should look like ?Prefer letting `mudra-preview` or `mudra-xr` activate directly when Claude Code's skill-discovery heuristic matches a child skill's description confidently on an explicit-dimension prompt. This skill is the router for ambiguous prompts and the safe fallback for explicit-dimension prompts that still reach it.
|
|
24
|
-
|
|
25
19
|
### The verbatim-prompt rule (FR-012)
|
|
26
20
|
|
|
27
21
|
At no point may the agent modify, summarise, enrich, rephrase, prefix, suffix, or markdown-wrap the user's prompt. The string received is the exact string passed as `args` to the child skill, trimmed only at its leading and trailing whitespace edges. No "Build a 2D …" rewrites. No context injection. No examples appended.
|
|
@@ -8,23 +8,23 @@ description: Generate a working Mudra Band interactive app preview as a single-f
|
|
|
8
8
|
|
|
9
9
|
Generate a complete, working single-file HTML app controlled by Mudra Band signals.
|
|
10
10
|
|
|
11
|
-
**Mandatory feature
|
|
11
|
+
**Mandatory feature :** Every generated app MUST
|
|
12
12
|
include a Manual/Mudra **Mode toggle** as defined in `references/promt.md`
|
|
13
13
|
§ "Mode Toggle (Manual / Mudra) — Required". Manual is the default; Mudra
|
|
14
14
|
opens a single WebSocket lazily and disables the simulator panel so signals
|
|
15
15
|
come only from the band.
|
|
16
16
|
|
|
17
|
-
**
|
|
17
|
+
**No disconnect overlay:** Do **not** render a "Band disconnected" overlay,
|
|
18
18
|
toast, or any other separate disconnect alert. Connection state is
|
|
19
19
|
communicated **only** through the existing connection-status pill (which
|
|
20
20
|
shows "Connecting…" / "Connected" / "Disconnected"). No extra notice,
|
|
21
21
|
no banner, no modal.
|
|
22
22
|
|
|
23
|
-
**Branding
|
|
23
|
+
**Branding:** The footer/badge text MUST be exactly
|
|
24
24
|
**"Created by Mudra"** — never "Created with Mudra Studio" or any
|
|
25
25
|
other variant.
|
|
26
26
|
|
|
27
|
-
**Mandatory feature
|
|
27
|
+
**Mandatory feature:** Connection state MUST reflect the **band**, not
|
|
28
28
|
the WebSocket. The Companion service accepts socket connections even when no
|
|
29
29
|
band is paired, so flipping to "Connected" on `ws.onopen` is a lie. Every
|
|
30
30
|
generated app MUST send `{command:"get_status"}` on open and poll it every
|
|
@@ -32,7 +32,7 @@ generated app MUST send `{command:"get_status"}` on open and poll it every
|
|
|
32
32
|
`data.device.state === "connected"`. See `references/promt.md` §
|
|
33
33
|
"Disconnect detection — band state via `get_status` polling (mandatory)".
|
|
34
34
|
|
|
35
|
-
The canonical protocol is in `references/agent_protocol.json
|
|
35
|
+
The canonical protocol is in `references/agent_protocol.json`.
|
|
36
36
|
|
|
37
37
|
## Steps
|
|
38
38
|
|
|
@@ -87,7 +87,7 @@ Every generated app MUST include a **compact, always-visible simulator panel** w
|
|
|
87
87
|
|
|
88
88
|
6. **Report the file path** so the user can open it in a browser immediately.
|
|
89
89
|
|
|
90
|
-
## Default Signal Set
|
|
90
|
+
## Default Signal Set
|
|
91
91
|
|
|
92
92
|
Unless the user explicitly asks for a different signal, every generated
|
|
93
93
|
app MUST restrict itself to **at most these signals** (subject to the
|
|
@@ -127,7 +127,7 @@ Rules:
|
|
|
127
127
|
`nav_direction` — pick directional motion OR the IMU+Biometric bundle,
|
|
128
128
|
never both.
|
|
129
129
|
- Other gesture subtypes (`twist`, `double_twist`, etc.) and other
|
|
130
|
-
signals (`button`, `imu_acc`, `imu_gyro`, `snc
|
|
130
|
+
signals (`button`, `imu_acc`, `imu_gyro`, `snc`) are
|
|
131
131
|
**off by default**. Only include them when the user's prompt names
|
|
132
132
|
them, names a synonym from the Signal Inference table in
|
|
133
133
|
`references/promt.md` § "Signal Inference Reference", or describes
|
|
@@ -146,6 +146,5 @@ Rules:
|
|
|
146
146
|
- Motion modes are mutually exclusive: Pointer (`navigation`+`button`) / Direction (`nav_direction`) / IMU+Biometric (`imu_acc`+`imu_gyro`+`snc`, always all three together)
|
|
147
147
|
- IMU+Biometric bundle: `imu_acc`, `imu_gyro`, `snc` always subscribed together — never partially. The bundle is mutually exclusive with `navigation` and `nav_direction`.
|
|
148
148
|
- `gesture` and `pressure` are mutually exclusive — never combine them
|
|
149
|
-
- `button` and `battery` combine freely (subject to the Pointer XOR — `button` belongs to Pointer mode and never combines with `nav_direction`)
|
|
150
149
|
- **Navigation sensitivity is gentle by default**: keyboard `step = 3`, sim button `±3`, cursor multiplier `0.002`. Raise only when the prompt explicitly asks for fast/snappy movement. See `references/promt.md` § "Navigation sensitivity defaults".
|
|
151
|
-
- Canonical protocol JSON: `references/agent_protocol.json`
|
|
150
|
+
- Canonical protocol JSON: `references/agent_protocol.json`
|
|
@@ -299,18 +299,6 @@
|
|
|
299
299
|
"The data array is size 3, each cell corresponds to one sensor"
|
|
300
300
|
]
|
|
301
301
|
},
|
|
302
|
-
"battery": {
|
|
303
|
-
"description": "Battery level",
|
|
304
|
-
"data_format": {
|
|
305
|
-
"type": "battery",
|
|
306
|
-
"data": {
|
|
307
|
-
"level": 85,
|
|
308
|
-
"charging": false,
|
|
309
|
-
"timestamp": 1234567890
|
|
310
|
-
},
|
|
311
|
-
"timestamp": 1234567890
|
|
312
|
-
}
|
|
313
|
-
},
|
|
314
302
|
"button": {
|
|
315
303
|
"description": "Air touch button events",
|
|
316
304
|
"states": [
|
|
@@ -45,7 +45,7 @@ UX feel, correct protocol usage, and a fast testing loop.
|
|
|
45
45
|
`{ "command": "subscribe", "signal": "<name>" }`
|
|
46
46
|
- **NEVER** use `signals` (plural), arrays, or batch subscribe commands
|
|
47
47
|
- Valid signals: `gesture`, `button`, `pressure`, `navigation`,
|
|
48
|
-
`nav_direction`, `imu_acc`, `imu_gyro`, `snc
|
|
48
|
+
`nav_direction`, `imu_acc`, `imu_gyro`, `snc`
|
|
49
49
|
- Full command surface: `subscribe`, `unsubscribe`,
|
|
50
50
|
`get_subscriptions`, `enable`, `disable`, `get_status`,
|
|
51
51
|
`get_docs`, `trigger_gesture`
|
|
@@ -79,7 +79,7 @@ app MUST restrict itself to **at most these four signals**:
|
|
|
79
79
|
|
|
80
80
|
Drop any of the four when the concept does not need it (e.g. a pure
|
|
81
81
|
tap-counter subscribes to `gesture` only). All other signals
|
|
82
|
-
(`button`, `imu_acc`, `imu_gyro`, `snc
|
|
82
|
+
(`button`, `imu_acc`, `imu_gyro`, `snc`) and other gesture
|
|
83
83
|
subtypes (`twist`, `double_twist`, …) are **off by default** — only
|
|
84
84
|
include them when the user names them, names a synonym from the Signal
|
|
85
85
|
Inference Reference below, or describes an interaction that genuinely
|
|
@@ -129,11 +129,6 @@ ws.send(JSON.stringify({ command: 'subscribe', signal: 'imu_acc' })); // miss
|
|
|
129
129
|
- `nav_direction` + `imu_acc` / `imu_gyro` / `snc`
|
|
130
130
|
- `button` + `nav_direction`
|
|
131
131
|
|
|
132
|
-
### Free-combining signals
|
|
133
|
-
|
|
134
|
-
`button` and `battery` combine freely with any group (subject to the
|
|
135
|
-
XOR rules above — `button` belongs to Pointer mode and never combines
|
|
136
|
-
with `nav_direction`).
|
|
137
132
|
|
|
138
133
|
When a conflict appears, explain the limitation and recommend one path.
|
|
139
134
|
|
|
@@ -342,11 +337,6 @@ class MudraWebSocket {
|
|
|
342
337
|
}
|
|
343
338
|
}, 50));
|
|
344
339
|
|
|
345
|
-
// Battery: once on connect
|
|
346
|
-
this._timers.push(setTimeout(() => {
|
|
347
|
-
if (!this._subscriptions.has('battery')) return;
|
|
348
|
-
this._emit({ type: 'battery', data: { level: 85, charging: false, timestamp: Date.now() }, timestamp: Date.now() });
|
|
349
|
-
}, 500));
|
|
350
340
|
}
|
|
351
341
|
|
|
352
342
|
close() {
|
|
@@ -391,7 +381,7 @@ The user picks how the app is driven via a visible **Mode** control:
|
|
|
391
381
|
`aria-disabled="true"`) and emits no synthetic signals. If the band
|
|
392
382
|
disconnects, the connection-status pill turns red and reads
|
|
393
383
|
"Disconnected" — **no separate overlay, toast, or banner is rendered
|
|
394
|
-
|
|
384
|
+
**. The WebSocket retries with backoff until reconnect or the
|
|
395
385
|
user switches back to Manual.
|
|
396
386
|
|
|
397
387
|
**The Mode toggle MUST remain fully clickable and keyboard-focusable at
|
|
@@ -481,7 +471,7 @@ Outbound:
|
|
|
481
471
|
|
|
482
472
|
Inbound shape: `{ "type": "...", "data": { ... }, "timestamp": <ms> }`.
|
|
483
473
|
Handle types: `connection_status`, `gesture`, `pressure`, `navigation`,
|
|
484
|
-
`nav_direction`, `button`, `imu_acc`, `imu_gyro`, `snc
|
|
474
|
+
`nav_direction`, `button`, `imu_acc`, `imu_gyro`, `snc`
|
|
485
475
|
Anything else: log + ignore.
|
|
486
476
|
|
|
487
477
|
### Disconnect detection — band state via `get_status` polling (mandatory)
|
|
@@ -613,7 +603,6 @@ Every generated app MUST render these three elements at all times:
|
|
|
613
603
|
case "imu_acc": handleImuAcc(msg.data); break;
|
|
614
604
|
case "imu_gyro": handleImuGyro(msg.data); break;
|
|
615
605
|
case "snc": handleSnc(msg.data); break;
|
|
616
|
-
case "battery": handleBattery(msg.data); break;
|
|
617
606
|
case "connection_status": handleConnectionStatus(msg.data); break;
|
|
618
607
|
case "status": handleStatus(msg.data); break; // get_status response
|
|
619
608
|
}
|
|
@@ -641,7 +630,7 @@ Every generated app MUST render these three elements at all times:
|
|
|
641
630
|
```
|
|
642
631
|
|
|
643
632
|
- No disconnect notice ever — disconnect is shown only by the
|
|
644
|
-
connection-status pill
|
|
633
|
+
connection-status pill.
|
|
645
634
|
|
|
646
635
|
### Mudra mode rules
|
|
647
636
|
|
|
@@ -898,8 +887,7 @@ Pick exactly ONE motion mode per app: **Pointer** (`navigation` + `button`)
|
|
|
898
887
|
**XOR** **Direction** (`nav_direction`) **XOR** **IMU+Biometric**
|
|
899
888
|
(`imu_acc` + `imu_gyro` + `snc`, always all three together). The Mode
|
|
900
889
|
toggle does NOT relax this rule. Additional XOR rules: `gesture` and
|
|
901
|
-
`pressure` are mutually exclusive — never combine them. `button`
|
|
902
|
-
`battery` combine freely (subject to the Pointer/Direction/IMU XOR).
|
|
890
|
+
`pressure` are mutually exclusive — never combine them. `button` combines freely (subject to the Pointer/Direction/IMU XOR).
|
|
903
891
|
|
|
904
892
|
---
|
|
905
893
|
|
|
@@ -1542,10 +1530,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
1542
1530
|
<div class="metric-label">Nav Direction</div>
|
|
1543
1531
|
<div class="metric-value" id="navDirectionValue">None</div>
|
|
1544
1532
|
</div>
|
|
1545
|
-
<div class="metric">
|
|
1546
|
-
<div class="metric-label">Battery</div>
|
|
1547
|
-
<div class="metric-value" id="batteryValue">--</div>
|
|
1548
|
-
</div>
|
|
1549
1533
|
</div>
|
|
1550
1534
|
</article>
|
|
1551
1535
|
</section>
|
|
@@ -1588,8 +1572,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
1588
1572
|
imuAcc: [0, 0, 9.81],
|
|
1589
1573
|
imuGyro: [0, 0, 0],
|
|
1590
1574
|
snc: [0, 0, 0],
|
|
1591
|
-
battery: null,
|
|
1592
|
-
charging: false,
|
|
1593
1575
|
navDirection: "None"
|
|
1594
1576
|
};
|
|
1595
1577
|
|
|
@@ -1608,7 +1590,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
1608
1590
|
sncValue: document.getElementById("sncValue"),
|
|
1609
1591
|
sncBar: document.getElementById("sncBar"),
|
|
1610
1592
|
navDirectionValue: document.getElementById("navDirectionValue"),
|
|
1611
|
-
batteryValue: document.getElementById("batteryValue"),
|
|
1612
1593
|
log: document.getElementById("log")
|
|
1613
1594
|
};
|
|
1614
1595
|
|
|
@@ -1696,12 +1677,11 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
1696
1677
|
if (msg.type === "imu_acc") handleImuAcc(msg.data);
|
|
1697
1678
|
if (msg.type === "imu_gyro") handleImuGyro(msg.data);
|
|
1698
1679
|
if (msg.type === "snc") handleSnc(msg.data);
|
|
1699
|
-
if (msg.type === "battery") handleBattery(msg.data);
|
|
1700
1680
|
};
|
|
1701
1681
|
}
|
|
1702
1682
|
|
|
1703
1683
|
function getActiveSignals() {
|
|
1704
|
-
const base = ["gesture", "pressure", "snc"
|
|
1684
|
+
const base = ["gesture", "pressure", "snc"];
|
|
1705
1685
|
if (controlMode === "pointer") return [...base, "navigation", "button"];
|
|
1706
1686
|
if (controlMode === "direction") return [...base, "nav_direction"];
|
|
1707
1687
|
return [...base, "imu_acc", "imu_gyro"];
|
|
@@ -1802,12 +1782,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
1802
1782
|
ui.sncBar.style.width = `${Math.round(energy * 100)}%`;
|
|
1803
1783
|
}
|
|
1804
1784
|
|
|
1805
|
-
function handleBattery(data = {}) {
|
|
1806
|
-
if (typeof data.level !== "number") return;
|
|
1807
|
-
state.battery = data.level;
|
|
1808
|
-
state.charging = Boolean(data.charging);
|
|
1809
|
-
ui.batteryValue.textContent = `${state.battery}% ${state.charging ? "(charging)" : ""}`;
|
|
1810
|
-
}
|
|
1811
1785
|
|
|
1812
1786
|
function projectImuToStage() {
|
|
1813
1787
|
const x = clamp(state.imuAcc[0] * 12 + state.imuGyro[2] * 0.15, -110, 110);
|
|
@@ -2024,27 +1998,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
2024
1998
|
.conn-dot.on { background: var(--success); box-shadow: 0 0 0 3px rgba(34,197,94,0.18); }
|
|
2025
1999
|
.conn-dot.err { background: var(--error); box-shadow: 0 0 0 3px rgba(239,68,68,0.18); }
|
|
2026
2000
|
|
|
2027
|
-
.battery-pill {
|
|
2028
|
-
display: inline-flex; align-items: center; gap: 6px;
|
|
2029
|
-
padding: 6px 12px; border-radius: 999px;
|
|
2030
|
-
border: 1px solid var(--border-dim);
|
|
2031
|
-
background: rgba(0,0,0,0.02);
|
|
2032
|
-
font-size: 0.82rem; font-weight: 500;
|
|
2033
|
-
}
|
|
2034
|
-
.battery-icon {
|
|
2035
|
-
width: 22px; height: 12px; border: 1.5px solid var(--text-secondary);
|
|
2036
|
-
border-radius: 2px; position: relative;
|
|
2037
|
-
}
|
|
2038
|
-
.battery-icon::after {
|
|
2039
|
-
content: ''; position: absolute; right: -4px; top: 2px;
|
|
2040
|
-
width: 2px; height: 6px; background: var(--text-secondary); border-radius: 0 1px 1px 0;
|
|
2041
|
-
}
|
|
2042
|
-
.battery-fill {
|
|
2043
|
-
height: 100%; border-radius: 1px;
|
|
2044
|
-
background: var(--success); transition: width 300ms;
|
|
2045
|
-
}
|
|
2046
|
-
.battery-fill.low { background: var(--error); }
|
|
2047
|
-
.battery-fill.mid { background: var(--warning); }
|
|
2048
2001
|
|
|
2049
2002
|
/* ── Dashboard Grid ── */
|
|
2050
2003
|
.dashboard {
|
|
@@ -2401,10 +2354,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
2401
2354
|
</div>
|
|
2402
2355
|
</div>
|
|
2403
2356
|
<div class="topbar-right">
|
|
2404
|
-
<div class="battery-pill" id="batteryPill" style="display:none">
|
|
2405
|
-
<div class="battery-icon"><div class="battery-fill" id="batteryFill" style="width:0%"></div></div>
|
|
2406
|
-
<span id="batteryText">--%</span>
|
|
2407
|
-
</div>
|
|
2408
2357
|
<div class="pkt-counter"><span id="pktCount">0</span> packets</div>
|
|
2409
2358
|
</div>
|
|
2410
2359
|
</header>
|
|
@@ -2597,11 +2546,6 @@ Below are all reference apps. When generating a new app, select the best-matchin
|
|
|
2597
2546
|
<span class="sig-pkts" id="pkts-snc">0</span>
|
|
2598
2547
|
<div class="toggle-switch on" data-sub="snc"></div>
|
|
2599
2548
|
</div>
|
|
2600
|
-
<div class="toggle-row" data-signal="battery">
|
|
2601
|
-
<span class="sig-name">battery</span>
|
|
2602
|
-
<span class="sig-pkts" id="pkts-battery">0</span>
|
|
2603
|
-
<div class="toggle-switch on" data-sub="battery"></div>
|
|
2604
|
-
</div>
|
|
2605
2549
|
|
|
2606
2550
|
<div class="sub-group-label">Pointer Mode</div>
|
|
2607
2551
|
<div class="toggle-row" data-signal="navigation">
|
|
@@ -2715,9 +2659,9 @@ let logFilter = "all";
|
|
|
2715
2659
|
const pressureSamples = [];
|
|
2716
2660
|
|
|
2717
2661
|
// Subscription tracking
|
|
2718
|
-
const activeSubs = new Set(["gesture", "pressure", "snc", "
|
|
2662
|
+
const activeSubs = new Set(["gesture", "pressure", "snc", "navigation", "button"]);
|
|
2719
2663
|
const packetCounts = {};
|
|
2720
|
-
const ALL_SIGNALS = ["gesture", "pressure", "snc", "
|
|
2664
|
+
const ALL_SIGNALS = ["gesture", "pressure", "snc", "navigation", "button", "nav_direction", "imu_acc", "imu_gyro"];
|
|
2721
2665
|
const MOTION_CONFLICTS = {
|
|
2722
2666
|
navigation: ["nav_direction", "imu_acc", "imu_gyro"],
|
|
2723
2667
|
button: ["nav_direction", "imu_acc", "imu_gyro"],
|
|
@@ -2735,7 +2679,6 @@ const state = {
|
|
|
2735
2679
|
imuAcc: [0, 0, 9.81],
|
|
2736
2680
|
imuGyro: [0, 0, 0],
|
|
2737
2681
|
snc: [0, 0, 0],
|
|
2738
|
-
battery: { level: -1, charging: false }
|
|
2739
2682
|
};
|
|
2740
2683
|
|
|
2741
2684
|
const gestureHistory = [];
|
|
@@ -2757,7 +2700,6 @@ const logEntries = [];
|
|
|
2757
2700
|
const $ = id => document.getElementById(id);
|
|
2758
2701
|
const ui = {
|
|
2759
2702
|
connDot: $("connDot"), connText: $("connText"),
|
|
2760
|
-
batteryPill: $("batteryPill"), batteryFill: $("batteryFill"), batteryText: $("batteryText"),
|
|
2761
2703
|
pktCount: $("pktCount"),
|
|
2762
2704
|
gestureType: $("gestureType"), gestureConf: $("gestureConf"), gestureHistory: $("gestureHistory"),
|
|
2763
2705
|
gaugeFill: $("gaugeFill"), gaugeText: $("gaugeText"),
|
|
@@ -2918,7 +2860,7 @@ function switchMode(mode) {
|
|
|
2918
2860
|
});
|
|
2919
2861
|
|
|
2920
2862
|
// Also re-subscribe universal ones
|
|
2921
|
-
["gesture", "pressure", "snc"
|
|
2863
|
+
["gesture", "pressure", "snc"].forEach(s => {
|
|
2922
2864
|
if (activeSubs.has(s)) send({ command: "subscribe", signal: s });
|
|
2923
2865
|
});
|
|
2924
2866
|
|
|
@@ -2957,7 +2899,6 @@ function routeMessage(msg) {
|
|
|
2957
2899
|
else if (t === "imu_acc") onImuAcc(d);
|
|
2958
2900
|
else if (t === "imu_gyro") onImuGyro(d);
|
|
2959
2901
|
else if (t === "snc") onSnc(d);
|
|
2960
|
-
else if (t === "battery") onBattery(d);
|
|
2961
2902
|
|
|
2962
2903
|
addLog(categorize(t), t, d);
|
|
2963
2904
|
}
|
|
@@ -3096,16 +3037,6 @@ function onSnc(d) {
|
|
|
3096
3037
|
ui.sncRadial.textContent = state.snc[2].toFixed(3);
|
|
3097
3038
|
}
|
|
3098
3039
|
|
|
3099
|
-
function onBattery(d) {
|
|
3100
|
-
if (typeof d.level !== "number") return;
|
|
3101
|
-
state.battery.level = d.level;
|
|
3102
|
-
state.battery.charging = !!d.charging;
|
|
3103
|
-
|
|
3104
|
-
ui.batteryPill.style.display = "inline-flex";
|
|
3105
|
-
ui.batteryFill.style.width = d.level + "%";
|
|
3106
|
-
ui.batteryFill.className = "battery-fill" + (d.level <= 15 ? " low" : d.level <= 40 ? " mid" : "");
|
|
3107
|
-
ui.batteryText.textContent = d.level + "%" + (d.charging ? " ⚡" : "");
|
|
3108
|
-
}
|
|
3109
3040
|
|
|
3110
3041
|
// ── Motion helpers ──
|
|
3111
3042
|
function projectImu() {
|
|
@@ -3613,10 +3544,6 @@ window.addEventListener("resize", () => {
|
|
|
3613
3544
|
<span class="hud-value" id="hudPressure">0%</span>
|
|
3614
3545
|
</div>
|
|
3615
3546
|
<div class="hud-bar"><span id="hudPressureBar"></span></div>
|
|
3616
|
-
<div class="hud-row">
|
|
3617
|
-
<span class="hud-label">Battery</span>
|
|
3618
|
-
<span class="hud-value" id="hudBattery">--</span>
|
|
3619
|
-
</div>
|
|
3620
3547
|
</div>
|
|
3621
3548
|
</div>
|
|
3622
3549
|
|
|
@@ -3750,8 +3677,6 @@ window.addEventListener("resize", () => {
|
|
|
3750
3677
|
navDeltaDisplay: "0",
|
|
3751
3678
|
pressure: 0,
|
|
3752
3679
|
smoothPressure: 0,
|
|
3753
|
-
battery: null,
|
|
3754
|
-
charging: false,
|
|
3755
3680
|
// Game-facing signals
|
|
3756
3681
|
deltaX: 0, // accumulated navigation delta, consumed each frame
|
|
3757
3682
|
shootTriggered: false,
|
|
@@ -3766,7 +3691,6 @@ window.addEventListener("resize", () => {
|
|
|
3766
3691
|
direction: document.getElementById('hudDirection'),
|
|
3767
3692
|
pressure: document.getElementById('hudPressure'),
|
|
3768
3693
|
pressureBar: document.getElementById('hudPressureBar'),
|
|
3769
|
-
battery: document.getElementById('hudBattery')
|
|
3770
3694
|
};
|
|
3771
3695
|
|
|
3772
3696
|
function connectMudra() {
|
|
@@ -3793,7 +3717,6 @@ window.addEventListener("resize", () => {
|
|
|
3793
3717
|
if (msg.type === "gesture") handleMudraGesture(msg.data);
|
|
3794
3718
|
if (msg.type === "navigation") handleMudraNavigation(msg.data);
|
|
3795
3719
|
if (msg.type === "pressure") handleMudraPressure(msg.data);
|
|
3796
|
-
if (msg.type === "battery") handleMudraBattery(msg.data);
|
|
3797
3720
|
};
|
|
3798
3721
|
|
|
3799
3722
|
mudra.ws.onerror = () => {
|
|
@@ -3816,7 +3739,7 @@ window.addEventListener("resize", () => {
|
|
|
3816
3739
|
function subscribeSignals() {
|
|
3817
3740
|
// Pointer mode: navigation for continuous movement + gesture/pressure freely combine
|
|
3818
3741
|
// (no nav_direction, no imu — signal-safe)
|
|
3819
|
-
const signals = ["gesture", "navigation", "pressure"
|
|
3742
|
+
const signals = ["gesture", "navigation", "pressure"];
|
|
3820
3743
|
signals.forEach(signal => sendMudra({ command: "subscribe", signal }));
|
|
3821
3744
|
}
|
|
3822
3745
|
|
|
@@ -3871,12 +3794,6 @@ window.addEventListener("resize", () => {
|
|
|
3871
3794
|
hud.pressureBar.style.width = `${Math.round(mudra.smoothPressure * 100)}%`;
|
|
3872
3795
|
}
|
|
3873
3796
|
|
|
3874
|
-
function handleMudraBattery(data = {}) {
|
|
3875
|
-
if (typeof data.level !== "number") return;
|
|
3876
|
-
mudra.battery = data.level;
|
|
3877
|
-
mudra.charging = Boolean(data.charging);
|
|
3878
|
-
hud.battery.textContent = `${mudra.battery}%${mudra.charging ? " ⚡" : ""}`;
|
|
3879
|
-
}
|
|
3880
3797
|
|
|
3881
3798
|
function pulseHud(el) {
|
|
3882
3799
|
el.style.transition = 'none';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mudra-xr
|
|
3
|
-
version:
|
|
3
|
+
version: 3.0.0
|
|
4
4
|
description: Generate a single-file Mudra-controlled 3D/XR app using XR Blocks. Use when the user describes a 3D, XR, VR, or AR experience controlled by the Mudra Band.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -9,13 +9,13 @@ description: Generate a single-file Mudra-controlled 3D/XR app using XR Blocks.
|
|
|
9
9
|
Generate a complete, working single-file HTML 3D/XR app controlled by Mudra Band signals.
|
|
10
10
|
The output runs in any modern Chromium browser — no build step, no server, no headset required.
|
|
11
11
|
|
|
12
|
-
**Mandatory feature
|
|
12
|
+
**Mandatory feature:** Every generated app MUST include a
|
|
13
13
|
Manual/Mudra **Mode toggle** as defined in `references/promt.md`
|
|
14
14
|
§ Section 15 "Mode Toggle (Manual / Mudra) — Required". Manual is the
|
|
15
15
|
default; Mudra opens a single WebSocket lazily and disables the
|
|
16
16
|
simulator panel so signals come only from the band.
|
|
17
17
|
|
|
18
|
-
**Mandatory feature
|
|
18
|
+
**Mandatory feature:** Connection state MUST reflect the
|
|
19
19
|
**band**, not the WebSocket. The Companion service accepts socket
|
|
20
20
|
connections even when no band is paired, so flipping to "Connected" on
|
|
21
21
|
`ws.onopen` is a lie. Every generated app MUST send
|
|
@@ -29,7 +29,7 @@ overlay, toast, or any other separate disconnect alert. Connection
|
|
|
29
29
|
state is communicated **only** through the existing connection-status
|
|
30
30
|
pill (`Manual` / `Connecting…` / `Connected` / `Disconnected`).
|
|
31
31
|
|
|
32
|
-
**Branding
|
|
32
|
+
**Branding:** The footer/badge text MUST be exactly
|
|
33
33
|
**"Created by Mudra"** — never "Created with Mudra Studio",
|
|
34
34
|
"Powered by Mudra", or any other variant. See § Section 16.
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ sub-actions the app actually handles. If the app maps `nav_direction` to
|
|
|
38
38
|
Up/Down only, omit Left/Right/Roll L/Roll R. If `gesture` only handles
|
|
39
39
|
`tap`, omit Twist/2Twist/2Tap. See § Section 5.
|
|
40
40
|
|
|
41
|
-
The canonical protocol
|
|
41
|
+
The canonical protocol rules are in `references/prompt.md` Section 1 (signals, commands, error frames) and Section 15 (state machine, hand chip). See also `specs/001-mudra-xr-connection-update/contracts/websocket-protocol.md` for the wire-protocol contract.
|
|
42
42
|
|
|
43
43
|
## Invocation
|
|
44
44
|
|
|
@@ -237,7 +237,7 @@ Print the absolute path to the written file and a one-line summary:
|
|
|
237
237
|
- Motion modes are mutually exclusive: Pointer (`navigation`+`button`) / Direction (`nav_direction`) / IMU+Biometric (`imu_acc`+`imu_gyro`+`snc`)
|
|
238
238
|
- IMU+Biometric bundle: `imu_acc`, `imu_gyro`, `snc` always subscribed together — never partially
|
|
239
239
|
- `gesture` and `pressure` are mutually exclusive — never combine them
|
|
240
|
-
- Free-combining signals (one or the other, not both): `gesture` OR `pressure`, plus `button
|
|
240
|
+
- Free-combining signals (one or the other, not both): `gesture` OR `pressure`, plus `button`
|
|
241
241
|
- **Navigation sensitivity is gentle by default**: sim button + keyboard `I`/`J`/`K`/`L` emit `±3` per event; cursor multiplier on inbound `delta_x`/`delta_y` is `0.002`. Raise only when the prompt explicitly asks for fast/snappy movement. See Section 6 + Section 11 of `references/promt.md`.
|
|
242
242
|
- **Reserved for XR Blocks desktop simulator** — Mudra never claims these: `W`/`A`/`S`/`D` and arrow keys (camera walk), `Q`/`E` (roll/vertical), `R` (reset), right-click drag (orbit), mouse wheel (zoom). Mudra navigation uses `I`/`J`/`K`/`L`; Mudra IMU uses `U`/`O`/`M`/`N`. See Section 6 of `references/promt.md`.
|
|
243
243
|
- Keyboard handlers: `{ capture: true }` + `stopPropagation()` on Mudra-claimed keys
|
|
@@ -348,18 +348,6 @@
|
|
|
348
348
|
"For HUD: use buf[buf.length-1]; for time-series plots: draw all buffered samples"
|
|
349
349
|
]
|
|
350
350
|
},
|
|
351
|
-
"battery": {
|
|
352
|
-
"description": "Battery level",
|
|
353
|
-
"data_format": {
|
|
354
|
-
"type": "battery",
|
|
355
|
-
"data": {
|
|
356
|
-
"level": 85,
|
|
357
|
-
"charging": false,
|
|
358
|
-
"timestamp": 1234567890
|
|
359
|
-
},
|
|
360
|
-
"timestamp": 1234567890
|
|
361
|
-
}
|
|
362
|
-
},
|
|
363
351
|
"button": {
|
|
364
352
|
"description": "Air touch button events (Pointer mode — pairs with navigation)",
|
|
365
353
|
"states": [
|
|
@@ -424,7 +412,6 @@
|
|
|
424
412
|
"can_combine": [
|
|
425
413
|
"gesture OR pressure",
|
|
426
414
|
"button (with pointer/direction/imu_biometric/none)",
|
|
427
|
-
"battery",
|
|
428
415
|
"ONE OF: pointer_mode OR direction_mode OR imu_biometric_mode"
|
|
429
416
|
],
|
|
430
417
|
"cannot_combine": [
|
|
@@ -492,7 +479,7 @@
|
|
|
492
479
|
"reason": "Button is part of pointer mode - cannot mix with direction mode"
|
|
493
480
|
}
|
|
494
481
|
],
|
|
495
|
-
"guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+snc as an inseparable bundle for orientation/tilt/rotation/biometric. Pick ONE per app. gesture XOR pressure; button
|
|
482
|
+
"guidance": "Three mutually exclusive motion groups: (1) Pointer mode: navigation+button for continuous cursor/drag/pan, (2) Direction mode: nav_direction for discrete swipe-like gestures, (3) IMU+Biometric mode: imu_acc+imu_gyro+snc as an inseparable bundle for orientation/tilt/rotation/biometric. Pick ONE per app. gesture XOR pressure; button combines freely (subject to mode rules)."
|
|
496
483
|
},
|
|
497
484
|
"websocket_api": {
|
|
498
485
|
"url": "ws://127.0.0.1:8766",
|