bundle: update (2026-01-16)
This commit is contained in:
149
extensions/botbox3000/placements.py
Normal file
149
extensions/botbox3000/placements.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
deps_dir = Path(__file__).parent / "deps"
|
||||
if deps_dir.exists() and str(deps_dir) not in sys.path:
|
||||
sys.path.insert(0, str(deps_dir))
|
||||
|
||||
import math
|
||||
import inkex
|
||||
from inkex import PathElement, Rectangle, Transform
|
||||
|
||||
|
||||
def calculate_path_length(path):
|
||||
if isinstance(path, str):
|
||||
path = inkex.Path(path)
|
||||
|
||||
csp = path.to_superpath()
|
||||
total_length = 0.0
|
||||
|
||||
for subpath in csp:
|
||||
for i, seg in enumerate(subpath[:-1]):
|
||||
next_seg = subpath[i + 1]
|
||||
bezier = (seg[1], seg[2], next_seg[0], next_seg[1])
|
||||
seg_length = inkex.bezier.bezierlength(bezier, tolerance=0.01)
|
||||
total_length += seg_length
|
||||
|
||||
return total_length
|
||||
|
||||
|
||||
def point_at_length(path, target_length):
|
||||
if isinstance(path, str):
|
||||
path = inkex.Path(path)
|
||||
|
||||
csp = path.to_superpath()
|
||||
current_length = 0.0
|
||||
|
||||
for subpath in csp:
|
||||
for i, seg in enumerate(subpath[:-1]):
|
||||
next_seg = subpath[i + 1]
|
||||
bezier = (seg[1], seg[2], next_seg[0], next_seg[1])
|
||||
seg_length = inkex.bezier.bezierlength(bezier, tolerance=0.01)
|
||||
|
||||
if current_length + seg_length >= target_length:
|
||||
t = (target_length - current_length) / seg_length if seg_length > 0 else 0
|
||||
t = max(0.0, min(1.0, t))
|
||||
|
||||
p0, p1, p2, p3 = bezier
|
||||
p0_p1_dist = ((p1[0] - p0[0])**2 + (p1[1] - p0[1])**2)**0.5
|
||||
p2_p3_dist = ((p3[0] - p2[0])**2 + (p3[1] - p2[1])**2)**0.5
|
||||
|
||||
if p0_p1_dist < 0.001 and p2_p3_dist < 0.001:
|
||||
point = (
|
||||
p0[0] + t * (p3[0] - p0[0]),
|
||||
p0[1] + t * (p3[1] - p0[1])
|
||||
)
|
||||
else:
|
||||
point = inkex.bezier.bezierpointatt(bezier, t)
|
||||
|
||||
p0, p1, p2, p3 = bezier
|
||||
one_minus_t = 1.0 - t
|
||||
|
||||
dx = (3 * one_minus_t * one_minus_t * (p1[0] - p0[0]) +
|
||||
6 * one_minus_t * t * (p2[0] - p1[0]) +
|
||||
3 * t * t * (p3[0] - p2[0]))
|
||||
dy = (3 * one_minus_t * one_minus_t * (p1[1] - p0[1]) +
|
||||
6 * one_minus_t * t * (p2[1] - p1[1]) +
|
||||
3 * t * t * (p3[1] - p2[1]))
|
||||
|
||||
length = (dx * dx + dy * dy) ** 0.5
|
||||
if length > 0:
|
||||
dx /= length
|
||||
dy /= length
|
||||
|
||||
return (point[0], point[1]), (dx, dy)
|
||||
|
||||
current_length += seg_length
|
||||
|
||||
last_subpath = csp[-1]
|
||||
last_point = last_subpath[-1][1]
|
||||
return (last_point[0], last_point[1]), (1.0, 0.0)
|
||||
|
||||
|
||||
def pattern_along_path(path, num_items, item_width, start_offset, spacing, create_shape_fn):
|
||||
|
||||
if isinstance(path, str):
|
||||
path = inkex.Path(path)
|
||||
|
||||
if num_items <= 0:
|
||||
return []
|
||||
|
||||
total_length = calculate_path_length(path)
|
||||
items = []
|
||||
|
||||
if spacing == "even":
|
||||
gap = (total_length - num_items * item_width) / num_items
|
||||
|
||||
for i in range(num_items):
|
||||
item_start = (start_offset + i * (gap + item_width)) % total_length
|
||||
item_center = (item_start + item_width / 2) % total_length
|
||||
|
||||
point, tangent = point_at_length(path, item_center)
|
||||
angle = math.degrees(math.atan2(tangent[1], tangent[0]))
|
||||
|
||||
item = create_shape_fn(i)
|
||||
|
||||
transform = Transform()
|
||||
transform.add_translate(point[0], point[1])
|
||||
transform.add_rotate(angle)
|
||||
|
||||
item.transform = transform
|
||||
items.append(item)
|
||||
|
||||
elif spacing == "endpoints":
|
||||
for i in range(num_items):
|
||||
base_distance = total_length * i / (num_items - 1) if num_items > 1 else 0
|
||||
distance = (base_distance + start_offset) % total_length
|
||||
point, tangent = point_at_length(path, distance)
|
||||
angle = math.degrees(math.atan2(tangent[1], tangent[0]))
|
||||
|
||||
item = create_shape_fn(i)
|
||||
|
||||
transform = Transform()
|
||||
transform.add_translate(point[0], point[1])
|
||||
transform.add_rotate(angle)
|
||||
|
||||
item.transform = transform
|
||||
items.append(item)
|
||||
|
||||
elif spacing == "simple":
|
||||
gap = (total_length - num_items * item_width) / num_items
|
||||
|
||||
for i in range(num_items):
|
||||
item_start = (start_offset + i * (gap + item_width)) % total_length
|
||||
distance = (item_start + item_width / 2) % total_length
|
||||
point, tangent = point_at_length(path, distance)
|
||||
angle = math.degrees(math.atan2(tangent[1], tangent[0]))
|
||||
|
||||
item = create_shape_fn(i)
|
||||
|
||||
transform = Transform()
|
||||
transform.add_translate(point[0], point[1])
|
||||
transform.add_rotate(angle)
|
||||
|
||||
item.transform = transform
|
||||
items.append(item)
|
||||
|
||||
return items
|
||||
Reference in New Issue
Block a user