from flask import abort, g, jsonify, redirect, request, url_for from werkzeug.wrappers import Response import auth import db import screens from core.flask_app import app from core.sprite_gate import forbid_admin_only_background, forbid_admin_only_character @app.route("/game//screens//scene-objects/add", methods=["POST"]) def scene_object_add(slug: str, screen_id: int) -> Response: """Pose un nouvel objet sur la scène — mirror de routes/elements/ element_add.py, mais pour _scene_objects (voir screens/scenes/ add_scene_object.py). Appelée en AJAX (voir addSceneObject(), static/js/scenes/scene-editor.js) plutôt qu'en soumission de formulaire classique — sans ça, poser un objet rechargeait TOUTE la page : perceptible comme un "vidage" de l'éditeur de collision/quêtes le temps du rechargement, et perdait tout état JS en cours (modale ouverte, etc.). Répond en JSON avec de quoi ajouter l'objet au canevas SANS recharger — jamais sélectionné/ouvert dans le panneau de propriétés pour autant (ça, voir onSceneObjectMouseDown, reste un rechargement pour l'instant). La liste "🧩 Collision" n'est PAS incluse ici : addSceneObject() la redemande ensuite à routes/scenes/scene_collision_rule_objects.py (même endpoint que pour un changement de rôle/nom, ou une suppression) plutôt que de dupliquer ici la logique de build_collision_rule_objects().""" kind = request.form.get("kind", "personnage") forge_character = request.form.get("forge_character") background_slug = request.form.get("background_slug") asset_id = request.form.get("asset_id", type=int) # Vrai glisser-déposer depuis la galerie (demande explicite : "n'importe # où sur la scène", voir static/js/scenes/scene-editor.js::onCanvasDrop) # — position EXACTE du dépôt, en pixels "monde" déjà calculés côté # client (pas de zoom sur #canvas, voir onCanvasDrop). Absents pour un # simple clic sur la galerie (comportement par défaut inchangé). drop_x = request.form.get("x", type=float) drop_y = request.form.get("y", type=float) if forge_character: forbid_admin_only_character(forge_character) if background_slug: forbid_admin_only_background(background_slug) image_url = None image_width = image_height = None if asset_id: # "Mes assets" (voir auth/user_assets_dir.py) : isolation stricte # par propriétaire, même esprit que les projets (core/auth_guard.py) # — jamais poser l'asset d'un autre compte, même en devinant son id. asset = auth.get_user_asset(asset_id) if not asset or asset["user_id"] != g.current_user["id"]: abort(403) image_url = url_for("serve_user_asset", user_id=asset["user_id"], filename=asset["filename"]) if kind == "fond": # Sans ça, un "fond" importé (contrairement à un fond du # catalogue Forge, voir background_library.py) retombait # TOUJOURS sur 128×128 (les valeurs par défaut du schéma) — # trop petit pour agrandir le "monde" au-delà du champ de la # caméra, rendant impossible d'y poser un objet en dehors de # ce champ (voir screens.resolve_scene_world_size). Un "decor" # n'en a pas besoin (demande explicite : toujours 200×200 à la # pose, voir add_scene_object.py::_DECOR_DEFAULT_SIZE). import os path = os.path.join(auth.user_assets_dir(asset["user_id"]), asset["filename"]) try: with open(path, "rb") as fh: dims = auth.image_dimensions(fh.read()) except OSError: dims = None if dims: image_width, image_height = dims # Un "fond" reste TOUJOURS ancré au coin haut-gauche du monde (0,0), # jamais à la position du dépôt (bug corrigé : "décalage" — le vrai # glisser-déposer envoie une position EXACTE même pour un fond, ce qui # le décalait du coin de la scène et laissait un bandeau de canevas vide # visible sur le dessus/le côté). Un "decor"/"personnage" garde lui sa # position de dépôt normale. new_id = screens.add_scene_object( slug, screen_id, kind=kind, forge_character=forge_character, background_slug=background_slug, image_url=image_url, image_width=image_width, image_height=image_height, x=(None if kind == "fond" else drop_x), y=(None if kind == "fond" else drop_y), ) if drop_x is not None and drop_y is not None and kind != "fond": # Ramène dans le cadre de la caméra si le dépôt était trop proche # d'un bord (l'objet, une fois sa taille réelle connue, pourrait # légèrement déborder) — même esprit que ai/tools.py:: # _clamp_to_camera pour l'IA, ici pour un dépôt manuel. obj_for_clamp = screens.get_scene_object(slug, new_id) screen = screens.get_screen(slug, screen_id) obj_for_clamp = db.assert_not_none( obj_for_clamp, "new_id vient d'etre cree par screens.add_scene_object juste au-dessus" ) screen = db.assert_not_none( screen, "screen_id valide : add_scene_object aurait leve IntegrityError sinon (FK scene_id)" ) max_x = max(0, screen["scene_width"] - obj_for_clamp["width"]) max_y = max(0, screen["scene_height"] - obj_for_clamp["height"]) clamped_x = min(max(drop_x, 0), max_x) clamped_y = min(max(drop_y, 0), max_y) if (clamped_x, clamped_y) != (obj_for_clamp["x"], obj_for_clamp["y"]): screens.update_scene_object_geometry( slug, new_id, clamped_x, clamped_y, obj_for_clamp["width"], obj_for_clamp["height"] ) if request.headers.get("X-Requested-With") == "fetch": obj = screens.get_scene_object(slug, new_id) obj = db.assert_not_none(obj, "new_id vient d'etre cree par screens.add_scene_object plus haut") return jsonify( { "id": new_id, "kind": obj["kind"], "x": obj["x"], "y": obj["y"], "width": obj["width"], "height": obj["height"], "rendered_html": screens.render_scene_object(obj), # Relance l'aperçu animé tout de suite (voir personnage-preview.js # ::restartPersonnageIdlePreview) — sans ça, un personnage # fraîchement posé restait figé sur sa première frame jusqu'au # prochain rechargement de page (qui n'arrive plus jamais, voir # addSceneObject() côté JS). "animations": screens.resolve_personnage_animations(obj) if obj["kind"] == "personnage" else None, } ) # Même route que l'éditeur document (/game//screens//edit, # endpoint "screen_edit") — c'est CE dispatcher qui bascule vers la # vue de scène selon game_type, voir routes/screens/screen_edit.py. # Conservé pour tout appelant non-JS (aucun connu actuellement, mais # ne coûte rien à garder en secours). return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, selected=new_id))