diff --git a/data_only_viz/web/public/control.html b/data_only_viz/web/public/control.html
index e0d84ce..6e6a868 100644
--- a/data_only_viz/web/public/control.html
+++ b/data_only_viz/web/public/control.html
@@ -168,6 +168,19 @@
+
Modes visuels (oF / Metal)
+
+
+
+
+
+
+
+
+
+
+
+
Retour SC
BPM —
Beat —
diff --git a/data_only_viz/web/public/js/control.js b/data_only_viz/web/public/js/control.js
index 4bc87f4..2acf073 100644
--- a/data_only_viz/web/public/js/control.js
+++ b/data_only_viz/web/public/js/control.js
@@ -48,15 +48,25 @@ document.querySelectorAll(".slider-row").forEach((row) => {
});
// ---------- Scene buttons ----------
-document.querySelectorAll(".scene-btn").forEach((btn) => {
+document.querySelectorAll("#scenes .scene-btn").forEach((btn) => {
btn.addEventListener("click", () => {
- document.querySelectorAll(".scene-btn")
+ document.querySelectorAll("#scenes .scene-btn")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
send("/scene/play", [btn.dataset.scene]);
});
});
+// ---------- Visual mode buttons ----------
+document.querySelectorAll("#vizmodes .scene-btn").forEach((btn) => {
+ btn.addEventListener("click", () => {
+ document.querySelectorAll("#vizmodes .scene-btn")
+ .forEach((b) => b.classList.remove("active"));
+ btn.classList.add("active");
+ send("/control/vizMode", [parseInt(btn.dataset.viz, 10)]);
+ });
+});
+
// ---------- XY pad ----------
const pad = document.getElementById("xy");
const cursor = document.getElementById("xy-cursor");
diff --git a/data_only_viz/web/public/js/dashboard.js b/data_only_viz/web/public/js/dashboard.js
index 71f6c9e..69a2c2d 100644
--- a/data_only_viz/web/public/js/dashboard.js
+++ b/data_only_viz/web/public/js/dashboard.js
@@ -235,6 +235,107 @@ const handlers = {
drawSpark(c);
}
},
+ // ---- Nouveaux feeds (gdelt, wikimedia, tides, atc, pose, mempool,
+ // github, rte_eco2mix) -------------------------------------
+ gdelt: (m) => {
+ if (m.sub === "batch") {
+ const [n, countries, tone] = m.args;
+ const c = ensureCard("gdelt", { title: "GDELT · evenements 15min",
+ order: 19, wide: true,
+ alert: tone < -5, warn: tone < -2, green: tone > 2 });
+ c.valueEl.textContent = String(n | 0);
+ c.subEl.textContent = `${countries | 0} pays · tone ${fmt(tone, 2)}`;
+ pushHistory(c, n);
+ drawSpark(c);
+ }
+ },
+ wikimedia: (m) => {
+ if (m.sub === "rate") {
+ const c = ensureCard("wiki", { title: "Wikipedia firehose", order: 20 });
+ c.valueEl.textContent = `${fmt(m.args[0], 1)} /s`;
+ pushHistory(c, m.args[0]);
+ drawSpark(c, "#6fa6ff");
+ } else if (m.sub === "edit") {
+ const c = ensureCard("wiki", { title: "Wikipedia firehose", order: 20 });
+ c.subEl.textContent = `${m.args[0]}: ${m.args[1]}`;
+ }
+ },
+ tides: (m) => {
+ if (m.sub === "level") {
+ const [obs, pred, residual] = m.args;
+ const c = ensureCard("tides", { title: "Marees NOAA", order: 21, wide: true });
+ c.valueEl.textContent = `${fmt(obs, 2)} m`;
+ c.subEl.textContent = `predit ${fmt(pred, 2)} m · residual ${fmt(residual, 2)} m`;
+ pushHistory(c, obs);
+ drawSpark(c, "#6fa6ff");
+ } else if (m.sub === "moon") {
+ const [phase, illum] = m.args;
+ const c = ensureCard("moon", { title: "Phase lunaire", order: 22 });
+ const phaseName = phase < 0.05 || phase > 0.95 ? "nouvelle"
+ : phase < 0.25 ? "premier croissant"
+ : phase < 0.30 ? "premier quartier"
+ : phase < 0.45 ? "gibbeuse croissante"
+ : phase < 0.55 ? "pleine"
+ : phase < 0.70 ? "gibbeuse decroissante"
+ : phase < 0.80 ? "dernier quartier"
+ : "dernier croissant";
+ c.valueEl.textContent = `${fmt(illum * 100, 0)}%`;
+ c.subEl.textContent = phaseName;
+ }
+ },
+ atc: (m) => {
+ if (m.sub === "total") {
+ const [total, hubs] = m.args;
+ const c = ensureCard("atc", { title: "ATC · auditeurs", order: 23 });
+ c.valueEl.textContent = String(total | 0);
+ c.subEl.textContent = `${hubs | 0} hubs actifs`;
+ pushHistory(c, total);
+ drawSpark(c, "#6fe9b3");
+ }
+ },
+ pose: (m) => {
+ if (m.sub === "count") {
+ const c = ensureCard("pose", { title: "Pose YOLO · personnes", order: 3 });
+ c.valueEl.textContent = String(m.args[0] | 0);
+ pushHistory(c, m.args[0]);
+ drawSpark(c, "#ff8838");
+ }
+ },
+ mempool: (m) => {
+ if (m.sub === "fee") {
+ const c = ensureCard("btc", { title: "Bitcoin · fee sat/vB", order: 24 });
+ c.valueEl.textContent = String(m.args[0] | 0);
+ pushHistory(c, m.args[0]);
+ drawSpark(c, "#ff8838");
+ } else if (m.sub === "tx") {
+ const c = ensureCard("btc", { title: "Bitcoin · fee sat/vB", order: 24 });
+ c.subEl.textContent = `${m.args[0] | 0} tx en attente`;
+ }
+ },
+ github: (m) => {
+ if (m.sub === "rate") {
+ const c = ensureCard("gh", { title: "GitHub events", order: 25 });
+ c.valueEl.textContent = `${fmt(m.args[0], 1)} /s`;
+ pushHistory(c, m.args[0]);
+ drawSpark(c, "#6fa6ff");
+ }
+ },
+ rte_eco2mix: (m) => {
+ if (m.sub === "mix") {
+ const [nuclear, solar, wind, gas] = m.args;
+ const c = ensureCard("rte", { title: "RTE eCO2mix · GW", order: 26, wide: true });
+ c.valueEl.textContent = `${fmt((nuclear + solar + wind + gas) / 1000, 1)} GW`;
+ c.subEl.textContent = `nuc ${fmt(nuclear / 1000, 1)} · sol ${fmt(solar / 1000, 1)} · eol ${fmt(wind / 1000, 1)} · gaz ${fmt(gas / 1000, 1)}`;
+ pushHistory(c, (nuclear + solar + wind + gas) / 1000);
+ drawSpark(c, "#6fe9b3");
+ } else if (m.sub === "co2") {
+ const c = ensureCard("co2", { title: "Intensite CO2 · g/kWh", order: 27,
+ alert: m.args[0] > 200, warn: m.args[0] > 100, green: m.args[0] < 50 });
+ c.valueEl.textContent = String(m.args[0] | 0);
+ pushHistory(c, m.args[0]);
+ drawSpark(c);
+ }
+ },
};
function handleMessage(msg) {
diff --git a/data_only_viz/web/public/js/map.js b/data_only_viz/web/public/js/map.js
index f68a549..b40a835 100644
--- a/data_only_viz/web/public/js/map.js
+++ b/data_only_viz/web/public/js/map.js
@@ -87,6 +87,13 @@ const handlers = {
ephemeralMarker(lat, lon, "volcano",
`volcan ${region} (VEI ${vei})`);
},
+ gdelt: (m) => {
+ if (m.sub !== "event") return;
+ const [lat, lon, tone, country] = m.args;
+ if (!lat && !lon) return;
+ const tip = `GDELT ${country || "?"} (tone ${(+tone).toFixed(1)})`;
+ ephemeralMarker(lat, lon, "gdelt", tip);
+ },
};
function handleMessage(msg) {
diff --git a/data_only_viz/web/public/map.html b/data_only_viz/web/public/map.html
index aa2dbf9..5184c60 100644
--- a/data_only_viz/web/public/map.html
+++ b/data_only_viz/web/public/map.html
@@ -27,6 +27,7 @@
.plane { background: #6fe9b3; border-radius: 50%; }
.iss { background: #6fa6ff; border-radius: 50%; border: 2px solid #fff; }
.volcano { background: #ff8838; border-radius: 50%; }
+ .gdelt { background: #c878ff; border-radius: 50%; opacity: 0.8; }
diff --git a/data_only_viz/web/server.js b/data_only_viz/web/server.js
index 13e8523..2def7d2 100644
--- a/data_only_viz/web/server.js
+++ b/data_only_viz/web/server.js
@@ -25,6 +25,7 @@ const OSC_DATA_IN = parseInt(process.env.OSC_DATA_IN ?? "57124", 10);
const OSC_SYNC_IN = parseInt(process.env.OSC_SYNC_IN ?? "57125", 10);
const SC_HOST = process.env.SC_HOST ?? "127.0.0.1";
const SC_PORT_OUT = parseInt(process.env.SC_PORT_OUT ?? "57121", 10);
+const OF_PORT_OUT = parseInt(process.env.OF_PORT_OUT ?? "57123", 10);
const app = express();
app.use(express.static(join(__dirname, "public")));
@@ -69,7 +70,12 @@ wss.on("connection", (ws) => {
if (typeof v === "number") return { type: "f", value: v };
return { type: "s", value: String(v) };
});
- udpOut.send({ address: msg.path, args }, SC_HOST, SC_PORT_OUT);
+ // Routing par path :
+ // /control/viz* -> oscope-of (:57123) — Metal viz + skeleton renderer
+ // tout le reste -> SC (:57121)
+ const port = msg.path.startsWith("/control/viz")
+ ? OF_PORT_OUT : SC_PORT_OUT;
+ udpOut.send({ address: msg.path, args }, SC_HOST, port);
});
});
diff --git a/launcher/AV-Live-Body/Sources/AVLiveBody/SettingsPanel.swift b/launcher/AV-Live-Body/Sources/AVLiveBody/SettingsPanel.swift
index 87a7e95..65176f5 100644
--- a/launcher/AV-Live-Body/Sources/AVLiveBody/SettingsPanel.swift
+++ b/launcher/AV-Live-Body/Sources/AVLiveBody/SettingsPanel.swift
@@ -64,7 +64,7 @@ struct SettingsPanel: View {
label: "Fil de fer",
isOn: $settings.showWireframe)
layerRow(icon: "figure.stand",
- label: "Squelette (articulations)",
+ label: "Squelette (à venir)",
isOn: $settings.showSkeleton)
}
}
diff --git a/launcher/Sources/AVLiveLauncher/ProcessManager.swift b/launcher/Sources/AVLiveLauncher/ProcessManager.swift
index 106e75b..afe5c94 100644
--- a/launcher/Sources/AVLiveLauncher/ProcessManager.swift
+++ b/launcher/Sources/AVLiveLauncher/ProcessManager.swift
@@ -17,7 +17,7 @@ enum LaunchMode: String, CaseIterable, Identifiable {
var displayName: String {
switch self {
case .full: return "Full AV-Live"
- case .dataOnly: return "Data-only (SC + oF + feeds, no web)"
+ case .dataOnly: return "Data-only (SC + Metal viz + feeds + web)"
case .bodyMesh: return "Body Mesh (Multi-HMR + RealityKit)"
}
}