Skip to content

Commit bba8c74

Browse files
committed
feat(paths): enhance file resolution for intermediate assets and ensure idempotency
Implement logic to detect existing shape and painted files in both the main and intermediate directories, allowing for resuming of asset processing. Refactor the `move_to_intermediate` function to ensure it is idempotent, preventing unnecessary file operations if the source is already at the destination. Update related tests to validate these enhancements.
1 parent 8c8d1e3 commit bba8c74

3 files changed

Lines changed: 108 additions & 7 deletions

File tree

GameAssets/src/gameassets/paths.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ def move_to_intermediate(src: Path, mesh_final: Path) -> Path:
167167
if not suffix:
168168
suffix = ""
169169
dst = dst_dir / f"{base}{suffix}{src.suffix}"
170+
# Idempotente: se src já está no destino (resume), não faz nada.
171+
try:
172+
if dst.exists() and src.resolve() == dst.resolve():
173+
return dst
174+
except OSError:
175+
pass
170176
if dst.exists():
171177
try:
172178
dst.unlink()
@@ -188,6 +194,32 @@ def _valid_file(p: Path) -> bool:
188194
return p.is_file() and p.stat().st_size > 0
189195

190196

197+
def _resolve_intermediate_or_main(canonical: Path, mesh_final: Path) -> Path | None:
198+
"""Round 2: aceita o ficheiro em ``meshes/`` ou em ``meshes/_intermediate/``.
199+
200+
O master pipeline move ``shape``/``painted`` para ``_intermediate/`` no fim.
201+
Para retomar uma pipeline parcial precisamos detectar o ficheiro em
202+
qualquer das duas localizações. Devolve o path existente (preferindo a
203+
localização canónica) ou ``None`` se nenhum existe e é válido.
204+
"""
205+
if _valid_file(canonical):
206+
return canonical
207+
intermediate = _intermediate_dir(mesh_final) / canonical.name
208+
if _valid_file(intermediate):
209+
return intermediate
210+
return None
211+
212+
213+
def _shape_existing(mesh_final: Path) -> Path | None:
214+
"""Devolve o ``id_shape.glb`` existente em ``meshes/`` ou ``_intermediate/``."""
215+
return _resolve_intermediate_or_main(_shape_path(mesh_final), mesh_final)
216+
217+
218+
def _painted_existing(mesh_final: Path) -> Path | None:
219+
"""Devolve o ``id_painted.glb`` existente em ``meshes/`` ou ``_intermediate/``."""
220+
return _resolve_intermediate_or_main(_painted_path(mesh_final), mesh_final)
221+
222+
191223
def _classify_row_state(
192224
*,
193225
img_final: Path,
@@ -239,21 +271,21 @@ def _classify_row_state_master(
239271
paint → bake-master (lod0) → lod gen → rig_hi → transfer → animate →
240272
validate. Devolve o primeiro estágio que ainda falta.
241273
"""
242-
shape = _shape_path(mesh_final)
274+
shape_any = _shape_existing(mesh_final)
275+
painted_any = _painted_existing(mesh_final)
243276
clean = _clean_path(mesh_final)
244-
painted = _painted_path(mesh_final)
245277
lod0 = _lod_path(mesh_final, 0)
246278
lod1 = _lod_path(mesh_final, 1)
247279
lod2 = _lod_path(mesh_final, 2)
248280
rigged_hi = _rigged_hi_path(mesh_final)
249281

250-
if not _valid_file(img_final) and not _valid_file(shape):
282+
if not _valid_file(img_final) and shape_any is None:
251283
return _ROW_NEED_IMAGE
252-
if not _valid_file(shape):
284+
if shape_any is None:
253285
return _ROW_NEED_SHAPE
254286
if not _valid_file(clean):
255287
return _ROW_NEED_TOPOLOGY_FIX
256-
if want_texture and not _valid_file(painted):
288+
if want_texture and painted_any is None:
257289
return _ROW_NEED_PAINT
258290
if not _valid_file(lod0):
259291
return _ROW_NEED_BAKE_MASTER

GameAssets/src/gameassets/pipeline_master.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@
3535
_lod_animated_path,
3636
_lod_path,
3737
_lod_rigged_path,
38+
_painted_existing,
3839
_painted_path,
3940
_rigged_hi_path,
41+
_shape_existing,
4042
_shape_path,
4143
move_to_intermediate,
4244
)
@@ -221,8 +223,10 @@ def _run(name: str, argv: list[str], output: Path | None = None) -> StageResult:
221223
res.stages.append(StageResult("setup", False, 0.0, "text3d não encontrado"))
222224
return res
223225

224-
shape_p = _shape_path(mesh_final)
225-
painted_p = _painted_path(mesh_final)
226+
# Round 2: shape/painted podem estar em meshes/ OU em meshes/_intermediate/
227+
# (após uma run anterior). Resolve dinamicamente para permitir resume.
228+
shape_p = _shape_existing(mesh_final) or _shape_path(mesh_final)
229+
painted_p = _painted_existing(mesh_final) or _painted_path(mesh_final)
226230
clean_p = _clean_path(mesh_final)
227231

228232
if not shape_p.is_file():

GameAssets/tests/test_resume_master.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,68 @@ def test_resume_master_pipeline_importable() -> None:
155155

156156
assert hasattr(pipeline_master, "resume_master_pipeline")
157157
assert hasattr(pipeline_master, "run_master_pipeline")
158+
159+
160+
def test_classify_master_detects_shape_in_intermediate(tmp_path: Path) -> None:
161+
"""Round 2: shape/painted em _intermediate/ devem ser detectados (resume)."""
162+
from gameassets.paths import (
163+
_ROW_NEED_TOPOLOGY_FIX,
164+
_classify_row_state_master,
165+
_intermediate_dir,
166+
_shape_path,
167+
)
168+
169+
img = tmp_path / "img.png"
170+
_touch(img)
171+
mesh = tmp_path / "mesh.glb"
172+
# shape vai direto para _intermediate/ (simula run anterior).
173+
canonical_shape = _shape_path(mesh)
174+
intermediate_shape = _intermediate_dir(mesh) / canonical_shape.name
175+
_touch(intermediate_shape)
176+
assert not canonical_shape.exists()
177+
178+
state = _classify_row_state_master(
179+
img_final=img, mesh_final=mesh, want_texture=True, wants_rig=False, wants_animate=False
180+
)
181+
# Shape detectado → próximo é topology-fix (clean ainda não existe).
182+
assert state == _ROW_NEED_TOPOLOGY_FIX
183+
184+
185+
def test_classify_master_detects_painted_in_intermediate(tmp_path: Path) -> None:
186+
"""Round 2: painted em _intermediate/ deve ser detectado (resume)."""
187+
from gameassets.paths import (
188+
_ROW_NEED_BAKE_MASTER,
189+
_classify_row_state_master,
190+
_clean_path,
191+
_intermediate_dir,
192+
_painted_path,
193+
_shape_path,
194+
)
195+
196+
img = tmp_path / "img.png"
197+
_touch(img)
198+
mesh = tmp_path / "mesh.glb"
199+
_touch(_intermediate_dir(mesh) / _shape_path(mesh).name)
200+
_touch(_clean_path(mesh))
201+
_touch(_intermediate_dir(mesh) / _painted_path(mesh).name)
202+
203+
state = _classify_row_state_master(
204+
img_final=img, mesh_final=mesh, want_texture=True, wants_rig=False, wants_animate=False
205+
)
206+
# Tem shape (intermediate), clean, painted (intermediate) — falta lod0.
207+
assert state == _ROW_NEED_BAKE_MASTER
208+
209+
210+
def test_move_to_intermediate_idempotent(tmp_path: Path) -> None:
211+
"""move_to_intermediate é safe se o ficheiro já está no destino."""
212+
from gameassets.paths import _intermediate_dir, _shape_path, move_to_intermediate
213+
214+
mesh = tmp_path / "mesh.glb"
215+
intermediate = _intermediate_dir(mesh) / _shape_path(mesh).name
216+
intermediate.parent.mkdir(parents=True, exist_ok=True)
217+
intermediate.write_bytes(b"data")
218+
# passar o path do intermediate como src — deve preservar.
219+
result = move_to_intermediate(intermediate, mesh)
220+
assert intermediate.is_file()
221+
assert intermediate.read_bytes() == b"data"
222+
assert result.resolve() == intermediate.resolve()

0 commit comments

Comments
 (0)