coordinate-system 4.0.3__cp313-cp313-win_amd64.whl → 5.2.0__cp313-cp313-win_amd64.whl
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.
- coordinate_system/__init__.py +42 -4
- coordinate_system/coordinate_system.cp313-win_amd64.pyd +0 -0
- coordinate_system/curve_interpolation.py +507 -0
- coordinate_system/fourier_frames.py +530 -0
- coordinate_system/visualization.py +666 -0
- {coordinate_system-4.0.3.dist-info → coordinate_system-5.2.0.dist-info}/METADATA +47 -4
- coordinate_system-5.2.0.dist-info/RECORD +13 -0
- coordinate_system-4.0.3.dist-info/RECORD +0 -10
- {coordinate_system-4.0.3.dist-info → coordinate_system-5.2.0.dist-info}/LICENSE +0 -0
- {coordinate_system-4.0.3.dist-info → coordinate_system-5.2.0.dist-info}/WHEEL +0 -0
- {coordinate_system-4.0.3.dist-info → coordinate_system-5.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
"""
|
|
2
|
+
3D Coordinate System and Curve Visualization Module
|
|
3
|
+
===================================================
|
|
4
|
+
|
|
5
|
+
This module provides tools for visualizing coordinate systems and curves in 3D space.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Draw coordinate systems with RGB colors (X=Red, Y=Green, Z=Blue)
|
|
9
|
+
- Visualize curves with vertices, tangents, and normals
|
|
10
|
+
- Display coordinate frame fields along curves
|
|
11
|
+
- Support for both single frames and frame arrays
|
|
12
|
+
|
|
13
|
+
Author: PanGuoJun
|
|
14
|
+
Date: 2025-12-01
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import matplotlib.pyplot as plt
|
|
19
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
20
|
+
from typing import List, Optional, Tuple, Callable, Union
|
|
21
|
+
|
|
22
|
+
# Import from the C extension module
|
|
23
|
+
try:
|
|
24
|
+
from .coordinate_system import vec3, coord3
|
|
25
|
+
except ImportError:
|
|
26
|
+
# Fallback for development
|
|
27
|
+
import coordinate_system
|
|
28
|
+
vec3 = coordinate_system.vec3
|
|
29
|
+
coord3 = coordinate_system.coord3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CoordinateSystemVisualizer:
|
|
33
|
+
"""
|
|
34
|
+
三维坐标系可视化工具
|
|
35
|
+
|
|
36
|
+
绘制坐标系时使用RGB颜色方案:
|
|
37
|
+
- X轴: 红色 (Red)
|
|
38
|
+
- Y轴: 绿色 (Green)
|
|
39
|
+
- Z轴: 蓝色 (Blue)
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, figsize: Tuple[int, int] = (12, 9)):
|
|
43
|
+
"""
|
|
44
|
+
初始化可视化工具
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
figsize: 图形大小 (width, height)
|
|
48
|
+
"""
|
|
49
|
+
self.fig = plt.figure(figsize=figsize)
|
|
50
|
+
self.ax = self.fig.add_subplot(111, projection='3d')
|
|
51
|
+
self._setup_axis()
|
|
52
|
+
|
|
53
|
+
def _setup_axis(self):
|
|
54
|
+
"""设置坐标轴样式"""
|
|
55
|
+
self.ax.set_xlabel('X', fontsize=12, color='red')
|
|
56
|
+
self.ax.set_ylabel('Y', fontsize=12, color='green')
|
|
57
|
+
self.ax.set_zlabel('Z', fontsize=12, color='blue')
|
|
58
|
+
self.ax.grid(True, alpha=0.3)
|
|
59
|
+
|
|
60
|
+
def draw_coord_system(
|
|
61
|
+
self,
|
|
62
|
+
coord: coord3,
|
|
63
|
+
scale: float = 1.0,
|
|
64
|
+
linewidth: float = 2.0,
|
|
65
|
+
alpha: float = 0.8,
|
|
66
|
+
label_prefix: str = ""
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
绘制单个坐标系
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
coord: coord3对象
|
|
73
|
+
scale: 轴长度缩放因子
|
|
74
|
+
linewidth: 线宽
|
|
75
|
+
alpha: 透明度
|
|
76
|
+
label_prefix: 标签前缀
|
|
77
|
+
"""
|
|
78
|
+
origin = coord.o
|
|
79
|
+
|
|
80
|
+
# X轴 (红色)
|
|
81
|
+
x_end = origin + coord.ux * scale
|
|
82
|
+
self.ax.plot(
|
|
83
|
+
[origin.x, x_end.x],
|
|
84
|
+
[origin.y, x_end.y],
|
|
85
|
+
[origin.z, x_end.z],
|
|
86
|
+
'r-', linewidth=linewidth, alpha=alpha,
|
|
87
|
+
label=f'{label_prefix}X' if label_prefix else None
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Y轴 (绿色)
|
|
91
|
+
y_end = origin + coord.uy * scale
|
|
92
|
+
self.ax.plot(
|
|
93
|
+
[origin.x, y_end.x],
|
|
94
|
+
[origin.y, y_end.y],
|
|
95
|
+
[origin.z, y_end.z],
|
|
96
|
+
'g-', linewidth=linewidth, alpha=alpha,
|
|
97
|
+
label=f'{label_prefix}Y' if label_prefix else None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Z轴 (蓝色)
|
|
101
|
+
z_end = origin + coord.uz * scale
|
|
102
|
+
self.ax.plot(
|
|
103
|
+
[origin.x, z_end.x],
|
|
104
|
+
[origin.y, z_end.y],
|
|
105
|
+
[origin.z, z_end.z],
|
|
106
|
+
'b-', linewidth=linewidth, alpha=alpha,
|
|
107
|
+
label=f'{label_prefix}Z' if label_prefix else None
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def draw_world_coord(
|
|
111
|
+
self,
|
|
112
|
+
origin: vec3 = None,
|
|
113
|
+
scale: float = 1.0,
|
|
114
|
+
linewidth: float = 3.0
|
|
115
|
+
):
|
|
116
|
+
"""
|
|
117
|
+
绘制世界坐标系
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
origin: 原点位置,默认为(0,0,0)
|
|
121
|
+
scale: 轴长度
|
|
122
|
+
linewidth: 线宽
|
|
123
|
+
"""
|
|
124
|
+
if origin is None:
|
|
125
|
+
origin = vec3(0, 0, 0)
|
|
126
|
+
|
|
127
|
+
world_coord = coord3()
|
|
128
|
+
world_coord.o = origin
|
|
129
|
+
|
|
130
|
+
self.draw_coord_system(
|
|
131
|
+
world_coord,
|
|
132
|
+
scale=scale,
|
|
133
|
+
linewidth=linewidth,
|
|
134
|
+
alpha=1.0,
|
|
135
|
+
label_prefix="World-"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def set_equal_aspect(self):
|
|
139
|
+
"""设置等比例显示"""
|
|
140
|
+
# 获取当前所有数据的范围
|
|
141
|
+
xlim = self.ax.get_xlim3d()
|
|
142
|
+
ylim = self.ax.get_ylim3d()
|
|
143
|
+
zlim = self.ax.get_zlim3d()
|
|
144
|
+
|
|
145
|
+
# 计算范围
|
|
146
|
+
x_range = abs(xlim[1] - xlim[0])
|
|
147
|
+
y_range = abs(ylim[1] - ylim[0])
|
|
148
|
+
z_range = abs(zlim[1] - zlim[0])
|
|
149
|
+
|
|
150
|
+
# 使用最大范围
|
|
151
|
+
max_range = max(x_range, y_range, z_range)
|
|
152
|
+
|
|
153
|
+
# 计算中心点
|
|
154
|
+
x_middle = np.mean(xlim)
|
|
155
|
+
y_middle = np.mean(ylim)
|
|
156
|
+
z_middle = np.mean(zlim)
|
|
157
|
+
|
|
158
|
+
# 设置相等的范围
|
|
159
|
+
self.ax.set_xlim3d([x_middle - max_range/2, x_middle + max_range/2])
|
|
160
|
+
self.ax.set_ylim3d([y_middle - max_range/2, y_middle + max_range/2])
|
|
161
|
+
self.ax.set_zlim3d([z_middle - max_range/2, z_middle + max_range/2])
|
|
162
|
+
|
|
163
|
+
def show(self):
|
|
164
|
+
"""显示图形"""
|
|
165
|
+
self.ax.legend()
|
|
166
|
+
plt.tight_layout()
|
|
167
|
+
plt.show()
|
|
168
|
+
|
|
169
|
+
def save(self, filename: str, dpi: int = 300):
|
|
170
|
+
"""
|
|
171
|
+
保存图形
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
filename: 文件名
|
|
175
|
+
dpi: 分辨率
|
|
176
|
+
"""
|
|
177
|
+
self.ax.legend()
|
|
178
|
+
plt.tight_layout()
|
|
179
|
+
plt.savefig(filename, dpi=dpi, bbox_inches='tight')
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class CurveVisualizer(CoordinateSystemVisualizer):
|
|
183
|
+
"""
|
|
184
|
+
曲线可视化工具
|
|
185
|
+
|
|
186
|
+
支持绘制:
|
|
187
|
+
- 曲线顶点
|
|
188
|
+
- 切线
|
|
189
|
+
- 法线
|
|
190
|
+
- 完整坐标系场
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
def __init__(self, figsize: Tuple[int, int] = (12, 9)):
|
|
194
|
+
"""
|
|
195
|
+
初始化曲线可视化工具
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
figsize: 图形大小
|
|
199
|
+
"""
|
|
200
|
+
super().__init__(figsize)
|
|
201
|
+
|
|
202
|
+
def draw_curve_vertices(
|
|
203
|
+
self,
|
|
204
|
+
points: List[vec3],
|
|
205
|
+
color: str = 'black',
|
|
206
|
+
linewidth: float = 2.0,
|
|
207
|
+
marker: str = 'o',
|
|
208
|
+
markersize: float = 4,
|
|
209
|
+
alpha: float = 0.7,
|
|
210
|
+
label: str = "Curve"
|
|
211
|
+
):
|
|
212
|
+
"""
|
|
213
|
+
绘制曲线顶点
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
points: 顶点列表
|
|
217
|
+
color: 颜色
|
|
218
|
+
linewidth: 线宽
|
|
219
|
+
marker: 标记样式
|
|
220
|
+
markersize: 标记大小
|
|
221
|
+
alpha: 透明度
|
|
222
|
+
label: 标签
|
|
223
|
+
"""
|
|
224
|
+
if not points:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
x = [p.x for p in points]
|
|
228
|
+
y = [p.y for p in points]
|
|
229
|
+
z = [p.z for p in points]
|
|
230
|
+
|
|
231
|
+
self.ax.plot(
|
|
232
|
+
x, y, z,
|
|
233
|
+
color=color,
|
|
234
|
+
linewidth=linewidth,
|
|
235
|
+
marker=marker,
|
|
236
|
+
markersize=markersize,
|
|
237
|
+
alpha=alpha,
|
|
238
|
+
label=label
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def draw_tangents(
|
|
242
|
+
self,
|
|
243
|
+
points: List[vec3],
|
|
244
|
+
tangents: List[vec3],
|
|
245
|
+
scale: float = 0.5,
|
|
246
|
+
color: str = 'orange',
|
|
247
|
+
linewidth: float = 1.5,
|
|
248
|
+
alpha: float = 0.8,
|
|
249
|
+
arrow: bool = True
|
|
250
|
+
):
|
|
251
|
+
"""
|
|
252
|
+
绘制切线
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
points: 曲线点列表
|
|
256
|
+
tangents: 切线向量列表
|
|
257
|
+
scale: 切线长度缩放
|
|
258
|
+
color: 颜色
|
|
259
|
+
linewidth: 线宽
|
|
260
|
+
alpha: 透明度
|
|
261
|
+
arrow: 是否使用箭头
|
|
262
|
+
"""
|
|
263
|
+
if len(points) != len(tangents):
|
|
264
|
+
raise ValueError("Points and tangents must have the same length")
|
|
265
|
+
|
|
266
|
+
for point, tangent in zip(points, tangents):
|
|
267
|
+
end = point + tangent * scale
|
|
268
|
+
|
|
269
|
+
if arrow:
|
|
270
|
+
self.ax.quiver(
|
|
271
|
+
point.x, point.y, point.z,
|
|
272
|
+
tangent.x * scale, tangent.y * scale, tangent.z * scale,
|
|
273
|
+
color=color,
|
|
274
|
+
linewidth=linewidth,
|
|
275
|
+
alpha=alpha,
|
|
276
|
+
arrow_length_ratio=0.3
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
self.ax.plot(
|
|
280
|
+
[point.x, end.x],
|
|
281
|
+
[point.y, end.y],
|
|
282
|
+
[point.z, end.z],
|
|
283
|
+
color=color,
|
|
284
|
+
linewidth=linewidth,
|
|
285
|
+
alpha=alpha
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
def draw_normals(
|
|
289
|
+
self,
|
|
290
|
+
points: List[vec3],
|
|
291
|
+
normals: List[vec3],
|
|
292
|
+
scale: float = 0.5,
|
|
293
|
+
color: str = 'purple',
|
|
294
|
+
linewidth: float = 1.5,
|
|
295
|
+
alpha: float = 0.8,
|
|
296
|
+
arrow: bool = True
|
|
297
|
+
):
|
|
298
|
+
"""
|
|
299
|
+
绘制法线
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
points: 曲线点列表
|
|
303
|
+
normals: 法线向量列表
|
|
304
|
+
scale: 法线长度缩放
|
|
305
|
+
color: 颜色
|
|
306
|
+
linewidth: 线宽
|
|
307
|
+
alpha: 透明度
|
|
308
|
+
arrow: 是否使用箭头
|
|
309
|
+
"""
|
|
310
|
+
if len(points) != len(normals):
|
|
311
|
+
raise ValueError("Points and normals must have the same length")
|
|
312
|
+
|
|
313
|
+
for point, normal in zip(points, normals):
|
|
314
|
+
end = point + normal * scale
|
|
315
|
+
|
|
316
|
+
if arrow:
|
|
317
|
+
self.ax.quiver(
|
|
318
|
+
point.x, point.y, point.z,
|
|
319
|
+
normal.x * scale, normal.y * scale, normal.z * scale,
|
|
320
|
+
color=color,
|
|
321
|
+
linewidth=linewidth,
|
|
322
|
+
alpha=alpha,
|
|
323
|
+
arrow_length_ratio=0.3
|
|
324
|
+
)
|
|
325
|
+
else:
|
|
326
|
+
self.ax.plot(
|
|
327
|
+
[point.x, end.x],
|
|
328
|
+
[point.y, end.y],
|
|
329
|
+
[point.z, end.z],
|
|
330
|
+
color=color,
|
|
331
|
+
linewidth=linewidth,
|
|
332
|
+
alpha=alpha
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def draw_curve_frames(
|
|
336
|
+
self,
|
|
337
|
+
frames: List[coord3],
|
|
338
|
+
scale: float = 0.3,
|
|
339
|
+
linewidth: float = 1.5,
|
|
340
|
+
alpha: float = 0.7,
|
|
341
|
+
skip: int = 1
|
|
342
|
+
):
|
|
343
|
+
"""
|
|
344
|
+
绘制曲线上的坐标系场
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
frames: 坐标系列表
|
|
348
|
+
scale: 坐标轴长度
|
|
349
|
+
linewidth: 线宽
|
|
350
|
+
alpha: 透明度
|
|
351
|
+
skip: 跳过间隔(每skip个绘制一个)
|
|
352
|
+
"""
|
|
353
|
+
for i, frame in enumerate(frames):
|
|
354
|
+
if i % skip == 0:
|
|
355
|
+
self.draw_coord_system(
|
|
356
|
+
frame,
|
|
357
|
+
scale=scale,
|
|
358
|
+
linewidth=linewidth,
|
|
359
|
+
alpha=alpha
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
def draw_complete_curve(
|
|
363
|
+
self,
|
|
364
|
+
points: List[vec3],
|
|
365
|
+
tangents: Optional[List[vec3]] = None,
|
|
366
|
+
normals: Optional[List[vec3]] = None,
|
|
367
|
+
frames: Optional[List[coord3]] = None,
|
|
368
|
+
curve_color: str = 'black',
|
|
369
|
+
tangent_scale: float = 0.5,
|
|
370
|
+
normal_scale: float = 0.5,
|
|
371
|
+
frame_scale: float = 0.3,
|
|
372
|
+
frame_skip: int = 5,
|
|
373
|
+
show_world_coord: bool = True
|
|
374
|
+
):
|
|
375
|
+
"""
|
|
376
|
+
绘制完整的曲线及其几何属性
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
points: 曲线顶点
|
|
380
|
+
tangents: 切线向量(可选)
|
|
381
|
+
normals: 法线向量(可选)
|
|
382
|
+
frames: 坐标系列表(可选)
|
|
383
|
+
curve_color: 曲线颜色
|
|
384
|
+
tangent_scale: 切线长度缩放
|
|
385
|
+
normal_scale: 法线长度缩放
|
|
386
|
+
frame_scale: 坐标系轴长度
|
|
387
|
+
frame_skip: 坐标系绘制间隔
|
|
388
|
+
show_world_coord: 是否显示世界坐标系
|
|
389
|
+
|
|
390
|
+
Note:
|
|
391
|
+
如果提供了 frames (Frenet标架),将使用RGB颜色方案绘制完整标架,
|
|
392
|
+
此时会忽略单独的 tangents 和 normals 参数,避免颜色混淆。
|
|
393
|
+
"""
|
|
394
|
+
# 绘制世界坐标系
|
|
395
|
+
if show_world_coord:
|
|
396
|
+
self.draw_world_coord(scale=1.0)
|
|
397
|
+
|
|
398
|
+
# 绘制曲线
|
|
399
|
+
self.draw_curve_vertices(points, color=curve_color, label="Curve")
|
|
400
|
+
|
|
401
|
+
# 如果提供了完整标架,优先使用RGB标架,不再绘制单独的切线/法线
|
|
402
|
+
if frames is not None:
|
|
403
|
+
# 绘制完整Frenet标架 (红=T, 绿=N, 蓝=B)
|
|
404
|
+
self.draw_curve_frames(frames, scale=frame_scale, skip=frame_skip)
|
|
405
|
+
else:
|
|
406
|
+
# 否则绘制单独的切线和法线 (橙色/紫色)
|
|
407
|
+
if tangents is not None:
|
|
408
|
+
self.draw_tangents(points, tangents, scale=tangent_scale)
|
|
409
|
+
|
|
410
|
+
if normals is not None:
|
|
411
|
+
self.draw_normals(points, normals, scale=normal_scale)
|
|
412
|
+
|
|
413
|
+
# 设置等比例
|
|
414
|
+
self.set_equal_aspect()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class ParametricCurve:
|
|
418
|
+
"""
|
|
419
|
+
参数化曲线类
|
|
420
|
+
|
|
421
|
+
提供曲线的几何属性计算:
|
|
422
|
+
- 位置
|
|
423
|
+
- 切线
|
|
424
|
+
- 法线
|
|
425
|
+
- 副法线
|
|
426
|
+
- Frenet标架
|
|
427
|
+
"""
|
|
428
|
+
|
|
429
|
+
def __init__(
|
|
430
|
+
self,
|
|
431
|
+
position_func: Callable[[float], vec3],
|
|
432
|
+
t_range: Tuple[float, float] = (0, 1),
|
|
433
|
+
num_points: int = 100
|
|
434
|
+
):
|
|
435
|
+
"""
|
|
436
|
+
初始化参数化曲线
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
position_func: 位置函数 r(t) -> vec3
|
|
440
|
+
t_range: 参数范围 (t_min, t_max)
|
|
441
|
+
num_points: 采样点数
|
|
442
|
+
"""
|
|
443
|
+
self.position_func = position_func
|
|
444
|
+
self.t_range = t_range
|
|
445
|
+
self.num_points = num_points
|
|
446
|
+
self.h = 1e-6 # 数值微分步长
|
|
447
|
+
|
|
448
|
+
def position(self, t: float) -> vec3:
|
|
449
|
+
"""计算位置"""
|
|
450
|
+
return self.position_func(t)
|
|
451
|
+
|
|
452
|
+
def tangent(self, t: float, normalized: bool = True) -> vec3:
|
|
453
|
+
"""
|
|
454
|
+
计算切线 dr/dt
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
t: 参数值
|
|
458
|
+
normalized: 是否归一化
|
|
459
|
+
"""
|
|
460
|
+
r_plus = self.position_func(t + self.h)
|
|
461
|
+
r_minus = self.position_func(t - self.h)
|
|
462
|
+
tangent = (r_plus - r_minus) * (1.0 / (2.0 * self.h))
|
|
463
|
+
|
|
464
|
+
if normalized:
|
|
465
|
+
length = (tangent.x**2 + tangent.y**2 + tangent.z**2) ** 0.5
|
|
466
|
+
if length > 1e-10:
|
|
467
|
+
tangent = tangent * (1.0 / length)
|
|
468
|
+
|
|
469
|
+
return tangent
|
|
470
|
+
|
|
471
|
+
def second_derivative(self, t: float) -> vec3:
|
|
472
|
+
"""计算二阶导数 d²r/dt²"""
|
|
473
|
+
r_plus = self.position_func(t + self.h)
|
|
474
|
+
r_center = self.position_func(t)
|
|
475
|
+
r_minus = self.position_func(t - self.h)
|
|
476
|
+
|
|
477
|
+
d2r = (r_plus + r_minus - r_center * 2.0) * (1.0 / (self.h * self.h))
|
|
478
|
+
return d2r
|
|
479
|
+
|
|
480
|
+
def normal(self, t: float, normalized: bool = True) -> vec3:
|
|
481
|
+
"""
|
|
482
|
+
计算主法线(指向曲率中心)
|
|
483
|
+
|
|
484
|
+
使用Frenet-Serret公式:
|
|
485
|
+
N = (dT/dt) / |dT/dt|
|
|
486
|
+
其中 T = (dr/dt) / |dr/dt|
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
t: 参数值
|
|
490
|
+
normalized: 是否归一化
|
|
491
|
+
"""
|
|
492
|
+
# 计算 T(t+h) 和 T(t-h)
|
|
493
|
+
T_plus = self.tangent(t + self.h, normalized=True)
|
|
494
|
+
T_minus = self.tangent(t - self.h, normalized=True)
|
|
495
|
+
|
|
496
|
+
# 数值微分: dT/dt ≈ (T(t+h) - T(t-h)) / (2h)
|
|
497
|
+
dT_dt = (T_plus - T_minus) * (1.0 / (2.0 * self.h))
|
|
498
|
+
|
|
499
|
+
# 主法线是 dT/dt 的归一化
|
|
500
|
+
length = (dT_dt.x**2 + dT_dt.y**2 + dT_dt.z**2) ** 0.5
|
|
501
|
+
|
|
502
|
+
if length > 1e-10:
|
|
503
|
+
N = dT_dt * (1.0 / length) if normalized else dT_dt
|
|
504
|
+
else:
|
|
505
|
+
# 如果长度太小,返回一个默认值
|
|
506
|
+
N = vec3(0, 0, 1)
|
|
507
|
+
|
|
508
|
+
return N
|
|
509
|
+
|
|
510
|
+
def binormal(self, t: float, normalized: bool = True) -> vec3:
|
|
511
|
+
"""
|
|
512
|
+
计算副法线 B = T × N
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
t: 参数值
|
|
516
|
+
normalized: 是否归一化
|
|
517
|
+
"""
|
|
518
|
+
T = self.tangent(t, normalized=True)
|
|
519
|
+
N = self.normal(t, normalized=True)
|
|
520
|
+
B = T.cross(N)
|
|
521
|
+
|
|
522
|
+
if normalized:
|
|
523
|
+
length = (B.x**2 + B.y**2 + B.z**2) ** 0.5
|
|
524
|
+
if length > 1e-10:
|
|
525
|
+
B = B * (1.0 / length)
|
|
526
|
+
|
|
527
|
+
return B
|
|
528
|
+
|
|
529
|
+
def frenet_frame(self, t: float) -> coord3:
|
|
530
|
+
"""
|
|
531
|
+
计算Frenet标架 {T, N, B}
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
t: 参数值
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
coord3对象,其中:
|
|
538
|
+
- o: 位置
|
|
539
|
+
- ux: 切线 T (绘制时为红色)
|
|
540
|
+
- uy: 主法线 N (绘制时为绿色)
|
|
541
|
+
- uz: 副法线 B (绘制时为蓝色)
|
|
542
|
+
|
|
543
|
+
颜色映射:
|
|
544
|
+
- 切线 T = X轴 = 红色
|
|
545
|
+
- 主法线 N = Y轴 = 绿色
|
|
546
|
+
- 副法线 B = Z轴 = 蓝色
|
|
547
|
+
"""
|
|
548
|
+
frame = coord3()
|
|
549
|
+
frame.o = self.position(t)
|
|
550
|
+
frame.ux = self.tangent(t, normalized=True) # T → 红色
|
|
551
|
+
frame.uy = self.normal(t, normalized=True) # N → 绿色
|
|
552
|
+
frame.uz = self.binormal(t, normalized=True) # B → 蓝色
|
|
553
|
+
|
|
554
|
+
return frame
|
|
555
|
+
|
|
556
|
+
def sample_points(self) -> List[vec3]:
|
|
557
|
+
"""采样曲线点"""
|
|
558
|
+
t_min, t_max = self.t_range
|
|
559
|
+
t_values = np.linspace(t_min, t_max, self.num_points)
|
|
560
|
+
return [self.position(t) for t in t_values]
|
|
561
|
+
|
|
562
|
+
def sample_tangents(self) -> List[vec3]:
|
|
563
|
+
"""采样切线"""
|
|
564
|
+
t_min, t_max = self.t_range
|
|
565
|
+
t_values = np.linspace(t_min, t_max, self.num_points)
|
|
566
|
+
return [self.tangent(t) for t in t_values]
|
|
567
|
+
|
|
568
|
+
def sample_normals(self) -> List[vec3]:
|
|
569
|
+
"""采样主法线"""
|
|
570
|
+
t_min, t_max = self.t_range
|
|
571
|
+
t_values = np.linspace(t_min, t_max, self.num_points)
|
|
572
|
+
return [self.normal(t) for t in t_values]
|
|
573
|
+
|
|
574
|
+
def sample_frames(self) -> List[coord3]:
|
|
575
|
+
"""采样Frenet标架"""
|
|
576
|
+
t_min, t_max = self.t_range
|
|
577
|
+
t_values = np.linspace(t_min, t_max, self.num_points)
|
|
578
|
+
return [self.frenet_frame(t) for t in t_values]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# ========== 便捷函数 ==========
|
|
582
|
+
|
|
583
|
+
def visualize_coord_system(
|
|
584
|
+
coord: coord3,
|
|
585
|
+
scale: float = 1.0,
|
|
586
|
+
figsize: Tuple[int, int] = (10, 8),
|
|
587
|
+
show: bool = True,
|
|
588
|
+
save_path: Optional[str] = None
|
|
589
|
+
):
|
|
590
|
+
"""
|
|
591
|
+
快速可视化单个坐标系
|
|
592
|
+
|
|
593
|
+
Args:
|
|
594
|
+
coord: 坐标系对象
|
|
595
|
+
scale: 轴长度
|
|
596
|
+
figsize: 图形大小
|
|
597
|
+
show: 是否显示
|
|
598
|
+
save_path: 保存路径(可选)
|
|
599
|
+
"""
|
|
600
|
+
vis = CoordinateSystemVisualizer(figsize=figsize)
|
|
601
|
+
vis.draw_world_coord(scale=scale * 0.8)
|
|
602
|
+
vis.draw_coord_system(coord, scale=scale, label_prefix="Frame-")
|
|
603
|
+
vis.set_equal_aspect()
|
|
604
|
+
|
|
605
|
+
if save_path:
|
|
606
|
+
vis.save(save_path)
|
|
607
|
+
if show:
|
|
608
|
+
vis.show()
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def visualize_curve(
|
|
612
|
+
curve: ParametricCurve,
|
|
613
|
+
show_tangents: bool = True,
|
|
614
|
+
show_normals: bool = True,
|
|
615
|
+
show_frames: bool = False,
|
|
616
|
+
frame_skip: int = 5,
|
|
617
|
+
figsize: Tuple[int, int] = (12, 9),
|
|
618
|
+
show: bool = True,
|
|
619
|
+
save_path: Optional[str] = None
|
|
620
|
+
):
|
|
621
|
+
"""
|
|
622
|
+
快速可视化参数化曲线
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
curve: 参数化曲线对象
|
|
626
|
+
show_tangents: 是否显示切线
|
|
627
|
+
show_normals: 是否显示法线
|
|
628
|
+
show_frames: 是否显示完整坐标系
|
|
629
|
+
frame_skip: 坐标系绘制间隔
|
|
630
|
+
figsize: 图形大小
|
|
631
|
+
show: 是否显示
|
|
632
|
+
save_path: 保存路径(可选)
|
|
633
|
+
"""
|
|
634
|
+
vis = CurveVisualizer(figsize=figsize)
|
|
635
|
+
|
|
636
|
+
points = curve.sample_points()
|
|
637
|
+
tangents = curve.sample_tangents() if show_tangents else None
|
|
638
|
+
normals = curve.sample_normals() if show_normals else None
|
|
639
|
+
frames = curve.sample_frames() if show_frames else None
|
|
640
|
+
|
|
641
|
+
vis.draw_complete_curve(
|
|
642
|
+
points=points,
|
|
643
|
+
tangents=tangents,
|
|
644
|
+
normals=normals,
|
|
645
|
+
frames=frames,
|
|
646
|
+
frame_skip=frame_skip
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
if save_path:
|
|
650
|
+
vis.save(save_path)
|
|
651
|
+
if show:
|
|
652
|
+
vis.show()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ========== Export ==========
|
|
656
|
+
|
|
657
|
+
__all__ = [
|
|
658
|
+
# Classes
|
|
659
|
+
'CoordinateSystemVisualizer',
|
|
660
|
+
'CurveVisualizer',
|
|
661
|
+
'ParametricCurve',
|
|
662
|
+
|
|
663
|
+
# Functions
|
|
664
|
+
'visualize_coord_system',
|
|
665
|
+
'visualize_curve',
|
|
666
|
+
]
|