new grayscale output for testing laser, not working, will change to new design

This commit is contained in:
ykcheungab
2026-09-30 16:12:06 +08:00
parent 37e3e8400b
commit f0454253c2
27 changed files with 381132 additions and 270 deletions
+139 -1
View File
@@ -46,9 +46,13 @@ from random_patch_dd02c import ( # noqa: E402
_require_uv_layer,
_uv_centre,
build_patch_geometry,
fill_polygon_value,
generate_patch_texture,
precompute_cell_colors,
write_pdf_vector,
write_svg_vector,
)
from patch_geometry import clip_rect # noqa: E402
from texture_merge import ( # noqa: E402
MergeConfigError,
MergePolicy,
@@ -183,8 +187,12 @@ def _resolve_pairs(job: dict[str, Any]) -> list[dict[str, Any]]:
if not isinstance(artifacts, dict):
raise ConfigError(f"{where}.artifacts: must be a mapping")
paths: dict[str, str] = {}
for format_name in ARTIFACT_FORMATS:
# The three print formats are required. The `gray_*` laser-engraving set is optional,
# so it is validated only when the job asks for it.
for format_name in (*ARTIFACT_FORMATS, *(f"gray_{f}" for f in ARTIFACT_FORMATS)):
value = artifacts.get(format_name)
if value is None and format_name.startswith("gray_"):
continue
if not isinstance(value, str) or not value.strip():
raise ConfigError(
f"{where}.artifacts.{format_name}: must be a non-empty path"
@@ -207,6 +215,9 @@ def _resolve_pairs(job: dict[str, Any]) -> list[dict[str, Any]]:
"pattern_camera": pattern_camera,
"uv_map": uv_map,
"seed": None if seed is None else int(seed),
# Carried through, not defaulted: dropping this silently ignored every
# per-pair `page:` override because the global default masked it.
"page": entry.get("page"),
"artifacts": paths,
}
)
@@ -537,6 +548,100 @@ def _triangle_seam_edges(
return out
def _gray_pixel_polygon(uv_polygon, width, height):
"""The colour path's UV -> pixel mapping, reproduced exactly.
`generate_patch_texture` builds this as a nested closure over `texture_width`/`height`, so
it cannot be imported. Duplicating these two lines is the cost of keeping the gray image
registered with the colour image: **if the colour mapping ever changes, this must too.**
"""
return [
(u * (width - 1), v * (height - 1))
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)
]
def _gray_shapes(patches, patch_colors, width, height):
"""The laser-engraving artwork as (pixel_polygon, rgba) shapes.
**Polarity is inverted relative to the print texture, deliberately.** Most laser software
engraves what is DARK, and the print texture's background is black -- so a straight
luminance conversion would engrave the entire sheet and leave the marker cells almost
untouched. Here the page and the mesh ground are white (untouched) and only the pattern
cells carry tone, so the laser cuts the marker and nothing else.
Tone is Rec.709 luminance of the colour the cell would have been printed in, so engraving
depth varies cell to cell the way printed colour does. The seam margin is inherited from
``patches``, which is the already-clipped emitted set, so every gray format carries the
same alignment band as the colour file.
"""
shapes = []
for i, j, uv_poly in patches:
pixel_poly = _gray_pixel_polygon(uv_poly, width, height)
if len(pixel_poly) < 3:
continue
red, green, blue = patch_colors[j, i]
tone = 0.2126 * float(red) + 0.7152 * float(green) + 0.0722 * float(blue)
shapes.append((pixel_poly, (tone, tone, tone, 1.0)))
return shapes
def _write_grayscale_png(path, width, height, shapes):
"""Rasterise the laser-engraving artwork: pattern dark on white."""
grid = np.ones((height, width), dtype=np.float32)
for pixel_poly, (tone, _g, _b, _a) in shapes:
fill_polygon_value(grid, width, height, pixel_poly, tone)
rgba = np.ones((height, width, 4), dtype=np.float32)
rgba[:, :, 0] = grid
rgba[:, :, 1] = grid
rgba[:, :, 2] = grid
# Same row convention as the colour path: fill_polygon_value indexes grid[y, x] and the
# colour writer indexes pixels[(y * width + x) * 4], so the two images register exactly.
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
name = "LaserGray"
if name in bpy.data.images:
bpy.data.images.remove(bpy.data.images[name])
image = bpy.data.images.new(name, width=width, height=height, alpha=True)
try:
image.pixels.foreach_set(rgba.reshape(-1))
image.update()
image.filepath_raw = str(out)
image.file_format = "PNG"
image.save()
finally:
bpy.data.images.remove(image)
def _write_grayscale_vector(
svg_path, pdf_path, width, height, page_width_mm, page_height_mm, shapes, uv_name
):
"""Write the laser-engraving artwork as vector geometry.
White page, grey pattern cells, no mesh layer and no reference cross -- the same artwork
`_write_grayscale_png` rasterises, so the three formats cannot disagree about what is cut.
The mesh ground is omitted rather than drawn white: the page background is already white,
so emitting thousands of coincident white polygons would bloat the file for nothing.
"""
# White because a laser cuts what is dark and an unengraved sheet must be light; see
# `_gray_shapes` for why the polarity is inverted relative to the print texture.
white = (1.0, 1.0, 1.0)
no_cross: list = []
if svg_path:
write_svg_vector(
svg_path, width, height, page_width_mm, page_height_mm,
[], shapes, no_cross, (1.0, 0.0, 0.0, 1.0), uv_name, background=white,
)
if pdf_path:
write_pdf_vector(
pdf_path, width, height, page_width_mm, page_height_mm,
[], shapes, no_cross, (1.0, 0.0, 0.0, 1.0), background=white,
)
def _derive_pair_page(obj, camera, uv_name, depsgraph, px_per_mm):
"""Derive and verify one pair's page from the geometry its layout projects.
@@ -1018,6 +1123,39 @@ def main() -> int:
)
return 1
# The laser-engraving set is written before the artifact check below, so it is verified
# by the same existence test as the print set rather than trusted.
gray_paths = {
name: pair["artifacts"].get(f"gray_{name}") for name in ARTIFACT_FORMATS
}
if any(gray_paths.values()):
try:
gray_shapes = _gray_shapes(
emitted_patches, patch_colors, texture_width, texture_height
)
if gray_paths["png"]:
_write_grayscale_png(
gray_paths["png"], texture_width, texture_height, gray_shapes
)
_write_grayscale_vector(
gray_paths["svg"], gray_paths["pdf"],
texture_width, texture_height, page_width_mm, page_height_mm,
gray_shapes, uv_map,
)
except Exception as exc: # noqa: BLE001
print(
f"ERROR: laser-engraving texture failed for view {pair_id!r}: {exc}",
file=sys.stderr,
)
return 1
detail(
f"[{pair_id}] laser gray {texture_width}x{texture_height} px, "
f"{len(gray_shapes)} cells -> "
+ ", ".join(
Path(path).name for path in gray_paths.values() if path
)
)
for format_name, output in pair["artifacts"].items():
path = Path(output)
if not path.is_file() or path.stat().st_size == 0: