passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of passagemath-plot might be problematic. Click here for more details.

Files changed (82) hide show
  1. passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
  2. passagemath_plot-10.6.31rc3.dist-info/RECORD +82 -0
  3. passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
  4. passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
  5. passagemath_plot.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  6. passagemath_plot.libs/libgsl-cda90e79.so.28.0.0 +0 -0
  7. passagemath_plot.libs/libopenblasp-r0-6dcb67f9.3.29.so +0 -0
  8. passagemath_plot.libs/libquadmath-2284e583.so.0.0.0 +0 -0
  9. sage/all__sagemath_plot.py +15 -0
  10. sage/ext_data/threejs/animation.css +195 -0
  11. sage/ext_data/threejs/animation.html +85 -0
  12. sage/ext_data/threejs/animation.js +273 -0
  13. sage/ext_data/threejs/fat_lines.js +48 -0
  14. sage/ext_data/threejs/threejs-version.txt +1 -0
  15. sage/ext_data/threejs/threejs_template.html +597 -0
  16. sage/interfaces/all__sagemath_plot.py +1 -0
  17. sage/interfaces/gnuplot.py +196 -0
  18. sage/interfaces/jmoldata.py +208 -0
  19. sage/interfaces/povray.py +56 -0
  20. sage/plot/all.py +42 -0
  21. sage/plot/animate.py +1796 -0
  22. sage/plot/arc.py +504 -0
  23. sage/plot/arrow.py +671 -0
  24. sage/plot/bar_chart.py +205 -0
  25. sage/plot/bezier_path.py +400 -0
  26. sage/plot/circle.py +435 -0
  27. sage/plot/colors.py +1606 -0
  28. sage/plot/complex_plot.cpython-314-x86_64-linux-gnu.so +0 -0
  29. sage/plot/complex_plot.pyx +1446 -0
  30. sage/plot/contour_plot.py +1792 -0
  31. sage/plot/density_plot.py +318 -0
  32. sage/plot/disk.py +373 -0
  33. sage/plot/ellipse.py +375 -0
  34. sage/plot/graphics.py +3580 -0
  35. sage/plot/histogram.py +354 -0
  36. sage/plot/hyperbolic_arc.py +404 -0
  37. sage/plot/hyperbolic_polygon.py +416 -0
  38. sage/plot/hyperbolic_regular_polygon.py +296 -0
  39. sage/plot/line.py +626 -0
  40. sage/plot/matrix_plot.py +629 -0
  41. sage/plot/misc.py +509 -0
  42. sage/plot/multigraphics.py +1294 -0
  43. sage/plot/plot.py +4183 -0
  44. sage/plot/plot3d/all.py +23 -0
  45. sage/plot/plot3d/base.cpython-314-x86_64-linux-gnu.so +0 -0
  46. sage/plot/plot3d/base.pxd +12 -0
  47. sage/plot/plot3d/base.pyx +3378 -0
  48. sage/plot/plot3d/implicit_plot3d.py +659 -0
  49. sage/plot/plot3d/implicit_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  50. sage/plot/plot3d/implicit_surface.pyx +1453 -0
  51. sage/plot/plot3d/index_face_set.cpython-314-x86_64-linux-gnu.so +0 -0
  52. sage/plot/plot3d/index_face_set.pxd +32 -0
  53. sage/plot/plot3d/index_face_set.pyx +1873 -0
  54. sage/plot/plot3d/introduction.py +131 -0
  55. sage/plot/plot3d/list_plot3d.py +649 -0
  56. sage/plot/plot3d/parametric_plot3d.py +1130 -0
  57. sage/plot/plot3d/parametric_surface.cpython-314-x86_64-linux-gnu.so +0 -0
  58. sage/plot/plot3d/parametric_surface.pxd +12 -0
  59. sage/plot/plot3d/parametric_surface.pyx +893 -0
  60. sage/plot/plot3d/platonic.py +601 -0
  61. sage/plot/plot3d/plot3d.py +1442 -0
  62. sage/plot/plot3d/plot_field3d.py +162 -0
  63. sage/plot/plot3d/point_c.pxi +148 -0
  64. sage/plot/plot3d/revolution_plot3d.py +309 -0
  65. sage/plot/plot3d/shapes.cpython-314-x86_64-linux-gnu.so +0 -0
  66. sage/plot/plot3d/shapes.pxd +22 -0
  67. sage/plot/plot3d/shapes.pyx +1382 -0
  68. sage/plot/plot3d/shapes2.py +1512 -0
  69. sage/plot/plot3d/tachyon.py +1779 -0
  70. sage/plot/plot3d/texture.py +453 -0
  71. sage/plot/plot3d/transform.cpython-314-x86_64-linux-gnu.so +0 -0
  72. sage/plot/plot3d/transform.pxd +21 -0
  73. sage/plot/plot3d/transform.pyx +268 -0
  74. sage/plot/plot3d/tri_plot.py +589 -0
  75. sage/plot/plot_field.py +362 -0
  76. sage/plot/point.py +624 -0
  77. sage/plot/polygon.py +562 -0
  78. sage/plot/primitive.py +249 -0
  79. sage/plot/scatter_plot.py +199 -0
  80. sage/plot/step.py +85 -0
  81. sage/plot/streamline_plot.py +328 -0
  82. sage/plot/text.py +432 -0
@@ -0,0 +1,3378 @@
1
+ # sage_setup: distribution = sagemath-plot
2
+ r"""
3
+ Base classes for 3D graphics objects and plotting
4
+
5
+ The most important facts about these classes are
6
+ that you can simply add graphics objects
7
+ together (``G1+G2``, see :meth:`Graphics3d.__add__`),
8
+ and the :meth:`Graphics3d.show` method with its options for
9
+ choosing a viewer and setting
10
+ various parameters for displaying the graphics.
11
+
12
+ Most of the other methods of these classes are technical and
13
+ for special usage.
14
+
15
+ AUTHORS:
16
+
17
+ - Robert Bradshaw (2007-02): initial version
18
+
19
+ - Robert Bradshaw (2007-08): Cythonization, much optimization
20
+
21
+ - William Stein (2008)
22
+
23
+ - Paul Masson (2016): Three.js support
24
+
25
+ - Joshua Campbell (2020): Three.js animation support
26
+
27
+ - Günter Rote (2021): camera and light parameters for tachyon
28
+
29
+ .. TODO::
30
+
31
+ finish integrating tachyon -- good default lights
32
+
33
+ full documentation of three.js viewer parameters
34
+
35
+ zoom by changing camera parameters instead of scaling objects
36
+ """
37
+
38
+ # ****************************************************************************
39
+ # Copyright (C) 2007 Robert Bradshaw <robertwb@math.washington.edu>
40
+ #
41
+ # Distributed under the terms of the GNU General Public License (GPL)
42
+ #
43
+ # This code is distributed in the hope that it will be useful,
44
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
45
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
46
+ # General Public License for more details.
47
+ #
48
+ # The full text of the GPL is available at:
49
+ #
50
+ # https://www.gnu.org/licenses/
51
+ # ****************************************************************************
52
+
53
+ from cpython.list cimport *
54
+
55
+ import os
56
+ import sys
57
+ import zipfile
58
+
59
+ from functools import reduce
60
+ from io import StringIO
61
+ from random import randint
62
+
63
+ from sage.misc.temporary_file import tmp_filename
64
+ from sage.misc.fast_methods cimport hash_by_id
65
+ from sage.modules.free_module_element import vector
66
+ from sage.rings.real_double import RDF
67
+ from sage.plot.plot3d.texture import Texture
68
+ from sage.plot.plot3d.transform cimport Transformation, point_c, face_c
69
+ include "point_c.pxi"
70
+
71
+ from libc.math cimport INFINITY
72
+
73
+
74
+ default_texture = Texture()
75
+ pi = RDF.pi()
76
+
77
+
78
+ cdef class Graphics3d(SageObject):
79
+ """
80
+ This is the baseclass for all 3d graphics objects.
81
+
82
+ .. automethod:: __add__
83
+ .. automethod:: _rich_repr_
84
+ """
85
+ def __cinit__(self):
86
+ """
87
+ The Cython constructor.
88
+
89
+ EXAMPLES::
90
+
91
+ sage: gfx = sage.plot.plot3d.base.Graphics3d()
92
+ sage: gfx._extra_kwds
93
+ {}
94
+ """
95
+ self._extra_kwds = dict()
96
+
97
+ def __hash__(self):
98
+ r"""
99
+ TESTS::
100
+
101
+ sage: from sage.plot.plot3d.base import Graphics3d
102
+ sage: hash(Graphics3d()) # random
103
+ 140658972348064
104
+ """
105
+ return hash_by_id(<void *> self)
106
+
107
+ def _repr_(self):
108
+ """
109
+ Return a string representation.
110
+
111
+ OUTPUT: string
112
+
113
+ EXAMPLES::
114
+
115
+ sage: S = sphere((0, 0, 0), 1)
116
+ sage: print(S)
117
+ Graphics3d Object
118
+ """
119
+ return str(self)
120
+
121
+ def _rich_repr_(self, display_manager, **kwds):
122
+ """
123
+ Rich Output Magic Method.
124
+
125
+ See :mod:`sage.repl.rich_output` for details.
126
+
127
+ EXAMPLES::
128
+
129
+ sage: from sage.repl.rich_output import get_display_manager
130
+ sage: dm = get_display_manager()
131
+ sage: g = sphere()
132
+ sage: g._rich_repr_(dm) # OutputSceneThreejs container outside doctest mode
133
+ OutputSceneJmol container
134
+ """
135
+ ### First, figure out the best graphics format
136
+ types = display_manager.types
137
+ can_view_jmol = (types.OutputSceneJmol in display_manager.supported_output())
138
+ can_view_canvas3d = (types.OutputSceneCanvas3d in display_manager.supported_output())
139
+ can_view_wavefront = (types.OutputSceneWavefront in display_manager.supported_output())
140
+ can_view_threejs = (types.OutputSceneThreejs in display_manager.supported_output())
141
+ opts = self._process_viewing_options(kwds)
142
+ viewer = opts.get('viewer', None)
143
+ # make sure viewer is one of the supported options
144
+ if viewer not in [None, 'jmol', 'tachyon', 'canvas3d', 'wavefront', 'threejs']:
145
+ import warnings
146
+ warnings.warn('viewer={} is not supported'.format(viewer))
147
+ viewer = None
148
+ # select suitable default
149
+ if viewer is None:
150
+ viewer = SHOW_DEFAULTS['viewer']
151
+ # fall back to 2d image if necessary
152
+ if viewer == 'canvas3d' and not can_view_canvas3d:
153
+ viewer = 'jmol'
154
+ if viewer == 'wavefront' and not can_view_wavefront:
155
+ viewer = 'jmol'
156
+ if viewer == 'threejs' and not can_view_threejs:
157
+ viewer = 'jmol'
158
+ if viewer == 'jmol' and not can_view_jmol:
159
+ viewer = 'tachyon'
160
+ # Second, return the corresponding graphics file
161
+ if viewer == 'threejs':
162
+ return self._rich_repr_threejs(**opts)
163
+ elif viewer == 'jmol':
164
+ return self._rich_repr_jmol(**opts)
165
+ elif viewer == 'tachyon':
166
+ preferred = (
167
+ types.OutputImagePng,
168
+ types.OutputImageJpg,
169
+ types.OutputImageGif,
170
+ )
171
+ for output_container in preferred:
172
+ if output_container in display_manager.supported_output():
173
+ return self._rich_repr_tachyon(output_container, **opts)
174
+ elif viewer == 'canvas3d':
175
+ return self._rich_repr_canvas3d(**opts)
176
+ elif viewer == 'wavefront':
177
+ return self._rich_repr_wavefront(**opts)
178
+ else:
179
+ assert False # unreachable
180
+
181
+ def _rich_repr_tachyon(self, output_container, **kwds):
182
+ """
183
+ Rich Representation using Tachyon.
184
+
185
+ INPUT:
186
+
187
+ - ``output_container`` -- the rich output container to contain
188
+ the rendered image (can be png, gif, or jpg). Determines the
189
+ type of the output.
190
+
191
+ - ``**kwds`` -- optional keyword arguments are passed to the
192
+ Tachyon raytracer
193
+
194
+ OUTPUT:
195
+
196
+ Instance of
197
+ :class:`~sage.repl.rich_output.output_graphics.OutputImagePng`,
198
+ :class:`~sage.repl.rich_output.output_graphics.OutputImageGif`, or
199
+ :class:`~sage.repl.rich_output.output_graphics.OutputImageJpg`.
200
+
201
+ EXAMPLES::
202
+
203
+ sage: import sage.repl.rich_output.output_catalog as catalog
204
+ sage: sphere()._rich_repr_tachyon(catalog.OutputImagePng)
205
+ OutputImagePng container
206
+ sage: import sage.repl.rich_output.output_catalog as catalog
207
+ sage: sphere()._rich_repr_tachyon(catalog.OutputImageJpg) # optional -- libjpeg
208
+ OutputImageJpg container
209
+ """
210
+ filename = tmp_filename(ext='.png')
211
+ opts = self._process_viewing_options(kwds)
212
+ T = self._prepare_for_tachyon(
213
+ opts['frame'], opts['axes'], opts['frame_aspect_ratio'],
214
+ opts['aspect_ratio'],
215
+ 1 # opts['zoom']. Let zoom be handled by tachyon.
216
+ # We don't want the perspective to change by zooming
217
+ )
218
+
219
+ tachyon_args = dict((key,val) for key,val in opts.items() if key in Graphics3d.tachyon_keywords)
220
+ extra_opts = opts.get("extra_opts", "")
221
+ if "shade" in opts:
222
+ if opts["shade"] not in ["full", "medium", "low", "lowest"]:
223
+ raise ValueError("shade must be set to 'full', 'medium', 'low' or 'lowest'")
224
+ extra_opts += " -" + opts["shade"] + "shade"
225
+
226
+ from sage.interfaces.tachyon import tachyon_rt
227
+
228
+ tachyon_rt(T.tachyon(**tachyon_args), filename, opts['verbosity'], extra_opts)
229
+ from sage.repl.rich_output.buffer import OutputBuffer
230
+ import sage.repl.rich_output.output_catalog as catalog
231
+ import PIL.Image as Image
232
+ if output_container is catalog.OutputImagePng:
233
+ buf = OutputBuffer.from_file(filename)
234
+ elif output_container is catalog.OutputImageGif:
235
+ gif = tmp_filename(ext='.gif')
236
+ Image.open(filename).save(gif)
237
+ buf = OutputBuffer.from_file(gif)
238
+ elif output_container is catalog.OutputImageJpg:
239
+ jpg = tmp_filename(ext='.jpg')
240
+ Image.open(filename).save(jpg)
241
+ buf = OutputBuffer.from_file(jpg)
242
+ else:
243
+ raise ValueError('output_container not supported')
244
+ return output_container(buf)
245
+
246
+ def _rich_repr_jmol(self, **kwds):
247
+ """
248
+ Rich Representation as JMol scene.
249
+
250
+ INPUT:
251
+
252
+ - ``**kwds`` -- optional keyword arguments are passed to JMol
253
+
254
+ OUTPUT:
255
+
256
+ Instance of
257
+ :class:`sage.repl.rich_output.output_graphics3d.OutputSceneJmol`.
258
+
259
+ EXAMPLES::
260
+
261
+ sage: sphere()._rich_repr_jmol()
262
+ OutputSceneJmol container
263
+ """
264
+ from sage.misc.temporary_file import tmp_dir
265
+ root_dir = os.path.abspath(tmp_dir())
266
+ scene_zip = os.path.join(root_dir, 'scene.spt.zip')
267
+ preview_png = os.path.join(root_dir, 'preview.png')
268
+ opts = self._process_viewing_options(kwds)
269
+ zoom = opts['zoom']
270
+ T = self._prepare_for_jmol(
271
+ opts['frame'],
272
+ opts['axes'],
273
+ opts['frame_aspect_ratio'],
274
+ opts['aspect_ratio'],
275
+ zoom,
276
+ )
277
+ T.export_jmol(scene_zip, **opts)
278
+ from sage.interfaces.jmoldata import JmolData
279
+ jdata = JmolData()
280
+ if not jdata.is_jmol_available():
281
+ # We can only use JMol to generate preview if a jvm is installed
282
+ from sage.repl.rich_output.output_graphics import OutputImagePng
283
+ tachyon = self._rich_repr_tachyon(OutputImagePng, **opts)
284
+ tachyon.png.save_as(preview_png)
285
+ else:
286
+ # Java needs absolute paths
287
+ scene_native = scene_zip
288
+
289
+ script = '''set defaultdirectory "{0}"\nscript SCRIPT\n'''.format(scene_native)
290
+ jdata.export_image(targetfile=preview_png, datafile=script,
291
+ image_type='PNG',
292
+ figsize=opts['figsize'][0])
293
+ from sage.repl.rich_output.output_graphics3d import OutputSceneJmol
294
+ from sage.repl.rich_output.buffer import OutputBuffer
295
+ scene_zip = OutputBuffer.from_file(scene_zip)
296
+ preview_png = OutputBuffer.from_file(preview_png)
297
+ return OutputSceneJmol(scene_zip, preview_png)
298
+
299
+ def _rich_repr_wavefront(self, **kwds):
300
+ r"""
301
+ Rich Representation as Wavefront (obj + mtl) Scene.
302
+
303
+ INPUT:
304
+
305
+ - ``**kwds`` -- optional keyword arguments are ignored
306
+
307
+ OUTPUT:
308
+
309
+ Instance of
310
+ :class:`sage.repl.rich_output.output_graphics3d.OutputSceneWavefront`.
311
+
312
+ EXAMPLES::
313
+
314
+ sage: line = line3d([(0,0,0), (1,1,1)])
315
+ sage: out = line._rich_repr_wavefront()
316
+ sage: out
317
+ OutputSceneWavefront container
318
+ sage: out.obj.get_str()
319
+ 'mtllib ... 6 3 8 11\nf 8 7 12\nf 7 9 12\nf 9 10 12\nf 10 11 12\nf 11 8 12\n'
320
+ sage: out.mtl.get_str()
321
+ 'newmtl texture...\nKd 0.4 0.4 1.0\nKs 0.0 0.0 0.0\nillum 1\nNs 1.0\nd 1.0\n'
322
+ """
323
+ from sage.repl.rich_output.output_graphics3d import OutputSceneWavefront
324
+ from sage.repl.rich_output.buffer import OutputBuffer
325
+ obj = OutputBuffer('mtllib scene.mtl\n' + self.obj())
326
+ return OutputSceneWavefront(obj, self.mtl_str())
327
+
328
+ def _rich_repr_canvas3d(self, **kwds):
329
+ r"""
330
+ Rich Representation as Canvas3D Scene.
331
+
332
+ INPUT:
333
+
334
+ - ``**kwds`` -- optional keyword arguments
335
+
336
+ OUTPUT:
337
+
338
+ Instance of
339
+ :class:`sage.repl.rich_output.output_graphics3d.OutputSceneCanvas3d`.
340
+
341
+ EXAMPLES::
342
+
343
+ sage: out = sphere()._rich_repr_canvas3d()
344
+ sage: out
345
+ OutputSceneCanvas3d container
346
+ sage: out.canvas3d.get_str()
347
+ '[{"vertices":[{"x":0,"y":0,"z":-1},..., "color":"#6666ff", "opacity":1.0}]'
348
+ """
349
+ opts = self._process_viewing_options(kwds)
350
+ aspect_ratio = opts['aspect_ratio'] # this necessarily has a value now
351
+ frame_aspect_ratio = opts['frame_aspect_ratio']
352
+ zoom = opts['zoom']
353
+ frame = opts['frame']
354
+ axes = opts['axes']
355
+ T = self._prepare_for_tachyon(frame, axes, frame_aspect_ratio, aspect_ratio, zoom)
356
+ data = flatten_list(T.json_repr(T.default_render_params()))
357
+ canvas3d = '[' + ','.join(data) + ']'
358
+ from sage.repl.rich_output.output_catalog import OutputSceneCanvas3d
359
+ return OutputSceneCanvas3d(canvas3d)
360
+
361
+ def _rich_repr_threejs(self, **kwds):
362
+ r"""
363
+ Rich Representation as Three.js Scene.
364
+
365
+ INPUT:
366
+
367
+ - ``**kwds`` -- optional keyword arguments
368
+
369
+ OUTPUT:
370
+
371
+ Instance of
372
+ :class:`sage.repl.rich_output.output_graphics3d.OutputSceneThreejs`.
373
+
374
+ EXAMPLES::
375
+
376
+ sage: sphere(online=True)._rich_repr_threejs()
377
+ OutputSceneThreejs container
378
+
379
+ TESTS::
380
+
381
+ sage: js = '// animation.js'
382
+ sage: css = '/* animation.css */'
383
+ sage: html = '<!-- animation.html -->'
384
+ sage: d = dodecahedron()
385
+ sage: i = icosahedron()
386
+ sage: g1 = animate([d]).interactive()
387
+ sage: g2 = animate([d, i]).interactive()
388
+
389
+ Animation files are only included when at least 2 frames are present::
390
+
391
+ sage: str = g1._rich_repr_threejs(online=True).html.get_str()
392
+ sage: (js in str) or (css in str) or (html in str)
393
+ False
394
+ sage: str = g2._rich_repr_threejs(online=True).html.get_str()
395
+ sage: (js in str) and (css in str) and (html in str)
396
+ True
397
+
398
+ Animation can be explicitly disabled by setting animate=False::
399
+
400
+ sage: str = g2._rich_repr_threejs(online=True, animate=False).html.get_str()
401
+ sage: (js in str) or (css in str) or (html in str)
402
+ False
403
+
404
+ Animation CSS and HTML are not included when animation_controls=False::
405
+
406
+ sage: str = g2._rich_repr_threejs(online=True, animation_controls=False).html.get_str()
407
+ sage: js in str
408
+ True
409
+ sage: (css in str) or (html in str)
410
+ False
411
+
412
+ "Fat" line scripts are only included when at least one line
413
+ (or one surface with ``mesh=True``) exists with ``thickness > 1``::
414
+
415
+ sage: fat = '// fat_lines.js'
416
+ sage: L = line3d([(0, 0, 0), (1, 1, 1)], thickness=1)
417
+ sage: str = L._rich_repr_threejs(online=True).html.get_str()
418
+ sage: fat in str
419
+ False
420
+ sage: L = line3d([(0, 0, 0), (1, 1, 1)], thickness=10)
421
+ sage: str = L._rich_repr_threejs(online=True).html.get_str()
422
+ sage: fat in str
423
+ True
424
+ sage: d = dodecahedron(mesh=False, thickness=10)
425
+ sage: str = d._rich_repr_threejs(online=True).html.get_str()
426
+ sage: fat in str
427
+ False
428
+ sage: d = dodecahedron(mesh=True, thickness=1)
429
+ sage: str = d._rich_repr_threejs(online=True).html.get_str()
430
+ sage: fat in str
431
+ False
432
+ sage: d = dodecahedron(mesh=True, thickness=10)
433
+ sage: str = d._rich_repr_threejs(online=True).html.get_str()
434
+ sage: fat in str
435
+ True
436
+
437
+ If a page title is provided, it is stripped and HTML-escaped::
438
+
439
+ sage: d = dodecahedron(page_title='\t"Page" & <Title>\n')
440
+ sage: str = d._rich_repr_threejs(online=True).html.get_str()
441
+ sage: '<title>&quot;Page&quot; &amp; &lt;Title&gt;</title>' in str
442
+ True
443
+ """
444
+ options = self._process_viewing_options(kwds)
445
+ options.setdefault('online', False)
446
+
447
+ js_options = {} # options passed to Three.js template
448
+
449
+ js_options['animate'] = options.get('animate', True)
450
+ js_options['animationControls'] = options.get('animation_controls', True)
451
+ js_options['aspectRatio'] = options.get('aspect_ratio', [1,1,1])
452
+ js_options['autoScaling'] = options.get('auto_scaling', [False, False, False])
453
+ js_options['autoPlay'] = options.get('auto_play', True)
454
+ js_options['axes'] = options.get('axes', False)
455
+ js_options['axesLabels'] = options.get('axes_labels', ['x','y','z'])
456
+ js_options['axesLabelsStyle'] = options.get('axes_labels_style')
457
+ js_options['decimals'] = options.get('decimals', 2)
458
+ js_options['delay'] = options.get('delay', 20)
459
+ js_options['frame'] = options.get('frame', True)
460
+ js_options['loop'] = options.get('loop', True)
461
+ js_options['projection'] = options.get('projection', 'perspective')
462
+ js_options['theme'] = options.get('theme', 'light')
463
+ js_options['viewpoint'] = options.get('viewpoint', False)
464
+
465
+ if js_options['projection'] not in ['perspective', 'orthographic']:
466
+ import warnings
467
+ warnings.warn('projection={} is not supported; using perspective'.format(js_options['projection']))
468
+ js_options['projection'] = 'perspective'
469
+
470
+ if js_options['theme'] not in ['light', 'dark']:
471
+ import warnings
472
+ warnings.warn('theme={} is not supported; using light theme'.format(js_options['theme']))
473
+ js_options['theme'] = 'light'
474
+
475
+ # Normalization of options values for proper JSONing
476
+ js_options['aspectRatio'] = [float(i) for i in js_options['aspectRatio']]
477
+ js_options['decimals'] = int(js_options['decimals'])
478
+ js_options['delay'] = int(js_options['delay'])
479
+
480
+ if js_options['viewpoint']:
481
+ if len(js_options['viewpoint']) != 2 or len(js_options['viewpoint'][0]) != 3:
482
+ import warnings
483
+ warnings.warn('viewpoint must be of the form [[x,y,z],angle]')
484
+ js_options['viewpoint'] = False
485
+ else:
486
+ if type(js_options['viewpoint']) is tuple:
487
+ js_options['viewpoint'] = list(js_options['viewpoint'])
488
+ if type(js_options['viewpoint'][0]) is tuple:
489
+ js_options['viewpoint'][0] = list(js_options['viewpoint'][0])
490
+ js_options['viewpoint'][0] = [float(i) for i in js_options['viewpoint'][0]]
491
+ js_options['viewpoint'][1] = float(js_options['viewpoint'][1])
492
+
493
+ if not js_options['frame']:
494
+ js_options['axesLabels'] = False
495
+ js_options['axesLabelsStyle'] = None
496
+
497
+ if js_options['axesLabelsStyle'] is not None:
498
+ from sage.plot.plot3d.shapes import _validate_threejs_text_style
499
+ style = js_options['axesLabelsStyle']
500
+ if isinstance(style, dict):
501
+ style = _validate_threejs_text_style(style)
502
+ style = [style, style, style]
503
+ elif isinstance(style, list) and len(style) == 3 and all([isinstance(s, dict) for s in style]):
504
+ style = [_validate_threejs_text_style(s) for s in style]
505
+ else:
506
+ import warnings
507
+ warnings.warn("axes_labels_style must be a dict or a list of 3 dicts")
508
+ style = [dict(), dict(), dict()]
509
+ js_options['axesLabelsStyle'] = style
510
+
511
+ from sage.repl.rich_output import get_display_manager
512
+ scripts = get_display_manager().threejs_scripts(options['online'])
513
+ styles = ''
514
+ extra_html = ''
515
+
516
+ b = self.bounding_box()
517
+ bounds = '[{{"x":{}, "y":{}, "z":{}}}, {{"x":{}, "y":{}, "z":{}}}]'.format(
518
+ b[0][0], b[0][1], b[0][2], b[1][0], b[1][1], b[1][2])
519
+
520
+ from sage.plot.colors import Color
521
+ lights = '[{{"x":-5, "y":3, "z":0, "color":"{}", "parent":"camera"}}]'.format(
522
+ Color(.5,.5,.5).html_color())
523
+ ambient = '{{"color":"{}"}}'.format(Color(.5,.5,.5).html_color())
524
+
525
+ import json
526
+
527
+ reprs = {'point': [], 'line': [], 'text': [], 'surface': []}
528
+ frame_count = 0
529
+ fat_lines = False
530
+ for kind, desc in self.threejs_repr(self.default_render_params()):
531
+ reprs[kind].append(desc)
532
+ keyframe = int(desc.get('keyframe', -1))
533
+ frame_count = max(frame_count, keyframe + 1)
534
+ if kind == 'line' or (kind == 'surface' and desc.get('showMeshGrid')):
535
+ linewidth = float(desc.get('linewidth', 1))
536
+ if linewidth > 1:
537
+ fat_lines = True
538
+ reprs = {kind: json.dumps(descs) for kind, descs in reprs.items()}
539
+
540
+ from sage.env import SAGE_EXTCODE
541
+ with open(os.path.join(
542
+ SAGE_EXTCODE, 'threejs', 'threejs_template.html')) as f:
543
+ html = f.read()
544
+
545
+ if fat_lines:
546
+ with open(os.path.join(SAGE_EXTCODE, 'threejs', 'fat_lines.js')) as f:
547
+ scripts += '<script>' + f.read() + '</script>'
548
+
549
+ js_options['animate'] = js_options['animate'] and frame_count > 1
550
+ if js_options['animate']:
551
+ if js_options['animationControls']:
552
+ with open(os.path.join(SAGE_EXTCODE, 'threejs', 'animation.css')) as f:
553
+ css = f.read()
554
+ css = css.replace('SAGE_FRAME_COUNT', str(frame_count))
555
+ styles += '<style>' + css + '</style>'
556
+ with open(os.path.join(SAGE_EXTCODE, 'threejs', 'animation.html')) as f:
557
+ extra_html += f.read()
558
+ with open(os.path.join(SAGE_EXTCODE, 'threejs', 'animation.js')) as f:
559
+ extra_html += '<script>' + f.read() + '</script>'
560
+
561
+ page_title = options.get('page_title')
562
+ if page_title is None:
563
+ page_title = ""
564
+ else:
565
+ from html import escape as html_escape
566
+ page_title = html_escape(str(page_title).strip())
567
+
568
+ html = html.replace('SAGE_TITLE', page_title)
569
+ html = html.replace('SAGE_SCRIPTS', scripts)
570
+ html = html.replace('SAGE_STYLES', styles)
571
+ html = html.replace('SAGE_EXTRA_HTML', extra_html)
572
+ html = html.replace('SAGE_OPTIONS', json.dumps(js_options))
573
+ html = html.replace('SAGE_BOUNDS', bounds)
574
+ html = html.replace('SAGE_LIGHTS', lights)
575
+ html = html.replace('SAGE_AMBIENT', ambient)
576
+ html = html.replace('SAGE_TEXTS', str(reprs['text']))
577
+ html = html.replace('SAGE_POINTS', str(reprs['point']))
578
+ html = html.replace('SAGE_LINES', str(reprs['line']))
579
+ html = html.replace('SAGE_SURFACES', str(reprs['surface']))
580
+
581
+ from sage.repl.rich_output.output_catalog import OutputSceneThreejs
582
+ return OutputSceneThreejs(html)
583
+
584
+ def __str__(self):
585
+ """
586
+ EXAMPLES::
587
+
588
+ sage: S = sphere((0, 0, 0), 1)
589
+ sage: str(S)
590
+ 'Graphics3d Object'
591
+ """
592
+ return "Graphics3d Object"
593
+
594
+ def __add__(left, right):
595
+ """
596
+ Addition of objects adds them to the same scene.
597
+
598
+ EXAMPLES::
599
+
600
+ sage: A = sphere((0,0,0), 1, color='red')
601
+ sage: B = dodecahedron((2, 0, 0), color='yellow')
602
+ sage: A+B
603
+ Graphics3d Object
604
+
605
+ For convenience, we take 0 and ``None`` to be the additive identity::
606
+
607
+ sage: A + 0 is A
608
+ True
609
+ sage: A + None is A, 0 + A is A, None + A is A
610
+ (True, True, True)
611
+
612
+ In particular, this allows us to use the sum() function without
613
+ having to provide an empty starting object::
614
+
615
+ sage: sum(point3d((cos(n), sin(n), n)) for n in [0..10, step=.1])
616
+ Graphics3d Object
617
+
618
+ A Graphics 3d object and a 2d object can also be added::
619
+
620
+ sage: A = sphere((0, 0, 0), 1) + circle((0, 0), 1.5)
621
+ sage: A.show(aspect_ratio=1)
622
+ """
623
+ if right == 0 or right is None:
624
+ return left
625
+ elif left == 0 or left is None:
626
+ return right
627
+ elif not isinstance(left, Graphics3d):
628
+ left = left.plot3d()
629
+ elif not isinstance(right, Graphics3d):
630
+ right = right.plot3d()
631
+ return Graphics3dGroup([left, right])
632
+
633
+ def _set_extra_kwds(self, kwds):
634
+ """
635
+ Allow one to pass rendering arguments on as if they were set
636
+ in the constructor.
637
+
638
+ EXAMPLES::
639
+
640
+ sage: S = sphere((0, 0, 0), 1)
641
+ sage: S._set_extra_kwds({'aspect_ratio': [1, 2, 2]})
642
+ sage: S
643
+ Graphics3d Object
644
+ """
645
+ self._extra_kwds = kwds
646
+
647
+ def aspect_ratio(self, v=None):
648
+ """
649
+ Set or get the preferred aspect ratio.
650
+
651
+ INPUT:
652
+
653
+ - ``v`` -- (default: ``None``) must be a list or tuple of length three,
654
+ or the integer ``1``. If no arguments are provided then the
655
+ default aspect ratio is returned.
656
+
657
+ EXAMPLES::
658
+
659
+ sage: D = dodecahedron()
660
+ sage: D.aspect_ratio()
661
+ [1.0, 1.0, 1.0]
662
+ sage: D.aspect_ratio([1,2,3])
663
+ sage: D.aspect_ratio()
664
+ [1.0, 2.0, 3.0]
665
+ sage: D.aspect_ratio(1)
666
+ sage: D.aspect_ratio()
667
+ [1.0, 1.0, 1.0]
668
+ """
669
+ if v is not None:
670
+ if v == 1:
671
+ v = (1, 1, 1)
672
+ if not isinstance(v, (tuple, list)):
673
+ raise TypeError("aspect_ratio must be a list or tuple of "
674
+ "length 3 or the integer 1")
675
+ self._aspect_ratio = [float(z) for z in v]
676
+ else:
677
+ if self._aspect_ratio is None:
678
+ self._aspect_ratio = [1.0, 1.0, 1.0]
679
+ return self._aspect_ratio
680
+
681
+ def frame_aspect_ratio(self, v=None):
682
+ """
683
+ Set or get the preferred frame aspect ratio.
684
+
685
+ INPUT:
686
+
687
+ - ``v`` -- (default: ``None``) must be a list or tuple of
688
+ length three, or the integer ``1``. If no arguments are
689
+ provided then the default frame aspect ratio is returned.
690
+
691
+ EXAMPLES::
692
+
693
+ sage: D = dodecahedron()
694
+ sage: D.frame_aspect_ratio()
695
+ [1.0, 1.0, 1.0]
696
+ sage: D.frame_aspect_ratio([2,2,1])
697
+ sage: D.frame_aspect_ratio()
698
+ [2.0, 2.0, 1.0]
699
+ sage: D.frame_aspect_ratio(1)
700
+ sage: D.frame_aspect_ratio()
701
+ [1.0, 1.0, 1.0]
702
+ """
703
+ if v is not None:
704
+ if v == 1:
705
+ v = (1, 1, 1)
706
+ if not isinstance(v, (tuple, list)):
707
+ raise TypeError("frame_aspect_ratio must be a list or tuple of "
708
+ "length 3 or the integer 1")
709
+ self._frame_aspect_ratio = [float(z) for z in v]
710
+ else:
711
+ if self._frame_aspect_ratio is None:
712
+ self._frame_aspect_ratio = [1.0, 1.0, 1.0]
713
+ return self._frame_aspect_ratio
714
+
715
+ def _determine_frame_aspect_ratio(self, aspect_ratio):
716
+ a_min, a_max = self._safe_bounding_box()
717
+ return [(a_max[i] - a_min[i])*aspect_ratio[i] for i in range(3)]
718
+
719
+ def _safe_bounding_box(self):
720
+ """
721
+ Return a bounding box but where no side length is 0.
722
+
723
+ This is used to avoid zero-division errors for pathological
724
+ plots.
725
+
726
+ EXAMPLES::
727
+
728
+ sage: G = line3d([(0, 0, 0), (0, 0, 1)])
729
+ sage: G.bounding_box()
730
+ ((0.0, 0.0, 0.0), (0.0, 0.0, 1.0))
731
+ sage: G._safe_bounding_box()
732
+ ([-1.0, -1.0, 0.0], [1.0, 1.0, 1.0])
733
+ """
734
+ a_min, a_max = self.bounding_box()
735
+ a_min = list(a_min)
736
+ a_max = list(a_max)
737
+ for i in range(3):
738
+ if a_min[i] == a_max[i]:
739
+ a_min[i] = a_min[i] - 1
740
+ a_max[i] = a_max[i] + 1
741
+ return a_min, a_max
742
+
743
+ def bounding_box(self):
744
+ """
745
+ Return the lower and upper corners of a 3d bounding box.
746
+
747
+ This is used for rendering, and the scene should fit entirely
748
+ within this box.
749
+
750
+ Specifically, the first point returned has x, y, and z
751
+ coordinates that are the respective minimum over all points
752
+ in the graphics, and the second point is the maximum.
753
+
754
+ The default return value is simply the box containing the origin.
755
+
756
+ EXAMPLES::
757
+
758
+ sage: sphere((1,1,1), 2).bounding_box()
759
+ ((-1.0, -1.0, -1.0), (3.0, 3.0, 3.0))
760
+ sage: G = line3d([(1, 2, 3), (-1,-2,-3)])
761
+ sage: G.bounding_box()
762
+ ((-1.0, -2.0, -3.0), (1.0, 2.0, 3.0))
763
+ """
764
+ return ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
765
+
766
+ def transform(self, **kwds):
767
+ """
768
+ Apply a transformation, where the inputs are
769
+ passed onto a TransformGroup object.
770
+
771
+ Mostly for internal use; see the translate, scale, and rotate
772
+ methods for more details.
773
+
774
+ EXAMPLES::
775
+
776
+ sage: sphere((0,0,0), 1).transform(trans=(1, 0, 0), scale=(2,3,4)).bounding_box()
777
+ ((-1.0, -3.0, -4.0), (3.0, 3.0, 4.0))
778
+ """
779
+ return TransformGroup([self], **kwds)
780
+
781
+ def translate(self, *x):
782
+ """
783
+ Return the object translated by the given vector (which can be
784
+ given either as a 3-iterable or via positional arguments).
785
+
786
+ EXAMPLES::
787
+
788
+ sage: icosahedron() + sum(icosahedron(opacity=0.25).translate(2*n, 0, 0) for n in [1..4])
789
+ Graphics3d Object
790
+ sage: icosahedron() + sum(icosahedron(opacity=0.25).translate([-2*n, n, n^2]) for n in [1..4])
791
+ Graphics3d Object
792
+
793
+ TESTS::
794
+
795
+ sage: G = sphere((0, 0, 0), 1)
796
+ sage: G.bounding_box()
797
+ ((-1.0, -1.0, -1.0), (1.0, 1.0, 1.0))
798
+ sage: G.translate(0, 0, 1).bounding_box()
799
+ ((-1.0, -1.0, 0.0), (1.0, 1.0, 2.0))
800
+ sage: G.translate(-1, 5, 0).bounding_box()
801
+ ((-2.0, 4.0, -1.0), (0.0, 6.0, 1.0))
802
+ """
803
+ if len(x) == 1:
804
+ x = x[0]
805
+ return self.transform(trans=x)
806
+
807
+ def scale(self, *x):
808
+ """
809
+ Return the object scaled in the x, y, and z directions.
810
+
811
+ EXAMPLES::
812
+
813
+ sage: G = dodecahedron() + dodecahedron(opacity=.5).scale(2)
814
+ sage: G.show(aspect_ratio=1)
815
+ sage: G = icosahedron() + icosahedron(opacity=.5).scale([1, 1/2, 2])
816
+ sage: G.show(aspect_ratio=1)
817
+
818
+ TESTS::
819
+
820
+ sage: G = sphere((0, 0, 0), 1)
821
+ sage: G.scale(2)
822
+ Graphics3d Object
823
+ sage: G.scale(1, 2, 1/2).show(aspect_ratio=1)
824
+ sage: G.scale(2).bounding_box()
825
+ ((-2.0, -2.0, -2.0), (2.0, 2.0, 2.0))
826
+ """
827
+ if isinstance(x[0], (tuple, list)):
828
+ x = x[0]
829
+ return self.transform(scale=x)
830
+
831
+ def rotate(self, v, theta):
832
+ r"""
833
+ Return the object rotated about the vector `v` by `\theta` radians.
834
+
835
+ EXAMPLES::
836
+
837
+ sage: from math import pi
838
+ sage: from sage.plot.plot3d.shapes import Cone
839
+ sage: v = (1,2,3)
840
+ sage: G = arrow3d((0, 0, 0), v)
841
+ sage: G += Cone(1/5, 1).translate((0, 0, 2))
842
+ sage: C = Cone(1/5, 1, opacity=.25).translate((0, 0, 2))
843
+ sage: G += sum(C.rotate(v, pi*t/4) for t in [1..7])
844
+ sage: G.show(aspect_ratio=1)
845
+
846
+ sage: from sage.plot.plot3d.shapes import Box
847
+ sage: Box(1/3, 1/5, 1/7).rotate((1, 1, 1), pi/3).show(aspect_ratio=1)
848
+ """
849
+ vx, vy, vz = v
850
+ return self.transform(rot=[vx, vy, vz, theta])
851
+
852
+ def rotateX(self, theta):
853
+ """
854
+ Return the object rotated about the `x`-axis by the given angle.
855
+
856
+ EXAMPLES::
857
+
858
+ sage: from math import pi
859
+ sage: from sage.plot.plot3d.shapes import Cone
860
+ sage: G = Cone(1/5, 1) + Cone(1/5, 1, opacity=.25).rotateX(pi/2)
861
+ sage: G.show(aspect_ratio=1)
862
+ """
863
+ return self.rotate((1, 0, 0), theta)
864
+
865
+ def rotateY(self, theta):
866
+ """
867
+ Return the object rotated about the `y`-axis by the given angle.
868
+
869
+ EXAMPLES::
870
+
871
+ sage: from math import pi
872
+ sage: from sage.plot.plot3d.shapes import Cone
873
+ sage: G = Cone(1/5, 1) + Cone(1/5, 1, opacity=.25).rotateY(pi/3)
874
+ sage: G.show(aspect_ratio=1)
875
+ """
876
+ return self.rotate((0, 1, 0), theta)
877
+
878
+ def rotateZ(self, theta):
879
+ """
880
+ Return the object rotated about the `z`-axis by the given angle.
881
+
882
+ EXAMPLES::
883
+
884
+ sage: from math import pi
885
+ sage: from sage.plot.plot3d.shapes import Box
886
+ sage: G = Box(1/2, 1/3, 1/5) + Box(1/2, 1/3, 1/5, opacity=.25).rotateZ(pi/5)
887
+ sage: G.show(aspect_ratio=1)
888
+ """
889
+ return self.rotate((0, 0, 1), theta)
890
+
891
+ def viewpoint(self):
892
+ """
893
+ Return the viewpoint of this plot.
894
+
895
+ Currently only a stub for x3d.
896
+
897
+ EXAMPLES::
898
+
899
+ sage: type(dodecahedron().viewpoint())
900
+ <class 'sage.plot.plot3d.base.Viewpoint'>
901
+ """
902
+ # This should probably be reworked somehow.
903
+ return Viewpoint(0,0,6)
904
+
905
+ def default_render_params(self):
906
+ """
907
+ Return an instance of RenderParams suitable for plotting this object.
908
+
909
+ EXAMPLES::
910
+
911
+ sage: type(dodecahedron().default_render_params())
912
+ <class 'sage.plot.plot3d.base.RenderParams'>
913
+ """
914
+ return RenderParams(ds=.075)
915
+
916
+ def testing_render_params(self):
917
+ """
918
+ Return an instance of RenderParams suitable for testing this object.
919
+
920
+ In particular, it opens up a temporary file as an auxiliary zip
921
+ file for jmol.
922
+
923
+ EXAMPLES::
924
+
925
+ sage: type(dodecahedron().testing_render_params())
926
+ <class 'sage.plot.plot3d.base.RenderParams'>
927
+ """
928
+ params = RenderParams(ds=.075)
929
+ fn = tmp_filename(ext='.zip')
930
+ params.output_archive = zipfile.ZipFile(fn, 'w', zipfile.ZIP_STORED, True)
931
+ return params
932
+
933
+ def x3d(self):
934
+ """
935
+ An x3d scene file (as a string) containing the this object.
936
+
937
+ EXAMPLES::
938
+
939
+ sage: print(sphere((1, 2, 3), 5).x3d())
940
+ <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 '>
941
+ <head>
942
+ <meta name='title' content='sage3d'/>
943
+ </head>
944
+ <Scene>
945
+ <Viewpoint position='0 0 6'/>
946
+ <Transform translation='1 2 3'>
947
+ <Shape><Sphere radius='5.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
948
+ </Transform>
949
+ </Scene>
950
+ </X3D>
951
+
952
+ sage: G = icosahedron() + sphere((0,0,0), 0.5, color='red')
953
+ sage: print(G.x3d())
954
+ <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 '>
955
+ <head>
956
+ <meta name='title' content='sage3d'/>
957
+ </head>
958
+ <Scene>
959
+ <Viewpoint position='0 0 6'/>
960
+ <Shape>
961
+ <IndexedFaceSet coordIndex='...'>
962
+ <Coordinate point='...'/>
963
+ </IndexedFaceSet>
964
+ <Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
965
+ <Transform translation='0 0 0'>
966
+ <Shape><Sphere radius='0.5'/><Appearance><Material diffuseColor='1.0 0.0 0.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
967
+ </Transform>
968
+ </Scene>
969
+ </X3D>
970
+ """
971
+ return """
972
+ <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 '>
973
+ <head>
974
+ <meta name='title' content='sage3d'/>
975
+ </head>
976
+ <Scene>
977
+ %s
978
+ %s
979
+ </Scene>
980
+ </X3D>
981
+ """ % (self.viewpoint().x3d_str(), self.x3d_str())
982
+
983
+ ################ TACHYON ################
984
+
985
+ ####### insertion of camera parameters
986
+
987
+ tachyon_keywords = (
988
+ "antialiasing",
989
+ # "aspectratio",
990
+ "zoom", # zoom was previously handled directly by scaling the scene.
991
+ # This has now been disabled, and zoom is handled by tachyon.
992
+ "raydepth", "figsize", "light_position",
993
+ "camera_position","updir",
994
+ # "look_at", # omit look_at. viewdir is sufficient for most purposes
995
+ "viewdir")
996
+
997
+ # The tachyon "aspectratio" parameter is outdated for normal users:
998
+ # From the tachyon documentation:
999
+ # "By using the aspect ratio parameter, one can produce images which look
1000
+ # correct on any screen. Aspect ratio alters the relative width of the image,
1001
+ # while keeping plane the height of the image plane constant. In general,
1002
+ # most workstation displays have an aspect ratio of 1.0."
1003
+
1004
+ # Instead, the tachyon aspectratio is set to match nonsquare
1005
+ # drawing area in "figsize".
1006
+
1007
+ # Parameters are mostly taken from tachyon.py,
1008
+ # but camera_center is renamed camera_position.
1009
+ # Apparently reST strips () from default parameters in the automatic documentation.
1010
+ # Thus, I replaced () by [] as default values.
1011
+
1012
+ def tachyon(self,
1013
+ zoom=1.0,
1014
+ antialiasing=False,
1015
+ figsize=[5,5], # resolution = 100*figsize
1016
+ raydepth=8,
1017
+ camera_position=[2.3, 2.4, 2.0], # old default values
1018
+ updir=[0, 0, 1],
1019
+ # look_at=(0, 0, 0), # could be nice to have, but viewdir is good enough
1020
+ light_position=[4.0, 3.0, 2.0],
1021
+ viewdir=None,
1022
+ # projection='PERSPECTIVE', # future extension, allow different projection types
1023
+ ):
1024
+ """
1025
+ A tachyon input file (as a string) containing the this object.
1026
+
1027
+ EXAMPLES::
1028
+
1029
+ sage: print(sphere((1, 2, 3), 5, color='yellow').tachyon())
1030
+ <BLANKLINE>
1031
+ begin_scene
1032
+ resolution 500 500
1033
+ <BLANKLINE>
1034
+ camera
1035
+ ...
1036
+ plane
1037
+ center -592.870151560437 618.647114671761 -515.539262226467
1038
+ normal -2.3 2.4 -2.0
1039
+ TEXTURE
1040
+ AMBIENT 1.0 DIFFUSE 0.0 SPECULAR 0.0 OPACITY 1.0
1041
+ COLOR 1.0 1.0 1.0
1042
+ TEXFUNC 0
1043
+ <BLANKLINE>
1044
+ Texdef texture...
1045
+ Ambient 0.3333333333333333 Diffuse 0.6666666666666666 Specular 0.0 Opacity 1.0
1046
+ Color 1.0 1.0 0.0
1047
+ TexFunc 0
1048
+ <BLANKLINE>
1049
+ Sphere center 1.0 -2.0 3.0 Rad 5.0 texture...
1050
+ <BLANKLINE>
1051
+ end_scene
1052
+
1053
+ sage: G = icosahedron(color='red') + sphere((1,2,3), 0.5, color='yellow')
1054
+ sage: G.show(viewer='tachyon', frame=false)
1055
+ sage: print(G.tachyon())
1056
+ begin_scene
1057
+ ...
1058
+ Texdef texture...
1059
+ Ambient 0.3333333333333333 Diffuse 0.6666666666666666 Specular 0.0 Opacity 1.0
1060
+ Color 1.0 1.0 0.0
1061
+ TexFunc 0
1062
+ TRI V0 ...
1063
+ Sphere center 1.0 -2.0 3.0 Rad 0.5 texture...
1064
+ end_scene
1065
+ """
1066
+ render_params = self.default_render_params()
1067
+ # switch from LH to RH coords to be consistent with java rendition
1068
+ render_params.push_transform(Transformation(scale=[1,-1,1]))
1069
+
1070
+ if len(camera_position)!=3:
1071
+ raise ValueError('Camera center must consist of three numbers')
1072
+
1073
+ if viewdir is None:
1074
+ viewdir = [float(- camera_position[i]) for i in range(3)]
1075
+ if viewdir == [0.0,0.0,0.0]:
1076
+ viewdir = (1,0,0) # issue a Warning? "camera_position at origin"
1077
+ # switch from LH to RH coords to be consistent with java rendition
1078
+ viewdir = _flip_orientation(viewdir)
1079
+ updir = _flip_orientation(updir)
1080
+ camera_position = _flip_orientation(camera_position)
1081
+ light_position = _flip_orientation(light_position)
1082
+
1083
+ return """
1084
+ begin_scene
1085
+ resolution {resolution_x:d} {resolution_y:d}
1086
+
1087
+ camera
1088
+ zoom {zoom:f}
1089
+ aspectratio {aspectratio:f}
1090
+ antialiasing {antialiasing:d}
1091
+ raydepth {raydepth:d}
1092
+ center {camera_position}
1093
+ viewdir {viewdir}
1094
+ updir {updir}
1095
+ end_camera
1096
+
1097
+ light center {light_position}
1098
+ rad 0.2
1099
+ color 1.0 1.0 1.0
1100
+
1101
+ plane
1102
+ center {viewdir1000}
1103
+ normal {viewdir}
1104
+ TEXTURE
1105
+ AMBIENT 1.0 DIFFUSE 0.0 SPECULAR 0.0 OPACITY 1.0
1106
+ COLOR 1.0 1.0 1.0
1107
+ TEXFUNC 0
1108
+
1109
+ {scene}
1110
+
1111
+ {render_parameters}
1112
+
1113
+ end_scene""".format(
1114
+ # render_params.antialiasing, this only provided the default value of 8
1115
+ scene = "\n".join(sorted([t.tachyon_str() for t in self.texture_set()])),
1116
+ render_parameters =
1117
+ "\n".join(flatten_list(self.tachyon_repr(render_params))),
1118
+ viewdir1000=self._tostring(1000*vector(viewdir).normalized().n()),
1119
+ viewdir=self._tostring(viewdir),
1120
+ camera_position=self._tostring(camera_position),
1121
+ updir=self._tostring(updir),
1122
+ light_position=self._tostring(light_position),
1123
+ zoom=zoom,
1124
+ antialiasing=antialiasing,
1125
+ resolution_x=figsize[0]*100,
1126
+ resolution_y=figsize[1]*100,
1127
+ aspectratio=float(figsize[1])/float(figsize[0]),
1128
+ raydepth=raydepth,
1129
+ )
1130
+
1131
+ def obj(self):
1132
+ """
1133
+ An .obj scene file (as a string) containing the this object.
1134
+
1135
+ A .mtl file of the same name must also be produced for
1136
+ coloring.
1137
+
1138
+ EXAMPLES::
1139
+
1140
+ sage: from sage.plot.plot3d.shapes import ColorCube
1141
+ sage: print(ColorCube(1, ['red', 'yellow', 'blue']).obj())
1142
+ g obj_1
1143
+ usemtl ...
1144
+ v 1 1 1
1145
+ v -1 1 1
1146
+ v -1 -1 1
1147
+ v 1 -1 1
1148
+ f 1 2 3 4
1149
+ ...
1150
+ g obj_6
1151
+ usemtl ...
1152
+ v -1 -1 1
1153
+ v -1 1 1
1154
+ v -1 1 -1
1155
+ v -1 -1 -1
1156
+ f 21 22 23 24
1157
+ """
1158
+ return "\n".join(flatten_list([self.obj_repr(self.default_render_params()), ""]))
1159
+
1160
+ @staticmethod
1161
+ def _tostring(s):
1162
+ r"""
1163
+ Convert vector information to a space-separated string.
1164
+
1165
+ EXAMPLES::
1166
+
1167
+ sage: sage.plot.plot3d.base.Graphics3d._tostring((1.0,1.2,-1.3))
1168
+ '1.00000000000000 1.20000000000000 -1.30000000000000'
1169
+ """
1170
+ return ' '.join(map(str,s))
1171
+
1172
+ def export_jmol(self, filename='jmol_shape.jmol', force_reload=False,
1173
+ zoom=1, spin=False, background=(1,1,1), stereo=False,
1174
+ mesh=False, dots=False,
1175
+ perspective_depth = True,
1176
+ orientation = (-764,-346,-545,76.39), **ignored_kwds):
1177
+ # orientation chosen to look same as tachyon
1178
+ """
1179
+ A jmol scene consists of a script which refers to external files.
1180
+ Fortunately, we are able to put all of them in a single zip archive,
1181
+ which is the output of this call.
1182
+
1183
+ EXAMPLES::
1184
+
1185
+ sage: out_file = tmp_filename(ext='.jmol')
1186
+ sage: G = sphere((1, 2, 3), 5) + cube() + sage.plot.plot3d.shapes.Text("hi")
1187
+ sage: G.export_jmol(out_file)
1188
+ sage: import zipfile
1189
+ sage: z = zipfile.ZipFile(out_file)
1190
+ sage: z.namelist()
1191
+ ['obj_...pmesh', 'SCRIPT']
1192
+
1193
+ sage: print(z.read('SCRIPT').decode('ascii'))
1194
+ data "model list"
1195
+ 2
1196
+ empty
1197
+ Xx 0 0 0
1198
+ Xx 5.5 5.5 5.5
1199
+ end "model list"; show data
1200
+ select *
1201
+ wireframe off; spacefill off
1202
+ set labelOffset 0 0
1203
+ background [255,255,255]
1204
+ spin OFF
1205
+ moveto 0 -764 -346 -545 76.39
1206
+ centerAt absolute {0 0 0}
1207
+ zoom 100
1208
+ frank OFF
1209
+ set perspectivedepth ON
1210
+ isosurface sphere_1 center {1.0 2.0 3.0} sphere 5.0
1211
+ color isosurface [102,102,255]
1212
+ pmesh obj_... "obj_...pmesh"
1213
+ color pmesh [102,102,255]
1214
+ select atomno = 1
1215
+ color atom [102,102,255]
1216
+ label "hi"
1217
+ isosurface fullylit; pmesh o* fullylit; set antialiasdisplay on;
1218
+
1219
+ sage: print(z.read(z.namelist()[0]).decode('ascii'))
1220
+ 24
1221
+ 0.5 0.5 0.5
1222
+ -0.5 0.5 0.5
1223
+ ...
1224
+ -0.5 -0.5 -0.5
1225
+ 6
1226
+ 5
1227
+ 0
1228
+ 1
1229
+ ...
1230
+ """
1231
+ render_params = self.default_render_params()
1232
+ render_params.mesh = mesh
1233
+ render_params.dots = dots
1234
+ render_params.output_file = filename
1235
+ render_params.force_reload = render_params.randomize_counter = force_reload
1236
+ render_params.output_archive = zipfile.ZipFile(filename, 'w', zipfile.ZIP_DEFLATED, True)
1237
+ # Render the data
1238
+ all = flatten_list([self.jmol_repr(render_params), ""])
1239
+
1240
+ f = StringIO()
1241
+
1242
+ if render_params.atom_list:
1243
+ # Load the atom model
1244
+ f.write('data "model list"\n')
1245
+ f.write('%s\nempty\n' % (len(render_params.atom_list) + 1))
1246
+ for atom in render_params.atom_list:
1247
+ f.write('Xx %s %s %s\n' % atom)
1248
+ f.write('Xx 5.5 5.5 5.5\n') # so the zoom fits the box
1249
+ f.write('end "model list"; show data\n')
1250
+ f.write('select *\n')
1251
+ f.write('wireframe off; spacefill off\n')
1252
+ f.write('set labelOffset 0 0\n')
1253
+
1254
+ # Set the scene background color
1255
+ f.write('background [%s,%s,%s]\n' % tuple(int(a*255) for a in background))
1256
+ if spin:
1257
+ f.write('spin ON\n')
1258
+ else:
1259
+ f.write('spin OFF\n')
1260
+ if stereo:
1261
+ if stereo is True:
1262
+ stereo = "redblue"
1263
+ f.write('stereo %s\n' % stereo)
1264
+ if orientation:
1265
+ f.write('moveto 0 %s %s %s %s\n' % tuple(orientation))
1266
+
1267
+ f.write('centerAt absolute {0 0 0}\n')
1268
+ f.write('zoom {0}\n'.format(zoom * 100))
1269
+ f.write('frank OFF\n') # jmol logo
1270
+
1271
+ if perspective_depth:
1272
+ f.write('set perspectivedepth ON\n')
1273
+ else:
1274
+ f.write('set perspectivedepth OFF\n')
1275
+
1276
+ # Put the rest of the object in
1277
+ f.write("\n".join(all))
1278
+ # Make sure the lighting is correct
1279
+ f.write("isosurface fullylit; pmesh o* fullylit; set antialiasdisplay on;\n")
1280
+
1281
+ render_params.output_archive.writestr('SCRIPT', f.getvalue())
1282
+ render_params.output_archive.close()
1283
+
1284
+ def json_repr(self, render_params):
1285
+ """
1286
+ A (possibly nested) list of strings. Each entry is formatted
1287
+ as JSON, so that a JavaScript client could eval it and get an
1288
+ object. Each object has fields to encapsulate the faces and
1289
+ vertices of the object. This representation is intended to be
1290
+ consumed by the canvas3d viewer backend.
1291
+
1292
+ EXAMPLES::
1293
+
1294
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1295
+ sage: G.json_repr(G.default_render_params())
1296
+ []
1297
+ """
1298
+ return []
1299
+
1300
+ def jmol_repr(self, render_params):
1301
+ r"""
1302
+ A (possibly nested) list of strings which will be concatenated and
1303
+ used by jmol to render the object.
1304
+
1305
+ (Nested lists of strings are used because otherwise all the
1306
+ intermediate concatenations can kill performance). This may
1307
+ refer to several remove files, which are stored in
1308
+ render_parames.output_archive.
1309
+
1310
+ EXAMPLES::
1311
+
1312
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1313
+ sage: G.jmol_repr(G.default_render_params())
1314
+ []
1315
+ sage: G = sphere((1, 2, 3))
1316
+ sage: G.jmol_repr(G.default_render_params())
1317
+ [['isosurface sphere_1 center {1.0 2.0 3.0} sphere 1.0\ncolor isosurface [102,102,255]']]
1318
+ """
1319
+ return []
1320
+
1321
+ def tachyon_repr(self, render_params):
1322
+ r"""
1323
+ A (possibly nested) list of strings which will be concatenated and
1324
+ used by tachyon to render the object.
1325
+
1326
+ (Nested lists of strings are used because otherwise all the
1327
+ intermediate concatenations can kill performance). This may
1328
+ include a reference to color information which is stored
1329
+ elsewhere.
1330
+
1331
+ EXAMPLES::
1332
+
1333
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1334
+ sage: G.tachyon_repr(G.default_render_params())
1335
+ []
1336
+ sage: G = sphere((1, 2, 3))
1337
+ sage: G.tachyon_repr(G.default_render_params())
1338
+ ['Sphere center 1.0 2.0 3.0 Rad 1.0 texture...']
1339
+ """
1340
+ return []
1341
+
1342
+ def obj_repr(self, render_params):
1343
+ """
1344
+ A (possibly nested) list of strings which will be concatenated and
1345
+ used to construct an .obj file of the object.
1346
+
1347
+ (Nested lists of strings are used because otherwise all the
1348
+ intermediate concatenations can kill performance). This may
1349
+ include a reference to color information which is stored
1350
+ elsewhere.
1351
+
1352
+ EXAMPLES::
1353
+
1354
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1355
+ sage: G.obj_repr(G.default_render_params())
1356
+ []
1357
+ sage: G = cube()
1358
+ sage: G.obj_repr(G.default_render_params())
1359
+ ['g obj_1',
1360
+ 'usemtl ...',
1361
+ ['v 0.5 0.5 0.5',
1362
+ 'v -0.5 0.5 0.5',
1363
+ 'v -0.5 -0.5 0.5',
1364
+ 'v 0.5 -0.5 0.5',
1365
+ 'v 0.5 0.5 -0.5',
1366
+ 'v -0.5 0.5 -0.5',
1367
+ 'v 0.5 -0.5 -0.5',
1368
+ 'v -0.5 -0.5 -0.5'],
1369
+ ['f 1 2 3 4',
1370
+ 'f 1 5 6 2',
1371
+ 'f 1 4 7 5',
1372
+ 'f 6 5 7 8',
1373
+ 'f 7 4 3 8',
1374
+ 'f 3 2 6 8'],
1375
+ []]
1376
+ """
1377
+ return []
1378
+
1379
+ def threejs_repr(self, render_params):
1380
+ """
1381
+ A flat list of ``(kind, desc)`` tuples where ``kind`` is one of:
1382
+ 'point', 'line', 'text', or 'surface'; and where ``desc`` is a dictionary
1383
+ describing a point, line, text, or surface.
1384
+
1385
+ EXAMPLES::
1386
+
1387
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1388
+ sage: G.threejs_repr(G.default_render_params())
1389
+ []
1390
+ """
1391
+ return []
1392
+
1393
+ def texture_set(self):
1394
+ """
1395
+ Often the textures of a 3d file format are kept separate from the
1396
+ objects themselves. This function returns the set of textures used,
1397
+ so they can be defined in a preamble or separate file.
1398
+
1399
+ EXAMPLES::
1400
+
1401
+ sage: sage.plot.plot3d.base.Graphics3d().texture_set()
1402
+ set()
1403
+
1404
+ sage: G = tetrahedron(color='red') + tetrahedron(color='yellow') + tetrahedron(color='red', opacity=0.5)
1405
+ sage: [t for t in G.texture_set() if t.color == colors.red] # we should have two red textures
1406
+ [Texture(texture..., red, ff0000), Texture(texture..., red, ff0000)]
1407
+ sage: [t for t in G.texture_set() if t.color == colors.yellow] # ...and one yellow
1408
+ [Texture(texture..., yellow, ffff00)]
1409
+ """
1410
+ return set()
1411
+
1412
+ def mtl_str(self):
1413
+ """
1414
+ Return the contents of a .mtl file, to be used to provide coloring
1415
+ information for an .obj file.
1416
+
1417
+ EXAMPLES::
1418
+
1419
+ sage: G = tetrahedron(color='red') + tetrahedron(color='yellow', opacity=0.5)
1420
+ sage: print(G.mtl_str())
1421
+ newmtl ...
1422
+ Ka 0.5 5e-06 5e-06
1423
+ Kd 1.0 1e-05 1e-05
1424
+ Ks 0.0 0.0 0.0
1425
+ illum 1
1426
+ Ns 1.0
1427
+ d 1.0
1428
+ newmtl ...
1429
+ Ka 0.5 0.5 5e-06
1430
+ Kd 1.0 1.0 1e-05
1431
+ Ks 0.0 0.0 0.0
1432
+ illum 1
1433
+ Ns 1.0
1434
+ d 0.5
1435
+ """
1436
+ return "\n\n".join(sorted([t.mtl_str() for t in self.texture_set()])) + "\n"
1437
+
1438
+ def flatten(self):
1439
+ """
1440
+ Try to reduce the depth of the scene tree by consolidating groups
1441
+ and transformations.
1442
+
1443
+ The generic Graphics3d object cannot be made flatter.
1444
+
1445
+ EXAMPLES::
1446
+
1447
+ sage: G = sage.plot.plot3d.base.Graphics3d()
1448
+ sage: G.flatten() is G
1449
+ True
1450
+ """
1451
+ return self
1452
+
1453
+ def _rescale_for_frame_aspect_ratio_and_zoom(self, b, frame_aspect_ratio, zoom):
1454
+ if frame_aspect_ratio is None:
1455
+ return (b*zoom,b*zoom,b*zoom), (-b*zoom,-b*zoom,-b*zoom)
1456
+ box = [b*w for w in frame_aspect_ratio]
1457
+ # Now take the maximum length in box and rescale to b.
1458
+ s = b / max(box)
1459
+ box_max = tuple([s*w*zoom for w in box])
1460
+ box_min = tuple([-w*zoom for w in box_max])
1461
+ return box_min, box_max
1462
+
1463
+ def _prepare_for_jmol(self, frame, axes, frame_aspect_ratio, aspect_ratio, zoom):
1464
+ s = 3
1465
+ box_min, box_max = self._rescale_for_frame_aspect_ratio_and_zoom(s, frame_aspect_ratio, zoom)
1466
+ a_min, a_max = self._box_for_aspect_ratio(aspect_ratio, box_min, box_max)
1467
+ return self._transform_to_bounding_box(box_min, box_max, a_min, a_max, frame=frame,
1468
+ axes=axes, thickness=1,
1469
+ labels = True) # jmol labels are implemented
1470
+
1471
+ def _prepare_for_tachyon(self, frame, axes, frame_aspect_ratio, aspect_ratio, zoom):
1472
+ box_min, box_max = self._rescale_for_frame_aspect_ratio_and_zoom(1.0, frame_aspect_ratio, zoom)
1473
+ a_min, a_max = self._box_for_aspect_ratio(aspect_ratio, box_min, box_max)
1474
+ return self._transform_to_bounding_box(box_min, box_max, a_min, a_max,
1475
+ frame=frame, axes=axes, thickness=.75,
1476
+ labels = False) # no tachyon text implemented yet
1477
+
1478
+ def _box_for_aspect_ratio(self, aspect_ratio, box_min, box_max):
1479
+ # 1. Find a box around self so that when self gets rescaled into the
1480
+ # box defined by box_min, box_max, it has the right aspect ratio
1481
+ a_min, a_max = self._safe_bounding_box()
1482
+
1483
+ if aspect_ratio == "automatic" or aspect_ratio == [1.0]*3:
1484
+ return a_min, a_max
1485
+
1486
+ longest_side = 0
1487
+ longest_length = a_max[0] - a_min[0]
1488
+ shortest_side = 0
1489
+ shortest_length = a_max[0] - a_min[0]
1490
+
1491
+ for i in range(3):
1492
+ s = a_max[i] - a_min[i]
1493
+ if s > longest_length:
1494
+ longest_length = s
1495
+ longest_side = i
1496
+ if s < shortest_length:
1497
+ shortest_length = s
1498
+ shortest_side = i
1499
+
1500
+ # 2. Rescale aspect_ratio so the shortest side is 1.
1501
+ r = float(aspect_ratio[shortest_side])
1502
+ aspect_ratio = [a/r for a in aspect_ratio]
1503
+
1504
+ # 3. Extend the bounding box of self by rescaling so the sides
1505
+ # have the same ratio as aspect_ratio, and without changing
1506
+ # the longest side.
1507
+ long_box_side = box_max[longest_side] - box_min[longest_side]
1508
+ sc = [1.0,1.0,1.0]
1509
+ for i in range(3):
1510
+ # compute the length we want:
1511
+ new_length = longest_length / aspect_ratio[i]
1512
+ # change the side length by a_min and a_max so
1513
+ # that a_max[i] - a_min[i] = new_length
1514
+
1515
+ # We have to take into account the ratio of the
1516
+ # sides after transforming to the bounding box.
1517
+ z = long_box_side / (box_max[i] - box_min[i])
1518
+ w = new_length / ((a_max[i] - a_min[i]) * z)
1519
+ sc[i] = w
1520
+
1521
+ w = min(sc)
1522
+ sc = [z/w for z in sc]
1523
+ for i in range(3):
1524
+ a_min[i] *= sc[i]
1525
+ a_max[i] *= sc[i]
1526
+
1527
+ return a_min, a_max
1528
+
1529
+ def _transform_to_bounding_box(self, xyz_min, xyz_max, a_min, a_max, frame, axes, thickness, labels):
1530
+
1531
+ a_min_orig = a_min
1532
+ a_max_orig = a_max
1533
+
1534
+ # Rescale in each direction
1535
+ scale = [float(xyz_max[i] - xyz_min[i]) / (a_max[i] - a_min[i]) for i in range(3)]
1536
+ X = self.scale(scale)
1537
+ a_min = [scale[i]*a_min[i] for i in range(3)]
1538
+ a_max = [scale[i]*a_max[i] for i in range(3)]
1539
+
1540
+ # Translate so lower left corner of original bounding box
1541
+ # is in the right spot
1542
+ T = [xyz_min[i] - a_min[i] for i in range(3)]
1543
+ X = X.translate(T)
1544
+ if frame:
1545
+ from sage.plot.plot3d.shapes2 import frame3d, frame_labels
1546
+ F = frame3d(xyz_min, xyz_max, opacity=0.5, color=(0,0,0), thickness=thickness)
1547
+ if labels:
1548
+ F += frame_labels(xyz_min, xyz_max, a_min_orig, a_max_orig)
1549
+
1550
+ X += F
1551
+
1552
+ if axes:
1553
+ # draw axes
1554
+ from sage.plot.plot3d.shapes import arrow3d
1555
+ A = (arrow3d((min(0,a_min[0]),0, 0), (max(0,a_max[0]), 0,0),
1556
+ thickness, color='blue'),
1557
+ arrow3d((0,min(0,a_min[1]), 0), (0, max(0,a_max[1]), 0),
1558
+ thickness, color='blue'),
1559
+ arrow3d((0, 0, min(0,a_min[2])), (0, 0, max(0,a_max[2])),
1560
+ thickness, color='blue'))
1561
+ X += sum(A).translate([-z for z in T])
1562
+
1563
+ return X
1564
+
1565
+ def _process_viewing_options(self, kwds):
1566
+ """
1567
+ Process viewing options (the keywords passed to show()) and return a new
1568
+ dictionary. Defaults will be filled in for missing options and taken from
1569
+ self._extra_kwds as well. Options that have the value "automatic" will be
1570
+ automatically determined. Finally, the provided dictionary is modified
1571
+ to remove all of the keys that were used -- so that the unused keywords
1572
+ can be used elsewhere.
1573
+ """
1574
+ opts = {}
1575
+ opts.update(SHOW_DEFAULTS)
1576
+ opts.update(self._extra_kwds)
1577
+ opts.update(kwds)
1578
+
1579
+ # Remove all of the keys that are viewing options, since the remaining
1580
+ # kwds might be passed on.
1581
+ for key_to_remove in SHOW_DEFAULTS:
1582
+ kwds.pop(key_to_remove, None)
1583
+
1584
+ # deal with any aspect_ratio instances passed from the default options to plot
1585
+ if opts['aspect_ratio'] == 'auto':
1586
+ opts['aspect_ratio'] = 'automatic'
1587
+ if opts['aspect_ratio'] != 'automatic':
1588
+ # We need this round about way to make sure that we do not
1589
+ # store the aspect ratio that was passed on to show() by the
1590
+ # user. We let the .aspect_ratio() method take care of the
1591
+ # validity of the arguments that was passed on to show()
1592
+ original_aspect_ratio = self.aspect_ratio()
1593
+ self.aspect_ratio(opts['aspect_ratio'])
1594
+ opts['aspect_ratio'] = self.aspect_ratio()
1595
+ self.aspect_ratio(original_aspect_ratio)
1596
+
1597
+ if opts['frame_aspect_ratio'] == 'automatic':
1598
+ if opts['aspect_ratio'] != 'automatic':
1599
+ # Set the aspect_ratio of the frame to be the same as that
1600
+ # of the object we are rendering given the aspect_ratio
1601
+ # we'll use for it.
1602
+ opts['frame_aspect_ratio'] = \
1603
+ self._determine_frame_aspect_ratio(opts['aspect_ratio'])
1604
+ else:
1605
+ opts['frame_aspect_ratio'] = self.frame_aspect_ratio()
1606
+ else:
1607
+ # We need this round about way to make sure that we do not
1608
+ # store the frame aspect ratio that was passed on to show() by
1609
+ # the user. We let the .frame_aspect_ratio() method take care
1610
+ # of the validity of the arguments that was passed on to show()
1611
+ original_aspect_ratio = self.frame_aspect_ratio()
1612
+ self.frame_aspect_ratio(opts['frame_aspect_ratio'])
1613
+ opts['frame_aspect_ratio'] = self.frame_aspect_ratio()
1614
+ self.frame_aspect_ratio(original_aspect_ratio)
1615
+
1616
+ if opts['aspect_ratio'] == 'automatic':
1617
+ opts['aspect_ratio'] = self.aspect_ratio()
1618
+
1619
+ if not isinstance(opts['figsize'], (list,tuple)):
1620
+ opts['figsize'] = [opts['figsize'], opts['figsize']]
1621
+
1622
+ return opts
1623
+
1624
+ def show(self, **kwds):
1625
+ r"""
1626
+ Display graphics immediately.
1627
+
1628
+ This method attempts to display the graphics immediately,
1629
+ without waiting for the currently running code (if any) to
1630
+ return to the command line. Be careful, calling it from within
1631
+ a loop will potentially launch a large number of external
1632
+ viewer programs.
1633
+
1634
+ INPUT:
1635
+
1636
+ - ``viewer`` -- string (default: ``'threejs'``); how to view the plot.
1637
+ Admissible values are
1638
+
1639
+ * ``'threejs'``: interactive web-based 3D viewer using JavaScript
1640
+ and a WebGL renderer
1641
+
1642
+ * ``'jmol'``: interactive 3D viewer using Java
1643
+
1644
+ * ``'tachyon'``: ray tracer generating a static PNG image;
1645
+ can produce high-resolution graphics, but does
1646
+ not show any text labels
1647
+
1648
+ * ``'canvas3d'``: web-based 3D viewer using JavaScript
1649
+ and a canvas renderer (Sage notebook only)
1650
+
1651
+ - ``verbosity`` -- display information about rendering
1652
+ the figure
1653
+
1654
+ - ``figsize`` -- (default: 5) x or pair [x,y] for
1655
+ numbers, e.g., [5,5]; controls the size of the output figure.
1656
+ With 'tachyon', the resolution (in number of pixels) is 100 times
1657
+ ``figsize``. This is ignored for the jmol embedded renderer.
1658
+
1659
+ - ``aspect_ratio`` -- (default: ``'automatic'``) aspect
1660
+ ratio of the coordinate system itself; give [1,1,1] or 1 to make
1661
+ spheres look round
1662
+
1663
+ - ``frame_aspect_ratio`` -- (default: ``'automatic'``)
1664
+ aspect ratio of frame that contains the 3d scene
1665
+
1666
+ - ``zoom`` -- (default: 1) how zoomed in
1667
+
1668
+ - ``frame`` -- boolean (default: ``True``); if ``True``, draw a
1669
+ bounding frame with labels
1670
+
1671
+ - ``axes`` -- boolean (default: ``False``); if ``True``, draw coordinate
1672
+ axes
1673
+
1674
+ - ``camera_position`` -- (for tachyon) (default: (2.3, 2.4, 2.0))
1675
+ the viewpoint, with respect to the cube
1676
+ $[-1,1]\\times[-1,1]\\times[-1,1]$,
1677
+ into which the bounding box of the scene
1678
+ is scaled and centered.
1679
+ The default viewing direction is towards the origin.
1680
+
1681
+ - ``viewdir`` -- (for tachyon) (default: ``None``) three coordinates
1682
+ specifying the viewing direction
1683
+
1684
+ - ``updir`` -- (for tachyon) (default: (0,0,1)) the "upward"
1685
+ direction of the camera
1686
+
1687
+ - ``light_position`` -- (for tachyon) (default: (4,3,2)) the position
1688
+ of the single light source in the scene (in addition to ambient light)
1689
+
1690
+ - ``antialiasing`` -- (for tachyon) (default: ``False``)
1691
+
1692
+ - ``raydepth`` -- (for tachyon) (default: 8)
1693
+ see the :class:`sage.plot.plot3d.tachyon.Tachyon` class
1694
+
1695
+ - ``shade`` -- (for tachyon) string (default: ``'full'``);
1696
+ shading options. Admissible values are
1697
+
1698
+ * ``'full'``: best quality rendering (and slowest).
1699
+ Sets tachyon command line flag ``-fullshade``.
1700
+
1701
+ * ``'medium``: good quality rendering, but no shadows.
1702
+ Sets tachyon command line flag ``-mediumshade``.
1703
+
1704
+ * ``'low'``: low quality rendering, preview (and fast).
1705
+ Sets tachyon command line flag ``-lowshade``.
1706
+
1707
+ * ``'lowest'``: worst quality rendering, preview (and fastest).
1708
+ Sets tachyon command line flag ``-lowestshade``.
1709
+
1710
+ - ``extra_opts`` -- (for tachyon) string (default: empty string);
1711
+ extra options that will be appended to the tachyon command line
1712
+
1713
+ - ``**kwds`` -- other options, which make sense for particular
1714
+ rendering engines
1715
+
1716
+ OUTPUT:
1717
+
1718
+ This method does not return anything. Use :meth:`save` if you
1719
+ want to save the figure as an image file.
1720
+
1721
+ .. WARNING::
1722
+
1723
+ By default, the jmol and tachyon viewers perform
1724
+ some non-uniform scaling of the axes.
1725
+
1726
+ If this is not desired, one can set ``aspect_ratio=1``::
1727
+
1728
+ sage: p = plot3d(lambda u,v:(cos(u)-cos(v)), (-0.2,0.2),(-0.2,0.2))
1729
+ sage: p.show(viewer='threejs')
1730
+ sage: p.show(viewer='jmol')
1731
+ sage: p.show(viewer='jmol',aspect_ratio=1)
1732
+ sage: p.show(viewer='tachyon',camera_position=(4,0,0))
1733
+ sage: p.show(viewer='tachyon',camera_position=(2,2,0.3),aspect_ratio=1)
1734
+
1735
+ CHANGING DEFAULTS: Defaults can be uniformly changed by importing a
1736
+ dictionary and changing it. For example, here we change the default
1737
+ so images display without a frame instead of with one::
1738
+
1739
+ sage: from sage.plot.plot3d.base import SHOW_DEFAULTS
1740
+ sage: SHOW_DEFAULTS['frame'] = False
1741
+
1742
+ This sphere will not have a frame around it::
1743
+
1744
+ sage: sphere((0,0,0))
1745
+ Graphics3d Object
1746
+
1747
+ We change the default back::
1748
+
1749
+ sage: SHOW_DEFAULTS['frame'] = True
1750
+
1751
+ Now this sphere is enclosed in a frame::
1752
+
1753
+ sage: sphere((0,0,0))
1754
+ Graphics3d Object
1755
+
1756
+ EXAMPLES: We illustrate use of the ``aspect_ratio`` option::
1757
+
1758
+ sage: x, y = var('x,y') # needs sage.symbolic
1759
+ sage: p = plot3d(2*sin(x*y), (x, -pi, pi), (y, -pi, pi)) # needs sage.symbolic
1760
+ sage: p.show(aspect_ratio=[1,1,1]) # needs sage.symbolic
1761
+
1762
+ This looks flattened, but filled with the plot::
1763
+
1764
+ sage: p.show(frame_aspect_ratio=[1,1,1/16]) # needs sage.symbolic
1765
+
1766
+ This looks flattened, but the plot is square and smaller::
1767
+
1768
+ sage: p.show(aspect_ratio=[1,1,1], frame_aspect_ratio=[1,1,1/8]) # needs sage.symbolic
1769
+
1770
+ This example shows indirectly that the defaults
1771
+ from :func:`~sage.plot.plot.plot` are dealt with properly::
1772
+
1773
+ sage: plot(vector([1,2,3]))
1774
+ Graphics3d Object
1775
+
1776
+ We use the 'canvas3d' backend from inside the notebook to get a view of
1777
+ the plot rendered inline using HTML canvas::
1778
+
1779
+ sage: p.show(viewer='canvas3d') # needs sage.symbolic
1780
+
1781
+ Sometimes shadows in Tachyon-produced images can lead to confusing
1782
+ plots. To remove them::
1783
+
1784
+ sage: p.show(viewer='tachyon', shade='medium') # needs sage.symbolic
1785
+
1786
+ One can also pass Tachyon command line flags directly. For example,
1787
+ the following line produces the same result as the previous one::
1788
+
1789
+ sage: p.show(viewer='tachyon', extra_opts='-mediumshade') # needs sage.symbolic
1790
+ """
1791
+ from sage.repl.rich_output import get_display_manager
1792
+ dm = get_display_manager()
1793
+ dm.display_immediately(self, **kwds)
1794
+
1795
+ def _save_image_png(self, filename, **kwds):
1796
+ r"""
1797
+ Save a PNG rendering.
1798
+
1799
+ This private method is only for use by :meth:`save_image`.
1800
+
1801
+ EXAMPLES::
1802
+
1803
+ sage: s = sphere()
1804
+ sage: png = tmp_filename(ext='.png')
1805
+ sage: s._save_image_png(png)
1806
+ sage: with open(png, 'rb') as fobj:
1807
+ ....: fobj.read().startswith(b'\x89PNG')
1808
+ True
1809
+
1810
+ sage: s._save_image_png('/path/to/foo.bar')
1811
+ Traceback (most recent call last):
1812
+ ...
1813
+ AssertionError
1814
+ """
1815
+ assert filename.endswith('.png')
1816
+ opts = self._process_viewing_options(kwds)
1817
+ viewer = opts['viewer']
1818
+ if viewer == 'threejs':
1819
+ try:
1820
+ scene = self._rich_repr_threejs(**opts)
1821
+ from playwright.sync_api import sync_playwright
1822
+ import subprocess
1823
+ # From https://github.com/jupyter/nbconvert/blob/55ff3e9ac1d892165bfb4352379672019b66ddff/nbconvert/exporters/webpdf.py#L87
1824
+ cmd = [sys.executable, "-m", "playwright", "install", "chromium"]
1825
+ subprocess.check_call(cmd)
1826
+ with sync_playwright() as p:
1827
+ browser_type = getattr(p, kwds.pop('browser_type', 'chromium'))
1828
+ browser = browser_type.launch()
1829
+ page = browser.new_page()
1830
+ page.goto('file://' + scene.html.filename('html'))
1831
+ page.screenshot(path=filename)
1832
+ browser.close()
1833
+ return
1834
+ except Exception:
1835
+ viewer = 'jmol'
1836
+ if viewer == 'tachyon':
1837
+ from sage.repl.rich_output.output_catalog import OutputImagePng
1838
+ render = self._rich_repr_tachyon(OutputImagePng, **opts)
1839
+ render.png.save_as(filename)
1840
+ elif viewer == 'jmol':
1841
+ scene = self._rich_repr_jmol(**opts)
1842
+ scene.preview_png.save_as(filename)
1843
+ else:
1844
+ raise ValueError('cannot use viewer={0} to render image'.format(viewer))
1845
+
1846
+ def save_image(self, filename, **kwds):
1847
+ r"""
1848
+ Save a 2-D image rendering.
1849
+
1850
+ The image type is determined by the extension of the filename.
1851
+ For example, this could be ``.png``, ``.jpg``, ``.gif``,
1852
+ ``.pdf``, ``.svg``.
1853
+
1854
+ INPUT:
1855
+
1856
+ - ``filename`` -- string; the file name under which to save
1857
+ the image
1858
+
1859
+ Any further keyword arguments are passed to the renderer.
1860
+
1861
+ EXAMPLES::
1862
+
1863
+ sage: G = sphere()
1864
+ sage: png = tmp_filename(ext='.png')
1865
+ sage: G.save_image(png)
1866
+ sage: with open(png, 'rb') as fobj:
1867
+ ....: assert fobj.read().startswith(b'\x89PNG')
1868
+
1869
+ sage: gif = tmp_filename(ext='.gif')
1870
+ sage: G.save_image(gif)
1871
+ sage: with open(gif, 'rb') as fobj:
1872
+ ....: assert fobj.read().startswith(b'GIF')
1873
+ """
1874
+ ext = os.path.splitext(filename)[1].lower()
1875
+ if ext not in ['.bmp', '.png', '.gif', '.ppm', '.tiff', '.tif',
1876
+ '.jpg', '.jpeg']:
1877
+ raise ValueError('unknown image file type: {0}'.format(ext))
1878
+ if ext == '.png':
1879
+ self._save_image_png(filename, **kwds)
1880
+ else:
1881
+ png = tmp_filename(ext='.png')
1882
+ self._save_image_png(png, **kwds)
1883
+ import PIL.Image as Image
1884
+ Image.open(png).save(filename)
1885
+
1886
+ def save(self, filename, **kwds):
1887
+ """
1888
+ Save the graphic in a file.
1889
+
1890
+ The file type depends on the file extension you give in the
1891
+ filename. This can be either:
1892
+
1893
+ - an image file (of type: PNG, BMP, GIF, PPM, or TIFF) rendered
1894
+ using Jmol (default) or Tachyon,
1895
+
1896
+ - a Sage object file (of type ``.sobj``) that you can load back later
1897
+ (a pickle),
1898
+
1899
+ - an HTML file depicting the graphic using the Three.js viewer,
1900
+
1901
+ - a data file (of type: X3D, STL, AMF, PLY) for export and use in
1902
+ other software.
1903
+
1904
+ For data files, the support is only partial. For instance STL and
1905
+ AMF only works for triangulated surfaces. The prefered format is X3D.
1906
+
1907
+ INPUT:
1908
+
1909
+ - ``filename`` -- string; where to save the image or object
1910
+
1911
+ - ``**kwds`` -- when specifying an image file to be rendered by Tachyon
1912
+ or Jmol, any of the viewing options accepted by :meth:`show` are valid as
1913
+ keyword arguments to this function and they will behave in the same
1914
+ way. Accepted keywords include: ``viewer``, ``verbosity``,
1915
+ ``figsize``, ``aspect_ratio``, ``frame_aspect_ratio``, ``zoom``,
1916
+ ``frame``, and ``axes``. Default values are provided.
1917
+
1918
+ EXAMPLES::
1919
+
1920
+ sage: f = tmp_filename(ext='.png')
1921
+ sage: G = sphere()
1922
+ sage: G.save(f)
1923
+
1924
+ We demonstrate using keyword arguments to control the appearance of the
1925
+ output image::
1926
+
1927
+ sage: G.save(f, zoom=2, figsize=[5, 10])
1928
+
1929
+ Using Tachyon instead of the default viewer (Jmol) to create the
1930
+ image::
1931
+
1932
+ sage: G.save(f, viewer='tachyon')
1933
+
1934
+ Since Tachyon only outputs PNG images, PIL will be used to convert to
1935
+ alternate formats::
1936
+
1937
+ sage: cube().save(tmp_filename(ext='.gif'), viewer='tachyon')
1938
+
1939
+ Here is how to save in one of the data formats::
1940
+
1941
+ sage: f = tmp_filename(ext='.x3d')
1942
+ sage: cube().save(f)
1943
+
1944
+ sage: open(f).read().splitlines()[7]
1945
+ "<Shape><Box size='0.5 0.5 0.5'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>"
1946
+
1947
+ Producing a Three.js-based HTML file::
1948
+
1949
+ sage: f = tmp_filename(ext='.html')
1950
+ sage: G.save(f, frame=False, online=True)
1951
+ """
1952
+ ext = os.path.splitext(filename)[1].lower()
1953
+ if ext == '' or ext == '.sobj':
1954
+ SageObject.save(self, filename)
1955
+ elif ext in ['.bmp', '.png', '.gif', '.ppm', '.tiff', '.tif',
1956
+ '.jpg', '.jpeg']:
1957
+ self.save_image(filename, **kwds)
1958
+ elif filename.endswith('.spt.zip'):
1959
+ scene = self._rich_repr_jmol(**kwds)
1960
+ scene.jmol.save(filename)
1961
+ elif ext == '.x3d':
1962
+ with open(filename, 'w') as outfile:
1963
+ outfile.write(self.x3d())
1964
+ elif ext == '.stl':
1965
+ with open(filename, 'wb') as outfile:
1966
+ outfile.write(self.stl_binary())
1967
+ elif ext == '.amf':
1968
+ # todo : zip the output file ?
1969
+ with open(filename, 'w') as outfile:
1970
+ outfile.write(self.amf_ascii_string())
1971
+ elif ext == '.ply':
1972
+ with open(filename, 'w') as outfile:
1973
+ outfile.write(self.ply_ascii_string())
1974
+ elif ext == '.html':
1975
+ self._rich_repr_threejs(**kwds).html.save_as(filename)
1976
+ else:
1977
+ raise ValueError('filetype {} not supported by save()'.format(ext))
1978
+
1979
+ def stl_binary(self):
1980
+ """
1981
+ Return an STL (STereoLithography) binary representation of the surface.
1982
+
1983
+ .. WARNING::
1984
+
1985
+ This only works for surfaces, transforms and unions of surfaces,
1986
+ but not for general plot objects!
1987
+
1988
+ OUTPUT: a binary string that represents the surface in the binary STL format
1989
+
1990
+ See :wikipedia:`STL_(file_format)`
1991
+
1992
+ .. SEEALSO:: :meth:`stl_ascii_string`
1993
+
1994
+ EXAMPLES::
1995
+
1996
+ sage: # needs sage.symbolic
1997
+ sage: x,y,z = var('x,y,z')
1998
+ sage: a = implicit_plot3d(x^2+y^2+z^2-9,[x,-5,5],[y,-5,5],[z,-5,5])
1999
+ sage: astl = a.stl_binary()
2000
+ sage: print(astl[:40].decode('ascii'))
2001
+ STL binary file / made by SageMath / ###
2002
+
2003
+ sage: p = polygon3d([[0,0,0], [1,2,3], [3,0,0]])
2004
+ sage: print(p.stl_binary()[:40].decode('ascii'))
2005
+ STL binary file / made by SageMath / ###
2006
+
2007
+ This works when faces have more then 3 sides::
2008
+
2009
+ sage: # needs sage.geometry.polyhedron sage.groups
2010
+ sage: P = polytopes.dodecahedron()
2011
+ sage: Q = P.plot().all[-1]
2012
+ sage: print(Q.stl_binary()[:40].decode('ascii'))
2013
+ STL binary file / made by SageMath / ###
2014
+ """
2015
+ import struct
2016
+ header = b'STL binary file / made by SageMath / '
2017
+ header += b'#' * (80 - len(header))
2018
+ # header = 80 bytes, arbitrary ascii characters
2019
+ data = self.stl_binary_repr(self.default_render_params())
2020
+ N_triangles = len(data)
2021
+ return b''.join([header, struct.pack('I', N_triangles)] + data)
2022
+
2023
+ def stl_ascii_string(self, name='surface'):
2024
+ """
2025
+ Return an STL (STereoLithography) representation of the surface.
2026
+
2027
+ .. WARNING::
2028
+
2029
+ This only works for surfaces, not for general plot objects!
2030
+
2031
+ INPUT:
2032
+
2033
+ - ``name`` -- string (default: ``'surface'``); name of the surface
2034
+
2035
+ OUTPUT: string that represents the surface in the STL format
2036
+
2037
+ See :wikipedia:`STL_(file_format)`
2038
+
2039
+ .. SEEALSO:: :meth:`stl_binary`
2040
+
2041
+ EXAMPLES::
2042
+
2043
+ sage: # needs sage.symbolic
2044
+ sage: x,y,z = var('x,y,z')
2045
+ sage: a = implicit_plot3d(x^2+y^2+z^2-9,[x,-5,5],[y,-5,5],[z,-5,5])
2046
+ sage: astl = a.stl_ascii_string()
2047
+ sage: astl.splitlines()[:7] # abs tol 1e-10
2048
+ ['solid surface',
2049
+ 'facet normal 0.9733285267845754 -0.16222142113076257 -0.16222142113076257',
2050
+ ' outer loop',
2051
+ ' vertex 2.94871794872 -0.384615384615 -0.39358974359',
2052
+ ' vertex 2.95021367521 -0.384615384615 -0.384615384615',
2053
+ ' vertex 2.94871794872 -0.39358974359 -0.384615384615',
2054
+ ' endloop']
2055
+
2056
+ sage: p = polygon3d([[0,0,0], [1,2,3], [3,0,0]])
2057
+ sage: print(p.stl_ascii_string(name='triangle'))
2058
+ solid triangle
2059
+ facet normal 0.0 0.8320502943378436 -0.5547001962252291
2060
+ outer loop
2061
+ vertex 0.0 0.0 0.0
2062
+ vertex 1.0 2.0 3.0
2063
+ vertex 3.0 0.0 0.0
2064
+ endloop
2065
+ endfacet
2066
+ endsolid triangle
2067
+
2068
+ Now works when faces have more then 3 sides::
2069
+
2070
+ sage: # needs sage.geometry.polyhedron sage.groups
2071
+ sage: P = polytopes.dodecahedron()
2072
+ sage: Q = P.plot().all[-1]
2073
+ sage: print(Q.stl_ascii_string().splitlines()[:7])
2074
+ ['solid surface',
2075
+ 'facet normal 0.0 0.5257311121191338 0.8506508083520399',
2076
+ ' outer loop',
2077
+ ' vertex -0.7639320225002102 0.7639320225002102 0.7639320225002102',
2078
+ ' vertex -0.4721359549995796 0.0 1.2360679774997898',
2079
+ ' vertex 0.4721359549995796 0.0 1.2360679774997898',
2080
+ ' endloop']
2081
+ """
2082
+ from sage.modules.free_module import FreeModule
2083
+ RR3 = FreeModule(RDF, 3)
2084
+
2085
+ faces = self.face_list()
2086
+ if not faces:
2087
+ self.triangulate()
2088
+ faces = self.face_list()
2089
+
2090
+ code = ("facet normal {!r} {!r} {!r}\n"
2091
+ " outer loop\n"
2092
+ " vertex {!r} {!r} {!r}\n"
2093
+ " vertex {!r} {!r} {!r}\n"
2094
+ " vertex {!r} {!r} {!r}\n"
2095
+ " endloop\n"
2096
+ "endfacet\n")
2097
+
2098
+ faces_iter = faces.__iter__()
2099
+
2100
+ def chopped_faces_iter():
2101
+ for face in faces_iter:
2102
+ n = len(face)
2103
+ if n == 3:
2104
+ yield face
2105
+ else:
2106
+ # naive cut into triangles
2107
+ v = face[-1]
2108
+ for i in range(n - 2):
2109
+ yield [v, face[i], face[i + 1]]
2110
+
2111
+ string_list = ["solid {}\n".format(name)]
2112
+ for i, j, k in chopped_faces_iter():
2113
+ ij = RR3(j) - RR3(i)
2114
+ ik = RR3(k) - RR3(i)
2115
+ n = ij.cross_product(ik)
2116
+ n = n / n.norm()
2117
+ string_list += [code.format(n[0], n[1], n[2],
2118
+ i[0], i[1], i[2],
2119
+ j[0], j[1], j[2],
2120
+ k[0], k[1], k[2])]
2121
+ string_list += ["endsolid {}".format(name)]
2122
+ return "".join(string_list)
2123
+
2124
+ def ply_ascii_string(self, name='surface'):
2125
+ """
2126
+ Return a PLY (Polygon File Format) representation of the surface.
2127
+
2128
+ INPUT:
2129
+
2130
+ - ``name`` -- string (default: ``'surface'``); name of the surface
2131
+
2132
+ OUTPUT: string that represents the surface in the PLY format
2133
+
2134
+ See :wikipedia:`PLY_(file_format)`.
2135
+
2136
+ EXAMPLES::
2137
+
2138
+ sage: # needs sage.symbolic
2139
+ sage: x,y,z = var('x,y,z')
2140
+ sage: a = implicit_plot3d(x^2+y^2+z^2-9,[x,-5,5],[y,-5,5],[z,-5,5])
2141
+ sage: astl = a.ply_ascii_string()
2142
+ sage: astl.splitlines()[:10]
2143
+ ['ply',
2144
+ 'format ascii 1.0',
2145
+ 'comment surface',
2146
+ 'element vertex 15540',
2147
+ 'property float x',
2148
+ 'property float y',
2149
+ 'property float z',
2150
+ 'element face 5180',
2151
+ 'property list uchar int vertex_indices',
2152
+ 'end_header']
2153
+
2154
+ sage: p = polygon3d([[0,0,0], [1,2,3], [3,0,0]])
2155
+ sage: print(p.ply_ascii_string(name='triangle'))
2156
+ ply
2157
+ format ascii 1.0
2158
+ comment triangle
2159
+ element vertex 3
2160
+ property float x
2161
+ property float y
2162
+ property float z
2163
+ element face 1
2164
+ property list uchar int vertex_indices
2165
+ end_header
2166
+ 0.0 0.0 0.0
2167
+ 1.0 2.0 3.0
2168
+ 3.0 0.0 0.0
2169
+ 3 0 1 2
2170
+ """
2171
+ faces = self.index_faces()
2172
+ if not faces:
2173
+ self.triangulate()
2174
+ faces = self.index_faces()
2175
+
2176
+ string_list = ["ply\nformat ascii 1.0\ncomment {}\nelement vertex {}\nproperty float x\nproperty float y\nproperty float z\nelement face {}\nproperty list uchar int vertex_indices\nend_header\n".format(name, len(self.vertex_list()), len(faces))]
2177
+
2178
+ vertex_template = '{} {} {}\n'
2179
+ for v in self.vertices():
2180
+ string_list += [vertex_template.format(*v)]
2181
+
2182
+ for f in faces:
2183
+ string_list += [str(len(f))
2184
+ + ''.join(' {}'.format(k) for k in f) + '\n']
2185
+
2186
+ return "".join(string_list)
2187
+
2188
+ def amf_ascii_string(self, name='surface'):
2189
+ """
2190
+ Return an AMF (Additive Manufacturing File Format) representation of
2191
+ the surface.
2192
+
2193
+ .. WARNING::
2194
+
2195
+ This only works for triangulated surfaces!
2196
+
2197
+ INPUT:
2198
+
2199
+ - ``name`` -- string (default: ``'surface'``); name of the surface
2200
+
2201
+ OUTPUT: string that represents the surface in the AMF format
2202
+
2203
+ See :wikipedia:`Additive_Manufacturing_File_Format`.
2204
+
2205
+ .. TODO::
2206
+
2207
+ This should rather be saved as a ZIP archive to save space.
2208
+
2209
+ EXAMPLES::
2210
+
2211
+ sage: # needs sage.symbolic
2212
+ sage: x,y,z = var('x,y,z')
2213
+ sage: a = implicit_plot3d(x^2+y^2+z^2-9,[x,-5,5],[y,-5,5],[z,-5,5])
2214
+ sage: a_amf = a.amf_ascii_string()
2215
+ sage: a_amf[:160]
2216
+ '<?xml version="1.0" encoding="utf-8"?><amf><object id="surface"><mesh><vertices><vertex><coordinates><x>2.948717948717948</x><y>-0.384615384615385</y><z>-0.3935'
2217
+
2218
+ sage: p = polygon3d([[0,0,0], [1,2,3], [3,0,0]])
2219
+ sage: print(p.amf_ascii_string(name='triangle'))
2220
+ <?xml version="1.0" encoding="utf-8"?><amf><object id="triangle"><mesh><vertices><vertex><coordinates><x>0.0</x><y>0.0</y><z>0.0</z></coordinates></vertex><vertex><coordinates><x>1.0</x><y>2.0</y><z>3.0</z></coordinates></vertex><vertex><coordinates><x>3.0</x><y>0.0</y><z>0.0</z></coordinates></vertex></vertices><volume><triangle><v1>0</v1><v2>1</v2><v3>2</v3></triangle></volume></mesh></object></amf>
2221
+ """
2222
+ faces = self.index_faces()
2223
+ if not faces:
2224
+ self.triangulate()
2225
+ faces = self.index_faces()
2226
+
2227
+ if len(faces[0]) > 3:
2228
+ raise ValueError('not made of triangles')
2229
+
2230
+ string_list = ['<?xml version="1.0" encoding="utf-8"?><amf><object id="{}"><mesh>'.format(name)]
2231
+
2232
+ string_list += ['<vertices>']
2233
+ vertex_template = '<vertex><coordinates><x>{!r}</x><y>{!r}</y><z>{!r}</z></coordinates></vertex>'
2234
+ for v in self.vertices():
2235
+ string_list += [vertex_template.format(*v)]
2236
+ string_list += ['</vertices><volume>']
2237
+
2238
+ face_template = '<triangle><v1>{}</v1><v2>{}</v2><v3>{}</v3></triangle>'
2239
+ for i, j, k in faces:
2240
+ string_list += face_template.format(i, j, k)
2241
+
2242
+ string_list += ['</volume></mesh></object></amf>']
2243
+ return "".join(string_list)
2244
+
2245
+ def plot(self):
2246
+ """
2247
+ Draw a 3D plot of this graphics object, which just returns this
2248
+ object since this is already a 3D graphics object.
2249
+ Needed to support PLOT in docstrings, see :issue:`17498`
2250
+
2251
+ EXAMPLES::
2252
+
2253
+ sage: S = sphere((0,0,0), 2)
2254
+ sage: S.plot() is S
2255
+ True
2256
+ """
2257
+ return self
2258
+
2259
+ # if you add any default parameters you must update some code below
2260
+ SHOW_DEFAULTS = {'viewer': 'threejs',
2261
+ 'verbosity': 0,
2262
+ 'figsize': 5,
2263
+ 'aspect_ratio': "automatic",
2264
+ 'frame_aspect_ratio': "automatic",
2265
+ 'zoom': 1,
2266
+ 'frame': True,
2267
+ 'axes': False}
2268
+
2269
+
2270
+ class Graphics3dGroup(Graphics3d):
2271
+ """
2272
+ This class represents a collection of 3d objects. Usually they are formed
2273
+ implicitly by summing.
2274
+ """
2275
+ def __init__(self, all=(), rot=None, trans=None, scale=None, T=None):
2276
+ """
2277
+ EXAMPLES::
2278
+
2279
+ sage: sage.plot.plot3d.base.Graphics3dGroup([icosahedron(), dodecahedron(opacity=.5)])
2280
+ Graphics3d Object
2281
+ sage: type(icosahedron() + dodecahedron(opacity=.5))
2282
+ <class 'sage.plot.plot3d.base.Graphics3dGroup'>
2283
+ """
2284
+ self.all = list(all)
2285
+ self.frame_aspect_ratio(optimal_aspect_ratios([a.frame_aspect_ratio() for a in all]))
2286
+ self.aspect_ratio(optimal_aspect_ratios([a.aspect_ratio() for a in all]))
2287
+ self._set_extra_kwds(optimal_extra_kwds([a._extra_kwds for a in all]))
2288
+
2289
+ def __add__(self, other):
2290
+ """
2291
+ We override this here to make large sums more efficient.
2292
+
2293
+ EXAMPLES::
2294
+
2295
+ sage: G = sum(tetrahedron(opacity=1-t/11).translate(t, 0, 0) for t in range(10))
2296
+ sage: G
2297
+ Graphics3d Object
2298
+ sage: len(G.all)
2299
+ 10
2300
+
2301
+ We check that :issue:`17258` is solved::
2302
+
2303
+ sage: g = point3d([0,-2,-2]); g += point3d([2,-2,-2])
2304
+ sage: len(g.all)
2305
+ 2
2306
+ sage: h = g + arrow([0,-2,-2], [2,-2,-2])
2307
+ sage: len(g.all)
2308
+ 2
2309
+ sage: g == h
2310
+ False
2311
+ """
2312
+ if type(self) is Graphics3dGroup and isinstance(other, Graphics3d):
2313
+ s_all = list(self.all)
2314
+ s_all.append(other)
2315
+ return Graphics3dGroup(s_all)
2316
+ else:
2317
+ return Graphics3d.__add__(self, other)
2318
+
2319
+ def bounding_box(self):
2320
+ """
2321
+ Box that contains the bounding boxes of
2322
+ the objects.
2323
+
2324
+ EXAMPLES::
2325
+
2326
+ sage: A = sphere((0,0,0), 5)
2327
+ sage: B = sphere((1, 5, 10), 1)
2328
+ sage: A.bounding_box()
2329
+ ((-5.0, -5.0, -5.0), (5.0, 5.0, 5.0))
2330
+ sage: B.bounding_box()
2331
+ ((0.0, 4.0, 9.0), (2.0, 6.0, 11.0))
2332
+ sage: (A+B).bounding_box()
2333
+ ((-5.0, -5.0, -5.0), (5.0, 6.0, 11.0))
2334
+ sage: (A+B).show(aspect_ratio=1, frame=True)
2335
+
2336
+ sage: sage.plot.plot3d.base.Graphics3dGroup([]).bounding_box()
2337
+ ((0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
2338
+ """
2339
+ if len(self.all) == 0:
2340
+ return Graphics3d.bounding_box(self)
2341
+ v = [obj.bounding_box() for obj in self.all]
2342
+ return min3([a[0] for a in v]), max3([a[1] for a in v])
2343
+
2344
+ def transform(self, **kwds):
2345
+ """
2346
+ Transforming this entire group simply makes a transform group with
2347
+ the same contents.
2348
+
2349
+ EXAMPLES::
2350
+
2351
+ sage: G = dodecahedron(color='red', opacity=.5) + icosahedron(color='blue')
2352
+ sage: G
2353
+ Graphics3d Object
2354
+ sage: G.transform(scale=(2,1/2,1))
2355
+ Graphics3d Object
2356
+ sage: G.transform(trans=(1,1,3))
2357
+ Graphics3d Object
2358
+ """
2359
+ T = TransformGroup(self.all, **kwds)
2360
+ T._set_extra_kwds(self._extra_kwds)
2361
+ return T
2362
+
2363
+ def set_texture(self, **kwds):
2364
+ """
2365
+ EXAMPLES::
2366
+
2367
+ sage: G = dodecahedron(color='red', opacity=.5) + icosahedron((3, 0, 0), color='blue')
2368
+ sage: G
2369
+ Graphics3d Object
2370
+ sage: G.set_texture(color='yellow')
2371
+ sage: G
2372
+ Graphics3d Object
2373
+ """
2374
+ for g in self.all:
2375
+ g.set_texture(**kwds)
2376
+
2377
+ def json_repr(self, render_params):
2378
+ """
2379
+ The JSON representation of a group is simply the concatenation of the
2380
+ representations of its objects.
2381
+
2382
+ EXAMPLES::
2383
+
2384
+ sage: G = sphere() + sphere((1, 2, 3))
2385
+ sage: G.json_repr(G.default_render_params())
2386
+ [[['{"vertices":...']], [['{"vertices":...']]]
2387
+ """
2388
+ return [g.json_repr(render_params) for g in self.all]
2389
+
2390
+ def tachyon_repr(self, render_params):
2391
+ """
2392
+ The tachyon representation of a group is simply the concatenation of
2393
+ the representations of its objects.
2394
+
2395
+ EXAMPLES::
2396
+
2397
+ sage: G = sphere() + sphere((1,2,3))
2398
+ sage: G.tachyon_repr(G.default_render_params())
2399
+ [['Sphere center 0.0 0.0 0.0 Rad 1.0 texture...'],
2400
+ ['Sphere center 1.0 2.0 3.0 Rad 1.0 texture...']]
2401
+ """
2402
+ return [g.tachyon_repr(render_params) for g in self.all]
2403
+
2404
+ def x3d_str(self):
2405
+ """
2406
+ The x3d representation of a group is simply the concatenation of
2407
+ the representation of its objects.
2408
+
2409
+ EXAMPLES::
2410
+
2411
+ sage: G = sphere() + sphere((1,2,3))
2412
+ sage: print(G.x3d_str())
2413
+ <Transform translation='0 0 0'>
2414
+ <Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
2415
+ </Transform>
2416
+ <Transform translation='1 2 3'>
2417
+ <Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>
2418
+ </Transform>
2419
+ """
2420
+ return "\n".join(g.x3d_str() for g in self.all)
2421
+
2422
+ def obj_repr(self, render_params):
2423
+ """
2424
+ The obj representation of a group is simply the concatenation of
2425
+ the representation of its objects.
2426
+
2427
+ EXAMPLES::
2428
+
2429
+ sage: G = tetrahedron() + tetrahedron().translate(10, 10, 10)
2430
+ sage: G.obj_repr(G.default_render_params())
2431
+ [['g obj_1',
2432
+ 'usemtl ...',
2433
+ ['v 0 0 1',
2434
+ 'v 0.942809 0 -0.333333',
2435
+ 'v -0.471405 0.816497 -0.333333',
2436
+ 'v -0.471405 -0.816497 -0.333333'],
2437
+ ['f 1 2 3', 'f 2 4 3', 'f 1 3 4', 'f 1 4 2'],
2438
+ []],
2439
+ [['g obj_2',
2440
+ 'usemtl ...',
2441
+ ['v 10 10 11',
2442
+ 'v 10.9428 10 9.66667',
2443
+ 'v 9.5286 10.8165 9.66667',
2444
+ 'v 9.5286 9.1835 9.66667'],
2445
+ ['f 5 6 7', 'f 6 8 7', 'f 5 7 8', 'f 5 8 6'],
2446
+ []]]]
2447
+ """
2448
+ return [g.obj_repr(render_params) for g in self.all]
2449
+
2450
+ def jmol_repr(self, render_params):
2451
+ r"""
2452
+ The jmol representation of a group is simply the concatenation of
2453
+ the representation of its objects.
2454
+
2455
+ EXAMPLES::
2456
+
2457
+ sage: G = sphere() + sphere((1,2,3))
2458
+ sage: G.jmol_repr(G.default_render_params())
2459
+ [[['isosurface sphere_1 center {0.0 0.0 0.0} sphere 1.0\ncolor isosurface [102,102,255]']],
2460
+ [['isosurface sphere_2 center {1.0 2.0 3.0} sphere 1.0\ncolor isosurface [102,102,255]']]]
2461
+ """
2462
+ return [g.jmol_repr(render_params) for g in self.all]
2463
+
2464
+ def threejs_repr(self, render_params):
2465
+ r"""
2466
+ The three.js representation of a group is the concatenation of the
2467
+ representations of its objects.
2468
+
2469
+ EXAMPLES::
2470
+
2471
+ sage: G = point3d((1,2,3)) + point3d((4,5,6)) + line3d([(1,2,3), (4,5,6)])
2472
+ sage: G.threejs_repr(G.default_render_params())
2473
+ [('point',
2474
+ {'color': '#6666ff', 'opacity': 1.0, 'point': (1.0, 2.0, 3.0), 'size': 5.0}),
2475
+ ('point',
2476
+ {'color': '#6666ff', 'opacity': 1.0, 'point': (4.0, 5.0, 6.0), 'size': 5.0}),
2477
+ ('line',
2478
+ {'color': '#6666ff',
2479
+ 'linewidth': 1.0,
2480
+ 'opacity': 1.0,
2481
+ 'points': [(1.0, 2.0, 3.0), (4.0, 5.0, 6.0)]})]
2482
+ """
2483
+ reprs = []
2484
+ for g in self.all:
2485
+ reprs += g.threejs_repr(render_params)
2486
+ return reprs
2487
+
2488
+ def stl_binary_repr(self, render_params):
2489
+ r"""
2490
+ The stl binary representation of a group is simply the
2491
+ concatenation of the representation of its objects.
2492
+
2493
+ The STL binary representation is a list of binary strings,
2494
+ one for each triangle.
2495
+
2496
+ EXAMPLES::
2497
+
2498
+ sage: G = sphere() + sphere((1,2,3))
2499
+ sage: len(G.stl_binary_repr(G.default_render_params()))
2500
+ 2736
2501
+ """
2502
+ data = []
2503
+ for sub in self.all:
2504
+ data.extend(sub.stl_binary_repr(render_params))
2505
+ return data
2506
+
2507
+ def texture_set(self):
2508
+ """
2509
+ The texture set of a group is simply the union of the textures of
2510
+ all its objects.
2511
+
2512
+ EXAMPLES::
2513
+
2514
+ sage: G = sphere(color='red') + sphere(color='yellow')
2515
+ sage: [t for t in G.texture_set() if t.color == colors.red] # one red texture
2516
+ [Texture(texture..., red, ff0000)]
2517
+ sage: [t for t in G.texture_set() if t.color == colors.yellow] # one yellow texture
2518
+ [Texture(texture..., yellow, ffff00)]
2519
+
2520
+ sage: T = sage.plot.plot3d.texture.Texture('blue'); T
2521
+ Texture(texture..., blue, 0000ff)
2522
+ sage: G = sphere(texture=T) + sphere((1, 1, 1), texture=T)
2523
+ sage: len(G.texture_set())
2524
+ 1
2525
+
2526
+ TESTS:
2527
+
2528
+ Check that :issue:`23200` is fixed::
2529
+
2530
+ sage: G = sage.plot.plot3d.base.Graphics3dGroup()
2531
+ sage: G.texture_set()
2532
+ set()
2533
+ """
2534
+ if not self.all:
2535
+ return set()
2536
+ return reduce(set.union, [g.texture_set() for g in self.all])
2537
+
2538
+ def flatten(self):
2539
+ """
2540
+ Try to reduce the depth of the scene tree by consolidating groups
2541
+ and transformations.
2542
+
2543
+ EXAMPLES::
2544
+
2545
+ sage: G = sum([circle((0, 0), t) for t in [1..10]], sphere()); G
2546
+ Graphics3d Object
2547
+ sage: G.flatten()
2548
+ Graphics3d Object
2549
+ sage: len(G.all)
2550
+ 2
2551
+ sage: len(G.flatten().all)
2552
+ 11
2553
+ """
2554
+ if len(self.all) == 1:
2555
+ return self.all[0].flatten()
2556
+ all = []
2557
+ for g in self.all:
2558
+ g = g.flatten()
2559
+ if type(g) is Graphics3dGroup:
2560
+ all += g.all
2561
+ else:
2562
+ all.append(g)
2563
+ return Graphics3dGroup(all)
2564
+
2565
+ def plot(self):
2566
+ return self
2567
+
2568
+
2569
+ class TransformGroup(Graphics3dGroup):
2570
+ """
2571
+ This class is a container for a group of objects with a common
2572
+ transformation.
2573
+ """
2574
+ def __init__(self, all=[], rot=None, trans=None, scale=None, T=None):
2575
+ """
2576
+ EXAMPLES::
2577
+
2578
+ sage: sage.plot.plot3d.base.TransformGroup([sphere()], trans=(1,2,3)) + point3d((0,0,0))
2579
+ Graphics3d Object
2580
+
2581
+ The are usually constructed implicitly::
2582
+
2583
+ sage: type(sphere((1,2,3)))
2584
+ <class 'sage.plot.plot3d.base.TransformGroup'>
2585
+ sage: type(dodecahedron().scale(2))
2586
+ <class 'sage.plot.plot3d.base.TransformGroup'>
2587
+ """
2588
+ Graphics3dGroup.__init__(self, all)
2589
+ self._rot = rot
2590
+ self._trans = trans
2591
+ if scale is not None and len(scale) == 1:
2592
+ if isinstance(scale, (tuple, list)):
2593
+ scale = scale[0]
2594
+ scale = (scale, scale, scale)
2595
+ self._scale = scale
2596
+ if T is not None:
2597
+ self.T = T
2598
+ self.frame_aspect_ratio(optimal_aspect_ratios([a.frame_aspect_ratio() for a in all]))
2599
+ self.aspect_ratio(optimal_aspect_ratios([a.aspect_ratio() for a in all]))
2600
+ self._set_extra_kwds(optimal_extra_kwds([a._extra_kwds for a in all if a._extra_kwds is not None]))
2601
+
2602
+ def bounding_box(self):
2603
+ """
2604
+ Return the bounding box, i.e., the box containing the
2605
+ contents of the object after applying the transformation.
2606
+
2607
+ EXAMPLES::
2608
+
2609
+ sage: from math import pi
2610
+ sage: G = cube()
2611
+ sage: G.bounding_box()
2612
+ ((-0.5, -0.5, -0.5), (0.5, 0.5, 0.5))
2613
+ sage: G.scale(4).bounding_box()
2614
+ ((-2.0, -2.0, -2.0), (2.0, 2.0, 2.0))
2615
+ sage: G.rotateZ(pi/4).bounding_box()
2616
+ ((-0.7071067811865475, -0.7071067811865475, -0.5),
2617
+ (0.7071067811865475, 0.7071067811865475, 0.5))
2618
+ """
2619
+ try:
2620
+ return self._bounding_box
2621
+ except AttributeError:
2622
+ pass
2623
+
2624
+ cdef Transformation T = self.get_transformation()
2625
+ w = sum([T.transform_bounding_box(obj.bounding_box()) for obj in self.all], ())
2626
+ self._bounding_box = point_list_bounding_box(w)
2627
+ return self._bounding_box
2628
+
2629
+ def x3d_str(self):
2630
+ r"""
2631
+ To apply a transformation to a set of objects in x3d, simply make them
2632
+ all children of an x3d Transform node.
2633
+
2634
+ EXAMPLES::
2635
+
2636
+ sage: sphere((1,2,3)).x3d_str()
2637
+ "<Transform translation='1 2 3'>\n<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>\n\n</Transform>"
2638
+ """
2639
+ s = "<Transform"
2640
+ if self._rot is not None:
2641
+ s += " rotation='%s %s %s %s'" % tuple(self._rot)
2642
+ if self._trans is not None:
2643
+ s += " translation='%s %s %s'" % tuple(self._trans)
2644
+ if self._scale is not None:
2645
+ s += " scale='%s %s %s'" % tuple(self._scale)
2646
+ s += ">\n"
2647
+ s += Graphics3dGroup.x3d_str(self)
2648
+ s += "\n</Transform>"
2649
+ return s
2650
+
2651
+ def json_repr(self, render_params):
2652
+ """
2653
+ Transformations are applied at the leaf nodes.
2654
+
2655
+ EXAMPLES::
2656
+
2657
+ sage: G = cube().rotateX(0.2)
2658
+ sage: G.json_repr(G.default_render_params())
2659
+ [['{"vertices":[{"x":0.5,"y":0.589368,"z":0.390699},...']]
2660
+ """
2661
+ render_params.push_transform(self.get_transformation())
2662
+ rep = [g.json_repr(render_params) for g in self.all]
2663
+ render_params.pop_transform()
2664
+ return rep
2665
+
2666
+ def stl_binary_repr(self, render_params):
2667
+ """
2668
+ Transformations are applied at the leaf nodes.
2669
+
2670
+ The STL binary representation is a list of binary strings,
2671
+ one for each triangle.
2672
+
2673
+ EXAMPLES::
2674
+
2675
+ sage: G = sphere().translate((1,2,0))
2676
+ sage: len(G.stl_binary_repr(G.default_render_params()))
2677
+ 1368
2678
+ """
2679
+ render_params.push_transform(self.get_transformation())
2680
+ rep = sum([g.stl_binary_repr(render_params) for g in self.all], [])
2681
+ render_params.pop_transform()
2682
+ return rep
2683
+
2684
+ def tachyon_repr(self, render_params):
2685
+ """
2686
+ Transformations for Tachyon are applied at the leaf nodes.
2687
+
2688
+ EXAMPLES::
2689
+
2690
+ sage: G = sphere((1,2,3)).scale(2)
2691
+ sage: G.tachyon_repr(G.default_render_params())
2692
+ [['Sphere center 2.0 4.0 6.0 Rad 2.0 texture...']]
2693
+ """
2694
+ render_params.push_transform(self.get_transformation())
2695
+ rep = [g.tachyon_repr(render_params) for g in self.all]
2696
+ render_params.pop_transform()
2697
+ return rep
2698
+
2699
+ def obj_repr(self, render_params):
2700
+ """
2701
+ Transformations for .obj files are applied at the leaf nodes.
2702
+
2703
+ EXAMPLES::
2704
+
2705
+ sage: G = cube().scale(4).translate(1, 2, 3)
2706
+ sage: G.obj_repr(G.default_render_params())
2707
+ [[['g obj_1',
2708
+ 'usemtl ...',
2709
+ ['v 3 4 5',
2710
+ 'v -1 4 5',
2711
+ 'v -1 0 5',
2712
+ 'v 3 0 5',
2713
+ 'v 3 4 1',
2714
+ 'v -1 4 1',
2715
+ 'v 3 0 1',
2716
+ 'v -1 0 1'],
2717
+ ['f 1 2 3 4',
2718
+ 'f 1 5 6 2',
2719
+ 'f 1 4 7 5',
2720
+ 'f 6 5 7 8',
2721
+ 'f 7 4 3 8',
2722
+ 'f 3 2 6 8'],
2723
+ []]]]
2724
+ """
2725
+ render_params.push_transform(self.get_transformation())
2726
+ rep = [g.obj_repr(render_params) for g in self.all]
2727
+ render_params.pop_transform()
2728
+ return rep
2729
+
2730
+ def jmol_repr(self, render_params):
2731
+ r"""
2732
+ Transformations for jmol are applied at the leaf nodes.
2733
+
2734
+ EXAMPLES::
2735
+
2736
+ sage: G = sphere((1,2,3)).scale(2)
2737
+ sage: G.jmol_repr(G.default_render_params())
2738
+ [[['isosurface sphere_1 center {2.0 4.0 6.0} sphere 2.0\ncolor isosurface [102,102,255]']]]
2739
+ """
2740
+ render_params.push_transform(self.get_transformation())
2741
+ rep = [g.jmol_repr(render_params) for g in self.all]
2742
+ render_params.pop_transform()
2743
+ return rep
2744
+
2745
+ def threejs_repr(self, render_params):
2746
+ r"""
2747
+ Transformations for three.js are applied at the leaf nodes.
2748
+
2749
+ EXAMPLES::
2750
+
2751
+ sage: G = point3d((1,2,3)) + point3d((4,5,6))
2752
+ sage: G = G.translate(-1, -2, -3).scale(10)
2753
+ sage: G.threejs_repr(G.default_render_params())
2754
+ [('point',
2755
+ {'color': '#6666ff', 'opacity': 1.0, 'point': (0.0, 0.0, 0.0), 'size': 5.0}),
2756
+ ('point',
2757
+ {'color': '#6666ff',
2758
+ 'opacity': 1.0,
2759
+ 'point': (30.0, 30.0, 30.0),
2760
+ 'size': 5.0})]
2761
+ """
2762
+ render_params.push_transform(self.get_transformation())
2763
+ rep = Graphics3dGroup.threejs_repr(self, render_params)
2764
+ render_params.pop_transform()
2765
+ return rep
2766
+
2767
+ def get_transformation(self):
2768
+ """
2769
+ Return the current transformation object.
2770
+
2771
+ EXAMPLES::
2772
+
2773
+ sage: G = sphere().scale(100)
2774
+ sage: T = G.get_transformation()
2775
+ sage: T.get_matrix()
2776
+ [100.0 0.0 0.0 0.0]
2777
+ [ 0.0 100.0 0.0 0.0]
2778
+ [ 0.0 0.0 100.0 0.0]
2779
+ [ 0.0 0.0 0.0 1.0]
2780
+ """
2781
+ try:
2782
+ return self.T
2783
+ except AttributeError:
2784
+ self.T = Transformation(self._scale, self._rot, self._trans)
2785
+ return self.T
2786
+
2787
+ def flatten(self):
2788
+ """
2789
+ Try to reduce the depth of the scene tree by consolidating groups
2790
+ and transformations.
2791
+
2792
+ EXAMPLES::
2793
+
2794
+ sage: G = sphere((1,2,3)).scale(100)
2795
+ sage: T = G.get_transformation()
2796
+ sage: T.get_matrix()
2797
+ [100.0 0.0 0.0 0.0]
2798
+ [ 0.0 100.0 0.0 0.0]
2799
+ [ 0.0 0.0 100.0 0.0]
2800
+ [ 0.0 0.0 0.0 1.0]
2801
+
2802
+ sage: G.flatten().get_transformation().get_matrix()
2803
+ [100.0 0.0 0.0 100.0]
2804
+ [ 0.0 100.0 0.0 200.0]
2805
+ [ 0.0 0.0 100.0 300.0]
2806
+ [ 0.0 0.0 0.0 1.0]
2807
+ """
2808
+ G = Graphics3dGroup.flatten(self)
2809
+ if isinstance(G, TransformGroup):
2810
+ return TransformGroup(G.all, T=self.get_transformation() * G.get_transformation())
2811
+ elif isinstance(G, Graphics3dGroup):
2812
+ return TransformGroup(G.all, T=self.get_transformation())
2813
+ else:
2814
+ return TransformGroup([G], T=self.get_transformation())
2815
+
2816
+ def transform(self, **kwds):
2817
+ """
2818
+ Transforming this entire group can be done by composing transformations.
2819
+
2820
+ EXAMPLES::
2821
+
2822
+ sage: G = dodecahedron(color='red', opacity=.5) + icosahedron(color='blue')
2823
+ sage: G
2824
+ Graphics3d Object
2825
+ sage: G.transform(scale=(2,1/2,1))
2826
+ Graphics3d Object
2827
+ sage: G.transform(trans=(1,1,3))
2828
+ Graphics3d Object
2829
+ """
2830
+ return Graphics3d.transform(self, **kwds)
2831
+
2832
+
2833
+ class KeyframeAnimationGroup(Graphics3dGroup):
2834
+ """A group of objects, each depicting a single frame of animation"""
2835
+ def __init__(self, all=(), **kwds):
2836
+ r"""
2837
+ EXAMPLES::
2838
+
2839
+ sage: frames = [dodecahedron(), icosahedron(), tetrahedron()]
2840
+ sage: sage.plot.plot3d.base.KeyframeAnimationGroup(frames)
2841
+ Graphics3d Object
2842
+
2843
+ They are usually constructed from an class:`~sage.plot.animate.Animation`::
2844
+
2845
+ sage: type(animate(frames).interactive())
2846
+ <class 'sage.plot.plot3d.base.KeyframeAnimationGroup'>
2847
+ """
2848
+ Graphics3dGroup.__init__(self, all)
2849
+ self._extra_kwds.update(kwds)
2850
+
2851
+ def threejs_repr(self, render_params):
2852
+ r"""
2853
+ Add keyframe information to the representations of the group's contents.
2854
+
2855
+ EXAMPLES::
2856
+
2857
+ sage: a = point3d((0, 0, 1))
2858
+ sage: b = point3d((0, 1, 0))
2859
+ sage: c = point3d((1, 0, 0))
2860
+ sage: g = sage.plot.plot3d.base.KeyframeAnimationGroup([a, b, c])
2861
+ sage: g.threejs_repr(g.default_render_params())
2862
+ [('point', {..., 'keyframe': 0, ..., 'point': (0.0, 0.0, 1.0), ...}),
2863
+ ('point', {..., 'keyframe': 1, ..., 'point': (0.0, 1.0, 0.0), ...}),
2864
+ ('point', {..., 'keyframe': 2, ..., 'point': (1.0, 0.0, 0.0), ...})]
2865
+
2866
+ Only top-level objects get a unique keyframe. Nested objects share the
2867
+ same keyframe::
2868
+
2869
+ sage: g = sage.plot.plot3d.base.KeyframeAnimationGroup([a + b, c])
2870
+ sage: g.threejs_repr(g.default_render_params())
2871
+ [('point', {..., 'keyframe': 0, ..., 'point': (0.0, 0.0, 1.0), ...}),
2872
+ ('point', {..., 'keyframe': 0, ..., 'point': (0.0, 1.0, 0.0), ...}),
2873
+ ('point', {..., 'keyframe': 1, ..., 'point': (1.0, 0.0, 0.0), ...})]
2874
+ """
2875
+ reprs = []
2876
+ for i, g in enumerate(self.all):
2877
+ for (kind, desc) in g.threejs_repr(render_params):
2878
+ desc['keyframe'] = i
2879
+ reprs.append((kind, desc))
2880
+ return reprs
2881
+
2882
+
2883
+ class Viewpoint(Graphics3d):
2884
+ """
2885
+ This class represents a viewpoint, necessary for x3d.
2886
+
2887
+ In the future, there could be multiple viewpoints, and they could have
2888
+ more properties. (Currently they only hold a position).
2889
+ """
2890
+ def __init__(self, *x):
2891
+ """
2892
+ EXAMPLES::
2893
+
2894
+ sage: sage.plot.plot3d.base.Viewpoint(1, 2, 4).x3d_str()
2895
+ "<Viewpoint position='1 2 4'/>"
2896
+ """
2897
+ if isinstance(x[0], (tuple, list)):
2898
+ x = tuple(x[0])
2899
+ self.pos = x
2900
+
2901
+ def x3d_str(self):
2902
+ """
2903
+ EXAMPLES::
2904
+
2905
+ sage: sphere((0,0,0), 100).viewpoint().x3d_str()
2906
+ "<Viewpoint position='0 0 6'/>"
2907
+ """
2908
+ return "<Viewpoint position='%s %s %s'/>" % self.pos
2909
+
2910
+
2911
+ cdef class PrimitiveObject(Graphics3d):
2912
+ """
2913
+ This is the base class for the non-container 3d objects.
2914
+ """
2915
+ def __init__(self, **kwds):
2916
+ if 'texture' in kwds:
2917
+ self.texture = kwds['texture']
2918
+ if not isinstance(self.texture, Texture):
2919
+ self.texture = Texture(self.texture)
2920
+ else:
2921
+ self.texture = Texture(kwds)
2922
+
2923
+ def set_texture(self, texture=None, **kwds):
2924
+ """
2925
+ EXAMPLES::
2926
+
2927
+ sage: G = dodecahedron(color='red'); G
2928
+ Graphics3d Object
2929
+ sage: G.set_texture(color='yellow'); G
2930
+ Graphics3d Object
2931
+ """
2932
+ if not isinstance(texture, Texture):
2933
+ texture = Texture(texture, **kwds)
2934
+ self.texture = texture
2935
+
2936
+ def get_texture(self):
2937
+ """
2938
+ EXAMPLES::
2939
+
2940
+ sage: G = dodecahedron(color='red')
2941
+ sage: G.get_texture()
2942
+ Texture(texture..., red, ff0000)
2943
+ """
2944
+ return self.texture
2945
+
2946
+ def texture_set(self):
2947
+ """
2948
+ EXAMPLES::
2949
+
2950
+ sage: G = dodecahedron(color='red')
2951
+ sage: G.texture_set()
2952
+ {Texture(texture..., red, ff0000)}
2953
+ """
2954
+ return set([self.texture])
2955
+
2956
+ def x3d_str(self):
2957
+ r"""
2958
+ EXAMPLES::
2959
+
2960
+ sage: sphere().flatten().x3d_str()
2961
+ "<Transform>\n<Shape><Sphere radius='1.0'/><Appearance><Material diffuseColor='0.4 0.4 1.0' shininess='1.0' specularColor='0.0 0.0 0.0'/></Appearance></Shape>\n\n</Transform>"
2962
+ """
2963
+ return "<Shape>" + self.x3d_geometry() + self.texture.x3d_str() + "</Shape>\n"
2964
+
2965
+ def tachyon_repr(self, render_params):
2966
+ """
2967
+ Default behavior is to render the triangulation.
2968
+
2969
+ EXAMPLES::
2970
+
2971
+ sage: from sage.plot.plot3d.shapes import Torus
2972
+ sage: G = Torus(1, .5)
2973
+ sage: G.tachyon_repr(G.default_render_params())
2974
+ ['TRI V0 0 1 0.5
2975
+ ...
2976
+ 'texture...']
2977
+ """
2978
+ return self.triangulation().tachyon_repr(render_params)
2979
+
2980
+ def obj_repr(self, render_params):
2981
+ """
2982
+ Default behavior is to render the triangulation.
2983
+
2984
+ EXAMPLES::
2985
+
2986
+ sage: from sage.plot.plot3d.shapes import Torus
2987
+ sage: G = Torus(1, .5)
2988
+ sage: G.obj_repr(G.default_render_params())
2989
+ ['g obj_1',
2990
+ 'usemtl ...',
2991
+ ['v 0 1 0.5',
2992
+ ...
2993
+ 'f ...'],
2994
+ []]
2995
+ """
2996
+ return self.triangulation().obj_repr(render_params)
2997
+
2998
+ def jmol_repr(self, render_params):
2999
+ r"""
3000
+ Default behavior is to render the triangulation. The actual polygon
3001
+ data is stored in a separate file.
3002
+
3003
+ EXAMPLES::
3004
+
3005
+ sage: from sage.plot.plot3d.shapes import Torus
3006
+ sage: G = Torus(1, .5)
3007
+ sage: G.jmol_repr(G.testing_render_params())
3008
+ ['pmesh obj_1 "obj_1.pmesh"\ncolor pmesh [102,102,255]']
3009
+ """
3010
+ return self.triangulation().jmol_repr(render_params)
3011
+
3012
+ def threejs_repr(self, render_params):
3013
+ r"""
3014
+ Default behavior is to render the triangulation.
3015
+
3016
+ EXAMPLES::
3017
+
3018
+ sage: from sage.plot.plot3d.base import PrimitiveObject
3019
+ sage: class SimpleTriangle(PrimitiveObject):
3020
+ ....: def triangulation(self):
3021
+ ....: return polygon3d([(0,0,0), (1,0,0), (0,1,0)])
3022
+ sage: G = SimpleTriangle()
3023
+ sage: G.threejs_repr(G.default_render_params())
3024
+ [('surface',
3025
+ {'color': '#0000ff',
3026
+ 'faces': [[0, 1, 2]],
3027
+ 'opacity': 1.0,
3028
+ 'vertices': [{'x': 0.0, 'y': 0.0, 'z': 0.0},
3029
+ {'x': 1.0, 'y': 0.0, 'z': 0.0},
3030
+ {'x': 0.0, 'y': 1.0, 'z': 0.0}]})]
3031
+ """
3032
+ return self.triangulation().threejs_repr(render_params)
3033
+
3034
+
3035
+ class BoundingSphere(SageObject):
3036
+ """
3037
+ A bounding sphere is like a bounding box, but is simpler to deal with and
3038
+ behaves better under rotations.
3039
+ """
3040
+ def __init__(self, cen, r):
3041
+ """
3042
+ EXAMPLES::
3043
+
3044
+ sage: from sage.plot.plot3d.base import BoundingSphere
3045
+ sage: BoundingSphere((0,0,0), 1)
3046
+ Center (0.0, 0.0, 0.0) radius 1
3047
+ sage: BoundingSphere((0,-1,5), 2)
3048
+ Center (0.0, -1.0, 5.0) radius 2
3049
+ """
3050
+ self.cen = vector(RDF, cen)
3051
+ self.r = r
3052
+
3053
+ def __repr__(self):
3054
+ """
3055
+ TESTS::
3056
+
3057
+ sage: from sage.plot.plot3d.base import BoundingSphere
3058
+ sage: BoundingSphere((0,-1,10), 2)
3059
+ Center (0.0, -1.0, 10.0) radius 2
3060
+ """
3061
+ return "Center %s radius %s" % (self.cen, self.r)
3062
+
3063
+ def __add__(self, other):
3064
+ """
3065
+ Return the bounding sphere containing both terms.
3066
+
3067
+ EXAMPLES::
3068
+
3069
+ sage: from sage.plot.plot3d.base import BoundingSphere
3070
+ sage: BoundingSphere((0,0,0), 1) + BoundingSphere((0,0,0), 2)
3071
+ Center (0.0, 0.0, 0.0) radius 2
3072
+ sage: BoundingSphere((0,0,0), 1) + BoundingSphere((0,0,100), 1)
3073
+ Center (0.0, 0.0, 50.0) radius 51.0
3074
+ sage: BoundingSphere((0,0,0), 1) + BoundingSphere((1,1,1), 2)
3075
+ Center (0.7886751345948128, 0.7886751345948128, 0.7886751345948128) radius 2.3660254037844384
3076
+
3077
+ Treat None and 0 as the identity::
3078
+
3079
+ sage: BoundingSphere((1,2,3), 10) + None + 0
3080
+ Center (1.0, 2.0, 3.0) radius 10
3081
+ """
3082
+ if other == 0 or other is None:
3083
+ return self
3084
+ elif self == 0 or self is None:
3085
+ return other
3086
+ if self.cen == other.cen:
3087
+ return self if self.r > other.r else other
3088
+ diff = other.cen - self.cen
3089
+ dist = (diff[0]*diff[0] + diff[1]*diff[1] + diff[2]*diff[2]).sqrt()
3090
+ diam = dist + self.r + other.r
3091
+ off = diam/2 - self.r
3092
+ return BoundingSphere(self.cen + (off/dist)*diff, diam/2)
3093
+
3094
+ def transform(self, T):
3095
+ """
3096
+ Return the bounding sphere of this sphere acted on by T. This always
3097
+ returns a new sphere, even if the resulting object is an ellipsoid.
3098
+
3099
+ EXAMPLES::
3100
+
3101
+ sage: from sage.plot.plot3d.transform import Transformation
3102
+ sage: from sage.plot.plot3d.base import BoundingSphere
3103
+ sage: BoundingSphere((0,0,0), 10).transform(Transformation(trans=(1,2,3)))
3104
+ Center (1.0, 2.0, 3.0) radius 10.0
3105
+ sage: BoundingSphere((0,0,0), 10).transform(Transformation(scale=(1/2, 1, 2)))
3106
+ Center (0.0, 0.0, 0.0) radius 20.0
3107
+ sage: BoundingSphere((0,0,3), 10).transform(Transformation(scale=(2, 2, 2)))
3108
+ Center (0.0, 0.0, 6.0) radius 20.0
3109
+ """
3110
+ return BoundingSphere(T.transform_point(self.cen), self.r * T.max_scale())
3111
+
3112
+
3113
+ class RenderParams(SageObject):
3114
+ """
3115
+ This class is a container for all parameters that may be needed to
3116
+ render triangulate/render an object to a certain format. It can
3117
+ contain both cumulative and global parameters.
3118
+
3119
+ Of particular note is the transformation object, which holds the
3120
+ cumulative transformation from the root of the scene graph to this
3121
+ node in the tree.
3122
+ """
3123
+
3124
+ _uniq_counter = 0
3125
+ randomize_counter = 0
3126
+ force_reload = False
3127
+ mesh = False
3128
+ dots = False
3129
+ antialiasing = 8
3130
+
3131
+ def __init__(self, **kwds):
3132
+ """
3133
+ EXAMPLES::
3134
+
3135
+ sage: params = sage.plot.plot3d.base.RenderParams(foo='x')
3136
+ sage: params.transform_list
3137
+ []
3138
+ sage: params.foo
3139
+ 'x'
3140
+ """
3141
+ self.output_file = tmp_filename()
3142
+ self.obj_vertex_offset = 1
3143
+ self.transform_list = []
3144
+ self.transform = None
3145
+ self.ds = 1
3146
+ self.crease_threshold = .8
3147
+ self.__dict__.update(kwds)
3148
+ # for jmol, some things (such as labels) must be attached to atoms
3149
+ self.atom_list = []
3150
+
3151
+ def push_transform(self, T):
3152
+ """
3153
+ Push a transformation onto the stack, updating self.transform.
3154
+
3155
+ EXAMPLES::
3156
+
3157
+ sage: from sage.plot.plot3d.transform import Transformation
3158
+ sage: params = sage.plot.plot3d.base.RenderParams()
3159
+ sage: params.transform is None
3160
+ True
3161
+ sage: T = Transformation(scale=(10,20,30))
3162
+ sage: params.push_transform(T)
3163
+ sage: params.transform.get_matrix()
3164
+ [10.0 0.0 0.0 0.0]
3165
+ [ 0.0 20.0 0.0 0.0]
3166
+ [ 0.0 0.0 30.0 0.0]
3167
+ [ 0.0 0.0 0.0 1.0]
3168
+ sage: params.push_transform(T) # scale again
3169
+ sage: params.transform.get_matrix()
3170
+ [100.0 0.0 0.0 0.0]
3171
+ [ 0.0 400.0 0.0 0.0]
3172
+ [ 0.0 0.0 900.0 0.0]
3173
+ [ 0.0 0.0 0.0 1.0]
3174
+ """
3175
+ self.transform_list.append(self.transform)
3176
+ if self.transform is None:
3177
+ self.transform = T
3178
+ else:
3179
+ self.transform = self.transform * T
3180
+
3181
+ def pop_transform(self):
3182
+ """
3183
+ Remove the last transformation off the stack, resetting self.transform
3184
+ to the previous value.
3185
+
3186
+ EXAMPLES::
3187
+
3188
+ sage: from sage.plot.plot3d.transform import Transformation
3189
+ sage: params = sage.plot.plot3d.base.RenderParams()
3190
+ sage: T = Transformation(trans=(100, 500, 0))
3191
+ sage: params.push_transform(T)
3192
+ sage: params.transform.get_matrix()
3193
+ [ 1.0 0.0 0.0 100.0]
3194
+ [ 0.0 1.0 0.0 500.0]
3195
+ [ 0.0 0.0 1.0 0.0]
3196
+ [ 0.0 0.0 0.0 1.0]
3197
+ sage: params.push_transform(Transformation(trans=(-100, 500, 200)))
3198
+ sage: params.transform.get_matrix()
3199
+ [ 1.0 0.0 0.0 0.0]
3200
+ [ 0.0 1.0 0.0 1000.0]
3201
+ [ 0.0 0.0 1.0 200.0]
3202
+ [ 0.0 0.0 0.0 1.0]
3203
+ sage: params.pop_transform()
3204
+ sage: params.transform.get_matrix()
3205
+ [ 1.0 0.0 0.0 100.0]
3206
+ [ 0.0 1.0 0.0 500.0]
3207
+ [ 0.0 0.0 1.0 0.0]
3208
+ [ 0.0 0.0 0.0 1.0]
3209
+ """
3210
+ self.transform = self.transform_list.pop()
3211
+
3212
+ def unique_name(self, desc='name'):
3213
+ """
3214
+ Return a unique identifier starting with ``desc``.
3215
+
3216
+ INPUT:
3217
+
3218
+ - ``desc`` -- string (default: ``'name'``); the prefix of the names
3219
+
3220
+ EXAMPLES::
3221
+
3222
+ sage: params = sage.plot.plot3d.base.RenderParams()
3223
+ sage: params.unique_name()
3224
+ 'name_1'
3225
+ sage: params.unique_name()
3226
+ 'name_2'
3227
+ sage: params.unique_name('texture')
3228
+ 'texture_3'
3229
+ """
3230
+ if self.randomize_counter:
3231
+ self._uniq_counter = randint(1,1000000)
3232
+ else:
3233
+ self._uniq_counter += 1
3234
+ return "%s_%s" % (desc, self._uniq_counter)
3235
+
3236
+
3237
+ def flatten_list(L):
3238
+ """
3239
+ This is an optimized routine to turn a list of lists (of lists ...)
3240
+ into a single list. We generate data in a non-flat format to avoid
3241
+ multiple data copying, and then concatenate it all at the end.
3242
+
3243
+ This is NOT recursive, otherwise there would be a lot of redundant
3244
+ copying (which we are trying to avoid in the first place, though at
3245
+ least it would be just the pointers).
3246
+
3247
+ EXAMPLES::
3248
+
3249
+ sage: from sage.plot.plot3d.base import flatten_list
3250
+ sage: flatten_list([])
3251
+ []
3252
+ sage: flatten_list([[[[]]]])
3253
+ []
3254
+ sage: flatten_list([['a', 'b'], 'c'])
3255
+ ['a', 'b', 'c']
3256
+ sage: flatten_list([['a'], [[['b'], 'c'], ['d'], [[['e', 'f', 'g']]]]])
3257
+ ['a', 'b', 'c', 'd', 'e', 'f', 'g']
3258
+ """
3259
+ if type(L) is not list:
3260
+ return [L]
3261
+ flat = []
3262
+ L_stack = []
3263
+ L_pop = L_stack.pop
3264
+ i_stack = []
3265
+ i_pop = i_stack.pop
3266
+ cdef Py_ssize_t i = 0
3267
+ while i < PyList_GET_SIZE(L) or PyList_GET_SIZE(L_stack) > 0:
3268
+ while i < PyList_GET_SIZE(L):
3269
+ tmp = <object>PyList_GET_ITEM(L, i)
3270
+ if type(tmp) is list:
3271
+ PyList_Append(L_stack, L)
3272
+ L = tmp
3273
+ PyList_Append(i_stack, i)
3274
+ i = 0
3275
+ else:
3276
+ PyList_Append(flat, tmp)
3277
+ i += 1
3278
+ if PyList_GET_SIZE(L_stack) > 0:
3279
+ L = L_pop()
3280
+ i = i_pop()
3281
+ i += 1
3282
+ return flat
3283
+
3284
+
3285
+ def min3(v):
3286
+ """
3287
+ Return the componentwise minimum of a list of 3-tuples.
3288
+
3289
+ EXAMPLES::
3290
+
3291
+ sage: from sage.plot.plot3d.base import min3, max3
3292
+ sage: min3([(-1,2,5), (-3, 4, 2)])
3293
+ (-3, 2, 2)
3294
+ """
3295
+ return tuple([min([a[i] for a in v]) for i in range(3)])
3296
+
3297
+
3298
+ def max3(v):
3299
+ """
3300
+ Return the componentwise maximum of a list of 3-tuples.
3301
+
3302
+ EXAMPLES::
3303
+
3304
+ sage: from sage.plot.plot3d.base import min3, max3
3305
+ sage: max3([(-1,2,5), (-3, 4, 2)])
3306
+ (-1, 4, 5)
3307
+ """
3308
+ return tuple([max([a[i] for a in v]) for i in range(3)])
3309
+
3310
+
3311
+ def point_list_bounding_box(v):
3312
+ """
3313
+ Return the bounding box of a list of points.
3314
+
3315
+ EXAMPLES::
3316
+
3317
+ sage: from sage.plot.plot3d.base import point_list_bounding_box
3318
+ sage: point_list_bounding_box([(1,2,3),(4,5,6),(-10,0,10)])
3319
+ ((-10.0, 0.0, 3.0), (4.0, 5.0, 10.0))
3320
+ sage: point_list_bounding_box([(float('nan'), float('inf'), float('-inf')), (10,0,10)])
3321
+ ((10.0, 0.0, 10.0), (10.0, 0.0, 10.0))
3322
+ """
3323
+ cdef point_c low, high, cur
3324
+ low.x, low.y, low.z = INFINITY, INFINITY, INFINITY
3325
+ high.x, high.y, high.z = -INFINITY, -INFINITY, -INFINITY
3326
+
3327
+ for P in v:
3328
+ cur.x, cur.y, cur.z = P
3329
+ point_c_update_finite_lower_bound(&low, cur)
3330
+ point_c_update_finite_upper_bound(&high, cur)
3331
+ return ((low.x, low.y, low.z), (high.x, high.y, high.z))
3332
+
3333
+
3334
+ def optimal_aspect_ratios(ratios):
3335
+ """
3336
+ Average the aspect ratios.
3337
+ compute the elementwise maximum of triples.
3338
+
3339
+ TESTS::
3340
+
3341
+ sage: from sage.plot.plot3d.base import optimal_aspect_ratios
3342
+ sage: optimal_aspect_ratios([(2,4,6), (5,4,4), (1,2,7)])
3343
+ [5, 4, 7]
3344
+ """
3345
+ n = len(ratios)
3346
+ if n > 0:
3347
+ return [max([z[i] for z in ratios]) for i in range(3)]
3348
+ return [1.0, 1.0, 1.0]
3349
+
3350
+
3351
+ def optimal_extra_kwds(v):
3352
+ """
3353
+ Merge a list v of dictionaries such that later
3354
+ dictionaries have precedence.
3355
+
3356
+ TESTS::
3357
+
3358
+ sage: from sage.plot.plot3d.base import optimal_extra_kwds
3359
+ sage: optimal_extra_kwds([{1:2, 2:3}, {2:4, 3:5}])
3360
+ {1: 2, 2: 4, 3: 5}
3361
+ """
3362
+ a = {}
3363
+ for b in v:
3364
+ a.update(b)
3365
+ return a
3366
+
3367
+
3368
+ def _flip_orientation(v):
3369
+ """
3370
+ Switch from LH to RH coords to be consistent with Java rendition.
3371
+
3372
+ TESTS::
3373
+
3374
+ sage: from sage.plot.plot3d.base import _flip_orientation
3375
+ sage: _flip_orientation((1, 2, 3))
3376
+ (1, -2, 3)
3377
+ """
3378
+ return (v[0], -v[1], v[2])