coordinate-system 7.0.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.
@@ -0,0 +1,1069 @@
1
+ """
2
+ 3D Coordinate System and Surface Visualization Module
3
+ =====================================================
4
+
5
+ Comprehensive visualization tools for coordinate systems, curves, and surfaces.
6
+
7
+ Features:
8
+ - Coordinate frame visualization with RGB color scheme (X=Red, Y=Green, Z=Blue)
9
+ - Parametric curve visualization with Frenet frames
10
+ - Surface rendering with curvature coloring
11
+ - Frame field visualization on surfaces
12
+ - Multiple view angles and animation support
13
+
14
+ Author: Coordinate System Package
15
+ Date: 2025-12-03
16
+ """
17
+
18
+ import numpy as np
19
+ import matplotlib.pyplot as plt
20
+ from mpl_toolkits.mplot3d import Axes3D
21
+ from matplotlib import cm
22
+ from matplotlib.colors import Normalize, LinearSegmentedColormap
23
+ import matplotlib.animation as animation
24
+ from typing import List, Optional, Tuple, Callable, Union, Dict
25
+
26
+ # Import from the C extension module
27
+ try:
28
+ from .coordinate_system import vec3, coord3
29
+ except ImportError:
30
+ import coordinate_system
31
+ vec3 = coordinate_system.vec3
32
+ coord3 = coordinate_system.coord3
33
+
34
+ # Import differential geometry for surface visualization
35
+ try:
36
+ from .differential_geometry import Surface, Sphere, Torus, compute_gaussian_curvature, compute_mean_curvature
37
+ except ImportError:
38
+ Surface = None
39
+ Sphere = None
40
+ Torus = None
41
+ compute_gaussian_curvature = None
42
+ compute_mean_curvature = None
43
+
44
+
45
+ # ============================================================
46
+ # Color Schemes
47
+ # ============================================================
48
+
49
+ # Curvature colormap: blue (negative) -> white (zero) -> red (positive)
50
+ CURVATURE_COLORS = [
51
+ (0.0, 'blue'),
52
+ (0.5, 'white'),
53
+ (1.0, 'red')
54
+ ]
55
+
56
+ def create_curvature_colormap():
57
+ """Create a diverging colormap for curvature visualization."""
58
+ return LinearSegmentedColormap.from_list('curvature',
59
+ [(0.0, 'blue'), (0.5, 'white'), (1.0, 'red')])
60
+
61
+
62
+ # ============================================================
63
+ # Coordinate System Visualizer
64
+ # ============================================================
65
+
66
+ class CoordinateSystemVisualizer:
67
+ """
68
+ 3D coordinate system visualization tool.
69
+
70
+ RGB color scheme:
71
+ - X axis: Red
72
+ - Y axis: Green
73
+ - Z axis: Blue
74
+ """
75
+
76
+ def __init__(self, figsize: Tuple[int, int] = (12, 9), dpi: int = 100):
77
+ """
78
+ Initialize visualizer.
79
+
80
+ Args:
81
+ figsize: Figure size (width, height) in inches
82
+ dpi: Dots per inch for rendering
83
+ """
84
+ self.fig = plt.figure(figsize=figsize, dpi=dpi)
85
+ self.ax = self.fig.add_subplot(111, projection='3d')
86
+ self._setup_axis()
87
+
88
+ def _setup_axis(self):
89
+ """Configure axis style."""
90
+ self.ax.set_xlabel('X', fontsize=12, color='red', fontweight='bold')
91
+ self.ax.set_ylabel('Y', fontsize=12, color='green', fontweight='bold')
92
+ self.ax.set_zlabel('Z', fontsize=12, color='blue', fontweight='bold')
93
+ self.ax.grid(True, alpha=0.3, linestyle='--')
94
+ self.ax.xaxis.pane.fill = False
95
+ self.ax.yaxis.pane.fill = False
96
+ self.ax.zaxis.pane.fill = False
97
+
98
+ def draw_coord_system(
99
+ self,
100
+ coord: coord3,
101
+ scale: float = 1.0,
102
+ linewidth: float = 2.0,
103
+ alpha: float = 0.8,
104
+ label_prefix: str = "",
105
+ arrow_style: bool = True
106
+ ):
107
+ """
108
+ Draw a single coordinate frame.
109
+
110
+ Args:
111
+ coord: coord3 object
112
+ scale: Axis length scale factor
113
+ linewidth: Line width
114
+ alpha: Transparency
115
+ label_prefix: Label prefix for legend
116
+ arrow_style: Use arrow heads if True
117
+ """
118
+ origin = coord.o
119
+
120
+ if arrow_style:
121
+ # Use quiver for arrows
122
+ self.ax.quiver(
123
+ origin.x, origin.y, origin.z,
124
+ coord.ux.x * scale, coord.ux.y * scale, coord.ux.z * scale,
125
+ color='red', linewidth=linewidth, alpha=alpha,
126
+ arrow_length_ratio=0.15
127
+ )
128
+ self.ax.quiver(
129
+ origin.x, origin.y, origin.z,
130
+ coord.uy.x * scale, coord.uy.y * scale, coord.uy.z * scale,
131
+ color='green', linewidth=linewidth, alpha=alpha,
132
+ arrow_length_ratio=0.15
133
+ )
134
+ self.ax.quiver(
135
+ origin.x, origin.y, origin.z,
136
+ coord.uz.x * scale, coord.uz.y * scale, coord.uz.z * scale,
137
+ color='blue', linewidth=linewidth, alpha=alpha,
138
+ arrow_length_ratio=0.15
139
+ )
140
+ else:
141
+ # Use simple lines
142
+ x_end = origin + coord.ux * scale
143
+ y_end = origin + coord.uy * scale
144
+ z_end = origin + coord.uz * scale
145
+
146
+ self.ax.plot([origin.x, x_end.x], [origin.y, x_end.y], [origin.z, x_end.z],
147
+ 'r-', linewidth=linewidth, alpha=alpha)
148
+ self.ax.plot([origin.x, y_end.x], [origin.y, y_end.y], [origin.z, y_end.z],
149
+ 'g-', linewidth=linewidth, alpha=alpha)
150
+ self.ax.plot([origin.x, z_end.x], [origin.y, z_end.y], [origin.z, z_end.z],
151
+ 'b-', linewidth=linewidth, alpha=alpha)
152
+
153
+ def draw_world_coord(
154
+ self,
155
+ origin: vec3 = None,
156
+ scale: float = 1.0,
157
+ linewidth: float = 3.0
158
+ ):
159
+ """
160
+ Draw world coordinate system.
161
+
162
+ Args:
163
+ origin: Origin position, default is (0, 0, 0)
164
+ scale: Axis length
165
+ linewidth: Line width
166
+ """
167
+ if origin is None:
168
+ origin = vec3(0, 0, 0)
169
+
170
+ world_coord = coord3()
171
+ world_coord.o = origin
172
+
173
+ self.draw_coord_system(
174
+ world_coord,
175
+ scale=scale,
176
+ linewidth=linewidth,
177
+ alpha=1.0,
178
+ label_prefix="World-"
179
+ )
180
+
181
+ def draw_point(
182
+ self,
183
+ point: vec3,
184
+ color: str = 'black',
185
+ size: float = 50,
186
+ marker: str = 'o',
187
+ label: str = None
188
+ ):
189
+ """
190
+ Draw a single point.
191
+
192
+ Args:
193
+ point: Point position
194
+ color: Point color
195
+ size: Marker size
196
+ marker: Marker style
197
+ label: Label for legend
198
+ """
199
+ self.ax.scatter([point.x], [point.y], [point.z],
200
+ c=color, s=size, marker=marker, label=label)
201
+
202
+ def draw_vector(
203
+ self,
204
+ start: vec3,
205
+ direction: vec3,
206
+ color: str = 'black',
207
+ linewidth: float = 2.0,
208
+ alpha: float = 0.8,
209
+ label: str = None
210
+ ):
211
+ """
212
+ Draw a vector as an arrow.
213
+
214
+ Args:
215
+ start: Starting point
216
+ direction: Direction vector
217
+ color: Arrow color
218
+ linewidth: Line width
219
+ alpha: Transparency
220
+ label: Label for legend
221
+ """
222
+ self.ax.quiver(
223
+ start.x, start.y, start.z,
224
+ direction.x, direction.y, direction.z,
225
+ color=color, linewidth=linewidth, alpha=alpha,
226
+ arrow_length_ratio=0.15, label=label
227
+ )
228
+
229
+ def set_equal_aspect(self):
230
+ """Set equal aspect ratio for all axes."""
231
+ xlim = self.ax.get_xlim3d()
232
+ ylim = self.ax.get_ylim3d()
233
+ zlim = self.ax.get_zlim3d()
234
+
235
+ x_range = abs(xlim[1] - xlim[0])
236
+ y_range = abs(ylim[1] - ylim[0])
237
+ z_range = abs(zlim[1] - zlim[0])
238
+
239
+ max_range = max(x_range, y_range, z_range)
240
+
241
+ x_middle = np.mean(xlim)
242
+ y_middle = np.mean(ylim)
243
+ z_middle = np.mean(zlim)
244
+
245
+ self.ax.set_xlim3d([x_middle - max_range/2, x_middle + max_range/2])
246
+ self.ax.set_ylim3d([y_middle - max_range/2, y_middle + max_range/2])
247
+ self.ax.set_zlim3d([z_middle - max_range/2, z_middle + max_range/2])
248
+
249
+ def set_view(self, elev: float = 30, azim: float = 45):
250
+ """
251
+ Set camera view angle.
252
+
253
+ Args:
254
+ elev: Elevation angle in degrees
255
+ azim: Azimuth angle in degrees
256
+ """
257
+ self.ax.view_init(elev=elev, azim=azim)
258
+
259
+ def set_title(self, title: str, fontsize: int = 14):
260
+ """Set plot title."""
261
+ self.ax.set_title(title, fontsize=fontsize, fontweight='bold')
262
+
263
+ def show(self):
264
+ """Display the figure."""
265
+ self.ax.legend(loc='upper left')
266
+ plt.tight_layout()
267
+ plt.show()
268
+
269
+ def save(self, filename: str, dpi: int = 300):
270
+ """
271
+ Save figure to file.
272
+
273
+ Args:
274
+ filename: Output filename
275
+ dpi: Resolution
276
+ """
277
+ self.ax.legend(loc='upper left')
278
+ plt.tight_layout()
279
+ plt.savefig(filename, dpi=dpi, bbox_inches='tight')
280
+ print(f"Saved: {filename}")
281
+
282
+
283
+ # ============================================================
284
+ # Surface Visualizer
285
+ # ============================================================
286
+
287
+ class SurfaceVisualizer(CoordinateSystemVisualizer):
288
+ """
289
+ Surface visualization with curvature coloring.
290
+
291
+ Supports:
292
+ - Wireframe and surface rendering
293
+ - Gaussian/Mean curvature coloring
294
+ - Frame field overlay
295
+ - Normal vector visualization
296
+ """
297
+
298
+ def __init__(self, figsize: Tuple[int, int] = (14, 10), dpi: int = 100):
299
+ super().__init__(figsize, dpi)
300
+ self.colorbar = None
301
+
302
+ def draw_surface(
303
+ self,
304
+ surface: 'Surface',
305
+ u_range: Tuple[float, float] = (0.1, np.pi - 0.1),
306
+ v_range: Tuple[float, float] = (0, 2 * np.pi),
307
+ nu: int = 30,
308
+ nv: int = 40,
309
+ color: str = 'cyan',
310
+ alpha: float = 0.6,
311
+ wireframe: bool = True,
312
+ surface_plot: bool = True
313
+ ):
314
+ """
315
+ Draw a parametric surface.
316
+
317
+ Args:
318
+ surface: Surface object
319
+ u_range: Parameter u range
320
+ v_range: Parameter v range
321
+ nu: Number of u samples
322
+ nv: Number of v samples
323
+ color: Surface color
324
+ alpha: Transparency
325
+ wireframe: Show wireframe
326
+ surface_plot: Show filled surface
327
+ """
328
+ u = np.linspace(u_range[0], u_range[1], nu)
329
+ v = np.linspace(v_range[0], v_range[1], nv)
330
+ U, V = np.meshgrid(u, v)
331
+
332
+ X = np.zeros_like(U)
333
+ Y = np.zeros_like(U)
334
+ Z = np.zeros_like(U)
335
+
336
+ for i in range(nu):
337
+ for j in range(nv):
338
+ pos = surface.position(U[j, i], V[j, i])
339
+ X[j, i] = pos.x
340
+ Y[j, i] = pos.y
341
+ Z[j, i] = pos.z
342
+
343
+ if surface_plot:
344
+ self.ax.plot_surface(X, Y, Z, color=color, alpha=alpha,
345
+ edgecolor='none', shade=True)
346
+
347
+ if wireframe:
348
+ self.ax.plot_wireframe(X, Y, Z, color='gray', alpha=0.3,
349
+ linewidth=0.5, rstride=2, cstride=2)
350
+
351
+ def draw_surface_curvature(
352
+ self,
353
+ surface: 'Surface',
354
+ curvature_type: str = 'gaussian',
355
+ u_range: Tuple[float, float] = (0.1, np.pi - 0.1),
356
+ v_range: Tuple[float, float] = (0, 2 * np.pi),
357
+ nu: int = 30,
358
+ nv: int = 40,
359
+ alpha: float = 0.8,
360
+ show_colorbar: bool = True,
361
+ step_size: float = 1e-3
362
+ ):
363
+ """
364
+ Draw surface with curvature coloring.
365
+
366
+ Args:
367
+ surface: Surface object
368
+ curvature_type: 'gaussian' or 'mean'
369
+ u_range: Parameter u range
370
+ v_range: Parameter v range
371
+ nu: Number of u samples
372
+ nv: Number of v samples
373
+ alpha: Transparency
374
+ show_colorbar: Show colorbar
375
+ step_size: Step size for curvature computation
376
+ """
377
+ if compute_gaussian_curvature is None:
378
+ raise ImportError("differential_geometry module not available")
379
+
380
+ u = np.linspace(u_range[0], u_range[1], nu)
381
+ v = np.linspace(v_range[0], v_range[1], nv)
382
+ U, V = np.meshgrid(u, v)
383
+
384
+ X = np.zeros_like(U)
385
+ Y = np.zeros_like(U)
386
+ Z = np.zeros_like(U)
387
+ K = np.zeros_like(U)
388
+
389
+ compute_func = compute_gaussian_curvature if curvature_type == 'gaussian' else compute_mean_curvature
390
+
391
+ for i in range(nu):
392
+ for j in range(nv):
393
+ pos = surface.position(U[j, i], V[j, i])
394
+ X[j, i] = pos.x
395
+ Y[j, i] = pos.y
396
+ Z[j, i] = pos.z
397
+ K[j, i] = compute_func(surface, U[j, i], V[j, i], step_size)
398
+
399
+ # Normalize curvature for coloring
400
+ K_abs_max = max(abs(K.min()), abs(K.max()))
401
+ if K_abs_max > 1e-10:
402
+ K_normalized = (K / K_abs_max + 1) / 2 # Map to [0, 1]
403
+ else:
404
+ K_normalized = np.ones_like(K) * 0.5
405
+
406
+ # Create colormap
407
+ cmap = create_curvature_colormap()
408
+ colors = cmap(K_normalized)
409
+
410
+ # Draw surface
411
+ surf = self.ax.plot_surface(X, Y, Z, facecolors=colors, alpha=alpha,
412
+ shade=True, linewidth=0, antialiased=True)
413
+
414
+ if show_colorbar:
415
+ norm = Normalize(vmin=-K_abs_max, vmax=K_abs_max)
416
+ sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
417
+ sm.set_array([])
418
+ self.colorbar = self.fig.colorbar(sm, ax=self.ax, shrink=0.6, aspect=20,
419
+ label=f'{curvature_type.capitalize()} Curvature')
420
+
421
+ def draw_surface_normals(
422
+ self,
423
+ surface: 'Surface',
424
+ u_range: Tuple[float, float] = (0.1, np.pi - 0.1),
425
+ v_range: Tuple[float, float] = (0, 2 * np.pi),
426
+ nu: int = 8,
427
+ nv: int = 12,
428
+ scale: float = 0.3,
429
+ color: str = 'blue',
430
+ alpha: float = 0.8
431
+ ):
432
+ """
433
+ Draw surface normal vectors.
434
+
435
+ Args:
436
+ surface: Surface object
437
+ u_range: Parameter u range
438
+ v_range: Parameter v range
439
+ nu: Number of u samples
440
+ nv: Number of v samples
441
+ scale: Normal vector length
442
+ color: Vector color
443
+ alpha: Transparency
444
+ """
445
+ u = np.linspace(u_range[0], u_range[1], nu)
446
+ v = np.linspace(v_range[0], v_range[1], nv)
447
+
448
+ for ui in u:
449
+ for vi in v:
450
+ pos = surface.position(ui, vi)
451
+ n = surface.normal(ui, vi)
452
+
453
+ self.ax.quiver(
454
+ pos.x, pos.y, pos.z,
455
+ n.x * scale, n.y * scale, n.z * scale,
456
+ color=color, alpha=alpha, arrow_length_ratio=0.2
457
+ )
458
+
459
+ def draw_surface_frames(
460
+ self,
461
+ surface: 'Surface',
462
+ u_range: Tuple[float, float] = (0.1, np.pi - 0.1),
463
+ v_range: Tuple[float, float] = (0, 2 * np.pi),
464
+ nu: int = 6,
465
+ nv: int = 8,
466
+ scale: float = 0.25,
467
+ linewidth: float = 1.5,
468
+ alpha: float = 0.8
469
+ ):
470
+ """
471
+ Draw frame field on surface.
472
+
473
+ Args:
474
+ surface: Surface object
475
+ u_range: Parameter u range
476
+ v_range: Parameter v range
477
+ nu: Number of u samples
478
+ nv: Number of v samples
479
+ scale: Frame axis length
480
+ linewidth: Line width
481
+ alpha: Transparency
482
+ """
483
+ u = np.linspace(u_range[0], u_range[1], nu)
484
+ v = np.linspace(v_range[0], v_range[1], nv)
485
+
486
+ for ui in u:
487
+ for vi in v:
488
+ pos = surface.position(ui, vi)
489
+ r_u = surface.tangent_u(ui, vi)
490
+ r_v = surface.tangent_v(ui, vi)
491
+ n = surface.normal(ui, vi)
492
+
493
+ # Normalize
494
+ r_u_norm = (r_u.x**2 + r_u.y**2 + r_u.z**2) ** 0.5
495
+ r_v_norm = (r_v.x**2 + r_v.y**2 + r_v.z**2) ** 0.5
496
+
497
+ if r_u_norm > 1e-10:
498
+ r_u = r_u * (1.0 / r_u_norm)
499
+ if r_v_norm > 1e-10:
500
+ r_v = r_v * (1.0 / r_v_norm)
501
+
502
+ # Draw frame (red=u, green=v, blue=normal)
503
+ self.ax.quiver(pos.x, pos.y, pos.z,
504
+ r_u.x * scale, r_u.y * scale, r_u.z * scale,
505
+ color='red', linewidth=linewidth, alpha=alpha,
506
+ arrow_length_ratio=0.15)
507
+ self.ax.quiver(pos.x, pos.y, pos.z,
508
+ r_v.x * scale, r_v.y * scale, r_v.z * scale,
509
+ color='green', linewidth=linewidth, alpha=alpha,
510
+ arrow_length_ratio=0.15)
511
+ self.ax.quiver(pos.x, pos.y, pos.z,
512
+ n.x * scale, n.y * scale, n.z * scale,
513
+ color='blue', linewidth=linewidth, alpha=alpha,
514
+ arrow_length_ratio=0.15)
515
+
516
+
517
+ # ============================================================
518
+ # Curve Visualizer
519
+ # ============================================================
520
+
521
+ class CurveVisualizer(CoordinateSystemVisualizer):
522
+ """
523
+ Curve visualization with Frenet frame support.
524
+
525
+ Features:
526
+ - Curve path rendering
527
+ - Tangent, normal, binormal vectors
528
+ - Frenet frame field
529
+ - Animation support
530
+ """
531
+
532
+ def __init__(self, figsize: Tuple[int, int] = (12, 9), dpi: int = 100):
533
+ super().__init__(figsize, dpi)
534
+
535
+ def draw_curve_vertices(
536
+ self,
537
+ points: List[vec3],
538
+ color: str = 'black',
539
+ linewidth: float = 2.0,
540
+ marker: str = '',
541
+ markersize: float = 4,
542
+ alpha: float = 0.9,
543
+ label: str = "Curve"
544
+ ):
545
+ """
546
+ Draw curve through vertices.
547
+
548
+ Args:
549
+ points: Vertex list
550
+ color: Line color
551
+ linewidth: Line width
552
+ marker: Marker style (empty for no markers)
553
+ markersize: Marker size
554
+ alpha: Transparency
555
+ label: Legend label
556
+ """
557
+ if not points:
558
+ return
559
+
560
+ x = [p.x for p in points]
561
+ y = [p.y for p in points]
562
+ z = [p.z for p in points]
563
+
564
+ self.ax.plot(x, y, z, color=color, linewidth=linewidth,
565
+ marker=marker, markersize=markersize,
566
+ alpha=alpha, label=label)
567
+
568
+ def draw_tangents(
569
+ self,
570
+ points: List[vec3],
571
+ tangents: List[vec3],
572
+ scale: float = 0.5,
573
+ color: str = 'red',
574
+ linewidth: float = 1.5,
575
+ alpha: float = 0.8
576
+ ):
577
+ """
578
+ Draw tangent vectors.
579
+
580
+ Args:
581
+ points: Curve points
582
+ tangents: Tangent vectors
583
+ scale: Vector length scale
584
+ color: Vector color
585
+ linewidth: Line width
586
+ alpha: Transparency
587
+ """
588
+ for point, tangent in zip(points, tangents):
589
+ self.ax.quiver(
590
+ point.x, point.y, point.z,
591
+ tangent.x * scale, tangent.y * scale, tangent.z * scale,
592
+ color=color, linewidth=linewidth, alpha=alpha,
593
+ arrow_length_ratio=0.2
594
+ )
595
+
596
+ def draw_normals(
597
+ self,
598
+ points: List[vec3],
599
+ normals: List[vec3],
600
+ scale: float = 0.5,
601
+ color: str = 'green',
602
+ linewidth: float = 1.5,
603
+ alpha: float = 0.8
604
+ ):
605
+ """
606
+ Draw normal vectors.
607
+
608
+ Args:
609
+ points: Curve points
610
+ normals: Normal vectors
611
+ scale: Vector length scale
612
+ color: Vector color
613
+ linewidth: Line width
614
+ alpha: Transparency
615
+ """
616
+ for point, normal in zip(points, normals):
617
+ self.ax.quiver(
618
+ point.x, point.y, point.z,
619
+ normal.x * scale, normal.y * scale, normal.z * scale,
620
+ color=color, linewidth=linewidth, alpha=alpha,
621
+ arrow_length_ratio=0.2
622
+ )
623
+
624
+ def draw_binormals(
625
+ self,
626
+ points: List[vec3],
627
+ binormals: List[vec3],
628
+ scale: float = 0.5,
629
+ color: str = 'blue',
630
+ linewidth: float = 1.5,
631
+ alpha: float = 0.8
632
+ ):
633
+ """
634
+ Draw binormal vectors.
635
+
636
+ Args:
637
+ points: Curve points
638
+ binormals: Binormal vectors
639
+ scale: Vector length scale
640
+ color: Vector color
641
+ linewidth: Line width
642
+ alpha: Transparency
643
+ """
644
+ for point, binormal in zip(points, binormals):
645
+ self.ax.quiver(
646
+ point.x, point.y, point.z,
647
+ binormal.x * scale, binormal.y * scale, binormal.z * scale,
648
+ color=color, linewidth=linewidth, alpha=alpha,
649
+ arrow_length_ratio=0.2
650
+ )
651
+
652
+ def draw_curve_frames(
653
+ self,
654
+ frames: List[coord3],
655
+ scale: float = 0.3,
656
+ linewidth: float = 1.5,
657
+ alpha: float = 0.7,
658
+ skip: int = 1
659
+ ):
660
+ """
661
+ Draw Frenet frames along curve.
662
+
663
+ Args:
664
+ frames: List of coord3 frames
665
+ scale: Axis length
666
+ linewidth: Line width
667
+ alpha: Transparency
668
+ skip: Draw every nth frame
669
+ """
670
+ for i, frame in enumerate(frames):
671
+ if i % skip == 0:
672
+ self.draw_coord_system(frame, scale=scale,
673
+ linewidth=linewidth, alpha=alpha)
674
+
675
+ def draw_complete_curve(
676
+ self,
677
+ points: List[vec3],
678
+ tangents: Optional[List[vec3]] = None,
679
+ normals: Optional[List[vec3]] = None,
680
+ binormals: Optional[List[vec3]] = None,
681
+ frames: Optional[List[coord3]] = None,
682
+ curve_color: str = 'black',
683
+ tangent_scale: float = 0.5,
684
+ normal_scale: float = 0.5,
685
+ binormal_scale: float = 0.5,
686
+ frame_scale: float = 0.3,
687
+ frame_skip: int = 5,
688
+ show_world_coord: bool = True
689
+ ):
690
+ """
691
+ Draw complete curve with geometric properties.
692
+
693
+ Args:
694
+ points: Curve vertices
695
+ tangents: Tangent vectors (optional)
696
+ normals: Normal vectors (optional)
697
+ binormals: Binormal vectors (optional)
698
+ frames: Frenet frames (optional, overrides individual vectors)
699
+ curve_color: Curve color
700
+ tangent_scale: Tangent vector scale
701
+ normal_scale: Normal vector scale
702
+ binormal_scale: Binormal vector scale
703
+ frame_scale: Frame axis length
704
+ frame_skip: Frame drawing interval
705
+ show_world_coord: Show world coordinate system
706
+ """
707
+ if show_world_coord:
708
+ self.draw_world_coord(scale=1.0)
709
+
710
+ self.draw_curve_vertices(points, color=curve_color, label="Curve")
711
+
712
+ if frames is not None:
713
+ # Use complete Frenet frames (RGB coloring)
714
+ self.draw_curve_frames(frames, scale=frame_scale, skip=frame_skip)
715
+ else:
716
+ # Draw individual vectors
717
+ if tangents is not None:
718
+ self.draw_tangents(points, tangents, scale=tangent_scale)
719
+ if normals is not None:
720
+ self.draw_normals(points, normals, scale=normal_scale)
721
+ if binormals is not None:
722
+ self.draw_binormals(points, binormals, scale=binormal_scale)
723
+
724
+ self.set_equal_aspect()
725
+
726
+
727
+ # ============================================================
728
+ # Parametric Curve
729
+ # ============================================================
730
+
731
+ class ParametricCurve:
732
+ """
733
+ Parametric curve with Frenet frame computation.
734
+
735
+ Provides:
736
+ - Position r(t)
737
+ - Tangent T(t)
738
+ - Normal N(t)
739
+ - Binormal B(t)
740
+ - Frenet frame {T, N, B}
741
+ - Curvature and torsion
742
+ """
743
+
744
+ def __init__(
745
+ self,
746
+ position_func: Callable[[float], vec3],
747
+ t_range: Tuple[float, float] = (0, 1),
748
+ num_points: int = 100
749
+ ):
750
+ """
751
+ Initialize parametric curve.
752
+
753
+ Args:
754
+ position_func: Position function r(t) -> vec3
755
+ t_range: Parameter range (t_min, t_max)
756
+ num_points: Number of sample points
757
+ """
758
+ self.position_func = position_func
759
+ self.t_range = t_range
760
+ self.num_points = num_points
761
+ self.h = 1e-6 # Numerical differentiation step
762
+
763
+ def position(self, t: float) -> vec3:
764
+ """Compute position at parameter t."""
765
+ return self.position_func(t)
766
+
767
+ def tangent(self, t: float, normalized: bool = True) -> vec3:
768
+ """
769
+ Compute tangent vector T = dr/dt.
770
+
771
+ Args:
772
+ t: Parameter value
773
+ normalized: Normalize to unit vector
774
+ """
775
+ r_plus = self.position_func(t + self.h)
776
+ r_minus = self.position_func(t - self.h)
777
+ tangent = (r_plus - r_minus) * (1.0 / (2.0 * self.h))
778
+
779
+ if normalized:
780
+ length = (tangent.x**2 + tangent.y**2 + tangent.z**2) ** 0.5
781
+ if length > 1e-10:
782
+ tangent = tangent * (1.0 / length)
783
+
784
+ return tangent
785
+
786
+ def second_derivative(self, t: float) -> vec3:
787
+ """Compute second derivative d^2r/dt^2."""
788
+ r_plus = self.position_func(t + self.h)
789
+ r_center = self.position_func(t)
790
+ r_minus = self.position_func(t - self.h)
791
+ return (r_plus + r_minus - r_center * 2.0) * (1.0 / (self.h * self.h))
792
+
793
+ def normal(self, t: float, normalized: bool = True) -> vec3:
794
+ """
795
+ Compute principal normal N = dT/ds / |dT/ds|.
796
+
797
+ Args:
798
+ t: Parameter value
799
+ normalized: Normalize to unit vector
800
+ """
801
+ T_plus = self.tangent(t + self.h, normalized=True)
802
+ T_minus = self.tangent(t - self.h, normalized=True)
803
+ dT_dt = (T_plus - T_minus) * (1.0 / (2.0 * self.h))
804
+
805
+ length = (dT_dt.x**2 + dT_dt.y**2 + dT_dt.z**2) ** 0.5
806
+
807
+ if length > 1e-10:
808
+ N = dT_dt * (1.0 / length) if normalized else dT_dt
809
+ else:
810
+ N = vec3(0, 0, 1)
811
+
812
+ return N
813
+
814
+ def binormal(self, t: float, normalized: bool = True) -> vec3:
815
+ """
816
+ Compute binormal B = T x N.
817
+
818
+ Args:
819
+ t: Parameter value
820
+ normalized: Normalize to unit vector
821
+ """
822
+ T = self.tangent(t, normalized=True)
823
+ N = self.normal(t, normalized=True)
824
+ B = T.cross(N)
825
+
826
+ if normalized:
827
+ length = (B.x**2 + B.y**2 + B.z**2) ** 0.5
828
+ if length > 1e-10:
829
+ B = B * (1.0 / length)
830
+
831
+ return B
832
+
833
+ def curvature(self, t: float) -> float:
834
+ """
835
+ Compute curvature kappa = |dT/ds|.
836
+
837
+ Args:
838
+ t: Parameter value
839
+
840
+ Returns:
841
+ Curvature value
842
+ """
843
+ T_plus = self.tangent(t + self.h, normalized=True)
844
+ T_minus = self.tangent(t - self.h, normalized=True)
845
+ dT_dt = (T_plus - T_minus) * (1.0 / (2.0 * self.h))
846
+
847
+ # Get speed |dr/dt|
848
+ dr_dt = (self.position_func(t + self.h) - self.position_func(t - self.h)) * (1.0 / (2.0 * self.h))
849
+ speed = (dr_dt.x**2 + dr_dt.y**2 + dr_dt.z**2) ** 0.5
850
+
851
+ if speed > 1e-10:
852
+ kappa = (dT_dt.x**2 + dT_dt.y**2 + dT_dt.z**2) ** 0.5 / speed
853
+ else:
854
+ kappa = 0.0
855
+
856
+ return kappa
857
+
858
+ def frenet_frame(self, t: float) -> coord3:
859
+ """
860
+ Compute Frenet frame {T, N, B}.
861
+
862
+ Returns coord3 with:
863
+ - o: Position
864
+ - ux: Tangent T (red)
865
+ - uy: Normal N (green)
866
+ - uz: Binormal B (blue)
867
+ """
868
+ frame = coord3()
869
+ frame.o = self.position(t)
870
+ frame.ux = self.tangent(t, normalized=True)
871
+ frame.uy = self.normal(t, normalized=True)
872
+ frame.uz = self.binormal(t, normalized=True)
873
+ return frame
874
+
875
+ def sample_points(self) -> List[vec3]:
876
+ """Sample curve positions."""
877
+ t_min, t_max = self.t_range
878
+ t_values = np.linspace(t_min, t_max, self.num_points)
879
+ return [self.position(t) for t in t_values]
880
+
881
+ def sample_tangents(self) -> List[vec3]:
882
+ """Sample tangent vectors."""
883
+ t_min, t_max = self.t_range
884
+ t_values = np.linspace(t_min, t_max, self.num_points)
885
+ return [self.tangent(t) for t in t_values]
886
+
887
+ def sample_normals(self) -> List[vec3]:
888
+ """Sample normal vectors."""
889
+ t_min, t_max = self.t_range
890
+ t_values = np.linspace(t_min, t_max, self.num_points)
891
+ return [self.normal(t) for t in t_values]
892
+
893
+ def sample_binormals(self) -> List[vec3]:
894
+ """Sample binormal vectors."""
895
+ t_min, t_max = self.t_range
896
+ t_values = np.linspace(t_min, t_max, self.num_points)
897
+ return [self.binormal(t) for t in t_values]
898
+
899
+ def sample_frames(self) -> List[coord3]:
900
+ """Sample Frenet frames."""
901
+ t_min, t_max = self.t_range
902
+ t_values = np.linspace(t_min, t_max, self.num_points)
903
+ return [self.frenet_frame(t) for t in t_values]
904
+
905
+ def sample_curvature(self) -> List[float]:
906
+ """Sample curvature values."""
907
+ t_min, t_max = self.t_range
908
+ t_values = np.linspace(t_min, t_max, self.num_points)
909
+ return [self.curvature(t) for t in t_values]
910
+
911
+
912
+ # ============================================================
913
+ # Convenience Functions
914
+ # ============================================================
915
+
916
+ def visualize_coord_system(
917
+ coord: coord3,
918
+ scale: float = 1.0,
919
+ figsize: Tuple[int, int] = (10, 8),
920
+ show: bool = True,
921
+ save_path: Optional[str] = None,
922
+ title: str = "Coordinate System"
923
+ ):
924
+ """
925
+ Quick visualization of a single coordinate frame.
926
+
927
+ Args:
928
+ coord: Coordinate frame
929
+ scale: Axis length
930
+ figsize: Figure size
931
+ show: Display figure
932
+ save_path: Save path (optional)
933
+ title: Plot title
934
+ """
935
+ vis = CoordinateSystemVisualizer(figsize=figsize)
936
+ vis.draw_world_coord(scale=scale * 0.8)
937
+ vis.draw_coord_system(coord, scale=scale, label_prefix="Frame-")
938
+ vis.set_equal_aspect()
939
+ vis.set_title(title)
940
+
941
+ if save_path:
942
+ vis.save(save_path)
943
+ if show:
944
+ vis.show()
945
+
946
+
947
+ def visualize_curve(
948
+ curve: ParametricCurve,
949
+ show_tangents: bool = False,
950
+ show_normals: bool = False,
951
+ show_binormals: bool = False,
952
+ show_frames: bool = True,
953
+ frame_skip: int = 5,
954
+ figsize: Tuple[int, int] = (12, 9),
955
+ show: bool = True,
956
+ save_path: Optional[str] = None,
957
+ title: str = "Parametric Curve"
958
+ ):
959
+ """
960
+ Quick visualization of a parametric curve.
961
+
962
+ Args:
963
+ curve: Parametric curve object
964
+ show_tangents: Show tangent vectors
965
+ show_normals: Show normal vectors
966
+ show_binormals: Show binormal vectors
967
+ show_frames: Show complete Frenet frames
968
+ frame_skip: Frame drawing interval
969
+ figsize: Figure size
970
+ show: Display figure
971
+ save_path: Save path (optional)
972
+ title: Plot title
973
+ """
974
+ vis = CurveVisualizer(figsize=figsize)
975
+
976
+ points = curve.sample_points()
977
+ tangents = curve.sample_tangents() if show_tangents and not show_frames else None
978
+ normals = curve.sample_normals() if show_normals and not show_frames else None
979
+ binormals = curve.sample_binormals() if show_binormals and not show_frames else None
980
+ frames = curve.sample_frames() if show_frames else None
981
+
982
+ vis.draw_complete_curve(
983
+ points=points,
984
+ tangents=tangents,
985
+ normals=normals,
986
+ binormals=binormals,
987
+ frames=frames,
988
+ frame_skip=frame_skip
989
+ )
990
+ vis.set_title(title)
991
+
992
+ if save_path:
993
+ vis.save(save_path)
994
+ if show:
995
+ vis.show()
996
+
997
+
998
+ def visualize_surface(
999
+ surface: 'Surface',
1000
+ curvature_type: Optional[str] = None,
1001
+ show_normals: bool = False,
1002
+ show_frames: bool = False,
1003
+ u_range: Tuple[float, float] = (0.1, np.pi - 0.1),
1004
+ v_range: Tuple[float, float] = (0, 2 * np.pi),
1005
+ figsize: Tuple[int, int] = (14, 10),
1006
+ show: bool = True,
1007
+ save_path: Optional[str] = None,
1008
+ title: str = "Surface"
1009
+ ):
1010
+ """
1011
+ Quick visualization of a parametric surface.
1012
+
1013
+ Args:
1014
+ surface: Surface object
1015
+ curvature_type: 'gaussian' or 'mean' for curvature coloring (None for plain)
1016
+ show_normals: Show normal vectors
1017
+ show_frames: Show frame field
1018
+ u_range: Parameter u range
1019
+ v_range: Parameter v range
1020
+ figsize: Figure size
1021
+ show: Display figure
1022
+ save_path: Save path (optional)
1023
+ title: Plot title
1024
+ """
1025
+ vis = SurfaceVisualizer(figsize=figsize)
1026
+
1027
+ if curvature_type:
1028
+ vis.draw_surface_curvature(surface, curvature_type=curvature_type,
1029
+ u_range=u_range, v_range=v_range)
1030
+ else:
1031
+ vis.draw_surface(surface, u_range=u_range, v_range=v_range)
1032
+
1033
+ if show_normals:
1034
+ vis.draw_surface_normals(surface, u_range=u_range, v_range=v_range)
1035
+
1036
+ if show_frames:
1037
+ vis.draw_surface_frames(surface, u_range=u_range, v_range=v_range)
1038
+
1039
+ vis.draw_world_coord(scale=0.5)
1040
+ vis.set_equal_aspect()
1041
+ vis.set_title(title)
1042
+
1043
+ if save_path:
1044
+ vis.save(save_path)
1045
+ if show:
1046
+ vis.show()
1047
+
1048
+
1049
+ # ============================================================
1050
+ # Export
1051
+ # ============================================================
1052
+
1053
+ __all__ = [
1054
+ # Visualizer classes
1055
+ 'CoordinateSystemVisualizer',
1056
+ 'CurveVisualizer',
1057
+ 'SurfaceVisualizer',
1058
+
1059
+ # Curve class
1060
+ 'ParametricCurve',
1061
+
1062
+ # Convenience functions
1063
+ 'visualize_coord_system',
1064
+ 'visualize_curve',
1065
+ 'visualize_surface',
1066
+
1067
+ # Utility
1068
+ 'create_curvature_colormap',
1069
+ ]