增加笔画生产尺寸与数量报告
按最终矢量页面汇总每个相机单元的笔画包络,并标出低于 0.2 mm 的数量。 已核对 SVG、清单哈希和 Blender 导出,175 项相关测试通过。
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""Production dimensions are measured per visible stroke, not per UV triangle."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parents[1] / "blender" / "scripts"
|
||||
sys.path.insert(0, str(SCRIPT_DIR))
|
||||
|
||||
from stroke_production_report import summarize_view, write_report # noqa: E402
|
||||
|
||||
|
||||
def test_fragments_of_one_cell_are_one_stroke(tmp_path):
|
||||
svg = tmp_path / "stroke_texture_uv_1.svg"
|
||||
svg.write_text("<svg/>", encoding="utf-8")
|
||||
# 100 px/mm; the first 0.6 x 0.2 mm mark is split by a UV triangle.
|
||||
polygons = [
|
||||
(1, 2, [(10, 10), (40, 10), (40, 30), (10, 30)]),
|
||||
(1, 2, [(40, 10), (70, 10), (70, 30), (40, 30)]),
|
||||
(3, 4, [(100, 10), (130, 10), (130, 20), (100, 20)]),
|
||||
]
|
||||
mesh = [[(0, 0), (200, 0), (200, 200), (0, 200)]]
|
||||
view = summarize_view("uv_1", "uv_1", polygons, mesh,
|
||||
(1000, 1000), (10, 10), svg)
|
||||
assert view["stroke_count"] == 2
|
||||
assert view["vector_fragment_count"] == 3
|
||||
assert view["width_mm"]["min"] == pytest.approx(0.1)
|
||||
assert view["width_mm"]["max"] == pytest.approx(0.2)
|
||||
assert view["length_mm"]["max"] == pytest.approx(0.6)
|
||||
assert view["width_below_target_count"] == 1
|
||||
assert view["black_to_base_area_ratio_estimate"] == pytest.approx(0.0375)
|
||||
|
||||
|
||||
def test_rotated_stroke_dimensions_use_print_page_units(tmp_path):
|
||||
svg = tmp_path / "stroke.svg"
|
||||
svg.write_text("<svg/>", encoding="utf-8")
|
||||
angle = math.radians(31)
|
||||
ux, uy = math.cos(angle), math.sin(angle)
|
||||
vx, vy = -uy, ux
|
||||
# 2 px/mm horizontally, 4 px/mm vertically: convert the mm corners to pixels.
|
||||
corners = [(sx * 0.3 * ux + sy * 0.1 * vx,
|
||||
sx * 0.3 * uy + sy * 0.1 * vy) for sx, sy in
|
||||
((-1, -1), (1, -1), (1, 1), (-1, 1))]
|
||||
pixels = [(x * 2 + 50, y * 4 + 50) for x, y in corners]
|
||||
view = summarize_view("compute", "compute", [(7, 8, pixels)], [],
|
||||
(200, 400), (100, 100), svg)
|
||||
assert view["width_mm"]["min"] == pytest.approx(0.2)
|
||||
assert view["length_mm"]["max"] == pytest.approx(0.6)
|
||||
|
||||
|
||||
def test_report_counts_split_camera_cells_across_views(tmp_path):
|
||||
source = tmp_path / "job.json"
|
||||
source.write_text('{"settings": {"nx": 10, "ny": 8}}', encoding="utf-8")
|
||||
a = {"camera_cells": [[1, 2], [3, 4]], "stroke_count": 2}
|
||||
b = {"camera_cells": [[3, 4], [5, 6]], "stroke_count": 2}
|
||||
output = tmp_path / "report.json"
|
||||
report = write_report(output, source, {"seed": 7}, [a, b])
|
||||
assert report["unique_camera_cell_count"] == 3
|
||||
assert report["cells_in_multiple_views"] == 1
|
||||
assert report["total_view_strokes"] == 4
|
||||
assert report["camera_grid"] == [10, 8]
|
||||
assert json.loads(output.read_text(encoding="utf-8"))["width_target_mm"] == 0.2
|
||||
@@ -12,6 +12,21 @@ from orisys_sensor_design.manifest import RevisionPaths, sha256_file
|
||||
REVISION = Path(__file__).resolve().parents[1] / "sensors" / "finger-xynova" / "r01"
|
||||
|
||||
|
||||
def _production_report(output_dir: Path, ids: tuple[str, ...]) -> None:
|
||||
views = []
|
||||
for view_id in ids:
|
||||
svg = output_dir / f"stroke_texture_{view_id}.svg"
|
||||
views.append({
|
||||
"id": view_id, "stroke_count": 3, "width_mm": {"min": 0.1, "max": 0.3},
|
||||
"length_mm": {"min": 0.2, "max": 0.5}, "width_below_target_count": 1,
|
||||
"svg": {"sha256": sha256_file(svg)},
|
||||
})
|
||||
(output_dir / "stroke_texture_production_report.json").write_text(json.dumps({
|
||||
"schema": "orisys-sensor-design/stroke-production-report-v1",
|
||||
"width_target_mm": 0.2, "views": views,
|
||||
}), encoding="utf-8")
|
||||
|
||||
|
||||
def _args(job: Path, output_dir: Path, views=False) -> argparse.Namespace:
|
||||
return argparse.Namespace(
|
||||
views=views, source_job=job, output_dir=output_dir,
|
||||
@@ -42,6 +57,7 @@ def test_exports_from_geometry_job(tmp_path, monkeypatch):
|
||||
calls.append((script, blend, args))
|
||||
for fmt in ("png", "svg", "pdf"):
|
||||
(output_dir / f"stroke_texture_compute.{fmt}").write_bytes(fmt.encode())
|
||||
_production_report(output_dir, ("compute",))
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(cli, "_run_blender_script", run_blender)
|
||||
@@ -55,6 +71,7 @@ def test_exports_from_geometry_job(tmp_path, monkeypatch):
|
||||
assert manifest["schema"] == "orisys-sensor-design/stroke-texture-manifest-v2"
|
||||
assert manifest["source_job"]["sha256"] == sha256_file(job)
|
||||
assert set(manifest["outputs"][0]["artifacts"]) == {"png", "svg", "pdf"}
|
||||
assert "stroke_texture_production_report.json" in manifest["auxiliary"]
|
||||
assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 2
|
||||
assert len(calls) == 1
|
||||
|
||||
@@ -78,6 +95,7 @@ def test_multiview_exports_owned_pairs_without_colour_artwork(tmp_path, monkeypa
|
||||
(output_dir / f"stroke_texture_{pair}.{fmt}").write_bytes(pair.encode())
|
||||
for name in ("_multiview_ground_truth.json", "_print_views.json"):
|
||||
(output_dir / name).write_text("{}", encoding="utf-8")
|
||||
_production_report(output_dir, ("uv_1", "uv_2"))
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(cli, "_run_blender_script", run_blender)
|
||||
@@ -87,5 +105,24 @@ def test_multiview_exports_owned_pairs_without_colour_artwork(tmp_path, monkeypa
|
||||
manifest = json.loads((output_dir / "stroke_texture_manifest.json").read_text())
|
||||
assert manifest["views"] is True
|
||||
assert [entry["id"] for entry in manifest["outputs"]] == ["uv_1", "uv_2"]
|
||||
assert set(manifest["auxiliary"]) == {"_multiview_ground_truth.json", "_print_views.json"}
|
||||
assert set(manifest["auxiliary"]) == {
|
||||
"_multiview_ground_truth.json", "_print_views.json",
|
||||
"stroke_texture_production_report.json",
|
||||
}
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_rejects_report_for_a_different_svg(tmp_path, monkeypatch):
|
||||
job = _job(tmp_path)
|
||||
output_dir = tmp_path / "strokes"
|
||||
|
||||
def run_blender(_script, _blend, _args):
|
||||
for fmt in ("png", "svg", "pdf"):
|
||||
(output_dir / f"stroke_texture_compute.{fmt}").write_bytes(fmt.encode())
|
||||
_production_report(output_dir, ("compute",))
|
||||
(output_dir / "stroke_texture_compute.svg").write_bytes(b"changed")
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(cli, "_run_blender_script", run_blender)
|
||||
assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir)) == 3
|
||||
assert not (output_dir / "stroke_texture_manifest.json").exists()
|
||||
|
||||
Reference in New Issue
Block a user