passagemath-plot 10.6.31rc3__cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.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.
- passagemath_plot-10.6.31rc3.dist-info/METADATA +172 -0
- passagemath_plot-10.6.31rc3.dist-info/RECORD +81 -0
- passagemath_plot-10.6.31rc3.dist-info/WHEEL +6 -0
- passagemath_plot-10.6.31rc3.dist-info/top_level.txt +2 -0
- passagemath_plot.libs/libgfortran-e1b7dfc8.so.5.0.0 +0 -0
- passagemath_plot.libs/libgsl-e3525837.so.28.0.0 +0 -0
- passagemath_plot.libs/libopenblasp-r0-4c5b64b1.3.29.so +0 -0
- sage/all__sagemath_plot.py +15 -0
- sage/ext_data/threejs/animation.css +195 -0
- sage/ext_data/threejs/animation.html +85 -0
- sage/ext_data/threejs/animation.js +273 -0
- sage/ext_data/threejs/fat_lines.js +48 -0
- sage/ext_data/threejs/threejs-version.txt +1 -0
- sage/ext_data/threejs/threejs_template.html +597 -0
- sage/interfaces/all__sagemath_plot.py +1 -0
- sage/interfaces/gnuplot.py +196 -0
- sage/interfaces/jmoldata.py +208 -0
- sage/interfaces/povray.py +56 -0
- sage/plot/all.py +42 -0
- sage/plot/animate.py +1796 -0
- sage/plot/arc.py +504 -0
- sage/plot/arrow.py +671 -0
- sage/plot/bar_chart.py +205 -0
- sage/plot/bezier_path.py +400 -0
- sage/plot/circle.py +435 -0
- sage/plot/colors.py +1606 -0
- sage/plot/complex_plot.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/complex_plot.pyx +1446 -0
- sage/plot/contour_plot.py +1792 -0
- sage/plot/density_plot.py +318 -0
- sage/plot/disk.py +373 -0
- sage/plot/ellipse.py +375 -0
- sage/plot/graphics.py +3580 -0
- sage/plot/histogram.py +354 -0
- sage/plot/hyperbolic_arc.py +404 -0
- sage/plot/hyperbolic_polygon.py +416 -0
- sage/plot/hyperbolic_regular_polygon.py +296 -0
- sage/plot/line.py +626 -0
- sage/plot/matrix_plot.py +629 -0
- sage/plot/misc.py +509 -0
- sage/plot/multigraphics.py +1294 -0
- sage/plot/plot.py +4183 -0
- sage/plot/plot3d/all.py +23 -0
- sage/plot/plot3d/base.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/base.pxd +12 -0
- sage/plot/plot3d/base.pyx +3378 -0
- sage/plot/plot3d/implicit_plot3d.py +659 -0
- sage/plot/plot3d/implicit_surface.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/implicit_surface.pyx +1453 -0
- sage/plot/plot3d/index_face_set.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/index_face_set.pxd +32 -0
- sage/plot/plot3d/index_face_set.pyx +1873 -0
- sage/plot/plot3d/introduction.py +131 -0
- sage/plot/plot3d/list_plot3d.py +649 -0
- sage/plot/plot3d/parametric_plot3d.py +1130 -0
- sage/plot/plot3d/parametric_surface.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/parametric_surface.pxd +12 -0
- sage/plot/plot3d/parametric_surface.pyx +893 -0
- sage/plot/plot3d/platonic.py +601 -0
- sage/plot/plot3d/plot3d.py +1442 -0
- sage/plot/plot3d/plot_field3d.py +162 -0
- sage/plot/plot3d/point_c.pxi +148 -0
- sage/plot/plot3d/revolution_plot3d.py +309 -0
- sage/plot/plot3d/shapes.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/shapes.pxd +22 -0
- sage/plot/plot3d/shapes.pyx +1382 -0
- sage/plot/plot3d/shapes2.py +1512 -0
- sage/plot/plot3d/tachyon.py +1779 -0
- sage/plot/plot3d/texture.py +453 -0
- sage/plot/plot3d/transform.cpython-314-aarch64-linux-gnu.so +0 -0
- sage/plot/plot3d/transform.pxd +21 -0
- sage/plot/plot3d/transform.pyx +268 -0
- sage/plot/plot3d/tri_plot.py +589 -0
- sage/plot/plot_field.py +362 -0
- sage/plot/point.py +624 -0
- sage/plot/polygon.py +562 -0
- sage/plot/primitive.py +249 -0
- sage/plot/scatter_plot.py +199 -0
- sage/plot/step.py +85 -0
- sage/plot/streamline_plot.py +328 -0
- 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>"Page" & <Title></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])
|