feat(gamedev): comfyui_build_projectile_workflow — proyectiles/balas/hechizos orientados (glow→luma-alpha, sólido→rembg)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 00:41:52 +02:00
parent b88730b7cb
commit 19ad2b3e5d
4 changed files with 530 additions and 0 deletions
@@ -0,0 +1,125 @@
---
name: comfyui_build_projectile_workflow
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_build_projectile_workflow(projectile: str, *, direction: str = \"right\", glow: bool = False, style: str = \"game projectile, 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 = \"projectile\") -> dict"
description: "Construye el dict (API format) del workflow de UN proyectil orientado 2D: flecha, bala, bola de fuego, rayo, misil o hechizo — sprite pequeno con orientacion (horizontal por defecto, apuntando a la derecha), aislado sobre fondo recortable a alpha, listo para instanciar como proyectil en el motor (rotar segun el angulo de disparo). Opcion `direction` (right/left/up/down) y `glow`: glow=False (defecto) -> proyectil SOLIDO con Image Rembg para alpha; glow=True -> proyectil BRILLANTE/magico sobre fondo NEGRO sin Rembg, como insumo de comfyui_matting_luma_to_alpha (luminancia-como-alpha) que el caller aplica luego. Compone comfyui_build_txt2img_workflow + comfyui_inject_lora (estilo opcional) + Image Rembg (si solido y transparent). Hermano de comfyui_build_item_icon/topdown_sprite/vfx_spritesheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info."
tags: [comfyui, ml, gamedev, gamedev-2d, projectile, bullet, fireball, spell, vfx, rembg, luma-to-alpha, 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: projectile
desc: "Nombre del proyectil (ej. 'arrow', 'bullet', 'fireball', 'lightning bolt', 'missile', 'magic spell'). Se inserta en un prompt scaffold de proyectil. No puede estar vacio."
- name: direction
desc: "Orientacion hacia la que apunta el proyectil. 'right' por defecto (apuntando a la derecha, angulo 0 — el convenio del motor que luego rota el sprite), 'left', 'up', 'down' (tambien diagonales como 'up-right'). None/'' = sin orientacion explicita. keyword-only."
- name: glow
desc: "Si True el proyectil es brillante/magico (bola de fuego, rayo, hechizo): scaffold 'glowing, on black background' y NO se inyecta Rembg (recortaria el halo translucido). La imagen queda sobre fondo negro como INSUMO de comfyui_matting_luma_to_alpha. glow=True ignora transparent/rembg_model. False (defecto) = proyectil solido por Rembg. keyword-only."
- name: style
desc: "Descriptor de estilo que mantiene consistentes los proyectiles del set (ej. 'game projectile, side view', 'hand-painted RPG projectile', 'pixel art bullet'). Pasa el MISMO style + checkpoint + lora a todos los proyectiles 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 (y glow=False) inyecta Image Rembg y el PNG sale con alpha (silueta recortada). False = proyectil opaco sobre fondo plano, recortable luego por el caller. Ignorado cuando glow=True. keyword-only."
- name: seed
desc: "Semilla del KSampler. keyword-only."
- name: lora
desc: "LoRA de estilo opcional en models/loras (ej. 'detail_tweaker_sd15.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 y glow=False. keyword-only."
- name: negative
desc: "Prompt negativo. None usa el negativo por defecto pensado para proyectiles (un objeto aislado, sin escenario/personaje, sin texto/foto). No menciona 'black background' para no rechazar el fondo negro que glow necesita. 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 proyectil ('a {projectile}, {style}, pointing {direction}, isolated, single object, {plain background | glowing, on black background}, game asset, ...') + LoRA de estilo opcional. Si glow=False y transparent, ademas Image Rembg para alpha. Si glow=True, la imagen sale sobre fondo negro (insumo de comfyui_matting_luma_to_alpha). UN proyectil; un set -> llamar por proyectil con el mismo style/checkpoint/lora."
tested: true
tests: ["golden solido transparent: clases CheckpointLoaderSimple/KSampler/VAEDecode/SaveImage/Image Rembg; projectile + 'game projectile, side view' + 'pointing right' + 'isolated' + 'plain background' + 'game asset' en prompt; SaveImage <- Rembg; transparency True", "golden glow sobre negro: 'glowing, on black background' en prompt, sin 'plain background', sin Rembg, SaveImage <- VAEDecode", "glow ignora transparent: glow=True + transparent=True no inyecta Rembg", "edge direction reflejada: left/up/down/up-right aparecen como 'pointing {d}'", "edge direction opcional: sin direction no hay 'pointing'", "edge solido opaco: glow=False + transparent=False sin Rembg, SaveImage <- VAEDecode", "edge size: width==height==768 (cuadrado)", "edge style en prompt", "edge lora: LoraLoader presente con strength", "negativo por defecto permite black background (glow lo necesita)", "error projectile vacio -> ValueError", "determinismo"]
test_file_path: "python/functions/ml/comfyui_build_projectile_workflow_test.py"
file_path: "python/functions/ml/comfyui_build_projectile_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_projectile_workflow import comfyui_build_projectile_workflow
# Proyectil brillante (bola de fuego) apuntando a la derecha, sobre fondo negro
# como insumo de luma->alpha. Listo para submit.
wf = comfyui_build_projectile_workflow(
"fireball",
direction="right",
glow=True,
style="game projectile, side view",
seed=42,
)
# comfyui_submit_workflow(wf) -> comfyui_wait_result -> comfyui_fetch_output_image
# -> comfyui_matting_luma_to_alpha (brillante=opaco, negro=transparente)
# Proyectil solido (flecha) con alpha directo via Rembg:
wf2 = comfyui_build_projectile_workflow("arrow", direction="right", glow=False, transparent=True, seed=5)
```
O lanzable directo con: `./fn run comfyui_build_projectile_workflow` (imprime nodos + class_types del ejemplo).
## Cuando usarla
Cuando necesites el sprite de un PROYECTIL de juego — flecha, bala, bola de fuego,
rayo, misil, hechizo, plasma — orientado (apuntando a la derecha por defecto, angulo
0) y aislado para instanciar en el motor y rotar segun el angulo de disparo. Elige el
camino a alpha segun la naturaleza del proyectil:
- **`glow=False` (proyectil solido: flecha, bala, misil, espada lanzada)**: la silueta
esta definida; con `transparent=True` se recorta limpio con Image Rembg dejando alpha.
- **`glow=True` (proyectil brillante/magico: bola de fuego, rayo, hechizo)**: el halo es
translucido; se genera sobre fondo NEGRO y NO se recorta con Rembg. Despues aplica
`comfyui_matting_luma_to_alpha` (luminancia-como-alpha) en un paso aparte para sacar
el halo con transparencia gradual.
Usa `direction` para fijar hacia donde apunta el sprite si el motor no rota desde
"right". Pasa el MISMO `style` + `checkpoint` + (`lora`) a todos los proyectiles del
juego para que combinen.
## Gotchas
- **glow elige el camino a alpha**: `glow=True` NO inyecta Rembg (recortaria el halo) y
pone el proyectil sobre fondo negro como insumo de `comfyui_matting_luma_to_alpha`.
`glow=False` + `transparent=True` recorta la silueta solida con Rembg. No mezcles: un
proyectil brillante recortado por silueta pierde el halo; uno solido por luma-to-alpha
pierde las zonas oscuras del objeto.
- **glow=True ignora `transparent`/`rembg_model`**: el camino de un brillante es siempre
luma-to-alpha, no Rembg. Esos dos parametros solo aplican a `glow=False`.
- **El negativo por defecto NO rechaza "black background"**: es deliberado, para que el
fondo negro de `glow=True` salga. Si pasas un `negative` propio para un proyectil glow,
NO incluyas "black background" o anularas el insumo de luma-to-alpha.
- **`direction` se inserta como "pointing {direction}"**: `direction="up"` -> "pointing up"
en el prompt. Deja `direction=""`/None para un proyectil sin orientacion fija. El
convenio tipico del motor es apuntar a la derecha (angulo 0) y rotar el sprite.
- **El matting/recorte es un paso aparte**: esta funcion solo arma el dict. La generacion
real (GPU) la hacen `comfyui_submit_workflow` + `comfyui_wait_result` +
`comfyui_fetch_output_image`; el luma-to-alpha de un proyectil glow es
`comfyui_matting_luma_to_alpha` en un pipeline posterior.
- **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.
- Es una funcion **pura**: solo arma el dict, sin red ni I/O. Determinista para los mismos
argumentos.
@@ -0,0 +1,248 @@
"""Construye el workflow ComfyUI de UN proyectil orientado (API format).
Proyectiles de juego (flecha, bala, bola de fuego, rayo, misil, hechizo): sprite
pequeno con ORIENTACION (horizontal por defecto, apuntando a la derecha), aislado
sobre fondo recortable a alpha, listo para instanciar como proyectil en el motor
(rotar segun el angulo de disparo). Es el builder hermano de
comfyui_build_item_icon_workflow / comfyui_build_vfx_spritesheet_workflow /
comfyui_build_topdown_sprite_workflow: mismo patron (PURO, dict API format) que
compone funciones existentes del registry, no reescribe el grafo.
Cableado:
CheckpointLoaderSimple -> [LoraLoader opcional de estilo] -> KSampler
-> CLIPTextEncode (prompt scaffold de proyectil) ...
-> VAEDecode -> [Image Rembg opcional] -> SaveImage
Compone:
- comfyui_build_txt2img_workflow -> base txt2img cuadrada
- comfyui_inject_lora -> LoRA de estilo opcional (consistencia)
- 'Image Rembg (Remove Background)' (helper local) -> fondo transparente (alpha)
Dos caminos a alpha segun la naturaleza del proyectil:
- glow=False (DEFECTO): proyectil SOLIDO (flecha, bala, misil, espada lanzada)
con silueta definida. El scaffold pide "plain background" y, si transparent,
se inyecta Rembg que recorta la silueta dejando alpha limpio. Es el camino
analogo a item_icon / topdown_sprite.
- glow=True: proyectil BRILLANTE/MAGICO (bola de fuego, rayo, hechizo, plasma)
cuyo halo es translucido. El scaffold pide "glowing, on black background" y NO
se inyecta Rembg (recortaria el halo). La imagen queda sobre fondo negro,
pensada como INSUMO de comfyui_matting_luma_to_alpha (luminancia-como-alpha:
brillante=opaco, negro=transparente), que el caller aplica luego en un
pipeline. Mismo motivo por el que comfyui_build_vfx_spritesheet_workflow genera
sobre fondo negro. Por eso glow=True ignora `transparent`/`rembg_model`.
Por que `direction` y no meterlo en `style`: separar la orientacion del estilo deja
fijar hacia donde "apunta" el proyectil (right/left/up/down) manteniendo el resto.
El convenio del motor suele ser apuntar a la derecha (angulo 0) y rotar el sprite;
por eso direction="right" por defecto. direction vacio = sin orientacion explicita.
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 proyectiles: UN objeto aislado, sin escenario ni
# personaje que lo sostenga, sin texto/marcas ni recortes. No menciona "black
# background" para no rechazar el fondo negro que glow=True necesita.
_PROJECTILE_NEGATIVE = (
"blurry, lowres, multiple objects, cluttered background, busy background, "
"scenery, landscape, character, person, hand holding, text, watermark, "
"signature, photo, photorealistic, jpeg artifacts, cropped, out of frame, "
"deformed"
)
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 / topdown_sprite: el nodo
recorta la silueta del proyectil solido 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_projectile_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_projectile_workflow(
projectile: str,
*,
direction: str = "right",
glow: bool = False,
style: str = "game projectile, 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 = "projectile",
) -> dict:
"""Construye el dict (API format) del workflow de UN proyectil orientado.
Args:
projectile: nombre del proyectil (ej. "arrow", "bullet", "fireball",
"lightning bolt", "missile", "magic spell"). Se inserta en un prompt
scaffold de proyectil. No puede estar vacio.
direction: orientacion hacia la que apunta el proyectil. Tipico "right"
(apuntando a la derecha, angulo 0 — el convenio del motor que luego
rota el sprite), "left", "up", "down" (tambien diagonales como
"up-right" si el motor las usa). None/"" = sin orientacion explicita.
keyword-only.
glow: si True el proyectil es brillante/magico (bola de fuego, rayo,
hechizo): el scaffold pide "glowing, on black background" y NO se
inyecta Rembg (recortaria el halo translucido). La imagen queda sobre
fondo negro como INSUMO de comfyui_matting_luma_to_alpha
(luminancia-como-alpha), que el caller aplica luego en un pipeline.
glow=True ignora `transparent`/`rembg_model`. Si False (defecto) el
proyectil es solido y va por Rembg. keyword-only.
style: descriptor de estilo que mantiene consistentes los proyectiles de un
set (ej. "game projectile, side view", "hand-painted RPG projectile",
"pixel art bullet"). Pasa el MISMO style + checkpoint + (lora) a todos
los proyectiles del set 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 (y glow=False) inyecta Rembg y el PNG sale con alpha
(silueta recortada). Si False deja el proyectil opaco sobre fondo plano,
recortable luego por el caller/pipeline. Ignorado cuando glow=True.
keyword-only.
seed: semilla del KSampler. keyword-only.
lora: LoRA de estilo opcional en models/loras (ej.
'detail_tweaker_sd15.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 y glow=False. keyword-only.
negative: prompt negativo. None usa el negativo por defecto pensado para
proyectiles (un objeto aislado, sin escenario/personaje, sin texto/foto).
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 proyectil ('a {projectile}, {style}, pointing
{direction}, isolated, ...') + LoRA de estilo opcional. Si glow=False y
transparent, ademas Image Rembg para alpha. Si glow=True, la imagen sale
sobre fondo negro (insumo de comfyui_matting_luma_to_alpha). Es UN proyectil;
un set -> llamar por proyectil con el mismo style/checkpoint/lora.
Raises:
ValueError: si projectile 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 projectile or not projectile.strip():
raise ValueError(
"comfyui_build_projectile_workflow: 'projectile' no puede estar vacio"
)
projectile = projectile.strip()
direction = (direction or "").strip()
lora_strength = max(0.0, min(2.0, float(lora_strength)))
neg = _PROJECTILE_NEGATIVE if negative is None else negative
# Fondo segun la naturaleza: brillante sobre negro (luma->alpha) vs solido sobre
# fondo plano (recorte por silueta).
background = "glowing, on black background" if glow else "plain background"
pointing = f"pointing {direction}, " if direction else ""
positive = (
f"a {projectile}, {style}, {pointing}isolated, single object, "
f"{background}, game asset, dynamic, 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
)
# Solo el proyectil solido va por Rembg. El brillante va por luma->alpha (caller).
if transparent and not glow:
wf = _inject_rembg(wf, rembg_model)
return wf
if __name__ == "__main__":
import json
wf = comfyui_build_projectile_workflow(
"fireball",
direction="right",
glow=True,
style="game projectile, side view",
seed=42,
)
print(
json.dumps(
{
"nodes": list(wf),
"classes": sorted({n["class_type"] for n in wf.values()}),
},
indent=2,
)
)
@@ -0,0 +1,156 @@
"""Tests offline de comfyui_build_projectile_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_projectile_workflow import ( # noqa: E402
comfyui_build_projectile_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_solid_transparent_recipe():
# Proyectil solido (glow=False) + transparent -> cadena base txt2img + Rembg.
wf = comfyui_build_projectile_workflow(
"arrow", direction="right", glow=False, transparent=True, seed=5
)
cls = _classes(wf)
assert "CheckpointLoaderSimple" in cls
assert "KSampler" in cls
assert "VAEDecode" in cls
assert "SaveImage" in cls
assert "Image Rembg (Remove Background)" in cls
# El proyectil + estilo + orientacion aparecen en el prompt positivo.
pos = _pos_with(wf, "arrow")
txt = pos["inputs"]["text"]
assert "game projectile, side view" in txt
assert "pointing right" in txt
assert "isolated" in txt
assert "plain background" in txt
assert "game asset" 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_golden_glow_on_black_no_rembg():
# glow=True -> "glowing, on black background" + SIN Rembg (insumo luma->alpha).
wf = comfyui_build_projectile_workflow(
"fireball", direction="right", glow=True, seed=42
)
cls = _classes(wf)
assert "Image Rembg (Remove Background)" not in cls
pos = _pos_with(wf, "fireball")
txt = pos["inputs"]["text"]
assert "glowing, on black background" in txt
assert "plain background" not in txt
# SaveImage toma del VAEDecode directamente (no hay Rembg).
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_glow_ignores_transparent():
# Aunque transparent=True, glow=True NO inyecta Rembg.
wf = comfyui_build_projectile_workflow(
"lightning bolt", glow=True, transparent=True
)
assert "Image Rembg (Remove Background)" not in _classes(wf)
def test_edge_direction_reflected():
for d in ["left", "up", "down", "up-right"]:
wf = comfyui_build_projectile_workflow("bullet", direction=d, glow=False)
pos = _pos_with(wf, "bullet")
assert f"pointing {d}" in pos["inputs"]["text"]
def test_edge_direction_optional():
# Sin orientacion, el prompt no inserta "pointing".
wf = comfyui_build_projectile_workflow("missile", direction="", glow=False, transparent=False)
pos = _pos_with(wf, "missile")
assert "pointing" not in pos["inputs"]["text"]
def test_edge_solid_opaque_no_rembg():
# glow=False + transparent=False -> sin Rembg, SaveImage del VAEDecode.
wf = comfyui_build_projectile_workflow("bullet", glow=False, transparent=False)
assert "Image Rembg (Remove Background)" not in _classes(wf)
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_projectile_workflow("arrow", size=768)
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
assert latent["width"] == 768
assert latent["height"] == 768 # cuadrado
def test_edge_style_reflected():
wf = comfyui_build_projectile_workflow(
"fireball", style="pixel art bullet", glow=True
)
pos = _pos_with(wf, "fireball")
assert "pixel art bullet" in pos["inputs"]["text"]
def test_edge_lora_reflected():
wf = comfyui_build_projectile_workflow(
"arrow", lora="detail_tweaker_sd15.safetensors", lora_strength=0.9
)
loras = _by_class(wf, "LoraLoader")
assert len(loras) == 1
assert loras[0]["inputs"]["lora_name"] == "detail_tweaker_sd15.safetensors"
assert loras[0]["inputs"]["strength_model"] == 0.9
def test_default_negative_allows_black_background():
# El negativo por defecto NO debe rechazar "black background" (glow lo necesita).
wf = comfyui_build_projectile_workflow("fireball", glow=True)
neg = next(
n["inputs"]["text"]
for n in wf.values()
if n["class_type"] == "CLIPTextEncode" and "watermark" in n["inputs"]["text"]
)
assert "black background" not in neg
def test_error_empty_projectile():
try:
comfyui_build_projectile_workflow(" ")
assert False
except ValueError as e:
assert "projectile" in str(e)
def test_determinism():
a = comfyui_build_projectile_workflow(
"fireball", direction="right", glow=True, lora="detail_tweaker_sd15.safetensors", seed=7
)
b = comfyui_build_projectile_workflow(
"fireball", direction="right", glow=True, lora="detail_tweaker_sd15.safetensors", seed=7
)
assert a == b