Spaces:
Running
Running
Upload 26 files
Browse files- mouh/learning/manifest.json +5 -5
- mouh/world.py +29 -2
- tests/test_frontend.py +7 -0
- tests/test_world.py +13 -0
- unique_ext/__init__.py +1 -1
- web/symbioza/static/js/app.js +7 -0
- web/symbioza/static/js/flat.js +88 -42
mouh/learning/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"version": "v4.5.
|
| 3 |
"files": {
|
| 4 |
"app.py": "36ff9411bd29395d",
|
| 5 |
"requirements.txt": "613d00e516f8f4b8",
|
|
@@ -25,7 +25,7 @@
|
|
| 25 |
"engine/__init__.py": "9ed43b8c6346ad0e",
|
| 26 |
"engine/trainer.py": "778e08d76a7731e3",
|
| 27 |
"mouh/webui.py": "de56bcb7b5a926c5",
|
| 28 |
-
"mouh/world.py": "
|
| 29 |
"mouh/lab/__init__.py": "374f8a6b3c61de0a",
|
| 30 |
"mouh/lab/common.py": "3e81863585cd64a8",
|
| 31 |
"mouh/lab/algorithms.py": "603da6df0953b6d0",
|
|
@@ -37,7 +37,7 @@
|
|
| 37 |
"mouh/lab/api.py": "e3b2d3894f81a5de",
|
| 38 |
"mouh/lab/examples.py": "e1218f95b6f3c220",
|
| 39 |
"unique_ext/README.md": "0f0be41551e6bacb",
|
| 40 |
-
"unique_ext/__init__.py": "
|
| 41 |
"unique_ext/backends/__init__.py": "e3b0c44298fc1c14",
|
| 42 |
"unique_ext/backends/hf.py": "506d29e81c127e1e",
|
| 43 |
"unique_ext/contracts.py": "4ce5632e921dfcdc",
|
|
@@ -63,8 +63,8 @@
|
|
| 63 |
"web/symbioza/index.html": "af4f860577542cea",
|
| 64 |
"web/symbioza/static/css/hub.css": "18b95106f5f2022a",
|
| 65 |
"web/symbioza/static/css/hud.css": "87ba7a20761a9d6a",
|
| 66 |
-
"web/symbioza/static/js/app.js": "
|
| 67 |
-
"web/symbioza/static/js/flat.js": "
|
| 68 |
"web/symbioza/static/js/hub.js": "dc1ca2ac931a0c2e",
|
| 69 |
"web/symbioza/static/js/shaders.js": "0e070676691c054c",
|
| 70 |
"web/symbioza/static/vendor/three/LICENSE.txt": "8b378ebe60e2fe50",
|
|
|
|
| 1 |
{
|
| 2 |
+
"version": "v4.5.3",
|
| 3 |
"files": {
|
| 4 |
"app.py": "36ff9411bd29395d",
|
| 5 |
"requirements.txt": "613d00e516f8f4b8",
|
|
|
|
| 25 |
"engine/__init__.py": "9ed43b8c6346ad0e",
|
| 26 |
"engine/trainer.py": "778e08d76a7731e3",
|
| 27 |
"mouh/webui.py": "de56bcb7b5a926c5",
|
| 28 |
+
"mouh/world.py": "11dfe4d90d48b2d7",
|
| 29 |
"mouh/lab/__init__.py": "374f8a6b3c61de0a",
|
| 30 |
"mouh/lab/common.py": "3e81863585cd64a8",
|
| 31 |
"mouh/lab/algorithms.py": "603da6df0953b6d0",
|
|
|
|
| 37 |
"mouh/lab/api.py": "e3b2d3894f81a5de",
|
| 38 |
"mouh/lab/examples.py": "e1218f95b6f3c220",
|
| 39 |
"unique_ext/README.md": "0f0be41551e6bacb",
|
| 40 |
+
"unique_ext/__init__.py": "7aae5378f906d108",
|
| 41 |
"unique_ext/backends/__init__.py": "e3b0c44298fc1c14",
|
| 42 |
"unique_ext/backends/hf.py": "506d29e81c127e1e",
|
| 43 |
"unique_ext/contracts.py": "4ce5632e921dfcdc",
|
|
|
|
| 63 |
"web/symbioza/index.html": "af4f860577542cea",
|
| 64 |
"web/symbioza/static/css/hub.css": "18b95106f5f2022a",
|
| 65 |
"web/symbioza/static/css/hud.css": "87ba7a20761a9d6a",
|
| 66 |
+
"web/symbioza/static/js/app.js": "c977fdb2efe9c392",
|
| 67 |
+
"web/symbioza/static/js/flat.js": "62503f1c83fc2151",
|
| 68 |
"web/symbioza/static/js/hub.js": "dc1ca2ac931a0c2e",
|
| 69 |
"web/symbioza/static/js/shaders.js": "0e070676691c054c",
|
| 70 |
"web/symbioza/static/vendor/three/LICENSE.txt": "8b378ebe60e2fe50",
|
mouh/world.py
CHANGED
|
@@ -305,11 +305,38 @@ def _compact(path: Path) -> None:
|
|
| 305 |
|
| 306 |
|
| 307 |
def objects(world_dir: Path, limit: int = 60) -> list[dict]:
|
| 308 |
-
"""Index du monde,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
with _LOCK:
|
| 310 |
latest = list(_latest_by_slot(Path(world_dir) / OBJECTS).values())
|
| 311 |
latest.sort(key=lambda r: r.get("at", ""), reverse=True)
|
| 312 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
|
| 314 |
|
| 315 |
def object_render(world_dir: Path, object_id: str) -> dict | None:
|
|
|
|
| 305 |
|
| 306 |
|
| 307 |
def objects(world_dir: Path, limit: int = 60) -> list[dict]:
|
| 308 |
+
"""Index du monde, un objet par emplacement, **sans** son rendu (léger), le plus récent d'abord.
|
| 309 |
+
|
| 310 |
+
Quand le monde dépasse ``limit``, chaque secteur garde sa part (v4.5.2) et les artefacts simulés passent avant les
|
| 311 |
+
structures de code : une reconstruction de 60 sessions de code ne doit pas chasser de l'arène le drone ou le pont
|
| 312 |
+
que l'essaim vient de valider.
|
| 313 |
+
"""
|
| 314 |
with _LOCK:
|
| 315 |
latest = list(_latest_by_slot(Path(world_dir) / OBJECTS).values())
|
| 316 |
latest.sort(key=lambda r: r.get("at", ""), reverse=True)
|
| 317 |
+
limit = max(1, limit)
|
| 318 |
+
if len(latest) > limit:
|
| 319 |
+
quota = max(1, limit // len(SECTORS))
|
| 320 |
+
chosen: list[dict] = []
|
| 321 |
+
taken: set[int] = set()
|
| 322 |
+
per: dict[str, int] = {}
|
| 323 |
+
for artefacts_first in (True, False): # 1) artefacts par secteur, 2) structures par secteur
|
| 324 |
+
for i, r in enumerate(latest):
|
| 325 |
+
d = r.get("domain", "algorithmique")
|
| 326 |
+
if i in taken or (r.get("matter") == "artefact") != artefacts_first or per.get(d, 0) >= quota:
|
| 327 |
+
continue
|
| 328 |
+
chosen.append(r)
|
| 329 |
+
taken.add(i)
|
| 330 |
+
per[d] = per.get(d, 0) + 1
|
| 331 |
+
for i, r in enumerate(latest): # 3) le reste de la place, au plus récent
|
| 332 |
+
if len(chosen) >= limit:
|
| 333 |
+
break
|
| 334 |
+
if i not in taken:
|
| 335 |
+
chosen.append(r)
|
| 336 |
+
taken.add(i)
|
| 337 |
+
order = {id(r): i for i, r in enumerate(latest)}
|
| 338 |
+
latest = sorted(chosen[:limit], key=lambda r: order[id(r)])
|
| 339 |
+
return [{k: v for k, v in r.items() if k != "render"} for r in latest]
|
| 340 |
|
| 341 |
|
| 342 |
def object_render(world_dir: Path, object_id: str) -> dict | None:
|
tests/test_frontend.py
CHANGED
|
@@ -209,3 +209,10 @@ def test_cache_policy_revalidates_the_page_and_spares_the_api():
|
|
| 209 |
assert cache_policy("/symbioza/static/css/hud.css") == "no-cache"
|
| 210 |
assert cache_policy("/static/vendor/three/build/three.module.js").startswith("public, max-age=")
|
| 211 |
assert cache_policy("/api/ui/meta") is None and cache_policy("/gradio/") is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
assert cache_policy("/symbioza/static/css/hud.css") == "no-cache"
|
| 210 |
assert cache_policy("/static/vendor/three/build/three.module.js").startswith("public, max-age=")
|
| 211 |
assert cache_policy("/api/ui/meta") is None and cache_policy("/gradio/") is None
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def test_the_flat_view_is_frugal_on_old_machines():
|
| 215 |
+
"""v4.5.3 : tracés groupés, budget de segments par image, aucune image tracée quand rien ne bouge."""
|
| 216 |
+
flat = FLAT.read_text(encoding="utf-8")
|
| 217 |
+
assert "new Path2D()" in flat and "EDGE_BUDGET" in flat and "FRAME_MS" in flat
|
| 218 |
+
assert "(!dirty && !animated())" in flat
|
tests/test_world.py
CHANGED
|
@@ -207,3 +207,16 @@ def test_rebuild_is_admin_only_and_rebenches_old_sessions(unified):
|
|
| 207 |
r = c.post("/api/ui/jobs/world", json={"limit": 10}, headers={"x-admin-key": "secret"})
|
| 208 |
j = wait(c, r.json()["job"])
|
| 209 |
assert j["state"] == "done" and "dont 1 en rupture" in j["result"]["message"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
r = c.post("/api/ui/jobs/world", json={"limit": 10}, headers={"x-admin-key": "secret"})
|
| 208 |
j = wait(c, r.json()["job"])
|
| 209 |
assert j["state"] == "done" and "dont 1 en rupture" in j["result"]["message"]
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def test_a_flood_of_code_structures_never_pushes_validated_artifacts_out_of_the_arena(tmp_path):
|
| 213 |
+
"""Régression v4.5.1 : 59 sessions de code reconstruites après le drone le chassaient des 40 objets affichés."""
|
| 214 |
+
report = lab.evaluate({"solution.py": TRUSS["candidate"]}, lab_tests(TRUSS))
|
| 215 |
+
world.deposit(tmp_path, instruction="pont", files={}, task_hash="p" * 64, certified=True, report=report)
|
| 216 |
+
time.sleep(1.1) # les structures arrivent après, comme une reconstruction
|
| 217 |
+
for i in range(59):
|
| 218 |
+
world.deposit(tmp_path, instruction=f"code {i}", files={"solution.py": f"def f{i}(x):\n return x + {i}\n"},
|
| 219 |
+
task_hash=f"s{i:03d}" + "x" * 60, certified=True, report=None)
|
| 220 |
+
shown = world.objects(tmp_path, limit=40)
|
| 221 |
+
assert len(shown) == 40 and any(o["domain"] == "mecanique" for o in shown)
|
| 222 |
+
assert [o["at"] for o in shown] == sorted((o["at"] for o in shown), reverse=True)
|
unique_ext/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
| 13 |
import os
|
| 14 |
|
| 15 |
API_VERSION = 1 # contrat entre le job d'entraînement et ce paquet : le job refuse une autre valeur
|
| 16 |
-
__version__ = "4.5.
|
| 17 |
|
| 18 |
|
| 19 |
def enabled(name: str) -> bool:
|
|
|
|
| 13 |
import os
|
| 14 |
|
| 15 |
API_VERSION = 1 # contrat entre le job d'entraînement et ce paquet : le job refuse une autre valeur
|
| 16 |
+
__version__ = "4.5.3"
|
| 17 |
|
| 18 |
|
| 19 |
def enabled(name: str) -> bool:
|
web/symbioza/static/js/app.js
CHANGED
|
@@ -1104,6 +1104,11 @@ async function poll() {
|
|
| 1104 |
const shown = flat.update(lastRecords, building);
|
| 1105 |
setText('m-shown', String(shown));
|
| 1106 |
body.dataset.items = String(shown);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1107 |
setSource(shown ? 'live' : 'demo');
|
| 1108 |
} else {
|
| 1109 |
setSource('off');
|
|
@@ -1159,6 +1164,7 @@ function toggleBloom() {
|
|
| 1159 |
function toggleRotate() {
|
| 1160 |
state.rotate = !state.rotate;
|
| 1161 |
setPressed('btn-rotate', state.rotate);
|
|
|
|
| 1162 |
}
|
| 1163 |
|
| 1164 |
function onKey(e) {
|
|
@@ -1274,6 +1280,7 @@ if (!ctx) {
|
|
| 1274 |
showGlMessage(glError);
|
| 1275 |
// Repli : le même monde en 2D isométrique. Un clic sur un objet ouvre l'inspecteur, comme dans la 3D.
|
| 1276 |
flat = createFlat($('scene'), {
|
|
|
|
| 1277 |
area: () => {
|
| 1278 |
const w = window.innerWidth;
|
| 1279 |
const h = window.innerHeight;
|
|
|
|
| 1104 |
const shown = flat.update(lastRecords, building);
|
| 1105 |
setText('m-shown', String(shown));
|
| 1106 |
body.dataset.items = String(shown);
|
| 1107 |
+
// Jauges en vue en plan : le coût réel du dessin 2D sur cette machine (les calculs, eux, sont faits sur le Space).
|
| 1108 |
+
const st = flat.stats();
|
| 1109 |
+
setText('m-fps', st.animated ? st.fps.toFixed(0) : 'repos');
|
| 1110 |
+
setText('m-frame', `${st.drawMs.toFixed(1)} ms`);
|
| 1111 |
+
setText('m-tris', `${st.segments.toLocaleString('fr-FR')} seg.`);
|
| 1112 |
setSource(shown ? 'live' : 'demo');
|
| 1113 |
} else {
|
| 1114 |
setSource('off');
|
|
|
|
| 1164 |
function toggleRotate() {
|
| 1165 |
state.rotate = !state.rotate;
|
| 1166 |
setPressed('btn-rotate', state.rotate);
|
| 1167 |
+
if (typeof flat !== 'undefined' && flat) flat.redraw();
|
| 1168 |
}
|
| 1169 |
|
| 1170 |
function onKey(e) {
|
|
|
|
| 1280 |
showGlMessage(glError);
|
| 1281 |
// Repli : le même monde en 2D isométrique. Un clic sur un objet ouvre l'inspecteur, comme dans la 3D.
|
| 1282 |
flat = createFlat($('scene'), {
|
| 1283 |
+
rotating: () => state.rotate,
|
| 1284 |
area: () => {
|
| 1285 |
const w = window.innerWidth;
|
| 1286 |
const h = window.innerHeight;
|
web/symbioza/static/js/flat.js
CHANGED
|
@@ -9,7 +9,9 @@ const NAMES = {
|
|
| 9 |
mecanique: 'MÉCANIQUE & ROBOTIQUE', electronique: 'ÉLECTRONIQUE & EMBARQUÉ',
|
| 10 |
biologie: 'GÉNÉTIQUE & BIO SYNTHÉTIQUE', algorithmique: 'ALGORITHMIQUE & ARCHITECTURES',
|
| 11 |
};
|
| 12 |
-
const MAX_EDGES = 1600;
|
|
|
|
|
|
|
| 13 |
const BOX = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]];
|
| 14 |
|
| 15 |
function edgesOf(vertices, faces) {
|
|
@@ -85,7 +87,7 @@ function prepare(e, i, n) {
|
|
| 85 |
return item;
|
| 86 |
}
|
| 87 |
|
| 88 |
-
export function createFlat(host, { onPick, area } = {}) {
|
| 89 |
const canvas = document.createElement('canvas');
|
| 90 |
canvas.className = 'flat';
|
| 91 |
host.appendChild(canvas);
|
|
@@ -97,6 +99,8 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 97 |
let view = { cx: 0, cy: 0, s: 40 };
|
| 98 |
const started = performance.now();
|
| 99 |
let last = 0;
|
|
|
|
|
|
|
| 100 |
|
| 101 |
function resize() {
|
| 102 |
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -124,31 +128,34 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 124 |
return [view.cx + view.s * u, view.cy + view.s * (0.5 * v - 0.9 * y), v];
|
| 125 |
}
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
g.strokeStyle = color;
|
| 131 |
g.lineWidth = width;
|
| 132 |
-
g.
|
| 133 |
-
g.moveTo(p[0], p[1]);
|
| 134 |
-
g.lineTo(q[0], q[1]);
|
| 135 |
-
g.stroke();
|
| 136 |
}
|
| 137 |
|
| 138 |
-
function
|
| 139 |
const c = P(x, FLOOR_Y, z);
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
g.beginPath();
|
| 143 |
-
g.ellipse(c[0], c[1], r * view.s, r * view.s * 0.5, 0, 0, Math.PI * 2);
|
| 144 |
-
g.stroke();
|
| 145 |
}
|
| 146 |
|
| 147 |
function drawSectors() {
|
|
|
|
| 148 |
for (let k = 0; k < 4; k += 1) {
|
| 149 |
const a = (k * Math.PI) / 2;
|
| 150 |
-
|
| 151 |
}
|
|
|
|
| 152 |
g.font = `600 ${view.s > 14 ? 11 : 9}px ui-monospace, monospace`;
|
| 153 |
g.textAlign = 'center';
|
| 154 |
g.fillStyle = 'rgba(201, 155, 255, 0.85)';
|
|
@@ -159,7 +166,7 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 159 |
}
|
| 160 |
}
|
| 161 |
|
| 162 |
-
function
|
| 163 |
const r = it.rig;
|
| 164 |
const total = r.frames.length * r.dt;
|
| 165 |
const tt = t % (total + 1.2);
|
|
@@ -177,45 +184,80 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 177 |
const w = rotate(bq, [local[0] + geo.pos[0], local[1] + geo.pos[1], local[2] + geo.pos[2]]);
|
| 178 |
corners.push(toScene([w[0] + bp[0], w[1] + bp[1], w[2] + bp[2]]));
|
| 179 |
}
|
| 180 |
-
for (const [a, b] of BOX)
|
| 181 |
}
|
| 182 |
}
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
function draw(now) {
|
| 185 |
requestAnimationFrame(draw);
|
| 186 |
-
if (document.hidden || now - last <
|
|
|
|
| 187 |
last = now;
|
|
|
|
|
|
|
| 188 |
const t = (now - started) / 1000;
|
| 189 |
-
if (!drag) theta += 0.0016;
|
| 190 |
g.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
| 191 |
drawSectors();
|
| 192 |
-
const
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
const [px, pz] = it.place;
|
| 195 |
const e = it.entry;
|
| 196 |
-
|
| 197 |
-
if (it.rig)
|
| 198 |
-
for (
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
const p = P(px + m.p[0], m.p[1], pz + m.p[2]);
|
| 202 |
-
g.fillStyle = m.s === 2 ? `rgba(255, 42, 77, ${0.55 + 0.45 * Math.abs(Math.sin(t * 3))})` : LINK[m.s] || LINK[1];
|
| 203 |
-
g.beginPath();
|
| 204 |
-
g.arc(p[0], p[1], 3.5, 0, Math.PI * 2);
|
| 205 |
-
g.fill();
|
| 206 |
}
|
|
|
|
|
|
|
| 207 |
}
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
}
|
|
|
|
| 218 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
}
|
| 220 |
|
| 221 |
function pick(ev) {
|
|
@@ -233,6 +275,7 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 233 |
canvas.addEventListener('pointermove', (ev) => {
|
| 234 |
if (!drag) return;
|
| 235 |
theta += (ev.clientX - drag.x) * 0.006;
|
|
|
|
| 236 |
drag.moved += Math.abs(ev.clientX - drag.x);
|
| 237 |
drag.x = ev.clientX;
|
| 238 |
});
|
|
@@ -240,7 +283,7 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 240 |
if (drag && drag.moved < 6) pick(ev);
|
| 241 |
drag = null;
|
| 242 |
});
|
| 243 |
-
window.addEventListener('resize', () => { resize(); fit(); });
|
| 244 |
resize();
|
| 245 |
fit();
|
| 246 |
requestAnimationFrame(draw);
|
|
@@ -250,7 +293,10 @@ export function createFlat(host, { onPick, area } = {}) {
|
|
| 250 |
items = records.map((e, i) => prepare(e, i, records.length));
|
| 251 |
sites = (building || []).filter((b) => Array.isArray(b.place));
|
| 252 |
fit();
|
|
|
|
| 253 |
return items.length;
|
| 254 |
},
|
|
|
|
|
|
|
| 255 |
};
|
| 256 |
}
|
|
|
|
| 9 |
mecanique: 'MÉCANIQUE & ROBOTIQUE', electronique: 'ÉLECTRONIQUE & EMBARQUÉ',
|
| 10 |
biologie: 'GÉNÉTIQUE & BIO SYNTHÉTIQUE', algorithmique: 'ALGORITHMIQUE & ARCHITECTURES',
|
| 11 |
};
|
| 12 |
+
const MAX_EDGES = 1600; // segments gardés par objet
|
| 13 |
+
const EDGE_BUDGET = 12000; // segments tracés au plus par image, tous objets confondus
|
| 14 |
+
const FRAME_MS = 42; // 24 images/s au plus : assez pour une rejouée, sobre pour un vieux processeur
|
| 15 |
const BOX = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]];
|
| 16 |
|
| 17 |
function edgesOf(vertices, faces) {
|
|
|
|
| 87 |
return item;
|
| 88 |
}
|
| 89 |
|
| 90 |
+
export function createFlat(host, { onPick, area, rotating } = {}) {
|
| 91 |
const canvas = document.createElement('canvas');
|
| 92 |
canvas.className = 'flat';
|
| 93 |
host.appendChild(canvas);
|
|
|
|
| 99 |
let view = { cx: 0, cy: 0, s: 40 };
|
| 100 |
const started = performance.now();
|
| 101 |
let last = 0;
|
| 102 |
+
let dirty = true;
|
| 103 |
+
const stats = { drawMs: 0, fps: 0, segments: 0 };
|
| 104 |
|
| 105 |
function resize() {
|
| 106 |
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
|
|
| 128 |
return [view.cx + view.s * u, view.cy + view.s * (0.5 * v - 0.9 * y), v];
|
| 129 |
}
|
| 130 |
|
| 131 |
+
// Tracés groupés : un seul chemin par couleur, tracé une fois par image (des milliers de segments en un appel),
|
| 132 |
+
// et un budget de segments par image pour qu'une machine modeste ne sature jamais.
|
| 133 |
+
function seg(path, a, b) {
|
| 134 |
+
const p = P(a[0], a[1], a[2]);
|
| 135 |
+
const q = P(b[0], b[1], b[2]);
|
| 136 |
+
path.moveTo(p[0], p[1]);
|
| 137 |
+
path.lineTo(q[0], q[1]);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function stroke(path, color, width) {
|
| 141 |
g.strokeStyle = color;
|
| 142 |
g.lineWidth = width;
|
| 143 |
+
g.stroke(path);
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
|
| 146 |
+
function ring(path, x, z, r) {
|
| 147 |
const c = P(x, FLOOR_Y, z);
|
| 148 |
+
path.moveTo(c[0] + r * view.s, c[1]);
|
| 149 |
+
path.ellipse(c[0], c[1], r * view.s, r * view.s * 0.5, 0, 0, Math.PI * 2);
|
|
|
|
|
|
|
|
|
|
| 150 |
}
|
| 151 |
|
| 152 |
function drawSectors() {
|
| 153 |
+
const path = new Path2D();
|
| 154 |
for (let k = 0; k < 4; k += 1) {
|
| 155 |
const a = (k * Math.PI) / 2;
|
| 156 |
+
seg(path, [Math.cos(a) * 3.2, FLOOR_Y, Math.sin(a) * 3.2], [Math.cos(a) * 40, FLOOR_Y, Math.sin(a) * 40]);
|
| 157 |
}
|
| 158 |
+
stroke(path, 'rgba(162, 75, 255, 0.28)', 1);
|
| 159 |
g.font = `600 ${view.s > 14 ? 11 : 9}px ui-monospace, monospace`;
|
| 160 |
g.textAlign = 'center';
|
| 161 |
g.fillStyle = 'rgba(201, 155, 255, 0.85)';
|
|
|
|
| 166 |
}
|
| 167 |
}
|
| 168 |
|
| 169 |
+
function rigEdges(path, it, t) {
|
| 170 |
const r = it.rig;
|
| 171 |
const total = r.frames.length * r.dt;
|
| 172 |
const tt = t % (total + 1.2);
|
|
|
|
| 184 |
const w = rotate(bq, [local[0] + geo.pos[0], local[1] + geo.pos[1], local[2] + geo.pos[2]]);
|
| 185 |
corners.push(toScene([w[0] + bp[0], w[1] + bp[1], w[2] + bp[2]]));
|
| 186 |
}
|
| 187 |
+
for (const [a, b] of BOX) seg(path, corners[a], corners[b]);
|
| 188 |
}
|
| 189 |
}
|
| 190 |
|
| 191 |
+
// Une image n'est tracée que si quelque chose bouge (rotation, robot rejoué, chantier) ou a changé : à l'arrêt,
|
| 192 |
+
// la vue ne coûte rien au processeur.
|
| 193 |
+
function animated() {
|
| 194 |
+
return (rotating ? rotating() : true) || sites.length > 0 || items.some((it) => it.rig);
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
function draw(now) {
|
| 198 |
requestAnimationFrame(draw);
|
| 199 |
+
if (document.hidden || (!dirty && !animated()) || now - last < FRAME_MS) return;
|
| 200 |
+
const interval = now - last;
|
| 201 |
last = now;
|
| 202 |
+
dirty = false;
|
| 203 |
+
const t0 = performance.now();
|
| 204 |
const t = (now - started) / 1000;
|
| 205 |
+
if (!drag && (rotating ? rotating() : true)) theta += 0.0016 * Math.min(interval, 200) / 33;
|
| 206 |
g.clearRect(0, 0, window.innerWidth, window.innerHeight);
|
| 207 |
drawSectors();
|
| 208 |
+
const total = items.reduce((n, it) => n + it.edges.length, 0);
|
| 209 |
+
const stride = Math.max(1, Math.ceil(total / EDGE_BUDGET));
|
| 210 |
+
const edges = new Path2D();
|
| 211 |
+
const rigs = new Path2D();
|
| 212 |
+
const links = [new Path2D(), new Path2D(), new Path2D()];
|
| 213 |
+
const rings = { valide: new Path2D(), rupture: new Path2D(), libre: new Path2D() };
|
| 214 |
+
const marks = [[], [], []];
|
| 215 |
+
for (const it of items) {
|
| 216 |
const [px, pz] = it.place;
|
| 217 |
const e = it.entry;
|
| 218 |
+
ring(rings[e.lab ? (e.status === 'rupture' ? 'rupture' : 'valide') : 'libre'], px, pz, 1.45);
|
| 219 |
+
if (it.rig) rigEdges(rigs, it, t);
|
| 220 |
+
for (let k = 0; k < it.edges.length; k += stride) {
|
| 221 |
+
const [a, b] = it.edges[k];
|
| 222 |
+
seg(edges, [px + a[0], a[1], pz + a[2]], [px + b[0], b[1], pz + b[2]]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
}
|
| 224 |
+
for (const l of it.links) seg(links[l.s] || links[0], [px + l.a[0], l.a[1], pz + l.a[2]], [px + l.b[0], l.b[1], pz + l.b[2]]);
|
| 225 |
+
for (const m of it.marks) marks[m.s in marks ? m.s : 2].push(P(px + m.p[0], m.p[1], pz + m.p[2]));
|
| 226 |
}
|
| 227 |
+
stroke(rings.libre, 'rgba(255, 230, 0, 0.6)', 1.6);
|
| 228 |
+
stroke(rings.valide, STATUS.valide, 1.6);
|
| 229 |
+
stroke(rings.rupture, STATUS.rupture, 1.8);
|
| 230 |
+
stroke(edges, 'rgba(201, 155, 255, 0.5)', 1);
|
| 231 |
+
stroke(rigs, 'rgba(214, 170, 255, 0.95)', 1.4);
|
| 232 |
+
links.forEach((path, k) => stroke(path, LINK[k], 1.6));
|
| 233 |
+
marks.forEach((pts, k) => {
|
| 234 |
+
if (!pts.length) return;
|
| 235 |
+
const path = new Path2D();
|
| 236 |
+
for (const p of pts) { path.moveTo(p[0] + 3.5, p[1]); path.arc(p[0], p[1], 3.5, 0, Math.PI * 2); }
|
| 237 |
+
g.globalAlpha = k === 2 ? 0.55 + 0.45 * Math.abs(Math.sin(t * 3)) : 1;
|
| 238 |
+
g.fillStyle = LINK[k];
|
| 239 |
+
g.fill(path);
|
| 240 |
+
g.globalAlpha = 1;
|
| 241 |
+
});
|
| 242 |
+
if (sites.length) { // chantiers : losange en fil de fer qui tourne
|
| 243 |
+
const path = new Path2D();
|
| 244 |
+
for (const b of sites) {
|
| 245 |
+
const [x, z] = b.place;
|
| 246 |
+
ring(path, x, z, 1.45);
|
| 247 |
+
const a = t * 1.5;
|
| 248 |
+
const pts = [0, 1, 2, 3].map((k) => [x + 0.9 * Math.cos(a + (k * Math.PI) / 2), 0, z + 0.9 * Math.sin(a + (k * Math.PI) / 2)]);
|
| 249 |
+
for (let k = 0; k < 4; k += 1) {
|
| 250 |
+
seg(path, pts[k], pts[(k + 1) % 4]);
|
| 251 |
+
seg(path, pts[k], [x, 1, z]);
|
| 252 |
+
seg(path, pts[k], [x, -1, z]);
|
| 253 |
+
}
|
| 254 |
}
|
| 255 |
+
stroke(path, 'rgba(162, 75, 255, 0.85)', 1.3);
|
| 256 |
}
|
| 257 |
+
const spent = performance.now() - t0;
|
| 258 |
+
stats.drawMs = stats.drawMs ? stats.drawMs * 0.9 + spent * 0.1 : spent;
|
| 259 |
+
stats.fps = interval > 0 && interval < 1000 ? (stats.fps ? stats.fps * 0.9 + (1000 / interval) * 0.1 : 1000 / interval) : stats.fps;
|
| 260 |
+
stats.segments = Math.ceil(total / stride);
|
| 261 |
}
|
| 262 |
|
| 263 |
function pick(ev) {
|
|
|
|
| 275 |
canvas.addEventListener('pointermove', (ev) => {
|
| 276 |
if (!drag) return;
|
| 277 |
theta += (ev.clientX - drag.x) * 0.006;
|
| 278 |
+
dirty = true;
|
| 279 |
drag.moved += Math.abs(ev.clientX - drag.x);
|
| 280 |
drag.x = ev.clientX;
|
| 281 |
});
|
|
|
|
| 283 |
if (drag && drag.moved < 6) pick(ev);
|
| 284 |
drag = null;
|
| 285 |
});
|
| 286 |
+
window.addEventListener('resize', () => { resize(); fit(); dirty = true; });
|
| 287 |
resize();
|
| 288 |
fit();
|
| 289 |
requestAnimationFrame(draw);
|
|
|
|
| 293 |
items = records.map((e, i) => prepare(e, i, records.length));
|
| 294 |
sites = (building || []).filter((b) => Array.isArray(b.place));
|
| 295 |
fit();
|
| 296 |
+
dirty = true;
|
| 297 |
return items.length;
|
| 298 |
},
|
| 299 |
+
redraw() { dirty = true; },
|
| 300 |
+
stats() { return { ...stats, animated: animated() }; },
|
| 301 |
};
|
| 302 |
}
|