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,507 @@
1
+ """
2
+ Frame Field Curve Interpolation System
3
+ =======================================
4
+
5
+ Frame field spline interpolation based on C++ implementation,
6
+ providing geometrically continuous curve reconstruction equivalent to NURBS.
7
+
8
+ Main Features:
9
+ - Generate Frenet frames from discrete points
10
+ - Frame field interpolation with multiple parameterization methods
11
+ - C2-continuous high-order interpolation
12
+ - B-spline and frame field hybrid interpolation
13
+ - Curvature distribution analysis
14
+
15
+ Author: PanGuoJun
16
+ Date: 2025-12-01
17
+ """
18
+
19
+ import math
20
+ import numpy as np
21
+ from typing import List, Tuple, Optional
22
+
23
+ try:
24
+ from .coordinate_system import vec3, coord3, quat
25
+ except ImportError:
26
+ import coordinate_system
27
+ vec3 = coordinate_system.vec3
28
+ coord3 = coordinate_system.coord3
29
+ quat = coordinate_system.quat
30
+
31
+
32
+ # ========== Utility Functions ==========
33
+
34
+ def catmull_rom(p0: vec3, p1: vec3, p2: vec3, p3: vec3, t: float) -> vec3:
35
+ """
36
+ Catmull-Rom spline interpolation for smooth position interpolation
37
+
38
+ Args:
39
+ p0, p1, p2, p3: Four control points
40
+ t: Parameter [0, 1] between p1 and p2
41
+
42
+ Returns:
43
+ Interpolated position
44
+ """
45
+ t2 = t * t
46
+ t3 = t2 * t
47
+
48
+ # Catmull-Rom basis functions
49
+ result = (
50
+ p0 * (-0.5 * t3 + 1.0 * t2 - 0.5 * t) +
51
+ p1 * ( 1.5 * t3 - 2.5 * t2 + 1.0) +
52
+ p2 * (-1.5 * t3 + 2.0 * t2 + 0.5 * t) +
53
+ p3 * ( 0.5 * t3 - 0.5 * t2)
54
+ )
55
+
56
+ return result
57
+
58
+
59
+ def squad_interp(q0: quat, q1: quat, q2: quat, q3: quat, t: float) -> quat:
60
+ """
61
+ SQUAD (Spherical Quadrangle) quaternion interpolation for C2-continuous rotation
62
+
63
+ Args:
64
+ q0, q1, q2, q3: Four quaternions
65
+ t: Parameter [0, 1] between q1 and q2
66
+
67
+ Returns:
68
+ Interpolated quaternion
69
+ """
70
+ # Compute intermediate control quaternions for smooth interpolation
71
+ def compute_intermediate(qa: quat, qb: quat, qc: quat) -> quat:
72
+ """Compute intermediate control quaternion"""
73
+ # Ensure shortest path
74
+ if qa.dot(qb) < 0:
75
+ qb = quat(-qb.w, -qb.x, -qb.y, -qb.z)
76
+ if qb.dot(qc) < 0:
77
+ qc = quat(-qc.w, -qc.x, -qc.y, -qc.z)
78
+
79
+ # Log map
80
+ inv_qb = qb.inverse()
81
+ log_qa_qb = (inv_qb * qa).ln()
82
+ log_qc_qb = (inv_qb * qc).ln()
83
+
84
+ # Intermediate control point
85
+ intermediate = qb * (((log_qa_qb + log_qc_qb) * -0.25).exp())
86
+ return intermediate
87
+
88
+ # Compute control quaternions
89
+ try:
90
+ s1 = compute_intermediate(q0, q1, q2)
91
+ s2 = compute_intermediate(q1, q2, q3)
92
+ except:
93
+ # Fallback to simple slerp if SQUAD fails
94
+ return quat.slerp(q1, q2, t)
95
+
96
+ # SQUAD interpolation: slerp(slerp(q1, q2, t), slerp(s1, s2, t), 2t(1-t))
97
+ slerp1 = quat.slerp(q1, q2, t)
98
+ slerp2 = quat.slerp(s1, s2, t)
99
+ h = 2.0 * t * (1.0 - t)
100
+
101
+ return quat.slerp(slerp1, slerp2, h)
102
+
103
+
104
+ # ========== Core Functions ==========
105
+
106
+ def generate_frenet_frames(points: List[vec3]) -> List[coord3]:
107
+ """
108
+ Generate Frenet-like frames from discrete point sequence
109
+
110
+ Args:
111
+ points: Discrete point sequence
112
+
113
+ Returns:
114
+ Corresponding frame sequence
115
+ """
116
+ frames = []
117
+ n = len(points)
118
+
119
+ if n < 3:
120
+ # Use simple frames when not enough points
121
+ for i in range(n):
122
+ if i == 0:
123
+ tangent = (points[1] - points[0]).normalized()
124
+ elif i == n-1:
125
+ tangent = (points[n-1] - points[n-2]).normalized()
126
+ else:
127
+ tangent = (points[i+1] - points[i-1]).normalized()
128
+
129
+ # Construct orthogonal frame
130
+ UZ = vec3(0, 0, 1)
131
+ UY = vec3(0, 1, 0)
132
+
133
+ if abs(1.0 - abs(tangent.dot(UZ))) > 0.1:
134
+ normal = tangent.cross(UZ).normalized()
135
+ else:
136
+ normal = tangent.cross(UY).normalized()
137
+
138
+ binormal = tangent.cross(normal).normalized()
139
+
140
+ frame = coord3.from_axes(tangent, normal, binormal)
141
+ frame.o = points[i]
142
+ frames.append(frame)
143
+
144
+ return frames
145
+
146
+ # Calculate Frenet frame for each point
147
+ for i in range(n):
148
+ if i == 0:
149
+ # Start point: forward difference
150
+ tangent = (points[1] - points[0]).normalized()
151
+ v1 = points[1] - points[0]
152
+ v2 = points[2] - points[1]
153
+ binormal = v1.cross(v2).normalized()
154
+
155
+ if binormal.length() < 1e-6:
156
+ UZ = vec3(0, 0, 1)
157
+ UY = vec3(0, 1, 0)
158
+ if abs(1.0 - abs(tangent.dot(UZ))) > 0.1:
159
+ binormal = tangent.cross(UZ).normalized()
160
+ else:
161
+ binormal = tangent.cross(UY).normalized()
162
+
163
+ elif i == n-1:
164
+ # End point: backward difference
165
+ tangent = (points[n-1] - points[n-2]).normalized()
166
+ v1 = points[n-2] - points[n-3]
167
+ v2 = points[n-1] - points[n-2]
168
+ binormal = v1.cross(v2).normalized()
169
+
170
+ if binormal.length() < 1e-6:
171
+ UZ = vec3(0, 0, 1)
172
+ UY = vec3(0, 1, 0)
173
+ if abs(1.0 - abs(tangent.dot(UZ))) > 0.1:
174
+ binormal = tangent.cross(UZ).normalized()
175
+ else:
176
+ binormal = tangent.cross(UY).normalized()
177
+ else:
178
+ # Interior point: central difference
179
+ tangent = (points[i+1] - points[i-1]).normalized()
180
+ v1 = points[i] - points[i-1]
181
+ v2 = points[i+1] - points[i]
182
+ binormal = v1.cross(v2).normalized()
183
+
184
+ if binormal.length() < 1e-6:
185
+ UZ = vec3(0, 0, 1)
186
+ UY = vec3(0, 1, 0)
187
+ if abs(1.0 - abs(tangent.dot(UZ))) > 0.1:
188
+ binormal = tangent.cross(UZ).normalized()
189
+ else:
190
+ binormal = tangent.cross(UY).normalized()
191
+
192
+ # Re-orthogonalize
193
+ normal = binormal.cross(tangent).normalized()
194
+ binormal = tangent.cross(normal).normalized()
195
+
196
+ frame = coord3.from_axes(tangent, normal, binormal)
197
+ frame.o = points[i]
198
+ frames.append(frame)
199
+
200
+ return frames
201
+
202
+
203
+ def frame_field_spline(frames: List[coord3], t: float, curve_type: int = 1) -> coord3:
204
+ """
205
+ Frame field spline interpolation
206
+
207
+ Args:
208
+ frames: Frame sequence
209
+ t: Global parameter [0,1]
210
+ curve_type: Curve type (0=uniform, 1=chord-length, 2=centripetal)
211
+
212
+ Returns:
213
+ Interpolated frame
214
+ """
215
+ if not frames:
216
+ return coord3()
217
+ if len(frames) == 1:
218
+ return frames[0]
219
+
220
+ n = len(frames)
221
+
222
+ # Compute node vector
223
+ nodes = [0.0]
224
+
225
+ if curve_type == 0:
226
+ # Uniform parameterization
227
+ for i in range(1, n):
228
+ nodes.append(i / (n - 1))
229
+ else:
230
+ # Chord-length or centripetal parameterization
231
+ total_length = 0.0
232
+ segment_lengths = []
233
+
234
+ for i in range(n - 1):
235
+ dist = (frames[i+1].o - frames[i].o).length()
236
+ if curve_type == 2:
237
+ dist = math.sqrt(dist)
238
+ segment_lengths.append(dist)
239
+ total_length += dist
240
+
241
+ for i in range(1, n):
242
+ nodes.append(nodes[i-1] + segment_lengths[i-1] / total_length)
243
+
244
+ # Find the segment containing parameter t
245
+ segment_index = 0
246
+ for i in range(n - 1):
247
+ if nodes[i] <= t <= nodes[i+1]:
248
+ segment_index = i
249
+ break
250
+ if t >= nodes[n-1]:
251
+ segment_index = n - 2
252
+
253
+ # Local parameter
254
+ local_t = (t - nodes[segment_index]) / (nodes[segment_index+1] - nodes[segment_index])
255
+ local_t = max(0.0, min(1.0, local_t))
256
+
257
+ # SE(3) interpolation using slerp
258
+ return coord3.slerp(frames[segment_index], frames[segment_index+1], local_t)
259
+
260
+
261
+ def frame_field_spline_c2(frames: List[coord3], t: float) -> coord3:
262
+ """
263
+ C2-continuous frame field spline with high-order continuity
264
+ Uses Catmull-Rom for position and SQUAD for rotation
265
+
266
+ Args:
267
+ frames: Frame sequence (requires at least 4 frames)
268
+ t: Global parameter [0,1]
269
+
270
+ Returns:
271
+ Interpolated frame with C2 continuity
272
+ """
273
+ if len(frames) < 4:
274
+ # Fallback to C1 continuity when not enough frames
275
+ return frame_field_spline(frames, t, 1)
276
+
277
+ n = len(frames)
278
+
279
+ # Compute node vector using chord-length parameterization
280
+ nodes = [0.0]
281
+ total_length = 0.0
282
+ segment_lengths = []
283
+
284
+ for i in range(n - 1):
285
+ dist = (frames[i+1].o - frames[i].o).length()
286
+ segment_lengths.append(dist)
287
+ total_length += dist
288
+
289
+ for i in range(1, n):
290
+ nodes.append(nodes[i-1] + segment_lengths[i-1] / total_length)
291
+
292
+ # Find the segment containing parameter t
293
+ i = 0
294
+ for i in range(n - 1):
295
+ if t >= nodes[i] and t <= nodes[i+1]:
296
+ break
297
+ if t >= nodes[n-1]:
298
+ i = n - 2
299
+
300
+ # Local parameter
301
+ local_t = (t - nodes[i]) / (nodes[i+1] - nodes[i])
302
+ local_t = max(0.0, min(1.0, local_t))
303
+
304
+ # Get 4 control frames for interpolation
305
+ i0 = max(0, i - 1)
306
+ i1 = i
307
+ i2 = i + 1
308
+ i3 = min(n - 1, i + 2)
309
+
310
+ # Position using Catmull-Rom spline
311
+ pos = catmull_rom(frames[i0].o, frames[i1].o, frames[i2].o, frames[i3].o, local_t)
312
+
313
+ # Rotation using SQUAD interpolation
314
+ q0 = frames[i0].Q()
315
+ q1 = frames[i1].Q()
316
+ q2 = frames[i2].Q()
317
+ q3 = frames[i3].Q()
318
+
319
+ rot = squad_interp(q0, q1, q2, q3, local_t)
320
+
321
+ return coord3(pos, rot)
322
+
323
+
324
+ def reconstruct_curve_from_polygon(
325
+ polygon: List[vec3],
326
+ samples: int = 100,
327
+ curve_type: int = 1
328
+ ) -> List[vec3]:
329
+ """
330
+ Reconstruct curve from polygon vertices using frame field spline
331
+
332
+ Args:
333
+ polygon: Input polygon vertices
334
+ samples: Number of sample points
335
+ curve_type: Curve type (0=uniform, 1=chord-length, 2=centripetal, 3=C2-continuous)
336
+
337
+ Returns:
338
+ Reconstructed curve point sequence
339
+ """
340
+ if len(polygon) < 2:
341
+ return []
342
+
343
+ # Generate frame field from polygon vertices
344
+ frames = generate_frenet_frames(polygon)
345
+
346
+ # Interpolate and sample
347
+ curve_points = []
348
+ for i in range(samples):
349
+ t = i / (samples - 1)
350
+
351
+ if curve_type == 3:
352
+ # C2-continuous interpolation
353
+ interpolated_frame = frame_field_spline_c2(frames, t)
354
+ else:
355
+ # Standard interpolation
356
+ interpolated_frame = frame_field_spline(frames, t, curve_type)
357
+
358
+ curve_points.append(interpolated_frame.o)
359
+
360
+ return curve_points
361
+
362
+
363
+ def compute_curvature_profile(curve: List[vec3]) -> List[float]:
364
+ """
365
+ Compute curvature distribution of reconstructed curve
366
+
367
+ Args:
368
+ curve: Curve point sequence
369
+
370
+ Returns:
371
+ Curvature value sequence
372
+ """
373
+ if len(curve) < 3:
374
+ return []
375
+
376
+ curvatures = []
377
+
378
+ for i in range(1, len(curve) - 1):
379
+ v1 = curve[i] - curve[i-1]
380
+ v2 = curve[i+1] - curve[i]
381
+
382
+ # Calculate angle change
383
+ dot_product = v1.normalized().dot(v2.normalized())
384
+ dot_product = max(-1.0, min(1.0, dot_product))
385
+ delta_angle = math.acos(dot_product)
386
+
387
+ avg_length = (v1.length() + v2.length()) * 0.5
388
+
389
+ # Curvature = angle change rate / arc length
390
+ curvature = delta_angle / (avg_length + 1e-6)
391
+ curvatures.append(curvature)
392
+
393
+ # Use boundary values for endpoints
394
+ if curvatures:
395
+ curvatures.insert(0, curvatures[0])
396
+ curvatures.append(curvatures[-1])
397
+
398
+ return curvatures
399
+
400
+
401
+ # ========== Main Class ==========
402
+
403
+ class InterpolatedCurve:
404
+ """
405
+ Interpolated curve class - Encapsulates frame field interpolation functionality
406
+ """
407
+
408
+ def __init__(self, control_points: List[vec3], curve_type: int = 1, c2_continuity: bool = False):
409
+ """
410
+ Initialize interpolated curve
411
+
412
+ Args:
413
+ control_points: Control point list
414
+ curve_type: Curve type (0=uniform, 1=chord-length, 2=centripetal)
415
+ c2_continuity: Enable C2-continuous interpolation (requires 4+ points)
416
+ """
417
+ self.control_points = control_points
418
+ self.curve_type = curve_type
419
+ self.c2_continuity = c2_continuity
420
+ self.frames = generate_frenet_frames(control_points)
421
+
422
+ def evaluate(self, t: float) -> vec3:
423
+ """
424
+ Evaluate position at parameter t
425
+
426
+ Args:
427
+ t: Parameter value [0, 1]
428
+
429
+ Returns:
430
+ Point on curve
431
+ """
432
+ frame = self.evaluate_frame(t)
433
+ return frame.o
434
+
435
+ def evaluate_frame(self, t: float) -> coord3:
436
+ """
437
+ Evaluate complete frame at parameter t
438
+
439
+ Args:
440
+ t: Parameter value [0, 1]
441
+
442
+ Returns:
443
+ Frame on curve
444
+ """
445
+ if self.c2_continuity and len(self.frames) >= 4:
446
+ return frame_field_spline_c2(self.frames, t)
447
+ else:
448
+ return frame_field_spline(self.frames, t, self.curve_type)
449
+
450
+ def sample(self, num_samples: int = 100) -> List[vec3]:
451
+ """
452
+ Sample curve points
453
+
454
+ Args:
455
+ num_samples: Number of sample points
456
+
457
+ Returns:
458
+ Sampled point list
459
+ """
460
+ curve_points = []
461
+ for i in range(num_samples):
462
+ t = i / (num_samples - 1)
463
+ curve_points.append(self.evaluate(t))
464
+ return curve_points
465
+
466
+ def sample_frames(self, num_samples: int = 100) -> List[coord3]:
467
+ """
468
+ Sample frames
469
+
470
+ Args:
471
+ num_samples: Number of sample points
472
+
473
+ Returns:
474
+ Frame list
475
+ """
476
+ sampled_frames = []
477
+ for i in range(num_samples):
478
+ t = i / (num_samples - 1)
479
+ sampled_frames.append(self.evaluate_frame(t))
480
+ return sampled_frames
481
+
482
+ def get_curvature_profile(self, num_samples: int = 100) -> List[float]:
483
+ """
484
+ Get curvature distribution
485
+
486
+ Args:
487
+ num_samples: Number of sample points
488
+
489
+ Returns:
490
+ Curvature value list
491
+ """
492
+ curve_points = self.sample(num_samples)
493
+ return compute_curvature_profile(curve_points)
494
+
495
+
496
+ # ========== Export ==========
497
+
498
+ __all__ = [
499
+ 'generate_frenet_frames',
500
+ 'frame_field_spline',
501
+ 'frame_field_spline_c2',
502
+ 'reconstruct_curve_from_polygon',
503
+ 'compute_curvature_profile',
504
+ 'InterpolatedCurve',
505
+ 'catmull_rom',
506
+ 'squad_interp',
507
+ ]