mirror of
https://github.com/gumyr/build123d.git
synced 2025-12-06 02:30:55 -08:00
Adding Mixin1D.curvature_comb
Some checks failed
benchmarks / benchmarks (macos-13, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-13, 3.10) (push) Has been cancelled
tests / tests (macos-13, 3.13) (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
Some checks failed
benchmarks / benchmarks (macos-13, 3.12) (push) Has been cancelled
benchmarks / benchmarks (macos-14, 3.12) (push) Has been cancelled
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Has been cancelled
benchmarks / benchmarks (windows-latest, 3.12) (push) Has been cancelled
Upload coverage reports to Codecov / run (push) Has been cancelled
pylint / lint (3.10) (push) Has been cancelled
Run type checker / typecheck (3.10) (push) Has been cancelled
Run type checker / typecheck (3.13) (push) Has been cancelled
Wheel building and publishing / Build wheel on ubuntu-latest (push) Has been cancelled
tests / tests (macos-13, 3.10) (push) Has been cancelled
tests / tests (macos-13, 3.13) (push) Has been cancelled
tests / tests (macos-14, 3.10) (push) Has been cancelled
tests / tests (macos-14, 3.13) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
tests / tests (windows-latest, 3.10) (push) Has been cancelled
tests / tests (windows-latest, 3.13) (push) Has been cancelled
Wheel building and publishing / upload_pypi (push) Has been cancelled
This commit is contained in:
parent
5681bfb905
commit
790f0eaced
2 changed files with 147 additions and 3 deletions
|
|
@ -53,10 +53,11 @@ from __future__ import annotations
|
|||
|
||||
import copy
|
||||
import itertools
|
||||
import numpy as np
|
||||
import warnings
|
||||
from collections.abc import Iterable
|
||||
from itertools import combinations
|
||||
from math import radians, inf, pi, cos, copysign, ceil, floor
|
||||
from math import radians, inf, pi, cos, copysign, ceil, floor, isclose
|
||||
from typing import cast as tcast
|
||||
from typing import Literal, overload, TYPE_CHECKING
|
||||
from typing_extensions import Self
|
||||
|
|
@ -493,6 +494,89 @@ class Mixin1D(Shape):
|
|||
|
||||
return result
|
||||
|
||||
def curvature_comb(
|
||||
self, count: int = 100, max_tooth_size: float | None = None
|
||||
) -> ShapeList[Edge]:
|
||||
"""
|
||||
Build a *curvature comb* for a planar (XY) 1D curve.
|
||||
|
||||
A curvature comb is a set of short line segments (“teeth”) erected
|
||||
perpendicular to the curve that visualize the signed curvature κ(u).
|
||||
Tooth length is proportional to |κ| and the direction encodes the sign
|
||||
(left normal for κ>0, right normal for κ<0). This is useful for inspecting
|
||||
fairness and continuity (C0/C1/C2) of edges and wires.
|
||||
|
||||
Args:
|
||||
count (int, optional): Number of uniformly spaced samples over the normalized
|
||||
parameter. Increase for a denser comb. Defaults to 100.
|
||||
max_tooth_size (float | None, optional): Maximum tooth height in model units.
|
||||
If None, set to 10% maximum curve dimension. Defaults to None.
|
||||
|
||||
Raises:
|
||||
ValueError: Empty curve.
|
||||
ValueError: If the curve is not planar on `Plane.XY`.
|
||||
|
||||
Returns:
|
||||
ShapeList[Edge]: A list of short `Edge` objects (lines) anchored on the curve
|
||||
and oriented along the left normal `n̂ = normalize(t) × +Z`.
|
||||
|
||||
Notes:
|
||||
- On circles, κ = 1/R so tooth length is constant.
|
||||
- On straight segments, κ = 0 so no teeth are drawn.
|
||||
- At inflection points κ→0 and the tooth flips direction.
|
||||
- At C0 corners the tangent is discontinuous; nearby teeth may jump.
|
||||
C1 yields continuous direction; C2 yields continuous magnitude as well.
|
||||
|
||||
Example:
|
||||
>>> comb = my_wire.curvature_comb(count=200, max_tooth_size=2.0)
|
||||
>>> show(my_wire, Curve(comb))
|
||||
|
||||
"""
|
||||
if self.wrapped is None:
|
||||
raise ValueError("Can't create curvature_comb for empty curve")
|
||||
pln = self.common_plane()
|
||||
if pln is None or not isclose(abs(pln.z_dir.Z), 1.0, abs_tol=TOLERANCE):
|
||||
raise ValueError("curvature_comb only works for curves on Plane.XY")
|
||||
|
||||
# If periodic the first and last tooth would be the same so skip them
|
||||
u_values = np.linspace(0, 1, count, endpoint=not self.is_closed)
|
||||
|
||||
# first pass: gather kappas for scaling
|
||||
kappas = []
|
||||
tangents, curvatures = [], []
|
||||
for u in u_values:
|
||||
tangent = self.derivative_at(u, 1)
|
||||
curvature = self.derivative_at(u, 2)
|
||||
tangents.append(tangent)
|
||||
curvatures.append(curvature)
|
||||
cross = tangent.cross(curvature)
|
||||
kappa = cross.length / (tangent.length**3 + TOLERANCE)
|
||||
# signed for XY:
|
||||
sign = 1.0 if cross.Z >= 0 else -1.0
|
||||
kappas.append(sign * kappa)
|
||||
|
||||
# choose a scale so the tallest tooth is max_tooth_size
|
||||
max_kappa_size = max(TOLERANCE, max(abs(k) for k in kappas))
|
||||
curve_size = max(self.bounding_box().size)
|
||||
max_tooth_size = (
|
||||
max_tooth_size if max_tooth_size is not None else curve_size / 10
|
||||
)
|
||||
scale = max_tooth_size / max_kappa_size
|
||||
|
||||
comb_edges = ShapeList[Edge]()
|
||||
for u, kappa, tangent in zip(u_values, kappas, tangents):
|
||||
# Avoid tiny teeth
|
||||
if abs(length := scale * kappa) < TOLERANCE:
|
||||
continue
|
||||
pnt_on_curve = self @ u
|
||||
# left normal in XY (principal normal direction for a planar curve)
|
||||
kappa_dir = tangent.normalized().cross(Vector(0, 0, 1))
|
||||
comb_edges.append(
|
||||
Edge.make_line(pnt_on_curve, pnt_on_curve + length * kappa_dir)
|
||||
)
|
||||
|
||||
return comb_edges
|
||||
|
||||
def derivative_at(
|
||||
self,
|
||||
position: float | VectorLike,
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ from build123d.build_enums import (
|
|||
Side,
|
||||
SortBy,
|
||||
)
|
||||
from build123d.geometry import Axis, Location, Plane, Vector
|
||||
from build123d.objects_curve import Polyline
|
||||
from build123d.geometry import Axis, Location, Plane, Rot, Vector, TOLERANCE
|
||||
from build123d.objects_curve import CenterArc, Line, Polyline
|
||||
from build123d.objects_part import Box, Cylinder
|
||||
from build123d.operations_part import extrude
|
||||
from build123d.operations_generic import fillet
|
||||
|
|
@ -437,5 +437,65 @@ class TestMixin1D(unittest.TestCase):
|
|||
Edge.extrude(pnt, (0, 0, 1))
|
||||
|
||||
|
||||
class TestCurvatureComb(unittest.TestCase):
|
||||
def test_raises_if_not_on_XY(self):
|
||||
line_xz = Polyline((0, 0, 0), (1, 0, 0), (0, 0, 1))
|
||||
with self.assertRaises(ValueError):
|
||||
_ = line_xz.curvature_comb()
|
||||
|
||||
def test_empty_curve(self):
|
||||
c = CenterArc((0, 0), 1, 0, 360)
|
||||
c.wrapped = None
|
||||
with self.assertRaises(ValueError):
|
||||
c.curvature_comb()
|
||||
|
||||
def test_circle_constant_height_and_count(self):
|
||||
radius = 5.0
|
||||
count = 64
|
||||
max_tooth = 2.0
|
||||
|
||||
# A closed circle in the XY plane
|
||||
c = CenterArc((0, 0), radius, 0, 360)
|
||||
comb = c.curvature_comb(count=count, max_tooth_size=max_tooth)
|
||||
|
||||
# For a closed curve, endpoint is excluded but the method still returns `count` samples.
|
||||
self.assertEqual(len(comb), count)
|
||||
|
||||
# On a circle, kappa = 1/R => all teeth should have the same length = max_tooth
|
||||
lengths = [edge.length for edge in comb]
|
||||
self.assertTrue(all(abs(L - max_tooth) <= TOLERANCE for L in lengths))
|
||||
|
||||
# Direction check: teeth should be radial (perpendicular to tangent),
|
||||
# i.e., aligned with (start_point - center). For Circle(...) center is (0,0,0).
|
||||
center = Vector(0, 0, 0)
|
||||
for edge in comb[:: max(1, len(comb) // 8)]: # sample a few
|
||||
p0 = edge.position_at(0.0)
|
||||
p1 = edge.position_at(1.0)
|
||||
tooth_dir = (p1 - p0).normalized()
|
||||
radial = (p0 - center).normalized()
|
||||
# allow either direction (outward/inward), check colinearity
|
||||
cross_len = tooth_dir.cross(radial).length
|
||||
self.assertLessEqual(cross_len, 1e-3)
|
||||
|
||||
def test_line_near_zero_teeth_and_count(self):
|
||||
# Straight segment in XY => curvature = 0 everywhere
|
||||
line = Line((0, 0), (10, 0))
|
||||
|
||||
count = 25
|
||||
comb = line.curvature_comb(count=count, max_tooth_size=3.0)
|
||||
|
||||
self.assertEqual(len(comb), 0) # They are 0 length so skipped
|
||||
|
||||
def test_open_arc_count_and_variation(self):
|
||||
# Open arc: teeth count == requested count; lengths not constant in general
|
||||
arc = CenterArc((0, 0), 5, 0, 180) # open, CCW half-circle
|
||||
count = 40
|
||||
comb = arc.curvature_comb(count=count, max_tooth_size=1.0)
|
||||
self.assertEqual(len(comb), count)
|
||||
# For a circular arc, curvature is constant, so lengths should still be constant
|
||||
lengths = [e.length for e in comb]
|
||||
self.assertLessEqual(max(lengths) - min(lengths), 1e-6)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue