pyroll-core 3.1.13__tar.gz → 3.2.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/PKG-INFO +1 -1
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/__init__.py +19 -6
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/hookimpls.py +0 -4
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/__init__.py +10 -1
- pyroll_core-3.2.0b2/pyroll/core/grooves/angles/__init__.py +8 -0
- pyroll_core-3.2.0b2/pyroll/core/grooves/angles/_geometry.py +224 -0
- pyroll_core-3.2.0b2/pyroll/core/grooves/angles/_path.py +203 -0
- pyroll_core-3.2.0b2/pyroll/core/grooves/angles/butterfly_groove.py +544 -0
- pyroll_core-3.2.0b2/pyroll/core/grooves/angles/factories.py +67 -0
- pyroll_core-3.2.0b2/pyroll/core/grooves/generic_angle.py +129 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/hooks.py +22 -33
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/hookimpls.py +1 -2
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/__init__.py +2 -0
- pyroll_core-3.2.0b2/pyroll/core/roll_pass/asymmetric_two_roll_pass.py +159 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/base.py +0 -22
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/__init__.py +1 -0
- pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/asymmetric_two_roll_pass.py +194 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/deformation_unit.py +1 -5
- pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/engine.py +47 -0
- pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/helpers.py +65 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/profile.py +9 -1
- pyroll_core-3.2.0b2/pyroll/core/roll_pass/hookimpls/roll.py +182 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/symmetric_roll_pass.py +18 -4
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/sequence.py +46 -8
- pyroll_core-3.2.0b2/pyroll/core/transport/__init__.py +7 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/__init__.py +0 -2
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/cooling_pipe.py +1 -2
- pyroll_core-3.2.0b2/pyroll/core/transport/hookimpls/spooler.py +68 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/hookimpls/transport.py +2 -10
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/spooler.py +0 -21
- pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/engine.py +0 -29
- pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/helpers.py +0 -25
- pyroll_core-3.1.13/pyroll/core/roll_pass/hookimpls/roll.py +0 -75
- pyroll_core-3.1.13/pyroll/core/transport/__init__.py +0 -9
- pyroll_core-3.1.13/pyroll/core/transport/hookimpls/induction_furnace.py +0 -23
- pyroll_core-3.1.13/pyroll/core/transport/hookimpls/spooler.py +0 -170
- pyroll_core-3.1.13/pyroll/core/transport/hookimpls/stelmor_conveyor.py +0 -11
- pyroll_core-3.1.13/pyroll/core/transport/induction_furnace.py +0 -81
- pyroll_core-3.1.13/pyroll/core/transport/stelmor_conveyor.py +0 -71
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/.gitignore +1 -1
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/LICENSE +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/README.md +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/hatch.toml +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyproject.toml +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/config.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/disk_element_unit.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/disk_elements/hookimpls.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/engine/engine.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/base.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/box.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/constricted_box.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/boxes/upset_box.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/diamond.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/gothic.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/diamonds/square.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/equivalent_ripped_groove.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/flat.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/generic_elongation.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/generic_elongation_solvers.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/hexagonal.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/circular_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/constricted_circular_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/constricted_swedish_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/flat_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/oval_3radii.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/oval_3radii_flanked.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/swedish_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/ovals/upset_oval.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/false_round.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/rounds/round.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/grooves/spline.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/log.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/hookimpls.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/profile/profile.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/repr.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll/roll.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/deformation_unit.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/base_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/disk_element.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/symmetric_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/three_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/hookimpls/two_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/three_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/roll_pass/two_roll_pass.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/hookimpls.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/rotator/rotator.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/sequence/hookimpls.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/shapes.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/cooling_pipe.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/transport/transport.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/__init__.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/hookimpls.py +0 -0
- {pyroll_core-3.1.13 → pyroll_core-3.2.0b2}/pyroll/core/unit/unit.py +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyroll-core
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0b2
|
|
4
4
|
Summary: PyRoll rolling simulation framework - core library.
|
|
5
5
|
Project-URL: Homepage, https://pyroll-project.github.io
|
|
6
6
|
Project-URL: Repository, https://github.com/pyroll-project/pyroll-core
|
|
@@ -24,10 +24,20 @@ from .grooves import (
|
|
|
24
24
|
FlatGroove,
|
|
25
25
|
HexagonalGroove,
|
|
26
26
|
EquivalentRibbedGroove,
|
|
27
|
+
ButterflyGroove,
|
|
28
|
+
GroovePair,
|
|
29
|
+
butterfly_grooves,
|
|
27
30
|
create_groove_by_type_name,
|
|
28
31
|
)
|
|
29
|
-
from .transport import Transport, CoolingPipe, Spooler
|
|
30
|
-
from .roll_pass import
|
|
32
|
+
from .transport import Transport, CoolingPipe, Spooler
|
|
33
|
+
from .roll_pass import (
|
|
34
|
+
BaseRollPass,
|
|
35
|
+
DeformationUnit,
|
|
36
|
+
ThreeRollPass,
|
|
37
|
+
SymmetricRollPass,
|
|
38
|
+
TwoRollPass,
|
|
39
|
+
AsymmetricTwoRollPass,
|
|
40
|
+
)
|
|
31
41
|
from .roll_pass import TwoRollPass as RollPass
|
|
32
42
|
from .unit import Unit
|
|
33
43
|
from .roll import Roll
|
|
@@ -45,7 +55,7 @@ from .hooks import Hook, HookHost, HookFunction, root_hooks
|
|
|
45
55
|
from .disk_elements import DiskElementUnit
|
|
46
56
|
from .config import Config, config, PlottingBackend, ConfigValue, ConfigMeta
|
|
47
57
|
|
|
48
|
-
VERSION = "3.
|
|
58
|
+
VERSION = "3.2.0b2"
|
|
49
59
|
|
|
50
60
|
__all__ = [
|
|
51
61
|
# grooves
|
|
@@ -72,6 +82,9 @@ __all__ = [
|
|
|
72
82
|
"FlatGroove",
|
|
73
83
|
"HexagonalGroove",
|
|
74
84
|
"EquivalentRibbedGroove",
|
|
85
|
+
"ButterflyGroove",
|
|
86
|
+
"GroovePair",
|
|
87
|
+
"butterfly_grooves",
|
|
75
88
|
"create_groove_by_type_name",
|
|
76
89
|
# profile
|
|
77
90
|
"Profile",
|
|
@@ -85,8 +98,6 @@ __all__ = [
|
|
|
85
98
|
"Transport",
|
|
86
99
|
"CoolingPipe",
|
|
87
100
|
"Spooler",
|
|
88
|
-
"StelmorConveyor",
|
|
89
|
-
"InductionFurnace",
|
|
90
101
|
# roll_pass
|
|
91
102
|
"RollPass",
|
|
92
103
|
"BaseRollPass",
|
|
@@ -118,8 +129,10 @@ __all__ = [
|
|
|
118
129
|
|
|
119
130
|
root_hooks.extend(
|
|
120
131
|
[
|
|
132
|
+
AsymmetricTwoRollPass.InProfile.cross_section,
|
|
121
133
|
BaseRollPass.roll_force,
|
|
122
|
-
|
|
134
|
+
SymmetricRollPass.Roll.roll_torque,
|
|
135
|
+
AsymmetricTwoRollPass.Roll.roll_torque,
|
|
123
136
|
BaseRollPass.Engine.power,
|
|
124
137
|
BaseRollPass.elongation_efficiency,
|
|
125
138
|
Unit.power,
|
|
@@ -2,11 +2,16 @@ from .base import GrooveBase
|
|
|
2
2
|
|
|
3
3
|
from .spline import SplineGroove
|
|
4
4
|
from .generic_elongation import GenericElongationGroove
|
|
5
|
+
from .generic_angle import GenericAngleGroove
|
|
5
6
|
|
|
6
7
|
from .boxes import BoxGroove, ConstrictedBoxGroove, UpsetBoxGroove, ConstrictedUpsetBoxGroove
|
|
7
8
|
|
|
8
9
|
from .diamonds import DiamondGroove, SquareGroove, GothicGroove
|
|
9
|
-
|
|
10
|
+
from .angles import (
|
|
11
|
+
ButterflyGroove,
|
|
12
|
+
GroovePair,
|
|
13
|
+
butterfly_grooves,
|
|
14
|
+
)
|
|
10
15
|
from .ovals import (
|
|
11
16
|
CircularOvalGroove,
|
|
12
17
|
FlatOvalGroove,
|
|
@@ -50,6 +55,10 @@ __all__ = [
|
|
|
50
55
|
"FlatGroove",
|
|
51
56
|
"HexagonalGroove",
|
|
52
57
|
"EquivalentRibbedGroove",
|
|
58
|
+
"ButterflyGroove",
|
|
59
|
+
"GroovePair",
|
|
60
|
+
"butterfly_grooves",
|
|
61
|
+
"GenericAngleGroove",
|
|
53
62
|
"create_groove_by_type_name",
|
|
54
63
|
]
|
|
55
64
|
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"line_through",
|
|
7
|
+
"line_point_angle",
|
|
8
|
+
"horizontal_line",
|
|
9
|
+
"vertical_line",
|
|
10
|
+
"intersect",
|
|
11
|
+
"offset_toward",
|
|
12
|
+
"perpendicular_foot",
|
|
13
|
+
"fillet_line_line",
|
|
14
|
+
"fillet_line_circle",
|
|
15
|
+
"line_circle_intersection",
|
|
16
|
+
"circle_circle_intersection",
|
|
17
|
+
"arc_points",
|
|
18
|
+
"Circle",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
Line = np.ndarray
|
|
22
|
+
Point = np.ndarray
|
|
23
|
+
|
|
24
|
+
Circle = Tuple[float, float, float]
|
|
25
|
+
"""Circle as tuple ``(center_z, center_y, radius)``."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def line_through(p1, p2) -> Line:
|
|
29
|
+
"""Line through two points (Matlab ``gpp``)."""
|
|
30
|
+
p1 = np.asarray(p1, dtype=float)
|
|
31
|
+
p2 = np.asarray(p2, dtype=float)
|
|
32
|
+
a = p2[1] - p1[1]
|
|
33
|
+
b = p1[0] - p2[0]
|
|
34
|
+
c = -a * p1[0] - b * p1[1]
|
|
35
|
+
return np.array([a, b, c])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def line_point_angle(p, angle: float) -> Line:
|
|
39
|
+
"""Line through a point with given angle measured from the vertical axis.
|
|
40
|
+
|
|
41
|
+
:param angle: angle to the y-axis in radians
|
|
42
|
+
"""
|
|
43
|
+
p = np.asarray(p, dtype=float)
|
|
44
|
+
p2 = p + np.array([-np.sin(angle), np.cos(angle)])
|
|
45
|
+
return line_through(p, p2)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def horizontal_line(y: float) -> Line:
|
|
49
|
+
"""Horizontal line at given y."""
|
|
50
|
+
return np.array([0.0, 1.0, -float(y)])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def vertical_line(z: float) -> Line:
|
|
54
|
+
"""Vertical line at given z."""
|
|
55
|
+
return np.array([1.0, 0.0, -float(z)])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def intersect(g1: Line, g2: Line) -> Point:
|
|
59
|
+
"""Intersection point of two lines."""
|
|
60
|
+
d = g1[0] * g2[1] - g2[0] * g1[1]
|
|
61
|
+
if np.isclose(d, 0):
|
|
62
|
+
raise ValueError("Lines are parallel, no intersection exists.")
|
|
63
|
+
z = (g2[2] * g1[1] - g1[2] * g2[1]) / d
|
|
64
|
+
y = (g1[2] * g2[0] - g2[2] * g1[0]) / d
|
|
65
|
+
return np.array([z, y])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _normalized(g: Line) -> Line:
|
|
69
|
+
n = np.hypot(g[0], g[1])
|
|
70
|
+
return g / n
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def signed_distance(g: Line, p) -> float:
|
|
74
|
+
"""Signed distance of a point from a line."""
|
|
75
|
+
g = _normalized(g)
|
|
76
|
+
p = np.asarray(p, dtype=float)
|
|
77
|
+
return g[0] * p[0] + g[1] * p[1] + g[2]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def offset_toward(g: Line, dist: float, toward) -> Line:
|
|
81
|
+
"""Parallel line at given distance, shifted towards the hint point."""
|
|
82
|
+
g = _normalized(g)
|
|
83
|
+
side = np.sign(signed_distance(g, toward))
|
|
84
|
+
if side == 0:
|
|
85
|
+
raise ValueError("Hint point lies on the line, offset side is ambiguous.")
|
|
86
|
+
return np.array([g[0], g[1], g[2] - side * dist])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def perpendicular_foot(g: Line, p) -> Point:
|
|
90
|
+
"""Foot of the perpendicular from a point onto a line."""
|
|
91
|
+
g = _normalized(g)
|
|
92
|
+
p = np.asarray(p, dtype=float)
|
|
93
|
+
return p - signed_distance(g, p) * g[:2]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def fillet_line_line(g1: Line, g2: Line, radius: float, toward, toward2=None) -> Tuple[Point, Point, Point]:
|
|
97
|
+
"""
|
|
98
|
+
Fillet circle of given radius tangent to two lines.
|
|
99
|
+
|
|
100
|
+
:param toward: hint point on the side of ``g1`` where the fillet center lies
|
|
101
|
+
:param toward2: hint point on the side of ``g2`` where the fillet center lies, defaults to ``toward``
|
|
102
|
+
:return: tuple of (center, tangent point on g1, tangent point on g2)
|
|
103
|
+
"""
|
|
104
|
+
if toward2 is None:
|
|
105
|
+
toward2 = toward
|
|
106
|
+
center = intersect(offset_toward(g1, radius, toward), offset_toward(g2, radius, toward2))
|
|
107
|
+
return center, perpendicular_foot(g1, center), perpendicular_foot(g2, center)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def fillet_corner(g1: Line, g2: Line, radius: float, along1, along2) -> Tuple[Point, Point, Point]:
|
|
111
|
+
"""
|
|
112
|
+
Fillet circle smoothing the corner between two lines, selecting the branch by the
|
|
113
|
+
retained line segments: the fillet lies inside the wedge spanned by the half-lines
|
|
114
|
+
from the corner towards ``along1`` and ``along2``.
|
|
115
|
+
|
|
116
|
+
:param along1: point on (or towards) the retained part of ``g1``
|
|
117
|
+
:param along2: point on (or towards) the retained part of ``g2``
|
|
118
|
+
:return: tuple of (center, tangent point on g1, tangent point on g2)
|
|
119
|
+
"""
|
|
120
|
+
corner = intersect(g1, g2)
|
|
121
|
+
|
|
122
|
+
d1 = np.asarray(along1, dtype=float) - corner
|
|
123
|
+
d2 = np.asarray(along2, dtype=float) - corner
|
|
124
|
+
d1 = d1 / np.hypot(*d1)
|
|
125
|
+
d2 = d2 / np.hypot(*d2)
|
|
126
|
+
|
|
127
|
+
bisector = d1 + d2
|
|
128
|
+
norm = np.hypot(*bisector)
|
|
129
|
+
if np.isclose(norm, 0):
|
|
130
|
+
raise ValueError("Retained segments are opposite, fillet side is ambiguous.")
|
|
131
|
+
|
|
132
|
+
hint = corner + bisector / norm
|
|
133
|
+
return fillet_line_line(g1, g2, radius, toward=hint)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def line_circle_intersection(g: Line, k: Circle, toward) -> Point:
|
|
137
|
+
"""Intersection of a line and a circle nearest to the hint point."""
|
|
138
|
+
g = _normalized(g)
|
|
139
|
+
m = np.array([k[0], k[1]])
|
|
140
|
+
r = k[2]
|
|
141
|
+
|
|
142
|
+
d = signed_distance(g, m)
|
|
143
|
+
if abs(d) > r:
|
|
144
|
+
raise ValueError("Line and circle do not intersect.")
|
|
145
|
+
|
|
146
|
+
foot_point = m - d * g[:2]
|
|
147
|
+
half_chord = np.sqrt(max(r**2 - d**2, 0))
|
|
148
|
+
direction = np.array([-g[1], g[0]])
|
|
149
|
+
|
|
150
|
+
candidates = [foot_point + half_chord * direction, foot_point - half_chord * direction]
|
|
151
|
+
toward = np.asarray(toward, dtype=float)
|
|
152
|
+
return min(candidates, key=lambda p: np.hypot(*(p - toward)))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def circle_circle_intersection(k1: Circle, k2: Circle, toward) -> Point:
|
|
156
|
+
"""Intersection of two circles nearest to the hint point; tangency point if they just touch."""
|
|
157
|
+
m1 = np.array([k1[0], k1[1]])
|
|
158
|
+
m2 = np.array([k2[0], k2[1]])
|
|
159
|
+
d = np.hypot(*(m2 - m1))
|
|
160
|
+
|
|
161
|
+
if d > k1[2] + k2[2] and not np.isclose(d, k1[2] + k2[2]):
|
|
162
|
+
raise ValueError("Circles do not intersect.")
|
|
163
|
+
if d < abs(k1[2] - k2[2]) and not np.isclose(d, abs(k1[2] - k2[2])):
|
|
164
|
+
raise ValueError("One circle lies inside the other, no intersection exists.")
|
|
165
|
+
|
|
166
|
+
a = (k1[2] ** 2 - k2[2] ** 2 + d**2) / (2 * d)
|
|
167
|
+
h = np.sqrt(max(k1[2] ** 2 - a**2, 0))
|
|
168
|
+
|
|
169
|
+
direction = (m2 - m1) / d
|
|
170
|
+
base = m1 + a * direction
|
|
171
|
+
perp = np.array([-direction[1], direction[0]])
|
|
172
|
+
|
|
173
|
+
candidates = [base + h * perp, base - h * perp]
|
|
174
|
+
toward = np.asarray(toward, dtype=float)
|
|
175
|
+
return min(candidates, key=lambda p: np.hypot(*(p - toward)))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def fillet_line_circle(
|
|
179
|
+
g: Line, k: Circle, radius: float, toward, externally: bool = True
|
|
180
|
+
) -> Tuple[Point, Point, Point]:
|
|
181
|
+
"""
|
|
182
|
+
Fillet circle of given radius tangent to a line and a circle.
|
|
183
|
+
|
|
184
|
+
:param toward: hint point selecting the offset side of the line and the solution branch
|
|
185
|
+
:param externally: whether the fillet touches the circle from outside (default) or inside
|
|
186
|
+
:return: tuple of (center, tangent point on the line, tangent point on the circle)
|
|
187
|
+
"""
|
|
188
|
+
g_offset = offset_toward(g, radius, toward)
|
|
189
|
+
expanded = (k[0], k[1], k[2] + radius if externally else k[2] - radius)
|
|
190
|
+
center = line_circle_intersection(g_offset, expanded, toward)
|
|
191
|
+
|
|
192
|
+
tangent_line = perpendicular_foot(g, center)
|
|
193
|
+
|
|
194
|
+
circle_center = np.array([k[0], k[1]])
|
|
195
|
+
direction = center - circle_center
|
|
196
|
+
direction = direction / np.hypot(*direction)
|
|
197
|
+
tangent_circle = circle_center + k[2] * direction
|
|
198
|
+
|
|
199
|
+
return center, tangent_line, tangent_circle
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def arc_points(center, start, end, count: int) -> np.ndarray:
|
|
203
|
+
"""
|
|
204
|
+
Discretize the circular arc from start to end point around center,
|
|
205
|
+
always sweeping the smaller angle.
|
|
206
|
+
|
|
207
|
+
:return: array of shape (count, 2) including start and end point
|
|
208
|
+
"""
|
|
209
|
+
center = np.asarray(center, dtype=float)
|
|
210
|
+
v1 = np.asarray(start, dtype=float) - center
|
|
211
|
+
v2 = np.asarray(end, dtype=float) - center
|
|
212
|
+
|
|
213
|
+
radius = np.hypot(*v1)
|
|
214
|
+
phi1 = np.arctan2(v1[1], v1[0])
|
|
215
|
+
phi2 = np.arctan2(v2[1], v2[0])
|
|
216
|
+
|
|
217
|
+
sweep = phi2 - phi1
|
|
218
|
+
if sweep > np.pi:
|
|
219
|
+
sweep -= 2 * np.pi
|
|
220
|
+
elif sweep < -np.pi:
|
|
221
|
+
sweep += 2 * np.pi
|
|
222
|
+
|
|
223
|
+
phi = phi1 + np.linspace(0, sweep, count)
|
|
224
|
+
return center + radius * np.stack([np.cos(phi), np.sin(phi)], axis=1)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from typing import Sequence, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from . import _geometry as geo
|
|
6
|
+
from ...config import Config
|
|
7
|
+
|
|
8
|
+
__all__ = ["chain_contour", "build_filleted_contour"]
|
|
9
|
+
|
|
10
|
+
Carrier = Union[np.ndarray, geo.Circle]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_line(carrier: Carrier) -> bool:
|
|
14
|
+
return isinstance(carrier, np.ndarray)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def chain_contour(start, steps: Sequence[tuple]) -> np.ndarray:
|
|
18
|
+
"""
|
|
19
|
+
Assemble a contour point array from explicit steps.
|
|
20
|
+
|
|
21
|
+
:param start: starting point of the contour
|
|
22
|
+
:param steps: sequence of steps, each either ``("line", point)`` for a straight
|
|
23
|
+
segment to ``point`` or ``("arc", center, point)`` for a circular arc around
|
|
24
|
+
``center`` to ``point`` (sweeping the smaller angle)
|
|
25
|
+
:return: array of shape (n, 2)
|
|
26
|
+
"""
|
|
27
|
+
n = Config.GROOVE_RADIUS_POINT_COUNT
|
|
28
|
+
current = np.asarray(start, dtype=float)
|
|
29
|
+
parts = [current.reshape(1, 2)]
|
|
30
|
+
|
|
31
|
+
for step in steps:
|
|
32
|
+
if step[0] == "line":
|
|
33
|
+
target = np.asarray(step[1], dtype=float)
|
|
34
|
+
parts.append(target.reshape(1, 2))
|
|
35
|
+
elif step[0] == "arc":
|
|
36
|
+
center, target = step[1], np.asarray(step[2], dtype=float)
|
|
37
|
+
arc = geo.arc_points(center, current, target, n)[1:]
|
|
38
|
+
arc[-1] = target # snap the recomputed arc end onto the exact target
|
|
39
|
+
parts.append(arc)
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Unknown contour step type: {step[0]!r}")
|
|
42
|
+
current = target
|
|
43
|
+
|
|
44
|
+
return np.vstack(parts)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _nominal_vertex(c1: Carrier, c2: Carrier, toward) -> np.ndarray:
|
|
48
|
+
"""Nominal junction point of two carriers: intersection resp. tangency nearest the hint."""
|
|
49
|
+
if _is_line(c1) and _is_line(c2):
|
|
50
|
+
return geo.intersect(c1, c2)
|
|
51
|
+
|
|
52
|
+
if _is_line(c1) or _is_line(c2):
|
|
53
|
+
line, circle = (c1, c2) if _is_line(c1) else (c2, c1)
|
|
54
|
+
try:
|
|
55
|
+
return geo.line_circle_intersection(line, circle, toward)
|
|
56
|
+
except ValueError:
|
|
57
|
+
center = np.array([circle[0], circle[1]])
|
|
58
|
+
foot = geo.perpendicular_foot(line, center)
|
|
59
|
+
direction = foot - center
|
|
60
|
+
direction = direction / np.hypot(*direction)
|
|
61
|
+
return (foot + center + circle[2] * direction) / 2
|
|
62
|
+
|
|
63
|
+
m1 = np.array([c1[0], c1[1]])
|
|
64
|
+
m2 = np.array([c2[0], c2[1]])
|
|
65
|
+
direction = m2 - m1
|
|
66
|
+
direction = direction / np.hypot(*direction)
|
|
67
|
+
return m1 + c1[2] * direction
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _arc_midpoint(circle: geo.Circle, p1, p2) -> np.ndarray:
|
|
71
|
+
"""Midpoint of the smaller arc of the circle between the projections of two points onto it."""
|
|
72
|
+
center = np.array([circle[0], circle[1]])
|
|
73
|
+
phi = []
|
|
74
|
+
for p in (p1, p2):
|
|
75
|
+
d = np.asarray(p, dtype=float) - center
|
|
76
|
+
phi.append(np.arctan2(d[1], d[0]))
|
|
77
|
+
|
|
78
|
+
sweep = phi[1] - phi[0]
|
|
79
|
+
if sweep > np.pi:
|
|
80
|
+
sweep -= 2 * np.pi
|
|
81
|
+
elif sweep < -np.pi:
|
|
82
|
+
sweep += 2 * np.pi
|
|
83
|
+
|
|
84
|
+
mid = phi[0] + sweep / 2
|
|
85
|
+
return center + circle[2] * np.array([np.cos(mid), np.sin(mid)])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fillet_line_circle_auto(
|
|
89
|
+
line: np.ndarray, circle: geo.Circle, radius: float, vertex, line_ref, circle_ref, hint=None
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
Fillet between a line and a circle with automatic branch selection.
|
|
93
|
+
|
|
94
|
+
The offset side hint is taken along the bisector between the retained line direction
|
|
95
|
+
and the direction to the midpoint of the retained circle arc (or from the explicit
|
|
96
|
+
``hint`` point); the internal/external branch by the solution whose tangent points
|
|
97
|
+
lie nearest to the nominal junction vertex.
|
|
98
|
+
"""
|
|
99
|
+
if hint is None:
|
|
100
|
+
d1 = np.asarray(line_ref, dtype=float) - vertex
|
|
101
|
+
d2 = _arc_midpoint(circle, vertex, circle_ref) - vertex
|
|
102
|
+
d1 = d1 / np.hypot(*d1)
|
|
103
|
+
norm2 = np.hypot(*d2)
|
|
104
|
+
if np.isclose(norm2, 0):
|
|
105
|
+
# junction vertex coincides with the retained arc midpoint: tangent line and circle,
|
|
106
|
+
# the fillet side degenerates to the circle side of the line
|
|
107
|
+
d2 = np.array([circle[0], circle[1]]) - vertex
|
|
108
|
+
norm2 = np.hypot(*d2)
|
|
109
|
+
d2 = d2 / norm2
|
|
110
|
+
bisector = d1 + d2
|
|
111
|
+
norm = np.hypot(*bisector)
|
|
112
|
+
if np.isclose(norm, 0):
|
|
113
|
+
raise ValueError("Retained segments are opposite, fillet side is ambiguous.")
|
|
114
|
+
hint = vertex + bisector / norm * radius
|
|
115
|
+
|
|
116
|
+
solutions = []
|
|
117
|
+
for externally in (True, False):
|
|
118
|
+
try:
|
|
119
|
+
center, t_line, t_circle = geo.fillet_line_circle(line, circle, radius, toward=hint, externally=externally)
|
|
120
|
+
except ValueError:
|
|
121
|
+
continue
|
|
122
|
+
score = np.hypot(*(t_line - vertex)) + np.hypot(*(t_circle - vertex))
|
|
123
|
+
solutions.append((score, center, t_line, t_circle))
|
|
124
|
+
|
|
125
|
+
if not solutions:
|
|
126
|
+
raise ValueError("No fillet between line and circle exists for the given radius.")
|
|
127
|
+
|
|
128
|
+
_, center, t_line, t_circle = min(solutions, key=lambda s: s[0])
|
|
129
|
+
return center, t_line, t_circle
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def build_filleted_contour(start, path: Sequence, end, hints: dict = None) -> np.ndarray:
|
|
133
|
+
"""
|
|
134
|
+
Build a contour from a chain of carriers with transition radii at the junctions.
|
|
135
|
+
|
|
136
|
+
:param start: starting point, lying on the first carrier
|
|
137
|
+
:param path: flat alternating sequence ``carrier, radius, carrier, radius, ..., carrier``;
|
|
138
|
+
lines as ``numpy`` arrays (see :py:mod:`._geometry`), circles as
|
|
139
|
+
``(center_z, center_y, radius)`` tuples; a radius of 0 gives a sharp corner
|
|
140
|
+
(line-line) resp. a direct tangent junction (with a circle carrier)
|
|
141
|
+
:param end: end point, lying on the last carrier
|
|
142
|
+
:param hints: optional explicit fillet side hint points by junction index (0-based,
|
|
143
|
+
counting the radii in ``path``), overriding the automatic side selection for
|
|
144
|
+
geometrically ambiguous junctions
|
|
145
|
+
:return: array of shape (n, 2)
|
|
146
|
+
"""
|
|
147
|
+
hints = hints or {}
|
|
148
|
+
carriers = list(path[0::2])
|
|
149
|
+
radii = list(path[1::2])
|
|
150
|
+
if len(carriers) != len(radii) + 1:
|
|
151
|
+
raise ValueError("Path must be an alternating sequence carrier, radius, ..., carrier.")
|
|
152
|
+
|
|
153
|
+
start = np.asarray(start, dtype=float)
|
|
154
|
+
end = np.asarray(end, dtype=float)
|
|
155
|
+
|
|
156
|
+
vertices = []
|
|
157
|
+
ref = start
|
|
158
|
+
for c1, c2 in zip(carriers[:-1], carriers[1:]):
|
|
159
|
+
if not _is_line(c1):
|
|
160
|
+
ref = 2 * np.array([c1[0], c1[1]]) - ref
|
|
161
|
+
ref = _nominal_vertex(c1, c2, ref)
|
|
162
|
+
vertices.append(ref)
|
|
163
|
+
|
|
164
|
+
ref_points = [start, *vertices, end]
|
|
165
|
+
|
|
166
|
+
steps = []
|
|
167
|
+
for i, radius in enumerate(radii):
|
|
168
|
+
c1, c2 = carriers[i], carriers[i + 1]
|
|
169
|
+
vertex = vertices[i]
|
|
170
|
+
prev_ref, next_ref = ref_points[i], ref_points[i + 2]
|
|
171
|
+
|
|
172
|
+
if radius == 0:
|
|
173
|
+
junction_steps = [("line", vertex)] if _is_line(c1) else [("arc", (c1[0], c1[1]), vertex)]
|
|
174
|
+
steps.append((junction_steps, vertex))
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
hint = hints.get(i)
|
|
178
|
+
|
|
179
|
+
if _is_line(c1) and _is_line(c2):
|
|
180
|
+
if hint is not None:
|
|
181
|
+
center, t1, t2 = geo.fillet_line_line(c1, c2, radius, toward=hint)
|
|
182
|
+
else:
|
|
183
|
+
center, t1, t2 = geo.fillet_corner(c1, c2, radius, along1=prev_ref, along2=next_ref)
|
|
184
|
+
elif _is_line(c1) or _is_line(c2):
|
|
185
|
+
if _is_line(c1):
|
|
186
|
+
line, circle, line_ref, circle_ref = c1, c2, prev_ref, next_ref
|
|
187
|
+
else:
|
|
188
|
+
line, circle, line_ref, circle_ref = c2, c1, next_ref, prev_ref
|
|
189
|
+
center, t_line, t_circle = _fillet_line_circle_auto(
|
|
190
|
+
line, circle, radius, vertex, line_ref, circle_ref, hint=hint
|
|
191
|
+
)
|
|
192
|
+
t1, t2 = (t_line, t_circle) if _is_line(c1) else (t_circle, t_line)
|
|
193
|
+
else:
|
|
194
|
+
raise NotImplementedError("Fillets between two circle carriers are not supported.")
|
|
195
|
+
|
|
196
|
+
approach = [("line", t1)] if _is_line(c1) else [("arc", (c1[0], c1[1]), t1)]
|
|
197
|
+
steps.append((approach + [("arc", center, t2)], t2))
|
|
198
|
+
|
|
199
|
+
last = carriers[-1]
|
|
200
|
+
final_step = ("line", end) if _is_line(last) else ("arc", (last[0], last[1]), end)
|
|
201
|
+
|
|
202
|
+
all_steps = [s for junction_steps, _ in steps for s in junction_steps] + [final_step]
|
|
203
|
+
return chain_contour(start, all_steps)
|