diff --git a/docs/capabilities/gamedev-2d.md b/docs/capabilities/gamedev-2d.md index c0ea8d16..253e219b 100644 --- a/docs/capabilities/gamedev-2d.md +++ b/docs/capabilities/gamedev-2d.md @@ -43,6 +43,7 @@ VFX (ver `reports/0143`). | `comfyui_build_ui_hud_workflow_py_ml` | `(element, *, ui_style="fantasy game UI", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN elemento de interfaz/HUD de juego (botón, marco/panel, barra de vida/maná/XP, icono de UI, cursor, viñeta de menú): txt2img cuadrado + prompt scaffold de UI (`{element}, {ui_style}, game UI element, centered, clean, plain background…`) + LoRA estilo opcional + Rembg (alpha). HUD coherente = mismo `ui_style`/`checkpoint`/`lora` por pieza, varía solo `element`. El texto/label lo pone el motor (negativo empuja a `no text`). Probado e2e en GPU (`reports/0152`). SD1.5. | | `comfyui_build_card_art_workflow_py_ml` | `(subject, *, card_style="fantasy trading card art", checkpoint="juggernaut_xl_v11…", width=512, height=768, hires=True, seed=0, lora=None, …) -> dict` | LA ilustración central de UNA carta coleccionable (TCG): criatura/personaje/hechizo en formato **vertical** de carta (`width dict` | UN enemigo/criatura de juego (goblin, esqueleto, slime, dragón, boss, elemental): figura de **cuerpo entero** centrada, fondo limpio recortable a alpha (`{variant} {creature}, {style}, full body, centered, plain background, game asset…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). `variant` (ice/fire/elite/corrupted…) se antepone a la criatura para generar la familia del MISMO enemigo (misma `creature`/`seed`/`style`, varía solo `variant`); bestiario coherente = mismo `style`/`checkpoint`/`lora`, varía solo `creature`. El negativo empuja a UNA criatura entera sin recorte. Probado e2e en GPU con SD1.5 (`reports/0154`). SD1.5. | +| `comfyui_build_prop_object_workflow_py_ml` | `(prop, *, style="game prop, isometric or side view", checkpoint="dreamshaper_8…", size=512, transparent=True, seed=0, lora=None, …) -> dict` | UN prop/objeto de escenario (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua): objeto inanimado aislado a **escala de escena y perspectiva de juego** (iso/lateral), centrado, fondo limpio recortable a alpha (`{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object…`) → txt2img cuadrado + LoRA estilo opcional + Rembg (alpha). **Objeto de MUNDO**, no icono plano de inventario (≠ `item_icon`, que es para una casilla de UI); este puebla el nivel. Atrezzo coherente = mismo `style`/`checkpoint`/`lora`, varía solo `prop`. El negativo excluye personas/criaturas (objeto inanimado). Probado e2e en GPU con SD1.5 (`reports/0155`). SD1.5. | ## Funciones de post-proceso y puente (`gamedev`, CPU) diff --git a/python/functions/ml/comfyui_build_prop_object_workflow.md b/python/functions/ml/comfyui_build_prop_object_workflow.md new file mode 100644 index 00000000..e52daee9 --- /dev/null +++ b/python/functions/ml/comfyui_build_prop_object_workflow.md @@ -0,0 +1,112 @@ +--- +name: comfyui_build_prop_object_workflow +kind: function +lang: py +domain: ml +version: "1.0.0" +purity: pure +signature: "def comfyui_build_prop_object_workflow(prop: str, *, style: str = \"game prop, isometric or side view\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, transparent: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, rembg_model: str = \"u2net\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"prop_object\") -> dict" +description: "Construye el dict (API format) del workflow de UN prop/objeto de escenario de juego 2D (barril, cofre, antorcha, planta, mueble, roca, fuente, estatua, decoracion): UN objeto inanimado aislado a escala de escena y perspectiva de juego (isometrica/lateral), centrado, fondo limpio uniforme recortable a alpha, estilo consistente para poblar un nivel. Diferenciado de comfyui_build_item_icon (objeto de MUNDO vs icono plano de inventario). Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (fondo transparente si transparent). Hermano de comfyui_build_enemy_creature/item_icon/ui_hud_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info." +tags: [comfyui, ml, gamedev, gamedev-2d, prop, object, scenery, environment, rembg, workflow] +uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_inject_lora_py_ml] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: prop + desc: "Descripcion del objeto de escenario (ej. 'wooden barrel', 'treasure chest', 'lit torch', 'potted plant', 'stone fountain', 'wooden table', 'mossy rock'). Se inserta en un prompt scaffold de prop. No puede estar vacio." + - name: style + desc: "Descriptor de estilo/perspectiva que mantiene consistentes los props del set y los situa en el MUNDO, no en la UI (ej. 'game prop, isometric or side view', 'top-down RPG prop', 'side-scroller platformer prop', 'low poly stylized prop'). Pasa el MISMO style + checkpoint + lora a todos los props del nivel para coherencia visual. keyword-only." + - name: checkpoint + desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' para SDXL (mas VRAM, subir size). keyword-only." + - name: size + desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto. keyword-only." + - name: transparent + desc: "Si True inyecta Image Rembg y el PNG sale con alpha (fondo recortado, listo para soltar sobre el tilemap). False = objeto opaco sobre fondo plano, recortable luego por el caller. keyword-only." + - name: seed + desc: "Semilla del KSampler. Misma seed + mismo prop/style -> mismo objeto. keyword-only." + - name: lora + desc: "LoRA de estilo opcional en models/loras (ej. 'isometric_game_assets_sd15.safetensors', 'stylized_props_xl.safetensors'). None = sin LoRA. keyword-only." + - name: lora_strength + desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only." + - name: rembg_model + desc: "Modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo se usa si transparent=True. keyword-only." + - name: negative + desc: "Prompt negativo. None usa el negativo por defecto pensado para props (un objeto inanimado entero, sin personas/criaturas, fondo limpio, sin texto/recorte). keyword-only." + - name: steps + desc: "Pasos del KSampler. keyword-only." + - name: cfg + desc: "CFG del KSampler. keyword-only." + - name: sampler_name + desc: "Sampler del KSampler. keyword-only." + - name: scheduler + desc: "Scheduler del KSampler. keyword-only." + - name: filename_prefix + desc: "Prefijo del PNG en output/. keyword-only." +output: "dict en API format listo para comfyui_submit_workflow: base txt2img cuadrada con prompt scaffold de prop ('{prop}, {style}, game asset, single object, centered, plain background, scene prop, world object, ...') + LoRA de estilo opcional + Image Rembg (si transparent). UN objeto; para poblar un nivel -> llamar por cada prop con mismo style/checkpoint/lora; contact-sheet del atrezzo -> montar los PNG con comfyui_build_grid." +tested: true +tests: ["golden transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; prop + 'single object' + 'centered' + 'game asset' + 'plain background' en prompt; SaveImage <- Rembg; transparency True", "edge transparent=False: sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge prop al inicio del scaffold", "edge style de mundo en prompt", "edge objeto de mundo (scene prop/world object) y negativo anti-persona/criatura (diferenciado de item_icon)", "edge lora: LoraLoader presente con strength", "edge transparent default True", "error prop vacio -> ValueError", "determinismo"] +test_file_path: "python/functions/ml/comfyui_build_prop_object_workflow_test.py" +file_path: "python/functions/ml/comfyui_build_prop_object_workflow.py" +--- + +## Ejemplo + +```python +import sys, os +sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions")) +from ml.comfyui_build_prop_object_workflow import comfyui_build_prop_object_workflow + +# Un prop de escenario con fondo transparente (alpha), listo para soltar en el nivel. +wf = comfyui_build_prop_object_workflow( + "wooden treasure chest", + style="game prop, isometric or side view", + transparent=True, + seed=7, +) +# Poblar un nivel: variar `prop` con el MISMO style/checkpoint/(lora) para coherencia. +# for p in ["wooden barrel", "treasure chest", "lit torch", "potted plant", "mossy rock"]: +# wf = comfyui_build_prop_object_workflow(p, style="game prop, isometric or side view", seed=7) +# comfyui_submit_workflow(wf) # -> comfyui_wait_result -> comfyui_fetch_output_image +# Contact-sheet del atrezzo: montar los PNG resultantes con comfyui_build_grid. +``` + +O lanzable directo con: `./fn run comfyui_build_prop_object_workflow` (imprime nodos + class_types del ejemplo). + +## Cuando usarla + +Cuando necesites atrezzo/decorado de escenario para un juego (RPG, plataformas, +top-down, isometrico): barriles, cofres, antorchas, plantas, muebles, rocas, +fuentes, estatuas, decoracion. A diferencia de `comfyui_build_item_icon_workflow` +(icono PLANO de inventario para una casilla de UI), aqui el objeto es de MUNDO: +conserva la perspectiva del juego (isometrica/lateral) y la escala de la escena para +encajar con los tiles. Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos los +props del nivel para que combinen visualmente; varia solo `prop`. `transparent` +recorta el fondo (alpha) listo para soltar sobre el tilemap. Para un atlas/contact- +sheet del atrezzo, genera cada prop y monta los PNG con `comfyui_build_grid`. + +## Gotchas + +- **Prop (objeto de mundo) != item icon (icono de inventario)**: si lo que quieres es + un icono cuadrado plano para una casilla de UI, usa + `comfyui_build_item_icon_workflow`. Este builder situa el objeto en la ESCENA + (perspectiva isometrica/lateral, escala de mundo) para poblar el nivel. +- **El recorte usa Rembg, NO luma-to-alpha**: un prop es un objeto solido con silueta + definida, rembg lo recorta limpio. `comfyui_matting_luma_to_alpha` es para + translucidos sobre negro (humo/fuego/magia). Si el prop es etereo o translucido + (cristal magico, llama suelta), pon `transparent=False` y recorta con luma-to-alpha + en un paso aparte. +- **Coherencia del set = mismos parametros**: si cambias `style`/`checkpoint`/`lora`/ + `seed` entre props, el atrezzo deja de combinar. Fija esos y varia solo `prop`. +- **SDXL pide mas VRAM y resolucion**: con `checkpoint="juggernaut_xl_v11.safetensors"` + sube `size` a 768/1024; con dreamshaper_8 (SD1.5) deja 512 (holgado en 8GB lowvram). + Si hay OOM, baja `size` o usa SD1.5. +- Si el modelo mete una persona/criatura o varios objetos, el negativo por defecto ya + empuja a "single object / no characters / no cropped"; refuerza `style` con + "isolated object, single item, no people" si insiste. +- `transparent=False` deja el objeto opaco sobre fondo plano: util si prefieres + recortar fuera del workflow o el motor compone sobre un fondo solido. +- Es una funcion **pura**: solo arma el dict. La generacion real (GPU) la hacen + `comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`. diff --git a/python/functions/ml/comfyui_build_prop_object_workflow.py b/python/functions/ml/comfyui_build_prop_object_workflow.py new file mode 100644 index 00000000..7d4c06cc --- /dev/null +++ b/python/functions/ml/comfyui_build_prop_object_workflow.py @@ -0,0 +1,235 @@ +"""Construye el workflow ComfyUI de UN prop/objeto de escenario de juego (API format). + +Prop/objeto de mundo (barril, cofre, antorcha, planta, mueble, roca, fuente, +estatua, decoracion...): UN objeto inanimado aislado, a escala de escena y con la +perspectiva del juego (isometrica o lateral), centrado, fondo limpio y uniforme +recortable a alpha, estilo consistente para poblar un nivel. Es el builder hermano +de comfyui_build_item_icon_workflow / comfyui_build_enemy_creature_workflow / +comfyui_build_ui_hud_workflow: mismo patron (PURO, dict API format) que compone +funciones existentes del registry, no reescribe el grafo. + +Diferencia con item_icon (clave para no duplicar): un *item icon* es un icono PLANO +de inventario (encuadre cuadrado, sujeto presentado de frente, pensado para una +casilla de UI). Un *prop* es un objeto de MUNDO: se coloca en el escenario, conserva +la perspectiva del juego (isometrica/lateral) y la escala de la escena para encajar +con los tiles y el resto de decorado. Por eso el style por defecto empuja +"game prop, isometric or side view, scene prop" en vez de "game icon, clean". + +Cableado: + + CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler + -> CLIPTextEncode (prompt scaffold de prop) ... + -> VAEDecode -> [Image Rembg opcional] -> SaveImage + +Compone: + - comfyui_build_txt2img_workflow -> base txt2img cuadrada + - comfyui_inject_lora -> LoRA de estilo opcional (consistencia del set) + - 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha) + +Por que Rembg y NO comfyui_matting_luma_to_alpha: un prop es un objeto SOLIDO con +silueta definida (barril, cofre, mueble); rembg recorta limpio la silueta dejando +alpha, listo para soltar sobre el tilemap. La luma-to-alpha es para translucidos +sobre negro (humo/fuego/magia), donde aplanaria el objeto. Si el prop es etereo o +translucido (cristal magico, llama de antorcha sola) y se quiere conservar la +translucidez, recortar fuera del workflow (transparent=False) y componer con +luma-to-alpha en un paso aparte. Para el atrezzo tipico (barril, cofre, roca, +planta) rembg es lo correcto. + +Por que un solo objeto centrado y fondo plano: un prop se inserta como sprite/objeto +suelto en el motor; el scaffold empuja a "single object, centered, plain background, +game asset" y el negativo por defecto rechaza "person, character, creature, multiple +objects, cropped, out of frame" para mantener UN objeto entero, inanimado y +recortable. + +class_types/inputs verificados contra /object_info del servidor (8GB lowvram): +CheckpointLoaderSimple, CLIPTextEncode, EmptyLatentImage, KSampler, VAEDecode, +SaveImage, LoraLoader, 'Image Rembg (Remove Background)' (transparency BOOLEAN). + +Funcion pura: sin red, sin I/O. No muta dicts de entrada (copia profunda en el +helper de rembg). Determinista para los mismos argumentos. +""" +from __future__ import annotations + +import copy +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +# Negativo por defecto pensado para props/objetos de escenario: UN objeto inanimado +# entero, fondo limpio, sin personas/criaturas, sin texto/marcas ni recortes. No +# filtra ningun tipo de objeto (barril, cofre, planta, roca, fuente... son validos). +_PROP_NEGATIVE = ( + "person, people, character, creature, animal, hands, face, " + "multiple objects, cluttered, blurry, lowres, deformed, bad perspective, " + "text, watermark, signature, logo, photo, photorealistic, " + "cropped, cut off, out of frame, jpeg artifacts" +) + + +def _inject_rembg(workflow: dict, model: str) -> dict: + """Inserta 'Image Rembg (Remove Background)' (transparency=True) entre VAEDecode y SaveImage. + + Mismo helper que usan comfyui_build_item_icon_workflow / enemy_creature: el nodo + recorta la silueta del objeto dejando alpha. Repunta SaveImage.images a la salida + del Rembg. + """ + wf = copy.deepcopy(workflow) + vaedecode_id = next( + (nid for nid, n in wf.items() if n.get("class_type") == "VAEDecode"), None + ) + save_id = next((nid for nid, n in wf.items() if n.get("class_type") == "SaveImage"), None) + if vaedecode_id is None or save_id is None: + raise ValueError( + "comfyui_build_prop_object_workflow: no se encontro VAEDecode/SaveImage para Rembg" + ) + numeric = [int(k) for k in wf.keys() if str(k).isdigit()] + rembg_id = str((max(numeric) + 1) if numeric else len(wf) + 1) + wf[rembg_id] = { + "class_type": "Image Rembg (Remove Background)", + "inputs": { + "images": [vaedecode_id, 0], + "transparency": True, + "model": model, + "post_processing": False, + "only_mask": False, + "alpha_matting": False, + "alpha_matting_foreground_threshold": 240, + "alpha_matting_background_threshold": 10, + "alpha_matting_erode_size": 10, + "background_color": "none", + }, + } + wf[save_id]["inputs"]["images"] = [rembg_id, 0] + return wf + + +def comfyui_build_prop_object_workflow( + prop: str, + *, + style: str = "game prop, isometric or side view", + checkpoint: str = "dreamshaper_8.safetensors", + size: int = 512, + transparent: bool = True, + seed: int = 0, + lora: str | None = None, + lora_strength: float = 1.0, + rembg_model: str = "u2net", + negative: str | None = None, + steps: int = 28, + cfg: float = 7.0, + sampler_name: str = "dpmpp_2m", + scheduler: str = "karras", + filename_prefix: str = "prop_object", +) -> dict: + """Construye el dict (API format) del workflow de un prop/objeto de escenario. + + Args: + prop: descripcion del objeto de escenario (ej. "wooden barrel", + "treasure chest", "lit torch", "potted plant", "stone fountain", + "wooden table", "mossy rock"). Se inserta en un prompt scaffold de prop. + No puede estar vacio. + style: descriptor de estilo/perspectiva que mantiene consistentes los props + del set y los situa en el MUNDO, no en la UI (ej. "game prop, isometric + or side view", "top-down RPG prop", "side-scroller platformer prop", + "low poly stylized prop"). Pasa el MISMO style + checkpoint + (lora) a + todos los props del nivel para coherencia visual. keyword-only. + checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, + holgado en 8GB lowvram) por defecto; 'juggernaut_xl_v11.safetensors' + para SDXL (mas VRAM, subir size a 768/1024). keyword-only. + size: lado del cuadrado en px (width = height = size). 512 SD1.5 por + defecto. keyword-only. + transparent: si True inyecta Image Rembg y el PNG sale con alpha (fondo + recortado, listo para soltar sobre el tilemap). Si False deja el objeto + opaco sobre fondo plano, recortable luego por el caller/pipeline. + keyword-only. + seed: semilla del KSampler. Misma seed + mismo prop/style -> mismo objeto. + keyword-only. + lora: LoRA de estilo opcional en models/loras (ej. + 'isometric_game_assets_sd15.safetensors', 'stylized_props_xl.safetensors'). + None = sin LoRA. keyword-only. + lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. + keyword-only. + rembg_model: modelo Rembg ('u2net' general, 'isnet-anime' para anime). Solo + se usa si transparent=True. keyword-only. + negative: prompt negativo. None usa el negativo por defecto pensado para + props (un objeto inanimado entero, sin personas/criaturas, fondo limpio, + sin texto/recorte). keyword-only. + steps, cfg, sampler_name, scheduler, filename_prefix: parametros de + generacion. keyword-only. + + Returns: + dict en API format listo para comfyui_submit_workflow: txt2img base cuadrada + con prompt scaffold de prop ('{prop}, {style}, game asset, single object, + centered, plain background, ...') + LoRA de estilo opcional + Image Rembg (si + transparent). Es UN objeto; para poblar un nivel -> llamar por cada prop con + el mismo style/checkpoint/(lora). Montar el set con comfyui_build_grid si se + quiere un contact-sheet del atrezzo. + + Raises: + ValueError: si prop esta vacio, o si la base no tiene VAEDecode/SaveImage + donde inyectar el Rembg (propagado por el helper). + """ + from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow + + if not prop or not prop.strip(): + raise ValueError( + "comfyui_build_prop_object_workflow: 'prop' no puede estar vacio" + ) + + prop = prop.strip() + lora_strength = max(0.0, min(2.0, float(lora_strength))) + neg = _PROP_NEGATIVE if negative is None else negative + + # Prompt scaffold de prop: un objeto de mundo entero, centrado, fondo plano, + # listo como asset de juego suelto (sprite/objeto) recortable. + positive = ( + f"{prop}, {style}, game asset, single object, centered, plain background, " + "scene prop, world object, no characters, high detail" + ) + + wf = comfyui_build_txt2img_workflow( + checkpoint, + positive, + neg, + steps=steps, + cfg=cfg, + width=size, + height=size, + seed=seed, + sampler_name=sampler_name, + scheduler=scheduler, + filename_prefix=filename_prefix, + ) + + if lora: + from ml.comfyui_inject_lora import comfyui_inject_lora + + wf = comfyui_inject_lora( + wf, lora, strength_model=lora_strength, strength_clip=lora_strength + ) + + if transparent: + wf = _inject_rembg(wf, rembg_model) + + return wf + + +if __name__ == "__main__": + import json + + wf = comfyui_build_prop_object_workflow( + "wooden treasure chest", + style="game prop, isometric or side view", + transparent=True, + seed=7, + ) + print( + json.dumps( + { + "nodes": list(wf), + "classes": sorted({n["class_type"] for n in wf.values()}), + }, + indent=2, + ) + ) diff --git a/python/functions/ml/comfyui_build_prop_object_workflow_test.py b/python/functions/ml/comfyui_build_prop_object_workflow_test.py new file mode 100644 index 00000000..97980ad4 --- /dev/null +++ b/python/functions/ml/comfyui_build_prop_object_workflow_test.py @@ -0,0 +1,135 @@ +"""Tests offline de comfyui_build_prop_object_workflow (estructura del dict, sin GPU).""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from ml.comfyui_build_prop_object_workflow import ( # noqa: E402 + comfyui_build_prop_object_workflow, +) + + +def _classes(wf): + return sorted({n["class_type"] for n in wf.values()}) + + +def _by_class(wf, cls): + return [n for n in wf.values() if n["class_type"] == cls] + + +def _id_of(wf, cls): + return next(nid for nid, n in wf.items() if n["class_type"] == cls) + + +def _pos_with(wf, needle): + return next( + n for n in wf.values() + if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"] + ) + + +def test_golden_transparent_recipe(): + wf = comfyui_build_prop_object_workflow( + "wooden treasure chest", transparent=True, seed=7 + ) + cls = _classes(wf) + # Cadena base txt2img + Rembg para alpha. + assert "CheckpointLoaderSimple" in cls + assert "KSampler" in cls + assert "VAEDecode" in cls + assert "SaveImage" in cls + assert "Image Rembg (Remove Background)" in cls + # El prop aparece en el prompt positivo con el scaffold de objeto de mundo. + pos = _pos_with(wf, "wooden treasure chest") + txt = pos["inputs"]["text"] + assert "single object" in txt + assert "centered" in txt + assert "game asset" in txt + assert "plain background" in txt + # SaveImage toma la imagen del Rembg (no del VAEDecode). + rembg_id = _id_of(wf, "Image Rembg (Remove Background)") + save = next(n for n in wf.values() if n["class_type"] == "SaveImage") + assert save["inputs"]["images"] == [rembg_id, 0] + assert _by_class(wf, "Image Rembg (Remove Background)")[0]["inputs"]["transparency"] is True + + +def test_edge_opaque_no_rembg(): + wf = comfyui_build_prop_object_workflow("stone fountain", transparent=False) + assert "Image Rembg (Remove Background)" not in _classes(wf) + # SaveImage toma del VAEDecode directamente. + vd_id = _id_of(wf, "VAEDecode") + save = next(n for n in wf.values() if n["class_type"] == "SaveImage") + assert save["inputs"]["images"] == [vd_id, 0] + + +def test_edge_size_reflected(): + wf = comfyui_build_prop_object_workflow("mossy rock", size=768) + latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"] + assert latent["width"] == 768 + assert latent["height"] == 768 # cuadrado + + +def test_edge_prop_at_start(): + # El scaffold arranca directamente con el prop. + wf = comfyui_build_prop_object_workflow("potted plant", transparent=False) + pos = _pos_with(wf, "potted plant") + assert pos["inputs"]["text"].startswith("potted plant") + + +def test_edge_style_in_prompt(): + # Estilo de MUNDO reflejado (diferenciado del icono plano de inventario). + wf = comfyui_build_prop_object_workflow( + "wooden barrel", style="top-down RPG prop", transparent=False + ) + pos = _pos_with(wf, "wooden barrel") + assert "top-down RPG prop" in pos["inputs"]["text"] + + +def test_edge_world_object_not_inventory_icon(): + # Por defecto el scaffold lo trata como objeto de mundo (scene prop), no icono UI. + wf = comfyui_build_prop_object_workflow("lit torch", transparent=False) + txt = _pos_with(wf, "lit torch")["inputs"]["text"] + assert "scene prop" in txt + assert "world object" in txt + # El negativo por defecto excluye personas/criaturas (objeto inanimado). + neg = next( + n for n in wf.values() + if n["class_type"] == "CLIPTextEncode" and "person" in n["inputs"]["text"] + ) + assert "creature" in neg["inputs"]["text"] + + +def test_edge_lora_reflected(): + wf = comfyui_build_prop_object_workflow( + "ornate vase", + lora="isometric_game_assets_sd15.safetensors", + lora_strength=0.9, + ) + loras = _by_class(wf, "LoraLoader") + assert len(loras) == 1 + assert loras[0]["inputs"]["lora_name"] == "isometric_game_assets_sd15.safetensors" + assert loras[0]["inputs"]["strength_model"] == 0.9 + + +def test_edge_transparent_default_true(): + # transparent por defecto True -> Rembg presente sin pasar el flag. + wf = comfyui_build_prop_object_workflow("treasure chest") + assert "Image Rembg (Remove Background)" in _classes(wf) + + +def test_error_empty_prop(): + try: + comfyui_build_prop_object_workflow(" ") + assert False + except ValueError as e: + assert "prop" in str(e) + + +def test_determinism(): + a = comfyui_build_prop_object_workflow( + "wooden barrel", lora="stylized_props_xl.safetensors", seed=7 + ) + b = comfyui_build_prop_object_workflow( + "wooden barrel", lora="stylized_props_xl.safetensors", seed=7 + ) + assert a == b