new grayscale output for testing laser, not working, will change to new design
This commit is contained in:
@@ -681,9 +681,21 @@ def _clipped_shapes(shapes, width, height):
|
||||
|
||||
def write_svg_vector(
|
||||
output_path, texture_width, texture_height, width_mm, height_mm,
|
||||
mesh_polygons, patch_shapes, cross_rects, cross_color, uv_map
|
||||
mesh_polygons, patch_shapes, cross_rects, cross_color, uv_map,
|
||||
background=None,
|
||||
):
|
||||
"""Write editable grouped SVG geometry; no raster data is embedded."""
|
||||
"""Write editable grouped SVG geometry; no raster data is embedded.
|
||||
|
||||
``background`` is the page fill. Left as ``None`` it is the black production ground; the
|
||||
laser-engraving variant passes white, because a laser cuts what is dark and an
|
||||
unengraved sheet must be light.
|
||||
"""
|
||||
if background is None:
|
||||
background_fill = "#000000"
|
||||
else:
|
||||
background_fill = "#" + "".join(
|
||||
f"{round(_clamp_channel(channel) * 255):02x}" for channel in background[:3]
|
||||
)
|
||||
lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
(
|
||||
@@ -705,7 +717,7 @@ def write_svg_vector(
|
||||
(
|
||||
f' <rect id="background-fill" x="0" y="0" '
|
||||
f'width="{texture_width}" height="{texture_height}" '
|
||||
f'fill="#000000" stroke="none"/>'
|
||||
f'fill="{background_fill}" stroke="none"/>'
|
||||
),
|
||||
" </g>",
|
||||
' <g id="mesh">',
|
||||
@@ -785,15 +797,26 @@ def _build_pdf(objects):
|
||||
|
||||
def write_pdf_vector(
|
||||
output_path, texture_width, texture_height, width_mm, height_mm,
|
||||
mesh_polygons, patch_shapes, cross_rects, cross_color
|
||||
mesh_polygons, patch_shapes, cross_rects, cross_color,
|
||||
background=None,
|
||||
):
|
||||
"""Write native vector PDF paths at the configured physical page size."""
|
||||
"""Write native vector PDF paths at the configured physical page size.
|
||||
|
||||
``background`` as for `write_svg_vector`. The default emits the original literal command
|
||||
so existing documents stay byte-identical.
|
||||
"""
|
||||
page_width_pt = float(width_mm) * 72.0 / 25.4
|
||||
page_height_pt = float(height_mm) * 72.0 / 25.4
|
||||
scale_x = page_width_pt / float(texture_width)
|
||||
scale_y = page_height_pt / float(texture_height)
|
||||
|
||||
content = ["q", "% background", "0 0 0 rg"]
|
||||
if background is None:
|
||||
background_command = "0 0 0 rg"
|
||||
else:
|
||||
background_command = _pdf_set_fill(
|
||||
(background[0], background[1], background[2], 1.0)
|
||||
)
|
||||
content = ["q", "% background", background_command]
|
||||
content.append(
|
||||
f"0 0 {_format_number(page_width_pt)} {_format_number(page_height_pt)} re f"
|
||||
)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user