fix algining xynova texture to uv printing fixture.

This commit is contained in:
ykcheungab
2026-09-29 16:33:31 +08:00
parent 267334d543
commit 37e3e8400b
33 changed files with 758477 additions and 886701 deletions
+10 -1
View File
@@ -579,8 +579,17 @@ def cmd_texture_views(revision_dir: Path, args: argparse.Namespace) -> int:
by_id = {view.get("id"): view for view in print_views.get("views", [])}
for pair, _artifacts in targets:
view = by_id.get(pair.id) or {}
page = view.get("page_mm") or []
source = view.get("page_source")
# The derived page is read back from what the run wrote, never recomputed here, so
# the console cannot describe a page the run did not use.
page_text = (
f" page {page[0]:.3f}x{page[1]:.3f}mm ({source})"
if len(page) == 2
else ""
)
console.report(
f" [{pair.id}] region={pair.camera} -> {pair.uv_map}"
f" [{pair.id}] region={pair.camera} -> {pair.uv_map}{page_text}"
f" owns {view.get('owned_triangles', 0):,} triangles,"
f" emits {view.get('emitted_fragments', 0):,} fragments"
)
+66 -2
View File
@@ -64,8 +64,8 @@ MULTIVIEW_JOB_FILENAME = "_texture_views_job.json"
VIEWS_BLOCK_KEY = "texture_patch_views"
_BLOCK_KEYS = frozenset({"enabled", "pattern_camera", "pairs", "merge", "seam", "registration",
"print"})
_PAIR_KEYS = frozenset({"id", "camera", "uv_map", "filename", "seed", "pattern_camera"})
"print", "page"})
_PAIR_KEYS = frozenset({"id", "camera", "uv_map", "filename", "seed", "pattern_camera", "page"})
_MERGE_KEYS = frozenset({
"mode", "incidence_limit_deg", "min_footprint_mm2", "min_triangle_coverage",
"max_hole_fraction", "component_policy",
@@ -94,6 +94,20 @@ GAP_TREATMENTS = ("no_print", "reduced_density")
GAP_DEFAULT_TREATMENT = "no_print"
GAP_DEFAULT_ALIGNMENT_TOLERANCE_MM = 0.1
_PRINT_KEYS = frozenset({"base_fill"})
_PAGE_KEYS = frozenset({"mode", "px_per_mm"})
#: How a pair's page is chosen.
#:
#: ``auto`` derive it from the geometry: a layout authored by *Project from View (Bounds)*
#: from its region camera maps [0,1]^2 to that camera's image-plane extent, so the
#: page is measurable. Only valid for such a layout, and the driver **proves** it is
#: one before using the result.
#: ``fixed`` keep ``texture_patch.vector``. Required for unwrapped layouts (`produce`,
#: `compute`), which are not projections and for which a camera-derived page would
#: be meaningless. Also the escape hatch when `auto` refuses.
PAGE_MODES = ("auto", "fixed")
PAGE_DEFAULT_PX_PER_MM = 200.0
PAGE_DEFAULTS = {"mode": "auto", "px_per_mm": PAGE_DEFAULT_PX_PER_MM}
#: What is drawn under the colour patches, i.e. the white ground the marker sits on.
#:
@@ -151,6 +165,7 @@ class ViewPair:
uv_map: str
target: TextureOutputTarget
seed: int | None
page_mode: str = PAGE_DEFAULTS["mode"]
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
return {
@@ -159,6 +174,7 @@ class ViewPair:
"pattern_camera": self.pattern_camera,
"uv_map": self.uv_map,
"seed": self.seed,
"page": self.page_mode,
"artifacts": {
format_name: str(path)
for format_name, path in self.target.artifact_paths(output_dir).items()
@@ -235,6 +251,12 @@ def validate_views_block(
value = pair.get("pattern_camera")
if not isinstance(value, str) or not value.strip():
errors.append(f"{where}.pattern_camera: must be a non-empty object name")
if "page" in pair:
value = pair.get("page")
if value not in PAGE_MODES:
errors.append(
f"{where}.page: must be one of {list(PAGE_MODES)} (got {value!r})"
)
pair_id = pair.get("id")
if isinstance(pair_id, str) and pair_id.strip():
if pair_id in seen_ids:
@@ -280,6 +302,7 @@ def validate_views_block(
)
seen_camera_layout[key] = str(pair_id)
errors.extend(validate_page_block(block.get("page")))
errors.extend(validate_merge_block(block.get("merge")))
errors.extend(validate_seam_block(block.get("seam")))
errors.extend(validate_print_block(block.get("print")))
@@ -303,6 +326,28 @@ def validate_print_block(print_block: Any) -> list[str]:
return errors
def validate_page_block(page: Any) -> list[str]:
"""Validate the optional ``page`` sub-block (how each pair's page is sized)."""
if page is None:
return []
where = f"{VIEWS_BLOCK_KEY}.page"
if not isinstance(page, Mapping):
return [f"{where}: must be a mapping"]
errors: list[str] = []
extra = sorted(set(page) - _PAGE_KEYS)
if extra:
errors.append(f"{where}: unknown key(s) {extra}; allowed: {sorted(_PAGE_KEYS)}")
mode = page.get("mode", PAGE_DEFAULTS["mode"])
if mode not in PAGE_MODES:
errors.append(f"{where}.mode: must be one of {list(PAGE_MODES)} (got {mode!r})")
density = page.get("px_per_mm", PAGE_DEFAULTS["px_per_mm"])
if not isinstance(density, (int, float)) or isinstance(density, bool):
errors.append(f"{where}.px_per_mm: must be a number")
elif float(density) <= 0.0:
errors.append(f"{where}.px_per_mm: must be > 0 (got {density!r})")
return errors
def validate_merge_block(merge: Any) -> list[str]:
"""Validate the optional ``merge`` sub-block (ownership policy)."""
if merge is None:
@@ -453,6 +498,7 @@ def resolve_view_pairs(
# View) and define each pair's REGION for ownership; neither is the pattern source, so
# the pattern default is the sensing camera that also defines `compute`.
pattern_camera = str(block.get("pattern_camera") or settings.camera)
default_page_mode = str(resolve_page_settings(block)["mode"])
base_seed = settings.seed
pairs: list[ViewPair] = []
for raw in block["pairs"]:
@@ -473,6 +519,7 @@ def resolve_view_pairs(
filename=str(raw.get("filename") or _default_filename(pair_id)),
),
seed=None if seed is None else int(seed),
page_mode=str(raw.get("page") or default_page_mode),
)
)
return tuple(pairs)
@@ -554,6 +601,22 @@ def resolve_print_settings(block: Mapping[str, Any] | None) -> dict[str, Any]:
return settings
def resolve_page_settings(block: Mapping[str, Any] | None) -> dict[str, Any]:
"""Resolve the optional ``page`` sub-block into the job's page settings.
The page itself is not computed here: deriving it needs the mesh and the camera, so the
driver does it and the host only carries the mode and the raster density.
"""
settings = dict(PAGE_DEFAULTS)
sub = (block or {}).get("page") or {}
if isinstance(sub, Mapping):
for key in _PAGE_KEYS:
if key in sub:
settings[key] = sub[key]
settings["px_per_mm"] = float(settings["px_per_mm"])
return settings
def build_multiview_job(
paths: RevisionPaths,
settings: TexturePatchSettings,
@@ -579,6 +642,7 @@ def build_multiview_job(
"merge": resolve_merge_policy(block),
"seam": resolve_seam_settings(block),
"print": resolve_print_settings(block),
"page": resolve_page_settings(block),
"settings": settings.to_job_settings(),
"pairs": [pair.to_job_entry(Path(output_dir)) for pair in pairs],
}