first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s

This commit is contained in:
william
2026-08-21 16:23:49 +02:00
committed by william
commit 3f4ebc4527
248 changed files with 10180 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
"""Logique visuelle (éditeur de flow à nœuds) : déclencheurs, conditions,
actions, sous forme de graphe propre à chaque écran."""
+13
View File
@@ -0,0 +1,13 @@
import db
def add_flow_edge(slug, screen_id, from_node_id, from_port, to_node_id):
conn = db.connect(slug)
conn.execute(
"INSERT INTO _flow_edges (screen_id, from_node_id, from_port, to_node_id) VALUES (?, ?, ?, ?)",
(screen_id, from_node_id, from_port or "out", to_node_id),
)
edge_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
conn.commit()
conn.close()
return edge_id
+21
View File
@@ -0,0 +1,21 @@
import db
from .ensure_flow_schema import ensure_flow_schema
from .constants import FLOW_NODE_FIELDS
def add_flow_node(slug, screen_id, node_type, x=40, y=40, **fields):
ensure_flow_schema(slug)
cols = ["screen_id", "node_type", "x", "y"]
vals = [screen_id, node_type, x, y]
for k, v in fields.items():
if k in FLOW_NODE_FIELDS and v not in (None, ""):
cols.append(k)
vals.append(v)
placeholders = ", ".join("?" for _ in cols)
conn = db.connect(slug)
conn.execute(f"INSERT INTO _flow_nodes ({', '.join(cols)}) VALUES ({placeholders})", vals)
node_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
conn.commit()
conn.close()
return node_id
+47
View File
@@ -0,0 +1,47 @@
# ---------- Logique visuelle (éditeur de flow à nœuds) ----------
#
# Remplace l'ancien système "Actions" attaché à chaque élément : toute la
# logique d'une scène (déclencheurs, conditions, actions) vit maintenant dans
# un graphe de nœuds propre à l'écran, visible dans le panneau "Logique de
# la scène" de l'éditeur (voir screen_edit.html) — un nœud Déclencheur
# ("Au clic sur X") relié à des nœuds Condition (deux sorties Vrai/Faux) et
# Action (les mêmes effets qu'avant : changer d'écran, modifier un élément,
# modifier une donnée). L'ancien système _actions reste en base pour ne rien
# casser sur les jeux déjà créés, mais n'est plus exposé dans l'éditeur ni
# utilisé par le mode jouable.
TRIGGER_EVENTS = [
("clic", "Au clic"),
("soumission", "À la soumission"),
# Pas de "trigger_element_id" pour celui-ci : il concerne l'ÉCRAN entier,
# pas un élément précis (voir 1.2 dans claude/forge-engine-lacunes-boitemail.md
# côté projet Forge — "affichage piloté par la donnée"). Se déclenche
# côté jouable à chaque fois que l'écran est montré (premier affichage,
# retour en arrière, changement d'écran...) ET après toute donnée
# modifiée pendant qu'on est déjà sur cet écran — ce qui permet de
# cacher/montrer un élément selon l'état de la partie SANS qu'un clic
# explicite soit nécessaire pour le réévaluer.
("affichage", "À l'affichage de l'écran"),
]
CONDITION_OPERATORS = [
("egal", "est égal à"),
("different", "est différent de"),
("superieur", "est supérieur à"),
("inferieur", "est inférieur à"),
("superieur_egal", "est supérieur ou égal à"),
("inferieur_egal", "est inférieur ou égal à"),
]
CONDITION_OPERATOR_LABELS = dict(CONDITION_OPERATORS)
FLOW_NODE_FIELDS = {
"trigger_element_id", "trigger_event",
"cond_definition_id", "cond_row_id", "cond_field", "cond_field_type", "cond_operator", "cond_value",
# 2.4 — conditions combinées (ET/OU) : cond_clauses est une liste JSON de
# clauses supplémentaires (en plus de la clause "historique" ci-dessus,
# qui reste la première clause) + cond_combinator ('et'/'ou') pour savoir
# comment les combiner. Absents => comportement legacy (une seule clause).
"cond_clauses", "cond_combinator",
"action_type", "target_screen_id", "target_element_id", "element_property", "element_value",
"target_definition_id", "target_row_id", "target_field", "data_operation", "data_value",
}
+8
View File
@@ -0,0 +1,8 @@
import db
def delete_flow_edge(slug, edge_id):
conn = db.connect(slug)
conn.execute("DELETE FROM _flow_edges WHERE id = ?", (edge_id,))
conn.commit()
conn.close()
+9
View File
@@ -0,0 +1,9 @@
import db
def delete_flow_node(slug, node_id):
conn = db.connect(slug)
conn.execute("DELETE FROM _flow_edges WHERE from_node_id = ? OR to_node_id = ?", (node_id, node_id))
conn.execute("DELETE FROM _flow_nodes WHERE id = ?", (node_id,))
conn.commit()
conn.close()
+55
View File
@@ -0,0 +1,55 @@
import db
def ensure_flow_schema(slug):
conn = db.connect(slug)
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS _flow_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
node_type TEXT NOT NULL,
x REAL NOT NULL DEFAULT 40,
y REAL NOT NULL DEFAULT 40,
trigger_element_id INTEGER REFERENCES _screen_elements(id),
trigger_event TEXT,
cond_definition_id INTEGER,
cond_row_id INTEGER,
cond_field TEXT,
cond_field_type TEXT,
cond_operator TEXT,
cond_value TEXT,
action_type TEXT,
target_screen_id INTEGER REFERENCES _screens(id),
target_element_id INTEGER REFERENCES _screen_elements(id),
element_property TEXT,
element_value TEXT,
target_definition_id INTEGER,
target_row_id INTEGER,
target_field TEXT,
data_operation TEXT,
data_value TEXT
);
CREATE TABLE IF NOT EXISTS _flow_edges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE,
from_node_id INTEGER NOT NULL REFERENCES _flow_nodes(id) ON DELETE CASCADE,
from_port TEXT NOT NULL DEFAULT 'out',
to_node_id INTEGER NOT NULL REFERENCES _flow_nodes(id) ON DELETE CASCADE
);
"""
)
# 2.4 — conditions combinées (ET/OU) : un nœud Condition peut tester
# PLUSIEURS champs à la fois plutôt qu'un seul. cond_clauses (JSON, une
# liste de clauses en plus de la clause "historique" cond_definition_id/
# cond_field/etc.) + cond_combinator ('et'/'ou') sont ajoutés en ALTER
# TABLE pour ne rien casser sur les jeux déjà créés — un nœud sans
# cond_clauses garde exactement son comportement d'avant (une seule
# comparaison), voir evaluateConditionNode() dans templates/play.html.
cols = {r["name"] for r in conn.execute("PRAGMA table_info(_flow_nodes)").fetchall()}
if "cond_clauses" not in cols:
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_clauses TEXT")
if "cond_combinator" not in cols:
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_combinator TEXT NOT NULL DEFAULT 'et'")
conn.commit()
conn.close()
+8
View File
@@ -0,0 +1,8 @@
import db
def get_flow_node(slug, node_id):
conn = db.connect(slug)
row = conn.execute("SELECT * FROM _flow_nodes WHERE id = ?", (node_id,)).fetchone()
conn.close()
return dict(row) if row else None
+11
View File
@@ -0,0 +1,11 @@
import db
from .ensure_flow_schema import ensure_flow_schema
def list_flow_edges(slug, screen_id):
ensure_flow_schema(slug)
conn = db.connect(slug)
rows = conn.execute("SELECT * FROM _flow_edges WHERE screen_id = ? ORDER BY id", (screen_id,)).fetchall()
conn.close()
return [dict(r) for r in rows]
+11
View File
@@ -0,0 +1,11 @@
import db
from .ensure_flow_schema import ensure_flow_schema
def list_flow_nodes(slug, screen_id):
ensure_flow_schema(slug)
conn = db.connect(slug)
rows = conn.execute("SELECT * FROM _flow_nodes WHERE screen_id = ? ORDER BY id", (screen_id,)).fetchall()
conn.close()
return [dict(r) for r in rows]
+8
View File
@@ -0,0 +1,8 @@
import db
def move_flow_node(slug, node_id, x, y):
conn = db.connect(slug)
conn.execute("UPDATE _flow_nodes SET x = ?, y = ? WHERE id = ?", (x, y, node_id))
conn.commit()
conn.close()