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