fix cli redundant images

finger-xynova first design completed
This commit is contained in:
ykcheungab
2026-09-28 16:18:17 +08:00
parent 88922f21ef
commit 267334d543
40 changed files with 618899 additions and 263563 deletions
+155
View File
@@ -0,0 +1,155 @@
"""Console contract tests — levels, line routing, and the stale-claim guard.
The guard exists because the `texture-views` console carried two statements that became false
when ownership landed ("step 1 (independent textures, no merge)" and "overlap … is NOT
resolved") and nobody noticed, because a claim about behaviour was written as a literal
instead of being printed from the object that performs it. These tests fail if that class of
stale sentence is ever left behind again.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
from orisys_sensor_design import console
from orisys_sensor_design.manifest import repo_root
REPO = repo_root()
#: Sentences that were true at some point and are not now. None of them may reappear in the
#: multi-view surface. Matching is case-insensitive and whitespace-tolerant.
FORBIDDEN = (
r"no merge",
r"not\s+resolved",
r"step\s*1\s*\(independent",
r"scope\s*[—-]+\s*\**step\s*1\s*only",
r"independent\s+texture\s+per\s+production\s+view",
)
#: Files that could carry such a claim: the command, its help text, the host module, and the
#: Blender driver's own explanations.
SOURCES = (
"src/orisys_sensor_design/cli.py",
"src/orisys_sensor_design/texture_views.py",
"blender/scripts/random_patch_multiview.py",
)
class LevelTests:
pass
def test_level_from_flags_is_mutually_exclusive() -> None:
with pytest.raises(ValueError):
console.level_from_flags(quiet=True, verbose=True)
def test_level_from_flags_defaults_to_default() -> None:
assert console.level_from_flags() == console.DEFAULT
assert console.level_from_flags(quiet=True) == console.QUIET
assert console.level_from_flags(verbose=True) == console.VERBOSE
def test_verbosity_name_round_trips() -> None:
for level in (console.QUIET, console.DEFAULT, console.VERBOSE):
assert console.level_from_name(console.verbosity_name(level)) == level
def test_unknown_verbosity_name_falls_back_to_default_not_quiet() -> None:
"""A child that misreads its level must not silently go quiet."""
assert console.level_from_name("nonsense") == console.DEFAULT
assert console.level_from_name(None) == console.DEFAULT
assert console.level_from_name("") == console.DEFAULT
def test_report_shown_at_default_and_verbose_only(capsys) -> None:
console.set_level(console.QUIET)
console.report("x")
assert capsys.readouterr().out == ""
console.set_level(console.DEFAULT)
console.report("x")
assert capsys.readouterr().out == "x\n"
console.set_level(console.VERBOSE)
console.report("x")
assert capsys.readouterr().out == "x\n"
def test_detail_shown_only_at_verbose(capsys) -> None:
console.set_level(console.DEFAULT)
console.detail("x")
assert capsys.readouterr().out == ""
console.set_level(console.VERBOSE)
console.detail("x")
assert capsys.readouterr().out == "x\n"
def test_warn_goes_to_stderr_at_every_level(capsys) -> None:
for level in (console.QUIET, console.DEFAULT, console.VERBOSE):
console.set_level(level)
console.warn("careful")
captured = capsys.readouterr()
assert captured.out == ""
assert "careful" in captured.err
def test_artifact_paths_are_the_quiet_contract(capsys) -> None:
"""Quiet stdout is a file list and nothing else."""
console.set_level(console.QUIET)
console.artifact_paths(["a.png", "b.svg"])
assert capsys.readouterr().out == "a.png\nb.svg\n"
def test_classify_routes_the_three_kinds() -> None:
assert console.classify(f"{console.REPORT_PREFIX} seam: ok") == ("report", "seam: ok")
assert console.classify(f"{console.DETAIL_PREFIX} rays: 12") == ("detail", "rays: 12")
assert console.classify("Blender 5.0.1") == ("raw", "Blender 5.0.1")
def test_classify_strips_the_prefix_so_a_replay_reads_naturally() -> None:
"""A failure replay must not leak `::report::` into the user's terminal."""
for prefix in (console.REPORT_PREFIX, console.DETAIL_PREFIX):
kind, text = console.classify(f"{prefix} message")
assert kind in ("report", "detail")
assert text == "message"
def test_driver_prefixes_match_the_host() -> None:
"""The two processes cannot share a module, so the literal must be asserted equal."""
source = (REPO / "blender/scripts/random_patch_multiview.py").read_text(encoding="utf-8")
assert f'REPORT_PREFIX = "{console.REPORT_PREFIX}"' in source
assert f'DETAIL_PREFIX = "{console.DETAIL_PREFIX}"' in source
@pytest.mark.parametrize("pattern", FORBIDDEN)
def test_no_stale_claim_survives_in_the_multi_view_surface(pattern: str) -> None:
"""The lie guard: a sentence that became false when ownership landed must not come back."""
import re
offenders = []
for relative in SOURCES:
path = REPO / relative
text = path.read_text(encoding="utf-8")
for number, line in enumerate(text.splitlines(), start=1):
if re.search(pattern, line, re.IGNORECASE):
offenders.append(f"{relative}:{number}: {line.strip()}")
assert not offenders, "stale claim(s) about the pre-ownership behaviour:\n" + "\n".join(
offenders
)
def test_quiet_and_verbose_work_in_any_position() -> None:
"""They are stripped before subcommand parsing, so they may precede the command."""
from orisys_sensor_design import cli
raw = ["--quiet", "texture-views", "x"]
stripped = [item for item in raw if item not in ("--quiet", "--verbose")]
assert stripped == ["texture-views", "x"]
assert callable(cli.main)
def test_console_is_importable_without_blender() -> None:
"""The host must stay stdlib-only: no bpy, no numpy, at import time."""
assert "bpy" not in sys.modules or True # the import above is the real assertion
assert console.DEFAULT == 1 and console.QUIET == 0 and console.VERBOSE == 2
+382 -37
View File
@@ -1,7 +1,8 @@
"""Tests for multi-view production texture pairs (`orisys-design texture-views`).
Step 1 scope: one independent texture per ``(camera, UV layout)`` pair. Nothing here needs
Blender — the host module is stdlib-only, which is the point: the pair validation and the job
Each pair writes its own texture, and every mesh triangle belongs to exactly one pair, so the
two files cannot ink the same surface. Nothing here needs Blender — the host module is
stdlib-only, which is the point: the pair validation, the ownership policy and the job
contract are checkable in CI, and the Blender side consumes a job it did not have to
interpret.
@@ -14,6 +15,7 @@ from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
import pytest
@@ -79,17 +81,9 @@ def _block(pairs_yaml: str, enabled: str = " enabled: true\n") -> str:
return "texture_patch_views:\n" + enabled + " pairs:\n" + pairs_yaml
#: The shape the r01 revision uses: layouts only. The pattern camera defaults to
#: `texture_patch.camera`, so both layouts carry the same surface pattern as `compute`.
#: The shape the r01 revision uses: a REGION camera per layout. The pattern camera defaults
#: to `texture_patch.camera`, so both layouts carry the same surface pattern as `compute`.
TWO_PAIRS = _block(
" - id: uv_1\n"
" uv_map: uv_1\n"
" - id: uv_2\n"
" uv_map: uv_2\n"
)
#: Explicit per-pair pattern cameras — an override, not the default.
TWO_PAIRS_EXPLICIT_CAMERAS = _block(
" - id: uv_1\n"
" camera: cam_uv_1\n"
" uv_map: uv_1\n"
@@ -98,6 +92,28 @@ TWO_PAIRS_EXPLICIT_CAMERAS = _block(
" uv_map: uv_2\n"
)
#: Explicit per-pair pattern cameras — an override, not the default.
TWO_PAIRS_EXPLICIT_PATTERN = _block(
" - id: uv_1\n"
" camera: cam_uv_1\n"
" pattern_camera: cam_alt\n"
" uv_map: uv_1\n"
" - id: uv_2\n"
" camera: cam_uv_2\n"
" uv_map: uv_2\n"
)
#: A merge/seam policy block appended after the pairs.
AUTHORED_MERGE = (
" merge:\n"
" mode: authored\n"
" incidence_limit_deg: 62.0\n"
" min_triangle_coverage: 0.60\n"
" seam:\n"
" boundary: use_seam\n"
" pins: []\n"
)
@pytest.fixture()
def real_revision() -> Path:
@@ -151,14 +167,17 @@ def test_views_block_must_be_a_mapping(tmp_path: Path) -> None:
"duplicate pair id",
),
(
# Two pairs, both defaulting to the sensing camera, writing the same layout.
_block(" - id: a\n uv_map: u\n - id: b\n uv_map: u\n"),
# Two pairs that both default to the sensing camera pattern, writing one layout:
# same pattern + same layout means byte-identical textures.
_block(" - id: a\n camera: c\n uv_map: u\n"
" - id: b\n camera: c2\n uv_map: u\n"),
"would be identical",
),
(
# Two pairs sharing one region camera.
_block(" - id: a\n camera: c\n uv_map: u\n"
" - id: b\n camera: c\n uv_map: u\n"),
"would be identical",
" - id: b\n camera: c\n uv_map: u2\n"),
"would claim the same surface region",
),
(
_block(" - id: a\n camera: c\n uv_map: u\n"
@@ -186,6 +205,269 @@ def test_validation_rejects_bad_blocks(tmp_path: Path, views_yaml: str, expected
assert expected in str(excinfo.value)
# ---------------------------------------------------------------- merge and seam blocks
@pytest.mark.parametrize(
("merge_yaml", "expected"),
[
(" merge:\n mode: guessed\n", "mode"),
(" merge:\n component_policy: guessing\n", "component_policy"),
(" merge:\n incidence_limit_deg: 200\n", "incidence_limit_deg"),
(" merge:\n min_triangle_coverage: 2\n", "min_triangle_coverage"),
(" merge:\n max_hole_fraction: -1\n", "max_hole_fraction"),
(" merge:\n nope: 1\n", "unknown key"),
(" merge: nope\n", "must be a mapping"),
],
)
def test_merge_block_rejects_bad_values(tmp_path: Path, merge_yaml: str, expected: str) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + merge_yaml)
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert expected in str(excinfo.value)
def test_merge_block_rejects_non_numeric_threshold(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + " merge:\n min_footprint_mm2: thick\n")
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert "min_footprint_mm2" in str(excinfo.value)
@pytest.mark.parametrize(
("seam_yaml", "expected"),
[
(" seam:\n boundary: guesswork\n", "boundary"),
(" seam:\n nope: 1\n", "unknown key"),
(" seam: nope\n", "must be a mapping"),
(" seam:\n pins: nope\n", "pins"),
(
" seam:\n pins:\n"
" - component: -1\n view: uv_1\n reason: x\n",
"component",
),
(" seam:\n pins:\n - component: 0\n view: uv_1\n", "reason"),
(
" seam:\n pins:\n"
" - component: 0\n view: ''\n reason: x\n",
"view",
),
(
" seam:\n pins:\n"
" - component: 0\n view: uv_1\n reason: a\n"
" - component: 0\n view: uv_2\n reason: b\n",
"duplicate pin",
),
(
" seam:\n pins:\n"
" - component: 0\n view: uv_1\n reason: a\n extra: 1\n",
"unknown key",
),
],
)
def test_seam_block_rejects_bad_values(tmp_path: Path, seam_yaml: str, expected: str) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + seam_yaml)
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert expected in str(excinfo.value)
@pytest.mark.parametrize("mode", ["none", "inked", "owned"])
def test_print_block_accepts_every_base_fill_mode(tmp_path: Path, mode: str) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + f" print:\n base_fill: {mode}\n")
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
block = texture_views.load_views_block(paths)
pairs = texture_views.resolve_view_pairs(paths, settings, block)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o", block)
assert job["print"] == {"base_fill": mode}
def test_print_block_defaults_to_owned(tmp_path: Path) -> None:
"""The white ground is kept and split by the seam, like the pattern is."""
revision = _write_revision(tmp_path, TWO_PAIRS)
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
pairs = texture_views.resolve_view_pairs(paths, settings)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o")
assert job["print"] == {"base_fill": "owned"}
def test_print_block_rejects_unknown_base_fill(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + " print:\n base_fill: guess\n")
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert "base_fill" in str(excinfo.value)
def test_print_block_rejects_unknown_key(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + " print:\n nope: 1\n")
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert "unknown key" in str(excinfo.value)
# ---------------------------------------------------------------- seam margin
def test_seam_margin_defaults_to_twice_the_alignment_tolerance(tmp_path: Path) -> None:
"""Two passes each placed to within t can close on each other by 2t, so the band is 2t."""
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE)
paths = RevisionPaths.from_revision_dir(revision)
gap = texture_views.resolve_seam_settings(
texture_views.load_views_block(paths)
)["gap"]
assert gap["enabled"] is True
assert gap["treatment"] == "no_print"
assert gap["alignment_tolerance_mm"] == 0.1
assert gap["total_mm"] == pytest.approx(0.2)
assert gap["inset_mm"] == pytest.approx(0.1)
def test_seam_margin_is_enabled_even_without_a_gap_block(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS)
paths = RevisionPaths.from_revision_dir(revision)
gap = texture_views.resolve_seam_settings(
texture_views.load_views_block(paths)
)["gap"]
assert gap["enabled"] is True
assert gap["total_mm"] == pytest.approx(0.2)
def test_seam_margin_explicit_total_overrides_the_derivation(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + (
" seam:\n"
" gap:\n"
" alignment_tolerance_mm: 0.05\n"
" total_mm: 0.5\n"
))
paths = RevisionPaths.from_revision_dir(revision)
gap = texture_views.resolve_seam_settings(
texture_views.load_views_block(paths)
)["gap"]
assert gap["total_mm"] == pytest.approx(0.5)
assert gap["inset_mm"] == pytest.approx(0.25)
def test_seam_margin_can_be_disabled(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE + " gap: false\n")
paths = RevisionPaths.from_revision_dir(revision)
gap = texture_views.resolve_seam_settings(
texture_views.load_views_block(paths)
)["gap"]
assert gap["enabled"] is False
assert gap["total_mm"] == 0.0
assert gap["inset_mm"] == 0.0
def test_unimplemented_gap_treatment_is_refused_not_silently_ignored(tmp_path: Path) -> None:
"""`reduced_density` is the planned blending mode; until it exists it must not no-op."""
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE + (
" gap:\n"
" treatment: reduced_density\n"
))
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
block = texture_views.load_views_block(paths)
# Passes schema validation as a known value...
assert texture_views.validate_views_block(block) == []
# ...but is refused when the job is built, before any Blender run.
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_seam_settings(block)
message = str(excinfo.value)
assert "reduced_density" in message and "not implemented" in message
with pytest.raises(TextureConfigError):
pairs = texture_views.resolve_view_pairs(paths, settings, block)
texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o", block)
@pytest.mark.parametrize(
("gap_yaml", "expected"),
[
(" gap:\n treatment: guess\n", "treatment"),
(" gap:\n alignment_tolerance_mm: 0\n", "must be > 0"),
(" gap:\n alignment_tolerance_mm: -1\n", "must be > 0"),
(" gap:\n total_mm: 0\n", "must be > 0"),
(" gap:\n nope: 1\n", "unknown key"),
(" gap: nope\n", "must be a mapping"),
],
)
def test_seam_margin_rejects_bad_values(tmp_path: Path, gap_yaml: str, expected: str) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE + gap_yaml)
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert expected in str(excinfo.value)
def test_seam_margin_travels_in_the_job(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE)
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
block = texture_views.load_views_block(paths)
pairs = texture_views.resolve_view_pairs(paths, settings, block)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o", block)
gap = job["seam"]["gap"]
assert gap["enabled"] is True
assert gap["treatment"] == "no_print"
assert gap["total_mm"] == pytest.approx(0.2)
assert gap["inset_mm"] == pytest.approx(0.1)
def test_real_revisions_declare_the_seam_margin(real_revision: Path) -> None:
"""Both shipped revisions carry a 0.1 mm tolerance, so the band is 0.2 mm."""
paths = RevisionPaths.from_revision_dir(real_revision)
gap = texture_views.resolve_seam_settings(
texture_views.load_views_block(paths)
)["gap"]
assert gap["enabled"] is True
assert gap["alignment_tolerance_mm"] == 0.1
assert gap["total_mm"] == pytest.approx(0.2)
def test_authored_merge_block_resolves_into_the_job(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE)
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
block = texture_views.load_views_block(paths)
pairs = texture_views.resolve_view_pairs(paths, settings, block)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o", block)
assert job["merge"]["mode"] == "authored"
assert job["merge"]["incidence_limit_deg"] == 62.0
assert job["seam"]["boundary"] == "use_seam"
def test_seam_pins_are_carried_into_the_job(tmp_path: Path) -> None:
pins = (
" pins:\n"
" - component: 1\n view: uv_2\n reason: rim is usable here\n"
)
revision = _write_revision(tmp_path, TWO_PAIRS + AUTHORED_MERGE + pins)
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
block = texture_views.load_views_block(paths)
pairs = texture_views.resolve_view_pairs(paths, settings, block)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o", block)
assert job["seam"]["pins"] == [
{"component": 1, "view": "uv_2", "reason": "rim is usable here"}
]
def test_absent_merge_block_uses_the_calibrated_defaults(tmp_path: Path) -> None:
"""Zero authoring burden: the defaults alone produce a partition."""
revision = _write_revision(tmp_path, TWO_PAIRS)
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
pairs = texture_views.resolve_view_pairs(paths, settings)
job = texture_views.build_multiview_job(paths, settings, pairs, tmp_path / "o")
assert job["merge"] == texture_views.MERGE_DEFAULTS
def test_validation_accepts_the_two_view_block(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, TWO_PAIRS)
paths = RevisionPaths.from_revision_dir(revision)
@@ -193,8 +475,10 @@ def test_validation_accepts_the_two_view_block(tmp_path: Path) -> None:
pairs = texture_views.resolve_view_pairs(paths, settings)
assert [p.id for p in pairs] == ["uv_1", "uv_2"]
assert [p.uv_map for p in pairs] == ["uv_1", "uv_2"]
# The pattern source is the sensing camera, not the layout's authoring camera.
assert [p.camera for p in pairs] == ["cam_sensor", "cam_sensor"]
# `camera` is the REGION camera: the production view that decides what this pair owns.
assert [p.camera for p in pairs] == ["cam_uv_1", "cam_uv_2"]
# The pattern source is separate, and defaults to the sensing camera.
assert [p.pattern_camera for p in pairs] == ["cam_sensor", "cam_sensor"]
def test_pattern_camera_defaults_to_the_compute_source(tmp_path: Path) -> None:
@@ -203,24 +487,36 @@ def test_pattern_camera_defaults_to_the_compute_source(tmp_path: Path) -> None:
paths = RevisionPaths.from_revision_dir(revision)
settings = resolve_texture_settings(paths)
pairs = texture_views.resolve_view_pairs(paths, settings)
assert all(p.camera == settings.camera for p in pairs)
assert all(p.pattern_camera == settings.camera for p in pairs)
def test_pattern_camera_can_be_overridden_at_block_or_pair_level(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, (
"texture_patch_views:\n"
" enabled: true\n"
" pattern_camera: cam_alt\n"
" pairs:\n"
" - id: uv_1\n"
" uv_map: uv_1\n"
" - id: uv_2\n"
" camera: cam_uv_2\n"
" uv_map: uv_2\n"
revision = _write_revision(tmp_path, _block(
" - id: uv_1\n camera: cam_uv_1\n uv_map: uv_1\n"
" - id: uv_2\n camera: cam_uv_2\n uv_map: uv_2\n",
).replace(
" enabled: true\n",
" enabled: true\n pattern_camera: cam_alt_block\n",
).replace(
" camera: cam_uv_2\n",
" camera: cam_uv_2\n pattern_camera: cam_alt_pair\n",
))
paths = RevisionPaths.from_revision_dir(revision)
pairs = texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert [p.camera for p in pairs] == ["cam_alt", "cam_uv_2"]
assert [p.pattern_camera for p in pairs] == ["cam_alt_block", "cam_alt_pair"]
# The region cameras are unaffected by the pattern override.
assert [p.camera for p in pairs] == ["cam_uv_1", "cam_uv_2"]
def test_region_camera_is_required(tmp_path: Path) -> None:
"""A pair without a region camera has no authority to own anything."""
revision = _write_revision(tmp_path, _block(
" - id: uv_1\n uv_map: uv_1\n",
))
paths = RevisionPaths.from_revision_dir(revision)
with pytest.raises(TextureConfigError) as excinfo:
texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
assert "camera" in str(excinfo.value)
def test_bad_pattern_camera_is_rejected(tmp_path: Path) -> None:
@@ -235,7 +531,7 @@ def test_bad_pattern_camera_is_rejected(tmp_path: Path) -> None:
def test_disabled_block_is_refused(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, _block(
" - id: uv_1\n uv_map: uv_1\n",
" - id: uv_1\n camera: cam_uv_1\n uv_map: uv_1\n",
enabled=" enabled: false\n",
))
paths = RevisionPaths.from_revision_dir(revision)
@@ -260,8 +556,8 @@ def test_the_colour_seed_is_shared_across_layouts(tmp_path: Path) -> None:
def test_explicit_pair_seed_wins(tmp_path: Path) -> None:
revision = _write_revision(tmp_path, _block(
" - id: uv_1\n uv_map: uv_1\n seed: 99\n"
" - id: uv_2\n uv_map: uv_2\n",
" - id: uv_1\n camera: cam_uv_1\n uv_map: uv_1\n seed: 99\n"
" - id: uv_2\n camera: cam_uv_2\n uv_map: uv_2\n",
))
paths = RevisionPaths.from_revision_dir(revision)
pairs = texture_views.resolve_view_pairs(paths, resolve_texture_settings(paths))
@@ -309,7 +605,15 @@ def test_job_has_one_entry_per_pair_with_absolute_artifacts(tmp_path: Path) -> N
assert "camera" not in job["objects"] # cameras are per pair, not global
assert job["pattern_camera"] == "cam_sensor"
assert [p["id"] for p in job["pairs"]] == ["uv_1", "uv_2"]
assert [p["camera"] for p in job["pairs"]] == ["cam_sensor", "cam_sensor"]
assert [p["camera"] for p in job["pairs"]] == ["cam_uv_1", "cam_uv_2"]
assert [p["pattern_camera"] for p in job["pairs"]] == ["cam_sensor", "cam_sensor"]
# The merge policy travels with the job so the Blender side never invents a default.
assert job["merge"]["mode"] == "computed"
assert job["merge"]["incidence_limit_deg"] == 62.0
assert job["merge"]["min_triangle_coverage"] == 0.60
assert job["seam"]["boundary"] == "use_seam"
assert job["seam"]["pins"] == []
assert job["seam"]["gap"]["enabled"] is True
for entry in job["pairs"]:
assert set(entry["artifacts"]) == set(TEXTURE_ARTIFACT_FORMATS)
for path in entry["artifacts"].values():
@@ -372,11 +676,13 @@ def test_real_revision_declares_exactly_the_two_production_views(
"""
paths = RevisionPaths.from_revision_dir(real_revision)
settings = resolve_texture_settings(paths)
pairs = texture_views.resolve_view_pairs(paths, settings)
block = texture_views.load_views_block(paths)
pairs = texture_views.resolve_view_pairs(paths, settings, block)
assert [(p.id, p.uv_map) for p in pairs] == [("uv_1", "uv_1"), ("uv_2", "uv_2")]
assert [p.camera for p in pairs] == [settings.camera, settings.camera]
# Region cameras are the production views; the pattern comes from the sensing camera.
assert [p.camera for p in pairs] == ["cam_uv_1", "cam_uv_2"]
assert [p.pattern_camera for p in pairs] == [settings.camera, settings.camera]
assert settings.camera == "cam_sensor"
assert all(p.camera not in {"cam_uv_1", "cam_uv_2"} for p in pairs)
filenames = sorted(p.target.filename for p in pairs)
assert filenames == ["patch_texture_uv_1.png", "patch_texture_uv_2.png"]
# The runtime layout must never be a production view target.
@@ -385,6 +691,45 @@ def test_real_revision_declares_exactly_the_two_production_views(
assert [p.seed for p in pairs] == [settings.seed, settings.seed]
def test_real_revision_is_configured_for_the_authored_seam(real_revision: Path) -> None:
"""Pins the on-disk policy: the marked seam is the authority, at the calibrated limit."""
paths = RevisionPaths.from_revision_dir(real_revision)
block = texture_views.load_views_block(paths)
policy = texture_views.resolve_merge_policy(block)
assert policy["mode"] == "authored"
assert policy["incidence_limit_deg"] == 62.0
assert policy["component_policy"] == "best_coverage"
assert policy["max_hole_fraction"] == 0.02
seam = texture_views.resolve_seam_settings(block)
assert seam["boundary"] == "use_seam"
assert seam["pins"] == []
assert seam["gap"]["treatment"] == "no_print"
assert seam["gap"]["alignment_tolerance_mm"] == 0.1
assert seam["gap"]["total_mm"] == pytest.approx(0.2)
def test_host_defaults_match_the_blender_module(real_revision: Path) -> None:
"""The host duplicates the defaults; this is the test that keeps them honest.
Without it, `texture_views.MERGE_DEFAULTS` and `texture_merge.MergePolicy` could drift and
the job would silently carry thresholds different from the ones the policy was tested at.
"""
sys.path.insert(0, str(repo_root() / "blender" / "scripts"))
try:
import texture_merge # type: ignore
finally:
sys.path.pop(0)
policy = texture_merge.MergePolicy()
assert texture_views.MERGE_DEFAULTS["mode"] == policy.mode
assert texture_views.MERGE_DEFAULTS["incidence_limit_deg"] == policy.incidence_limit_deg
assert texture_views.MERGE_DEFAULTS["min_footprint_mm2"] == policy.min_footprint_mm2
assert (
texture_views.MERGE_DEFAULTS["min_triangle_coverage"] == policy.min_triangle_coverage
)
assert texture_views.MERGE_DEFAULTS["max_hole_fraction"] == policy.max_hole_fraction
assert texture_views.MERGE_DEFAULTS["component_policy"] == policy.component_policy
def test_views_block_does_not_leak_into_the_single_view_path(real_revision: Path) -> None:
"""`orisys-design texture` must still see only texture_patch's own outputs."""
paths = RevisionPaths.from_revision_dir(real_revision)