bundle: update (2026-01-16)
This commit is contained in:
776
extensions/botbox3000/offset.py
Normal file
776
extensions/botbox3000/offset.py
Normal file
@@ -0,0 +1,776 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import math
|
||||
|
||||
try:
|
||||
from inkex import Path
|
||||
except ImportError:
|
||||
Path = None
|
||||
|
||||
|
||||
def subpath_to_points(subpath, precision=1.0):
|
||||
points = []
|
||||
current_point = (0, 0)
|
||||
|
||||
for cmd in subpath:
|
||||
if isinstance(cmd, tuple):
|
||||
letter = cmd[0].upper()
|
||||
coord = cmd[1]
|
||||
else:
|
||||
letter = cmd.letter.upper()
|
||||
args = cmd.args
|
||||
|
||||
if letter == 'M':
|
||||
if isinstance(cmd, tuple):
|
||||
current_point = coord
|
||||
else:
|
||||
current_point = (args[0], args[1])
|
||||
points.append(current_point)
|
||||
|
||||
elif letter == 'L':
|
||||
if isinstance(cmd, tuple):
|
||||
current_point = coord
|
||||
else:
|
||||
current_point = (args[0], args[1])
|
||||
points.append(current_point)
|
||||
|
||||
elif letter == 'H':
|
||||
if isinstance(cmd, tuple):
|
||||
current_point = (coord[0], current_point[1])
|
||||
else:
|
||||
current_point = (args[0], current_point[1])
|
||||
points.append(current_point)
|
||||
|
||||
elif letter == 'V':
|
||||
if isinstance(cmd, tuple):
|
||||
current_point = (current_point[0], coord[1])
|
||||
else:
|
||||
current_point = (current_point[0], args[0])
|
||||
points.append(current_point)
|
||||
|
||||
elif letter == 'C':
|
||||
p0 = current_point
|
||||
p1 = (args[0], args[1])
|
||||
p2 = (args[2], args[3])
|
||||
p3 = (args[4], args[5])
|
||||
|
||||
chord_length = distance(p0, p3)
|
||||
control_length = distance(p0, p1) + distance(p1, p2) + distance(p2, p3)
|
||||
approx_length = (chord_length + control_length) / 2
|
||||
|
||||
num_segments = max(2, int(approx_length / precision))
|
||||
|
||||
for i in range(1, num_segments + 1):
|
||||
t = i / num_segments
|
||||
point = cubic_bezier_point(p0, p1, p2, p3, t)
|
||||
points.append(point)
|
||||
|
||||
current_point = p3
|
||||
|
||||
elif letter == 'S':
|
||||
p0 = current_point
|
||||
p3 = (args[2], args[3])
|
||||
|
||||
chord_length = distance(p0, p3)
|
||||
num_segments = max(2, int(chord_length / precision))
|
||||
|
||||
for i in range(1, num_segments + 1):
|
||||
t = i / num_segments
|
||||
x = p0[0] + t * (p3[0] - p0[0])
|
||||
y = p0[1] + t * (p3[1] - p0[1])
|
||||
points.append((x, y))
|
||||
|
||||
current_point = p3
|
||||
|
||||
elif letter == 'Q':
|
||||
p0 = current_point
|
||||
p1 = (args[0], args[1])
|
||||
p2 = (args[2], args[3])
|
||||
|
||||
chord_length = distance(p0, p2)
|
||||
control_length = distance(p0, p1) + distance(p1, p2)
|
||||
approx_length = (chord_length + control_length) / 2
|
||||
|
||||
num_segments = max(2, int(approx_length / precision))
|
||||
|
||||
for i in range(1, num_segments + 1):
|
||||
t = i / num_segments
|
||||
point = quadratic_bezier_point(p0, p1, p2, t)
|
||||
points.append(point)
|
||||
|
||||
current_point = p2
|
||||
|
||||
elif letter == 'T':
|
||||
p0 = current_point
|
||||
p2 = (args[0], args[1])
|
||||
|
||||
chord_length = distance(p0, p2)
|
||||
num_segments = max(2, int(chord_length / precision))
|
||||
|
||||
for i in range(1, num_segments + 1):
|
||||
t = i / num_segments
|
||||
x = p0[0] + t * (p2[0] - p0[0])
|
||||
y = p0[1] + t * (p2[1] - p0[1])
|
||||
points.append((x, y))
|
||||
|
||||
current_point = p2
|
||||
|
||||
elif letter == 'A':
|
||||
rx = abs(args[0])
|
||||
ry = abs(args[1])
|
||||
x_axis_rotation = args[2]
|
||||
large_arc_flag = args[3]
|
||||
sweep_flag = args[4]
|
||||
end_x = args[5]
|
||||
end_y = args[6]
|
||||
|
||||
start_x, start_y = current_point
|
||||
|
||||
beziers = arc_to_beziers(start_x, start_y, rx, ry, x_axis_rotation,
|
||||
large_arc_flag, sweep_flag, end_x, end_y)
|
||||
|
||||
for bez in beziers:
|
||||
p0, p1, p2, p3 = bez
|
||||
chord_length = distance(p0, p3)
|
||||
control_length = distance(p0, p1) + distance(p1, p2) + distance(p2, p3)
|
||||
approx_length = (chord_length + control_length) / 2
|
||||
|
||||
num_segments = max(2, int(approx_length / precision))
|
||||
|
||||
for i in range(1, num_segments + 1):
|
||||
t = i / num_segments
|
||||
point = cubic_bezier_point(p0, p1, p2, p3, t)
|
||||
points.append(point)
|
||||
|
||||
current_point = (end_x, end_y)
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def arc_to_beziers(x1, y1, rx, ry, phi, large_arc, sweep, x2, y2):
|
||||
if (x1, y1) == (x2, y2):
|
||||
return []
|
||||
if rx == 0 or ry == 0:
|
||||
return [((x1, y1), (x1, y1), (x2, y2), (x2, y2))]
|
||||
|
||||
phi_rad = math.radians(phi)
|
||||
cos_phi = math.cos(phi_rad)
|
||||
sin_phi = math.sin(phi_rad)
|
||||
|
||||
dx = (x1 - x2) / 2
|
||||
dy = (y1 - y2) / 2
|
||||
x1_prime = cos_phi * dx + sin_phi * dy
|
||||
y1_prime = -sin_phi * dx + cos_phi * dy
|
||||
|
||||
lambda_ = (x1_prime / rx) ** 2 + (y1_prime / ry) ** 2
|
||||
if lambda_ > 1:
|
||||
rx *= math.sqrt(lambda_)
|
||||
ry *= math.sqrt(lambda_)
|
||||
|
||||
sq = max(0, (rx * ry) ** 2 - (rx * y1_prime) ** 2 - (ry * x1_prime) ** 2)
|
||||
sq = math.sqrt(sq / ((rx * y1_prime) ** 2 + (ry * x1_prime) ** 2))
|
||||
|
||||
if large_arc == sweep:
|
||||
sq = -sq
|
||||
|
||||
cx_prime = sq * rx * y1_prime / ry
|
||||
cy_prime = -sq * ry * x1_prime / rx
|
||||
|
||||
cx = cos_phi * cx_prime - sin_phi * cy_prime + (x1 + x2) / 2
|
||||
cy = sin_phi * cx_prime + cos_phi * cy_prime + (y1 + y2) / 2
|
||||
|
||||
def angle_between(ux, uy, vx, vy):
|
||||
n = math.sqrt(ux * ux + uy * uy) * math.sqrt(vx * vx + vy * vy)
|
||||
c = (ux * vx + uy * vy) / n
|
||||
c = max(-1, min(1, c))
|
||||
angle = math.acos(c)
|
||||
if ux * vy - uy * vx < 0:
|
||||
angle = -angle
|
||||
return angle
|
||||
|
||||
theta1 = angle_between(1, 0, (x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry)
|
||||
dtheta = angle_between(
|
||||
(x1_prime - cx_prime) / rx, (y1_prime - cy_prime) / ry,
|
||||
(-x1_prime - cx_prime) / rx, (-y1_prime - cy_prime) / ry
|
||||
)
|
||||
|
||||
if sweep == 0 and dtheta > 0:
|
||||
dtheta -= 2 * math.pi
|
||||
elif sweep == 1 and dtheta < 0:
|
||||
dtheta += 2 * math.pi
|
||||
|
||||
segments = max(1, int(math.ceil(abs(dtheta) / (math.pi / 2))))
|
||||
delta = dtheta / segments
|
||||
|
||||
beziers = []
|
||||
for i in range(segments):
|
||||
theta_start = theta1 + i * delta
|
||||
theta_end = theta_start + delta
|
||||
|
||||
alpha = math.sin(delta) * (math.sqrt(4 + 3 * math.tan(delta / 2) ** 2) - 1) / 3
|
||||
|
||||
cos_start = math.cos(theta_start)
|
||||
sin_start = math.sin(theta_start)
|
||||
cos_end = math.cos(theta_end)
|
||||
sin_end = math.sin(theta_end)
|
||||
|
||||
q1x = cos_start
|
||||
q1y = sin_start
|
||||
q2x = cos_end
|
||||
q2y = sin_end
|
||||
|
||||
cp1x = q1x - q1y * alpha
|
||||
cp1y = q1y + q1x * alpha
|
||||
cp2x = q2x + q2y * alpha
|
||||
cp2y = q2y - q2x * alpha
|
||||
|
||||
def transform(x, y):
|
||||
x *= rx
|
||||
y *= ry
|
||||
nx = cos_phi * x - sin_phi * y
|
||||
ny = sin_phi * x + cos_phi * y
|
||||
return (nx + cx, ny + cy)
|
||||
|
||||
p0 = transform(q1x, q1y)
|
||||
p1 = transform(cp1x, cp1y)
|
||||
p2 = transform(cp2x, cp2y)
|
||||
p3 = transform(q2x, q2y)
|
||||
|
||||
beziers.append((p0, p1, p2, p3))
|
||||
|
||||
return beziers
|
||||
|
||||
|
||||
def distance(p1, p2):
|
||||
dx = p2[0] - p1[0]
|
||||
dy = p2[1] - p1[1]
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
|
||||
def cubic_bezier_point(p0, p1, p2, p3, t):
|
||||
mt = 1 - t
|
||||
mt2 = mt * mt
|
||||
mt3 = mt2 * mt
|
||||
t2 = t * t
|
||||
t3 = t2 * t
|
||||
|
||||
x = mt3 * p0[0] + 3 * mt2 * t * p1[0] + 3 * mt * t2 * p2[0] + t3 * p3[0]
|
||||
y = mt3 * p0[1] + 3 * mt2 * t * p1[1] + 3 * mt * t2 * p2[1] + t3 * p3[1]
|
||||
|
||||
return (x, y)
|
||||
|
||||
|
||||
def quadratic_bezier_point(p0, p1, p2, t):
|
||||
mt = 1 - t
|
||||
mt2 = mt * mt
|
||||
t2 = t * t
|
||||
|
||||
x = mt2 * p0[0] + 2 * mt * t * p1[0] + t2 * p2[0]
|
||||
y = mt2 * p0[1] + 2 * mt * t * p1[1] + t2 * p2[1]
|
||||
|
||||
return (x, y)
|
||||
|
||||
|
||||
def point_in_polygon(point, polygon):
|
||||
x, y = point
|
||||
n = len(polygon)
|
||||
inside = False
|
||||
|
||||
p1x, p1y = polygon[0]
|
||||
for i in range(1, n + 1):
|
||||
p2x, p2y = polygon[i % n]
|
||||
|
||||
if y > min(p1y, p2y):
|
||||
if y <= max(p1y, p2y):
|
||||
if x <= max(p1x, p2x):
|
||||
if p1y != p2y:
|
||||
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
||||
if p1x == p2x or x <= xinters:
|
||||
inside = not inside
|
||||
|
||||
p1x, p1y = p2x, p2y
|
||||
|
||||
return inside
|
||||
|
||||
|
||||
def perpendicular_distance(point, line_start, line_end):
|
||||
x0, y0 = point
|
||||
x1, y1 = line_start
|
||||
x2, y2 = line_end
|
||||
|
||||
dx = x2 - x1
|
||||
dy = y2 - y1
|
||||
line_length_sq = dx * dx + dy * dy
|
||||
|
||||
if line_length_sq == 0:
|
||||
return distance(point, line_start)
|
||||
|
||||
numerator = abs(dy * x0 - dx * y0 + x2 * y1 - y2 * x1)
|
||||
return numerator / math.sqrt(line_length_sq)
|
||||
|
||||
|
||||
def simplify_path_rdp(points, epsilon):
|
||||
if len(points) < 3:
|
||||
return points
|
||||
|
||||
max_dist = 0.0
|
||||
max_index = 0
|
||||
|
||||
for i in range(1, len(points) - 1):
|
||||
dist = perpendicular_distance(points[i], points[0], points[-1])
|
||||
if dist > max_dist:
|
||||
max_dist = dist
|
||||
max_index = i
|
||||
|
||||
if max_dist > epsilon:
|
||||
left = simplify_path_rdp(points[:max_index + 1], epsilon)
|
||||
right = simplify_path_rdp(points[max_index:], epsilon)
|
||||
|
||||
return left[:-1] + right
|
||||
else:
|
||||
return [points[0], points[-1]]
|
||||
|
||||
|
||||
def simplify_closed_path(points, epsilon):
|
||||
if len(points) < 4:
|
||||
return points
|
||||
|
||||
simplified = simplify_path_rdp(points, epsilon)
|
||||
|
||||
if simplified[0] != points[0]:
|
||||
simplified.insert(0, points[0])
|
||||
|
||||
return simplified
|
||||
|
||||
|
||||
def segments_intersect(p1, p2, p3, p4):
|
||||
x1, y1 = p1
|
||||
x2, y2 = p2
|
||||
x3, y3 = p3
|
||||
x4, y4 = p4
|
||||
|
||||
denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
|
||||
|
||||
if abs(denom) < 1e-10:
|
||||
return False, None
|
||||
|
||||
t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom
|
||||
u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom
|
||||
|
||||
if 0 < t < 1 and 0 < u < 1:
|
||||
ix = x1 + t * (x2 - x1)
|
||||
iy = y1 + t * (y2 - y1)
|
||||
return True, (ix, iy)
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
def remove_self_intersections(points, offset_distance, debug=False):
|
||||
if len(points) < 4:
|
||||
return points
|
||||
|
||||
centroid_x = sum(p[0] for p in points) / len(points)
|
||||
centroid_y = sum(p[1] for p in points) / len(points)
|
||||
|
||||
max_iterations = 100
|
||||
iteration = 0
|
||||
|
||||
while iteration < max_iterations:
|
||||
iteration += 1
|
||||
found_intersection = False
|
||||
|
||||
n = len(points)
|
||||
for i in range(n):
|
||||
if found_intersection:
|
||||
break
|
||||
|
||||
p1 = points[i]
|
||||
p2 = points[(i + 1) % n]
|
||||
|
||||
for j in range(i + 2, n):
|
||||
if j == (i + n - 1) % n:
|
||||
continue
|
||||
|
||||
p3 = points[j]
|
||||
p4 = points[(j + 1) % n]
|
||||
|
||||
intersects, int_point = segments_intersect(p1, p2, p3, p4)
|
||||
|
||||
if intersects:
|
||||
if debug:
|
||||
print(f" Found intersection between segments {i}-{i+1} and {j}-{j+1}")
|
||||
print(f" Intersection point: {int_point}")
|
||||
|
||||
path_a = points[:i+1] + [int_point] + points[j+1:]
|
||||
path_b = [int_point] + points[i+1:j+1]
|
||||
|
||||
def signed_area(pts):
|
||||
if len(pts) < 3:
|
||||
return 0.0
|
||||
area = 0.0
|
||||
for k in range(len(pts)):
|
||||
x1, y1 = pts[k]
|
||||
x2, y2 = pts[(k + 1) % len(pts)]
|
||||
area += (x2 - x1) * (y2 + y1)
|
||||
return area / 2.0
|
||||
|
||||
area_a = abs(signed_area(path_a))
|
||||
area_b = abs(signed_area(path_b))
|
||||
|
||||
if debug:
|
||||
print(f" Path A area (without loop): {area_a:.3f}")
|
||||
print(f" Path B area (loop only): {area_b:.3f}")
|
||||
|
||||
if area_a >= area_b:
|
||||
points = path_a
|
||||
if debug:
|
||||
print(f" Keeping path A (larger/outer envelope)")
|
||||
else:
|
||||
points = path_b
|
||||
if debug:
|
||||
print(f" Keeping path B (the loop was larger)")
|
||||
|
||||
found_intersection = True
|
||||
break
|
||||
|
||||
if not found_intersection:
|
||||
break
|
||||
|
||||
if debug and iteration > 1:
|
||||
print(f" Removed self-intersections in {iteration} iterations")
|
||||
|
||||
if len(points) < 3:
|
||||
if debug:
|
||||
print(f" WARNING: Ended with too few points ({len(points)}), this may indicate a problem")
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def calculate_polygon_winding(points):
|
||||
signed_area = 0.0
|
||||
n = len(points)
|
||||
for i in range(n):
|
||||
x1, y1 = points[i]
|
||||
x2, y2 = points[(i + 1) % n]
|
||||
signed_area += (x2 - x1) * (y2 + y1)
|
||||
|
||||
return -1 if signed_area > 0 else 1
|
||||
|
||||
|
||||
def calculate_perpendicular_offset(point, prev_point, next_point, offset_distance, polygon_winding, debug=False):
|
||||
e1x = point[0] - prev_point[0]
|
||||
e1y = point[1] - prev_point[1]
|
||||
e2x = next_point[0] - point[0]
|
||||
e2y = next_point[1] - point[1]
|
||||
|
||||
if debug:
|
||||
print(f" Edge vectors: e1=({e1x:.3f}, {e1y:.3f}), e2=({e2x:.3f}, {e2y:.3f})")
|
||||
|
||||
len1 = math.sqrt(e1x * e1x + e1y * e1y)
|
||||
len2 = math.sqrt(e2x * e2x + e2y * e2y)
|
||||
|
||||
if len1 > 0:
|
||||
e1x /= len1
|
||||
e1y /= len1
|
||||
if len2 > 0:
|
||||
e2x /= len2
|
||||
e2y /= len2
|
||||
|
||||
if debug:
|
||||
print(f" Normalized edges: e1=({e1x:.3f}, {e1y:.3f}), e2=({e2x:.3f}, {e2y:.3f})")
|
||||
|
||||
tangent_x = e1x + e2x
|
||||
tangent_y = e1y + e2y
|
||||
|
||||
tangent_len = math.sqrt(tangent_x * tangent_x + tangent_y * tangent_y)
|
||||
if tangent_len > 0:
|
||||
tangent_x /= tangent_len
|
||||
tangent_y /= tangent_len
|
||||
|
||||
if debug:
|
||||
print(f" Average tangent: ({tangent_x:.3f}, {tangent_y:.3f})")
|
||||
|
||||
normal_x = tangent_y
|
||||
normal_y = -tangent_x
|
||||
|
||||
if debug:
|
||||
print(f" Normal (perpendicular to tangent, 90° CW): ({normal_x:.3f}, {normal_y:.3f})")
|
||||
|
||||
offset_dir_x = normal_x
|
||||
offset_dir_y = normal_y
|
||||
|
||||
if debug:
|
||||
print(f" Offset direction (before flip): ({offset_dir_x:.3f}, {offset_dir_y:.3f})")
|
||||
print(f" Global polygon winding: {'CCW' if polygon_winding > 0 else 'CW'}")
|
||||
|
||||
should_flip = False
|
||||
if offset_distance > 0 and polygon_winding < 0:
|
||||
should_flip = True
|
||||
elif offset_distance < 0 and polygon_winding > 0:
|
||||
should_flip = True
|
||||
|
||||
if should_flip:
|
||||
offset_dir_x = -offset_dir_x
|
||||
offset_dir_y = -offset_dir_y
|
||||
if debug:
|
||||
if offset_distance < 0:
|
||||
print(f" Flipped for inward offset on CCW polygon: ({offset_dir_x:.3f}, {offset_dir_y:.3f})")
|
||||
else:
|
||||
print(f" Flipped for outward offset on CW polygon: ({offset_dir_x:.3f}, {offset_dir_y:.3f})")
|
||||
|
||||
dot_product = e1x * e2x + e1y * e2y
|
||||
dot_product = max(-1.0, min(1.0, dot_product))
|
||||
direction_angle = math.acos(dot_product)
|
||||
|
||||
turn_angle = math.pi - direction_angle
|
||||
|
||||
if debug:
|
||||
print(f" Direction angle: {math.degrees(direction_angle):.1f}°, Turn angle: {math.degrees(turn_angle):.1f}°")
|
||||
|
||||
if turn_angle > math.pi * 0.9:
|
||||
miter_limit = 1.0
|
||||
else:
|
||||
half_turn = turn_angle / 2
|
||||
if abs(math.sin(half_turn)) > 0.01:
|
||||
miter_limit = 1.0 / math.sin(half_turn)
|
||||
miter_limit = min(miter_limit, 2.0)
|
||||
else:
|
||||
miter_limit = 2.0
|
||||
|
||||
if debug:
|
||||
print(f" Miter limit: {miter_limit:.3f}")
|
||||
|
||||
adjusted_distance = abs(offset_distance) * miter_limit
|
||||
offset_x = point[0] + offset_dir_x * adjusted_distance
|
||||
offset_y = point[1] + offset_dir_y * adjusted_distance
|
||||
|
||||
if debug:
|
||||
print(f" Adjusted distance: {adjusted_distance:.3f}")
|
||||
print(f" Final offset point: ({offset_x:.3f}, {offset_y:.3f})")
|
||||
|
||||
return (offset_x, offset_y)
|
||||
|
||||
|
||||
def offset_path(subpath, offset_distance, precision=0.05, debug=False):
|
||||
try:
|
||||
try:
|
||||
from inkex import Path
|
||||
if isinstance(subpath, Path):
|
||||
subpath = list(subpath)
|
||||
except (ImportError, NameError):
|
||||
pass
|
||||
|
||||
points = subpath_to_points(subpath, precision)
|
||||
|
||||
if len(points) < 3:
|
||||
if debug:
|
||||
print(f"ERROR: Not enough points ({len(points)}) after approximation")
|
||||
return None
|
||||
|
||||
if len(points) > 1:
|
||||
first = points[0]
|
||||
last = points[-1]
|
||||
dist = math.sqrt((last[0] - first[0])**2 + (last[1] - first[1])**2)
|
||||
if dist < 0.001:
|
||||
points = points[:-1]
|
||||
if debug:
|
||||
print(f"Removed duplicate closing point (distance: {dist:.6f})")
|
||||
|
||||
original_count = len(points)
|
||||
points = simplify_closed_path(points, epsilon=precision * 2)
|
||||
if debug:
|
||||
print(f"Pre-simplified from {original_count} to {len(points)} points")
|
||||
|
||||
if len(points) < 3:
|
||||
if debug:
|
||||
print(f"ERROR: Not enough points ({len(points)}) after pre-simplification")
|
||||
return None
|
||||
|
||||
polygon_winding = calculate_polygon_winding(points)
|
||||
|
||||
if debug:
|
||||
print(f"\n=== OFFSET DEBUG ===")
|
||||
print(f"Original polygon ({len(points)} points after pre-simplification):")
|
||||
print(f" First 10 points: {points[:10]}")
|
||||
print(f" Last 5 points: {points[-5:]}")
|
||||
print(f"Offset distance: {offset_distance}")
|
||||
print(f"Precision: {precision}")
|
||||
print(f"Global polygon winding: {'CCW' if polygon_winding > 0 else 'CW'}")
|
||||
print(f"\nProcessing first 5 points to show edge vectors:")
|
||||
|
||||
offset_points = []
|
||||
n = len(points)
|
||||
for i, point in enumerate(points):
|
||||
prev_point = points[(i - 1) % n]
|
||||
next_point = points[(i + 1) % n]
|
||||
|
||||
offset_point = calculate_perpendicular_offset(point, prev_point, next_point, offset_distance, polygon_winding, debug=(debug and i < 5))
|
||||
|
||||
if debug and i < 5:
|
||||
e1_dx = point[0] - prev_point[0]
|
||||
e1_dy = point[1] - prev_point[1]
|
||||
e2_dx = next_point[0] - point[0]
|
||||
e2_dy = next_point[1] - point[1]
|
||||
print(f"\n Point [{i}]: {point}")
|
||||
print(f" Prev point: {prev_point}")
|
||||
print(f" Next point: {next_point}")
|
||||
print(f" Edge 1 vector (from prev): ({e1_dx:.3f}, {e1_dy:.3f})")
|
||||
print(f" Edge 2 vector (to next): ({e2_dx:.3f}, {e2_dy:.3f})")
|
||||
print(f" Offset point: {offset_point}")
|
||||
print(f" Movement: ({offset_point[0] - point[0]:.3f}, {offset_point[1] - point[1]:.3f})")
|
||||
|
||||
if offset_point:
|
||||
offset_points.append(offset_point)
|
||||
|
||||
if debug:
|
||||
print(f"\nResult:")
|
||||
print(f" Offset points generated: {len(offset_points)}")
|
||||
print(f" First 5 offset points: {offset_points[:5]}")
|
||||
print(f" Last 5 offset points: {offset_points[-5:]}")
|
||||
|
||||
if len(offset_points) < 3:
|
||||
if debug:
|
||||
print(f"ERROR: Not enough offset points ({len(offset_points)})")
|
||||
return None
|
||||
|
||||
if debug:
|
||||
print(f"\nRemoving self-intersections...")
|
||||
cleaned_points = remove_self_intersections(offset_points, offset_distance, debug=debug)
|
||||
|
||||
if debug:
|
||||
print(f" Points after cleaning: {len(cleaned_points)}")
|
||||
if len(cleaned_points) != len(offset_points):
|
||||
print(f" Removed {len(offset_points) - len(cleaned_points)} points from self-intersecting loops")
|
||||
|
||||
simplified_points = simplify_closed_path(cleaned_points, epsilon=precision)
|
||||
|
||||
if debug:
|
||||
print(f"\nSimplification:")
|
||||
print(f" Cleaned offset points: {len(cleaned_points)}")
|
||||
print(f" Simplified points: {len(simplified_points)}")
|
||||
print(f" Reduction: {len(cleaned_points) - len(simplified_points)} points ({100 * (1 - len(simplified_points) / len(cleaned_points)):.1f}%)")
|
||||
|
||||
offset_points = simplified_points
|
||||
|
||||
path_str = f"M {offset_points[0][0]},{offset_points[0][1]}"
|
||||
for point in offset_points[1:]:
|
||||
path_str += f" L {point[0]},{point[1]}"
|
||||
path_str += " Z"
|
||||
|
||||
if debug:
|
||||
print(f" Path string (first 100 chars): {path_str[:100]}...")
|
||||
print(f"=== END DEBUG ===\n")
|
||||
|
||||
try:
|
||||
from inkex import Path as InkexPath
|
||||
return InkexPath(path_str)
|
||||
except ImportError:
|
||||
result = [('M', offset_points[0])]
|
||||
for point in offset_points[1:]:
|
||||
result.append(('L', point))
|
||||
result.append(('Z', None))
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
print(f"Offset failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
def offset_lpe(element, offset_distance, unit="mm"):
|
||||
lpe_str = (
|
||||
f"offset,"
|
||||
f"0,1,"
|
||||
f"offset:{offset_distance},"
|
||||
f"linejoin_type:miter,"
|
||||
f"miter_limit:4,"
|
||||
f"attempt_force_join:false,"
|
||||
f"update_on_knot_move:true;"
|
||||
)
|
||||
|
||||
existing_lpe = element.get('inkscape:path-effect', '')
|
||||
if existing_lpe:
|
||||
new_lpe = existing_lpe + lpe_str
|
||||
else:
|
||||
new_lpe = lpe_str
|
||||
|
||||
element.set('inkscape:path-effect', new_lpe)
|
||||
|
||||
if not element.get('inkscape:original-d'):
|
||||
element.set('inkscape:original-d', element.get('d'))
|
||||
|
||||
return element
|
||||
|
||||
|
||||
def boolean_lpe(svg, element, operand_elements, operation="union"):
|
||||
if not operand_elements:
|
||||
return element
|
||||
|
||||
defs = svg.defs
|
||||
if defs is None:
|
||||
from lxml import etree
|
||||
defs = etree.SubElement(svg.getroot(), 'defs')
|
||||
|
||||
hidder_filter_id = "selectable_hidder_filter"
|
||||
hidder_filter = svg.getElementById(hidder_filter_id)
|
||||
if hidder_filter is None:
|
||||
from lxml import etree
|
||||
nsmap = {'inkscape': 'http://www.inkscape.org/namespaces/inkscape'}
|
||||
hidder_filter = etree.SubElement(defs, 'filter', {
|
||||
'id': hidder_filter_id,
|
||||
'width': '1',
|
||||
'height': '1',
|
||||
'x': '0',
|
||||
'y': '0',
|
||||
'style': 'color-interpolation-filters:sRGB;',
|
||||
'{http://www.inkscape.org/namespaces/inkscape}label': 'LPE boolean visibility'
|
||||
})
|
||||
fe_composite = etree.SubElement(hidder_filter, 'feComposite', {
|
||||
'id': 'boolops_hidder_primitive',
|
||||
'result': 'composite1',
|
||||
'operator': 'arithmetic',
|
||||
'in2': 'SourceGraphic',
|
||||
'in': 'BackgroundImage'
|
||||
})
|
||||
|
||||
lpe_refs = []
|
||||
|
||||
for operand in operand_elements:
|
||||
operand_id = operand.get('id')
|
||||
if not operand_id:
|
||||
continue
|
||||
|
||||
lpe_id = svg.get_unique_id('path-effect')
|
||||
|
||||
from lxml import etree
|
||||
path_effect = etree.SubElement(defs, '{http://www.inkscape.org/namespaces/inkscape}path-effect', {
|
||||
'effect': 'bool_op',
|
||||
'operand-path': f'#{operand_id}',
|
||||
'id': lpe_id,
|
||||
'is_visible': 'true',
|
||||
'lpeversion': '1',
|
||||
'operation': operation,
|
||||
'swap-operands': 'false',
|
||||
'filltype-this': 'from-curve',
|
||||
'filter': '',
|
||||
'filltype-operand': 'from-curve'
|
||||
})
|
||||
|
||||
lpe_refs.append(f'#{lpe_id}')
|
||||
|
||||
existing_style = operand.get('style', '')
|
||||
if 'filter:' not in existing_style:
|
||||
if existing_style and not existing_style.endswith(';'):
|
||||
existing_style += ';'
|
||||
operand.set('style', existing_style + f'filter:url(#{hidder_filter_id})')
|
||||
|
||||
existing_lpe = element.get('{http://www.inkscape.org/namespaces/inkscape}path-effect', '')
|
||||
if existing_lpe:
|
||||
new_lpe = existing_lpe + ';' + ';'.join(lpe_refs)
|
||||
else:
|
||||
new_lpe = ';'.join(lpe_refs)
|
||||
|
||||
element.set('{http://www.inkscape.org/namespaces/inkscape}path-effect', new_lpe)
|
||||
|
||||
return element
|
||||
Reference in New Issue
Block a user