adde produce + compute texture export
This commit is contained in:
+130
-30
@@ -15,6 +15,7 @@ from orisys_sensor_design.texture import (
|
||||
TEXTURE_JOB_FILENAME,
|
||||
TEXTURE_JOB_SCHEMA,
|
||||
TextureConfigError,
|
||||
TextureOutputTarget,
|
||||
build_texture_job,
|
||||
load_texture_patch_block,
|
||||
merge_texture_patch,
|
||||
@@ -22,10 +23,16 @@ from orisys_sensor_design.texture import (
|
||||
resolve_texture_settings,
|
||||
texture_job_path,
|
||||
texture_output_dir,
|
||||
texture_output_paths,
|
||||
texture_revision_rel,
|
||||
write_texture_job,
|
||||
)
|
||||
|
||||
DEFAULT_OUTPUTS = [
|
||||
("produce", "patch_texture_produce.png"),
|
||||
("compute", "patch_texture_compute.png"),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def revision_dir() -> Path:
|
||||
@@ -38,7 +45,6 @@ def _texture_namespace(
|
||||
namespace = argparse.Namespace(
|
||||
revision_dir=revision_dir,
|
||||
output_dir=output_dir,
|
||||
filename=None,
|
||||
mesh=None,
|
||||
cam=None,
|
||||
width=None,
|
||||
@@ -108,16 +114,34 @@ def test_output_dir_explicit_override_wins(revision_dir: Path, tmp_path: Path) -
|
||||
def test_output_dir_uses_directory_names_not_design_sensor_field(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""finger-xynova declares `sensor: finger`; the folder name must win."""
|
||||
"""The on-disk sensor folder must win over design.yaml's sensor field."""
|
||||
revision = _write_revision(tmp_path, sensor_dir="finger-xynova", revision_name="r01")
|
||||
paths = RevisionPaths.from_revision_dir(revision)
|
||||
assert texture_revision_rel(tmp_path, paths) == Path("finger-xynova") / "r01"
|
||||
|
||||
|
||||
def test_output_paths_pair_each_target(revision_dir: Path, tmp_path: Path) -> None:
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
settings = resolve_texture_settings(paths)
|
||||
|
||||
targets = texture_output_paths(tmp_path, settings)
|
||||
|
||||
assert [(target.uv_map, path.name) for target, path in targets] == DEFAULT_OUTPUTS
|
||||
assert all(path.parent == tmp_path.resolve() for _, path in targets)
|
||||
|
||||
|
||||
def test_job_path_sits_beside_texture(tmp_path: Path) -> None:
|
||||
assert texture_job_path(tmp_path / "out") == tmp_path / "out" / TEXTURE_JOB_FILENAME
|
||||
|
||||
|
||||
def test_output_target_entry_is_absolute(revision_dir: Path, tmp_path: Path) -> None:
|
||||
target = TextureOutputTarget(uv_map="produce", filename="a.png")
|
||||
entry = target.to_job_entry(tmp_path)
|
||||
assert entry["uv_map"] == "produce"
|
||||
assert Path(entry["output"]).is_absolute()
|
||||
assert Path(entry["output"]).name == "a.png"
|
||||
|
||||
|
||||
# ------------------------------------------------------- settings merge
|
||||
|
||||
|
||||
@@ -125,7 +149,7 @@ def test_defaults_come_from_design_yaml(revision_dir: Path) -> None:
|
||||
settings = resolve_texture_settings(RevisionPaths.from_revision_dir(revision_dir))
|
||||
assert settings.mesh == "sensing_surface"
|
||||
assert settings.camera == "cam_sensor"
|
||||
assert settings.filename == "patch_texture.png"
|
||||
assert [(target.uv_map, target.filename) for target in settings.outputs] == DEFAULT_OUTPUTS
|
||||
assert (settings.width, settings.height) == (3280, 3200)
|
||||
assert (settings.nx, settings.ny) == (80, 45)
|
||||
assert settings.color_r == pytest.approx(0.05)
|
||||
@@ -144,11 +168,23 @@ def test_builtin_defaults_when_design_block_absent(tmp_path: Path) -> None:
|
||||
settings = resolve_texture_settings(paths)
|
||||
assert settings.width == DEFAULT_TEXTURE_PATCH["width"]
|
||||
assert settings.height == DEFAULT_TEXTURE_PATCH["height"]
|
||||
assert [target.uv_map for target in settings.outputs] == ["produce", "compute"]
|
||||
# Object names fall back to design.yaml `objects` defaults.
|
||||
assert settings.mesh == "sensing_surface"
|
||||
assert settings.camera == "cam_sensor"
|
||||
|
||||
|
||||
def test_design_yaml_uv_outputs_replace_builtin_defaults(tmp_path: Path) -> None:
|
||||
revision = _write_revision(
|
||||
tmp_path,
|
||||
"texture_patch:\n uv_outputs:\n custom: my_custom_layout.png\n",
|
||||
)
|
||||
settings = resolve_texture_settings(RevisionPaths.from_revision_dir(revision))
|
||||
assert [(target.uv_map, target.filename) for target in settings.outputs] == [
|
||||
("custom", "my_custom_layout.png")
|
||||
]
|
||||
|
||||
|
||||
def test_cli_overrides_beat_design_yaml(revision_dir: Path) -> None:
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
settings = resolve_texture_settings(
|
||||
@@ -177,6 +213,8 @@ def test_cli_overrides_beat_design_yaml(revision_dir: Path) -> None:
|
||||
assert settings.cross_length_px == 8
|
||||
# A 3-channel cross colour is widened to RGBA.
|
||||
assert settings.cross_color == (0.0, 1.0, 0.0, 1.0)
|
||||
# Shared rendering settings leave both targets intact.
|
||||
assert [target.uv_map for target in settings.outputs] == ["produce", "compute"]
|
||||
|
||||
|
||||
def test_unset_overrides_do_not_erase_configured_values(revision_dir: Path) -> None:
|
||||
@@ -194,6 +232,14 @@ def test_unknown_design_key_is_rejected(tmp_path: Path) -> None:
|
||||
assert "widht" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_legacy_filename_key_is_rejected(tmp_path: Path) -> None:
|
||||
"""The singular filename key was replaced by uv_outputs."""
|
||||
revision = _write_revision(tmp_path, "texture_patch:\n filename: patch_texture.png\n")
|
||||
with pytest.raises(TextureConfigError) as excinfo:
|
||||
resolve_texture_settings(RevisionPaths.from_revision_dir(revision))
|
||||
assert "filename" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_unknown_cross_key_is_rejected(tmp_path: Path) -> None:
|
||||
revision = _write_revision(
|
||||
tmp_path, "texture_patch:\n cross:\n thickness: 3\n"
|
||||
@@ -220,9 +266,6 @@ def test_texture_patch_must_be_a_mapping(tmp_path: Path) -> None:
|
||||
({"color_r": 2.0}, "color_r"),
|
||||
({"edge_sample_grid": 1}, "edge_sample_grid"),
|
||||
({"seed": "abc"}, "seed"),
|
||||
({"filename": "tex.jpg"}, "filename"),
|
||||
({"filename": "nested/tex.png"}, "filename"),
|
||||
({"filename": "..\\tex.png"}, "filename"),
|
||||
({"cross": {"enabled": "yes"}}, "cross.enabled"),
|
||||
({"cross": {"length_px": 0}}, "cross.length_px"),
|
||||
({"cross": {"color": [2.0, 0.0, 0.0, 1.0]}}, "cross.color"),
|
||||
@@ -238,6 +281,31 @@ def test_invalid_overrides_are_rejected(
|
||||
assert expected in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("overrides", "expected"),
|
||||
[
|
||||
({"uv_outputs": {}}, "uv_outputs"),
|
||||
({"uv_outputs": "produce.png"}, "uv_outputs"),
|
||||
({"uv_outputs": {"": "produce.png"}}, "uv_outputs"),
|
||||
({"uv_outputs": {"produce": ""}}, "uv_outputs.produce"),
|
||||
({"uv_outputs": {"produce": "texture.jpg"}}, "uv_outputs.produce"),
|
||||
({"uv_outputs": {"produce": "nested/a.png"}}, "uv_outputs.produce"),
|
||||
({"uv_outputs": {"produce": "..\\a.png"}}, "uv_outputs.produce"),
|
||||
(
|
||||
{"uv_outputs": {"produce": "same.png", "compute": "same.png"}},
|
||||
"duplicate output file name",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_uv_outputs_are_rejected(
|
||||
revision_dir: Path, overrides: dict, expected: str
|
||||
) -> None:
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
with pytest.raises(TextureConfigError) as excinfo:
|
||||
resolve_texture_settings(paths, overrides)
|
||||
assert expected in str(excinfo.value)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- job I/O
|
||||
|
||||
|
||||
@@ -245,9 +313,8 @@ def test_job_round_trips_with_absolute_paths(revision_dir: Path, tmp_path: Path)
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
settings = resolve_texture_settings(paths, {"width": 256, "height": 256, "seed": 3})
|
||||
output_dir = tmp_path / "textures"
|
||||
output_path = output_dir / settings.filename
|
||||
|
||||
job = build_texture_job(paths, settings, output_path)
|
||||
job = build_texture_job(paths, settings, output_dir)
|
||||
assert job["schema"] == TEXTURE_JOB_SCHEMA
|
||||
assert job["mode"] == "patches"
|
||||
assert set(job) == {
|
||||
@@ -255,10 +322,16 @@ def test_job_round_trips_with_absolute_paths(revision_dir: Path, tmp_path: Path)
|
||||
"mode",
|
||||
"revision_dir",
|
||||
"source_blend",
|
||||
"output",
|
||||
"outputs",
|
||||
"objects",
|
||||
"settings",
|
||||
}
|
||||
assert "output" not in job
|
||||
assert [entry["uv_map"] for entry in job["outputs"]] == ["produce", "compute"]
|
||||
assert [Path(entry["output"]).name for entry in job["outputs"]] == [
|
||||
filename for _, filename in DEFAULT_OUTPUTS
|
||||
]
|
||||
assert all(Path(entry["output"]).is_absolute() for entry in job["outputs"])
|
||||
assert set(job["objects"]) == {"mesh", "camera"}
|
||||
assert set(job["settings"]) == {
|
||||
"width",
|
||||
@@ -280,9 +353,8 @@ def test_job_round_trips_with_absolute_paths(revision_dir: Path, tmp_path: Path)
|
||||
job_path = write_texture_job(job, output_dir)
|
||||
assert job_path == output_dir / TEXTURE_JOB_FILENAME
|
||||
loaded = json.loads(job_path.read_text(encoding="utf-8"))
|
||||
assert loaded["output"] == str(output_path.resolve())
|
||||
assert loaded["outputs"] == job["outputs"]
|
||||
assert loaded["source_blend"] == str(paths.source_blend.resolve())
|
||||
assert Path(loaded["output"]).is_absolute()
|
||||
assert loaded["objects"] == {"mesh": "sensing_surface", "camera": "cam_sensor"}
|
||||
assert loaded["settings"]["seed"] == 3
|
||||
assert loaded["settings"]["cross"]["color"] == [1.0, 0.0, 0.0, 1.0]
|
||||
@@ -292,56 +364,76 @@ def test_write_job_creates_output_directory(revision_dir: Path, tmp_path: Path)
|
||||
paths = RevisionPaths.from_revision_dir(revision_dir)
|
||||
settings = resolve_texture_settings(paths)
|
||||
nested = tmp_path / "a" / "b"
|
||||
write_texture_job(build_texture_job(paths, settings, nested / settings.filename), nested)
|
||||
write_texture_job(build_texture_job(paths, settings, nested), nested)
|
||||
assert (nested / TEXTURE_JOB_FILENAME).is_file()
|
||||
|
||||
|
||||
# ----------------------------------------------------------- CLI wiring
|
||||
|
||||
|
||||
def _fake_blender(monkeypatch: pytest.MonkeyPatch, exit_code: int, write_png: bool):
|
||||
def _fake_blender(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
exit_code: int,
|
||||
write_pngs: bool = True,
|
||||
skip_uv: str | None = None,
|
||||
):
|
||||
calls: list[tuple[str, Path, list[str]]] = []
|
||||
|
||||
def fake_run(script: str, blend: Path, extra_args: list[str]):
|
||||
calls.append((script, blend, list(extra_args)))
|
||||
if write_png:
|
||||
if write_pngs:
|
||||
job_path = Path(extra_args[extra_args.index("--job") + 1])
|
||||
job = json.loads(job_path.read_text(encoding="utf-8"))
|
||||
Path(job["output"]).write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
for entry in job["outputs"]:
|
||||
if skip_uv is not None and entry["uv_map"] == skip_uv:
|
||||
continue
|
||||
Path(entry["output"]).write_bytes(b"\x89PNG\r\n\x1a\n")
|
||||
return exit_code
|
||||
|
||||
monkeypatch.setattr(cli, "_run_blender_script", fake_run)
|
||||
return calls
|
||||
|
||||
|
||||
def test_cmd_texture_forwards_job_and_verifies_artifact(
|
||||
def test_cmd_texture_forwards_job_and_verifies_both_artifacts(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
calls = _fake_blender(monkeypatch, 0, write_png=True)
|
||||
calls = _fake_blender(monkeypatch, 0)
|
||||
out_dir = tmp_path / "out"
|
||||
assert cli.cmd_texture(revision_dir, _texture_namespace(revision_dir, out_dir)) == 0
|
||||
|
||||
assert len(calls) == 1, "one Blender launch must render every target"
|
||||
script, blend, extra = calls[0]
|
||||
assert script == "random_patch_dd02c.py"
|
||||
assert blend == revision_dir.resolve() / "source" / "geometry.blend"
|
||||
assert extra[0] == "--job"
|
||||
job_path = Path(extra[1])
|
||||
assert job_path == out_dir.resolve() / TEXTURE_JOB_FILENAME
|
||||
assert (out_dir / "patch_texture.png").is_file()
|
||||
|
||||
for _, filename in DEFAULT_OUTPUTS:
|
||||
assert (out_dir / filename).is_file()
|
||||
|
||||
|
||||
def test_cmd_texture_propagates_blender_failure(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
_fake_blender(monkeypatch, 9, write_png=False)
|
||||
_fake_blender(monkeypatch, 9, write_pngs=False)
|
||||
namespace = _texture_namespace(revision_dir, tmp_path / "out")
|
||||
assert cli.cmd_texture(revision_dir, namespace) == 9
|
||||
|
||||
|
||||
def test_cmd_texture_detects_missing_texture(
|
||||
def test_cmd_texture_detects_both_textures_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
_fake_blender(monkeypatch, 0, write_png=False)
|
||||
_fake_blender(monkeypatch, 0, write_pngs=False)
|
||||
namespace = _texture_namespace(revision_dir, tmp_path / "out")
|
||||
assert cli.cmd_texture(revision_dir, namespace) == 3
|
||||
|
||||
|
||||
def test_cmd_texture_detects_partial_pair(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""One UV map written, the other missing, must not look like success."""
|
||||
_fake_blender(monkeypatch, 0, skip_uv="compute")
|
||||
namespace = _texture_namespace(revision_dir, tmp_path / "out")
|
||||
assert cli.cmd_texture(revision_dir, namespace) == 3
|
||||
|
||||
@@ -349,7 +441,7 @@ def test_cmd_texture_detects_missing_texture(
|
||||
def test_main_texture_parses_flags_into_job(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
) -> None:
|
||||
calls = _fake_blender(monkeypatch, 0, write_png=True)
|
||||
calls = _fake_blender(monkeypatch, 0)
|
||||
out_dir = tmp_path / "flagged"
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
cli.main(
|
||||
@@ -358,8 +450,6 @@ def test_main_texture_parses_flags_into_job(
|
||||
str(revision_dir),
|
||||
"--output-dir",
|
||||
str(out_dir),
|
||||
"--filename",
|
||||
"custom.png",
|
||||
"--mesh",
|
||||
"sensing_surface",
|
||||
"--width",
|
||||
@@ -377,21 +467,31 @@ def test_main_texture_parses_flags_into_job(
|
||||
)
|
||||
assert excinfo.value.code == 0
|
||||
|
||||
job = json.loads(
|
||||
Path(calls[0][2][1]).read_text(encoding="utf-8")
|
||||
)
|
||||
job = json.loads(Path(calls[0][2][1]).read_text(encoding="utf-8"))
|
||||
assert [entry["uv_map"] for entry in job["outputs"]] == ["produce", "compute"]
|
||||
assert job["settings"]["width"] == 256
|
||||
assert job["settings"]["nx"] == 4
|
||||
assert job["settings"]["ny"] == 3
|
||||
assert job["settings"]["seed"] == 7
|
||||
assert job["settings"]["cross"]["enabled"] is False
|
||||
assert (out_dir / "custom.png").is_file()
|
||||
for _, filename in DEFAULT_OUTPUTS:
|
||||
assert (out_dir / filename).is_file()
|
||||
|
||||
|
||||
def test_main_texture_rejects_filename_flag(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path
|
||||
) -> None:
|
||||
"""The singular --filename flag is gone; per-UV names come from design.yaml."""
|
||||
_fake_blender(monkeypatch, 0)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
cli.main(["texture", str(revision_dir), "--filename", "custom.png"])
|
||||
assert excinfo.value.code == 2
|
||||
|
||||
|
||||
def test_main_texture_rejects_invalid_flag_with_exit_2(
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
|
||||
monkeypatch: pytest.MonkeyPatch, revision_dir: Path
|
||||
) -> None:
|
||||
_fake_blender(monkeypatch, 0, write_png=True)
|
||||
_fake_blender(monkeypatch, 0)
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
cli.main(["texture", str(revision_dir), "--width", "0"])
|
||||
assert excinfo.value.code == 2
|
||||
|
||||
Reference in New Issue
Block a user