Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/plot/plot3d/shapes2.py
8815 views
1
r"""
2
Classes for Lines, Frames, Rulers, Spheres, Points, Dots, and Text
3
4
AUTHORS:
5
6
- William Stein (2007-12): initial version
7
8
- William Stein and Robert Bradshaw (2008-01): Many improvements
9
10
"""
11
#*****************************************************************************
12
# Copyright (C) 2007 William Stein <[email protected]>
13
# Copyright (C) 2008 Robert Bradshaw <[email protected]>
14
#
15
# Distributed under the terms of the GNU General Public License (GPL)
16
#
17
# This code is distributed in the hope that it will be useful,
18
# but WITHOUT ANY WARRANTY; without even the implied warranty of
19
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20
# General Public License for more details.
21
#
22
# The full text of the GPL is available at:
23
#
24
# http://www.gnu.org/licenses/
25
#*****************************************************************************
26
27
28
import math
29
import shapes
30
31
from base import PrimitiveObject, point_list_bounding_box
32
33
from sage.rings.real_double import RDF
34
from sage.modules.free_module_element import vector
35
from sage.misc.decorators import options, rename_keyword
36
from sage.misc.misc import srange
37
38
from texture import Texture
39
40
TACHYON_PIXEL = 1/200.0
41
42
from shapes import Text, Sphere
43
44
from sage.structure.element import is_Vector
45
46
def line3d(points, thickness=1, radius=None, arrow_head=False, **kwds):
47
r"""
48
Draw a 3d line joining a sequence of points.
49
50
One may specify either a thickness or radius. If a thickness is
51
specified, this line will have a constant diameter regardless of
52
scaling and zooming. If a radius is specified, it will behave as a
53
series of cylinders.
54
55
INPUT:
56
57
58
- ``points`` - a list of at least 2 points
59
60
- ``thickness`` - (default: 1)
61
62
- ``radius`` - (default: None)
63
64
- ``arrow_head`` - (default: False)
65
66
- ``color`` - a word that describes a color
67
68
- ``rgbcolor`` - (r,g,b) with r, g, b between 0 and 1
69
that describes a color
70
71
- ``opacity`` - (default: 1) if less than 1 then is
72
transparent
73
74
75
EXAMPLES:
76
77
A line in 3-space::
78
79
sage: line3d([(1,2,3), (1,0,-2), (3,1,4), (2,1,-2)])
80
81
The same line but red::
82
83
sage: line3d([(1,2,3), (1,0,-2), (3,1,4), (2,1,-2)], color='red')
84
85
The points of the line provided as a numpy array::
86
87
sage: import numpy
88
sage: line3d(numpy.array([(1,2,3), (1,0,-2), (3,1,4), (2,1,-2)]))
89
90
A transparent thick green line and a little blue line::
91
92
sage: line3d([(0,0,0), (1,1,1), (1,0,2)], opacity=0.5, radius=0.1, \
93
color='green') + line3d([(0,1,0), (1,0,2)])
94
95
A Dodecahedral complex of 5 tetrahedrons (a more elaborate examples
96
from Peter Jipsen)::
97
98
sage: def tetra(col):
99
... return line3d([(0,0,1), (2*sqrt(2.)/3,0,-1./3), (-sqrt(2.)/3, sqrt(6.)/3,-1./3),\
100
... (-sqrt(2.)/3,-sqrt(6.)/3,-1./3), (0,0,1), (-sqrt(2.)/3, sqrt(6.)/3,-1./3),\
101
... (-sqrt(2.)/3,-sqrt(6.)/3,-1./3), (2*sqrt(2.)/3,0,-1./3)],\
102
... color=col, thickness=10, aspect_ratio=[1,1,1])
103
...
104
sage: v = (sqrt(5.)/2-5/6, 5/6*sqrt(3.)-sqrt(15.)/2, sqrt(5.)/3)
105
sage: t = acos(sqrt(5.)/3)/2
106
sage: t1 = tetra('blue').rotateZ(t)
107
sage: t2 = tetra('red').rotateZ(t).rotate(v,2*pi/5)
108
sage: t3 = tetra('green').rotateZ(t).rotate(v,4*pi/5)
109
sage: t4 = tetra('yellow').rotateZ(t).rotate(v,6*pi/5)
110
sage: t5 = tetra('orange').rotateZ(t).rotate(v,8*pi/5)
111
sage: show(t1+t2+t3+t4+t5, frame=False)
112
113
TESTS:
114
115
Copies are made of the input list, so the input list does not change::
116
117
sage: mypoints = [vector([1,2,3]), vector([4,5,6])]
118
sage: type(mypoints[0])
119
<type 'sage.modules.vector_integer_dense.Vector_integer_dense'>
120
sage: L = line3d(mypoints)
121
sage: type(mypoints[0])
122
<type 'sage.modules.vector_integer_dense.Vector_integer_dense'>
123
124
The copies are converted to a list, so we can pass in immutable objects too::
125
126
sage: L = line3d(((0,0,0),(1,2,3)))
127
128
This function should work for anything than can be turned into a
129
list, such as iterators and such (see ticket #10478)::
130
131
sage: line3d(iter([(0,0,0), (sqrt(3), 2, 4)]))
132
sage: line3d((x, x^2, x^3) for x in range(5))
133
sage: from itertools import izip; line3d(izip([2,3,5,7], [11, 13, 17, 19], [-1, -2, -3, -4]))
134
"""
135
points = list(points)
136
if len(points) < 2:
137
raise ValueError, "there must be at least 2 points"
138
for i in range(len(points)):
139
x, y, z = points[i]
140
points[i] = float(x), float(y), float(z)
141
if radius is None:
142
L = Line(points, thickness=thickness, arrow_head=arrow_head, **kwds)
143
L._set_extra_kwds(kwds)
144
return L
145
else:
146
v = []
147
if kwds.has_key('texture'):
148
kwds = kwds.copy()
149
texture = kwds.pop('texture')
150
else:
151
texture = Texture(kwds)
152
for i in range(len(points) - 1):
153
line = shapes.arrow3d if i == len(points)-2 and arrow_head else shapes.LineSegment
154
v.append(line(points[i], points[i+1], texture=texture, radius=radius, **kwds))
155
w = sum(v)
156
w._set_extra_kwds(kwds)
157
return w
158
159
@options(opacity=1, color="blue", aspect_ratio=[1,1,1], thickness=2)
160
def bezier3d(path, **options):
161
"""
162
Draws a 3-dimensional bezier path. Input is similar to bezier_path, but each
163
point in the path and each control point is required to have 3 coordinates.
164
165
INPUT:
166
167
- ``path`` - a list of curves, which each is a list of points. See further
168
detail below.
169
170
- ``thickness`` - (default: 2)
171
172
- ``color`` - a word that describes a color
173
174
- ``opacity`` - (default: 1) if less than 1 then is
175
transparent
176
177
- ``aspect_ratio`` - (default:[1,1,1])
178
179
The path is a list of curves, and each curve is a list of points.
180
Each point is a tuple (x,y,z).
181
182
The first curve contains the endpoints as the first and last point
183
in the list. All other curves assume a starting point given by the
184
last entry in the preceding list, and take the last point in the list
185
as their opposite endpoint. A curve can have 0, 1 or 2 control points
186
listed between the endpoints. In the input example for path below,
187
the first and second curves have 2 control points, the third has one,
188
and the fourth has no control points::
189
190
path = [[p1, c1, c2, p2], [c3, c4, p3], [c5, p4], [p5], ...]
191
192
In the case of no control points, a straight line will be drawn
193
between the two endpoints. If one control point is supplied, then
194
the curve at each of the endpoints will be tangent to the line from
195
that endpoint to the control point. Similarly, in the case of two
196
control points, at each endpoint the curve will be tangent to the line
197
connecting that endpoint with the control point immediately after or
198
immediately preceding it in the list.
199
200
So in our example above, the curve between p1 and p2 is tangent to the
201
line through p1 and c1 at p1, and tangent to the line through p2 and c2
202
at p2. Similarly, the curve between p2 and p3 is tangent to line(p2,c3)
203
at p2 and tangent to line(p3,c4) at p3. Curve(p3,p4) is tangent to
204
line(p3,c5) at p3 and tangent to line(p4,c5) at p4. Curve(p4,p5) is a
205
straight line.
206
207
EXAMPLES::
208
209
sage: path = [[(0,0,0),(.5,.1,.2),(.75,3,-1),(1,1,0)],[(.5,1,.2),(1,.5,0)],[(.7,.2,.5)]]
210
sage: b = bezier3d(path, color='green')
211
sage: b
212
213
To construct a simple curve, create a list containing a single list::
214
215
sage: path = [[(0,0,0),(1,0,0),(0,1,0),(0,1,1)]]
216
sage: curve = bezier3d(path, thickness=5, color='blue')
217
sage: curve
218
"""
219
import parametric_plot3d as P3D
220
from sage.modules.free_module_element import vector
221
from sage.calculus.calculus import var
222
223
p0 = vector(path[0][-1])
224
t = var('t')
225
if len(path[0]) > 2:
226
B = (1-t)**3*vector(path[0][0])+3*t*(1-t)**2*vector(path[0][1])+3*t**2*(1-t)*vector(path[0][-2])+t**3*p0
227
G = P3D.parametric_plot3d(list(B), (0, 1), color=options['color'], aspect_ratio=options['aspect_ratio'], thickness=options['thickness'], opacity=options['opacity'])
228
else:
229
G = line3d([path[0][0], p0], color=options['color'], thickness=options['thickness'], opacity=options['opacity'])
230
231
for curve in path[1:]:
232
if len(curve) > 1:
233
p1 = vector(curve[0])
234
p2 = vector(curve[-2])
235
p3 = vector(curve[-1])
236
B = (1-t)**3*p0+3*t*(1-t)**2*p1+3*t**2*(1-t)*p2+t**3*p3
237
G += P3D.parametric_plot3d(list(B), (0, 1), color=options['color'], aspect_ratio=options['aspect_ratio'], thickness=options['thickness'], opacity=options['opacity'])
238
else:
239
G += line3d([p0,curve[0]], color=options['color'], thickness=options['thickness'], opacity=options['opacity'])
240
p0 = curve[-1]
241
return G
242
243
@rename_keyword(alpha='opacity')
244
@options(opacity=1, color=(0,0,1))
245
def polygon3d(points, **options):
246
"""
247
Draw a polygon in 3d.
248
249
INPUT:
250
251
- ``points`` - the vertices of the polygon
252
253
Type ``polygon3d.options`` for a dictionary of the default
254
options for polygons. You can change this to change
255
the defaults for all future polygons. Use ``polygon3d.reset()``
256
to reset to the default options.
257
258
EXAMPLES:
259
260
A simple triangle::
261
262
sage: polygon3d([[0,0,0], [1,2,3], [3,0,0]])
263
264
Some modern art -- a random polygon::
265
266
sage: v = [(randrange(-5,5), randrange(-5,5), randrange(-5, 5)) for _ in range(10)]
267
sage: polygon3d(v)
268
269
A bent transparent green triangle::
270
271
sage: polygon3d([[1, 2, 3], [0,1,0], [1,0,1], [3,0,0]], color=(0,1,0), alpha=0.7)
272
"""
273
from sage.plot.plot3d.index_face_set import IndexFaceSet
274
return IndexFaceSet([range(len(points))], points, **options)
275
276
def frame3d(lower_left, upper_right, **kwds):
277
"""
278
Draw a frame in 3-D. Primarily used as a helper function for
279
creating frames for 3-D graphics viewing.
280
281
INPUT:
282
283
- ``lower_left`` - the lower left corner of the frame, as a
284
list, tuple, or vector.
285
286
- ``upper_right`` - the upper right corner of the frame, as a
287
list, tuple, or vector.
288
289
Type ``line3d.options`` for a dictionary of the default
290
options for lines, which are also available.
291
292
EXAMPLES:
293
294
A frame::
295
296
sage: from sage.plot.plot3d.shapes2 import frame3d
297
sage: frame3d([1,3,2],vector([2,5,4]),color='red')
298
299
This is usually used for making an actual plot::
300
301
sage: y = var('y')
302
sage: plot3d(sin(x^2+y^2),(x,0,pi),(y,0,pi))
303
"""
304
x0,y0,z0 = lower_left
305
x1,y1,z1 = upper_right
306
L1 = line3d([(x0,y0,z0), (x0,y1,z0), (x1,y1,z0), (x1,y0,z0), (x0,y0,z0), # top square
307
(x0,y0,z1), (x0,y1,z1), (x1,y1,z1), (x1,y0,z1), (x0,y0,z1)], # bottom square
308
**kwds)
309
# 3 additional lines joining top to bottom
310
v2 = line3d([(x0,y1,z0), (x0,y1,z1)], **kwds)
311
v3 = line3d([(x1,y0,z0), (x1,y0,z1)], **kwds)
312
v4 = line3d([(x1,y1,z0), (x1,y1,z1)], **kwds)
313
F = L1 + v2 + v3 + v4
314
F._set_extra_kwds(kwds)
315
return F
316
317
def frame_labels(lower_left, upper_right,
318
label_lower_left, label_upper_right, eps = 1,
319
**kwds):
320
"""
321
Draw correct labels for a given frame in 3-D. Primarily
322
used as a helper function for creating frames for 3-D graphics
323
viewing - do not use directly unless you know what you are doing!
324
325
INPUT:
326
327
- ``lower_left`` - the lower left corner of the frame, as a
328
list, tuple, or vector.
329
330
- ``upper_right`` - the upper right corner of the frame, as a
331
list, tuple, or vector.
332
333
- ``label_lower_left`` - the label for the lower left corner
334
of the frame, as a list, tuple, or vector. This label must actually
335
have all coordinates less than the coordinates of the other label.
336
337
- ``label_upper_right`` - the label for the upper right corner
338
of the frame, as a list, tuple, or vector. This label must actually
339
have all coordinates greater than the coordinates of the other label.
340
341
- ``eps`` - (default: 1) a parameter for how far away from the frame
342
to put the labels.
343
344
Type ``line3d.options`` for a dictionary of the default
345
options for lines, which are also available.
346
347
EXAMPLES:
348
349
We can use it directly::
350
351
sage: from sage.plot.plot3d.shapes2 import frame_labels
352
sage: frame_labels([1,2,3],[4,5,6],[1,2,3],[4,5,6])
353
354
This is usually used for making an actual plot::
355
356
sage: y = var('y')
357
sage: P = plot3d(sin(x^2+y^2),(x,0,pi),(y,0,pi))
358
sage: a,b = P._rescale_for_frame_aspect_ratio_and_zoom(1.0,[1,1,1],1)
359
sage: F = frame_labels(a,b,*P._box_for_aspect_ratio("automatic",a,b))
360
sage: F.jmol_repr(F.default_render_params())[0]
361
[['select atomno = 1', 'color atom [76,76,76]', 'label "0.0"']]
362
363
TESTS::
364
365
sage: frame_labels([1,2,3],[4,5,6],[1,2,3],[1,3,4])
366
Traceback (most recent call last):
367
...
368
ValueError: Ensure the upper right labels are above and to the right of the lower left labels.
369
"""
370
x0,y0,z0 = lower_left
371
x1,y1,z1 = upper_right
372
lx0,ly0,lz0 = label_lower_left
373
lx1,ly1,lz1 = label_upper_right
374
if (lx1 - lx0) <= 0 or (ly1 - ly0) <= 0 or (lz1 - lz0) <= 0:
375
raise ValueError("Ensure the upper right labels are above and to the right of the lower left labels.")
376
377
# Helper function for formatting the frame labels
378
from math import log
379
log10 = log(10)
380
nd = lambda a: int(log(a)/log10)
381
def fmt_string(a):
382
b = a/2.0
383
if b >= 1:
384
return "%.1f"
385
n = max(0, 2 - nd(a/2.0))
386
return "%%.%sf"%n
387
388
# Slightly faster than mean for this situation
389
def avg(a,b):
390
return (a+b)/2.0
391
392
color = (0.3,0.3,0.3)
393
394
fmt = fmt_string(lx1 - lx0)
395
T = Text(fmt%lx0, color=color).translate((x0,y0-eps,z0))
396
T += Text(fmt%avg(lx0,lx1), color=color).translate((avg(x0,x1),y0-eps,z0))
397
T += Text(fmt%lx1, color=color).translate((x1,y0-eps,z0))
398
399
fmt = fmt_string(ly1 - ly0)
400
T += Text(fmt%ly0, color=color).translate((x1+eps,y0,z0))
401
T += Text(fmt%avg(ly0,ly1), color=color).translate((x1+eps,avg(y0,y1),z0))
402
T += Text(fmt%ly1, color=color).translate((x1+eps,y1,z0))
403
404
fmt = fmt_string(lz1 - lz0)
405
T += Text(fmt%lz0, color=color).translate((x0-eps,y0,z0))
406
T += Text(fmt%avg(lz0,lz1), color=color).translate((x0-eps,y0,avg(z0,z1)))
407
T += Text(fmt%lz1, color=color).translate((x0-eps,y0,z1))
408
return T
409
410
411
def ruler(start, end, ticks=4, sub_ticks=4, absolute=False, snap=False, **kwds):
412
"""
413
Draw a ruler in 3-D, with major and minor ticks.
414
415
INPUT:
416
417
- ``start`` - the beginning of the ruler, as a list,
418
tuple, or vector.
419
420
- ``end`` - the end of the ruler, as a list, tuple,
421
or vector.
422
423
- ``ticks`` - (default: 4) the number of major ticks
424
shown on the ruler.
425
426
- ``sub_ticks`` - (default: 4) the number of shown
427
subdivisions between each major tick.
428
429
- ``absolute`` - (default: ``False``) if ``True``, makes a huge ruler
430
in the direction of an axis.
431
432
- ``snap`` - (default: ``False``) if ``True``, snaps to an implied
433
grid.
434
435
Type ``line3d.options`` for a dictionary of the default
436
options for lines, which are also available.
437
438
EXAMPLES:
439
440
A ruler::
441
442
sage: from sage.plot.plot3d.shapes2 import ruler
443
sage: R = ruler([1,2,3],vector([2,3,4])); R
444
445
A ruler with some options::
446
447
sage: R = ruler([1,2,3],vector([2,3,4]),ticks=6, sub_ticks=2, color='red'); R
448
449
The keyword ``snap`` makes the ticks not necessarily coincide
450
with the ruler::
451
452
sage: ruler([1,2,3],vector([1,2,4]),snap=True)
453
454
The keyword ``absolute`` makes a huge ruler in one of the axis
455
directions::
456
457
sage: ruler([1,2,3],vector([1,2,4]),absolute=True)
458
459
TESTS::
460
461
sage: ruler([1,2,3],vector([1,3,4]),absolute=True)
462
Traceback (most recent call last):
463
...
464
ValueError: Absolute rulers only valid for axis-aligned paths
465
"""
466
start = vector(RDF, start)
467
end = vector(RDF, end)
468
dir = end - start
469
dist = math.sqrt(dir.dot_product(dir))
470
dir /= dist
471
472
one_tick = dist/ticks * 1.414
473
unit = 10 ** math.floor(math.log(dist/ticks, 10))
474
if unit * 5 < one_tick:
475
unit *= 5
476
elif unit * 2 < one_tick:
477
unit *= 2
478
479
if dir[0]:
480
tick = dir.cross_product(vector(RDF, (0,0,-dist/30)))
481
elif dir[1]:
482
tick = dir.cross_product(vector(RDF, (0,0,dist/30)))
483
else:
484
tick = vector(RDF, (dist/30,0,0))
485
486
if snap:
487
for i in range(3):
488
start[i] = unit * math.floor(start[i]/unit + 1e-5)
489
end[i] = unit * math.ceil(end[i]/unit - 1e-5)
490
491
if absolute:
492
if dir[0]*dir[1] or dir[1]*dir[2] or dir[0]*dir[2]:
493
raise ValueError, "Absolute rulers only valid for axis-aligned paths"
494
m = max(dir[0], dir[1], dir[2])
495
if dir[0] == m:
496
off = start[0]
497
elif dir[1] == m:
498
off = start[1]
499
else:
500
off = start[2]
501
first_tick = unit * math.ceil(off/unit - 1e-5) - off
502
else:
503
off = 0
504
first_tick = 0
505
506
ruler = shapes.LineSegment(start, end, **kwds)
507
for k in range(1, int(sub_ticks * first_tick/unit)):
508
P = start + dir*(k*unit/sub_ticks)
509
ruler += shapes.LineSegment(P, P + tick/2, **kwds)
510
for d in srange(first_tick, dist + unit/(sub_ticks+1), unit):
511
P = start + dir*d
512
ruler += shapes.LineSegment(P, P + tick, **kwds)
513
ruler += shapes.Text(str(d+off), **kwds).translate(P - tick)
514
if dist - d < unit:
515
sub_ticks = int(sub_ticks * (dist - d)/unit)
516
for k in range(1, sub_ticks):
517
P += dir * (unit/sub_ticks)
518
ruler += shapes.LineSegment(P, P + tick/2, **kwds)
519
return ruler
520
521
def ruler_frame(lower_left, upper_right, ticks=4, sub_ticks=4, **kwds):
522
"""
523
Draw a frame made of 3-D rulers, with major and minor ticks.
524
525
INPUT:
526
527
- ``lower_left`` - the lower left corner of the frame, as a
528
list, tuple, or vector.
529
530
- ``upper_right`` - the upper right corner of the frame, as a
531
list, tuple, or vector.
532
533
- ``ticks`` - (default: 4) the number of major ticks
534
shown on each ruler.
535
536
- ``sub_ticks`` - (default: 4) the number of shown
537
subdivisions between each major tick.
538
539
Type ``line3d.options`` for a dictionary of the default
540
options for lines, which are also available.
541
542
EXAMPLES:
543
544
A ruler frame::
545
546
sage: from sage.plot.plot3d.shapes2 import ruler_frame
547
sage: F = ruler_frame([1,2,3],vector([2,3,4])); F
548
549
A ruler frame with some options::
550
551
sage: F = ruler_frame([1,2,3],vector([2,3,4]),ticks=6, sub_ticks=2, color='red'); F
552
"""
553
return ruler(lower_left, (upper_right[0], lower_left[1], lower_left[2]), ticks=ticks, sub_ticks=sub_ticks, absolute=True, **kwds) \
554
+ ruler(lower_left, (lower_left[0], upper_right[1], lower_left[2]), ticks=ticks, sub_ticks=sub_ticks, absolute=True, **kwds) \
555
+ ruler(lower_left, (lower_left[0], lower_left[1], upper_right[2]), ticks=ticks, sub_ticks=sub_ticks, absolute=True, **kwds)
556
557
558
559
560
###########################
561
562
563
def sphere(center=(0,0,0), size=1, **kwds):
564
r"""
565
Return a plot of a sphere of radius size centered at
566
`(x,y,z)`.
567
568
INPUT:
569
570
571
- ``(x,y,z)`` - center (default: (0,0,0)
572
573
- ``size`` - the radius (default: 1)
574
575
576
EXAMPLES: A simple sphere::
577
578
sage: sphere()
579
580
Two spheres touching::
581
582
sage: sphere(center=(-1,0,0)) + sphere(center=(1,0,0), aspect_ratio=[1,1,1])
583
584
Spheres of radii 1 and 2 one stuck into the other::
585
586
sage: sphere(color='orange') + sphere(color=(0,0,0.3), \
587
center=(0,0,-2),size=2,opacity=0.9)
588
589
We draw a transparent sphere on a saddle.
590
591
::
592
593
sage: u,v = var('u v')
594
sage: saddle = plot3d(u^2 - v^2, (u,-2,2), (v,-2,2))
595
sage: sphere((0,0,1), color='red', opacity=0.5, aspect_ratio=[1,1,1]) + saddle
596
597
TESTS::
598
599
sage: T = sage.plot.plot3d.texture.Texture('red')
600
sage: S = sphere(texture=T)
601
sage: T in S.texture_set()
602
True
603
"""
604
kwds['texture'] = Texture(**kwds)
605
G = Sphere(size, **kwds)
606
H = G.translate(center)
607
H._set_extra_kwds(kwds)
608
return H
609
610
def text3d(txt, (x,y,z), **kwds):
611
r"""
612
Display 3d text.
613
614
INPUT:
615
616
617
- ``txt`` - some text
618
619
- ``(x,y,z)`` - position
620
621
- ``**kwds`` - standard 3d graphics options
622
623
624
.. note::
625
626
There is no way to change the font size or opacity yet.
627
628
EXAMPLES: We write the word Sage in red at position (1,2,3)::
629
630
sage: text3d("Sage", (1,2,3), color=(0.5,0,0))
631
632
We draw a multicolor spiral of numbers::
633
634
sage: sum([text3d('%.1f'%n, (cos(n),sin(n),n), color=(n/2,1-n/2,0)) \
635
for n in [0,0.2,..,8]])
636
637
Another example
638
639
::
640
641
sage: text3d("Sage is really neat!!",(2,12,1))
642
643
And in 3d in two places::
644
645
sage: text3d("Sage is...",(2,12,1), rgbcolor=(1,0,0)) + text3d("quite powerful!!",(4,10,0), rgbcolor=(0,0,1))
646
"""
647
if not kwds.has_key('color') and not kwds.has_key('rgbcolor'):
648
kwds['color'] = (0,0,0)
649
G = Text(txt, **kwds).translate((x,y,z))
650
G._set_extra_kwds(kwds)
651
652
return G
653
654
class Point(PrimitiveObject):
655
"""
656
Create a position in 3-space, represented by a sphere of fixed
657
size.
658
659
INPUT:
660
661
- ``center`` - point (3-tuple)
662
663
- ``size`` - (default: 1)
664
665
EXAMPLE:
666
667
We normally access this via the ``point3d`` function. Note that extra
668
keywords are correctly used::
669
670
sage: point3d((4,3,2),size=2,color='red',opacity=.5)
671
"""
672
def __init__(self, center, size=1, **kwds):
673
"""
674
Create the graphics primitive :class:`Point` in 3-D. See the
675
docstring of this class for full documentation.
676
677
EXAMPLES::
678
679
sage: from sage.plot.plot3d.shapes2 import Point
680
sage: P = Point((1,2,3),2)
681
sage: P.loc
682
(1.0, 2.0, 3.0)
683
"""
684
PrimitiveObject.__init__(self, **kwds)
685
self.loc = (float(center[0]), float(center[1]), float(center[2]))
686
self.size = size
687
self._set_extra_kwds(kwds)
688
689
def bounding_box(self):
690
"""
691
Returns the lower and upper corners of a 3-D bounding box for ``self``.
692
This is used for rendering and ``self`` should fit entirely within this
693
box. In this case, we simply return the center of the point.
694
695
TESTS::
696
697
sage: P = point3d((-3,2,10),size=7)
698
sage: P.bounding_box()
699
((-3.0, 2.0, 10.0), (-3.0, 2.0, 10.0))
700
"""
701
return self.loc, self.loc
702
703
def tachyon_repr(self, render_params):
704
"""
705
Returns representation of the point suitable for plotting
706
using the Tachyon ray tracer.
707
708
TESTS::
709
710
sage: P = point3d((1,2,3),size=3,color='purple')
711
sage: P.tachyon_repr(P.default_render_params())
712
'Sphere center 1.0 2.0 3.0 Rad 0.015 texture...'
713
"""
714
transform = render_params.transform
715
if transform is None:
716
cen = self.loc
717
else:
718
cen = transform.transform_point(self.loc)
719
return "Sphere center %s %s %s Rad %s %s" % (cen[0], cen[1], cen[2], self.size * TACHYON_PIXEL, self.texture.id)
720
721
def obj_repr(self, render_params):
722
"""
723
Returns complete representation of the point as a sphere.
724
725
TESTS::
726
727
sage: P = point3d((1,2,3),size=3,color='purple')
728
sage: P.obj_repr(P.default_render_params())[0][0:2]
729
['g obj_1', 'usemtl texture...']
730
"""
731
T = render_params.transform
732
if T is None:
733
import transform
734
T = transform.Transformation()
735
render_params.push_transform(~T)
736
S = shapes.Sphere(self.size / 200.0).translate(T(self.loc))
737
cmds = S.obj_repr(render_params)
738
render_params.pop_transform()
739
return cmds
740
741
def jmol_repr(self, render_params):
742
r"""
743
Returns representation of the object suitable for plotting
744
using Jmol.
745
746
TESTS::
747
748
sage: P = point3d((1,2,3),size=3,color='purple')
749
sage: P.jmol_repr(P.default_render_params())
750
['draw point_1 DIAMETER 3 {1.0 2.0 3.0}\ncolor $point_1 [128,0,128]']
751
"""
752
name = render_params.unique_name('point')
753
transform = render_params.transform
754
cen = self.loc if transform is None else transform(self.loc)
755
return ["draw %s DIAMETER %s {%s %s %s}\n%s" % (name, int(self.size), cen[0], cen[1], cen[2], self.texture.jmol_str('$' + name))]
756
757
class Line(PrimitiveObject):
758
r"""
759
Draw a 3d line joining a sequence of points.
760
761
This line has a fixed diameter unaffected by transformations and
762
zooming. It may be smoothed if ``corner_cutoff < 1``.
763
764
INPUT:
765
766
- ``points`` - list of points to pass through
767
768
- ``thickness`` - diameter of the line
769
770
- ``corner_cutoff`` - threshold for smoothing (see
771
the corners() method) this is the minimum cosine between adjacent
772
segments to smooth
773
774
- ``arrow_head`` - if True make this curve into an
775
arrow
776
777
778
EXAMPLES::
779
780
sage: from sage.plot.plot3d.shapes2 import Line
781
sage: Line([(i*math.sin(i), i*math.cos(i), i/3) for i in range(30)], arrow_head=True)
782
783
Smooth angles less than 90 degrees::
784
785
sage: Line([(0,0,0),(1,0,0),(2,1,0),(0,1,0)], corner_cutoff=0)
786
"""
787
def __init__(self, points, thickness=5, corner_cutoff=.5, arrow_head=False, **kwds):
788
"""
789
Create the graphics primitive :class:`Line` in 3-D. See the
790
docstring of this class for full documentation.
791
792
EXAMPLES::
793
794
sage: from sage.plot.plot3d.shapes2 import Line
795
sage: P = Line([(1,2,3),(1,2,2),(-1,2,2),(-1,3,2)],thickness=6,corner_cutoff=.2)
796
sage: P.points, P.arrow_head
797
([(1, 2, 3), (1, 2, 2), (-1, 2, 2), (-1, 3, 2)], False)
798
"""
799
if len(points) < 2:
800
raise ValueError, "there must be at least 2 points"
801
PrimitiveObject.__init__(self, **kwds)
802
self.points = points
803
self.thickness = thickness
804
self.corner_cutoff = corner_cutoff
805
self.arrow_head = arrow_head
806
807
def bounding_box(self):
808
"""
809
Returns the lower and upper corners of a 3-D bounding box for ``self``.
810
This is used for rendering and ``self`` should fit entirely within this
811
box. In this case, we return the highest and lowest values of each
812
coordinate among all points.
813
814
TESTS::
815
816
sage: from sage.plot.plot3d.shapes2 import Line
817
sage: L = Line([(i,i^2-1,-2*ln(i)) for i in [10,20,30]])
818
sage: L.bounding_box()
819
((10.0, 99.0, -6.802394763324311), (30.0, 899.0, -4.605170185988092))
820
"""
821
try:
822
return self.__bounding_box
823
except AttributeError:
824
self.__bounding_box = point_list_bounding_box(self.points)
825
return self.__bounding_box
826
827
828
def tachyon_repr(self, render_params):
829
"""
830
Returns representation of the line suitable for plotting
831
using the Tachyon ray tracer.
832
833
TESTS::
834
835
sage: L = line3d([(cos(i),sin(i),i^2) for i in srange(0,10,.01)],color='red')
836
sage: L.tachyon_repr(L.default_render_params())[0]
837
'FCylinder base 1.0 0.0 0.0 apex 0.999950000417 0.00999983333417 0.0001 rad 0.005 texture...'
838
"""
839
T = render_params.transform
840
cmds = []
841
px, py, pz = self.points[0] if T is None else T(self.points[0])
842
radius = self.thickness * TACHYON_PIXEL
843
for P in self.points[1:]:
844
x, y, z = P if T is None else T(P)
845
if self.arrow_head and P is self.points[-1]:
846
A = shapes.arrow3d((px, py, pz), (x, y, z), radius = radius, texture = self.texture)
847
render_params.push_transform(~T)
848
cmds.append(A.tachyon_repr(render_params))
849
render_params.pop_transform()
850
else:
851
cmds.append("FCylinder base %s %s %s apex %s %s %s rad %s %s" % (px, py, pz,
852
x, y, z,
853
radius,
854
self.texture.id))
855
px, py, pz = x, y, z
856
return cmds
857
858
def obj_repr(self, render_params):
859
"""
860
Returns complete representation of the line as an object.
861
862
TESTS::
863
864
sage: from sage.plot.plot3d.shapes2 import Line
865
sage: L = Line([(cos(i),sin(i),i^2) for i in srange(0,10,.01)],color='red')
866
sage: L.obj_repr(L.default_render_params())[0][0][0][2][:3]
867
['v 0.99995 0.00999983 0.0001', 'v 1.00007 0.0102504 -0.0248984', 'v 1.02376 0.010195 -0.00750607']
868
"""
869
T = render_params.transform
870
if T is None:
871
import transform
872
T = transform.Transformation()
873
render_params.push_transform(~T)
874
L = line3d([T(P) for P in self.points], radius=self.thickness / 200.0, arrow_head=self.arrow_head, texture=self.texture)
875
cmds = L.obj_repr(render_params)
876
render_params.pop_transform()
877
return cmds
878
879
def jmol_repr(self, render_params):
880
r"""
881
Returns representation of the object suitable for plotting
882
using Jmol.
883
884
TESTS::
885
886
sage: L = line3d([(cos(i),sin(i),i^2) for i in srange(0,10,.01)],color='red')
887
sage: L.jmol_repr(L.default_render_params())[0][:42]
888
'draw line_1 diameter 1 curve {1.0 0.0 0.0}'
889
"""
890
T = render_params.transform
891
corners = self.corners(max_len=255) # hardcoded limit in jmol
892
last_corner = corners[-1]
893
corners = set(corners)
894
cmds = []
895
cmd = None
896
for P in self.points:
897
TP = P if T is None else T(P)
898
if P in corners:
899
if cmd:
900
cmds.append(cmd + " {%s %s %s} " % TP)
901
cmds.append(self.texture.jmol_str('$'+name))
902
type = 'arrow' if self.arrow_head and P is last_corner else 'curve'
903
name = render_params.unique_name('line')
904
cmd = "draw %s diameter %s %s {%s %s %s} " % (name, int(self.thickness), type, TP[0], TP[1], TP[2])
905
else:
906
cmd += " {%s %s %s} " % TP
907
cmds.append(cmd)
908
cmds.append(self.texture.jmol_str('$'+name))
909
return cmds
910
911
def corners(self, corner_cutoff=None, max_len=None):
912
"""
913
Figures out where the curve turns too sharply to pretend it's
914
smooth.
915
916
INPUT: Maximum cosine of angle between adjacent line segments
917
before adding a corner
918
919
OUTPUT: List of points at which to start a new line. This always
920
includes the first point, and never the last.
921
922
EXAMPLES:
923
924
Every point::
925
926
sage: from sage.plot.plot3d.shapes2 import Line
927
sage: Line([(0,0,0),(1,0,0),(2,1,0),(0,1,0)], corner_cutoff=1).corners()
928
[(0, 0, 0), (1, 0, 0), (2, 1, 0)]
929
930
Greater than 90 degrees::
931
932
sage: Line([(0,0,0),(1,0,0),(2,1,0),(0,1,0)], corner_cutoff=0).corners()
933
[(0, 0, 0), (2, 1, 0)]
934
935
No corners::
936
937
sage: Line([(0,0,0),(1,0,0),(2,1,0),(0,1,0)], corner_cutoff=-1).corners()
938
(0, 0, 0)
939
940
An intermediate value::
941
942
sage: Line([(0,0,0),(1,0,0),(2,1,0),(0,1,0)], corner_cutoff=.5).corners()
943
[(0, 0, 0), (2, 1, 0)]
944
"""
945
if corner_cutoff is None:
946
corner_cutoff = self.corner_cutoff
947
if corner_cutoff >= 1:
948
if max_len:
949
self.points[:-1][::max_len-1]
950
else:
951
return self.points[:-1]
952
elif corner_cutoff <= -1:
953
return self.points[0]
954
else:
955
if not max_len:
956
max_len = len(self.points)+1
957
count = 2
958
# ... -- prev -- cur -- next -- ...
959
cur = self.points[0]
960
next = self.points[1]
961
next_dir = [next[i] - cur[i] for i in range(3)]
962
corners = [cur]
963
cur, prev_dir = next, next_dir
964
965
# quicker than making them vectors first
966
def dot((x0,y0,z0), (x1,y1,z1)):
967
return x0*x1 + y0*y1 + z0*z1
968
969
for next in self.points[2:]:
970
if next == cur:
971
corners.append(cur)
972
cur = next
973
count = 1
974
continue
975
next_dir = [next[i] - cur[i] for i in range(3)]
976
cos_angle = dot(prev_dir, next_dir) / math.sqrt(dot(prev_dir, prev_dir) * dot(next_dir, next_dir))
977
if cos_angle <= corner_cutoff or count > max_len-1:
978
corners.append(cur)
979
count = 1
980
cur, prev_dir = next, next_dir
981
count += 1
982
return corners
983
984
985
986
def point3d(v, size=5, **kwds):
987
"""
988
Plot a point or list of points in 3d space.
989
990
INPUT:
991
992
- ``v`` -- a point or list of points
993
994
- ``size`` -- (default: 5) size of the point (or
995
points)
996
997
- ``color`` -- a word that describes a color
998
999
- ``rgbcolor`` -- (r,g,b) with r, g, b between 0 and 1
1000
that describes a color
1001
1002
- ``opacity`` -- (default: 1) if less than 1 then is
1003
transparent
1004
1005
EXAMPLES::
1006
1007
sage: sum([point3d((i,i^2,i^3), size=5) for i in range(10)])
1008
1009
We check to make sure this works with vectors and other iterables::
1010
1011
sage: pl = point3d([vector(ZZ,(1, 0, 0)), vector(ZZ,(0, 1, 0)), (-1, -1, 0)])
1012
sage: print point(vector((2,3,4)))
1013
Graphics3d Object
1014
1015
sage: c = polytopes.n_cube(3)
1016
sage: v = c.vertices()[0]; v
1017
A vertex at (-1, -1, -1)
1018
sage: print point(v)
1019
Graphics3d Object
1020
1021
We check to make sure the options work::
1022
1023
sage: point3d((4,3,2),size=20,color='red',opacity=.5)
1024
1025
numpy arrays can be provided as input::
1026
1027
sage: import numpy
1028
sage: point3d(numpy.array([1,2,3]))
1029
1030
sage: point3d(numpy.array([[1,2,3], [4,5,6], [7,8,9]]))
1031
1032
"""
1033
if len(v) == 3:
1034
try:
1035
# check if the first element can be changed to a float
1036
tmp = RDF(v[0])
1037
return Point(v, size, **kwds)
1038
except TypeError:
1039
pass
1040
1041
A = sum([Point(z, size, **kwds) for z in v])
1042
A._set_extra_kwds(kwds)
1043
return A
1044