added svg patch output

This commit is contained in:
ykcheungab
2026-09-19 15:08:57 +08:00
parent b1815b15ea
commit a883288c13
18 changed files with 71615 additions and 180 deletions
+79 -18
View File
@@ -32,6 +32,7 @@ DEFAULT_OUTPUTS = [
("produce", "patch_texture_produce.png"),
("compute", "patch_texture_compute.png"),
]
ARTIFACT_FORMATS = ("png", "svg", "pdf")
@pytest.fixture()
@@ -126,8 +127,15 @@ def test_output_paths_pair_each_target(revision_dir: Path, tmp_path: Path) -> No
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)
assert [target.uv_map for target, _ in targets] == ["produce", "compute"]
for (target, artifacts), (_, png_name) in zip(targets, DEFAULT_OUTPUTS):
stem = Path(png_name).stem
assert list(artifacts) == list(ARTIFACT_FORMATS)
assert {name: path.name for name, path in artifacts.items()} == {
format_name: f"{stem}.{format_name}"
for format_name in ARTIFACT_FORMATS
}
assert all(path.parent == tmp_path.resolve() for path in artifacts.values())
def test_job_path_sits_beside_texture(tmp_path: Path) -> None:
@@ -138,8 +146,13 @@ def test_output_target_entry_is_absolute(revision_dir: Path, tmp_path: Path) ->
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"
assert list(entry["artifacts"]) == list(ARTIFACT_FORMATS)
assert all(Path(path).is_absolute() for path in entry["artifacts"].values())
assert {name: Path(path).name for name, path in entry["artifacts"].items()} == {
"png": "a.png",
"svg": "a.svg",
"pdf": "a.pdf",
}
# ------------------------------------------------------- settings merge
@@ -154,7 +167,10 @@ def test_defaults_come_from_design_yaml(revision_dir: Path) -> None:
assert (settings.nx, settings.ny) == (80, 45)
assert settings.color_r == pytest.approx(0.05)
assert settings.edge_sample_grid == 5
assert settings.seed is None
# Pinned in design.yaml so the committed PNG/SVG/PDF set is reproducible.
assert settings.seed == 7
assert settings.vector_width_mm == pytest.approx(164.0)
assert settings.vector_height_mm == pytest.approx(160.0)
assert settings.cross_enabled is True
assert settings.cross_length_px == 40
assert settings.cross_thickness_px == 3
@@ -199,6 +215,7 @@ def test_cli_overrides_beat_design_yaml(revision_dir: Path) -> None:
"color_r": 0.25,
"edge_sample_grid": 3,
"seed": 11,
"vector": {"width_mm": 82.0, "height_mm": 80.0},
"cross": {"enabled": False, "length_px": 8, "color": [0.0, 1.0, 0.0]},
},
)
@@ -209,6 +226,8 @@ def test_cli_overrides_beat_design_yaml(revision_dir: Path) -> None:
assert settings.color_r == pytest.approx(0.25)
assert settings.edge_sample_grid == 3
assert settings.seed == 11
assert settings.vector_width_mm == pytest.approx(82.0)
assert settings.vector_height_mm == pytest.approx(80.0)
assert settings.cross_enabled is False
assert settings.cross_length_px == 8
# A 3-channel cross colour is widened to RGBA.
@@ -249,6 +268,15 @@ def test_unknown_cross_key_is_rejected(tmp_path: Path) -> None:
assert "thickness" in str(excinfo.value)
def test_unknown_vector_key_is_rejected(tmp_path: Path) -> None:
revision = _write_revision(
tmp_path, "texture_patch:\n vector:\n dpi: 508\n"
)
with pytest.raises(TextureConfigError) as excinfo:
resolve_texture_settings(RevisionPaths.from_revision_dir(revision))
assert "dpi" in str(excinfo.value)
def test_texture_patch_must_be_a_mapping(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, "texture_patch: patches\n")
with pytest.raises(TextureConfigError) as excinfo:
@@ -266,6 +294,8 @@ 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"),
({"vector": {"width_mm": 0}}, "vector.width_mm"),
({"vector": {"height_mm": "large"}}, "vector.height_mm"),
({"cross": {"enabled": "yes"}}, "cross.enabled"),
({"cross": {"length_px": 0}}, "cross.length_px"),
({"cross": {"color": [2.0, 0.0, 0.0, 1.0]}}, "cross.color"),
@@ -328,10 +358,14 @@ def test_job_round_trips_with_absolute_paths(revision_dir: Path, tmp_path: Path)
}
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"])
for entry, (_, png_name) in zip(job["outputs"], DEFAULT_OUTPUTS):
stem = Path(png_name).stem
assert list(entry["artifacts"]) == list(ARTIFACT_FORMATS)
assert {name: Path(path).name for name, path in entry["artifacts"].items()} == {
format_name: f"{stem}.{format_name}"
for format_name in ARTIFACT_FORMATS
}
assert all(Path(path).is_absolute() for path in entry["artifacts"].values())
assert set(job["objects"]) == {"mesh", "camera"}
assert set(job["settings"]) == {
"width",
@@ -341,8 +375,13 @@ def test_job_round_trips_with_absolute_paths(revision_dir: Path, tmp_path: Path)
"color_r",
"edge_sample_grid",
"seed",
"vector",
"cross",
}
assert job["settings"]["vector"] == {
"width_mm": 164.0,
"height_mm": 160.0,
}
assert set(job["settings"]["cross"]) == {
"enabled",
"length_px",
@@ -374,20 +413,29 @@ def test_write_job_creates_output_directory(revision_dir: Path, tmp_path: Path)
def _fake_blender(
monkeypatch: pytest.MonkeyPatch,
exit_code: int,
write_pngs: bool = True,
write_artifacts: bool = True,
skip_uv: str | None = None,
skip_format: str | None = None,
):
calls: list[tuple[str, Path, list[str]]] = []
payloads = {
"png": b"\x89PNG\r\n\x1a\n",
"svg": b"<svg/>",
"pdf": b"%PDF-1.4\n",
}
def fake_run(script: str, blend: Path, extra_args: list[str]):
calls.append((script, blend, list(extra_args)))
if write_pngs:
if write_artifacts:
job_path = Path(extra_args[extra_args.index("--job") + 1])
job = json.loads(job_path.read_text(encoding="utf-8"))
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")
for format_name, raw_path in entry["artifacts"].items():
if skip_uv is not None and entry["uv_map"] == skip_uv:
continue
if skip_format is not None and format_name == skip_format:
continue
Path(raw_path).write_bytes(payloads[format_name])
return exit_code
monkeypatch.setattr(cli, "_run_blender_script", fake_run)
@@ -410,13 +458,15 @@ def test_cmd_texture_forwards_job_and_verifies_both_artifacts(
assert job_path == out_dir.resolve() / TEXTURE_JOB_FILENAME
for _, filename in DEFAULT_OUTPUTS:
assert (out_dir / filename).is_file()
stem = Path(filename).stem
for format_name in ARTIFACT_FORMATS:
assert (out_dir / f"{stem}.{format_name}").is_file()
def test_cmd_texture_propagates_blender_failure(
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
) -> None:
_fake_blender(monkeypatch, 9, write_pngs=False)
_fake_blender(monkeypatch, 9, write_artifacts=False)
namespace = _texture_namespace(revision_dir, tmp_path / "out")
assert cli.cmd_texture(revision_dir, namespace) == 9
@@ -424,7 +474,7 @@ def test_cmd_texture_propagates_blender_failure(
def test_cmd_texture_detects_both_textures_missing(
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
) -> None:
_fake_blender(monkeypatch, 0, write_pngs=False)
_fake_blender(monkeypatch, 0, write_artifacts=False)
namespace = _texture_namespace(revision_dir, tmp_path / "out")
assert cli.cmd_texture(revision_dir, namespace) == 3
@@ -438,6 +488,15 @@ def test_cmd_texture_detects_partial_pair(
assert cli.cmd_texture(revision_dir, namespace) == 3
def test_cmd_texture_detects_one_missing_vector_format(
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
) -> None:
"""Every UV target must have PNG, SVG, and PDF before success is reported."""
_fake_blender(monkeypatch, 0, skip_format="svg")
namespace = _texture_namespace(revision_dir, tmp_path / "out")
assert cli.cmd_texture(revision_dir, namespace) == 3
def test_main_texture_parses_flags_into_job(
monkeypatch: pytest.MonkeyPatch, revision_dir: Path, tmp_path: Path
) -> None:
@@ -475,7 +534,9 @@ def test_main_texture_parses_flags_into_job(
assert job["settings"]["seed"] == 7
assert job["settings"]["cross"]["enabled"] is False
for _, filename in DEFAULT_OUTPUTS:
assert (out_dir / filename).is_file()
stem = Path(filename).stem
for format_name in ARTIFACT_FORMATS:
assert (out_dir / f"{stem}.{format_name}").is_file()
def test_main_texture_rejects_filename_flag(