feat(gamedev): ronda 1 — pixelize + luma→alpha + export-godot (grupo gamedev)
Tres funciones CPU-only del lote gamedev 2D + 2 helpers puros + grupo de capacidad: - comfyui_pixelize_image_py_ml (impure): Fase 2 pixelart — downscale nearest + cuantizacion a N colores / paleta fija (game-boy/pico-8/nes) + re-upscale nearest. - comfyui_matting_luma_to_alpha_py_ml (impure): frame VFX sobre negro -> RGBA por luminancia ponderada (translucidos con additive blend). - comfyui_export_asset_to_godot_py_pipelines (impure): puente ComfyUI -> Godot 4 — copia a res://assets/<dir> por kind + .import por tipo + filtro Nearest si pixelart + reimport headless best-effort. Compone los 2 helpers puros. - godot_map_asset_dir_py_core, godot_clean_asset_name_py_core (pure): nucleos reutilizables del pipeline. - docs/capabilities/gamedev-2d.md + INDEX: grupo nuevo gamedev. Tests 33/33 verdes (offline PIL/numpy). Golden real verificado: asset de ~/ComfyUI/output -> /tmp/godot_test_proj con .import correcto y reimport headless real de Godot 4.7. Sin GPU, sin red, sin tocar proyectos del usuario. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: godot_clean_asset_name
|
||||
kind: function
|
||||
lang: py
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def godot_clean_asset_name(filename: str, *, override: str | None = None) -> str"
|
||||
description: "Normaliza el nombre de archivo de un asset salido de ComfyUI (patron <prefijo>_NNNNN_.<ext>) a un nombre limpio y seguro para res://: snake_case, minusculas, sin el sufijo numerico _NNNNN_, sin espacios ni caracteres raros, conservando la extension. Pura: solo manipula el string, no toca disco. Pensada para el puente ComfyUI -> Godot."
|
||||
tags: [godot, gamedev, core, assets, naming]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: filename
|
||||
desc: "nombre o ruta del asset de origen (se toma solo el basename)."
|
||||
- name: override
|
||||
desc: "nombre base deseado sin extension; si se pasa se usa en lugar del nombre de origen (igual se normaliza a snake_case y se le anade la extension del origen). keyword-only."
|
||||
output: "nombre de archivo limpio 'snake_case.ext' (ext en minuscula; sin punto si el origen no tenia extension)."
|
||||
tested: true
|
||||
tests: [test_strips_comfyui_suffix, test_normalizes_spaces_and_case, test_takes_basename_from_path, test_override_name, test_empty_fallback]
|
||||
test_file_path: "python/functions/core/godot_clean_asset_name_test.py"
|
||||
file_path: "python/functions/core/godot_clean_asset_name.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from core.godot_clean_asset_name import godot_clean_asset_name
|
||||
|
||||
godot_clean_asset_name("svd_motion_hi_00001_.webp") # 'svd_motion_hi.webp'
|
||||
godot_clean_asset_name("/x/ComfyUI/output/bench_00042_.png") # 'bench.png'
|
||||
godot_clean_asset_name("x.png", override="explosion loop") # 'explosion_loop.png'
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al exportar un asset de ComfyUI a Godot, para renombrar el `<prefijo>_NNNNN_.<ext>`
|
||||
a un nombre semántico y seguro (lo usa `comfyui_export_asset_to_godot`). También
|
||||
para limpiar cualquier nombre de archivo antes de meterlo en `res://`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Solo quita el sufijo numérico `_NNNNN_` del **nombre de origen**; con `override`
|
||||
se respetan los dígitos finales (p.ej. `hero_v2` no pierde el `2`).
|
||||
- Reduce cualquier carácter no `[a-z0-9_]` a `_` y colapsa repetidos; un nombre que
|
||||
quede vacío cae a `"asset"`.
|
||||
- Conserva la extensión en minúscula; si el origen no tiene extensión, devuelve solo
|
||||
el nombre sin punto.
|
||||
@@ -0,0 +1,49 @@
|
||||
"""godot_clean_asset_name — normaliza el nombre de archivo de un asset para Godot.
|
||||
|
||||
Funcion pura: toma el nombre de un asset salido de ComfyUI (patron
|
||||
`<prefijo>_NNNNN_.<ext>`, p.ej. `svd_motion_hi_00001_.webp`) y devuelve un nombre
|
||||
limpio, semantico y seguro para `res://`: snake_case, minusculas, sin el sufijo
|
||||
numerico `_NNNNN_`, sin espacios ni caracteres raros, conservando la extension.
|
||||
No toca disco. Pensada para el puente ComfyUI -> Godot (renombrar al exportar).
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
_NNNNN_SUFFIX = re.compile(r"_\d{3,}_?$") # sufijo numerico de ComfyUI: _00001_ / _00001
|
||||
_NON_SAFE = re.compile(r"[^a-z0-9_]+") # cualquier cosa que no sea snake_case
|
||||
_MULTI_US = re.compile(r"_{2,}")
|
||||
|
||||
|
||||
def godot_clean_asset_name(filename: str, *, override: str | None = None) -> str:
|
||||
"""Limpia el nombre de un asset a snake_case seguro conservando la extension.
|
||||
|
||||
Args:
|
||||
filename: nombre o ruta del asset de origen (se toma solo el basename).
|
||||
override: nombre base deseado (sin extension); si se pasa, se usa en lugar
|
||||
del nombre de origen (igual se normaliza a snake_case y se le anade la
|
||||
extension del origen). keyword-only.
|
||||
|
||||
Returns:
|
||||
Nombre de archivo limpio "snake_case.ext" (ext en minuscula, sin punto si
|
||||
el origen no tenia extension).
|
||||
"""
|
||||
base = os.path.basename(filename or "")
|
||||
stem, ext = os.path.splitext(base)
|
||||
ext = ext.lower()
|
||||
|
||||
raw = override if override is not None else stem
|
||||
name = raw.strip().lower().replace(" ", "_").replace("-", "_")
|
||||
if override is None:
|
||||
name = _NNNNN_SUFFIX.sub("", name) # quita _00001_ solo del nombre de origen
|
||||
name = _NON_SAFE.sub("_", name)
|
||||
name = _MULTI_US.sub("_", name).strip("_")
|
||||
if not name:
|
||||
name = "asset"
|
||||
return f"{name}{ext}" if ext else name
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
print(godot_clean_asset_name(sys.argv[1] if len(sys.argv) > 1 else "svd_motion_hi_00001_.webp"))
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Tests de godot_clean_asset_name (pura, offline)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from core.godot_clean_asset_name import godot_clean_asset_name # noqa: E402
|
||||
|
||||
|
||||
def test_strips_comfyui_suffix():
|
||||
assert godot_clean_asset_name("svd_motion_hi_00001_.webp") == "svd_motion_hi.webp"
|
||||
assert godot_clean_asset_name("3d_robot_mesh_00001_.glb") == "3d_robot_mesh.glb"
|
||||
|
||||
|
||||
def test_normalizes_spaces_and_case():
|
||||
assert godot_clean_asset_name("My Hero Sprite.PNG") == "my_hero_sprite.png"
|
||||
assert godot_clean_asset_name("fire-flare.png") == "fire_flare.png"
|
||||
|
||||
|
||||
def test_takes_basename_from_path():
|
||||
assert godot_clean_asset_name("/home/x/ComfyUI/output/bench_00042_.png") == "bench.png"
|
||||
|
||||
|
||||
def test_override_name():
|
||||
assert godot_clean_asset_name("svd_00001_.webp", override="explosion loop") == "explosion_loop.webp"
|
||||
# override conserva digitos finales (no es sufijo ComfyUI a quitar)
|
||||
assert godot_clean_asset_name("x.png", override="hero_v2") == "hero_v2.png"
|
||||
|
||||
|
||||
def test_empty_fallback():
|
||||
assert godot_clean_asset_name("_00001_.png") == "asset.png"
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: godot_map_asset_dir
|
||||
kind: function
|
||||
lang: py
|
||||
domain: core
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def godot_map_asset_dir(kind: str) -> str"
|
||||
description: "Mapea el tipo de asset (sprite, pixelart, tileset, vfx, sfx, music, model) a su subcarpeta canonica bajo res://assets/ de un proyecto Godot 4, segun la convencion del puente ComfyUI -> Godot. Pura: solo resuelve una ruta relativa POSIX, no toca disco. kind desconocido -> ValueError."
|
||||
tags: [godot, gamedev, core, assets, mapping]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: kind
|
||||
desc: "tipo de asset: 'sprite', 'pixelart', 'tileset', 'vfx', 'sfx', 'music' o 'model' (case-insensitive, se ignoran espacios)."
|
||||
output: "subruta relativa POSIX bajo assets/ (p.ej. 'sprites', 'tilesets', 'audio/music', 'models')."
|
||||
tested: true
|
||||
tests: [test_all_kinds, test_case_insensitive, test_unknown_kind_raises]
|
||||
test_file_path: "python/functions/core/godot_map_asset_dir_test.py"
|
||||
file_path: "python/functions/core/godot_map_asset_dir.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from core.godot_map_asset_dir import godot_map_asset_dir
|
||||
|
||||
godot_map_asset_dir("pixelart") # 'sprites'
|
||||
godot_map_asset_dir("music") # 'audio/music'
|
||||
godot_map_asset_dir("model") # 'models'
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Al construir la ruta destino de un asset dentro de un proyecto Godot (lo usa el
|
||||
pipeline `comfyui_export_asset_to_godot`). Centraliza la convención tipo -> carpeta
|
||||
para no esparcir el mapeo por varias funciones.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- `pixelart` cae en `sprites/` (el pixelart no es una carpeta, es un atributo: lo
|
||||
que cambia es el filtro Nearest, no la ubicación).
|
||||
- `kind` desconocido lanza `ValueError` (no devuelve un default silencioso).
|
||||
- Devuelve siempre subrutas POSIX (`/`), no rutas absolutas: el llamador antepone
|
||||
`<proyecto>/assets/`.
|
||||
@@ -0,0 +1,46 @@
|
||||
"""godot_map_asset_dir — mapea el tipo de asset a su subcarpeta res://assets/.
|
||||
|
||||
Funcion pura: dado el `kind` de un asset (sprite, pixelart, tileset, vfx, sfx,
|
||||
music, model) devuelve el subdirectorio canonico bajo `res://assets/` donde debe
|
||||
vivir en un proyecto Godot 4, segun la convencion del puente ComfyUI -> Godot
|
||||
(docs/comfyui-godot-integration.md). No toca disco: solo resuelve una ruta
|
||||
relativa. `kind` desconocido -> ValueError.
|
||||
"""
|
||||
|
||||
# kind -> subruta relativa bajo assets/ (convencion del puente ComfyUI->Godot).
|
||||
_KIND_TO_DIR = {
|
||||
"sprite": "sprites",
|
||||
"pixelart": "sprites",
|
||||
"tileset": "tilesets",
|
||||
"vfx": "vfx",
|
||||
"sfx": "audio/sfx",
|
||||
"music": "audio/music",
|
||||
"model": "models",
|
||||
}
|
||||
|
||||
|
||||
def godot_map_asset_dir(kind: str) -> str:
|
||||
"""Resuelve la subcarpeta de assets para un tipo de asset Godot.
|
||||
|
||||
Args:
|
||||
kind: tipo de asset: "sprite", "pixelart", "tileset", "vfx", "sfx",
|
||||
"music" o "model".
|
||||
|
||||
Returns:
|
||||
Subruta relativa (POSIX) bajo `assets/`, p.ej. "sprites" o "audio/music".
|
||||
|
||||
Raises:
|
||||
ValueError: si `kind` no es uno de los tipos soportados.
|
||||
"""
|
||||
key = (kind or "").strip().lower()
|
||||
if key not in _KIND_TO_DIR:
|
||||
raise ValueError(
|
||||
f"kind desconocido: {kind!r}. Soportados: {sorted(_KIND_TO_DIR)}"
|
||||
)
|
||||
return _KIND_TO_DIR[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
print(godot_map_asset_dir(sys.argv[1] if len(sys.argv) > 1 else "pixelart"))
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Tests de godot_map_asset_dir (pura, offline)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from core.godot_map_asset_dir import godot_map_asset_dir # noqa: E402
|
||||
|
||||
|
||||
def test_all_kinds():
|
||||
assert godot_map_asset_dir("sprite") == "sprites"
|
||||
assert godot_map_asset_dir("pixelart") == "sprites"
|
||||
assert godot_map_asset_dir("tileset") == "tilesets"
|
||||
assert godot_map_asset_dir("vfx") == "vfx"
|
||||
assert godot_map_asset_dir("sfx") == "audio/sfx"
|
||||
assert godot_map_asset_dir("music") == "audio/music"
|
||||
assert godot_map_asset_dir("model") == "models"
|
||||
|
||||
|
||||
def test_case_insensitive():
|
||||
assert godot_map_asset_dir("PixelArt") == "sprites"
|
||||
assert godot_map_asset_dir(" MODEL ") == "models"
|
||||
|
||||
|
||||
def test_unknown_kind_raises():
|
||||
with pytest.raises(ValueError):
|
||||
godot_map_asset_dir("hologram")
|
||||
Reference in New Issue
Block a user