Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/plot/plot3d/base.pyx
4045 views
1
r"""
2
Base classes for 3D Graphics objects and plotting
3
4
AUTHORS:
5
6
- Robert Bradshaw (2007-02): initial version
7
8
- Robert Bradshaw (2007-08): Cythonization, much optimization
9
10
- William Stein (2008)
11
12
TODO: - finish integrating tachyon - good default lights, camera
13
"""
14
15
#*****************************************************************************
16
# Copyright (C) 2007 Robert Bradshaw <[email protected]>
17
#
18
# Distributed under the terms of the GNU General Public License (GPL)
19
#
20
# This code is distributed in the hope that it will be useful,
21
# but WITHOUT ANY WARRANTY; without even the implied warranty of
22
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
23
# General Public License for more details.
24
#
25
# The full text of the GPL is available at:
26
#
27
# http://www.gnu.org/licenses/
28
#*****************************************************************************
29
30
31
include "../../ext/python_list.pxi"
32
33
import os
34
from math import atan2
35
from random import randint
36
import zipfile
37
from cStringIO import StringIO
38
39
import sage.misc.misc
40
41
from sage.modules.free_module_element import vector
42
43
from sage.rings.real_double import RDF
44
from sage.misc.functional import sqrt, atan, acos
45
46
from texture import Texture, is_Texture
47
from transform cimport Transformation, point_c, face_c
48
include "point_c.pxi"
49
50
from sage.interfaces.tachyon import tachyon_rt
51
52
from sage.plot.all import show_default
53
54
# import the double infinity constant
55
cdef extern from "math.h":
56
enum: INFINITY
57
58
59
default_texture = Texture()
60
pi = RDF.pi()
61
62
cdef class Graphics3d(SageObject):
63
"""
64
This is the baseclass for all 3d graphics objects.
65
"""
66
def __repr__(self):
67
"""
68
When show_default is True, objects are displayed rather than
69
string representations printed.
70
71
EXAMPLES::
72
73
sage: S = sphere((0, 0, 0), 1)
74
sage: show_default(False); S
75
Graphics3d Object
76
sage: show_default(True); S
77
78
"""
79
if show_default():
80
self.show()
81
return ''
82
else:
83
return str(self)
84
85
def __str__(self):
86
"""
87
EXAMPLES::
88
89
sage: S = sphere((0, 0, 0), 1)
90
sage: str(S)
91
'Graphics3d Object'
92
"""
93
return "Graphics3d Object"
94
95
def __add__(left, right):
96
"""
97
Addition of objects adds them to the same scene.
98
99
EXAMPLES::
100
sage: A = sphere((0,0,0), 1, color='red')
101
sage: B = dodecahedron((2, 0, 0), color='yellow')
102
sage: A+B
103
104
For convenience, we take 0 and None to be the additive identity::
105
106
sage: A + 0 is A
107
True
108
sage: A + None is A, 0 + A is A, None + A is A
109
(True, True, True)
110
111
In particular, this allows us to use the sum() function without
112
having to provide an empty starting object::
113
114
sage: sum(point3d((cos(n), sin(n), n)) for n in [0..10, step=.1])
115
116
A Graphics 3d object can also be added a 2d graphic object::
117
118
sage: A = sphere((0, 0, 0), 1) + circle((0, 0), 1.5)
119
sage: A.show(aspect_ratio=1)
120
"""
121
if right == 0 or right is None:
122
return left
123
elif left == 0 or left is None:
124
return right
125
elif not isinstance(left, Graphics3d):
126
left = left.plot3d()
127
elif not isinstance(right, Graphics3d):
128
right = right.plot3d()
129
return Graphics3dGroup([left, right])
130
131
def _set_extra_kwds(self, kwds):
132
"""
133
Allows one to pass rendering arguments on as if they were set in the constructor.
134
135
EXAMPLES::
136
137
sage: S = sphere((0, 0, 0), 1)
138
sage: S._set_extra_kwds({'aspect_ratio': [1, 2, 2]})
139
sage: S
140
"""
141
self._extra_kwds = kwds
142
143
def aspect_ratio(self, v=None):
144
"""
145
Sets or gets the preferred aspect ratio of self.
146
147
EXAMPLES::
148
149
sage: D = dodecahedron()
150
sage: D.aspect_ratio()
151
[1.0, 1.0, 1.0]
152
sage: D.aspect_ratio([1,2,3])
153
sage: D.aspect_ratio()
154
[1.0, 2.0, 3.0]
155
sage: D.aspect_ratio(1)
156
sage: D.aspect_ratio()
157
[1.0, 1.0, 1.0]
158
"""
159
if not v is None:
160
if v == 1:
161
v = (1,1,1)
162
if not isinstance(v, (tuple, list)):
163
raise TypeError, "aspect_ratio must be a list or tuple of length 3 or the integer 1"
164
self._aspect_ratio = [float(a) for a in v]
165
else:
166
if self._aspect_ratio is None:
167
self._aspect_ratio = [1.0,1.0,1.0]
168
return self._aspect_ratio
169
170
def frame_aspect_ratio(self, v=None):
171
"""
172
Sets or gets the preferred frame aspect ratio of self.
173
174
EXAMPLES::
175
176
sage: D = dodecahedron()
177
sage: D.frame_aspect_ratio()
178
[1.0, 1.0, 1.0]
179
sage: D.frame_aspect_ratio([2,2,1])
180
sage: D.frame_aspect_ratio()
181
[2.0, 2.0, 1.0]
182
sage: D.frame_aspect_ratio(1)
183
sage: D.frame_aspect_ratio()
184
[1.0, 1.0, 1.0]
185
"""
186
if not v is None:
187
if v == 1:
188
v = (1,1,1)
189
if not isinstance(v, (tuple, list)):
190
raise TypeError, "frame_aspect_ratio must be a list or tuple of length 3 or the integer 1"
191
self._frame_aspect_ratio = [float(a) for a in v]
192
else:
193
if self._frame_aspect_ratio is None:
194
self._frame_aspect_ratio = [1.0,1.0,1.0]
195
return self._frame_aspect_ratio
196
197
def _determine_frame_aspect_ratio(self, aspect_ratio):
198
a_min, a_max = self._safe_bounding_box()
199
return [(a_max[i] - a_min[i])*aspect_ratio[i] for i in range(3)]
200
201
def _safe_bounding_box(self):
202
"""
203
Returns a bounding box but where no side length is 0. This is used
204
to avoid zero-division errors for pathological plots.
205
206
EXAMPLES::
207
208
sage: G = line3d([(0, 0, 0), (0, 0, 1)])
209
sage: G.bounding_box()
210
((0.0, 0.0, 0.0), (0.0, 0.0, 1.0))
211
sage: G._safe_bounding_box()
212
([-1.0, -1.0, 0.0], [1.0, 1.0, 1.0])
213
"""
214
a_min, a_max = self.bounding_box()
215
a_min = list(a_min); a_max = list(a_max)
216
for i in range(3):
217
if a_min[i] == a_max[i]:
218
a_min[i] = a_min[i] - 1
219
a_max[i] = a_max[i] + 1
220
return a_min, a_max
221
222
223
def bounding_box(self):
224
"""
225
Returns the lower and upper corners of a 3d bounding box for self.
226
This is used for rendering and self should fit entirely within this
227
box.
228
229
Specifically, the first point returned should have x, y, and z
230
coordinates should be the respective infimum over all points in self,
231
and the second point is the supremum.
232
233
The default return value is simply the box containing the origin.
234
235
EXAMPLES::
236
237
sage: sphere((1,1,1), 2).bounding_box()
238
((-1.0, -1.0, -1.0), (3.0, 3.0, 3.0))
239
sage: G = line3d([(1, 2, 3), (-1,-2,-3)])
240
sage: G.bounding_box()
241
((-1.0, -2.0, -3.0), (1.0, 2.0, 3.0))
242
"""
243
return ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
244
245
def transform(self, **kwds):
246
"""
247
Apply a transformation to self, where the inputs are passed onto a
248
TransformGroup object. Mostly for internal use; see the translate,
249
scale, and rotate methods for more details.
250
251
EXAMPLES::
252
253
sage: sphere((0,0,0), 1).transform(trans=(1, 0, 0), scale=(2,3,4)).bounding_box()
254
((-1.0, -3.0, -4.0), (3.0, 3.0, 4.0))
255
"""
256
return TransformGroup([self], **kwds)
257
258
def translate(self, *x):
259
"""
260
Return self translated by the given vector (which can be given either
261
as a 3-iterable or via positional arguments).
262
263
EXAMPLES::
264
265
sage: icosahedron() + sum(icosahedron(opacity=0.25).translate(2*n, 0, 0) for n in [1..4])
266
sage: icosahedron() + sum(icosahedron(opacity=0.25).translate([-2*n, n, n^2]) for n in [1..4])
267
268
TESTS::
269
270
sage: G = sphere((0, 0, 0), 1)
271
sage: G.bounding_box()
272
((-1.0, -1.0, -1.0), (1.0, 1.0, 1.0))
273
sage: G.translate(0, 0, 1).bounding_box()
274
((-1.0, -1.0, 0.0), (1.0, 1.0, 2.0))
275
sage: G.translate(-1, 5, 0).bounding_box()
276
((-2.0, 4.0, -1.0), (0.0, 6.0, 1.0))
277
"""
278
if len(x)==1:
279
x = x[0]
280
return self.transform(trans=x)
281
282
def scale(self, *x):
283
"""
284
Returns self scaled in the x, y, and z directions.
285
286
EXAMPLES::
287
288
sage: G = dodecahedron() + dodecahedron(opacity=.5).scale(2)
289
sage: G.show(aspect_ratio=1)
290
sage: G = icosahedron() + icosahedron(opacity=.5).scale([1, 1/2, 2])
291
sage: G.show(aspect_ratio=1)
292
293
TESTS::
294
295
sage: G = sphere((0, 0, 0), 1)
296
sage: G.scale(2)
297
sage: G.scale(1, 2, 1/2).show(aspect_ratio=1)
298
sage: G.scale(2).bounding_box()
299
((-2.0, -2.0, -2.0), (2.0, 2.0, 2.0))
300
"""
301
if isinstance(x[0], (tuple, list)):
302
x = x[0]
303
return self.transform(scale=x)
304
305
def rotate(self, v, theta):
306
"""
307
Returns self rotated about the vector `v` by `theta` radians.
308
309
EXAMPLES::
310
311
sage: from sage.plot.plot3d.shapes import Cone
312
sage: v = (1,2,3)
313
sage: G = arrow3d((0, 0, 0), v)
314
sage: G += Cone(1/5, 1).translate((0, 0, 2))
315
sage: C = Cone(1/5, 1, opacity=.25).translate((0, 0, 2))
316
sage: G += sum(C.rotate(v, pi*t/4) for t in [1..7])
317
sage: G.show(aspect_ratio=1)
318
319
sage: from sage.plot.plot3d.shapes import Box
320
sage: Box(1/3, 1/5, 1/7).rotate((1, 1, 1), pi/3).show(aspect_ratio=1)
321
"""
322
vx, vy, vz = v
323
return self.transform(rot=[vx, vy, vz, theta])
324
325
def rotateX(self, theta):
326
"""
327
Returns self rotated about the `x`-axis by the given angle.
328
329
EXAMPLES::
330
331
sage: from sage.plot.plot3d.shapes import Cone
332
sage: G = Cone(1/5, 1) + Cone(1/5, 1, opacity=.25).rotateX(pi/2)
333
sage: G.show(aspect_ratio=1)
334
"""
335
return self.rotate((1,0,0), theta)
336
337
def rotateY(self, theta):
338
"""
339
Returns self rotated about the `y`-axis by the given angle.
340
341
EXAMPLES::
342
343
sage: from sage.plot.plot3d.shapes import Cone
344
sage: G = Cone(1/5, 1) + Cone(1/5, 1, opacity=.25).rotateY(pi/3)
345
sage: G.show(aspect_ratio=1)
346
"""
347
return self.rotate((0,1,0), theta)
348
349
def rotateZ(self, theta):
350
"""
351
Returns self rotated about the `z`-axis by the given angle.
352
353
EXAMPLES::
354
355
sage: from sage.plot.plot3d.shapes import Box
356
sage: G = Box(1/2, 1/3, 1/5) + Box(1/2, 1/3, 1/5, opacity=.25).rotateZ(pi/5)
357
sage: G.show(aspect_ratio=1)
358
"""
359
return self.rotate((0,0,1), theta)
360
361
362
def viewpoint(self):
363
"""
364
Returns the viewpoint of this plot. Currently only a stub for x3d.
365
366
EXAMPLES::
367
368
sage: type(dodecahedron().viewpoint())
369
<class 'sage.plot.plot3d.base.Viewpoint'>
370
"""
371
# This should probably be reworked somehow.
372
return Viewpoint(0,0,6)
373
374
def default_render_params(self):
375
"""
376
Returns an instance of RenderParams suitable for plotting this object.
377
378
EXAMPLES::
379
380
sage: type(dodecahedron().default_render_params())
381
<class 'sage.plot.plot3d.base.RenderParams'>
382
"""
383
return RenderParams(ds=.075)
384
385
def testing_render_params(self):
386
"""
387
Returns an instance of RenderParams suitable for testing this object.
388
In particular, it opens up '/dev/null' as an auxiliary zip file for jmol.
389
390
EXAMPLES::
391
392
sage: type(dodecahedron().testing_render_params())
393
<class 'sage.plot.plot3d.base.RenderParams'>
394
"""
395
params = RenderParams(ds=.075)
396
params.output_archive = zipfile.ZipFile('/dev/null', 'w', zipfile.ZIP_STORED, True)
397
return params
398
399
def x3d(self):
400
"""
401
An x3d scene file (as a string) containing the this object.
402
403
EXAMPLES::
404
405
sage: print sphere((1, 2, 3), 5).x3d()
406
<X3D version='3.0' profile='Immersive' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation=' http://www.web3d.org/specifications/x3d-3.0.xsd '>
407
<head>
408
<meta name='title' content='sage3d'/>
409
</head>
410
<Scene>
411
<Viewpoint position='0 0 6'/>
412
<Transform translation='1 2 3'>
413
<Shape><Sphere radius='5.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
414
</Transform>
415
</Scene>
416
</X3D>
417
418
sage: G = icosahedron() + sphere((0,0,0), 0.5, color='red')
419
sage: print G.x3d()
420
<X3D version='3.0' profile='Immersive' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation=' http://www.web3d.org/specifications/x3d-3.0.xsd '>
421
<head>
422
<meta name='title' content='sage3d'/>
423
</head>
424
<Scene>
425
<Viewpoint position='0 0 6'/>
426
<Shape>
427
<IndexedFaceSet coordIndex='...'>
428
<Coordinate point='...'/>
429
</IndexedFaceSet>
430
<Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
431
<Transform translation='0 0 0'>
432
<Shape><Sphere radius='0.5'/><Appearance><Material diffuseColor='1.0 0.0 0.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
433
</Transform>
434
</Scene>
435
</X3D>
436
437
"""
438
return """
439
<X3D version='3.0' profile='Immersive' xmlns:xsd='http://www.w3.org/2001/XMLSchema-instance' xsd:noNamespaceSchemaLocation=' http://www.web3d.org/specifications/x3d-3.0.xsd '>
440
<head>
441
<meta name='title' content='sage3d'/>
442
</head>
443
<Scene>
444
%s
445
%s
446
</Scene>
447
</X3D>
448
"""%(self.viewpoint().x3d_str(), self.x3d_str())
449
450
def tachyon(self):
451
"""
452
An tachyon input file (as a string) containing the this object.
453
454
EXAMPLES::
455
456
sage: print sphere((1, 2, 3), 5, color='yellow').tachyon()
457
begin_scene
458
resolution 400 400
459
camera
460
...
461
plane
462
center -2000 -1000 -500
463
normal 2.3 2.4 2.0
464
TEXTURE
465
AMBIENT 1.0 DIFFUSE 1.0 SPECULAR 1.0 OPACITY 1.0
466
COLOR 1.0 1.0 1.0
467
TEXFUNC 0
468
Texdef texture...
469
Ambient 0.333333333333 Diffuse 0.666666666667 Specular 0.0 Opacity 1
470
Color 1.0 1.0 0.0
471
TexFunc 0
472
Sphere center 1.0 -2.0 3.0 Rad 5.0 texture...
473
end_scene
474
475
sage: G = icosahedron(color='red') + sphere((1,2,3), 0.5, color='yellow')
476
sage: G.show(viewer='tachyon', frame=false)
477
sage: print G.tachyon()
478
begin_scene
479
...
480
Texdef texture...
481
Ambient 0.333333333333 Diffuse 0.666666666667 Specular 0.0 Opacity 1
482
Color 1.0 1.0 0.0
483
TexFunc 0
484
TRI V0 ...
485
Sphere center 1.0 -2.0 3.0 Rad 0.5 texture...
486
end_scene
487
"""
488
489
render_params = self.default_render_params()
490
# switch from LH to RH coords to be consistent with java rendition
491
render_params.push_transform(Transformation(scale=[1,-1,1]))
492
return """
493
begin_scene
494
resolution 400 400
495
496
camera
497
zoom 1.0
498
aspectratio 1.0
499
antialiasing %s
500
raydepth 8
501
center 2.3 2.4 2.0
502
viewdir -2.3 -2.4 -2.0
503
updir 0.0 0.0 1.0
504
end_camera
505
506
507
light center 4.0 3.0 2.0
508
rad 0.2
509
color 1.0 1.0 1.0
510
511
plane
512
center -2000 -1000 -500
513
normal 2.3 2.4 2.0
514
TEXTURE
515
AMBIENT 1.0 DIFFUSE 1.0 SPECULAR 1.0 OPACITY 1.0
516
COLOR 1.0 1.0 1.0
517
TEXFUNC 0
518
519
%s
520
521
%s
522
523
end_scene""" % (render_params.antialiasing,
524
"\n".join(sorted([t.tachyon_str() for t in self.texture_set()])),
525
"\n".join(flatten_list(self.tachyon_repr(render_params))))
526
527
def obj(self):
528
"""
529
An .obj scene file (as a string) containing the this object. A
530
.mtl file of the same name must also be produced for coloring.
531
532
EXAMPLES::
533
534
sage: from sage.plot.plot3d.shapes import ColorCube
535
sage: print ColorCube(1, ['red', 'yellow', 'blue']).obj()
536
g obj_1
537
usemtl ...
538
v 1 1 1
539
v -1 1 1
540
v -1 -1 1
541
v 1 -1 1
542
f 1 2 3 4
543
...
544
g obj_6
545
usemtl ...
546
v -1 -1 1
547
v -1 1 1
548
v -1 1 -1
549
v -1 -1 -1
550
f 21 22 23 24
551
"""
552
return "\n".join(flatten_list([self.obj_repr(self.default_render_params()), ""]))
553
554
def export_jmol(self, filename='jmol_shape.jmol', force_reload=False,
555
zoom=100, spin=False, background=(1,1,1), stereo=False,
556
mesh=False, dots=False,
557
perspective_depth = True,
558
orientation = (-764,-346,-545,76.39), **ignored_kwds):
559
# orientation chosen to look same as tachyon
560
"""
561
A jmol scene consists of a script which refers to external files.
562
Fortunately, we are able to put all of them in a single zip archive,
563
which is the output of this call.
564
565
EXAMPLES::
566
567
sage: out_file = sage.misc.misc.tmp_filename() + ".jmol"
568
sage: G = sphere((1, 2, 3), 5) + cube() + sage.plot.plot3d.shapes.Text("hi")
569
sage: G.export_jmol(out_file)
570
sage: import zipfile
571
sage: z = zipfile.ZipFile(out_file)
572
sage: z.namelist()
573
['obj_...pmesh', 'SCRIPT']
574
575
sage: print z.read('SCRIPT')
576
data "model list"
577
2
578
empty
579
Xx 0 0 0
580
Xx 5.5 5.5 5.5
581
end "model list"; show data
582
select *
583
wireframe off; spacefill off
584
set labelOffset 0 0
585
background [255,255,255]
586
spin OFF
587
moveto 0 -764 -346 -545 76.39
588
centerAt absolute {0 0 0}
589
zoom 100
590
frank OFF
591
set perspectivedepth ON
592
isosurface sphere_1 center {1.0 2.0 3.0} sphere 5.0
593
color isosurface [102,102,255]
594
pmesh obj_... "obj_...pmesh"
595
color pmesh [102,102,255]
596
select atomno = 1
597
color atom [102,102,255]
598
label "hi"
599
isosurface fullylit; pmesh o* fullylit; set antialiasdisplay on;
600
601
sage: print z.read(z.namelist()[0])
602
24
603
0.5 0.5 0.5
604
-0.5 0.5 0.5
605
...
606
-0.5 -0.5 -0.5
607
6
608
5
609
0
610
1
611
...
612
"""
613
render_params = self.default_render_params()
614
render_params.mesh = mesh
615
render_params.dots = dots
616
render_params.output_file = filename
617
render_params.force_reload = render_params.randomize_counter = force_reload
618
render_params.output_archive = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED, True)
619
# Render the data
620
all = flatten_list([self.jmol_repr(render_params), ""])
621
622
f = StringIO()
623
624
if render_params.atom_list:
625
# Load the atom model
626
f.write('data "model list"\n')
627
f.write('%s\nempty\n' % (len(render_params.atom_list) + 1))
628
for atom in render_params.atom_list:
629
f.write('Xx %s %s %s\n' % atom)
630
f.write('Xx 5.5 5.5 5.5\n') # so the zoom fits the box
631
f.write('end "model list"; show data\n')
632
f.write('select *\n')
633
f.write('wireframe off; spacefill off\n')
634
f.write('set labelOffset 0 0\n')
635
636
637
# Set the scene background color
638
f.write('background [%s,%s,%s]\n'%tuple([int(a*255) for a in background]))
639
if spin:
640
f.write('spin ON\n')
641
else:
642
f.write('spin OFF\n')
643
if stereo:
644
if stereo is True: stereo = "redblue"
645
f.write('stereo %s\n' % stereo)
646
if orientation:
647
f.write('moveto 0 %s %s %s %s\n'%tuple(orientation))
648
649
f.write('centerAt absolute {0 0 0}\n')
650
f.write('zoom %s\n'%zoom)
651
f.write('frank OFF\n') # jmol logo
652
653
if perspective_depth:
654
f.write('set perspectivedepth ON\n')
655
else:
656
f.write('set perspectivedepth OFF\n')
657
658
# Put the rest of the object in
659
f.write("\n".join(all))
660
# Make sure the lighting is correct
661
f.write("isosurface fullylit; pmesh o* fullylit; set antialiasdisplay on;\n")
662
663
render_params.output_archive.writestr('SCRIPT', f.getvalue())
664
render_params.output_archive.close()
665
666
def json_repr(self, render_params):
667
"""
668
A (possibly nested) list of strings. Each entry is formatted as JSON, so
669
that a JavaScript client could eval it and get an object. Each object
670
has fields to encapsulate the faces and vertices of self. This
671
representation is intended to be consumed by the canvas3d viewer backend.
672
673
EXAMPLES::
674
675
sage: G = sage.plot.plot3d.base.Graphics3d()
676
sage: G.json_repr(G.default_render_params())
677
[]
678
"""
679
return []
680
681
def jmol_repr(self, render_params):
682
r"""
683
A (possibly nested) list of strings which will be concatenated and
684
used by jmol to render self. (Nested lists of strings are used
685
because otherwise all the intermediate concatenations can kill
686
performance). This may refer to several remove files, which
687
are stored in render_parames.output_archive.
688
689
EXAMPLES::
690
691
sage: G = sage.plot.plot3d.base.Graphics3d()
692
sage: G.jmol_repr(G.default_render_params())
693
[]
694
sage: G = sphere((1, 2, 3))
695
sage: G.jmol_repr(G.default_render_params())
696
[['isosurface sphere_1 center {1.0 2.0 3.0} sphere 1.0\ncolor isosurface [102,102,255]']]
697
"""
698
return []
699
700
def tachyon_repr(self, render_params):
701
r"""
702
A (possibly nested) list of strings which will be concatenated and
703
used by tachyon to render self. (Nested lists of strings are used
704
because otherwise all the intermediate concatenations can kill
705
performance). This may include a reference to color information which
706
is stored elsewhere.
707
708
EXAMPLES::
709
710
sage: G = sage.plot.plot3d.base.Graphics3d()
711
sage: G.tachyon_repr(G.default_render_params())
712
[]
713
sage: G = sphere((1, 2, 3))
714
sage: G.tachyon_repr(G.default_render_params())
715
['Sphere center 1.0 2.0 3.0 Rad 1.0 texture...']
716
"""
717
return []
718
719
def obj_repr(self, render_params):
720
"""
721
A (possibly nested) list of strings which will be concatenated and
722
used to construct an .obj file of self. (Nested lists of strings are
723
used because otherwise all the intermediate concatenations can kill
724
performance). This may include a reference to color information which
725
is stored elsewhere.
726
727
EXAMPLES::
728
729
sage: G = sage.plot.plot3d.base.Graphics3d()
730
sage: G.obj_repr(G.default_render_params())
731
[]
732
sage: G = cube()
733
sage: G.obj_repr(G.default_render_params())
734
['g obj_1',
735
'usemtl ...',
736
['v 0.5 0.5 0.5',
737
'v -0.5 0.5 0.5',
738
'v -0.5 -0.5 0.5',
739
'v 0.5 -0.5 0.5',
740
'v 0.5 0.5 -0.5',
741
'v -0.5 0.5 -0.5',
742
'v 0.5 -0.5 -0.5',
743
'v -0.5 -0.5 -0.5'],
744
['f 1 2 3 4',
745
'f 1 5 6 2',
746
'f 1 4 7 5',
747
'f 6 5 7 8',
748
'f 7 4 3 8',
749
'f 3 2 6 8'],
750
[]]
751
"""
752
return []
753
754
def texture_set(self):
755
"""
756
Often the textures of a 3d file format are kept separate from the
757
objects themselves. This function returns the set of textures used,
758
so they can be defined in a preamble or separate file.
759
760
EXAMPLES::
761
762
sage: sage.plot.plot3d.base.Graphics3d().texture_set()
763
set([])
764
765
sage: G = tetrahedron(color='red') + tetrahedron(color='yellow') + tetrahedron(color='red', opacity=0.5)
766
sage: [t for t in G.texture_set() if t.color == colors.red] # we should have two red textures
767
[Texture(texture..., red, ff0000), Texture(texture..., red, ff0000)]
768
sage: [t for t in G.texture_set() if t.color == colors.yellow] # ...and one yellow
769
[Texture(texture..., yellow, ffff00)]
770
"""
771
return set()
772
773
def mtl_str(self):
774
"""
775
Returns the contents of a .mtl file, to be used to provide coloring
776
information for an .obj file.
777
778
EXAMPLES::
779
sage: G = tetrahedron(color='red') + tetrahedron(color='yellow', opacity=0.5)
780
sage: print G.mtl_str()
781
newmtl ...
782
Ka 0.5 5e-06 5e-06
783
Kd 1.0 1e-05 1e-05
784
Ks 0.0 0.0 0.0
785
illum 1
786
Ns 1
787
d 1
788
newmtl ...
789
Ka 0.5 0.5 5e-06
790
Kd 1.0 1.0 1e-05
791
Ks 0.0 0.0 0.0
792
illum 1
793
Ns 1
794
d 0.500000000000000
795
"""
796
return "\n\n".join(sorted([t.mtl_str() for t in self.texture_set()])) + "\n"
797
798
def flatten(self):
799
"""
800
Try to reduce the depth of the scene tree by consolidating groups
801
and transformations.
802
803
The generic Graphics3d object can't be made flatter.
804
805
EXAMPLES::
806
807
sage: G = sage.plot.plot3d.base.Graphics3d()
808
sage: G.flatten() is G
809
True
810
"""
811
return self
812
813
def _rescale_for_frame_aspect_ratio_and_zoom(self, b, frame_aspect_ratio, zoom):
814
if frame_aspect_ratio is None:
815
return (b*zoom,b*zoom,b*zoom), (-b*zoom,-b*zoom,-b*zoom)
816
box = [b*w for w in frame_aspect_ratio]
817
# Now take the maximum length in box and rescale to b.
818
s = b / max(box)
819
box_max = tuple([s*w*zoom for w in box])
820
box_min = tuple([-w*zoom for w in box_max])
821
return box_min, box_max
822
823
def _prepare_for_jmol(self, frame, axes, frame_aspect_ratio, aspect_ratio, zoom):
824
from sage.plot.plot import EMBEDDED_MODE
825
if EMBEDDED_MODE:
826
s = 6
827
else:
828
s = 3
829
box_min, box_max = self._rescale_for_frame_aspect_ratio_and_zoom(s, frame_aspect_ratio, zoom)
830
a_min, a_max = self._box_for_aspect_ratio(aspect_ratio, box_min, box_max)
831
return self._transform_to_bounding_box(box_min, box_max, a_min, a_max, frame=frame,
832
axes=axes, thickness=1,
833
labels = True) # jmol labels are implemented
834
835
def _prepare_for_tachyon(self, frame, axes, frame_aspect_ratio, aspect_ratio, zoom):
836
box_min, box_max = self._rescale_for_frame_aspect_ratio_and_zoom(1.0, frame_aspect_ratio, zoom)
837
a_min, a_max = self._box_for_aspect_ratio(aspect_ratio, box_min, box_max)
838
return self._transform_to_bounding_box(box_min, box_max, a_min, a_max,
839
frame=frame, axes=axes, thickness=.75,
840
labels = False) # no tachyon text implemented yet
841
842
def _box_for_aspect_ratio(self, aspect_ratio, box_min, box_max):
843
# 1. Find a box around self so that when self gets rescaled into the
844
# box defined by box_min, box_max, it has the right aspect ratio
845
a_min, a_max = self._safe_bounding_box()
846
847
if aspect_ratio == "automatic":
848
return a_min, a_max
849
850
longest_side = 0; longest_length = a_max[0] - a_min[0]
851
shortest_side = 0; shortest_length = a_max[0] - a_min[0]
852
853
for i in range(3):
854
s = a_max[i] - a_min[i]
855
if s > longest_length:
856
longest_length = s
857
longest_side = i
858
if s < shortest_length:
859
shortest_length = s
860
shortest_side = i
861
862
# 2. Rescale aspect_ratio so the shortest side is 1.
863
r = float(aspect_ratio[shortest_side])
864
aspect_ratio = [a/r for a in aspect_ratio]
865
866
# 3. Extend the bounding box of self by rescaling so the sides
867
# have the same ratio as aspect_ratio, and without changing
868
# the longest side.
869
long_box_side = box_max[longest_side] - box_min[longest_side]
870
sc = [1.0,1.0,1.0]
871
for i in range(3):
872
# compute the length we want:
873
new_length = longest_length / aspect_ratio[i]
874
# change the side length by a_min and a_max so
875
# that a_max[i] - a_min[i] = new_length
876
877
# We have to take into account the ratio of the
878
# sides after transforming to the bounding box.
879
z = long_box_side / (box_max[i] - box_min[i])
880
w = new_length / ((a_max[i] - a_min[i]) * z)
881
sc[i] = w
882
883
w = min(sc)
884
sc = [z/w for z in sc]
885
for i in range(3):
886
a_min[i] *= sc[i]
887
a_max[i] *= sc[i]
888
889
return a_min, a_max
890
891
def _transform_to_bounding_box(self, xyz_min, xyz_max, a_min, a_max, frame, axes, thickness, labels):
892
893
a_min_orig = a_min; a_max_orig = a_max
894
895
# Rescale in each direction
896
scale = [float(xyz_max[i] - xyz_min[i]) / (a_max[i] - a_min[i]) for i in range(3)]
897
X = self.scale(scale)
898
a_min = [scale[i]*a_min[i] for i in range(3)]
899
a_max = [scale[i]*a_max[i] for i in range(3)]
900
901
# Translate so lower left corner of original bounding box
902
# is in the right spot
903
T = [xyz_min[i] - a_min[i] for i in range(3)]
904
X = X.translate(T)
905
if frame:
906
from shapes2 import frame3d, frame_labels
907
F = frame3d(xyz_min, xyz_max, opacity=0.5, color=(0,0,0), thickness=thickness)
908
if labels:
909
F += frame_labels(xyz_min, xyz_max, a_min_orig, a_max_orig)
910
911
X += F
912
913
if axes:
914
# draw axes
915
from shapes import arrow3d
916
A = (arrow3d((min(0,a_min[0]),0, 0), (max(0,a_max[0]), 0,0),
917
thickness, color="blue"),
918
arrow3d((0,min(0,a_min[1]), 0), (0, max(0,a_max[1]), 0),
919
thickness, color="blue"),
920
arrow3d((0, 0, min(0,a_min[2])), (0, 0, max(0,a_max[2])),
921
thickness, color="blue"))
922
X += sum(A).translate([-z for z in T])
923
924
return X
925
926
def _process_viewing_options(self, kwds):
927
"""
928
Process viewing options (the keywords passed to show()) and return a new
929
dictionary. Defaults will be filled in for missing options and taken from
930
self._extra_kwds as well. Options that have the value "automatic" will be
931
automatically determined. Finally, the provided dictionary is modified
932
to remove all of the keys that were used -- so that the unused keywords
933
can be used elsewhere.
934
"""
935
opts = {}
936
opts.update(SHOW_DEFAULTS)
937
if self._extra_kwds is not None:
938
opts.update(self._extra_kwds)
939
opts.update(kwds)
940
941
# Remove all of the keys that are viewing options, since the remaining
942
# kwds might be passed on.
943
for key_to_remove in SHOW_DEFAULTS.keys():
944
kwds.pop(key_to_remove, None)
945
946
if opts['aspect_ratio'] == 1:
947
opts['aspect_ratio'] = (1, 1, 1)
948
if not isinstance(opts['aspect_ratio'], (str, list, tuple)):
949
raise TypeError, 'aspect ratio must be a string, list, tuple, or 1'
950
# deal with any aspect_ratio instances passed from the default options to plot
951
if opts['aspect_ratio'] == 'auto':
952
opts['aspect_ratio'] = 'automatic'
953
954
if opts['frame_aspect_ratio'] == 'automatic':
955
if opts['aspect_ratio'] != 'automatic':
956
# Set the aspect_ratio of the frame to be the same as that of
957
# the object we are rendering given the aspect_ratio we'll use
958
# for it.
959
opts['frame_aspect_ratio'] = \
960
self._determine_frame_aspect_ratio(opts['aspect_ratio'])
961
else:
962
opts['frame_aspect_ratio'] = self.frame_aspect_ratio()
963
964
if not isinstance(opts['figsize'], (list,tuple)):
965
opts['figsize'] = [opts['figsize'], opts['figsize']]
966
967
return opts
968
969
def show(self, **kwds):
970
"""
971
INPUT:
972
973
974
- ``viewer`` - string (default: 'jmol'), how to view
975
the plot
976
977
* 'jmol': Interactive 3D viewer using Java
978
979
* 'tachyon': Ray tracer generates a static PNG image
980
981
* 'java3d': Interactive OpenGL based 3D
982
983
* 'canvas3d': Web-based 3D viewer powered by JavaScript and
984
<canvas> (notebook only)
985
986
- ``filename`` - string (default: a temp file); file
987
to save the image to
988
989
- ``verbosity`` - display information about rendering
990
the figure
991
992
- ``figsize`` - (default: 5); x or pair [x,y] for
993
numbers, e.g., [5,5]; controls the size of the output figure. E.g.,
994
with Tachyon the number of pixels in each direction is 100 times
995
figsize[0]. This is ignored for the jmol embedded renderer.
996
997
- ``aspect_ratio`` - (default: "automatic") - aspect
998
ratio of the coordinate system itself. Give [1,1,1] to make spheres
999
look round.
1000
1001
- ``frame_aspect_ratio`` - (default: "automatic")
1002
aspect ratio of frame that contains the 3d scene.
1003
1004
- ``zoom`` - (default: 1) how zoomed in
1005
1006
- ``frame`` - (default: True) if True, draw a
1007
bounding frame with labels
1008
1009
- ``axes`` - (default: False) if True, draw coordinate
1010
axes
1011
1012
1013
- ``**kwds`` - other options, which make sense for particular
1014
rendering engines
1015
1016
CHANGING DEFAULTS: Defaults can be uniformly changed by importing a
1017
dictionary and changing it. For example, here we change the default
1018
so images display without a frame instead of with one::
1019
1020
sage: from sage.plot.plot3d.base import SHOW_DEFAULTS
1021
sage: SHOW_DEFAULTS['frame'] = False
1022
1023
This sphere will not have a frame around it::
1024
1025
sage: sphere((0,0,0))
1026
1027
We change the default back::
1028
1029
sage: SHOW_DEFAULTS['frame'] = True
1030
1031
Now this sphere is enclosed in a frame::
1032
1033
sage: sphere((0,0,0))
1034
1035
EXAMPLES: We illustrate use of the ``aspect_ratio`` option::
1036
1037
sage: x, y = var('x,y')
1038
sage: p = plot3d(2*sin(x*y), (x, -pi, pi), (y, -pi, pi))
1039
sage: p.show(aspect_ratio=[1,1,1])
1040
1041
This looks flattened, but filled with the plot::
1042
1043
sage: p.show(frame_aspect_ratio=[1,1,1/16])
1044
1045
This looks flattened, but the plot is square and smaller::
1046
1047
sage: p.show(aspect_ratio=[1,1,1], frame_aspect_ratio=[1,1,1/8])
1048
1049
This example shows indirectly that the defaults
1050
from :func:`~sage.plot.plot.plot` are dealt with properly::
1051
1052
sage: plot(vector([1,2,3]))
1053
1054
We use the 'canvas3d' backend from inside the notebook to get a view of
1055
the plot rendered inline using HTML canvas::
1056
1057
sage: p.show(viewer='canvas3d')
1058
"""
1059
1060
opts = self._process_viewing_options(kwds)
1061
1062
viewer = opts['viewer']
1063
verbosity = opts['verbosity']
1064
figsize = opts['figsize']
1065
aspect_ratio = opts['aspect_ratio']
1066
frame_aspect_ratio = opts['frame_aspect_ratio']
1067
zoom = opts['zoom']
1068
frame = opts['frame']
1069
axes = opts['axes']
1070
1071
import sage.misc.misc
1072
if 'filename' in kwds:
1073
filename = kwds['filename']
1074
del kwds['filename']
1075
else:
1076
filename = sage.misc.misc.tmp_filename()
1077
1078
from sage.plot.plot import EMBEDDED_MODE, DOCTEST_MODE
1079
ext = None
1080
1081
# Tachyon resolution options
1082
if DOCTEST_MODE:
1083
opts = '-res 10 10'
1084
filename = sage.misc.misc.SAGE_TMP + "/tmp"
1085
elif EMBEDDED_MODE:
1086
opts = '-res %s %s'%(figsize[0]*100, figsize[1]*100)
1087
filename = sage.misc.misc.graphics_filename()[:-4]
1088
else:
1089
opts = '-res %s %s'%(figsize[0]*100, figsize[1]*100)
1090
1091
if DOCTEST_MODE or viewer=='tachyon' or (viewer=='java3d' and EMBEDDED_MODE):
1092
T = self._prepare_for_tachyon(frame, axes, frame_aspect_ratio, aspect_ratio, zoom)
1093
tachyon_rt(T.tachyon(), filename+".png", verbosity, True, opts)
1094
ext = "png"
1095
import sage.misc.viewer
1096
viewer_app = sage.misc.viewer.browser()
1097
1098
if DOCTEST_MODE or viewer=='java3d':
1099
f = open(filename+".obj", "w")
1100
f.write("mtllib %s.mtl\n" % filename)
1101
f.write(self.obj())
1102
f.close()
1103
f = open(filename+".mtl", "w")
1104
f.write(self.mtl_str())
1105
f.close()
1106
ext = "obj"
1107
viewer_app = os.path.join(sage.misc.misc.SAGE_LOCAL, "bin/sage3d")
1108
1109
if DOCTEST_MODE or viewer=='jmol':
1110
# Temporary hack: encode the desired applet size in the end of the filename:
1111
# (This will be removed once we have dynamic resizing of applets in the browser.)
1112
base, ext = os.path.splitext(filename)
1113
fg = figsize[0]
1114
#if fg >= 2:
1115
# fg = 2
1116
filename = '%s-size%s%s'%(base, fg*100, ext)
1117
ext = "jmol"
1118
archive_name = "%s.%s.zip" % (filename, ext)
1119
if EMBEDDED_MODE:
1120
# jmol doesn't seem to correctly parse the ?params part of a URL
1121
archive_name = "%s-%s.%s.zip" % (filename, randint(0, 1 << 30), ext)
1122
1123
T = self._prepare_for_jmol(frame, axes, frame_aspect_ratio, aspect_ratio, zoom)
1124
T.export_jmol(archive_name, force_reload=EMBEDDED_MODE, zoom=zoom*100, **kwds)
1125
viewer_app = os.path.join(sage.misc.misc.SAGE_LOCAL, "bin/jmol")
1126
1127
# We need a script to load the file
1128
f = open(filename + '.jmol', 'w')
1129
f.write('set defaultdirectory "%s"\n' % archive_name)
1130
f.write('script SCRIPT\n')
1131
f.close()
1132
1133
if viewer == 'canvas3d':
1134
T = self._prepare_for_tachyon(frame, axes, frame_aspect_ratio, aspect_ratio, zoom)
1135
data = flatten_list(T.json_repr(T.default_render_params()))
1136
f = open(filename + '.canvas3d', 'w')
1137
f.write('[%s]' % ','.join(data))
1138
f.close()
1139
ext = 'canvas3d'
1140
1141
if ext is None:
1142
raise ValueError, "Unknown 3d plot type: %s" % viewer
1143
1144
if not DOCTEST_MODE and not EMBEDDED_MODE:
1145
if verbosity:
1146
pipes = "2>&1"
1147
else:
1148
pipes = "2>/dev/null 1>/dev/null &"
1149
os.system('%s "%s.%s" %s' % (viewer_app, filename, ext, pipes))
1150
1151
def save(self, filename, **kwds):
1152
"""
1153
Save the graphic to an image file (of type: PNG, BMP, GIF, PPM, or TIFF)
1154
rendered using Tachyon, or pickle it (stored as an SOBJ so you can load it
1155
later) depending on the file extension you give the filename.
1156
1157
INPUT:
1158
1159
- ``filename`` - Specify where to save the image or object.
1160
1161
- ``**kwds`` - When specifying an image file to be rendered by Tachyon,
1162
any of the viewing options accepted by show() are valid as keyword
1163
arguments to this function and they will behave in the same way.
1164
Accepted keywords include: ``viewer``, ``verbosity``, ``figsize``,
1165
``aspect_ratio``, ``frame_aspect_ratio``, ``zoom``, ``frame``, and
1166
``axes``. Default values are provided.
1167
1168
EXAMPLES::
1169
1170
sage: f = tmp_filename() + '.png'
1171
sage: G = sphere()
1172
sage: G.save(f)
1173
1174
We demonstrate using keyword arguments to control the appearance of the
1175
output image::
1176
1177
sage: G.save(f, zoom=2, figsize=[5, 10])
1178
1179
But some extra parameters don't make since (like ``viewer``, since
1180
rendering is done using Tachyon only). They will be ignored::
1181
1182
sage: G.save(f, viewer='jmol') # Looks the same
1183
1184
Since Tachyon only outputs PNG images, PIL will be used to convert to
1185
alternate formats::
1186
1187
sage: cube().save(tmp_filename() + '.gif')
1188
"""
1189
ext = os.path.splitext(filename)[1].lower()
1190
if ext == '' or ext == '.sobj':
1191
SageObject.save(self, filename)
1192
return
1193
elif ext in ['.bmp', '.png', '.gif', '.ppm', '.tiff', '.tif', '.jpg', '.jpeg']:
1194
opts = self._process_viewing_options(kwds)
1195
T = self._prepare_for_tachyon(
1196
opts['frame'], opts['axes'], opts['frame_aspect_ratio'],
1197
opts['aspect_ratio'], opts['zoom']
1198
)
1199
1200
if ext == 'png':
1201
# No conversion is necessary
1202
out_filename = filename
1203
else:
1204
# Save to a temporary file, and then convert using PIL
1205
out_filename = sage.misc.misc.tmp_filename()
1206
tachyon_rt(T.tachyon(), out_filename, opts['verbosity'], True,
1207
'-res %s %s' % (opts['figsize'][0]*100, opts['figsize'][1]*100))
1208
if ext != 'png':
1209
import Image
1210
Image.open(out_filename).save(filename)
1211
else:
1212
raise ValueError, 'filetype not supported by save()'
1213
1214
1215
1216
# if you add any default parameters you must update some code below
1217
SHOW_DEFAULTS = {'viewer':'jmol',
1218
'verbosity':0,
1219
'figsize':5,
1220
'aspect_ratio':"automatic",
1221
'frame_aspect_ratio':"automatic",
1222
'zoom':1,
1223
'frame':True,
1224
'axes':False}
1225
1226
1227
1228
1229
class Graphics3dGroup(Graphics3d):
1230
"""
1231
This class represents a collection of 3d objects. Usually they are formed
1232
implicitly by summing.
1233
"""
1234
def __init__(self, all=(), rot=None, trans=None, scale=None, T=None):
1235
"""
1236
EXAMPLES::
1237
1238
sage: sage.plot.plot3d.base.Graphics3dGroup([icosahedron(), dodecahedron(opacity=.5)])
1239
sage: type(icosahedron() + dodecahedron(opacity=.5))
1240
<class 'sage.plot.plot3d.base.Graphics3dGroup'>
1241
"""
1242
self.all = list(all)
1243
self.frame_aspect_ratio(optimal_aspect_ratios([a.frame_aspect_ratio() for a in all]))
1244
self.aspect_ratio(optimal_aspect_ratios([a.aspect_ratio() for a in all]))
1245
self._set_extra_kwds(optimal_extra_kwds([a._extra_kwds for a in all if a._extra_kwds is not None]))
1246
1247
def __add__(self, other):
1248
"""
1249
We override this here to make large sums more efficient.
1250
1251
EXAMPLES::
1252
sage: G = sum(tetrahedron(opacity=1-t/11).translate(t, 0, 0) for t in range(10))
1253
sage: G
1254
sage: len(G.all)
1255
10
1256
"""
1257
if type(self) is Graphics3dGroup and isinstance(other, Graphics3d):
1258
self.all.append(other)
1259
return self
1260
else:
1261
return Graphics3d.__add__(self, other)
1262
1263
def bounding_box(self):
1264
"""
1265
Box that contains the bounding boxes of
1266
all the objects that make up self.
1267
1268
EXAMPLES::
1269
1270
sage: A = sphere((0,0,0), 5)
1271
sage: B = sphere((1, 5, 10), 1)
1272
sage: A.bounding_box()
1273
((-5.0, -5.0, -5.0), (5.0, 5.0, 5.0))
1274
sage: B.bounding_box()
1275
((0.0, 4.0, 9.0), (2.0, 6.0, 11.0))
1276
sage: (A+B).bounding_box()
1277
((-5.0, -5.0, -5.0), (5.0, 6.0, 11.0))
1278
sage: (A+B).show(aspect_ratio=1, frame=True)
1279
1280
sage: sage.plot.plot3d.base.Graphics3dGroup([]).bounding_box()
1281
((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
1282
"""
1283
if len(self.all) == 0:
1284
return Graphics3d.bounding_box(self)
1285
v = [obj.bounding_box() for obj in self.all]
1286
return min3([a[0] for a in v]), max3([a[1] for a in v])
1287
1288
def transform(self, **kwds):
1289
"""
1290
Transforming this entire group simply makes a transform group with
1291
the same contents.
1292
1293
EXAMPLES::
1294
1295
sage: G = dodecahedron(color='red', opacity=.5) + icosahedron(color='blue')
1296
sage: G
1297
sage: G.transform(scale=(2,1/2,1))
1298
sage: G.transform(trans=(1,1,3))
1299
"""
1300
T = TransformGroup(self.all, **kwds)
1301
T._set_extra_kwds(self._extra_kwds)
1302
return T
1303
1304
def set_texture(self, **kwds):
1305
"""
1306
EXAMPLES::
1307
1308
sage: G = dodecahedron(color='red', opacity=.5) + icosahedron((3, 0, 0), color='blue')
1309
sage: G
1310
sage: G.set_texture(color='yellow')
1311
sage: G
1312
"""
1313
for g in self.all:
1314
g.set_texture(**kwds)
1315
1316
def json_repr(self, render_params):
1317
"""
1318
The JSON representation of a group is simply the concatenation of the
1319
representations of its objects.
1320
1321
EXAMPLES::
1322
1323
sage: G = sphere() + sphere((1, 2, 3))
1324
sage: G.json_repr(G.default_render_params())
1325
[[["{vertices:..."]], [["{vertices:..."]]]
1326
"""
1327
return [g.json_repr(render_params) for g in self.all]
1328
1329
def tachyon_repr(self, render_params):
1330
"""
1331
The tachyon representation of a group is simply the concatenation of
1332
the representations of its objects.
1333
1334
EXAMPLES::
1335
1336
sage: G = sphere() + sphere((1,2,3))
1337
sage: G.tachyon_repr(G.default_render_params())
1338
[['Sphere center 0.0 0.0 0.0 Rad 1.0 texture...'],
1339
['Sphere center 1.0 2.0 3.0 Rad 1.0 texture...']]
1340
"""
1341
return [g.tachyon_repr(render_params) for g in self.all]
1342
1343
def x3d_str(self):
1344
"""
1345
The x3d representation of a group is simply the concatenation of
1346
the representation of its objects.
1347
1348
EXAMPLES::
1349
1350
sage: G = sphere() + sphere((1,2,3))
1351
sage: print G.x3d_str()
1352
<Transform translation='0 0 0'>
1353
<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
1354
</Transform>
1355
<Transform translation='1 2 3'>
1356
<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
1357
</Transform>
1358
"""
1359
return "\n".join([g.x3d_str() for g in self.all])
1360
1361
def obj_repr(self, render_params):
1362
"""
1363
The obj representation of a group is simply the concatenation of
1364
the representation of its objects.
1365
1366
EXAMPLES::
1367
1368
sage: G = tetrahedron() + tetrahedron().translate(10, 10, 10)
1369
sage: G.obj_repr(G.default_render_params())
1370
[['g obj_1',
1371
'usemtl ...',
1372
['v 0 0 1',
1373
'v 0.942809 0 -0.333333',
1374
'v -0.471405 0.816497 -0.333333',
1375
'v -0.471405 -0.816497 -0.333333'],
1376
['f 1 2 3', 'f 2 4 3', 'f 1 3 4', 'f 1 4 2'],
1377
[]],
1378
[['g obj_2',
1379
'usemtl ...',
1380
['v 10 10 11',
1381
'v 10.9428 10 9.66667',
1382
'v 9.5286 10.8165 9.66667',
1383
'v 9.5286 9.1835 9.66667'],
1384
['f 5 6 7', 'f 6 8 7', 'f 5 7 8', 'f 5 8 6'],
1385
[]]]]
1386
"""
1387
return [g.obj_repr(render_params) for g in self.all]
1388
1389
def jmol_repr(self, render_params):
1390
r"""
1391
The jmol representation of a group is simply the concatenation of
1392
the representation of its objects.
1393
1394
EXAMPLES::
1395
1396
sage: G = sphere() + sphere((1,2,3))
1397
sage: G.jmol_repr(G.default_render_params())
1398
[[['isosurface sphere_1 center {0.0 0.0 0.0} sphere 1.0\ncolor isosurface [102,102,255]']],
1399
[['isosurface sphere_2 center {1.0 2.0 3.0} sphere 1.0\ncolor isosurface [102,102,255]']]]
1400
"""
1401
return [g.jmol_repr(render_params) for g in self.all]
1402
1403
def texture_set(self):
1404
"""
1405
The texture set of a group is simply the union of the textures of
1406
all its objects.
1407
1408
EXAMPLES::
1409
1410
sage: G = sphere(color='red') + sphere(color='yellow')
1411
sage: [t for t in G.texture_set() if t.color == colors.red] # one red texture
1412
[Texture(texture..., red, ff0000)]
1413
sage: [t for t in G.texture_set() if t.color == colors.yellow] # one yellow texture
1414
[Texture(texture..., yellow, ffff00)]
1415
1416
sage: T = sage.plot.plot3d.texture.Texture('blue'); T
1417
Texture(texture..., blue, 0000ff)
1418
sage: G = sphere(texture=T) + sphere((1, 1, 1), texture=T)
1419
sage: len(G.texture_set())
1420
1
1421
"""
1422
return reduce(set.union, [g.texture_set() for g in self.all])
1423
1424
def flatten(self):
1425
"""
1426
Try to reduce the depth of the scene tree by consolidating groups
1427
and transformations.
1428
1429
EXAMPLES::
1430
1431
sage: G = sum([circle((0, 0), t) for t in [1..10]], sphere()); G
1432
sage: G.flatten()
1433
sage: len(G.all)
1434
2
1435
sage: len(G.flatten().all)
1436
11
1437
"""
1438
if len(self.all) == 1:
1439
return self.all[0].flatten()
1440
all = []
1441
for g in self.all:
1442
g = g.flatten()
1443
if type(g) is Graphics3dGroup:
1444
all += g.all
1445
else:
1446
all.append(g)
1447
return Graphics3dGroup(all)
1448
1449
1450
1451
class TransformGroup(Graphics3dGroup):
1452
"""
1453
This class is a container for a group of objects with a common transformation.
1454
"""
1455
def __init__(self, all=[], rot=None, trans=None, scale=None, T=None):
1456
"""
1457
EXAMPLES::
1458
1459
sage: sage.plot.plot3d.base.TransformGroup([sphere()], trans=(1,2,3)) + point3d((0,0,0))
1460
1461
The are usually constructed implicitly::
1462
1463
sage: type(sphere((1,2,3)))
1464
<class 'sage.plot.plot3d.base.TransformGroup'>
1465
sage: type(dodecahedron().scale(2))
1466
<class 'sage.plot.plot3d.base.TransformGroup'>
1467
1468
"""
1469
Graphics3dGroup.__init__(self, all)
1470
self._rot = rot
1471
self._trans = trans
1472
if scale is not None and len(scale) == 1:
1473
if isinstance(scale, (tuple, list)):
1474
scale = scale[0]
1475
scale = (scale, scale, scale)
1476
self._scale = scale
1477
if T is not None:
1478
self.T = T
1479
self.frame_aspect_ratio(optimal_aspect_ratios([a.frame_aspect_ratio() for a in all]))
1480
self.aspect_ratio(optimal_aspect_ratios([a.aspect_ratio() for a in all]))
1481
self._set_extra_kwds(optimal_extra_kwds([a._extra_kwds for a in all if a._extra_kwds is not None]))
1482
1483
def bounding_box(self):
1484
"""
1485
Returns the bounding box of self, i.e. the box containing the
1486
contents of self after applying the transformation.
1487
1488
EXAMPLES::
1489
1490
sage: G = cube()
1491
sage: G.bounding_box()
1492
((-0.5, -0.5, -0.5), (0.5, 0.5, 0.5))
1493
sage: G.scale(4).bounding_box()
1494
((-2.0, -2.0, -2.0), (2.0, 2.0, 2.0))
1495
sage: G.rotateZ(pi/4).bounding_box()
1496
((-0.7071067811865475, -0.7071067811865475, -0.5),
1497
(0.7071067811865475, 0.7071067811865475, 0.5))
1498
"""
1499
try:
1500
return self._bounding_box
1501
except AttributeError:
1502
pass
1503
1504
cdef Transformation T = self.get_transformation()
1505
w = sum([T.transform_bounding_box(obj.bounding_box()) for obj in self.all], ())
1506
self._bounding_box = point_list_bounding_box(w)
1507
return self._bounding_box
1508
1509
def x3d_str(self):
1510
r"""
1511
To apply a transformation to a set of objects in x3d, simply make them
1512
all children of an x3d Transform node.
1513
1514
EXAMPLES::
1515
1516
sage: sphere((1,2,3)).x3d_str()
1517
"<Transform translation='1 2 3'>\n<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>\n\n</Transform>"
1518
"""
1519
s = "<Transform"
1520
if self._rot is not None:
1521
s += " rotation='%s %s %s %s'"%tuple(self._rot)
1522
if self._trans is not None:
1523
s += " translation='%s %s %s'"%tuple(self._trans)
1524
if self._scale is not None:
1525
s += " scale='%s %s %s'"%tuple(self._scale)
1526
s += ">\n"
1527
s += Graphics3dGroup.x3d_str(self)
1528
s += "\n</Transform>"
1529
return s
1530
1531
def json_repr(self, render_params):
1532
"""
1533
Transformations are applied at the leaf nodes.
1534
1535
EXAMPLES::
1536
1537
sage: G = cube().rotateX(0.2)
1538
sage: G.json_repr(G.default_render_params())
1539
[["{vertices:[{x:0.5,y:0.589368,z:0.390699},..."]]
1540
"""
1541
1542
render_params.push_transform(self.get_transformation())
1543
rep = [g.json_repr(render_params) for g in self.all]
1544
render_params.pop_transform()
1545
return rep
1546
1547
def tachyon_repr(self, render_params):
1548
"""
1549
Transformations for Tachyon are applied at the leaf nodes.
1550
1551
EXAMPLES::
1552
1553
sage: G = sphere((1,2,3)).scale(2)
1554
sage: G.tachyon_repr(G.default_render_params())
1555
[['Sphere center 2.0 4.0 6.0 Rad 2.0 texture...']]
1556
"""
1557
render_params.push_transform(self.get_transformation())
1558
rep = [g.tachyon_repr(render_params) for g in self.all]
1559
render_params.pop_transform()
1560
return rep
1561
1562
def obj_repr(self, render_params):
1563
"""
1564
Transformations for .obj files are applied at the leaf nodes.
1565
1566
EXAMPLES::
1567
1568
sage: G = cube().scale(4).translate(1, 2, 3)
1569
sage: G.obj_repr(G.default_render_params())
1570
[[['g obj_1',
1571
'usemtl ...',
1572
['v 3 4 5',
1573
'v -1 4 5',
1574
'v -1 0 5',
1575
'v 3 0 5',
1576
'v 3 4 1',
1577
'v -1 4 1',
1578
'v 3 0 1',
1579
'v -1 0 1'],
1580
['f 1 2 3 4',
1581
'f 1 5 6 2',
1582
'f 1 4 7 5',
1583
'f 6 5 7 8',
1584
'f 7 4 3 8',
1585
'f 3 2 6 8'],
1586
[]]]]
1587
"""
1588
render_params.push_transform(self.get_transformation())
1589
rep = [g.obj_repr(render_params) for g in self.all]
1590
render_params.pop_transform()
1591
return rep
1592
1593
def jmol_repr(self, render_params):
1594
r"""
1595
Transformations for jmol are applied at the leaf nodes.
1596
1597
EXAMPLES::
1598
1599
sage: G = sphere((1,2,3)).scale(2)
1600
sage: G.jmol_repr(G.default_render_params())
1601
[[['isosurface sphere_1 center {2.0 4.0 6.0} sphere 2.0\ncolor isosurface [102,102,255]']]]
1602
"""
1603
render_params.push_transform(self.get_transformation())
1604
rep = [g.jmol_repr(render_params) for g in self.all]
1605
render_params.pop_transform()
1606
return rep
1607
1608
def get_transformation(self):
1609
"""
1610
Returns the actual transformation object associated with self.
1611
1612
EXAMPLES::
1613
1614
sage: G = sphere().scale(100)
1615
sage: T = G.get_transformation()
1616
sage: T.get_matrix()
1617
[100.0 0.0 0.0 0.0]
1618
[ 0.0 100.0 0.0 0.0]
1619
[ 0.0 0.0 100.0 0.0]
1620
[ 0.0 0.0 0.0 1.0]
1621
"""
1622
try:
1623
return self.T
1624
except AttributeError:
1625
self.T = Transformation(self._scale, self._rot, self._trans)
1626
return self.T
1627
1628
def flatten(self):
1629
"""
1630
Try to reduce the depth of the scene tree by consolidating groups
1631
and transformations.
1632
1633
EXAMPLES::
1634
1635
sage: G = sphere((1,2,3)).scale(100)
1636
sage: T = G.get_transformation()
1637
sage: T.get_matrix()
1638
[100.0 0.0 0.0 0.0]
1639
[ 0.0 100.0 0.0 0.0]
1640
[ 0.0 0.0 100.0 0.0]
1641
[ 0.0 0.0 0.0 1.0]
1642
1643
sage: G.flatten().get_transformation().get_matrix()
1644
[100.0 0.0 0.0 100.0]
1645
[ 0.0 100.0 0.0 200.0]
1646
[ 0.0 0.0 100.0 300.0]
1647
[ 0.0 0.0 0.0 1.0]
1648
"""
1649
G = Graphics3dGroup.flatten(self)
1650
if isinstance(G, TransformGroup):
1651
return TransformGroup(G.all, T=self.get_transformation() * G.get_transformation())
1652
elif isinstance(G, Graphics3dGroup):
1653
return TransformGroup(G.all, T=self.get_transformation())
1654
else:
1655
return TransformGroup([G], T=self.get_transformation())
1656
1657
def transform(self, **kwds):
1658
"""
1659
Transforming this entire group can be done by composing transformations.
1660
1661
EXAMPLES::
1662
1663
sage: G = dodecahedron(color='red', opacity=.5) + icosahedron(color='blue')
1664
sage: G
1665
sage: G.transform(scale=(2,1/2,1))
1666
sage: G.transform(trans=(1,1,3))
1667
"""
1668
return Graphics3d.transform(self, **kwds)
1669
1670
class Viewpoint(Graphics3d):
1671
"""
1672
This class represents a viewpoint, necessary for x3d.
1673
1674
In the future, there could be multiple viewpoints, and they could have
1675
more properties. (Currently they only hold a position).
1676
"""
1677
def __init__(self, *x):
1678
"""
1679
EXAMPLES::
1680
1681
sage: sage.plot.plot3d.base.Viewpoint(1, 2, 4).x3d_str()
1682
"<Viewpoint position='1 2 4'/>"
1683
"""
1684
if isinstance(x[0], (tuple, list)):
1685
x = tuple(x[0])
1686
self.pos = x
1687
1688
def x3d_str(self):
1689
"""
1690
EXAMPLES::
1691
1692
sage: sphere((0,0,0), 100).viewpoint().x3d_str()
1693
"<Viewpoint position='0 0 6'/>"
1694
"""
1695
return "<Viewpoint position='%s %s %s'/>"%self.pos
1696
1697
1698
1699
cdef class PrimitiveObject(Graphics3d):
1700
"""
1701
This is the base class for the non-container 3d objects.
1702
"""
1703
def __init__(self, **kwds):
1704
if kwds.has_key('texture'):
1705
self.texture = kwds['texture']
1706
if not is_Texture(self.texture):
1707
self.texture = Texture(self.texture)
1708
else:
1709
self.texture = Texture(kwds)
1710
1711
def set_texture(self, texture=None, **kwds):
1712
"""
1713
EXAMPLES::
1714
1715
sage: G = dodecahedron(color='red'); G
1716
sage: G.set_texture(color='yellow'); G
1717
"""
1718
if not is_Texture(texture):
1719
texture = Texture(texture, **kwds)
1720
self.texture = texture
1721
1722
def get_texture(self):
1723
"""
1724
EXAMPLES::
1725
1726
sage: G = dodecahedron(color='red')
1727
sage: G.get_texture()
1728
Texture(texture..., red, ff0000)
1729
"""
1730
return self.texture
1731
1732
def texture_set(self):
1733
"""
1734
EXAMPLES::
1735
1736
sage: G = dodecahedron(color='red')
1737
sage: G.texture_set()
1738
set([Texture(texture..., red, ff0000)])
1739
"""
1740
return set([self.texture])
1741
1742
def x3d_str(self):
1743
r"""
1744
EXAMPLES::
1745
1746
sage: sphere().flatten().x3d_str()
1747
"<Transform>\n<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1' specularColor='0.0 0.0 0.0'/></Appearance></Shape>\n\n</Transform>"
1748
"""
1749
return "<Shape>" + self.x3d_geometry() + self.texture.x3d_str() + "</Shape>\n"
1750
1751
def tachyon_repr(self, render_params):
1752
"""
1753
Default behavior is to render the triangulation.
1754
1755
EXAMPLES::
1756
1757
sage: from sage.plot.plot3d.shapes import Torus
1758
sage: G = Torus(1, .5)
1759
sage: G.tachyon_repr(G.default_render_params())
1760
['TRI V0 0 1 0.5
1761
...
1762
'texture...']
1763
"""
1764
return self.triangulation().tachyon_repr(render_params)
1765
1766
def obj_repr(self, render_params):
1767
"""
1768
Default behavior is to render the triangulation.
1769
1770
EXAMPLES::
1771
1772
sage: from sage.plot.plot3d.shapes import Torus
1773
sage: G = Torus(1, .5)
1774
sage: G.obj_repr(G.default_render_params())
1775
['g obj_1',
1776
'usemtl ...',
1777
['v 0 1 0.5',
1778
...
1779
'f ...'],
1780
[]]
1781
"""
1782
return self.triangulation().obj_repr(render_params)
1783
1784
def jmol_repr(self, render_params):
1785
r"""
1786
Default behavior is to render the triangulation. The actual polygon
1787
data is stored in a separate file.
1788
1789
EXAMPLES::
1790
1791
sage: from sage.plot.plot3d.shapes import Torus
1792
sage: G = Torus(1, .5)
1793
sage: G.jmol_repr(G.testing_render_params())
1794
['pmesh obj_1 "obj_1.pmesh"\ncolor pmesh [102,102,255]']
1795
"""
1796
return self.triangulation().jmol_repr(render_params)
1797
1798
1799
1800
class BoundingSphere(SageObject):
1801
"""
1802
A bounding sphere is like a bounding box, but is simpler to deal with and
1803
behaves better under rotations.
1804
"""
1805
def __init__(self, cen, r):
1806
"""
1807
EXAMPLES::
1808
1809
sage: from sage.plot.plot3d.base import BoundingSphere
1810
sage: BoundingSphere((0,0,0), 1)
1811
Center (0.0, 0.0, 0.0) radius 1
1812
sage: BoundingSphere((0,-1,5), 2)
1813
Center (0.0, -1.0, 5.0) radius 2
1814
"""
1815
self.cen = vector(RDF, cen)
1816
self.r = r
1817
1818
def __repr__(self):
1819
"""
1820
TESTS::
1821
1822
sage: from sage.plot.plot3d.base import BoundingSphere
1823
sage: BoundingSphere((0,-1,10), 2)
1824
Center (0.0, -1.0, 10.0) radius 2
1825
"""
1826
return "Center %s radius %s" % (self.cen, self.r)
1827
1828
def __add__(self, other):
1829
"""
1830
Returns the bounding sphere containing both terms.
1831
1832
EXAMPLES::
1833
1834
sage: from sage.plot.plot3d.base import BoundingSphere
1835
sage: BoundingSphere((0,0,0), 1) + BoundingSphere((0,0,0), 2)
1836
Center (0.0, 0.0, 0.0) radius 2
1837
sage: BoundingSphere((0,0,0), 1) + BoundingSphere((0,0,100), 1)
1838
Center (0.0, 0.0, 50.0) radius 51.0
1839
sage: BoundingSphere((0,0,0), 1) + BoundingSphere((1,1,1), 2)
1840
Center (0.788675134595, 0.788675134595, 0.788675134595) radius 2.36602540378
1841
1842
Treat None and 0 as the identity::
1843
1844
sage: BoundingSphere((1,2,3), 10) + None + 0
1845
Center (1.0, 2.0, 3.0) radius 10
1846
1847
"""
1848
if other == 0 or other is None:
1849
return self
1850
elif self == 0 or self is None:
1851
return other
1852
if self.cen == other.cen:
1853
return self if self.r > other.r else other
1854
diff = other.cen - self.cen
1855
dist = (diff[0]*diff[0] + diff[1]*diff[1] + diff[2]*diff[2]).sqrt()
1856
diam = dist + self.r + other.r
1857
off = diam/2 - self.r
1858
return BoundingSphere(self.cen + (off/dist)*diff, diam/2)
1859
1860
def transform(self, T):
1861
"""
1862
Returns the bounding sphere of this sphere acted on by T. This always
1863
returns a new sphere, even if the resulting object is an ellipsoid.
1864
1865
EXAMPLES::
1866
1867
sage: from sage.plot.plot3d.transform import Transformation
1868
sage: from sage.plot.plot3d.base import BoundingSphere
1869
sage: BoundingSphere((0,0,0), 10).transform(Transformation(trans=(1,2,3)))
1870
Center (1.0, 2.0, 3.0) radius 10.0
1871
sage: BoundingSphere((0,0,0), 10).transform(Transformation(scale=(1/2, 1, 2)))
1872
Center (0.0, 0.0, 0.0) radius 20.0
1873
sage: BoundingSphere((0,0,3), 10).transform(Transformation(scale=(2, 2, 2)))
1874
Center (0.0, 0.0, 6.0) radius 20.0
1875
"""
1876
return BoundingSphere(T.transform_point(self.cen), self.r * T.max_scale())
1877
1878
1879
class RenderParams(SageObject):
1880
"""
1881
This class is a container for all parameters that may be needed to
1882
render triangulate/render an object to a certain format. It can
1883
contain both cumulative and global parameters.
1884
1885
Of particular note is the transformation object, which holds the
1886
cumulative transformation from the root of the scene graph to this
1887
node in the tree.
1888
"""
1889
1890
_uniq_counter = 0
1891
randomize_counter = 0
1892
force_reload = False
1893
mesh = False
1894
dots = False
1895
antialiasing = 8
1896
1897
def __init__(self, **kwds):
1898
"""
1899
EXAMPLES::
1900
1901
sage: params = sage.plot.plot3d.base.RenderParams(foo='x')
1902
sage: params.transform_list
1903
[]
1904
sage: params.foo
1905
'x'
1906
"""
1907
self.output_file = sage.misc.misc.tmp_filename()
1908
self.obj_vertex_offset = 1
1909
self.transform_list = []
1910
self.transform = None
1911
self.ds = 1
1912
self.crease_threshold = .8
1913
self.__dict__.update(kwds)
1914
# for jmol, some things (such as labels) must be attached to atoms
1915
self.atom_list = []
1916
1917
def push_transform(self, T):
1918
"""
1919
Push a transformation onto the stack, updating self.transform.
1920
1921
EXAMPLES::
1922
1923
sage: from sage.plot.plot3d.transform import Transformation
1924
sage: params = sage.plot.plot3d.base.RenderParams()
1925
sage: params.transform is None
1926
True
1927
sage: T = Transformation(scale=(10,20,30))
1928
sage: params.push_transform(T)
1929
sage: params.transform.get_matrix()
1930
[10.0 0.0 0.0 0.0]
1931
[ 0.0 20.0 0.0 0.0]
1932
[ 0.0 0.0 30.0 0.0]
1933
[ 0.0 0.0 0.0 1.0]
1934
sage: params.push_transform(T) # scale again
1935
sage: params.transform.get_matrix()
1936
[100.0 0.0 0.0 0.0]
1937
[ 0.0 400.0 0.0 0.0]
1938
[ 0.0 0.0 900.0 0.0]
1939
[ 0.0 0.0 0.0 1.0]
1940
"""
1941
self.transform_list.append(self.transform)
1942
if self.transform is None:
1943
self.transform = T
1944
else:
1945
self.transform = self.transform * T
1946
1947
def pop_transform(self):
1948
"""
1949
Remove the last transformation off the stack, resetting self.transform
1950
to the previous value.
1951
1952
EXAMPLES::
1953
1954
sage: from sage.plot.plot3d.transform import Transformation
1955
sage: params = sage.plot.plot3d.base.RenderParams()
1956
sage: T = Transformation(trans=(100, 500, 0))
1957
sage: params.push_transform(T)
1958
sage: params.transform.get_matrix()
1959
[ 1.0 0.0 0.0 100.0]
1960
[ 0.0 1.0 0.0 500.0]
1961
[ 0.0 0.0 1.0 0.0]
1962
[ 0.0 0.0 0.0 1.0]
1963
sage: params.push_transform(Transformation(trans=(-100, 500, 200)))
1964
sage: params.transform.get_matrix()
1965
[ 1.0 0.0 0.0 0.0]
1966
[ 0.0 1.0 0.0 1000.0]
1967
[ 0.0 0.0 1.0 200.0]
1968
[ 0.0 0.0 0.0 1.0]
1969
sage: params.pop_transform()
1970
sage: params.transform.get_matrix()
1971
[ 1.0 0.0 0.0 100.0]
1972
[ 0.0 1.0 0.0 500.0]
1973
[ 0.0 0.0 1.0 0.0]
1974
[ 0.0 0.0 0.0 1.0]
1975
1976
"""
1977
self.transform = self.transform_list.pop()
1978
1979
def unique_name(self, desc="name"):
1980
"""
1981
Returns a unique identifier starting with desc.
1982
1983
EXAMPLES::
1984
1985
sage: params = sage.plot.plot3d.base.RenderParams()
1986
sage: params.unique_name()
1987
'name_1'
1988
sage: params.unique_name()
1989
'name_2'
1990
sage: params.unique_name('texture')
1991
'texture_3'
1992
"""
1993
if self.randomize_counter:
1994
self._uniq_counter = randint(1,1000000)
1995
else:
1996
self._uniq_counter += 1
1997
return "%s_%s" % (desc, self._uniq_counter)
1998
1999
def flatten_list(L):
2000
"""
2001
This is an optimized routine to turn a list of lists (of lists ...)
2002
into a single list. We generate data in a non-flat format to avoid
2003
multiple data copying, and then concatenate it all at the end.
2004
2005
This is NOT recursive, otherwise there would be a lot of redundant
2006
copying (which we are trying to avoid in the first place, though at
2007
least it would be just the pointers).
2008
2009
EXAMPLES::
2010
2011
sage: from sage.plot.plot3d.base import flatten_list
2012
sage: flatten_list([])
2013
[]
2014
sage: flatten_list([[[[]]]])
2015
[]
2016
sage: flatten_list([['a', 'b'], 'c'])
2017
['a', 'b', 'c']
2018
sage: flatten_list([['a'], [[['b'], 'c'], ['d'], [[['e', 'f', 'g']]]]])
2019
['a', 'b', 'c', 'd', 'e', 'f', 'g']
2020
"""
2021
if not PyList_CheckExact(L):
2022
return [L]
2023
flat = []
2024
L_stack = []; L_pop = L_stack.pop
2025
i_stack = []; i_pop = i_stack.pop
2026
cdef Py_ssize_t i = 0
2027
while i < PyList_GET_SIZE(L) or PyList_GET_SIZE(L_stack) > 0:
2028
while i < PyList_GET_SIZE(L):
2029
tmp = <object>PyList_GET_ITEM(L, i)
2030
if PyList_CheckExact(tmp):
2031
PyList_Append(L_stack, L)
2032
L = tmp
2033
PyList_Append(i_stack, i)
2034
i = 0
2035
else:
2036
PyList_Append(flat, tmp)
2037
i += 1
2038
if PyList_GET_SIZE(L_stack) > 0:
2039
L = L_pop()
2040
i = i_pop()
2041
i += 1
2042
return flat
2043
2044
2045
def min3(v):
2046
"""
2047
Return the componentwise minimum of a list of 3-tuples.
2048
2049
EXAMPLES::
2050
2051
sage: from sage.plot.plot3d.base import min3, max3
2052
sage: min3([(-1,2,5), (-3, 4, 2)])
2053
(-3, 2, 2)
2054
"""
2055
return tuple([min([a[i] for a in v]) for i in range(3)])
2056
2057
def max3(v):
2058
"""
2059
Return the componentwise maximum of a list of 3-tuples.
2060
2061
EXAMPLES::
2062
2063
sage: from sage.plot.plot3d.base import min3, max3
2064
sage: max3([(-1,2,5), (-3, 4, 2)])
2065
(-1, 4, 5)
2066
"""
2067
return tuple([max([a[i] for a in v]) for i in range(3)])
2068
2069
def point_list_bounding_box(v):
2070
"""
2071
EXAMPLES::
2072
2073
sage: from sage.plot.plot3d.base import point_list_bounding_box
2074
sage: point_list_bounding_box([(1,2,3),(4,5,6),(-10,0,10)])
2075
((-10.0, 0.0, 3.0), (4.0, 5.0, 10.0))
2076
sage: point_list_bounding_box([(float('nan'), float('inf'), float('-inf')), (10,0,10)])
2077
((10.0, 0.0, 10.0), (10.0, 0.0, 10.0))
2078
"""
2079
cdef point_c low, high, cur
2080
low.x, low.y, low.z = INFINITY, INFINITY, INFINITY
2081
high.x, high.y, high.z = -INFINITY, -INFINITY, -INFINITY
2082
2083
for P in v:
2084
cur.x, cur.y, cur.z = P
2085
point_c_update_finite_lower_bound(&low, cur)
2086
point_c_update_finite_upper_bound(&high, cur)
2087
return ((low.x, low.y, low.z), (high.x, high.y, high.z))
2088
2089
def optimal_aspect_ratios(ratios):
2090
# average the aspect ratios
2091
n = len(ratios)
2092
if n > 0:
2093
return [max([z[i] for z in ratios]) for i in range(3)]
2094
else:
2095
return [1.0,1.0,1.0]
2096
2097
def optimal_extra_kwds(v):
2098
"""
2099
Given a list v of dictionaries, this function merges them such that
2100
later dictionaries have precedence.
2101
"""
2102
if len(v) == 0:
2103
return {}
2104
a = dict(v[0]) # make a copy!
2105
for b in v[1:]:
2106
for k,w in b.iteritems():
2107
a[k] = w
2108
return a
2109
2110
2111