Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
""" Indexed Face Sets
Graphics3D object that consists of a list of polygons, also used for triangulations of other objects.
Usually these objects are not created directly by users.
AUTHORS:
- Robert Bradshaw (2007-08-26): initial version - Robert Bradshaw (2007-08-28): significant optimizations
.. TODO::
Smooth triangles using vertex normals
""" #***************************************************************************** # Copyright (C) 2007 Robert Bradshaw <robertwb@math.washington.edu> # # Distributed under the terms of the GNU General Public License (GPL) # # This code is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # The full text of the GPL is available at: # # http://www.gnu.org/licenses/ #***************************************************************************** from __future__ import print_function, absolute_import
from libc.math cimport isfinite, INFINITY from libc.string cimport memset, memcpy from cysignals.memory cimport check_calloc, check_allocarray, check_reallocarray, sig_free from cysignals.signals cimport sig_check, sig_on, sig_off
cdef extern from *: int sprintf_3d "sprintf" (char*, char*, double, double, double) int sprintf_3i "sprintf" (char*, char*, int, int, int) int sprintf_4i "sprintf" (char*, char*, int, int, int, int) int sprintf_5i "sprintf" (char*, char*, int, int, int, int, int) int sprintf_6i "sprintf" (char*, char*, int, int, int, int, int, int) int sprintf_7i "sprintf" (char*, char*, int, int, int, int, int, int, int) int sprintf_9d "sprintf" (char*, char*, double, double, double, double, double, double, double, double, double)
from cpython.list cimport * from cpython.bytes cimport *
include "point_c.pxi"
from math import sin, cos, sqrt from random import randint
from sage.cpython.string cimport bytes_to_str
from sage.rings.real_double import RDF
from sage.matrix.constructor import matrix from sage.modules.free_module_element import vector
from sage.plot.colors import Color, float_to_integer from sage.plot.plot3d.base import Graphics3dGroup
from .transform cimport Transformation
# -------------------------------------------------------------------- # Fast routines for generating string representations of the polygons. # --------------------------------------------------------------------
cdef inline format_tachyon_texture(color_c rgb): cdef char rs[200] "TEXTURE\n AMBIENT 0.3 DIFFUSE 0.7 SPECULAR 0 OPACITY 1.0\n COLOR %g %g %g \n TEXFUNC 0", rgb.r, rgb.g, rgb.b)
cdef inline format_tachyon_triangle(point_c P, point_c Q, point_c R): cdef char ss[250] # PyBytes_FromFormat doesn't do floats? "TRI V0 %g %g %g V1 %g %g %g V2 %g %g %g", P.x, P.y, P.z, Q.x, Q.y, Q.z, R.x, R.y, R.z )
cdef inline format_json_vertex(point_c P): cdef char ss[100]
cdef inline format_json_face(face_c face):
cdef inline format_obj_vertex(point_c P): cdef char ss[100] # PyBytes_FromFormat doesn't do floats?
cdef inline format_obj_face(face_c face, int off): cdef char ss[100] cdef Py_ssize_t r, i elif face.n == 4: else: return "f " + " ".join([str(face.vertices[i] + off) for i from 0 <= i < face.n]) # PyBytes_FromFormat is almost twice as slow
cdef inline format_obj_face_back(face_c face, int off): cdef char ss[100] cdef Py_ssize_t r, i elif face.n == 4: else: return "f " + " ".join([str(face.vertices[i] + off) for i from face.n > i >= 0])
cdef inline format_pmesh_vertex(point_c P): cdef char ss[100] # PyBytes_FromFormat doesn't do floats?
cdef inline format_pmesh_face(face_c face, int has_color): cdef char ss[100] cdef Py_ssize_t r, i cdef int color # if the face has an individual color, has_color is -1 # otherwise it is 1 # it seems that Jmol does not like the 0 color at all color = 1
face.vertices[0], face.vertices[1], face.vertices[2], face.vertices[0]) else: face.vertices[0], face.vertices[1], face.vertices[2], elif face.n == 4: face.vertices[0], face.vertices[1], face.vertices[2], face.vertices[3], face.vertices[0]) else: face.vertices[0], face.vertices[1], face.vertices[2], face.vertices[3], else: # Naive triangulation face.vertices[0], face.vertices[i], face.vertices[i + 1], face.vertices[0]) else: for i from 1 <= i < face.n - 1: r = sprintf_6i(ss, "%d\n%d\n%d\n%d\n%d\n%d", has_color * 4, face.vertices[0], face.vertices[i], face.vertices[i + 1], face.vertices[0], color) PyList_Append(all, PyBytes_FromStringAndSize(ss, r)) # PyBytes_FromFormat is almost twice as slow
cdef class IndexFaceSet(PrimitiveObject): """ Graphics3D object that consists of a list of polygons, also used for triangulations of other objects.
Polygons (mostly triangles and quadrilaterals) are stored in the c struct ``face_c`` (see transform.pyx). Rather than storing the points directly for each polygon, each face consists a list of pointers into a common list of points which are basically triples of doubles in a ``point_c``.
Moreover, each face has an attribute ``color`` which is used to store color information when faces are colored. The red/green/blue components are then available as floats between 0 and 1 using ``color.r,color.g,color.b``.
Usually these objects are not created directly by users.
EXAMPLES::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: S = IndexFaceSet([[(1,0,0),(0,1,0),(0,0,1)],[(1,0,0),(0,1,0),(0,0,0)]]) sage: S.face_list() [[(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 0.0)]] sage: S.vertex_list() [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0)]
sage: def make_face(n): return [(0,0,n),(0,1,n),(1,1,n),(1,0,n)] sage: S = IndexFaceSet([make_face(n) for n in range(10)]) sage: S.show()
sage: point_list = [(1,0,0),(0,1,0)] + [(0,0,n) for n in range(10)] sage: face_list = [[0,1,n] for n in range(2,10)] sage: S = IndexFaceSet(face_list, point_list, color='red') sage: S.face_list() [[(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 0.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 2.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 3.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 4.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 5.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 6.0)], [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 7.0)]] sage: S.show()
A simple example of colored IndexFaceSet (:trac:`12212`)::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list = [Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: S.show(viewer='tachyon')
""" def __cinit__(self): def __init__(self, faces, point_list=None, enclosed=False, texture_list=None, **kwds):
cdef Py_ssize_t i
else:
cdef realloc(self, vcount, fcount, icount): r""" Allocates memory for vertices, faces, and face indices. Can only be called from Cython, so the doctests must be indirect.
EXAMPLES::
sage: var('x,y,z') (x, y, z) sage: G = implicit_plot3d(x^2+y^2+z^2 - 1, (x, -2, 2), (y, -2, 2), (z, -2, 2), plot_points=6) sage: G.triangulate() # indirect doctest sage: len(G.face_list()) 44 sage: len(G.vertex_list()) 132 sage: G = implicit_plot3d(x^2+y^2+z^2 - 100, (x, -2, 2), (y, -2, 2), (z, -2, 2), plot_points=6) sage: G.triangulate() # indirect doctest sage: len(G.face_list()) 0 sage: len(G.vertex_list()) 0 """
def _clean_point_list(self): """ Clean up the vertices and faces as follows:
- Remove all vertices with a coordinate which is NaN or infinity.
- If a removed vertex occurs in a face, remove it from that face, but keep other vertices in that face.
- Remove faces with less than 3 vertices.
- Remove unused vertices.
- Free unused memory for vertices and faces (not indices). """ cdef Py_ssize_t i, j, v
# point_map is an array old vertex index -> new vertex index. # The special value -1 means that the vertex is not mapped yet. # The special value -2 means that the vertex must be deleted # because a coordinate is NaN or infinity. # When we are done, all vertices with negative indices are not # used and will be removed.
# Process all faces cdef Py_ssize_t fv # number of new vertices on face
# Process vertices in face else:
# Skip faces with less than 3 vertices
# Store in newface
# Realloc face array
# Realloc and map vertex array # We cannot copy in-place since we permuted the vertices
def _separate_creases(self, threshold): """ Some rendering engines Gouraud shading, which is great for smooth surfaces but looks bad if one actually has a polyhedron.
INPUT:
``threshold`` -- the minimum cosine of the angle between adjacent faces a higher threshold separates more, all faces if >= 1, no faces if <= -1 """ cdef Py_ssize_t i, j, k cdef face_c *face # For each vertex, get number of faces # Running used as index into face list # Create an array, indexed by running_point_counts[v], to the list of faces containing that vertex. cdef face_c** point_faces except MemoryError: sig_free(point_counts) raise # Now, for each vertex, see if all faces are close enough, # or if it is a crease. cdef face_c** faces cdef bint any # We compare against face 0, and if it's not flat enough we push it to the end. # Then we come around again to compare everything that was put at the end, possibly # pushing stuff to the end again (until no further changes are needed). # Find creases # Reallocate room for vertices at end except MemoryError: sig_free(point_counts) sig_free(point_faces) self.vcount = self.fcount = self.icount = 0 # so we don't get segfaults on bad points sig_off() raise # We have a new vertex # Update the point_counts and point_faces arrays for the next time around.
def _mem_stats(self): return self.vcount, self.fcount, self.icount
def __dealloc__(self):
def is_enclosed(self): """ Whether or not it is necessary to render the back sides of the polygons.
One is assuming, of course, that they have the correct orientation.
This is may be passed in on construction. It is also calculated in :class:`sage.plot.plot3d.parametric_surface.ParametricSurface` by verifying the opposite edges of the rendered domain either line up or are pinched together.
EXAMPLES::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: IndexFaceSet([[(0,0,1),(0,1,0),(1,0,0)]]).is_enclosed() False """
def index_faces(self): """ Return the list over all faces of the indices of the vertices.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: S.index_faces() [[0, 1, 2, 3], [0, 4, 5, 1], [0, 3, 6, 4], [5, 4, 6, 7], [6, 3, 2, 7], [2, 1, 5, 7]] """ cdef Py_ssize_t i, j
def has_local_colors(self): """ Return ``True`` if and only if every face has an individual color.
EXAMPLES::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list=[Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: S.has_local_colors() True
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: S.has_local_colors() False """
def index_faces_with_colors(self): """ Return the list over all faces of (indices of the vertices, color).
This only works if every face has its own color.
.. SEEALSO::
:meth:`has_local_colors`
EXAMPLES:
A simple colored one::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list=[Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: S.index_faces_with_colors() [([0, 4, 5], '#ff0000'), ([3, 4, 5], '#ff9900'), ([2, 3, 4], '#cbff00'), ([1, 3, 5], '#33ff00')]
When the texture is global, an error is raised::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: S.index_faces_with_colors() Traceback (most recent call last): ... ValueError: the texture is global """ cdef Py_ssize_t i, j
def faces(self): """ An iterator over the faces.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: list(S.faces()) == S.face_list() True """
def face_list(self): """ Return the list of faces.
Every face is given as a tuple of vertices.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: S.face_list()[0] [(1.0, 2.0, 3.0), (-1.0, 2.0, 3.0), (-1.0, -2.0, 3.0), (1.0, -2.0, 3.0)] """ cdef Py_ssize_t i, j
def edges(self): """ An iterator over the edges.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: list(S.edges())[0] ((1.0, -2.0, 3.0), (1.0, 2.0, 3.0)) """
def edge_list(self): """ Return the list of edges.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: S.edge_list()[0] ((1.0, -2.0, 3.0), (1.0, 2.0, 3.0)) """
def vertices(self): """ An iterator over the vertices.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Cone(1,1) sage: list(S.vertices()) == S.vertex_list() True """
def vertex_list(self): """ Return the list of vertices.
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = polygon([(0,0,1), (1,1,1), (2,0,1)]) sage: S.vertex_list()[0] (0.0, 0.0, 1.0) """ cdef Py_ssize_t i
def x3d_geometry(self): """ Return the x3d data.
EXAMPLES:
A basic test with a triangle::
sage: G = polygon([(0,0,1), (1,1,1), (2,0,1)]) sage: print(G.x3d_geometry()) <BLANKLINE> <IndexedFaceSet coordIndex='0,1,2,-1'> <Coordinate point='0.0 0.0 1.0,1.0 1.0 1.0,2.0 0.0 1.0'/> </IndexedFaceSet> <BLANKLINE>
A simple colored one::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list=[Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: print(S.x3d_geometry()) <BLANKLINE> <IndexedFaceSet solid='False' colorPerVertex='False' coordIndex='0,4,5,-1,3,4,5,-1,2,3,4,-1,1,3,5,-1'> <Coordinate point='2.0 0.0 0.0,0.0 2.0 0.0,0.0 0.0 2.0,0.0 1.0 1.0,1.0 0.0 1.0,1.0 1.0 0.0'/> <Color color='1.0 0.0 0.0,1.0 0.6 0.0,0.8 1.0 0.0,0.2 1.0 0.0' /> </IndexedFaceSet> <BLANKLINE> """ cdef Py_ssize_t i <IndexedFaceSet solid='False' colorPerVertex='False' coordIndex='%s,-1'> <Coordinate point='%s'/> <Color color='%s' /> </IndexedFaceSet> <IndexedFaceSet coordIndex='%s,-1'> <Coordinate point='%s'/> </IndexedFaceSet>
def bounding_box(self): r""" Calculate the bounding box for the vertices in this object (ignoring infinite or NaN coordinates).
OUTPUT:
a tuple ( (low_x, low_y, low_z), (high_x, high_y, high_z)), which gives the coordinates of opposite corners of the bounding box.
EXAMPLES::
sage: x,y = var('x,y') sage: p = plot3d(sqrt(sin(x)*sin(y)), (x,0,2*pi),(y,0,2*pi)) sage: p.bounding_box() ((0.0, 0.0, -0.0), (6.283185307179586, 6.283185307179586, 0.9991889981715697)) """
cdef Py_ssize_t i cdef point_c low cdef point_c high
def partition(self, f): r""" Partition the faces of ``self``.
The partition is done according to the value of a map `f: \RR^3 \rightarrow \ZZ` applied to the center of each face.
INPUT:
- `f` -- a function from `\RR^3` to `\ZZ`
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: len(S.partition(lambda x,y,z : floor(x+y+z))) 6 """ cdef Py_ssize_t i, j, ix, face_ix cdef int part cdef point_c P cdef face_c *face cdef face_c *new_face cdef IndexFaceSet face_set
def tachyon_repr(self, render_params): """ Return a tachyon object for ``self``.
EXAMPLES:
A basic test with a triangle::
sage: G = polygon([(0,0,1), (1,1,1), (2,0,1)]) sage: s = G.tachyon_repr(G.default_render_params()); s ['TRI V0 0 0 1 V1 1 1 1 V2 2 0 1', ...]
A simple colored one::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list=[Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: S.tachyon_repr(S.default_render_params()) ['TRI V0 2 0 0 V1 1 0 1 V2 1 1 0', 'TEXTURE... AMBIENT 0.3 DIFFUSE 0.7 SPECULAR 0 OPACITY 1.0... COLOR 1 0 0 ... TEXFUNC 0',...] """ cdef point_c P, Q, R cdef face_c face cdef Py_ssize_t i, k else: else: else: else:
def json_repr(self, render_params): """ Return a json representation for ``self``.
TESTS:
A basic test with a triangle::
sage: G = polygon([(0,0,1), (1,1,1), (2,0,1)]) sage: G.json_repr(G.default_render_params()) ['{"vertices":[{"x":0,"y":0,"z":1},{"x":1,"y":1,"z":1},{"x":2,"y":0,"z":1}], "faces":[[0,1,2]], "color":"#0000ff", "opacity":1}']
A simple colored one::
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet sage: from sage.plot.plot3d.texture import Texture sage: point_list = [(2,0,0),(0,2,0),(0,0,2),(0,1,1),(1,0,1),(1,1,0)] sage: face_list = [[0,4,5],[3,4,5],[2,3,4],[1,3,5]] sage: col = rainbow(10, 'rgbtuple') sage: t_list=[Texture(col[i]) for i in range(10)] sage: S = IndexFaceSet(face_list, point_list, texture_list=t_list) sage: S.json_repr(S.default_render_params()) ['{"vertices":[{"x":2,"y":0,"z":0},..., "face_colors":["#ff0000","#ff9900","#cbff00","#33ff00"], "opacity":1}'] """ cdef point_c res
else:
else:
def obj_repr(self, render_params): """ Return an obj representation for ``self``.
TESTS::
sage: from sage.plot.plot3d.shapes import * sage: S = Cylinder(1,1) sage: s = S.obj_repr(S.default_render_params()) """ cdef Py_ssize_t i cdef point_c res
else:
else:
points, faces, back_faces]
def jmol_repr(self, render_params): """ Return a jmol representation for ``self``.
TESTS::
sage: from sage.plot.plot3d.shapes import * sage: S = Cylinder(1,1) sage: S.show(viewer='jmol') # indirect doctest """ cdef Py_ssize_t i cdef point_c res
else:
# activation of coloring in jmol else:
# If a face has more than 4 vertices, it gets chopped up in # format_pmesh_face
points, faces]
else: filename = "%s-%s.pmesh" % (render_params.output_file, name) f = open(filename, 'w') for line in all: f.write(line) f.write('\n') f.close()
else:
# Turn on display of the mesh lines or dots? s += '\npmesh %s dots\n' % name
def dual(self, **kwds): """ Return the dual.
EXAMPLES::
sage: S = cube() sage: T = S.dual() sage: len(T.vertex_list()) 6
""" cdef point_c P cdef face_c *face cdef Py_ssize_t i, j, ix, ff cdef int incoming, outgoing cdef dict dd
# is using dicts overly-heavy?
# Let the vertex be centered on the face according to a simple average
# Now compute the new face else: else:
continue
def stickers(self, colors, width, hover): """ Return a group of IndexFaceSets.
INPUT:
- ``colors`` -- list of colors/textures to use (in cyclic order)
- ``width`` -- offset perpendicular into the edge (to create a border) may also be negative
- ``hover`` -- offset normal to the face (usually have to float above the original surface so it shows, typically this value is very small compared to the actual object
OUTPUT:
Graphics3dGroup of stickers
EXAMPLES::
sage: from sage.plot.plot3d.shapes import Box sage: B = Box(.5,.4,.3, color='black') sage: S = B.stickers(['red','yellow','blue'], 0.1, 0.05) sage: S.show() sage: (S+B).show()
"""
def sticker(self, face_list, width, hover, **kwds): """ Return a sticker on the chosen faces. """ face_list = (face_list,)
cdef class FaceIter: """ A class for iteration over faces
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: len(list(S.faces())) == 6 # indirect doctest True """ def __init__(self, face_set): """ """
def __iter__(self):
def __next__(self): cdef point_c P else:
cdef class EdgeIter: """ A class for iteration over edges
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: len(list(S.edges())) == 12 # indirect doctest True """ def __init__(self, face_set): raise TypeError("Must be closed to use the simple iterator.")
def __iter__(self):
def __next__(self): cdef point_c P, Q else: else: else: if point_c_cmp(P, Q) > 0: P,Q = Q,P edge = ((P.x, P.y, P.z), (Q.x, Q.y, Q.z)) if not edge in self.seen: self.seen[edge] = edge return edge
cdef class VertexIter: """ A class for iteration over vertices
EXAMPLES::
sage: from sage.plot.plot3d.shapes import * sage: S = Box(1,2,3) sage: len(list(S.vertices())) == 8 # indirect doctest True """ def __init__(self, face_set):
def __iter__(self):
def __next__(self): else:
def len3d(v): """ Return the norm of a vector in three dimensions.
EXAMPLES::
sage: from sage.plot.plot3d.index_face_set import len3d sage: len3d((1,2,3)) 3.7416573867739413 """
def sticker(face, width, hover): """ Return a sticker over the given face. """ |