生成无重叠的 0.2 mm Xynova 笔画图案
在打印页毫米坐标中筛选候选笔画,更新双 UV 产物。38 项测试通过;SVG 检查无交叠。
This commit is contained in:
@@ -39,6 +39,7 @@ script_dir = str(Path(__file__).resolve().parent)
|
||||
if script_dir not in sys.path:
|
||||
sys.path.insert(0, script_dir)
|
||||
from random_patch_dd02c import save_png_image, write_pdf_vector, write_svg_vector # noqa: E402 - Blender omits the script directory from sys.path
|
||||
from stroke_geometry import capsule_segment, has_clearance # noqa: E402 - Blender script directory is on sys.path
|
||||
|
||||
MODE = "camera_compensated_width"
|
||||
|
||||
@@ -56,6 +57,8 @@ DEFAULT_PARAMS: dict[str, float] = {
|
||||
"length_ratio": 2.4,
|
||||
"coverage_cap": 0.45,
|
||||
"neighbourhood_mm": 0.5,
|
||||
"clearance_mm": 0.0,
|
||||
"candidate_count": 1,
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +75,10 @@ def validate_params(params: dict) -> None:
|
||||
raise ValueError(f"params.{key}: must be finite (got {value!r})")
|
||||
if params["min_cell_pixels"] < 1:
|
||||
raise ValueError("params.min_cell_pixels must be >= 1")
|
||||
if not isinstance(params["candidate_count"], int) or params["candidate_count"] < 1:
|
||||
raise ValueError("params.candidate_count must be an integer >= 1")
|
||||
if params["clearance_mm"] < 0:
|
||||
raise ValueError("params.clearance_mm must be >= 0")
|
||||
if not 0.0 <= params["jitter_fraction"] <= 1.0:
|
||||
raise ValueError("params.jitter_fraction must be within [0, 1]")
|
||||
if params["angle_span_deg"] < 0.0:
|
||||
@@ -146,7 +153,8 @@ def resolve_params(args: argparse.Namespace) -> dict:
|
||||
|
||||
|
||||
def cell_code(
|
||||
rgb: np.ndarray, seed: int, jitter_fraction: float, angle_span_deg: float
|
||||
rgb: np.ndarray, seed: int, jitter_fraction: float, angle_span_deg: float,
|
||||
candidate_index: int = 0,
|
||||
) -> tuple[float, float, float]:
|
||||
"""Return bounded offsets and angle from one exact source colour.
|
||||
|
||||
@@ -154,7 +162,8 @@ def cell_code(
|
||||
views without storing anything.
|
||||
"""
|
||||
digest = hashlib.blake2b(
|
||||
bytes(int(v) for v in rgb) + seed.to_bytes(8, "little", signed=True),
|
||||
bytes(int(v) for v in rgb) + seed.to_bytes(8, "little", signed=True)
|
||||
+ (candidate_index.to_bytes(4, "little") if candidate_index else b""),
|
||||
digest_size=8,
|
||||
).digest()
|
||||
values = [int.from_bytes(digest[k : k + 2], "little") / 65535 for k in (0, 2, 4, 6)]
|
||||
@@ -287,7 +296,7 @@ def capsule_polygon(
|
||||
return points
|
||||
|
||||
|
||||
def draw_mark(
|
||||
def draw_mark_candidate(
|
||||
canvas: np.ndarray,
|
||||
region,
|
||||
params: dict,
|
||||
@@ -295,6 +304,8 @@ def draw_mark(
|
||||
patterned: np.ndarray | None = None,
|
||||
camera_cell: tuple[int, int] | None = None,
|
||||
vector_shapes: list | None = None,
|
||||
accepted_capsules: list | None = None,
|
||||
candidate_index: int = 0,
|
||||
) -> float:
|
||||
"""Draw one camera-compensated capsule. Returns its printed width in pixels, or 0."""
|
||||
colour, cell_x, cell_y, width, height = region
|
||||
@@ -304,6 +315,7 @@ def draw_mark(
|
||||
params["seed"],
|
||||
params["jitter_fraction"],
|
||||
params["angle_span_deg"],
|
||||
candidate_index,
|
||||
)
|
||||
span = min(width, height)
|
||||
centre_x = cell_x + dx * width
|
||||
@@ -359,7 +371,15 @@ def draw_mark(
|
||||
added = np.count_nonzero(footprint & ~canvas[y0:y1, x0:x1])
|
||||
if (existing + added) / local_area > params["coverage_cap"]:
|
||||
return 0.0
|
||||
capsule = (*capsule_segment(centre_x / px_per_mm, centre_y / px_per_mm,
|
||||
half_length / px_per_mm, angle), radius / px_per_mm)
|
||||
if accepted_capsules is not None and not has_clearance(
|
||||
capsule, accepted_capsules, params["clearance_mm"]
|
||||
):
|
||||
return 0.0
|
||||
canvas[y0:y1, x0:x1] |= footprint
|
||||
if accepted_capsules is not None:
|
||||
accepted_capsules.append(capsule)
|
||||
if vector_shapes is not None:
|
||||
vector_shapes.append(
|
||||
(
|
||||
@@ -370,6 +390,19 @@ def draw_mark(
|
||||
return 2 * radius
|
||||
|
||||
|
||||
def draw_mark(canvas, region, params, px_per_mm, patterned=None,
|
||||
camera_cell=None, vector_shapes=None, accepted_capsules=None) -> float:
|
||||
"""Try deterministic alternatives without relaxing width or clearance."""
|
||||
for candidate_index in range(params["candidate_count"]):
|
||||
width = draw_mark_candidate(
|
||||
canvas, region, params, px_per_mm, patterned, camera_cell,
|
||||
vector_shapes, accepted_capsules, candidate_index,
|
||||
)
|
||||
if width:
|
||||
return width
|
||||
return 0.0
|
||||
|
||||
|
||||
def write_binary_png(path: Path, marks: np.ndarray, surface: np.ndarray) -> None:
|
||||
height, width = marks.shape
|
||||
rgba = np.zeros((height, width, 4), dtype=np.float32)
|
||||
@@ -569,6 +602,7 @@ def run(
|
||||
flush=True,
|
||||
)
|
||||
marks = np.zeros((height, width), dtype=bool)
|
||||
accepted_capsules = []
|
||||
vector_shapes = [] if vector_page is not None else None
|
||||
mode_regions = [
|
||||
region
|
||||
@@ -584,6 +618,7 @@ def run(
|
||||
patterned,
|
||||
camera_cell=lookup[region[0]],
|
||||
vector_shapes=vector_shapes,
|
||||
accepted_capsules=accepted_capsules,
|
||||
)
|
||||
for region in mode_regions
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user