GM3Dps 0.0.1__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.
@@ -0,0 +1 @@
1
+ from .core import *
@@ -0,0 +1,919 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Core part of ``GM3Dps``
4
+ """
5
+
6
+ from __future__ import annotations
7
+ import numpy as np
8
+
9
+ class GM3DpsError(ValueError) :
10
+ """
11
+ Error raise by GM3Dps when computation is no longer viable
12
+ """
13
+
14
+
15
+ class Figure3D :
16
+ """
17
+ Base of every figure where everything else will be drawn
18
+ """
19
+ def __init__(self,
20
+ width : float,
21
+ height : float,
22
+ zvector : np.ndarray = np.array([0., -1., 0.]),
23
+ origin : np.ndarray = np.array([0., 0., 0.]),
24
+ alpha : float = 0.) :
25
+ """
26
+ Base of every figure where everything else will be drawn
27
+
28
+ Parameters
29
+ ----------
30
+ width : float
31
+ Screen width of the figure
32
+ height : float
33
+ Screen height of the figure
34
+ zvector : 3D vector, default : [0., -1., 0.]
35
+ Vector facing in the screen (away from the observer)
36
+ origin : 3D vector, default : [0., 0., 0.]
37
+ Point in the center of the screen
38
+ alpha : float
39
+ Rotation (in degrees) of the figure on the screen anticlockwise
40
+ """
41
+ self._children = []
42
+ self._labels = []
43
+ self.width = width
44
+ """int: Width of the figure"""
45
+ self.height = height
46
+ """int: Height of the figure"""
47
+ self._zvector = zvector/np.linalg.norm(zvector)
48
+ self._alpha = alpha
49
+ self._origin = origin
50
+ self._posmatrixinit()
51
+
52
+ def _posmatrixinit(self) : #à tester
53
+ xtemp = np.cross(np.array([0., 0., 1.]), self._zvector)
54
+ x = xtemp/np.linalg.norm(xtemp)
55
+ ytemp = np.cross(self._zvector, x)
56
+ y = ytemp/np.linalg.norm(ytemp)
57
+ rot = np.array([[np.cos(self._alpha), np.sin(self._alpha)],
58
+ [-np.sin(self._alpha), np.cos(self._alpha)]])
59
+ self._positionmatrix = rot @ np.array([x, y])
60
+ return self._positionmatrix
61
+
62
+ def _screen_projection(self, point : np.ndarray) :
63
+ return np.dot(self._positionmatrix, point - self._origin)
64
+
65
+
66
+ def export(self, path : str, frame : bool = True) :
67
+ """
68
+ Method to export the figure to a PostScript file
69
+
70
+ Parameters
71
+ ----------
72
+ path : str
73
+ Path to save the figure
74
+ frame : bool, default : True
75
+ Whether or not to have frame around the bounding box
76
+ Note
77
+ ----
78
+ Since the extension of the file can be specified in the
79
+ path, the user can chose between a postscript and an
80
+ encapsulated postscript file with it.
81
+
82
+ But it will not change the heading of the file which eps ready.
83
+ """
84
+ with open(path, 'w') as dest :
85
+ dest.write("%!PS-Adobe-3.0 EPSF-3.0\n%%Creator : GM3Dps\n")
86
+ dest.write("%%BoundingBox : {} {} {} {}\n"
87
+ .format(300-self.width/2,
88
+ 421-self.height/2,
89
+ self.width,
90
+ self.height)) #centered bbox
91
+ dest.write("300 421 translate\ngsave\n") #center the figure
92
+ if frame :
93
+ dest.write("newpath {} dup 2 div neg {} dup 2 div neg 2 index exch moveto 2 index 0 rlineto exch 0 exch pop exch rlineto neg 0 rlineto closepath stroke\n".format(self.width, self.height))
94
+ for label in self._labels :
95
+ label._export(dest)
96
+ for child in self._children :
97
+ child._export(dest)
98
+ dest.write("grestore\n")
99
+ dest.write("showpage")
100
+
101
+ def plane2D(self, *args, **kwargs) :
102
+ """
103
+ Method to create :obj:`Plane2D` with the figure as its master
104
+ """
105
+ return Plane2D(self, *args, **kwargs)
106
+
107
+ def label(self, *args, **kwargs) :
108
+ """
109
+ Method to create :obj:`Label` with the figure as its master
110
+ """
111
+ return Label(self, *args, **kwargs)
112
+
113
+ class Label :
114
+ """
115
+ Label to have text on the figure
116
+ """
117
+ def __init__(self,
118
+ master : Figure3D,
119
+ text : str,
120
+ origin : np.ndarray,
121
+ offset2D : np.ndarray = np.array([0.,0.]), **kwargs) :
122
+ """
123
+ Parameters
124
+ ----------
125
+ master : :obj:`Figure3D`
126
+ Where to draw the label
127
+ text : str
128
+ Text to have on the label
129
+ origin :
130
+ 3D coordinates of the label (bottom left)
131
+ offset2D :
132
+ Screen offset to apply on the label
133
+ **kwargs :
134
+ Arguments passed down to style
135
+ """
136
+ self.text = text
137
+ """str: Text of the Label"""
138
+ self.master = master
139
+ """:py:obj:`Figure3D`: Figure where the label is drawn on"""
140
+ self.master._labels.append(self)
141
+ self.origin = origin
142
+ """3D Vector: 3D position of the label"""
143
+ self.offset2D = offset2D
144
+ """2D Vector: Screen offset relative to its origin"""
145
+ self._drawflag = True
146
+ self.style(**kwargs)
147
+
148
+ def style(self, font : str = "Times-New-Roman", size : int = 15) :
149
+ """
150
+ Edit the style of the label
151
+
152
+ Parameters
153
+ ----------
154
+ font : str, default : "Times-New-Roman"
155
+ Font used by the label
156
+ size : int, default : 15
157
+ Font size used by the label
158
+ """
159
+ self.font = font
160
+ self.size = size
161
+
162
+ @property
163
+ def draw(self) -> bool :
164
+ """
165
+ bool : Whether or not the label will be drawn. Default, True
166
+ """
167
+ return self._drawflag
168
+
169
+ @draw.setter
170
+ def draw(self, value : bool) :
171
+ self._drawflag = value
172
+
173
+ def _drawstyle(self, file) :
174
+ file.write("/{} findfont {} scalefont setfont\n".format(self.font, self.size))
175
+
176
+ def _export(self, file) :
177
+ if self._drawflag :
178
+ position = self.master._screen_projection(self.origin) + self.offset2D
179
+ file.write("{} {} moveto \n".format(position[0], position[1]))
180
+ self._drawstyle(file)
181
+ file.write("({}) show\n".format(self.text))
182
+
183
+ class Plane2D :
184
+ """
185
+ 2D plane on the Figure to draw 2D objects
186
+ """
187
+ def __init__(self,
188
+ master : Figure3D,
189
+ origin : np.ndarray,
190
+ normal : np.ndarray,
191
+ alpha : float = 0.) :
192
+ """
193
+ Parameters
194
+ ----------
195
+ master : :obj:`Figure3D`
196
+ Figure where to have the plane
197
+ origin : 3D Vector
198
+ 3D coordinates of the 0, 0 point of the plane
199
+ normal : 3D vector
200
+ Normal vector to the plane (does not need to be a unit vector)
201
+ alpha : float, default : 0.
202
+ Rotation of the plane aroung its normal vector
203
+ anticlockwise. By default, the second vector of the plane
204
+ will be as vertical as possible upwards
205
+ """
206
+ self.master = master
207
+ """:py:obj:`Figure3D`: Figure the Plane is drawn"""
208
+ self._children = []
209
+ master._children.append(self)
210
+ self.origin = origin
211
+ """3D Vector: Position of the center of the plane in 3D space"""
212
+ self._normal = normal
213
+ self._unit_vector()
214
+ self.alpha = alpha
215
+ """float: Anticlockwise rotation of the plane around its normal
216
+ vector, the second vector of the plane will be as vertical as
217
+ possible by default"""
218
+
219
+ def _unit_vector(self) :
220
+ #unit vector on the plane
221
+ self._ex = np.array([-self._normal[1], self._normal[0], 0.]) #Ez x normal
222
+ self._ex /= np.linalg.norm(self._ex)
223
+ self._ey = np.array([-self._normal[0]*self._normal[2],
224
+ -self._normal[1]*self._normal[2],
225
+ self._normal[0]**2 + self._normal[1]**2]).astype(np.float64) #normal x ex
226
+ self._ey /= np.linalg.norm(self._ey)
227
+ self._position_matrix = np.array([self._ex, self._ey])
228
+
229
+ self._lambda0 = None
230
+ self._lambda1 = None
231
+ self._alpha0 = None
232
+ self._alpha1 = None
233
+
234
+ @property
235
+ def normal(self) :
236
+ """
237
+ 3D Vector: Unit vector normal to the plane
238
+ """
239
+ return self._normal/np.linalg.norm(self._normal)
240
+
241
+ @normal.setter
242
+ def normal(self, value) :
243
+ self._normal = value
244
+ self._unit_vector()
245
+
246
+ def _position3d(self, coordinates : np.ndarray) :
247
+ return np.matvec(self._position_matrix.T, coordinates) + self.origin
248
+
249
+ def _coordinate(self, file) :
250
+ file.write("{} {} translate\n".format(self.origin[0], self.origin[2]))
251
+
252
+ #matrix computing
253
+ if self._lambda0 == None :
254
+ ex = np.dot(self.master._positionmatrix, self._ex)
255
+ ey = np.dot(self.master._positionmatrix, self._ey)
256
+ M= np.array([ex, ey]).T
257
+ D0, R0 = np.linalg.eig(M.T@M)
258
+ D1, R1 = np.linalg.eig(M@M.T)
259
+ D0 = np.real(D0)
260
+ D1 = np.real(D1)
261
+ R0 = np.real(R0)
262
+ R1 = np.real(R1)
263
+
264
+ if not (np.isclose(D0, D1).all()) :
265
+ R1 = np.array([R1[:,1], R1[:,0]])
266
+ if np.linalg.det(R0) < 0 :
267
+ R0[:,1] = -R0[:,1]
268
+ if np.linalg.det(R1) < 0 :
269
+ R1[:,1] = -R1[:,1]
270
+
271
+ self._lambda0 = np.dot(R1.T@M, R0[:,0])[0]
272
+ self._lambda1 = np.dot(R1.T@M, R0[:,1])[1]
273
+ self._alpha0 = -np.atan2(-R0[0,1], R0[0,0])
274
+ self._alpha1 = np.atan2(-R1[0,1], R1[0,0])
275
+
276
+ file.write("{} rotate\n".format(np.rad2deg(self._alpha1)))
277
+ file.write("{} {} scale\n".format(self._lambda0, self._lambda1))
278
+ file.write("{} rotate\n".format(np.rad2deg(self._alpha0)))
279
+ file.write("{} rotate\n".format(np.rad2deg(self.alpha)))
280
+
281
+ def _export(self, file) :
282
+ self._coordinate(file)
283
+ for child in self._children :
284
+ child._export(file)
285
+ file.write("grestore\n")
286
+
287
+ def point(self, *args, **kwargs) :
288
+ """
289
+ Method to create a :obj:`Point2D` on the plane
290
+ """
291
+ return Point2D(self, *args, **kwargs)
292
+
293
+ def line(self, *args, **kwargs) :
294
+ """
295
+ Method to create a :obj:`Line2D` on the plane
296
+ """
297
+ return Line2D(self, *args, **kwargs)
298
+
299
+ def polygon(self, *args, **kwargs) :
300
+ """
301
+ Method to create a :obj:`Polygon2D` on the plane
302
+ """
303
+ return Polygon2D(self, *args, **kwargs)
304
+
305
+ def rectangle(self, *args, **kwargs) :
306
+ """
307
+ Method to create a :obj:`Rectangle2D` on the plane
308
+ """
309
+ return Rectangle2D(self, *args, **kwargs)
310
+
311
+ def ellipse(self, *args, **kwargs) :
312
+ """
313
+ Method to create an :obj:`Ellipse2D` on the plane
314
+ """
315
+ return Ellipse2D(self, *args, **kwargs)
316
+
317
+ def arc(self, *args, **kwargs) :
318
+ """
319
+ Method to create an :obj:`Arc2D` on the plane
320
+ """
321
+ return Arc2D(self, *args, **kwargs)
322
+
323
+ def angle(self, *args, **kwargs) :
324
+ """
325
+ Method to create an :obj:`Angle2D` on the plane
326
+ """
327
+ return Angle2D(self, *args, **kwargs)
328
+
329
+ def _arrowhead(self, *args, **kwargs) :
330
+ return _ArrowHead2D(self, *args, **kwargs)
331
+
332
+
333
+ class _Object2D :
334
+ def __init__(self, master : Plane2D) :
335
+ self._children = []
336
+ self._style_children = []
337
+ self.master = master
338
+ """:py:obj:`Plane2D`: Plane where the object is drawn"""
339
+ self._drawflag = False
340
+ self._dash = None
341
+ master._children.append(self)
342
+ self._label_position = np.array([0., 0.])
343
+
344
+ def style(self, dash : list[float|int] =None) :
345
+ """
346
+ Edits the drawing style of an object
347
+
348
+ Parameters
349
+ ----------
350
+ dash : list[float|int]
351
+ Dashing style of the line
352
+ """
353
+ if dash != None :
354
+ self._dash = dash.copy()
355
+ for child in self._style_children :
356
+ child.style(dash)
357
+
358
+ def _drawstyle(self, file) :
359
+ if self._dash != None :
360
+ file.write("[")
361
+ for e in self._dash :
362
+ file.write("{} ".format(e))
363
+ file.write("] 0 setdash\n")
364
+ else :
365
+ file.write("[] 0 setdash\n")
366
+
367
+ @property
368
+ def draw(self) -> bool :
369
+ """
370
+ bool: Whether or not an element will be drawn where exporting
371
+ """
372
+ return self._drawflag
373
+
374
+ @draw.setter
375
+ def draw(self, value) :
376
+ self._drawflag = value
377
+ for child in self._children :
378
+ child.draw = value
379
+
380
+ def label(self,
381
+ text : str,
382
+ offset3d : np.ndarray=np.array([0., 0., 0.]),
383
+ offset2d : np.ndarray=np.array([0., 0.]),
384
+ **kwargs) :
385
+ """
386
+ Create a label positionned somewhat correctly whereas the object
387
+
388
+ Parameters
389
+ ----------
390
+ text : str
391
+ Text to print on the label
392
+ offset3d : 3D Vector
393
+ Displacement in the 3D space of the label relative to its
394
+ default position
395
+ offset2d : 2D Vector
396
+ Displacement on the screen of the label relative to its
397
+ position
398
+
399
+ Returns
400
+ -------
401
+ out : :py:obj:`Label`
402
+ Label object created
403
+
404
+ Note
405
+ ----
406
+ ``offset2D`` and ``offset3d`` are compatible with one another,
407
+ the usage of both will result in both the offsets being applied
408
+ """
409
+ return Label(self.master.master,
410
+ text,
411
+ self.master._position3d(self._label_position)+offset3d,
412
+ offset2d,
413
+ **kwargs)
414
+
415
+ def _export(self, file) :
416
+ return
417
+
418
+
419
+ class Point2D(_Object2D) :
420
+ """
421
+ 2D point on a plane
422
+ """
423
+ @staticmethod
424
+ def _compatibilitycheck(master, pointlike) :
425
+ if isinstance(pointlike, Point2D) :
426
+ return pointlike
427
+ elif isinstance(pointlike, np.ndarray) :
428
+ return Point2D(master, pointlike)
429
+ elif isinstance(pointlike, np.ndarray) :
430
+ return Point2D(master, np.array(pointlike))
431
+ else :
432
+ raise TypeError("expected Point2D, numpy ndarray or list got {} instead".format(type(pointlike)))
433
+
434
+ def __init__(self, master : Plane2D, pos : np.ndarray) :
435
+ """
436
+ Parameters
437
+ ----------
438
+ master : :obj:`Plane2D`
439
+ Plane on which the point will be placed
440
+ pos : 2D Vector
441
+ Position on the point on the plane
442
+ """
443
+ self._position = pos
444
+ super().__init__(master)
445
+ self._label_position = pos
446
+
447
+ @property
448
+ def position(self) :
449
+ """2D Vector: Where the point is located on the plane"""
450
+ return self._position
451
+
452
+ @position.setter
453
+ def position(self, pos) :
454
+ self._position = pos
455
+ self._label_position = pos
456
+
457
+ def line(self, end : np.ndarray|Point2D) :
458
+ """
459
+ Create a line between the point and another one
460
+
461
+ Parameters
462
+ ----------
463
+ end : :obj:`Point2D`
464
+ End of the line to draw
465
+
466
+ Returns
467
+ -------
468
+ :obj:`Line2D`
469
+ Line between the two points
470
+ """
471
+ return Line2D(self.master, self, end)
472
+
473
+ def __sub__(self, other : Point2D|np.ndarray) :
474
+ if isinstance(other, Point2D) :
475
+ return self.position - other.position
476
+ else :
477
+ return self.position - other
478
+
479
+ class Line2D(_Object2D):
480
+ """
481
+ Line to draw on a 2D plane
482
+ """
483
+ def __init__(self,
484
+ master : Plane2D,
485
+ origin : np.ndarray|Point2D,
486
+ end : np.ndarray|Point2D) :
487
+ """
488
+ Parameters
489
+ ----------
490
+ master : :obj:`Plane2D`
491
+ Plane on which the line will be drawn
492
+ origin : :obj:`Point2D`
493
+ Start of the line
494
+ end : :obj:`Point2D`
495
+ End of the line
496
+ """
497
+ super().__init__(master)
498
+ self._start = Point2D._compatibilitycheck(master, origin)
499
+ self._end = Point2D._compatibilitycheck(master, end)
500
+ self._label_position = (self._end._position + self._start._position)/2
501
+
502
+ @property
503
+ def end(self) :
504
+ """
505
+ :py:obj:`Point2D`: End of the line
506
+ """
507
+ return self._end
508
+
509
+ @end.setter
510
+ def end(self, end : Point2D) :
511
+ self._end = end
512
+ self._label_position = (self._end._position + self._start._position)/2
513
+
514
+ @property
515
+ def start(self) :
516
+ """
517
+ :py:obj:`Point2D`: Start of the line
518
+ """
519
+ return self._start
520
+
521
+ @start.setter
522
+ def start(self, start : Point2D) :
523
+ self._start = start
524
+ self._label_position = (self._end._position + self._start._position)/2
525
+
526
+ def _export(self, file) :
527
+ if self._drawflag :
528
+ self._drawstyle(file)
529
+ file.write("newpath {} {} moveto {} {} lineto stroke\n".format(self.start.position[0], self.start.position[1], self.end.position[0], self.end.position[1]))
530
+
531
+ def intersection(self, other : Line2D) :
532
+ """
533
+ Compute the intersection of two lines on the same plane
534
+
535
+ Parameters
536
+ ----------
537
+ other : :obj:`Line2D`
538
+ Other line with which to compute the intersection
539
+
540
+ Returns
541
+ -------
542
+ :obj:`Point2D`
543
+ Point of the intersection of the two lines
544
+ """
545
+ a, b = (self.end - self.start)[0], (self.end - self.start)[1]
546
+ c, d = (other.end - other.start)[0], (other.end - other.start)[1]
547
+ v0 = np.array([b, -a])
548
+ v1 = np.array([d, -c])
549
+ e = np.dot(self.start.position, v0)
550
+ f = np.dot(other.start.position, v1)
551
+ return self.master.point(np.linalg.solve(np.array([v0, v1]),
552
+ np.array([e,f])))
553
+
554
+ def angle(self, other : Line2D, **kwargs) :
555
+ """
556
+ Return an angle object to show the angle between to lines
557
+
558
+ Parameters
559
+ ----------
560
+ other : :obj:`Line2D`
561
+ Other line to draw the angle with
562
+ **kwargs :
563
+ Arguments passed down to :obj:`Angle2D`
564
+
565
+ Returns
566
+ -------
567
+ :obj:`Angle2D`
568
+ Angle between the two lines
569
+ """
570
+ center = self.intersection(other)
571
+ v0 = self.end - self.start
572
+ v1 = other.end - other.start
573
+ alpha0 = np.atan2(v0[1], v0[0])
574
+ alpha1 = np.atan2(v1[1], v1[0])
575
+ return self.master.angle(center, alpha0, alpha1, 15., **kwargs)
576
+
577
+
578
+ class Polygon2D(_Object2D) :
579
+ """
580
+ Polygon on a 2D plane
581
+ """
582
+ def __init__(self, master : Plane2D,
583
+ verteces : list[np.ndarray]|list[Point2D]) :
584
+ """
585
+ Parameters
586
+ ----------
587
+ master : :obj:`Plane2D`
588
+ Plane on which to plot the polygon
589
+ verteces : list[:obj:`Point2D`]
590
+ Verteces of the polygon
591
+ """
592
+ n = len(verteces)
593
+ self._verteces = n*[None]
594
+ self._edges = n*[None]
595
+ for i in range(n) :
596
+ self._verteces[i] = Point2D._compatibilitycheck(master, verteces[i])
597
+ super().__init__(master)
598
+ for i in range(n) :
599
+ self._edges[i] = self._verteces[i].line(self.verteces[i-n+1])
600
+ temp = [pt._position for pt in self._verteces]
601
+ self._label_position = np.mean(temp, axis=0)
602
+ self._children = self._edges
603
+ self._style_children = self._edges
604
+
605
+
606
+ class Rectangle2D(Polygon2D) :
607
+ """
608
+ Rectangle on a 2D plane
609
+ """
610
+ def __init__(self, master : Plane2D,
611
+ point0 : np.ndarray|Point2D,
612
+ point1 : np.ndarray|Point2D,
613
+ point3 : np.ndarray|Point2D) :
614
+ """
615
+ Parameters
616
+ ----------
617
+ master : :obj:`Plane2D`
618
+ Plane on which to plot the rectangle
619
+ point0 : :obj:`Point2D`
620
+ A vertex of the rectangle
621
+ point1 : :obj:`Point2D`
622
+ An adjacent vertex to point0 of the rectangle
623
+ point3 : :obj:`Point2D`
624
+ An other adjacent vertex to point0 of the rectangle
625
+ """
626
+ point3 = Point2D._compatibilitycheck(master, point3)
627
+ point0 = Point2D._compatibilitycheck(master, point0)
628
+ point1 = Point2D._compatibilitycheck(master, point1)
629
+ if np.abs(np.dot(point1 - point0, point3 - point0)) > 1.e-6 :
630
+ raise GM3DpsError("The two sides of the rectangle are not perpendicular to each other")
631
+ super().__init__(master,
632
+ [point0,
633
+ point1,
634
+ master.point(point1.position + (point3 - point0)),
635
+ point3])
636
+
637
+
638
+ class Ellipse2D(_Object2D) :
639
+ """
640
+ Ellipse on a 2D plane
641
+ """
642
+ def __init__(self, master : Plane2D,
643
+ center : Point2D|np.ndarray,
644
+ point0 : Point2D|np.ndarray,
645
+ point1 : Point2D|np.ndarray) :
646
+ """
647
+ Parameters
648
+ ----------
649
+ master : :obj:`Plane2D`
650
+ Plane on which to plot the ellipse
651
+ center : :obj:`Point2D`
652
+ Center of the ellipse
653
+ point0 : :obj:`Point2D`
654
+ Point at the end of the major axis
655
+ point1 : :obj:`Point2D`
656
+ Point at the end of the minor axis
657
+ """
658
+ self._center = Point2D._compatibilitycheck(master, center)
659
+ self._point0 = Point2D._compatibilitycheck(master, point0)
660
+ self._point1 = Point2D._compatibilitycheck(master, point1)
661
+ self._a = np.linalg.norm(self._point0-self._center)
662
+ self._b = np.linalg.norm(self._point1-self._center)
663
+ if self._a < self._b :
664
+ self._b, self._a = self._a, self._b
665
+ self._point0, self._point1 = self._point1, self._point0
666
+ self._eccentricity = np.sqrt(1-(self._b/self._a)**2)
667
+ if np.abs(np.dot(self._point0-self._center,
668
+ self._point1-self._center)) > 1.e-6 :
669
+ raise GM3DpsError("The minor axis and the major axis are not perpendicular to each other")
670
+ super().__init__(master)
671
+ self._label_position = self._center
672
+
673
+ def _export(self, file) :
674
+ if self._drawflag :
675
+ Maxis = self._point0 - self._center
676
+ alpha = np.rad2deg(np.atan2(Maxis[1], Maxis[0]))
677
+ file.write("{} {} translate ".format(self._center.position[0],
678
+ self._center.position[1]))
679
+ file.write("{} rotate ".format(alpha))
680
+ file.write("1 {} scale\n".format(self._b/self._a))
681
+ self._drawstyle(file)
682
+ file.write("newpath 0 0 {} 0 360 arc stroke\ngrestore\ngsave\n".format(np.linalg.norm(Maxis)))
683
+ self.master._coordinate(file)
684
+
685
+ def foci(self) :
686
+ """
687
+ Returns the foci of the ellipse
688
+
689
+ Returns
690
+ -------
691
+ focus0 : :obj:`Point2D`
692
+ Focus point closest to point0
693
+ focus1 : :obj:`Point2D`
694
+ Focus point furthest to point0
695
+ """
696
+ focus0 = self.master.point(self._center.position+self._eccentricity*(self._point0-self._center))
697
+ focus1 = self.master.point(self._center.position-self._eccentricity*(self._point0-self._center))
698
+ return focus0, focus1
699
+
700
+ def point(self, alpha : float, mode : str = "eccentric") :
701
+ """
702
+ Return a point on the ellipse corresponding to an angle.
703
+
704
+ Parameters
705
+ ----------
706
+ alpha : float
707
+ Angle to position the point
708
+ mode : str, default ``"eccentric"``
709
+ mode to mesure the angle
710
+
711
+ * ``"eccentric"`` : using the eccentric angle to
712
+ position the point, starting from ``point0`` and going
713
+ towards ``point1``
714
+
715
+ * ``"true0"`` : using the true anomaly starting from
716
+ ``point0`` going towards ``point1``
717
+
718
+ * ``"true1"`` : using the true anomaly starting from
719
+ the opposite point to ``point0`` and going the same
720
+ way as ``"true0"``
721
+
722
+ Returns
723
+ -------
724
+ out : :obj:`Point2D`
725
+ Corresponding to the angle
726
+ """
727
+ if mode == "eccentric" :
728
+ return self.master.point(np.cos(alpha)*(self._point0-self._center) + np.sin(alpha)*(self._point1-self._center))
729
+ elif mode == "true0" :
730
+ theta = 2.*np.atan2(np.sqrt((1.-self._eccentricity)/(1.+self._eccentricity))*np.sin(alpha/2.),np.cos(alpha/2.))
731
+ return self.point(theta, mode="eccentric")
732
+ elif mode == "true1" :
733
+ theta = 2.*np.atan2(np.sqrt((1.-self._eccentricity)/(1.+self._eccentricity))*np.sin(alpha/2.),np.cos(alpha/2.)) + np.pi
734
+ return self.point(theta, mode="eccentric")
735
+ else :
736
+ raise GM3DpsError("mode should be eccentric, true0 or true1 recieved {} instead".format(mode))
737
+
738
+
739
+ class _ArrowHead2D(_Object2D) :
740
+ def __init__(self, master : Plane2D, end : Point2D|np.ndarray, length : float, width : float, alpha : float) :
741
+ super().__init__(master)
742
+ self._end = Point2D._compatibilitycheck(master, end)
743
+ self.length = length
744
+ self.width = width
745
+ self.alpha = alpha
746
+ self._label_position = self._end._position
747
+
748
+ @property
749
+ def end(self) :
750
+ return self._end
751
+
752
+ @end.setter
753
+ def end(self, end : Point2D) :
754
+ self._end = end
755
+ self._label_position = self.end._position
756
+
757
+ def back(self) :
758
+ return self.master.point(self._end - self.length*np.array([np.cos(self.alpha), np.sin(self.alpha)]))
759
+
760
+ def _export(self, file) :
761
+ if self._drawflag :
762
+ file.write("{} {} moveto\n".format(self.end.position[0], self.end.position[1]))
763
+ file.write("{} dup rotate\n".format(np.rad2deg(self.alpha)))
764
+ self._drawstyle(file)
765
+ file.write("0 {} dup 2 div {} neg exch rlineto neg rlineto closepath fill\nneg rotate\n".format(self.width, self.length))
766
+
767
+
768
+ class Vector2D(_Object2D) :
769
+ """
770
+ Vector to plot on a 2D plane
771
+ """
772
+ def __init__(self, master : Plane2D,
773
+ start : Point2D|np.ndarray,
774
+ end : Point2D|np.ndarray) :
775
+ """
776
+ Parameters
777
+ ----------
778
+ master : :obj:`Plane2D`
779
+ Plane on which the vector will be plotted
780
+ start : :obj:`Point2D`
781
+ Start of the vector
782
+ end : :obj:`Point2D`
783
+ End of the vector
784
+ """
785
+ super().__init__(master)
786
+ self.start = Point2D._compatibilitycheck(master, start)
787
+ """:py:obj:`Point2D`: Start of the vector on the 2D plane"""
788
+ self.end = Point2D._compatibilitycheck(master, end)
789
+ """:py:obj:`Point2D`: End of the vector on the 2D plane"""
790
+ self.line = master.line(self.start, self.end)
791
+ """:py:obj:`Line2D`: Line body of the vector"""
792
+ temp = self.end - self.start
793
+ self.arrowhead = master._arrowhead(self.end, 10., 5., np.rad2deg(np.atan2(temp[1], temp[0])))
794
+ """:py:obj:`_Arrowhead2D`: Arrow head of the vector"""
795
+ self._label_position = self.line._label_position
796
+ self._children = [self.line, self.arrowhead]
797
+ self._style_children = [self.line]
798
+
799
+ class Arc2D(_Object2D) :
800
+ """
801
+ Arc to show an angle on a 2D plane
802
+ """
803
+ def __init__(self, master : Plane2D,
804
+ center : Point2D|np.ndarray,
805
+ alpha0 : float,
806
+ alpha1 : float,
807
+ radius : float) :
808
+ """
809
+ Parameters
810
+ ----------
811
+ master : :obj:`Plane2D`
812
+ Plane to plot the arc on
813
+ center : :obj:`Point2D`
814
+ Center of the arc
815
+ alpha0 : float
816
+ Start of the arc
817
+ alpha1 : float
818
+ End of the arc
819
+ radius : float
820
+ Radius of the arc
821
+ """
822
+ super().__init__(master)
823
+ self.center = Point2D._compatibilitycheck(self.master, center)
824
+ """:py:obj:`Point2D`: Center of the arc"""
825
+ self.radius = radius
826
+ """float: Radius of the arc"""
827
+ self.alpha0 = alpha0
828
+ """float: Starting angle of the arc"""
829
+ self.alpha1 = alpha1
830
+ """float: Ending angle of the arc"""
831
+ self._adjustement()
832
+
833
+ def _adjustement(self) :
834
+ self._label_position = self.center.position + self.radius*(np.array([np.cos((self.alpha0+self.alpha1)/2), np.sin((self.alpha0+self.alpha1)/2)]))
835
+
836
+ def _export(self, file) :
837
+ if self._drawflag :
838
+ self._drawstyle(file)
839
+ file.write("newpath {} {} {} {} {} arc stroke\n".format(self.center.position[0],
840
+ self.center.position[1],
841
+ self.radius,
842
+ np.rad2deg(self.alpha0),
843
+ np.rad2deg(self.alpha1)))
844
+
845
+ class Angle2D(_Object2D) :
846
+ """
847
+ Angle to show an angle on a 2D plane
848
+ """
849
+ def __init__(self, master : Plane2D,
850
+ center : Point2D|np.ndarray,
851
+ alpha0 : float,
852
+ alpha1 : float,
853
+ radius : float,
854
+ anticlockwise : bool = True) :
855
+ """
856
+ Parameters
857
+ ----------
858
+ master : :obj:`Plane2D`
859
+ Plane on which to plot the angle
860
+ center : :obj:`Point2D`
861
+ Center of the angle
862
+ alpha0 : float
863
+ Starting *angle* of the angle
864
+ alpha1 : float
865
+ Ending *angle* of the angle
866
+ radius : float
867
+ Radius of the angle
868
+ anticlockwise : bool, default : ``True``
869
+ Whether or not the angle will anticlockwise
870
+ """
871
+ super().__init__(master)
872
+ self.center = Point2D._compatibilitycheck(master, center)
873
+ """:py:obj:`Point2D`: Center of the angle on the plane"""
874
+ self._radius = radius
875
+ self._alpha0 = alpha0
876
+ self._alpha1 = alpha1
877
+ self._anticlockwise = anticlockwise
878
+ temp = master.point(center.position+self._radius*np.array([np.cos(alpha1), np.sin(alpha1)]))
879
+ if anticlockwise :
880
+ temp_arrowhead = master._arrowhead(temp, 6., 11., alpha1 + np.pi/2)
881
+ temp_arc = self.master.arc(self.center, self._alpha0, self._alpha1, self._radius)
882
+ else :
883
+ temp_arrowhead = master._arrowhead(temp, 6., 11., alpha1 - np.pi/2)
884
+ temp_arc = self.master.arc(self.center, self._alpha1, self._alpha0, self._radius)
885
+ self.arrowhead = temp_arrowhead
886
+ """:py:obj:`_Arrowhead2D`: Arrow head of the angle. Its ``width``,
887
+ ``lenght`` and ``style`` can be modified like other drawn object"""
888
+ self.arc = temp_arc
889
+ """:py:obj:`Arc2D`: Arc body of the angle"""
890
+ self._label_position = self.arc._label_position
891
+ self._children = [self.arrowhead, self.arc]
892
+
893
+ def _adjustement(self) :
894
+ if np.abs(np.sin(self._alpha1-self._alpha0)) > 2*self.arrowhead.length/self._radius :
895
+ self.arc.center = self.master.point(self.center.position -
896
+ self.arrowhead.length*np.array([np.cos(self._alpha0), np.sin(self._alpha0)])/np.sin(self._alpha1-self._alpha0)
897
+ )
898
+ self.arc.radius = self.radius - np.abs(self.arrowhead.length/np.tan(self._alpha1-self._alpha0))
899
+ self.arc._adjustement()
900
+
901
+ @property
902
+ def radius(self) :
903
+ """
904
+ float: Radius of the angle
905
+ """
906
+ return self._radius
907
+
908
+ @radius.setter
909
+ def radius(self, value : float) :
910
+ self._radius = value
911
+ self.arrowhead.end.position = self.center.position + self._radius*np.array([np.cos(self._alpha1), np.sin(self._alpha1)])
912
+ self._adjustement()
913
+
914
+
915
+ class _ObjectPseudo3D :
916
+ def __init__(self, master : Figure3D, origin : np.ndarray, normal : np.ndarray) :
917
+ self.master = master
918
+ self.origin = origin
919
+ self.plane = Plane2D(master, origin, normal, 0.)
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: GM3Dps
3
+ Version: 0.0.1
4
+ Summary: A package to create 3D (and 2D) Postscript figure in Python
5
+ Author-email: Yannis Benchaib--Langon <yannis.benchaib-langon@student.isae-supaero.fr>
6
+ Project-URL: Homepage, https://github.com/Gutenews/GM3Dps
7
+ Keywords: geometry,postscript
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy>=2.4.0
13
+ Dynamic: license-file
14
+
15
+ # GM3Dps
16
+ A python module to draw 3D (or 2D) figure in postscript.
17
+ Documentation available at : https://gm3dps.readthedocs.io/en/latest/index.html
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ ./GM3Dps/__init__.py
5
+ ./GM3Dps/core.py
6
+ GM3Dps/__init__.py
7
+ GM3Dps/core.py
8
+ GM3Dps.egg-info/PKG-INFO
9
+ GM3Dps.egg-info/SOURCES.txt
10
+ GM3Dps.egg-info/dependency_links.txt
11
+ GM3Dps.egg-info/requires.txt
12
+ GM3Dps.egg-info/top_level.txt
13
+ tests/test.py
@@ -0,0 +1 @@
1
+ numpy>=2.4.0
@@ -0,0 +1 @@
1
+ GM3Dps
gm3dps-0.0.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yannis Benchaib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gm3dps-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: GM3Dps
3
+ Version: 0.0.1
4
+ Summary: A package to create 3D (and 2D) Postscript figure in Python
5
+ Author-email: Yannis Benchaib--Langon <yannis.benchaib-langon@student.isae-supaero.fr>
6
+ Project-URL: Homepage, https://github.com/Gutenews/GM3Dps
7
+ Keywords: geometry,postscript
8
+ Classifier: Programming Language :: Python
9
+ Classifier: Programming Language :: Python :: 3
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy>=2.4.0
13
+ Dynamic: license-file
14
+
15
+ # GM3Dps
16
+ A python module to draw 3D (or 2D) figure in postscript.
17
+ Documentation available at : https://gm3dps.readthedocs.io/en/latest/index.html
gm3dps-0.0.1/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # GM3Dps
2
+ A python module to draw 3D (or 2D) figure in postscript.
3
+ Documentation available at : https://gm3dps.readthedocs.io/en/latest/index.html
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "GM3Dps"
7
+ version = "0.0.1"
8
+ description = "A package to create 3D (and 2D) Postscript figure in Python"
9
+ readme = "README.md"
10
+ authors = [{name = "Yannis Benchaib--Langon", email = "yannis.benchaib-langon@student.isae-supaero.fr"}]
11
+ license-files = ["LICENSE"]
12
+ classifiers = [
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3",
15
+ ]
16
+ keywords = [
17
+ "geometry",
18
+ "postscript"
19
+ ]
20
+ dependencies = [
21
+ "numpy>=2.4.0"
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/Gutenews/GM3Dps"
26
+
27
+ [tool.setuptools]
28
+ packages = ["GM3Dps"]
29
+ package-dir = {"" = "."}
gm3dps-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,22 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sun Oct 11 11:58:41 2026
4
+
5
+ @author: yanni
6
+ """
7
+
8
+ import GM3Dps as gm
9
+ import numpy as np
10
+
11
+ draw = gm.Figure3D(200, 200)
12
+ plan = gm.Plane2D(draw, np.array([0,0,0]), np.array([-1,-1,1]), 0.)
13
+ O = gm.Point2D(plan, np.array([0,0]))
14
+ droitex = gm.Line2D(plan, O, np.array([100,0]))
15
+ droitex.draw()
16
+ droitey = gm.Line2D(plan, O, np.array([0,100]))
17
+ droitey.style(dash = [5,5])
18
+ droitey.draw()
19
+ ell = gm.Ellipse2D(plan, O, droitex.end, droitey.end)
20
+ ell.draw()
21
+
22
+ draw.export("test.ps")