Add stroke texture PNG, SVG, and PDF export

Reuse colour artwork serializers and record source hashes. Verified single and multiview exports plus targeted tests.
This commit is contained in:
ykcheungab
2026-10-03 21:17:28 +08:00
parent 6b0694d6cb
commit 5bd7dd7a01
5 changed files with 452 additions and 66 deletions
+99
View File
@@ -0,0 +1,99 @@
"""The stroke CLI must preserve colour sources and verify all exported formats."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from orisys_sensor_design import cli
from orisys_sensor_design.manifest import RevisionPaths, sha256_file
REVISION = Path(__file__).resolve().parents[1] / "sensors" / "finger-xynova" / "r01"
def _args(job: Path, output_dir: Path) -> argparse.Namespace:
return argparse.Namespace(
views=False,
source_job=job,
output_dir=output_dir,
camera_width_fraction=0.5,
seed=13,
)
def _job(tmp_path: Path) -> tuple[Path, Path, Path]:
paths = RevisionPaths.from_revision_dir(REVISION)
png = tmp_path / "colour.png"
svg = tmp_path / "colour.svg"
png.write_bytes(b"colour pixels")
svg.write_bytes(b"<svg>colour geometry</svg>")
job_path = tmp_path / "_texture_job.json"
job_path.write_text(
json.dumps(
{
"schema": "orisys-sensor-design/texture-job-v3",
"revision_dir": str(paths.root),
"source_blend": str(paths.source_blend),
"outputs": [
{
"uv_map": "compute",
"artifacts": {"png": str(png), "svg": str(svg)},
}
],
}
),
encoding="utf-8",
)
return job_path, png, svg
def test_stroke_cli_exports_three_formats_and_manifest(tmp_path, monkeypatch):
job, png, svg = _job(tmp_path)
output_dir = tmp_path / "strokes"
calls = []
def run_blender(script, blend, args):
calls.append((script, blend, args))
output_dir.mkdir()
for fmt in ("png", "svg", "pdf"):
(output_dir / f"stroke_texture_compute.{fmt}").write_bytes(fmt.encode())
return 0
monkeypatch.setattr(cli, "_run_blender_script", run_blender)
assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 0
assert calls[0][0] == "random_stroke_pattern.py"
assert calls[0][2][-4:] == ["--seed", "13", "--camera-width-fraction", "0.5"]
manifest = json.loads((output_dir / "stroke_texture_manifest.json").read_text())
assert manifest["schema"] == "orisys-sensor-design/stroke-texture-manifest-v1"
assert manifest["source_job"]["sha256"] == sha256_file(job)
assert manifest["outputs"][0]["sources"]["png"]["sha256"] == sha256_file(png)
assert manifest["outputs"][0]["sources"]["svg"]["sha256"] == sha256_file(svg)
assert set(manifest["outputs"][0]["artifacts"]) == {"png", "svg", "pdf"}
assert png.read_bytes() == b"colour pixels"
assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 2
assert len(calls) == 1
def test_stroke_cli_refuses_missing_source_svg(tmp_path, monkeypatch):
job, _png, svg = _job(tmp_path)
svg.unlink()
monkeypatch.setattr(
cli,
"_run_blender_script",
lambda *_: (_ for _ in ()).throw(AssertionError("Blender must not run")),
)
assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "strokes")) == 3
def test_stroke_cli_rejects_incomplete_source_job(tmp_path, monkeypatch):
job, _png, _svg = _job(tmp_path)
data = json.loads(job.read_text(encoding="utf-8"))
del data["outputs"][0]["artifacts"]["svg"]
job.write_text(json.dumps(data), encoding="utf-8")
monkeypatch.setattr(
cli,
"_run_blender_script",
lambda *_: (_ for _ in ()).throw(AssertionError("Blender must not run")),
)
assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "strokes")) == 2