Files
Forge-Engine/routes/scenes/scene_object_add.py
T
williamandClaude Sonnet 5 559331f9cf
Build and deploy / test-python (push) Successful in 10m59s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance,
  video, son, visibilite, indication, attendre comme actions, utilisables
  aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par
  Ruby (IA), avec blocs deplacables/supprimables dans une chaine.
- Corrige plusieurs variantes du bug "impossible de poser un objet hors
  du champ de la camera" (troncature du chainage d'actions a 4 maillons,
  fond importe pose a 128x128 au lieu de sa taille reelle, decalage du
  fond au vrai glisser-depose, redimensionnement manuel jamais propage
  au monde).
- Ajoute un vrai glisser-depose depuis la galerie vers la scene, la
  gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/
  Videos, suppression, reclassement fond<->decor sans re-upload).
- Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement,
  limite 5 min), avec validation de la duree reelle du fichier, et une
  replique audio optionnelle dans une bulle de dialogue.
- Fixe la taille de pose d'un objet/decor importe a 200x200 avec une
  boite de collision de 150x150.
- Filtre le selecteur de fichier des actions son/video par type reel.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-12 00:28:46 +02:00

115 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from flask import g, request, redirect, url_for, jsonify, abort
import auth
import screens
from core.flask_app import app
from core.sprite_gate import forbid_admin_only_character, forbid_admin_only_background
@app.route("/game/<slug>/screens/<int:screen_id>/scene-objects/add", methods=["POST"])
def scene_object_add(slug, screen_id):
"""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)
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)
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/<slug>/screens/<id>/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))