bundle: update (2026-01-16)
This commit is contained in:
208
extensions/km-living-hinge/km_living_hinge.py
Normal file
208
extensions/km-living-hinge/km_living_hinge.py
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
#################################################
|
||||
#
|
||||
# KM Living Hinge
|
||||
# A highly opinionated living hinge generator
|
||||
#
|
||||
# With love, by Jondale
|
||||
#
|
||||
#################################################
|
||||
|
||||
import math
|
||||
import inkex
|
||||
from inkex import ShapeElement, bezier
|
||||
|
||||
from shapes import get_shape, get_config
|
||||
from livinghinge import generate_hinge, segments_to_svg_paths
|
||||
|
||||
|
||||
class KMLivingHinge(inkex.EffectExtension):
|
||||
def add_arguments(self, pars) -> None:
|
||||
pars.add_argument("--units", default="mm")
|
||||
pars.add_argument("--angle", type=float, default=0.0)
|
||||
pars.add_argument("--type", default="line")
|
||||
pars.add_argument("--line_height_pct", type=int, default=80)
|
||||
pars.add_argument("--line_x_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--line_y_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--fishbone_height", type=float, default=10.0)
|
||||
pars.add_argument("--fishbone_width", type=float, default=5.0)
|
||||
pars.add_argument("--fishbone_x_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--fishbone_y_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--cross_height", type=float, default=10.0)
|
||||
pars.add_argument("--cross_width", type=float, default=5.0)
|
||||
pars.add_argument("--bezier_height", type=float, default=10.0)
|
||||
pars.add_argument("--bezier_width", type=float, default=5.0)
|
||||
pars.add_argument("--bezier_x_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--bezier_y_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--wave_height", type=float, default=10.0)
|
||||
pars.add_argument("--wave_width", type=float, default=5.0)
|
||||
pars.add_argument("--fabric_height", type=float, default=10.0)
|
||||
pars.add_argument("--fabric_width", type=float, default=5.0)
|
||||
pars.add_argument("--circle_height", type=float, default=5.0)
|
||||
pars.add_argument("--circle_width", type=float, default=5.0)
|
||||
pars.add_argument("--circle_x_spacing", type=float, default=2.0)
|
||||
pars.add_argument("--circle_y_spacing", type=float, default=2.0)
|
||||
|
||||
def effect(self) -> None:
|
||||
opts = self.options
|
||||
pattern_type = opts.type or "line"
|
||||
if pattern_type not in ("line", "fishbone", "cross", "bezier", "wave", "fabric", "circle"):
|
||||
pattern_type = "line"
|
||||
|
||||
selection = getattr(self.svg, "selection", None)
|
||||
if not selection or len(selection) == 0:
|
||||
raise inkex.AbortExtension(
|
||||
"Select a shape (path, rectangle, circle, etc.); the hinge fills and is trimmed to your selection."
|
||||
)
|
||||
selected_elems = list(selection)
|
||||
|
||||
def _uu(value, unit):
|
||||
return float(self.svg.unittouu(f"{value}{unit}"))
|
||||
|
||||
def _superpath_to_polygons(superpath):
|
||||
polys = []
|
||||
for sub in superpath:
|
||||
if not sub:
|
||||
continue
|
||||
poly = []
|
||||
for node in sub:
|
||||
if len(node) >= 2 and len(node[1]) == 2:
|
||||
poly.append((node[1][0], node[1][1]))
|
||||
if len(poly) >= 3:
|
||||
if poly[0] != poly[-1]:
|
||||
poly.append(poly[0])
|
||||
polys.append(poly)
|
||||
return polys
|
||||
|
||||
seen_shapes = set()
|
||||
shapes_to_process = []
|
||||
|
||||
def _iter_shapes(elem):
|
||||
if isinstance(elem, ShapeElement) and not isinstance(elem, inkex.Group):
|
||||
yield elem
|
||||
if isinstance(elem, inkex.Group):
|
||||
for child in elem.iterdescendants():
|
||||
if isinstance(child, ShapeElement) and not isinstance(child, inkex.Group):
|
||||
yield child
|
||||
return
|
||||
for child in elem.iterdescendants():
|
||||
if isinstance(child, ShapeElement) and not isinstance(child, inkex.Group):
|
||||
yield child
|
||||
|
||||
for elem in selected_elems:
|
||||
for shape in _iter_shapes(elem):
|
||||
key = getattr(shape, "get_id", lambda: None)() or shape.get("id") or id(shape)
|
||||
if key in seen_shapes:
|
||||
continue
|
||||
seen_shapes.add(key)
|
||||
shapes_to_process.append(shape)
|
||||
|
||||
if not shapes_to_process:
|
||||
raise inkex.AbortExtension("Unable to read the selected shape geometry.")
|
||||
|
||||
min_x, min_y, max_x, max_y = float('inf'), float('inf'), float('-inf'), float('-inf')
|
||||
for shape in shapes_to_process:
|
||||
bbox = shape.bounding_box(shape.composed_transform())
|
||||
if bbox:
|
||||
min_x = min(min_x, bbox.left)
|
||||
min_y = min(min_y, bbox.top)
|
||||
max_x = max(max_x, bbox.right)
|
||||
max_y = max(max_y, bbox.bottom)
|
||||
bbox_width = max_x - min_x
|
||||
bbox_height = max_y - min_y
|
||||
bbox = (min_x, min_y, bbox_width, bbox_height)
|
||||
|
||||
unit = opts.units
|
||||
angle_rad = math.radians(float(opts.angle))
|
||||
|
||||
if pattern_type == "line":
|
||||
height = opts.line_height_pct / 100.0
|
||||
width = 0
|
||||
x_spacing = _uu(opts.line_x_spacing, unit)
|
||||
y_spacing = _uu(opts.line_y_spacing, unit)
|
||||
elif pattern_type == "fishbone":
|
||||
height = _uu(opts.fishbone_height, unit)
|
||||
width = _uu(opts.fishbone_width, unit)
|
||||
x_spacing = _uu(opts.fishbone_x_spacing, unit)
|
||||
y_spacing = _uu(opts.fishbone_y_spacing, unit)
|
||||
elif pattern_type == "cross":
|
||||
height = _uu(opts.cross_height, unit)
|
||||
width = _uu(opts.cross_width, unit)
|
||||
x_spacing = 0
|
||||
y_spacing = 0
|
||||
elif pattern_type == "bezier":
|
||||
height = _uu(opts.bezier_height, unit)
|
||||
width = _uu(opts.bezier_width, unit)
|
||||
x_spacing = _uu(opts.bezier_x_spacing, unit)
|
||||
y_spacing = _uu(opts.bezier_y_spacing, unit)
|
||||
elif pattern_type == "wave":
|
||||
height = _uu(opts.wave_height, unit)
|
||||
width = _uu(opts.wave_width, unit)
|
||||
x_spacing = 0
|
||||
y_spacing = 0
|
||||
elif pattern_type == "fabric":
|
||||
height = _uu(opts.fabric_height, unit)
|
||||
width = _uu(opts.fabric_width, unit)
|
||||
x_spacing = 0
|
||||
y_spacing = 0
|
||||
elif pattern_type == "circle":
|
||||
height = _uu(opts.circle_height, unit)
|
||||
width = _uu(opts.circle_width, unit)
|
||||
x_spacing = _uu(opts.circle_x_spacing, unit)
|
||||
y_spacing = _uu(opts.circle_y_spacing, unit)
|
||||
else:
|
||||
height = opts.line_height_pct / 100.0
|
||||
width = 0
|
||||
x_spacing = _uu(opts.line_x_spacing, unit)
|
||||
y_spacing = _uu(opts.line_y_spacing, unit)
|
||||
|
||||
config_fn = get_config(pattern_type)
|
||||
height, width, x_spacing, y_spacing, y_offset = config_fn(
|
||||
height, width, x_spacing, y_spacing, bbox
|
||||
)
|
||||
|
||||
if height <= 0:
|
||||
raise inkex.AbortExtension("Height must be greater than zero.")
|
||||
if width < 0:
|
||||
raise inkex.AbortExtension("Width must not be negative.")
|
||||
|
||||
stroke_width = float(self.svg.unittouu("0.25mm"))
|
||||
stroke_style = {
|
||||
"stroke": "#000000",
|
||||
"stroke-width": str(stroke_width),
|
||||
"fill": "none",
|
||||
"stroke-linecap": "round",
|
||||
}
|
||||
|
||||
parent = self.svg.get_current_layer()
|
||||
hinge_group = inkex.Group(id=self.svg.get_unique_id("km-living-hinge"))
|
||||
hinge_group.set("{http://www.inkscape.org/namespaces/inkscape}label", "KM Living Hinge")
|
||||
parent.add(hinge_group)
|
||||
|
||||
shape_fn = get_shape(pattern_type)
|
||||
|
||||
for shape in shapes_to_process:
|
||||
shape_path = shape.to_path_element().path.transform(shape.composed_transform())
|
||||
csp = shape_path.to_superpath()
|
||||
bezier.cspsubdiv(csp, 0.25)
|
||||
polygons = _superpath_to_polygons(csp)
|
||||
if not polygons:
|
||||
continue
|
||||
|
||||
segments = generate_hinge(
|
||||
polygons=polygons,
|
||||
shape_fn=shape_fn,
|
||||
height=height,
|
||||
width=width,
|
||||
x_spacing=x_spacing,
|
||||
y_spacing=y_spacing,
|
||||
y_offset=y_offset,
|
||||
angle_rad=angle_rad,
|
||||
)
|
||||
|
||||
segments_to_svg_paths(segments, stroke_style, hinge_group)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
KMLivingHinge().run()
|
||||
Reference in New Issue
Block a user