From 43abfc9b933e1be88215621610b4ba394ace8488 Mon Sep 17 00:00:00 2001 From: william Date: Mon, 31 Aug 2026 10:22:58 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20=C3=A9diteur=20de=20sc=C3=A8ne=202D=20:?= =?UTF-8?q?=20d=C3=A9calage=20visuel=20des=20objets=20+=20crash=20Timeline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux bugs remontés en test manuel sur l'éditeur de scène (Phase A, commit f0070fa) : 1. Décalage visuel à l'ajout d'un objet de scène (personnage/décor) : render_scene_object.py positionne son en absolu (left/top/ width/height en px), pensé pour être un enfant DIRECT de .playScreen.playScene en mode jouable. Dans scene_edit.html, la même balise est nichée dans .canvasElementInner, lui-même déjà positionné par .canvasElement — l'image se repositionnait donc EN PLUS depuis #canvas (son ancêtre positionné le plus proche), en double du décalage déjà appliqué par le conteneur. Corrigé par une règle CSS scoped à l'éditeur (.canvasElementInner > img) qui neutralise le positionnement propre de l'image et la fait simplement remplir son conteneur. 2. "FOREIGN KEY constraint failed" à l'ajout d'un clip de Timeline sur un objet de scène : _animation_clips.element_id portait une VRAIE contrainte FK vers _screen_elements depuis la création de la table. animation-timeline.js est réutilisé TEL QUEL entre les deux éditeurs (voir le plan "Fondations d'une plateforme multi-éditeurs") et n'opère aucune distinction — pour un jeu jeu_2d, element_id désigne en réalité un id de _scene_objects, absent de _screen_elements, d'où l'échec de l'INSERT sous PRAGMA foreign_keys=ON. ensure_animation_schema reconstruit maintenant la table (une fois, migration automatique à la volée comme le reste du schéma) sans cette contrainte FK — même patron que trigger_element_id/cond_element_a dans ensure_flow_schema.py. Comme la suppression en cascade reposait jusqu'ici sur cette FK, un nettoyage manuel des clips a été ajouté à la suppression d'un élément (delete_element.py) et d'un objet de scène (delete_scene_object.py). Nouveaux tests (tests/test_scene_edit_view.py, +4 cas) : ajout d'un clip de Timeline sur un objet de scène via la route, nettoyage des clips à la suppression d'un objet de scène et d'un élément DOM. Suite complète : 312 tests passent (aucune régression). Co-Authored-By: Claude Sonnet 5 --- screens/animations/ensure_animation_schema.py | 72 ++++++++++++++----- screens/elements/delete_element.py | 6 ++ screens/scenes/delete_scene_object.py | 7 +- templates/scene_edit.html | 11 +++ tests/test_scene_edit_view.py | 39 ++++++++++ 5 files changed, 115 insertions(+), 20 deletions(-) diff --git a/screens/animations/ensure_animation_schema.py b/screens/animations/ensure_animation_schema.py index 10b5bb80..a083b406 100644 --- a/screens/animations/ensure_animation_schema.py +++ b/screens/animations/ensure_animation_schema.py @@ -1,29 +1,63 @@ import db +_CREATE_TABLE = """ + CREATE TABLE _animation_clips ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE, + element_id INTEGER NOT NULL, + kind TEXT NOT NULL DEFAULT 'animate_css', + animate_name TEXT, + custom_keyframes TEXT, + start_time REAL NOT NULL DEFAULT 0, + duration REAL NOT NULL DEFAULT 1, + delay REAL NOT NULL DEFAULT 0, + easing TEXT NOT NULL DEFAULT 'ease', + iteration_count TEXT NOT NULL DEFAULT '1', + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + """ + def ensure_animation_schema(slug): """Crée la table des clips d'animation si elle n'existe pas encore — même principe que ensure_flow_schema.py : un jeu créé avant l'ajout de cette fonctionnalité n'a pas besoin d'être migré à la main, la première - ouverture du panneau timeline suffit.""" + ouverture du panneau timeline suffit. + + element_id désigne, selon game_type, un _screen_elements (éditeur + document) OU un _scene_objects (éditeur jeu_2d) — animation-timeline.js + est réutilisé TEL QUEL entre les deux éditeurs (voir le plan "Fondations + d'une plateforme multi-éditeurs") et ne fait aucune distinction. Pas de + contrainte FK vers _screen_elements : une scène jeu_2d n'a pas de ligne + correspondante dans cette table, ce qui ferait échouer l'insertion avec + PRAGMA foreign_keys=ON (voir db/connection.py). Le nettoyage à la + suppression d'un élément/objet est fait manuellement (voir + screens/elements/delete_element.py et screens/scenes/delete_scene_object.py), + même patron que trigger_element_id/cond_element_a dans + ensure_flow_schema.py.""" conn = db.connect(slug) - conn.executescript( - """ - CREATE TABLE IF NOT EXISTS _animation_clips ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - screen_id INTEGER NOT NULL REFERENCES _screens(id) ON DELETE CASCADE, - element_id INTEGER NOT NULL REFERENCES _screen_elements(id) ON DELETE CASCADE, - kind TEXT NOT NULL DEFAULT 'animate_css', - animate_name TEXT, - custom_keyframes TEXT, - start_time REAL NOT NULL DEFAULT 0, - duration REAL NOT NULL DEFAULT 1, - delay REAL NOT NULL DEFAULT 0, - easing TEXT NOT NULL DEFAULT 'ease', - iteration_count TEXT NOT NULL DEFAULT '1', - created_at TEXT DEFAULT CURRENT_TIMESTAMP - ); - """ - ) + cols = {r["name"]: r for r in conn.execute("PRAGMA table_info(_animation_clips)").fetchall()} + if not cols: + conn.executescript(_CREATE_TABLE) + elif cols["element_id"]["notnull"] and _has_fk_on_element_id(conn): + conn.executescript( + _CREATE_TABLE.replace("CREATE TABLE _animation_clips", "CREATE TABLE _animation_clips_new") + ) + conn.execute( + """INSERT INTO _animation_clips_new + (id, screen_id, element_id, kind, animate_name, custom_keyframes, + start_time, duration, delay, easing, iteration_count, created_at) + SELECT id, screen_id, element_id, kind, animate_name, custom_keyframes, + start_time, duration, delay, easing, iteration_count, created_at + FROM _animation_clips""" + ) + conn.execute("DROP TABLE _animation_clips") + conn.execute("ALTER TABLE _animation_clips_new RENAME TO _animation_clips") conn.commit() conn.close() + + +def _has_fk_on_element_id(conn): + return any( + fk["from"] == "element_id" for fk in conn.execute("PRAGMA foreign_key_list(_animation_clips)").fetchall() + ) diff --git a/screens/elements/delete_element.py b/screens/elements/delete_element.py index 8a769a87..c187d7da 100644 --- a/screens/elements/delete_element.py +++ b/screens/elements/delete_element.py @@ -1,11 +1,13 @@ import db +from ..animations.ensure_animation_schema import ensure_animation_schema from ..flow.ensure_flow_schema import ensure_flow_schema from .element_descendant_ids import element_descendant_ids def delete_element(slug, element_id): ensure_flow_schema(slug) + ensure_animation_schema(slug) conn = db.connect(slug) row = conn.execute("SELECT screen_id FROM _screen_elements WHERE id = ?", (element_id,)).fetchone() if not row: @@ -39,6 +41,10 @@ def delete_element(slug, element_id): ids + ids + ids + ids, ) conn.execute(f"DELETE FROM _actions WHERE target_element_id IN ({placeholders})", ids) + # _animation_clips.element_id n'a plus de contrainte FK (voir + # ensure_animation_schema.py — réutilisé aussi par les objets de scène + # jeu_2d) : nettoyage manuel, même raison que ci-dessus. + conn.execute(f"DELETE FROM _animation_clips WHERE element_id IN ({placeholders})", ids) conn.execute("DELETE FROM _screen_elements WHERE id = ?", (element_id,)) conn.commit() conn.close() diff --git a/screens/scenes/delete_scene_object.py b/screens/scenes/delete_scene_object.py index 14ae886e..5965724e 100644 --- a/screens/scenes/delete_scene_object.py +++ b/screens/scenes/delete_scene_object.py @@ -1,6 +1,7 @@ import db from .ensure_scene_schema import ensure_scene_schema +from ..animations.ensure_animation_schema import ensure_animation_schema from ..flow.ensure_flow_schema import ensure_flow_schema @@ -10,15 +11,19 @@ def delete_scene_object(slug, object_id): cond_element_b — aucun n'a de contrainte FK, voir ensure_flow_schema.py) : un nœud qui vise un objet supprimé n'a plus de sens, même patron que screens/elements/delete_element.py pour le - DOM.""" + DOM. Nettoie aussi ses clips d'animation (_animation_clips.element_id, + voir ensure_animation_schema.py — réutilisé tel quel par + animation-timeline.js pour les deux éditeurs, sans contrainte FK).""" ensure_scene_schema(slug) ensure_flow_schema(slug) + ensure_animation_schema(slug) conn = db.connect(slug) conn.execute( """DELETE FROM _flow_nodes WHERE trigger_object_id = ? OR target_object_id = ? OR cond_element_a = ? OR cond_element_b = ?""", (object_id, object_id, object_id, object_id), ) + conn.execute("DELETE FROM _animation_clips WHERE element_id = ?", (object_id,)) conn.execute("DELETE FROM _scene_objects WHERE id = ?", (object_id,)) conn.commit() conn.close() diff --git a/templates/scene_edit.html b/templates/scene_edit.html index 1a546673..b1265792 100644 --- a/templates/scene_edit.html +++ b/templates/scene_edit.html @@ -5,6 +5,17 @@ {% block extra_head %} + {% endblock %} {% block content %}