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