Ajoute les blocs de logique : organise le graphe de flow en sous-graphes nommés
Le graphe de logique d'une scène s'affichait jusqu'ici sur un seul canevas plat (toutes les scènes accumulant leurs nœuds sur la même grille), ce qui ne tient pas à l'échelle dès qu'une scène évolue au fil de l'avancée du joueur et accumule des centaines/milliers de nœuds. Ajoute les "Blocs de logique" : un bloc regroupe un sous-ensemble de nœuds/arêtes d'un écran sous un nom et une description (comme une fonction). L'onglet "Logique de la scène" devient une liste de blocs (nom, description tronquée à 3 phrases, éléments concernés, nombre de nœuds, bouton "Ouvrir"). Ouvrir un bloc affiche SON graphe dans une modale plein écran, redimensionnable et déplaçable (patron déjà mûr dans game_dashboard.html, porté tel quel : makeFloatPanelDraggable/ Resizable/Fullscreenable). Décision d'architecture : un bloc est un automate FERMÉ — impossible de relier un nœud d'un bloc à un nœud d'un autre bloc (rejeté côté serveur dans flow_edge_add.py). Toute communication entre deux blocs passe par le système d'événements personnalisés déjà en place (declencher_evenement / trigger_event="evenement"). Détails techniques : - Nouvelle colonne _flow_nodes.block_id (nullable, sans FK — même rationale que trigger_element_id/target_element_id, voir screens/elements/delete_element.py) et nouvelle table _flow_blocks (screens/flow/ensure_flow_schema.py, screens/flow/blocks/ensure_flow_blocks_schema.py). - Migration douce et automatique : les nœuds posés avant l'existence des blocs (block_id NULL) sont rattachés, à la première ouverture de l'onglet, à un "Bloc principal" auto-créé (screens/flow/blocks/ list_flow_blocks.py) — aucun script de migration séparé, aucune donnée perdue. - Suppression d'un bloc = cascade complète (bloc + tous ses nœuds/ arêtes), patron identique à screens/custom_events/delete_custom_event.py mais scopé à un seul bloc plutôt que game-wide. - Routes CRUD sous routes/flow_blocks/, montées comme routes/custom_events/. - templates/screen_edit.html : FLOW (global unique) renommé en ALL_FLOW (toutes les données de l'écran) ; un seul bloc ouvert à la fois (modale unique, à la Unity) — currentBlockNodes()/currentBlockEdges() filtrent ALL_FLOW par CURRENT_BLOCK_ID à chaque rendu, sans tenir de seconde copie à synchroniser manuellement. Vérifié : 215 tests passent (7 nouveaux dans tests/test_flow_blocks.py, dont un qui verrouille l'ordre d'appel list_flow_blocks()/ list_flow_nodes() dans screen_edit.py — la migration douce doit tourner AVANT le chargement des nœuds, sinon le compte de nœuds affiché juste après une migration est périmé), syntaxe JS validée (script de screen_edit.html rendu via le client de test puis node --check). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a7a315cce7
commit
1b7706b357
@@ -108,6 +108,13 @@ from .flow.delete_flow_node import delete_flow_node
|
||||
from .flow.add_flow_edge import add_flow_edge
|
||||
from .flow.delete_flow_edge import delete_flow_edge
|
||||
|
||||
from .flow.blocks.list_flow_blocks import list_flow_blocks
|
||||
from .flow.blocks.get_flow_block import get_flow_block
|
||||
from .flow.blocks.create_flow_block import create_flow_block
|
||||
from .flow.blocks.update_flow_block import update_flow_block
|
||||
from .flow.blocks.delete_flow_block import delete_flow_block
|
||||
from .flow.blocks.blocks_view import blocks_view
|
||||
|
||||
from .data_actions.apply_data_action import apply_data_action
|
||||
from .data_actions.apply_variable_action import apply_variable_action
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
def blocks_view(flow_blocks, flow_nodes, elements_labels):
|
||||
"""Enrichit chaque bloc (nom/description bruts) avec son nombre de
|
||||
nœuds et les éléments qu'il concerne (trigger_element_id des
|
||||
déclencheurs + target_element_id des actions, dédoublonnés) — calculé
|
||||
en Python à partir de ce que routes/screens/screen_edit.py a déjà
|
||||
chargé, sans requête SQL supplémentaire."""
|
||||
nodes_by_block = {}
|
||||
for n in flow_nodes:
|
||||
nodes_by_block.setdefault(n.get("block_id"), []).append(n)
|
||||
view = []
|
||||
for b in flow_blocks:
|
||||
block_nodes = nodes_by_block.get(b["id"], [])
|
||||
element_ids = {n["trigger_element_id"] for n in block_nodes if n.get("trigger_element_id")}
|
||||
element_ids |= {n["target_element_id"] for n in block_nodes if n.get("target_element_id")}
|
||||
view.append({
|
||||
**b,
|
||||
"node_count": len(block_nodes),
|
||||
"element_labels": [elements_labels[eid] for eid in element_ids if eid in elements_labels],
|
||||
})
|
||||
return view
|
||||
@@ -0,0 +1,19 @@
|
||||
import db
|
||||
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def create_flow_block(slug, screen_id, name, description=""):
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return None
|
||||
ensure_flow_blocks_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"INSERT INTO _flow_blocks (screen_id, name, description) VALUES (?, ?, ?)",
|
||||
(screen_id, name, (description or "").strip()),
|
||||
)
|
||||
new_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.commit()
|
||||
conn.close()
|
||||
return new_id
|
||||
@@ -0,0 +1,26 @@
|
||||
import db
|
||||
|
||||
from ..ensure_flow_schema import ensure_flow_schema
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def delete_flow_block(slug, block_id):
|
||||
"""Supprime le bloc ET tout ce qu'il contient (nœuds + arêtes) — décision
|
||||
explicite de l'utilisateur, un bloc de logique se comporte comme une
|
||||
fonction qu'on supprime avec son corps. Patron identique à
|
||||
screens/custom_events/delete_custom_event.py, mais scopé à un seul bloc
|
||||
(pas game-wide) : les AUTRES blocs du même écran ne sont pas touchés."""
|
||||
ensure_flow_schema(slug)
|
||||
ensure_flow_blocks_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"""DELETE FROM _flow_edges WHERE from_node_id IN
|
||||
(SELECT id FROM _flow_nodes WHERE block_id = ?)
|
||||
OR to_node_id IN
|
||||
(SELECT id FROM _flow_nodes WHERE block_id = ?)""",
|
||||
(block_id, block_id),
|
||||
)
|
||||
conn.execute("DELETE FROM _flow_nodes WHERE block_id = ?", (block_id,))
|
||||
conn.execute("DELETE FROM _flow_blocks WHERE id = ?", (block_id,))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,24 @@
|
||||
import db
|
||||
|
||||
|
||||
def ensure_flow_blocks_schema(slug):
|
||||
"""Un bloc de logique regroupe les nœuds d'UN écran (scène ou modèle)
|
||||
en sous-graphe nommé/décrit, pour rester lisible même quand un écran
|
||||
accumule des milliers de nœuds — voir _flow_nodes.block_id
|
||||
(screens/flow/ensure_flow_schema.py). Contrairement à un événement
|
||||
personnalisé (game-wide, voir db/custom_events/), un bloc n'a de sens
|
||||
que pour SON écran : "name" n'est donc PAS unique globalement."""
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS _flow_blocks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -0,0 +1,13 @@
|
||||
import db
|
||||
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def get_flow_block(slug, block_id):
|
||||
if not block_id:
|
||||
return None
|
||||
ensure_flow_blocks_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
row = conn.execute("SELECT * FROM _flow_blocks WHERE id = ?", (block_id,)).fetchone()
|
||||
conn.close()
|
||||
return dict(row) if row else None
|
||||
@@ -0,0 +1,45 @@
|
||||
import db
|
||||
|
||||
from ..ensure_flow_schema import ensure_flow_schema
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
DEFAULT_BLOCK_NAME = "Bloc principal"
|
||||
|
||||
|
||||
def list_flow_blocks(slug, screen_id):
|
||||
"""Migration douce : un écran qui a des nœuds posés AVANT l'existence
|
||||
des blocs (block_id NULL) se voit attribuer, à la première consultation
|
||||
de l'onglet Blocs, un bloc "Bloc principal" auto-créé qui les regroupe —
|
||||
aucun script de migration séparé à faire tourner, aucune donnée perdue.
|
||||
Idempotent : si "Bloc principal" existe déjà pour cet écran, on le
|
||||
réutilise plutôt que d'en recréer un second."""
|
||||
ensure_flow_schema(slug)
|
||||
ensure_flow_blocks_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
orphan_count = conn.execute(
|
||||
"SELECT COUNT(*) AS n FROM _flow_nodes WHERE screen_id = ? AND block_id IS NULL",
|
||||
(screen_id,),
|
||||
).fetchone()["n"]
|
||||
if orphan_count:
|
||||
default_block = conn.execute(
|
||||
"SELECT id FROM _flow_blocks WHERE screen_id = ? AND name = ?",
|
||||
(screen_id, DEFAULT_BLOCK_NAME),
|
||||
).fetchone()
|
||||
if default_block:
|
||||
default_block_id = default_block["id"]
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO _flow_blocks (screen_id, name, description) VALUES (?, ?, ?)",
|
||||
(screen_id, DEFAULT_BLOCK_NAME, ""),
|
||||
)
|
||||
default_block_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||
conn.execute(
|
||||
"UPDATE _flow_nodes SET block_id = ? WHERE screen_id = ? AND block_id IS NULL",
|
||||
(default_block_id, screen_id),
|
||||
)
|
||||
conn.commit()
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM _flow_blocks WHERE screen_id = ? ORDER BY id", (screen_id,)
|
||||
).fetchall()
|
||||
conn.close()
|
||||
return [dict(r) for r in rows]
|
||||
@@ -0,0 +1,17 @@
|
||||
import db
|
||||
|
||||
from .ensure_flow_blocks_schema import ensure_flow_blocks_schema
|
||||
|
||||
|
||||
def update_flow_block(slug, block_id, name, description=""):
|
||||
name = (name or "").strip()
|
||||
if not name:
|
||||
return
|
||||
ensure_flow_blocks_schema(slug)
|
||||
conn = db.connect(slug)
|
||||
conn.execute(
|
||||
"UPDATE _flow_blocks SET name = ?, description = ? WHERE id = ?",
|
||||
(name, (description or "").strip(), block_id),
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
@@ -86,4 +86,9 @@ FLOW_NODE_FIELDS = {
|
||||
# avec ses propres réglages habituels (target_element_id normal, pas
|
||||
# de mécanisme dynamique dédié).
|
||||
"trigger_custom_event_id", "target_custom_event_id",
|
||||
# Blocs de logique (voir screens/flow/blocks/) : à quel bloc ce nœud
|
||||
# appartient. Aucun `update_flow_node` n'existe (éditer un nœud le
|
||||
# recrée puis supprime l'ancien) — le client doit donc renvoyer
|
||||
# block_id à chaque (ré)création, pas juste le poser une fois.
|
||||
"block_id",
|
||||
}
|
||||
|
||||
@@ -83,5 +83,18 @@ def ensure_flow_schema(slug):
|
||||
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_custom_event_id INTEGER")
|
||||
if "target_custom_event_id" not in cols:
|
||||
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_custom_event_id INTEGER")
|
||||
if "block_id" not in cols:
|
||||
# Blocs de logique (voir screens/flow/blocks/) : regroupe les nœuds
|
||||
# d'un même écran en sous-graphes nommés/décrits pour rester lisible
|
||||
# à l'échelle de milliers de nœuds. Pas de FK vers _flow_blocks —
|
||||
# même rationale que trigger_element_id/target_element_id
|
||||
# (screens/elements/delete_element.py) : la suppression d'un bloc
|
||||
# nettoie déjà elle-même ces lignes (voir
|
||||
# screens/flow/blocks/delete_flow_block.py), une contrainte ferait
|
||||
# double emploi. NULL = nœud pas encore rattaché à un bloc (jeux
|
||||
# créés avant cette fonctionnalité) — list_flow_blocks() les
|
||||
# rattache paresseusement à un bloc "Bloc principal" généré à la
|
||||
# volée, la première fois que l'onglet Blocs est ouvert.
|
||||
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN block_id INTEGER")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
Reference in New Issue
Block a user