Skip to content

Commit ee6aaa2

Browse files
committed
add endpoint curvature examples
1 parent 4ec7c90 commit ee6aaa2

3 files changed

Lines changed: 470 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Example showing how to create a thickness distribution"""
2+
3+
# -------------------------------------------------------------------------------------------------------------------- #
4+
# Importing packages
5+
# -------------------------------------------------------------------------------------------------------------------- #
6+
import numpy as np
7+
import nurbspy.jax as nrb
8+
import matplotlib.pyplot as plt
9+
10+
11+
def cross2d(a, b):
12+
return a[0] * b[1] - a[1] * b[0]
13+
14+
15+
x0 = 0.00
16+
y0 = 0.00
17+
18+
x2 = 0.2
19+
y2 = 0.2
20+
21+
theta_0 = np.radians(90.0)
22+
theta_2 = np.radians(10.0)
23+
24+
R = 0.5
25+
26+
A = np.array([[np.cos(theta_0), np.cos(theta_2)], [np.sin(theta_0), np.sin(theta_2)]])
27+
b = np.array([x2 - x0, y2 - y0])
28+
d1, d2 = np.linalg.solve(A, b)
29+
30+
31+
x1 = x0 + d1 * np.cos(theta_0)
32+
y1 = y0 + d1 * np.sin(theta_0)
33+
34+
x1_bis = x2 - d2 * np.cos(theta_2)
35+
y1_bis = y2 - d2 * np.sin(theta_2)
36+
37+
38+
P = np.asarray([[x0, y0], [x1, y1], [x2, y2]]).T
39+
40+
D20 = P[:, 2] - P[:, 0] # P2 - P0
41+
D21 = P[:, 2] - P[:, 1] # P2 - P1
42+
43+
44+
w1 = np.sqrt((0.5 * R * abs(cross2d(D20, D21)) / np.linalg.norm(D21) ** 3))
45+
46+
print(f"Weight w1: {w1}")
47+
48+
49+
W = np.asarray([1, w1, 1])
50+
bezier = nrb.NurbsCurve(control_points=P, weights=W)
51+
fig, ax = bezier.plot(frenet_serret=False, control_points=True)
52+
53+
R_check = (
54+
(bezier.p - 1)
55+
/ bezier.p
56+
* (bezier.W[2] * bezier.W[0])
57+
/ bezier.W[1] ** 2
58+
* abs(cross2d(D20, D21))
59+
/ np.linalg.norm(D21) ** 3
60+
) ** -1
61+
62+
print(f"Input R: {R}")
63+
print(f"Checked R: {R_check}")
64+
65+
66+
plt.show()
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Example showing how to create quadratic rational Bézier curves with a prescribed endpoint radius."""
2+
3+
# -------------------------------------------------------------------------------------------------------------------- #
4+
# Importing packages
5+
# -------------------------------------------------------------------------------------------------------------------- #
6+
import numpy as np
7+
import nurbspy.jax as nrb
8+
import matplotlib.pyplot as plt
9+
10+
11+
def cross2d(a, b):
12+
"""Return the 2D scalar cross product."""
13+
return a[0] * b[1] - a[1] * b[0]
14+
15+
16+
def compute_intersection_control_point(x0, y0, x2, y2, theta_0, theta_2):
17+
"""
18+
Compute the middle control point P1 as the intersection of:
19+
- the ray starting at P0 with angle theta_0
20+
- the ray ending at P2 with tangent angle theta_2
21+
"""
22+
A = np.array(
23+
[
24+
[np.cos(theta_0), np.cos(theta_2)],
25+
[np.sin(theta_0), np.sin(theta_2)],
26+
]
27+
)
28+
b = np.array([x2 - x0, y2 - y0])
29+
30+
d1, d2 = np.linalg.solve(A, b)
31+
32+
x1 = x0 + d1 * np.cos(theta_0)
33+
y1 = y0 + d1 * np.sin(theta_0)
34+
35+
x1_bis = x2 - d2 * np.cos(theta_2)
36+
y1_bis = y2 - d2 * np.sin(theta_2)
37+
38+
# Optional consistency check
39+
if not np.allclose([x1, y1], [x1_bis, y1_bis]):
40+
raise ValueError("The two constructions of P1 do not match.")
41+
42+
return np.array([x1, y1])
43+
44+
45+
def build_control_points(x0, y0, x2, y2, theta_0, theta_2):
46+
"""Build the quadratic Bézier control points P0, P1, P2."""
47+
p1 = compute_intersection_control_point(x0, y0, x2, y2, theta_0, theta_2)
48+
P = np.asarray(
49+
[
50+
[x0, p1[0], x2],
51+
[y0, p1[1], y2],
52+
]
53+
)
54+
return P
55+
56+
57+
def compute_middle_weight_for_endpoint_radius(P, R):
58+
"""
59+
Compute the middle weight w1 for a quadratic rational Bézier curve
60+
with weights [1, w1, 1] so that the endpoint radius at u = 1 is R.
61+
"""
62+
D20 = P[:, 2] - P[:, 0] # P2 - P0
63+
D21 = P[:, 2] - P[:, 1] # P2 - P1
64+
65+
w1 = np.sqrt(0.5 * R * abs(cross2d(D20, D21)) / np.linalg.norm(D21) ** 3)
66+
return w1
67+
68+
69+
def compute_endpoint_curvature(P, W):
70+
"""
71+
Compute the analytic endpoint curvature at u = 1 for a quadratic rational Bézier curve.
72+
"""
73+
p = 2
74+
D20 = P[:, 2] - P[:, 0] # P2 - P0
75+
D21 = P[:, 2] - P[:, 1] # P2 - P1
76+
77+
curvature = (
78+
(p - 1)
79+
/ p
80+
* (W[2] * W[0])
81+
/ W[1] ** 2
82+
* abs(cross2d(D20, D21))
83+
/ np.linalg.norm(D21) ** 3
84+
)
85+
return curvature
86+
87+
88+
def compute_endpoint_radius(P, W):
89+
"""Compute the analytic endpoint radius at u = 1."""
90+
return 1.0 / compute_endpoint_curvature(P, W)
91+
92+
93+
def create_bezier_with_endpoint_radius(P, R):
94+
"""
95+
Create a quadratic rational Bézier curve with prescribed endpoint radius at u = 1.
96+
"""
97+
w1 = compute_middle_weight_for_endpoint_radius(P, R)
98+
W = np.asarray([1.0, w1, 1.0])
99+
bezier = nrb.NurbsCurve(control_points=P, weights=W)
100+
return bezier, W
101+
102+
103+
def evaluate_bezier_curve(bezier, num=300):
104+
"""Sample a Bézier/NURBS curve for plotting."""
105+
u = np.linspace(0.0, 1.0, num)
106+
C = bezier.get_value(u)
107+
return C
108+
109+
110+
# -------------------------------------------------------------------------------------------------------------------- #
111+
# Input data
112+
# -------------------------------------------------------------------------------------------------------------------- #
113+
x0 = 0.00
114+
y0 = 0.00
115+
116+
x2 = 0.2
117+
y2 = 0.2
118+
119+
theta_0 = np.radians(90.0)
120+
theta_2 = np.radians(00.0)
121+
122+
target_radii = [0.5, 1.0, 1.5]
123+
124+
125+
# -------------------------------------------------------------------------------------------------------------------- #
126+
# Build common control polygon
127+
# -------------------------------------------------------------------------------------------------------------------- #
128+
P = build_control_points(x0, y0, x2, y2, theta_0, theta_2)
129+
130+
131+
# -------------------------------------------------------------------------------------------------------------------- #
132+
# Create curves and plot
133+
# -------------------------------------------------------------------------------------------------------------------- #
134+
fig, ax = plt.subplots(figsize=(8, 6))
135+
136+
# Plot control polygon once
137+
ax.plot(P[0, :], P[1, :], "k--", marker="o", label="Control polygon")
138+
139+
for R in target_radii:
140+
bezier, W = create_bezier_with_endpoint_radius(P, R)
141+
C = evaluate_bezier_curve(bezier, num=400)
142+
143+
R_check = compute_endpoint_radius(P, W)
144+
145+
print(f"Input R: {R:.4f}")
146+
print(f"Weight w1: {W[1]:.6f}")
147+
print(f"Checked R: {R_check:.6f}")
148+
print()
149+
150+
ax.plot(C[0, :], C[1, :], label=f"R = {R:.1f}")
151+
152+
153+
# -------------------------------------------------------------------------------------------------------------------- #
154+
# Plot formatting
155+
# -------------------------------------------------------------------------------------------------------------------- #
156+
ax.set_xlabel("x")
157+
ax.set_ylabel("y")
158+
ax.set_title("Quadratic rational Bézier curves with prescribed endpoint radius")
159+
ax.axis("equal")
160+
ax.grid(True)
161+
ax.legend()
162+
163+
plt.show()

0 commit comments

Comments
 (0)