改用相机网格生成笔画纹理
统一笔画配置与彩色 patch 的投影网格,替换旧脚本及产物。 已验证 Blender 导出、16 项相关测试、文档与 lint 门禁。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""The stroke CLI must preserve colour sources and verify all exported formats."""
|
||||
"""The stroke CLI uses the texture geometry job and verifies its outputs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,96 +12,54 @@ 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:
|
||||
def _args(job: Path, output_dir: Path, views=False) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
views=False,
|
||||
source_job=job,
|
||||
output_dir=output_dir,
|
||||
camera_width_fraction=0.5,
|
||||
seed=13,
|
||||
views=views, source_job=job, output_dir=output_dir,
|
||||
width_ratio=0.4, length_ratio=0.8, orientation_mode=None,
|
||||
proposals=None, seed=13,
|
||||
)
|
||||
|
||||
|
||||
def _job(tmp_path: Path) -> tuple[Path, Path, Path]:
|
||||
def _job(tmp_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
|
||||
job = tmp_path / "_texture_job.json"
|
||||
job.write_text(json.dumps({
|
||||
"schema": "orisys-sensor-design/texture-job-v3",
|
||||
"revision_dir": str(paths.root), "source_blend": str(paths.source_blend),
|
||||
"settings": {"nx": 80, "ny": 80},
|
||||
"objects": {"mesh": "sensing_surface", "camera": "cam_sensor"},
|
||||
"outputs": [{"uv_map": "compute"}],
|
||||
}), encoding="utf-8")
|
||||
return job
|
||||
|
||||
|
||||
def test_stroke_cli_exports_three_formats_and_manifest(tmp_path, monkeypatch):
|
||||
job, png, svg = _job(tmp_path)
|
||||
def test_exports_from_geometry_job(tmp_path, monkeypatch):
|
||||
job = _job(tmp_path)
|
||||
output_dir = tmp_path / "strokes"
|
||||
calls = []
|
||||
|
||||
def run_blender(script, blend, args):
|
||||
calls.append((script, blend, args))
|
||||
# exist_ok: the CLI writes _stroke_pattern_params.json into the output directory before
|
||||
# it launches Blender, so the directory already exists.
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
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"
|
||||
# `--seed` and `--camera-width-fraction` are overrides folded into the params file, so the
|
||||
# contract is now the effective tuning the script reads.
|
||||
params = json.loads(
|
||||
Path(calls[0][2][calls[0][2].index("--params") + 1]).read_text(encoding="utf-8")
|
||||
)
|
||||
assert calls[0][0] == "camera_grid_stroke.py"
|
||||
params_path = Path(calls[0][2][calls[0][2].index("--params") + 1])
|
||||
params = json.loads(params_path.read_text(encoding="utf-8"))
|
||||
assert params["seed"] == 13
|
||||
assert params["width_fraction"] == 0.5
|
||||
assert params["width_ratio"] == 0.4
|
||||
manifest = json.loads((output_dir / "stroke_texture_manifest.json").read_text())
|
||||
assert manifest["schema"] == "orisys-sensor-design/stroke-texture-manifest-v1"
|
||||
assert manifest["schema"] == "orisys-sensor-design/stroke-texture-manifest-v2"
|
||||
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
|
||||
def test_rejects_multiview(tmp_path, monkeypatch):
|
||||
job = _job(tmp_path)
|
||||
monkeypatch.setattr(cli, "_run_blender_script", lambda *_: 99)
|
||||
assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "out", views=True)) == 2
|
||||
|
||||
Reference in New Issue
Block a user