AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
"""Plotting module that can plot 2D and 3D functions
|
||||
"""
|
||||
|
||||
from sympy.utilities.decorator import doctest_depends_on
|
||||
|
||||
@doctest_depends_on(modules=('pyglet',))
|
||||
def PygletPlot(*args, **kwargs):
|
||||
"""
|
||||
|
||||
Plot Examples
|
||||
=============
|
||||
|
||||
See examples/advanced/pyglet_plotting.py for many more examples.
|
||||
|
||||
>>> from sympy.plotting.pygletplot import PygletPlot as Plot
|
||||
>>> from sympy.abc import x, y, z
|
||||
|
||||
>>> Plot(x*y**3-y*x**3)
|
||||
[0]: -x**3*y + x*y**3, 'mode=cartesian'
|
||||
|
||||
>>> p = Plot()
|
||||
>>> p[1] = x*y
|
||||
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
||||
|
||||
>>> p = Plot()
|
||||
>>> p[1] = x**2+y**2
|
||||
>>> p[2] = -x**2-y**2
|
||||
|
||||
|
||||
Variable Intervals
|
||||
==================
|
||||
|
||||
The basic format is [var, min, max, steps], but the
|
||||
syntax is flexible and arguments left out are taken
|
||||
from the defaults for the current coordinate mode:
|
||||
|
||||
>>> Plot(x**2) # implies [x,-5,5,100]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
|
||||
>>> Plot(x**2, [], []) # [x,-1,1,40], [y,-1,1,40]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2-y**2, [100], [100]) # [x,-1,1,100], [y,-1,1,100]
|
||||
[0]: x**2 - y**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [x,-13,13,100])
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [-13,13]) # [x,-13,13,100]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [x,-13,13]) # [x,-13,13,100]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(1*x, [], [x], mode='cylindrical')
|
||||
... # [unbound_theta,0,2*Pi,40], [x,-1,1,20]
|
||||
[0]: x, 'mode=cartesian'
|
||||
|
||||
|
||||
Coordinate Modes
|
||||
================
|
||||
|
||||
Plot supports several curvilinear coordinate modes, and
|
||||
they independent for each plotted function. You can specify
|
||||
a coordinate mode explicitly with the 'mode' named argument,
|
||||
but it can be automatically determined for Cartesian or
|
||||
parametric plots, and therefore must only be specified for
|
||||
polar, cylindrical, and spherical modes.
|
||||
|
||||
Specifically, Plot(function arguments) and Plot[n] =
|
||||
(function arguments) will interpret your arguments as a
|
||||
Cartesian plot if you provide one function and a parametric
|
||||
plot if you provide two or three functions. Similarly, the
|
||||
arguments will be interpreted as a curve if one variable is
|
||||
used, and a surface if two are used.
|
||||
|
||||
Supported mode names by number of variables:
|
||||
|
||||
1: parametric, cartesian, polar
|
||||
2: parametric, cartesian, cylindrical = polar, spherical
|
||||
|
||||
>>> Plot(1, mode='spherical')
|
||||
|
||||
|
||||
Calculator-like Interface
|
||||
=========================
|
||||
|
||||
>>> p = Plot(visible=False)
|
||||
>>> f = x**2
|
||||
>>> p[1] = f
|
||||
>>> p[2] = f.diff(x)
|
||||
>>> p[3] = f.diff(x).diff(x)
|
||||
>>> p
|
||||
[1]: x**2, 'mode=cartesian'
|
||||
[2]: 2*x, 'mode=cartesian'
|
||||
[3]: 2, 'mode=cartesian'
|
||||
>>> p.show()
|
||||
>>> p.clear()
|
||||
>>> p
|
||||
<blank plot>
|
||||
>>> p[1] = x**2+y**2
|
||||
>>> p[1].style = 'solid'
|
||||
>>> p[2] = -x**2-y**2
|
||||
>>> p[2].style = 'wireframe'
|
||||
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
||||
>>> p[1].style = 'both'
|
||||
>>> p[2].style = 'both'
|
||||
>>> p.close()
|
||||
|
||||
|
||||
Plot Window Keyboard Controls
|
||||
=============================
|
||||
|
||||
Screen Rotation:
|
||||
X,Y axis Arrow Keys, A,S,D,W, Numpad 4,6,8,2
|
||||
Z axis Q,E, Numpad 7,9
|
||||
|
||||
Model Rotation:
|
||||
Z axis Z,C, Numpad 1,3
|
||||
|
||||
Zoom: R,F, PgUp,PgDn, Numpad +,-
|
||||
|
||||
Reset Camera: X, Numpad 5
|
||||
|
||||
Camera Presets:
|
||||
XY F1
|
||||
XZ F2
|
||||
YZ F3
|
||||
Perspective F4
|
||||
|
||||
Sensitivity Modifier: SHIFT
|
||||
|
||||
Axes Toggle:
|
||||
Visible F5
|
||||
Colors F6
|
||||
|
||||
Close Window: ESCAPE
|
||||
|
||||
=============================
|
||||
"""
|
||||
|
||||
from sympy.plotting.pygletplot.plot import PygletPlot
|
||||
return PygletPlot(*args, **kwargs)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,336 @@
|
||||
from sympy.core.basic import Basic
|
||||
from sympy.core.symbol import (Symbol, symbols)
|
||||
from sympy.utilities.lambdify import lambdify
|
||||
from .util import interpolate, rinterpolate, create_bounds, update_bounds
|
||||
from sympy.utilities.iterables import sift
|
||||
|
||||
|
||||
class ColorGradient:
|
||||
colors = [0.4, 0.4, 0.4], [0.9, 0.9, 0.9]
|
||||
intervals = 0.0, 1.0
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 2:
|
||||
self.colors = list(args)
|
||||
self.intervals = [0.0, 1.0]
|
||||
elif len(args) > 0:
|
||||
if len(args) % 2 != 0:
|
||||
raise ValueError("len(args) should be even")
|
||||
self.colors = [args[i] for i in range(1, len(args), 2)]
|
||||
self.intervals = [args[i] for i in range(0, len(args), 2)]
|
||||
assert len(self.colors) == len(self.intervals)
|
||||
|
||||
def copy(self):
|
||||
c = ColorGradient()
|
||||
c.colors = [e[::] for e in self.colors]
|
||||
c.intervals = self.intervals[::]
|
||||
return c
|
||||
|
||||
def _find_interval(self, v):
|
||||
m = len(self.intervals)
|
||||
i = 0
|
||||
while i < m - 1 and self.intervals[i] <= v:
|
||||
i += 1
|
||||
return i
|
||||
|
||||
def _interpolate_axis(self, axis, v):
|
||||
i = self._find_interval(v)
|
||||
v = rinterpolate(self.intervals[i - 1], self.intervals[i], v)
|
||||
return interpolate(self.colors[i - 1][axis], self.colors[i][axis], v)
|
||||
|
||||
def __call__(self, r, g, b):
|
||||
c = self._interpolate_axis
|
||||
return c(0, r), c(1, g), c(2, b)
|
||||
|
||||
default_color_schemes = {} # defined at the bottom of this file
|
||||
|
||||
|
||||
class ColorScheme:
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.f, self.gradient = None, ColorGradient()
|
||||
|
||||
if len(args) == 1 and not isinstance(args[0], Basic) and callable(args[0]):
|
||||
self.f = args[0]
|
||||
elif len(args) == 1 and isinstance(args[0], str):
|
||||
if args[0] in default_color_schemes:
|
||||
cs = default_color_schemes[args[0]]
|
||||
self.f, self.gradient = cs.f, cs.gradient.copy()
|
||||
else:
|
||||
self.f = lambdify('x,y,z,u,v', args[0])
|
||||
else:
|
||||
self.f, self.gradient = self._interpret_args(args)
|
||||
self._test_color_function()
|
||||
if not isinstance(self.gradient, ColorGradient):
|
||||
raise ValueError("Color gradient not properly initialized. "
|
||||
"(Not a ColorGradient instance.)")
|
||||
|
||||
def _interpret_args(self, args):
|
||||
f, gradient = None, self.gradient
|
||||
atoms, lists = self._sort_args(args)
|
||||
s = self._pop_symbol_list(lists)
|
||||
s = self._fill_in_vars(s)
|
||||
|
||||
# prepare the error message for lambdification failure
|
||||
f_str = ', '.join(str(fa) for fa in atoms)
|
||||
s_str = (str(sa) for sa in s)
|
||||
s_str = ', '.join(sa for sa in s_str if sa.find('unbound') < 0)
|
||||
f_error = ValueError("Could not interpret arguments "
|
||||
"%s as functions of %s." % (f_str, s_str))
|
||||
|
||||
# try to lambdify args
|
||||
if len(atoms) == 1:
|
||||
fv = atoms[0]
|
||||
try:
|
||||
f = lambdify(s, [fv, fv, fv])
|
||||
except TypeError:
|
||||
raise f_error
|
||||
|
||||
elif len(atoms) == 3:
|
||||
fr, fg, fb = atoms
|
||||
try:
|
||||
f = lambdify(s, [fr, fg, fb])
|
||||
except TypeError:
|
||||
raise f_error
|
||||
|
||||
else:
|
||||
raise ValueError("A ColorScheme must provide 1 or 3 "
|
||||
"functions in x, y, z, u, and/or v.")
|
||||
|
||||
# try to intrepret any given color information
|
||||
if len(lists) == 0:
|
||||
gargs = []
|
||||
|
||||
elif len(lists) == 1:
|
||||
gargs = lists[0]
|
||||
|
||||
elif len(lists) == 2:
|
||||
try:
|
||||
(r1, g1, b1), (r2, g2, b2) = lists
|
||||
except TypeError:
|
||||
raise ValueError("If two color arguments are given, "
|
||||
"they must be given in the format "
|
||||
"(r1, g1, b1), (r2, g2, b2).")
|
||||
gargs = lists
|
||||
|
||||
elif len(lists) == 3:
|
||||
try:
|
||||
(r1, r2), (g1, g2), (b1, b2) = lists
|
||||
except Exception:
|
||||
raise ValueError("If three color arguments are given, "
|
||||
"they must be given in the format "
|
||||
"(r1, r2), (g1, g2), (b1, b2). To create "
|
||||
"a multi-step gradient, use the syntax "
|
||||
"[0, colorStart, step1, color1, ..., 1, "
|
||||
"colorEnd].")
|
||||
gargs = [[r1, g1, b1], [r2, g2, b2]]
|
||||
|
||||
else:
|
||||
raise ValueError("Don't know what to do with collection "
|
||||
"arguments %s." % (', '.join(str(l) for l in lists)))
|
||||
|
||||
if gargs:
|
||||
try:
|
||||
gradient = ColorGradient(*gargs)
|
||||
except Exception as ex:
|
||||
raise ValueError(("Could not initialize a gradient "
|
||||
"with arguments %s. Inner "
|
||||
"exception: %s") % (gargs, str(ex)))
|
||||
|
||||
return f, gradient
|
||||
|
||||
def _pop_symbol_list(self, lists):
|
||||
symbol_lists = []
|
||||
for l in lists:
|
||||
mark = True
|
||||
for s in l:
|
||||
if s is not None and not isinstance(s, Symbol):
|
||||
mark = False
|
||||
break
|
||||
if mark:
|
||||
lists.remove(l)
|
||||
symbol_lists.append(l)
|
||||
if len(symbol_lists) == 1:
|
||||
return symbol_lists[0]
|
||||
elif len(symbol_lists) == 0:
|
||||
return []
|
||||
else:
|
||||
raise ValueError("Only one list of Symbols "
|
||||
"can be given for a color scheme.")
|
||||
|
||||
def _fill_in_vars(self, args):
|
||||
defaults = symbols('x,y,z,u,v')
|
||||
v_error = ValueError("Could not find what to plot.")
|
||||
if len(args) == 0:
|
||||
return defaults
|
||||
if not isinstance(args, (tuple, list)):
|
||||
raise v_error
|
||||
if len(args) == 0:
|
||||
return defaults
|
||||
for s in args:
|
||||
if s is not None and not isinstance(s, Symbol):
|
||||
raise v_error
|
||||
# when vars are given explicitly, any vars
|
||||
# not given are marked 'unbound' as to not
|
||||
# be accidentally used in an expression
|
||||
vars = [Symbol('unbound%i' % (i)) for i in range(1, 6)]
|
||||
# interpret as t
|
||||
if len(args) == 1:
|
||||
vars[3] = args[0]
|
||||
# interpret as u,v
|
||||
elif len(args) == 2:
|
||||
if args[0] is not None:
|
||||
vars[3] = args[0]
|
||||
if args[1] is not None:
|
||||
vars[4] = args[1]
|
||||
# interpret as x,y,z
|
||||
elif len(args) >= 3:
|
||||
# allow some of x,y,z to be
|
||||
# left unbound if not given
|
||||
if args[0] is not None:
|
||||
vars[0] = args[0]
|
||||
if args[1] is not None:
|
||||
vars[1] = args[1]
|
||||
if args[2] is not None:
|
||||
vars[2] = args[2]
|
||||
# interpret the rest as t
|
||||
if len(args) >= 4:
|
||||
vars[3] = args[3]
|
||||
# ...or u,v
|
||||
if len(args) >= 5:
|
||||
vars[4] = args[4]
|
||||
return vars
|
||||
|
||||
def _sort_args(self, args):
|
||||
lists, atoms = sift(args,
|
||||
lambda a: isinstance(a, (tuple, list)), binary=True)
|
||||
return atoms, lists
|
||||
|
||||
def _test_color_function(self):
|
||||
if not callable(self.f):
|
||||
raise ValueError("Color function is not callable.")
|
||||
try:
|
||||
result = self.f(0, 0, 0, 0, 0)
|
||||
if len(result) != 3:
|
||||
raise ValueError("length should be equal to 3")
|
||||
except TypeError:
|
||||
raise ValueError("Color function needs to accept x,y,z,u,v, "
|
||||
"as arguments even if it doesn't use all of them.")
|
||||
except AssertionError:
|
||||
raise ValueError("Color function needs to return 3-tuple r,g,b.")
|
||||
except Exception:
|
||||
pass # color function probably not valid at 0,0,0,0,0
|
||||
|
||||
def __call__(self, x, y, z, u, v):
|
||||
try:
|
||||
return self.f(x, y, z, u, v)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def apply_to_curve(self, verts, u_set, set_len=None, inc_pos=None):
|
||||
"""
|
||||
Apply this color scheme to a
|
||||
set of vertices over a single
|
||||
independent variable u.
|
||||
"""
|
||||
bounds = create_bounds()
|
||||
cverts = []
|
||||
if callable(set_len):
|
||||
set_len(len(u_set)*2)
|
||||
# calculate f() = r,g,b for each vert
|
||||
# and find the min and max for r,g,b
|
||||
for _u in range(len(u_set)):
|
||||
if verts[_u] is None:
|
||||
cverts.append(None)
|
||||
else:
|
||||
x, y, z = verts[_u]
|
||||
u, v = u_set[_u], None
|
||||
c = self(x, y, z, u, v)
|
||||
if c is not None:
|
||||
c = list(c)
|
||||
update_bounds(bounds, c)
|
||||
cverts.append(c)
|
||||
if callable(inc_pos):
|
||||
inc_pos()
|
||||
# scale and apply gradient
|
||||
for _u in range(len(u_set)):
|
||||
if cverts[_u] is not None:
|
||||
for _c in range(3):
|
||||
# scale from [f_min, f_max] to [0,1]
|
||||
cverts[_u][_c] = rinterpolate(bounds[_c][0], bounds[_c][1],
|
||||
cverts[_u][_c])
|
||||
# apply gradient
|
||||
cverts[_u] = self.gradient(*cverts[_u])
|
||||
if callable(inc_pos):
|
||||
inc_pos()
|
||||
return cverts
|
||||
|
||||
def apply_to_surface(self, verts, u_set, v_set, set_len=None, inc_pos=None):
|
||||
"""
|
||||
Apply this color scheme to a
|
||||
set of vertices over two
|
||||
independent variables u and v.
|
||||
"""
|
||||
bounds = create_bounds()
|
||||
cverts = []
|
||||
if callable(set_len):
|
||||
set_len(len(u_set)*len(v_set)*2)
|
||||
# calculate f() = r,g,b for each vert
|
||||
# and find the min and max for r,g,b
|
||||
for _u in range(len(u_set)):
|
||||
column = []
|
||||
for _v in range(len(v_set)):
|
||||
if verts[_u][_v] is None:
|
||||
column.append(None)
|
||||
else:
|
||||
x, y, z = verts[_u][_v]
|
||||
u, v = u_set[_u], v_set[_v]
|
||||
c = self(x, y, z, u, v)
|
||||
if c is not None:
|
||||
c = list(c)
|
||||
update_bounds(bounds, c)
|
||||
column.append(c)
|
||||
if callable(inc_pos):
|
||||
inc_pos()
|
||||
cverts.append(column)
|
||||
# scale and apply gradient
|
||||
for _u in range(len(u_set)):
|
||||
for _v in range(len(v_set)):
|
||||
if cverts[_u][_v] is not None:
|
||||
# scale from [f_min, f_max] to [0,1]
|
||||
for _c in range(3):
|
||||
cverts[_u][_v][_c] = rinterpolate(bounds[_c][0],
|
||||
bounds[_c][1], cverts[_u][_v][_c])
|
||||
# apply gradient
|
||||
cverts[_u][_v] = self.gradient(*cverts[_u][_v])
|
||||
if callable(inc_pos):
|
||||
inc_pos()
|
||||
return cverts
|
||||
|
||||
def str_base(self):
|
||||
return ", ".join(str(a) for a in self.args)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s" % (self.str_base())
|
||||
|
||||
|
||||
x, y, z, t, u, v = symbols('x,y,z,t,u,v')
|
||||
|
||||
default_color_schemes['rainbow'] = ColorScheme(z, y, x)
|
||||
default_color_schemes['zfade'] = ColorScheme(z, (0.4, 0.4, 0.97),
|
||||
(0.97, 0.4, 0.4), (None, None, z))
|
||||
default_color_schemes['zfade3'] = ColorScheme(z, (None, None, z),
|
||||
[0.00, (0.2, 0.2, 1.0),
|
||||
0.35, (0.2, 0.8, 0.4),
|
||||
0.50, (0.3, 0.9, 0.3),
|
||||
0.65, (0.4, 0.8, 0.2),
|
||||
1.00, (1.0, 0.2, 0.2)])
|
||||
|
||||
default_color_schemes['zfade4'] = ColorScheme(z, (None, None, z),
|
||||
[0.0, (0.3, 0.3, 1.0),
|
||||
0.30, (0.3, 1.0, 0.3),
|
||||
0.55, (0.95, 1.0, 0.2),
|
||||
0.65, (1.0, 0.95, 0.2),
|
||||
0.85, (1.0, 0.7, 0.2),
|
||||
1.0, (1.0, 0.3, 0.2)])
|
||||
@@ -0,0 +1,106 @@
|
||||
from pyglet.window import Window
|
||||
from pyglet.clock import Clock
|
||||
|
||||
from threading import Thread, Lock
|
||||
|
||||
gl_lock = Lock()
|
||||
|
||||
|
||||
class ManagedWindow(Window):
|
||||
"""
|
||||
A pyglet window with an event loop which executes automatically
|
||||
in a separate thread. Behavior is added by creating a subclass
|
||||
which overrides setup, update, and/or draw.
|
||||
"""
|
||||
fps_limit = 30
|
||||
default_win_args = {"width": 600,
|
||||
"height": 500,
|
||||
"vsync": False,
|
||||
"resizable": True}
|
||||
|
||||
def __init__(self, **win_args):
|
||||
"""
|
||||
It is best not to override this function in the child
|
||||
class, unless you need to take additional arguments.
|
||||
Do any OpenGL initialization calls in setup().
|
||||
"""
|
||||
|
||||
# check if this is run from the doctester
|
||||
if win_args.get('runfromdoctester', False):
|
||||
return
|
||||
|
||||
self.win_args = dict(self.default_win_args, **win_args)
|
||||
self.Thread = Thread(target=self.__event_loop__)
|
||||
self.Thread.start()
|
||||
|
||||
def __event_loop__(self, **win_args):
|
||||
"""
|
||||
The event loop thread function. Do not override or call
|
||||
directly (it is called by __init__).
|
||||
"""
|
||||
gl_lock.acquire()
|
||||
try:
|
||||
try:
|
||||
super().__init__(**self.win_args)
|
||||
self.switch_to()
|
||||
self.setup()
|
||||
except Exception as e:
|
||||
print("Window initialization failed: %s" % (str(e)))
|
||||
self.has_exit = True
|
||||
finally:
|
||||
gl_lock.release()
|
||||
|
||||
clock = Clock()
|
||||
clock.fps_limit = self.fps_limit
|
||||
while not self.has_exit:
|
||||
dt = clock.tick()
|
||||
gl_lock.acquire()
|
||||
try:
|
||||
try:
|
||||
self.switch_to()
|
||||
self.dispatch_events()
|
||||
self.clear()
|
||||
self.update(dt)
|
||||
self.draw()
|
||||
self.flip()
|
||||
except Exception as e:
|
||||
print("Uncaught exception in event loop: %s" % str(e))
|
||||
self.has_exit = True
|
||||
finally:
|
||||
gl_lock.release()
|
||||
super().close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the window.
|
||||
"""
|
||||
self.has_exit = True
|
||||
|
||||
def setup(self):
|
||||
"""
|
||||
Called once before the event loop begins.
|
||||
Override this method in a child class. This
|
||||
is the best place to put things like OpenGL
|
||||
initialization calls.
|
||||
"""
|
||||
pass
|
||||
|
||||
def update(self, dt):
|
||||
"""
|
||||
Called before draw during each iteration of
|
||||
the event loop. dt is the elapsed time in
|
||||
seconds since the last update. OpenGL rendering
|
||||
calls are best put in draw() rather than here.
|
||||
"""
|
||||
pass
|
||||
|
||||
def draw(self):
|
||||
"""
|
||||
Called after update during each iteration of
|
||||
the event loop. Put OpenGL rendering calls
|
||||
here.
|
||||
"""
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
ManagedWindow()
|
||||
@@ -0,0 +1,464 @@
|
||||
from threading import RLock
|
||||
|
||||
# it is sufficient to import "pyglet" here once
|
||||
try:
|
||||
import pyglet.gl as pgl
|
||||
except ImportError:
|
||||
raise ImportError("pyglet is required for plotting.\n "
|
||||
"visit https://pyglet.org/")
|
||||
|
||||
from sympy.core.numbers import Integer
|
||||
from sympy.external.gmpy import SYMPY_INTS
|
||||
from sympy.geometry.entity import GeometryEntity
|
||||
from sympy.plotting.pygletplot.plot_axes import PlotAxes
|
||||
from sympy.plotting.pygletplot.plot_mode import PlotMode
|
||||
from sympy.plotting.pygletplot.plot_object import PlotObject
|
||||
from sympy.plotting.pygletplot.plot_window import PlotWindow
|
||||
from sympy.plotting.pygletplot.util import parse_option_string
|
||||
from sympy.utilities.decorator import doctest_depends_on
|
||||
from sympy.utilities.iterables import is_sequence
|
||||
|
||||
from time import sleep
|
||||
from os import getcwd, listdir
|
||||
|
||||
import ctypes
|
||||
|
||||
@doctest_depends_on(modules=('pyglet',))
|
||||
class PygletPlot:
|
||||
"""
|
||||
Plot Examples
|
||||
=============
|
||||
|
||||
See examples/advanced/pyglet_plotting.py for many more examples.
|
||||
|
||||
>>> from sympy.plotting.pygletplot import PygletPlot as Plot
|
||||
>>> from sympy.abc import x, y, z
|
||||
|
||||
>>> Plot(x*y**3-y*x**3)
|
||||
[0]: -x**3*y + x*y**3, 'mode=cartesian'
|
||||
|
||||
>>> p = Plot()
|
||||
>>> p[1] = x*y
|
||||
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
||||
|
||||
>>> p = Plot()
|
||||
>>> p[1] = x**2+y**2
|
||||
>>> p[2] = -x**2-y**2
|
||||
|
||||
|
||||
Variable Intervals
|
||||
==================
|
||||
|
||||
The basic format is [var, min, max, steps], but the
|
||||
syntax is flexible and arguments left out are taken
|
||||
from the defaults for the current coordinate mode:
|
||||
|
||||
>>> Plot(x**2) # implies [x,-5,5,100]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [], []) # [x,-1,1,40], [y,-1,1,40]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2-y**2, [100], [100]) # [x,-1,1,100], [y,-1,1,100]
|
||||
[0]: x**2 - y**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [x,-13,13,100])
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [-13,13]) # [x,-13,13,100]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(x**2, [x,-13,13]) # [x,-13,13,10]
|
||||
[0]: x**2, 'mode=cartesian'
|
||||
>>> Plot(1*x, [], [x], mode='cylindrical')
|
||||
... # [unbound_theta,0,2*Pi,40], [x,-1,1,20]
|
||||
[0]: x, 'mode=cartesian'
|
||||
|
||||
|
||||
Coordinate Modes
|
||||
================
|
||||
|
||||
Plot supports several curvilinear coordinate modes, and
|
||||
they independent for each plotted function. You can specify
|
||||
a coordinate mode explicitly with the 'mode' named argument,
|
||||
but it can be automatically determined for Cartesian or
|
||||
parametric plots, and therefore must only be specified for
|
||||
polar, cylindrical, and spherical modes.
|
||||
|
||||
Specifically, Plot(function arguments) and Plot[n] =
|
||||
(function arguments) will interpret your arguments as a
|
||||
Cartesian plot if you provide one function and a parametric
|
||||
plot if you provide two or three functions. Similarly, the
|
||||
arguments will be interpreted as a curve if one variable is
|
||||
used, and a surface if two are used.
|
||||
|
||||
Supported mode names by number of variables:
|
||||
|
||||
1: parametric, cartesian, polar
|
||||
2: parametric, cartesian, cylindrical = polar, spherical
|
||||
|
||||
>>> Plot(1, mode='spherical')
|
||||
|
||||
|
||||
Calculator-like Interface
|
||||
=========================
|
||||
|
||||
>>> p = Plot(visible=False)
|
||||
>>> f = x**2
|
||||
>>> p[1] = f
|
||||
>>> p[2] = f.diff(x)
|
||||
>>> p[3] = f.diff(x).diff(x)
|
||||
>>> p
|
||||
[1]: x**2, 'mode=cartesian'
|
||||
[2]: 2*x, 'mode=cartesian'
|
||||
[3]: 2, 'mode=cartesian'
|
||||
>>> p.show()
|
||||
>>> p.clear()
|
||||
>>> p
|
||||
<blank plot>
|
||||
>>> p[1] = x**2+y**2
|
||||
>>> p[1].style = 'solid'
|
||||
>>> p[2] = -x**2-y**2
|
||||
>>> p[2].style = 'wireframe'
|
||||
>>> p[1].color = z, (0.4,0.4,0.9), (0.9,0.4,0.4)
|
||||
>>> p[1].style = 'both'
|
||||
>>> p[2].style = 'both'
|
||||
>>> p.close()
|
||||
|
||||
|
||||
Plot Window Keyboard Controls
|
||||
=============================
|
||||
|
||||
Screen Rotation:
|
||||
X,Y axis Arrow Keys, A,S,D,W, Numpad 4,6,8,2
|
||||
Z axis Q,E, Numpad 7,9
|
||||
|
||||
Model Rotation:
|
||||
Z axis Z,C, Numpad 1,3
|
||||
|
||||
Zoom: R,F, PgUp,PgDn, Numpad +,-
|
||||
|
||||
Reset Camera: X, Numpad 5
|
||||
|
||||
Camera Presets:
|
||||
XY F1
|
||||
XZ F2
|
||||
YZ F3
|
||||
Perspective F4
|
||||
|
||||
Sensitivity Modifier: SHIFT
|
||||
|
||||
Axes Toggle:
|
||||
Visible F5
|
||||
Colors F6
|
||||
|
||||
Close Window: ESCAPE
|
||||
|
||||
=============================
|
||||
|
||||
"""
|
||||
|
||||
@doctest_depends_on(modules=('pyglet',))
|
||||
def __init__(self, *fargs, **win_args):
|
||||
"""
|
||||
Positional Arguments
|
||||
====================
|
||||
|
||||
Any given positional arguments are used to
|
||||
initialize a plot function at index 1. In
|
||||
other words...
|
||||
|
||||
>>> from sympy.plotting.pygletplot import PygletPlot as Plot
|
||||
>>> from sympy.abc import x
|
||||
>>> p = Plot(x**2, visible=False)
|
||||
|
||||
...is equivalent to...
|
||||
|
||||
>>> p = Plot(visible=False)
|
||||
>>> p[1] = x**2
|
||||
|
||||
Note that in earlier versions of the plotting
|
||||
module, you were able to specify multiple
|
||||
functions in the initializer. This functionality
|
||||
has been dropped in favor of better automatic
|
||||
plot plot_mode detection.
|
||||
|
||||
|
||||
Named Arguments
|
||||
===============
|
||||
|
||||
axes
|
||||
An option string of the form
|
||||
"key1=value1; key2 = value2" which
|
||||
can use the following options:
|
||||
|
||||
style = ordinate
|
||||
none OR frame OR box OR ordinate
|
||||
|
||||
stride = 0.25
|
||||
val OR (val_x, val_y, val_z)
|
||||
|
||||
overlay = True (draw on top of plot)
|
||||
True OR False
|
||||
|
||||
colored = False (False uses Black,
|
||||
True uses colors
|
||||
R,G,B = X,Y,Z)
|
||||
True OR False
|
||||
|
||||
label_axes = False (display axis names
|
||||
at endpoints)
|
||||
True OR False
|
||||
|
||||
visible = True (show immediately
|
||||
True OR False
|
||||
|
||||
|
||||
The following named arguments are passed as
|
||||
arguments to window initialization:
|
||||
|
||||
antialiasing = True
|
||||
True OR False
|
||||
|
||||
ortho = False
|
||||
True OR False
|
||||
|
||||
invert_mouse_zoom = False
|
||||
True OR False
|
||||
|
||||
"""
|
||||
# Register the plot modes
|
||||
from . import plot_modes # noqa
|
||||
|
||||
self._win_args = win_args
|
||||
self._window = None
|
||||
|
||||
self._render_lock = RLock()
|
||||
|
||||
self._functions = {}
|
||||
self._pobjects = []
|
||||
self._screenshot = ScreenShot(self)
|
||||
|
||||
axe_options = parse_option_string(win_args.pop('axes', ''))
|
||||
self.axes = PlotAxes(**axe_options)
|
||||
self._pobjects.append(self.axes)
|
||||
|
||||
self[0] = fargs
|
||||
if win_args.get('visible', True):
|
||||
self.show()
|
||||
|
||||
## Window Interfaces
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Creates and displays a plot window, or activates it
|
||||
(gives it focus) if it has already been created.
|
||||
"""
|
||||
if self._window and not self._window.has_exit:
|
||||
self._window.activate()
|
||||
else:
|
||||
self._win_args['visible'] = True
|
||||
self.axes.reset_resources()
|
||||
|
||||
#if hasattr(self, '_doctest_depends_on'):
|
||||
# self._win_args['runfromdoctester'] = True
|
||||
|
||||
self._window = PlotWindow(self, **self._win_args)
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Closes the plot window.
|
||||
"""
|
||||
if self._window:
|
||||
self._window.close()
|
||||
|
||||
def saveimage(self, outfile=None, format='', size=(600, 500)):
|
||||
"""
|
||||
Saves a screen capture of the plot window to an
|
||||
image file.
|
||||
|
||||
If outfile is given, it can either be a path
|
||||
or a file object. Otherwise a png image will
|
||||
be saved to the current working directory.
|
||||
If the format is omitted, it is determined from
|
||||
the filename extension.
|
||||
"""
|
||||
self._screenshot.save(outfile, format, size)
|
||||
|
||||
## Function List Interfaces
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clears the function list of this plot.
|
||||
"""
|
||||
self._render_lock.acquire()
|
||||
self._functions = {}
|
||||
self.adjust_all_bounds()
|
||||
self._render_lock.release()
|
||||
|
||||
def __getitem__(self, i):
|
||||
"""
|
||||
Returns the function at position i in the
|
||||
function list.
|
||||
"""
|
||||
return self._functions[i]
|
||||
|
||||
def __setitem__(self, i, args):
|
||||
"""
|
||||
Parses and adds a PlotMode to the function
|
||||
list.
|
||||
"""
|
||||
if not (isinstance(i, (SYMPY_INTS, Integer)) and i >= 0):
|
||||
raise ValueError("Function index must "
|
||||
"be an integer >= 0.")
|
||||
|
||||
if isinstance(args, PlotObject):
|
||||
f = args
|
||||
else:
|
||||
if (not is_sequence(args)) or isinstance(args, GeometryEntity):
|
||||
args = [args]
|
||||
if len(args) == 0:
|
||||
return # no arguments given
|
||||
kwargs = {"bounds_callback": self.adjust_all_bounds}
|
||||
f = PlotMode(*args, **kwargs)
|
||||
|
||||
if f:
|
||||
self._render_lock.acquire()
|
||||
self._functions[i] = f
|
||||
self._render_lock.release()
|
||||
else:
|
||||
raise ValueError("Failed to parse '%s'."
|
||||
% ', '.join(str(a) for a in args))
|
||||
|
||||
def __delitem__(self, i):
|
||||
"""
|
||||
Removes the function in the function list at
|
||||
position i.
|
||||
"""
|
||||
self._render_lock.acquire()
|
||||
del self._functions[i]
|
||||
self.adjust_all_bounds()
|
||||
self._render_lock.release()
|
||||
|
||||
def firstavailableindex(self):
|
||||
"""
|
||||
Returns the first unused index in the function list.
|
||||
"""
|
||||
i = 0
|
||||
self._render_lock.acquire()
|
||||
while i in self._functions:
|
||||
i += 1
|
||||
self._render_lock.release()
|
||||
return i
|
||||
|
||||
def append(self, *args):
|
||||
"""
|
||||
Parses and adds a PlotMode to the function
|
||||
list at the first available index.
|
||||
"""
|
||||
self.__setitem__(self.firstavailableindex(), args)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Returns the number of functions in the function list.
|
||||
"""
|
||||
return len(self._functions)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Allows iteration of the function list.
|
||||
"""
|
||||
return self._functions.itervalues()
|
||||
|
||||
def __repr__(self):
|
||||
return str(self)
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
Returns a string containing a new-line separated
|
||||
list of the functions in the function list.
|
||||
"""
|
||||
s = ""
|
||||
if len(self._functions) == 0:
|
||||
s += "<blank plot>"
|
||||
else:
|
||||
self._render_lock.acquire()
|
||||
s += "\n".join(["%s[%i]: %s" % ("", i, str(self._functions[i]))
|
||||
for i in self._functions])
|
||||
self._render_lock.release()
|
||||
return s
|
||||
|
||||
def adjust_all_bounds(self):
|
||||
self._render_lock.acquire()
|
||||
self.axes.reset_bounding_box()
|
||||
for f in self._functions:
|
||||
self.axes.adjust_bounds(self._functions[f].bounds)
|
||||
self._render_lock.release()
|
||||
|
||||
def wait_for_calculations(self):
|
||||
sleep(0)
|
||||
self._render_lock.acquire()
|
||||
for f in self._functions:
|
||||
a = self._functions[f]._get_calculating_verts
|
||||
b = self._functions[f]._get_calculating_cverts
|
||||
while a() or b():
|
||||
sleep(0)
|
||||
self._render_lock.release()
|
||||
|
||||
class ScreenShot:
|
||||
def __init__(self, plot):
|
||||
self._plot = plot
|
||||
self.screenshot_requested = False
|
||||
self.outfile = None
|
||||
self.format = ''
|
||||
self.invisibleMode = False
|
||||
self.flag = 0
|
||||
|
||||
def __bool__(self):
|
||||
return self.screenshot_requested
|
||||
|
||||
def _execute_saving(self):
|
||||
if self.flag < 3:
|
||||
self.flag += 1
|
||||
return
|
||||
|
||||
size_x, size_y = self._plot._window.get_size()
|
||||
size = size_x*size_y*4*ctypes.sizeof(ctypes.c_ubyte)
|
||||
image = ctypes.create_string_buffer(size)
|
||||
pgl.glReadPixels(0, 0, size_x, size_y, pgl.GL_RGBA, pgl.GL_UNSIGNED_BYTE, image)
|
||||
from PIL import Image
|
||||
im = Image.frombuffer('RGBA', (size_x, size_y),
|
||||
image.raw, 'raw', 'RGBA', 0, 1)
|
||||
im.transpose(Image.FLIP_TOP_BOTTOM).save(self.outfile, self.format)
|
||||
|
||||
self.flag = 0
|
||||
self.screenshot_requested = False
|
||||
if self.invisibleMode:
|
||||
self._plot._window.close()
|
||||
|
||||
def save(self, outfile=None, format='', size=(600, 500)):
|
||||
self.outfile = outfile
|
||||
self.format = format
|
||||
self.size = size
|
||||
self.screenshot_requested = True
|
||||
|
||||
if not self._plot._window or self._plot._window.has_exit:
|
||||
self._plot._win_args['visible'] = False
|
||||
|
||||
self._plot._win_args['width'] = size[0]
|
||||
self._plot._win_args['height'] = size[1]
|
||||
|
||||
self._plot.axes.reset_resources()
|
||||
self._plot._window = PlotWindow(self._plot, **self._plot._win_args)
|
||||
self.invisibleMode = True
|
||||
|
||||
if self.outfile is None:
|
||||
self.outfile = self._create_unique_path()
|
||||
print(self.outfile)
|
||||
|
||||
def _create_unique_path(self):
|
||||
cwd = getcwd()
|
||||
l = listdir(cwd)
|
||||
path = ''
|
||||
i = 0
|
||||
while True:
|
||||
if not 'plot_%s.png' % i in l:
|
||||
path = cwd + '/plot_%s.png' % i
|
||||
break
|
||||
i += 1
|
||||
return path
|
||||
@@ -0,0 +1,251 @@
|
||||
import pyglet.gl as pgl
|
||||
from pyglet import font
|
||||
|
||||
from sympy.core import S
|
||||
from sympy.plotting.pygletplot.plot_object import PlotObject
|
||||
from sympy.plotting.pygletplot.util import billboard_matrix, dot_product, \
|
||||
get_direction_vectors, strided_range, vec_mag, vec_sub
|
||||
from sympy.utilities.iterables import is_sequence
|
||||
|
||||
|
||||
class PlotAxes(PlotObject):
|
||||
|
||||
def __init__(self, *args,
|
||||
style='', none=None, frame=None, box=None, ordinate=None,
|
||||
stride=0.25,
|
||||
visible='', overlay='', colored='', label_axes='', label_ticks='',
|
||||
tick_length=0.1,
|
||||
font_face='Arial', font_size=28,
|
||||
**kwargs):
|
||||
# initialize style parameter
|
||||
style = style.lower()
|
||||
|
||||
# allow alias kwargs to override style kwarg
|
||||
if none is not None:
|
||||
style = 'none'
|
||||
if frame is not None:
|
||||
style = 'frame'
|
||||
if box is not None:
|
||||
style = 'box'
|
||||
if ordinate is not None:
|
||||
style = 'ordinate'
|
||||
|
||||
if style in ['', 'ordinate']:
|
||||
self._render_object = PlotAxesOrdinate(self)
|
||||
elif style in ['frame', 'box']:
|
||||
self._render_object = PlotAxesFrame(self)
|
||||
elif style in ['none']:
|
||||
self._render_object = None
|
||||
else:
|
||||
raise ValueError(("Unrecognized axes style %s.") % (style))
|
||||
|
||||
# initialize stride parameter
|
||||
try:
|
||||
stride = eval(stride)
|
||||
except TypeError:
|
||||
pass
|
||||
if is_sequence(stride):
|
||||
if len(stride) != 3:
|
||||
raise ValueError("length should be equal to 3")
|
||||
self._stride = stride
|
||||
else:
|
||||
self._stride = [stride, stride, stride]
|
||||
self._tick_length = float(tick_length)
|
||||
|
||||
# setup bounding box and ticks
|
||||
self._origin = [0, 0, 0]
|
||||
self.reset_bounding_box()
|
||||
|
||||
def flexible_boolean(input, default):
|
||||
if input in [True, False]:
|
||||
return input
|
||||
if input in ('f', 'F', 'false', 'False'):
|
||||
return False
|
||||
if input in ('t', 'T', 'true', 'True'):
|
||||
return True
|
||||
return default
|
||||
|
||||
# initialize remaining parameters
|
||||
self.visible = flexible_boolean(kwargs, True)
|
||||
self._overlay = flexible_boolean(overlay, True)
|
||||
self._colored = flexible_boolean(colored, False)
|
||||
self._label_axes = flexible_boolean(label_axes, False)
|
||||
self._label_ticks = flexible_boolean(label_ticks, True)
|
||||
|
||||
# setup label font
|
||||
self.font_face = font_face
|
||||
self.font_size = font_size
|
||||
|
||||
# this is also used to reinit the
|
||||
# font on window close/reopen
|
||||
self.reset_resources()
|
||||
|
||||
def reset_resources(self):
|
||||
self.label_font = None
|
||||
|
||||
def reset_bounding_box(self):
|
||||
self._bounding_box = [[None, None], [None, None], [None, None]]
|
||||
self._axis_ticks = [[], [], []]
|
||||
|
||||
def draw(self):
|
||||
if self._render_object:
|
||||
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT | pgl.GL_DEPTH_BUFFER_BIT)
|
||||
if self._overlay:
|
||||
pgl.glDisable(pgl.GL_DEPTH_TEST)
|
||||
self._render_object.draw()
|
||||
pgl.glPopAttrib()
|
||||
|
||||
def adjust_bounds(self, child_bounds):
|
||||
b = self._bounding_box
|
||||
c = child_bounds
|
||||
for i in range(3):
|
||||
if abs(c[i][0]) is S.Infinity or abs(c[i][1]) is S.Infinity:
|
||||
continue
|
||||
b[i][0] = c[i][0] if b[i][0] is None else min([b[i][0], c[i][0]])
|
||||
b[i][1] = c[i][1] if b[i][1] is None else max([b[i][1], c[i][1]])
|
||||
self._bounding_box = b
|
||||
self._recalculate_axis_ticks(i)
|
||||
|
||||
def _recalculate_axis_ticks(self, axis):
|
||||
b = self._bounding_box
|
||||
if b[axis][0] is None or b[axis][1] is None:
|
||||
self._axis_ticks[axis] = []
|
||||
else:
|
||||
self._axis_ticks[axis] = strided_range(b[axis][0], b[axis][1],
|
||||
self._stride[axis])
|
||||
|
||||
def toggle_visible(self):
|
||||
self.visible = not self.visible
|
||||
|
||||
def toggle_colors(self):
|
||||
self._colored = not self._colored
|
||||
|
||||
|
||||
class PlotAxesBase(PlotObject):
|
||||
|
||||
def __init__(self, parent_axes):
|
||||
self._p = parent_axes
|
||||
|
||||
def draw(self):
|
||||
color = [([0.2, 0.1, 0.3], [0.2, 0.1, 0.3], [0.2, 0.1, 0.3]),
|
||||
([0.9, 0.3, 0.5], [0.5, 1.0, 0.5], [0.3, 0.3, 0.9])][self._p._colored]
|
||||
self.draw_background(color)
|
||||
self.draw_axis(2, color[2])
|
||||
self.draw_axis(1, color[1])
|
||||
self.draw_axis(0, color[0])
|
||||
|
||||
def draw_background(self, color):
|
||||
pass # optional
|
||||
|
||||
def draw_axis(self, axis, color):
|
||||
raise NotImplementedError()
|
||||
|
||||
def draw_text(self, text, position, color, scale=1.0):
|
||||
if len(color) == 3:
|
||||
color = (color[0], color[1], color[2], 1.0)
|
||||
|
||||
if self._p.label_font is None:
|
||||
self._p.label_font = font.load(self._p.font_face,
|
||||
self._p.font_size,
|
||||
bold=True, italic=False)
|
||||
|
||||
label = font.Text(self._p.label_font, text,
|
||||
color=color,
|
||||
valign=font.Text.BASELINE,
|
||||
halign=font.Text.CENTER)
|
||||
|
||||
pgl.glPushMatrix()
|
||||
pgl.glTranslatef(*position)
|
||||
billboard_matrix()
|
||||
scale_factor = 0.005 * scale
|
||||
pgl.glScalef(scale_factor, scale_factor, scale_factor)
|
||||
pgl.glColor4f(0, 0, 0, 0)
|
||||
label.draw()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
def draw_line(self, v, color):
|
||||
o = self._p._origin
|
||||
pgl.glBegin(pgl.GL_LINES)
|
||||
pgl.glColor3f(*color)
|
||||
pgl.glVertex3f(v[0][0] + o[0], v[0][1] + o[1], v[0][2] + o[2])
|
||||
pgl.glVertex3f(v[1][0] + o[0], v[1][1] + o[1], v[1][2] + o[2])
|
||||
pgl.glEnd()
|
||||
|
||||
|
||||
class PlotAxesOrdinate(PlotAxesBase):
|
||||
|
||||
def __init__(self, parent_axes):
|
||||
super().__init__(parent_axes)
|
||||
|
||||
def draw_axis(self, axis, color):
|
||||
ticks = self._p._axis_ticks[axis]
|
||||
radius = self._p._tick_length / 2.0
|
||||
if len(ticks) < 2:
|
||||
return
|
||||
|
||||
# calculate the vector for this axis
|
||||
axis_lines = [[0, 0, 0], [0, 0, 0]]
|
||||
axis_lines[0][axis], axis_lines[1][axis] = ticks[0], ticks[-1]
|
||||
axis_vector = vec_sub(axis_lines[1], axis_lines[0])
|
||||
|
||||
# calculate angle to the z direction vector
|
||||
pos_z = get_direction_vectors()[2]
|
||||
d = abs(dot_product(axis_vector, pos_z))
|
||||
d = d / vec_mag(axis_vector)
|
||||
|
||||
# don't draw labels if we're looking down the axis
|
||||
labels_visible = abs(d - 1.0) > 0.02
|
||||
|
||||
# draw the ticks and labels
|
||||
for tick in ticks:
|
||||
self.draw_tick_line(axis, color, radius, tick, labels_visible)
|
||||
|
||||
# draw the axis line and labels
|
||||
self.draw_axis_line(axis, color, ticks[0], ticks[-1], labels_visible)
|
||||
|
||||
def draw_axis_line(self, axis, color, a_min, a_max, labels_visible):
|
||||
axis_line = [[0, 0, 0], [0, 0, 0]]
|
||||
axis_line[0][axis], axis_line[1][axis] = a_min, a_max
|
||||
self.draw_line(axis_line, color)
|
||||
if labels_visible:
|
||||
self.draw_axis_line_labels(axis, color, axis_line)
|
||||
|
||||
def draw_axis_line_labels(self, axis, color, axis_line):
|
||||
if not self._p._label_axes:
|
||||
return
|
||||
axis_labels = [axis_line[0][::], axis_line[1][::]]
|
||||
axis_labels[0][axis] -= 0.3
|
||||
axis_labels[1][axis] += 0.3
|
||||
a_str = ['X', 'Y', 'Z'][axis]
|
||||
self.draw_text("-" + a_str, axis_labels[0], color)
|
||||
self.draw_text("+" + a_str, axis_labels[1], color)
|
||||
|
||||
def draw_tick_line(self, axis, color, radius, tick, labels_visible):
|
||||
tick_axis = {0: 1, 1: 0, 2: 1}[axis]
|
||||
tick_line = [[0, 0, 0], [0, 0, 0]]
|
||||
tick_line[0][axis] = tick_line[1][axis] = tick
|
||||
tick_line[0][tick_axis], tick_line[1][tick_axis] = -radius, radius
|
||||
self.draw_line(tick_line, color)
|
||||
if labels_visible:
|
||||
self.draw_tick_line_label(axis, color, radius, tick)
|
||||
|
||||
def draw_tick_line_label(self, axis, color, radius, tick):
|
||||
if not self._p._label_axes:
|
||||
return
|
||||
tick_label_vector = [0, 0, 0]
|
||||
tick_label_vector[axis] = tick
|
||||
tick_label_vector[{0: 1, 1: 0, 2: 1}[axis]] = [-1, 1, 1][
|
||||
axis] * radius * 3.5
|
||||
self.draw_text(str(tick), tick_label_vector, color, scale=0.5)
|
||||
|
||||
|
||||
class PlotAxesFrame(PlotAxesBase):
|
||||
|
||||
def __init__(self, parent_axes):
|
||||
super().__init__(parent_axes)
|
||||
|
||||
def draw_background(self, color):
|
||||
pass
|
||||
|
||||
def draw_axis(self, axis, color):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,124 @@
|
||||
import pyglet.gl as pgl
|
||||
from sympy.plotting.pygletplot.plot_rotation import get_spherical_rotatation
|
||||
from sympy.plotting.pygletplot.util import get_model_matrix, model_to_screen, \
|
||||
screen_to_model, vec_subs
|
||||
|
||||
|
||||
class PlotCamera:
|
||||
|
||||
min_dist = 0.05
|
||||
max_dist = 500.0
|
||||
|
||||
min_ortho_dist = 100.0
|
||||
max_ortho_dist = 10000.0
|
||||
|
||||
_default_dist = 6.0
|
||||
_default_ortho_dist = 600.0
|
||||
|
||||
rot_presets = {
|
||||
'xy': (0, 0, 0),
|
||||
'xz': (-90, 0, 0),
|
||||
'yz': (0, 90, 0),
|
||||
'perspective': (-45, 0, -45)
|
||||
}
|
||||
|
||||
def __init__(self, window, ortho=False):
|
||||
self.window = window
|
||||
self.axes = self.window.plot.axes
|
||||
self.ortho = ortho
|
||||
self.reset()
|
||||
|
||||
def init_rot_matrix(self):
|
||||
pgl.glPushMatrix()
|
||||
pgl.glLoadIdentity()
|
||||
self._rot = get_model_matrix()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
def set_rot_preset(self, preset_name):
|
||||
self.init_rot_matrix()
|
||||
if preset_name not in self.rot_presets:
|
||||
raise ValueError(
|
||||
"%s is not a valid rotation preset." % preset_name)
|
||||
r = self.rot_presets[preset_name]
|
||||
self.euler_rotate(r[0], 1, 0, 0)
|
||||
self.euler_rotate(r[1], 0, 1, 0)
|
||||
self.euler_rotate(r[2], 0, 0, 1)
|
||||
|
||||
def reset(self):
|
||||
self._dist = 0.0
|
||||
self._x, self._y = 0.0, 0.0
|
||||
self._rot = None
|
||||
if self.ortho:
|
||||
self._dist = self._default_ortho_dist
|
||||
else:
|
||||
self._dist = self._default_dist
|
||||
self.init_rot_matrix()
|
||||
|
||||
def mult_rot_matrix(self, rot):
|
||||
pgl.glPushMatrix()
|
||||
pgl.glLoadMatrixf(rot)
|
||||
pgl.glMultMatrixf(self._rot)
|
||||
self._rot = get_model_matrix()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
def setup_projection(self):
|
||||
pgl.glMatrixMode(pgl.GL_PROJECTION)
|
||||
pgl.glLoadIdentity()
|
||||
if self.ortho:
|
||||
# yep, this is pseudo ortho (don't tell anyone)
|
||||
pgl.gluPerspective(
|
||||
0.3, float(self.window.width)/float(self.window.height),
|
||||
self.min_ortho_dist - 0.01, self.max_ortho_dist + 0.01)
|
||||
else:
|
||||
pgl.gluPerspective(
|
||||
30.0, float(self.window.width)/float(self.window.height),
|
||||
self.min_dist - 0.01, self.max_dist + 0.01)
|
||||
pgl.glMatrixMode(pgl.GL_MODELVIEW)
|
||||
|
||||
def _get_scale(self):
|
||||
return 1.0, 1.0, 1.0
|
||||
|
||||
def apply_transformation(self):
|
||||
pgl.glLoadIdentity()
|
||||
pgl.glTranslatef(self._x, self._y, -self._dist)
|
||||
if self._rot is not None:
|
||||
pgl.glMultMatrixf(self._rot)
|
||||
pgl.glScalef(*self._get_scale())
|
||||
|
||||
def spherical_rotate(self, p1, p2, sensitivity=1.0):
|
||||
mat = get_spherical_rotatation(p1, p2, self.window.width,
|
||||
self.window.height, sensitivity)
|
||||
if mat is not None:
|
||||
self.mult_rot_matrix(mat)
|
||||
|
||||
def euler_rotate(self, angle, x, y, z):
|
||||
pgl.glPushMatrix()
|
||||
pgl.glLoadMatrixf(self._rot)
|
||||
pgl.glRotatef(angle, x, y, z)
|
||||
self._rot = get_model_matrix()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
def zoom_relative(self, clicks, sensitivity):
|
||||
|
||||
if self.ortho:
|
||||
dist_d = clicks * sensitivity * 50.0
|
||||
min_dist = self.min_ortho_dist
|
||||
max_dist = self.max_ortho_dist
|
||||
else:
|
||||
dist_d = clicks * sensitivity
|
||||
min_dist = self.min_dist
|
||||
max_dist = self.max_dist
|
||||
|
||||
new_dist = (self._dist - dist_d)
|
||||
if (clicks < 0 and new_dist < max_dist) or new_dist > min_dist:
|
||||
self._dist = new_dist
|
||||
|
||||
def mouse_translate(self, x, y, dx, dy):
|
||||
pgl.glPushMatrix()
|
||||
pgl.glLoadIdentity()
|
||||
pgl.glTranslatef(0, 0, -self._dist)
|
||||
z = model_to_screen(0, 0, 0)[2]
|
||||
d = vec_subs(screen_to_model(x, y, z), screen_to_model(x - dx, y - dy, z))
|
||||
pgl.glPopMatrix()
|
||||
self._x += d[0]
|
||||
self._y += d[1]
|
||||
@@ -0,0 +1,218 @@
|
||||
from pyglet.window import key
|
||||
from pyglet.window.mouse import LEFT, RIGHT, MIDDLE
|
||||
from sympy.plotting.pygletplot.util import get_direction_vectors, get_basis_vectors
|
||||
|
||||
|
||||
class PlotController:
|
||||
|
||||
normal_mouse_sensitivity = 4.0
|
||||
modified_mouse_sensitivity = 1.0
|
||||
|
||||
normal_key_sensitivity = 160.0
|
||||
modified_key_sensitivity = 40.0
|
||||
|
||||
keymap = {
|
||||
key.LEFT: 'left',
|
||||
key.A: 'left',
|
||||
key.NUM_4: 'left',
|
||||
|
||||
key.RIGHT: 'right',
|
||||
key.D: 'right',
|
||||
key.NUM_6: 'right',
|
||||
|
||||
key.UP: 'up',
|
||||
key.W: 'up',
|
||||
key.NUM_8: 'up',
|
||||
|
||||
key.DOWN: 'down',
|
||||
key.S: 'down',
|
||||
key.NUM_2: 'down',
|
||||
|
||||
key.Z: 'rotate_z_neg',
|
||||
key.NUM_1: 'rotate_z_neg',
|
||||
|
||||
key.C: 'rotate_z_pos',
|
||||
key.NUM_3: 'rotate_z_pos',
|
||||
|
||||
key.Q: 'spin_left',
|
||||
key.NUM_7: 'spin_left',
|
||||
key.E: 'spin_right',
|
||||
key.NUM_9: 'spin_right',
|
||||
|
||||
key.X: 'reset_camera',
|
||||
key.NUM_5: 'reset_camera',
|
||||
|
||||
key.NUM_ADD: 'zoom_in',
|
||||
key.PAGEUP: 'zoom_in',
|
||||
key.R: 'zoom_in',
|
||||
|
||||
key.NUM_SUBTRACT: 'zoom_out',
|
||||
key.PAGEDOWN: 'zoom_out',
|
||||
key.F: 'zoom_out',
|
||||
|
||||
key.RSHIFT: 'modify_sensitivity',
|
||||
key.LSHIFT: 'modify_sensitivity',
|
||||
|
||||
key.F1: 'rot_preset_xy',
|
||||
key.F2: 'rot_preset_xz',
|
||||
key.F3: 'rot_preset_yz',
|
||||
key.F4: 'rot_preset_perspective',
|
||||
|
||||
key.F5: 'toggle_axes',
|
||||
key.F6: 'toggle_axe_colors',
|
||||
|
||||
key.F8: 'save_image'
|
||||
}
|
||||
|
||||
def __init__(self, window, *, invert_mouse_zoom=False, **kwargs):
|
||||
self.invert_mouse_zoom = invert_mouse_zoom
|
||||
self.window = window
|
||||
self.camera = window.camera
|
||||
self.action = {
|
||||
# Rotation around the view Y (up) vector
|
||||
'left': False,
|
||||
'right': False,
|
||||
# Rotation around the view X vector
|
||||
'up': False,
|
||||
'down': False,
|
||||
# Rotation around the view Z vector
|
||||
'spin_left': False,
|
||||
'spin_right': False,
|
||||
# Rotation around the model Z vector
|
||||
'rotate_z_neg': False,
|
||||
'rotate_z_pos': False,
|
||||
# Reset to the default rotation
|
||||
'reset_camera': False,
|
||||
# Performs camera z-translation
|
||||
'zoom_in': False,
|
||||
'zoom_out': False,
|
||||
# Use alternative sensitivity (speed)
|
||||
'modify_sensitivity': False,
|
||||
# Rotation presets
|
||||
'rot_preset_xy': False,
|
||||
'rot_preset_xz': False,
|
||||
'rot_preset_yz': False,
|
||||
'rot_preset_perspective': False,
|
||||
# axes
|
||||
'toggle_axes': False,
|
||||
'toggle_axe_colors': False,
|
||||
# screenshot
|
||||
'save_image': False
|
||||
}
|
||||
|
||||
def update(self, dt):
|
||||
z = 0
|
||||
if self.action['zoom_out']:
|
||||
z -= 1
|
||||
if self.action['zoom_in']:
|
||||
z += 1
|
||||
if z != 0:
|
||||
self.camera.zoom_relative(z/10.0, self.get_key_sensitivity()/10.0)
|
||||
|
||||
dx, dy, dz = 0, 0, 0
|
||||
if self.action['left']:
|
||||
dx -= 1
|
||||
if self.action['right']:
|
||||
dx += 1
|
||||
if self.action['up']:
|
||||
dy -= 1
|
||||
if self.action['down']:
|
||||
dy += 1
|
||||
if self.action['spin_left']:
|
||||
dz += 1
|
||||
if self.action['spin_right']:
|
||||
dz -= 1
|
||||
|
||||
if not self.is_2D():
|
||||
if dx != 0:
|
||||
self.camera.euler_rotate(dx*dt*self.get_key_sensitivity(),
|
||||
*(get_direction_vectors()[1]))
|
||||
if dy != 0:
|
||||
self.camera.euler_rotate(dy*dt*self.get_key_sensitivity(),
|
||||
*(get_direction_vectors()[0]))
|
||||
if dz != 0:
|
||||
self.camera.euler_rotate(dz*dt*self.get_key_sensitivity(),
|
||||
*(get_direction_vectors()[2]))
|
||||
else:
|
||||
self.camera.mouse_translate(0, 0, dx*dt*self.get_key_sensitivity(),
|
||||
-dy*dt*self.get_key_sensitivity())
|
||||
|
||||
rz = 0
|
||||
if self.action['rotate_z_neg'] and not self.is_2D():
|
||||
rz -= 1
|
||||
if self.action['rotate_z_pos'] and not self.is_2D():
|
||||
rz += 1
|
||||
|
||||
if rz != 0:
|
||||
self.camera.euler_rotate(rz*dt*self.get_key_sensitivity(),
|
||||
*(get_basis_vectors()[2]))
|
||||
|
||||
if self.action['reset_camera']:
|
||||
self.camera.reset()
|
||||
|
||||
if self.action['rot_preset_xy']:
|
||||
self.camera.set_rot_preset('xy')
|
||||
if self.action['rot_preset_xz']:
|
||||
self.camera.set_rot_preset('xz')
|
||||
if self.action['rot_preset_yz']:
|
||||
self.camera.set_rot_preset('yz')
|
||||
if self.action['rot_preset_perspective']:
|
||||
self.camera.set_rot_preset('perspective')
|
||||
|
||||
if self.action['toggle_axes']:
|
||||
self.action['toggle_axes'] = False
|
||||
self.camera.axes.toggle_visible()
|
||||
|
||||
if self.action['toggle_axe_colors']:
|
||||
self.action['toggle_axe_colors'] = False
|
||||
self.camera.axes.toggle_colors()
|
||||
|
||||
if self.action['save_image']:
|
||||
self.action['save_image'] = False
|
||||
self.window.plot.saveimage()
|
||||
|
||||
return True
|
||||
|
||||
def get_mouse_sensitivity(self):
|
||||
if self.action['modify_sensitivity']:
|
||||
return self.modified_mouse_sensitivity
|
||||
else:
|
||||
return self.normal_mouse_sensitivity
|
||||
|
||||
def get_key_sensitivity(self):
|
||||
if self.action['modify_sensitivity']:
|
||||
return self.modified_key_sensitivity
|
||||
else:
|
||||
return self.normal_key_sensitivity
|
||||
|
||||
def on_key_press(self, symbol, modifiers):
|
||||
if symbol in self.keymap:
|
||||
self.action[self.keymap[symbol]] = True
|
||||
|
||||
def on_key_release(self, symbol, modifiers):
|
||||
if symbol in self.keymap:
|
||||
self.action[self.keymap[symbol]] = False
|
||||
|
||||
def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
|
||||
if buttons & LEFT:
|
||||
if self.is_2D():
|
||||
self.camera.mouse_translate(x, y, dx, dy)
|
||||
else:
|
||||
self.camera.spherical_rotate((x - dx, y - dy), (x, y),
|
||||
self.get_mouse_sensitivity())
|
||||
if buttons & MIDDLE:
|
||||
self.camera.zoom_relative([1, -1][self.invert_mouse_zoom]*dy,
|
||||
self.get_mouse_sensitivity()/20.0)
|
||||
if buttons & RIGHT:
|
||||
self.camera.mouse_translate(x, y, dx, dy)
|
||||
|
||||
def on_mouse_scroll(self, x, y, dx, dy):
|
||||
self.camera.zoom_relative([1, -1][self.invert_mouse_zoom]*dy,
|
||||
self.get_mouse_sensitivity())
|
||||
|
||||
def is_2D(self):
|
||||
functions = self.window.plot._functions
|
||||
for i in functions:
|
||||
if len(functions[i].i_vars) > 1 or len(functions[i].d_vars) > 2:
|
||||
return False
|
||||
return True
|
||||
@@ -0,0 +1,82 @@
|
||||
import pyglet.gl as pgl
|
||||
from sympy.core import S
|
||||
from sympy.plotting.pygletplot.plot_mode_base import PlotModeBase
|
||||
|
||||
|
||||
class PlotCurve(PlotModeBase):
|
||||
|
||||
style_override = 'wireframe'
|
||||
|
||||
def _on_calculate_verts(self):
|
||||
self.t_interval = self.intervals[0]
|
||||
self.t_set = list(self.t_interval.frange())
|
||||
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0]]
|
||||
evaluate = self._get_evaluator()
|
||||
|
||||
self._calculating_verts_pos = 0.0
|
||||
self._calculating_verts_len = float(self.t_interval.v_len)
|
||||
|
||||
self.verts = []
|
||||
b = self.bounds
|
||||
for t in self.t_set:
|
||||
try:
|
||||
_e = evaluate(t) # calculate vertex
|
||||
except (NameError, ZeroDivisionError):
|
||||
_e = None
|
||||
if _e is not None: # update bounding box
|
||||
for axis in range(3):
|
||||
b[axis][0] = min([b[axis][0], _e[axis]])
|
||||
b[axis][1] = max([b[axis][1], _e[axis]])
|
||||
self.verts.append(_e)
|
||||
self._calculating_verts_pos += 1.0
|
||||
|
||||
for axis in range(3):
|
||||
b[axis][2] = b[axis][1] - b[axis][0]
|
||||
if b[axis][2] == 0.0:
|
||||
b[axis][2] = 1.0
|
||||
|
||||
self.push_wireframe(self.draw_verts(False))
|
||||
|
||||
def _on_calculate_cverts(self):
|
||||
if not self.verts or not self.color:
|
||||
return
|
||||
|
||||
def set_work_len(n):
|
||||
self._calculating_cverts_len = float(n)
|
||||
|
||||
def inc_work_pos():
|
||||
self._calculating_cverts_pos += 1.0
|
||||
set_work_len(1)
|
||||
self._calculating_cverts_pos = 0
|
||||
self.cverts = self.color.apply_to_curve(self.verts,
|
||||
self.t_set,
|
||||
set_len=set_work_len,
|
||||
inc_pos=inc_work_pos)
|
||||
self.push_wireframe(self.draw_verts(True))
|
||||
|
||||
def calculate_one_cvert(self, t):
|
||||
vert = self.verts[t]
|
||||
return self.color(vert[0], vert[1], vert[2],
|
||||
self.t_set[t], None)
|
||||
|
||||
def draw_verts(self, use_cverts):
|
||||
def f():
|
||||
pgl.glBegin(pgl.GL_LINE_STRIP)
|
||||
for t in range(len(self.t_set)):
|
||||
p = self.verts[t]
|
||||
if p is None:
|
||||
pgl.glEnd()
|
||||
pgl.glBegin(pgl.GL_LINE_STRIP)
|
||||
continue
|
||||
if use_cverts:
|
||||
c = self.cverts[t]
|
||||
if c is None:
|
||||
c = (0, 0, 0)
|
||||
pgl.glColor3f(*c)
|
||||
else:
|
||||
pgl.glColor3f(*self.default_wireframe_color)
|
||||
pgl.glVertex3f(*p)
|
||||
pgl.glEnd()
|
||||
return f
|
||||
@@ -0,0 +1,181 @@
|
||||
from sympy.core.singleton import S
|
||||
from sympy.core.symbol import Symbol
|
||||
from sympy.core.sympify import sympify
|
||||
from sympy.core.numbers import Integer
|
||||
|
||||
|
||||
class PlotInterval:
|
||||
"""
|
||||
"""
|
||||
_v, _v_min, _v_max, _v_steps = None, None, None, None
|
||||
|
||||
def require_all_args(f):
|
||||
def check(self, *args, **kwargs):
|
||||
for g in [self._v, self._v_min, self._v_max, self._v_steps]:
|
||||
if g is None:
|
||||
raise ValueError("PlotInterval is incomplete.")
|
||||
return f(self, *args, **kwargs)
|
||||
return check
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) == 1:
|
||||
if isinstance(args[0], PlotInterval):
|
||||
self.fill_from(args[0])
|
||||
return
|
||||
elif isinstance(args[0], str):
|
||||
try:
|
||||
args = eval(args[0])
|
||||
except TypeError:
|
||||
s_eval_error = "Could not interpret string %s."
|
||||
raise ValueError(s_eval_error % (args[0]))
|
||||
elif isinstance(args[0], (tuple, list)):
|
||||
args = args[0]
|
||||
else:
|
||||
raise ValueError("Not an interval.")
|
||||
if not isinstance(args, (tuple, list)) or len(args) > 4:
|
||||
f_error = "PlotInterval must be a tuple or list of length 4 or less."
|
||||
raise ValueError(f_error)
|
||||
|
||||
args = list(args)
|
||||
if len(args) > 0 and (args[0] is None or isinstance(args[0], Symbol)):
|
||||
self.v = args.pop(0)
|
||||
if len(args) in [2, 3]:
|
||||
self.v_min = args.pop(0)
|
||||
self.v_max = args.pop(0)
|
||||
if len(args) == 1:
|
||||
self.v_steps = args.pop(0)
|
||||
elif len(args) == 1:
|
||||
self.v_steps = args.pop(0)
|
||||
|
||||
def get_v(self):
|
||||
return self._v
|
||||
|
||||
def set_v(self, v):
|
||||
if v is None:
|
||||
self._v = None
|
||||
return
|
||||
if not isinstance(v, Symbol):
|
||||
raise ValueError("v must be a SymPy Symbol.")
|
||||
self._v = v
|
||||
|
||||
def get_v_min(self):
|
||||
return self._v_min
|
||||
|
||||
def set_v_min(self, v_min):
|
||||
if v_min is None:
|
||||
self._v_min = None
|
||||
return
|
||||
try:
|
||||
self._v_min = sympify(v_min)
|
||||
float(self._v_min.evalf())
|
||||
except TypeError:
|
||||
raise ValueError("v_min could not be interpreted as a number.")
|
||||
|
||||
def get_v_max(self):
|
||||
return self._v_max
|
||||
|
||||
def set_v_max(self, v_max):
|
||||
if v_max is None:
|
||||
self._v_max = None
|
||||
return
|
||||
try:
|
||||
self._v_max = sympify(v_max)
|
||||
float(self._v_max.evalf())
|
||||
except TypeError:
|
||||
raise ValueError("v_max could not be interpreted as a number.")
|
||||
|
||||
def get_v_steps(self):
|
||||
return self._v_steps
|
||||
|
||||
def set_v_steps(self, v_steps):
|
||||
if v_steps is None:
|
||||
self._v_steps = None
|
||||
return
|
||||
if isinstance(v_steps, int):
|
||||
v_steps = Integer(v_steps)
|
||||
elif not isinstance(v_steps, Integer):
|
||||
raise ValueError("v_steps must be an int or SymPy Integer.")
|
||||
if v_steps <= S.Zero:
|
||||
raise ValueError("v_steps must be positive.")
|
||||
self._v_steps = v_steps
|
||||
|
||||
@require_all_args
|
||||
def get_v_len(self):
|
||||
return self.v_steps + 1
|
||||
|
||||
v = property(get_v, set_v)
|
||||
v_min = property(get_v_min, set_v_min)
|
||||
v_max = property(get_v_max, set_v_max)
|
||||
v_steps = property(get_v_steps, set_v_steps)
|
||||
v_len = property(get_v_len)
|
||||
|
||||
def fill_from(self, b):
|
||||
if b.v is not None:
|
||||
self.v = b.v
|
||||
if b.v_min is not None:
|
||||
self.v_min = b.v_min
|
||||
if b.v_max is not None:
|
||||
self.v_max = b.v_max
|
||||
if b.v_steps is not None:
|
||||
self.v_steps = b.v_steps
|
||||
|
||||
@staticmethod
|
||||
def try_parse(*args):
|
||||
"""
|
||||
Returns a PlotInterval if args can be interpreted
|
||||
as such, otherwise None.
|
||||
"""
|
||||
if len(args) == 1 and isinstance(args[0], PlotInterval):
|
||||
return args[0]
|
||||
try:
|
||||
return PlotInterval(*args)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _str_base(self):
|
||||
return ",".join([str(self.v), str(self.v_min),
|
||||
str(self.v_max), str(self.v_steps)])
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
A string representing the interval in class constructor form.
|
||||
"""
|
||||
return "PlotInterval(%s)" % (self._str_base())
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
A string representing the interval in list form.
|
||||
"""
|
||||
return "[%s]" % (self._str_base())
|
||||
|
||||
@require_all_args
|
||||
def assert_complete(self):
|
||||
pass
|
||||
|
||||
@require_all_args
|
||||
def vrange(self):
|
||||
"""
|
||||
Yields v_steps+1 SymPy numbers ranging from
|
||||
v_min to v_max.
|
||||
"""
|
||||
d = (self.v_max - self.v_min) / self.v_steps
|
||||
for i in range(self.v_steps + 1):
|
||||
a = self.v_min + (d * Integer(i))
|
||||
yield a
|
||||
|
||||
@require_all_args
|
||||
def vrange2(self):
|
||||
"""
|
||||
Yields v_steps pairs of SymPy numbers ranging from
|
||||
(v_min, v_min + step) to (v_max - step, v_max).
|
||||
"""
|
||||
d = (self.v_max - self.v_min) / self.v_steps
|
||||
a = self.v_min + (d * S.Zero)
|
||||
for i in range(self.v_steps):
|
||||
b = self.v_min + (d * Integer(i + 1))
|
||||
yield a, b
|
||||
a = b
|
||||
|
||||
def frange(self):
|
||||
for i in self.vrange():
|
||||
yield float(i.evalf())
|
||||
@@ -0,0 +1,400 @@
|
||||
from .plot_interval import PlotInterval
|
||||
from .plot_object import PlotObject
|
||||
from .util import parse_option_string
|
||||
from sympy.core.symbol import Symbol
|
||||
from sympy.core.sympify import sympify
|
||||
from sympy.geometry.entity import GeometryEntity
|
||||
from sympy.utilities.iterables import is_sequence
|
||||
|
||||
|
||||
class PlotMode(PlotObject):
|
||||
"""
|
||||
Grandparent class for plotting
|
||||
modes. Serves as interface for
|
||||
registration, lookup, and init
|
||||
of modes.
|
||||
|
||||
To create a new plot mode,
|
||||
inherit from PlotModeBase
|
||||
or one of its children, such
|
||||
as PlotSurface or PlotCurve.
|
||||
"""
|
||||
|
||||
## Class-level attributes
|
||||
## used to register and lookup
|
||||
## plot modes. See PlotModeBase
|
||||
## for descriptions and usage.
|
||||
|
||||
i_vars, d_vars = '', ''
|
||||
intervals = []
|
||||
aliases = []
|
||||
is_default = False
|
||||
|
||||
## Draw is the only method here which
|
||||
## is meant to be overridden in child
|
||||
## classes, and PlotModeBase provides
|
||||
## a base implementation.
|
||||
def draw(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
## Everything else in this file has to
|
||||
## do with registration and retrieval
|
||||
## of plot modes. This is where I've
|
||||
## hidden much of the ugliness of automatic
|
||||
## plot mode divination...
|
||||
|
||||
## Plot mode registry data structures
|
||||
_mode_alias_list = []
|
||||
_mode_map = {
|
||||
1: {1: {}, 2: {}},
|
||||
2: {1: {}, 2: {}},
|
||||
3: {1: {}, 2: {}},
|
||||
} # [d][i][alias_str]: class
|
||||
_mode_default_map = {
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
} # [d][i]: class
|
||||
_i_var_max, _d_var_max = 2, 3
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""
|
||||
This is the function which interprets
|
||||
arguments given to Plot.__init__ and
|
||||
Plot.__setattr__. Returns an initialized
|
||||
instance of the appropriate child class.
|
||||
"""
|
||||
|
||||
newargs, newkwargs = PlotMode._extract_options(args, kwargs)
|
||||
mode_arg = newkwargs.get('mode', '')
|
||||
|
||||
# Interpret the arguments
|
||||
d_vars, intervals = PlotMode._interpret_args(newargs)
|
||||
i_vars = PlotMode._find_i_vars(d_vars, intervals)
|
||||
i, d = max([len(i_vars), len(intervals)]), len(d_vars)
|
||||
|
||||
# Find the appropriate mode
|
||||
subcls = PlotMode._get_mode(mode_arg, i, d)
|
||||
|
||||
# Create the object
|
||||
o = object.__new__(subcls)
|
||||
|
||||
# Do some setup for the mode instance
|
||||
o.d_vars = d_vars
|
||||
o._fill_i_vars(i_vars)
|
||||
o._fill_intervals(intervals)
|
||||
o.options = newkwargs
|
||||
|
||||
return o
|
||||
|
||||
@staticmethod
|
||||
def _get_mode(mode_arg, i_var_count, d_var_count):
|
||||
"""
|
||||
Tries to return an appropriate mode class.
|
||||
Intended to be called only by __new__.
|
||||
|
||||
mode_arg
|
||||
Can be a string or a class. If it is a
|
||||
PlotMode subclass, it is simply returned.
|
||||
If it is a string, it can an alias for
|
||||
a mode or an empty string. In the latter
|
||||
case, we try to find a default mode for
|
||||
the i_var_count and d_var_count.
|
||||
|
||||
i_var_count
|
||||
The number of independent variables
|
||||
needed to evaluate the d_vars.
|
||||
|
||||
d_var_count
|
||||
The number of dependent variables;
|
||||
usually the number of functions to
|
||||
be evaluated in plotting.
|
||||
|
||||
For example, a Cartesian function y = f(x) has
|
||||
one i_var (x) and one d_var (y). A parametric
|
||||
form x,y,z = f(u,v), f(u,v), f(u,v) has two
|
||||
two i_vars (u,v) and three d_vars (x,y,z).
|
||||
"""
|
||||
# if the mode_arg is simply a PlotMode class,
|
||||
# check that the mode supports the numbers
|
||||
# of independent and dependent vars, then
|
||||
# return it
|
||||
try:
|
||||
m = None
|
||||
if issubclass(mode_arg, PlotMode):
|
||||
m = mode_arg
|
||||
except TypeError:
|
||||
pass
|
||||
if m:
|
||||
if not m._was_initialized:
|
||||
raise ValueError(("To use unregistered plot mode %s "
|
||||
"you must first call %s._init_mode().")
|
||||
% (m.__name__, m.__name__))
|
||||
if d_var_count != m.d_var_count:
|
||||
raise ValueError(("%s can only plot functions "
|
||||
"with %i dependent variables.")
|
||||
% (m.__name__,
|
||||
m.d_var_count))
|
||||
if i_var_count > m.i_var_count:
|
||||
raise ValueError(("%s cannot plot functions "
|
||||
"with more than %i independent "
|
||||
"variables.")
|
||||
% (m.__name__,
|
||||
m.i_var_count))
|
||||
return m
|
||||
# If it is a string, there are two possibilities.
|
||||
if isinstance(mode_arg, str):
|
||||
i, d = i_var_count, d_var_count
|
||||
if i > PlotMode._i_var_max:
|
||||
raise ValueError(var_count_error(True, True))
|
||||
if d > PlotMode._d_var_max:
|
||||
raise ValueError(var_count_error(False, True))
|
||||
# If the string is '', try to find a suitable
|
||||
# default mode
|
||||
if not mode_arg:
|
||||
return PlotMode._get_default_mode(i, d)
|
||||
# Otherwise, interpret the string as a mode
|
||||
# alias (e.g. 'cartesian', 'parametric', etc)
|
||||
else:
|
||||
return PlotMode._get_aliased_mode(mode_arg, i, d)
|
||||
else:
|
||||
raise ValueError("PlotMode argument must be "
|
||||
"a class or a string")
|
||||
|
||||
@staticmethod
|
||||
def _get_default_mode(i, d, i_vars=-1):
|
||||
if i_vars == -1:
|
||||
i_vars = i
|
||||
try:
|
||||
return PlotMode._mode_default_map[d][i]
|
||||
except KeyError:
|
||||
# Keep looking for modes in higher i var counts
|
||||
# which support the given d var count until we
|
||||
# reach the max i_var count.
|
||||
if i < PlotMode._i_var_max:
|
||||
return PlotMode._get_default_mode(i + 1, d, i_vars)
|
||||
else:
|
||||
raise ValueError(("Couldn't find a default mode "
|
||||
"for %i independent and %i "
|
||||
"dependent variables.") % (i_vars, d))
|
||||
|
||||
@staticmethod
|
||||
def _get_aliased_mode(alias, i, d, i_vars=-1):
|
||||
if i_vars == -1:
|
||||
i_vars = i
|
||||
if alias not in PlotMode._mode_alias_list:
|
||||
raise ValueError(("Couldn't find a mode called"
|
||||
" %s. Known modes: %s.")
|
||||
% (alias, ", ".join(PlotMode._mode_alias_list)))
|
||||
try:
|
||||
return PlotMode._mode_map[d][i][alias]
|
||||
except TypeError:
|
||||
# Keep looking for modes in higher i var counts
|
||||
# which support the given d var count and alias
|
||||
# until we reach the max i_var count.
|
||||
if i < PlotMode._i_var_max:
|
||||
return PlotMode._get_aliased_mode(alias, i + 1, d, i_vars)
|
||||
else:
|
||||
raise ValueError(("Couldn't find a %s mode "
|
||||
"for %i independent and %i "
|
||||
"dependent variables.")
|
||||
% (alias, i_vars, d))
|
||||
|
||||
@classmethod
|
||||
def _register(cls):
|
||||
"""
|
||||
Called once for each user-usable plot mode.
|
||||
For Cartesian2D, it is invoked after the
|
||||
class definition: Cartesian2D._register()
|
||||
"""
|
||||
name = cls.__name__
|
||||
cls._init_mode()
|
||||
|
||||
try:
|
||||
i, d = cls.i_var_count, cls.d_var_count
|
||||
# Add the mode to _mode_map under all
|
||||
# given aliases
|
||||
for a in cls.aliases:
|
||||
if a not in PlotMode._mode_alias_list:
|
||||
# Also track valid aliases, so
|
||||
# we can quickly know when given
|
||||
# an invalid one in _get_mode.
|
||||
PlotMode._mode_alias_list.append(a)
|
||||
PlotMode._mode_map[d][i][a] = cls
|
||||
if cls.is_default:
|
||||
# If this mode was marked as the
|
||||
# default for this d,i combination,
|
||||
# also set that.
|
||||
PlotMode._mode_default_map[d][i] = cls
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(("Failed to register "
|
||||
"plot mode %s. Reason: %s")
|
||||
% (name, (str(e))))
|
||||
|
||||
@classmethod
|
||||
def _init_mode(cls):
|
||||
"""
|
||||
Initializes the plot mode based on
|
||||
the 'mode-specific parameters' above.
|
||||
Only intended to be called by
|
||||
PlotMode._register(). To use a mode without
|
||||
registering it, you can directly call
|
||||
ModeSubclass._init_mode().
|
||||
"""
|
||||
def symbols_list(symbol_str):
|
||||
return [Symbol(s) for s in symbol_str]
|
||||
|
||||
# Convert the vars strs into
|
||||
# lists of symbols.
|
||||
cls.i_vars = symbols_list(cls.i_vars)
|
||||
cls.d_vars = symbols_list(cls.d_vars)
|
||||
|
||||
# Var count is used often, calculate
|
||||
# it once here
|
||||
cls.i_var_count = len(cls.i_vars)
|
||||
cls.d_var_count = len(cls.d_vars)
|
||||
|
||||
if cls.i_var_count > PlotMode._i_var_max:
|
||||
raise ValueError(var_count_error(True, False))
|
||||
if cls.d_var_count > PlotMode._d_var_max:
|
||||
raise ValueError(var_count_error(False, False))
|
||||
|
||||
# Try to use first alias as primary_alias
|
||||
if len(cls.aliases) > 0:
|
||||
cls.primary_alias = cls.aliases[0]
|
||||
else:
|
||||
cls.primary_alias = cls.__name__
|
||||
|
||||
di = cls.intervals
|
||||
if len(di) != cls.i_var_count:
|
||||
raise ValueError("Plot mode must provide a "
|
||||
"default interval for each i_var.")
|
||||
for i in range(cls.i_var_count):
|
||||
# default intervals must be given [min,max,steps]
|
||||
# (no var, but they must be in the same order as i_vars)
|
||||
if len(di[i]) != 3:
|
||||
raise ValueError("length should be equal to 3")
|
||||
|
||||
# Initialize an incomplete interval,
|
||||
# to later be filled with a var when
|
||||
# the mode is instantiated.
|
||||
di[i] = PlotInterval(None, *di[i])
|
||||
|
||||
# To prevent people from using modes
|
||||
# without these required fields set up.
|
||||
cls._was_initialized = True
|
||||
|
||||
_was_initialized = False
|
||||
|
||||
## Initializer Helper Methods
|
||||
|
||||
@staticmethod
|
||||
def _find_i_vars(functions, intervals):
|
||||
i_vars = []
|
||||
|
||||
# First, collect i_vars in the
|
||||
# order they are given in any
|
||||
# intervals.
|
||||
for i in intervals:
|
||||
if i.v is None:
|
||||
continue
|
||||
elif i.v in i_vars:
|
||||
raise ValueError(("Multiple intervals given "
|
||||
"for %s.") % (str(i.v)))
|
||||
i_vars.append(i.v)
|
||||
|
||||
# Then, find any remaining
|
||||
# i_vars in given functions
|
||||
# (aka d_vars)
|
||||
for f in functions:
|
||||
for a in f.free_symbols:
|
||||
if a not in i_vars:
|
||||
i_vars.append(a)
|
||||
|
||||
return i_vars
|
||||
|
||||
def _fill_i_vars(self, i_vars):
|
||||
# copy default i_vars
|
||||
self.i_vars = [Symbol(str(i)) for i in self.i_vars]
|
||||
# replace with given i_vars
|
||||
for i in range(len(i_vars)):
|
||||
self.i_vars[i] = i_vars[i]
|
||||
|
||||
def _fill_intervals(self, intervals):
|
||||
# copy default intervals
|
||||
self.intervals = [PlotInterval(i) for i in self.intervals]
|
||||
# track i_vars used so far
|
||||
v_used = []
|
||||
# fill copy of default
|
||||
# intervals with given info
|
||||
for i in range(len(intervals)):
|
||||
self.intervals[i].fill_from(intervals[i])
|
||||
if self.intervals[i].v is not None:
|
||||
v_used.append(self.intervals[i].v)
|
||||
# Find any orphan intervals and
|
||||
# assign them i_vars
|
||||
for i in range(len(self.intervals)):
|
||||
if self.intervals[i].v is None:
|
||||
u = [v for v in self.i_vars if v not in v_used]
|
||||
if len(u) == 0:
|
||||
raise ValueError("length should not be equal to 0")
|
||||
self.intervals[i].v = u[0]
|
||||
v_used.append(u[0])
|
||||
|
||||
@staticmethod
|
||||
def _interpret_args(args):
|
||||
interval_wrong_order = "PlotInterval %s was given before any function(s)."
|
||||
interpret_error = "Could not interpret %s as a function or interval."
|
||||
|
||||
functions, intervals = [], []
|
||||
if isinstance(args[0], GeometryEntity):
|
||||
for coords in list(args[0].arbitrary_point()):
|
||||
functions.append(coords)
|
||||
intervals.append(PlotInterval.try_parse(args[0].plot_interval()))
|
||||
else:
|
||||
for a in args:
|
||||
i = PlotInterval.try_parse(a)
|
||||
if i is not None:
|
||||
if len(functions) == 0:
|
||||
raise ValueError(interval_wrong_order % (str(i)))
|
||||
else:
|
||||
intervals.append(i)
|
||||
else:
|
||||
if is_sequence(a, include=str):
|
||||
raise ValueError(interpret_error % (str(a)))
|
||||
try:
|
||||
f = sympify(a)
|
||||
functions.append(f)
|
||||
except TypeError:
|
||||
raise ValueError(interpret_error % str(a))
|
||||
|
||||
return functions, intervals
|
||||
|
||||
@staticmethod
|
||||
def _extract_options(args, kwargs):
|
||||
newkwargs, newargs = {}, []
|
||||
for a in args:
|
||||
if isinstance(a, str):
|
||||
newkwargs = dict(newkwargs, **parse_option_string(a))
|
||||
else:
|
||||
newargs.append(a)
|
||||
newkwargs = dict(newkwargs, **kwargs)
|
||||
return newargs, newkwargs
|
||||
|
||||
|
||||
def var_count_error(is_independent, is_plotting):
|
||||
"""
|
||||
Used to format an error message which differs
|
||||
slightly in 4 places.
|
||||
"""
|
||||
if is_plotting:
|
||||
v = "Plotting"
|
||||
else:
|
||||
v = "Registering plot modes"
|
||||
if is_independent:
|
||||
n, s = PlotMode._i_var_max, "independent"
|
||||
else:
|
||||
n, s = PlotMode._d_var_max, "dependent"
|
||||
return ("%s with more than %i %s variables "
|
||||
"is not supported.") % (v, n, s)
|
||||
@@ -0,0 +1,378 @@
|
||||
import pyglet.gl as pgl
|
||||
from sympy.core import S
|
||||
from sympy.plotting.pygletplot.color_scheme import ColorScheme
|
||||
from sympy.plotting.pygletplot.plot_mode import PlotMode
|
||||
from sympy.utilities.iterables import is_sequence
|
||||
from time import sleep
|
||||
from threading import Thread, Event, RLock
|
||||
import warnings
|
||||
|
||||
|
||||
class PlotModeBase(PlotMode):
|
||||
"""
|
||||
Intended parent class for plotting
|
||||
modes. Provides base functionality
|
||||
in conjunction with its parent,
|
||||
PlotMode.
|
||||
"""
|
||||
|
||||
##
|
||||
## Class-Level Attributes
|
||||
##
|
||||
|
||||
"""
|
||||
The following attributes are meant
|
||||
to be set at the class level, and serve
|
||||
as parameters to the plot mode registry
|
||||
(in PlotMode). See plot_modes.py for
|
||||
concrete examples.
|
||||
"""
|
||||
|
||||
"""
|
||||
i_vars
|
||||
'x' for Cartesian2D
|
||||
'xy' for Cartesian3D
|
||||
etc.
|
||||
|
||||
d_vars
|
||||
'y' for Cartesian2D
|
||||
'r' for Polar
|
||||
etc.
|
||||
"""
|
||||
i_vars, d_vars = '', ''
|
||||
|
||||
"""
|
||||
intervals
|
||||
Default intervals for each i_var, and in the
|
||||
same order. Specified [min, max, steps].
|
||||
No variable can be given (it is bound later).
|
||||
"""
|
||||
intervals = []
|
||||
|
||||
"""
|
||||
aliases
|
||||
A list of strings which can be used to
|
||||
access this mode.
|
||||
'cartesian' for Cartesian2D and Cartesian3D
|
||||
'polar' for Polar
|
||||
'cylindrical', 'polar' for Cylindrical
|
||||
|
||||
Note that _init_mode chooses the first alias
|
||||
in the list as the mode's primary_alias, which
|
||||
will be displayed to the end user in certain
|
||||
contexts.
|
||||
"""
|
||||
aliases = []
|
||||
|
||||
"""
|
||||
is_default
|
||||
Whether to set this mode as the default
|
||||
for arguments passed to PlotMode() containing
|
||||
the same number of d_vars as this mode and
|
||||
at most the same number of i_vars.
|
||||
"""
|
||||
is_default = False
|
||||
|
||||
"""
|
||||
All of the above attributes are defined in PlotMode.
|
||||
The following ones are specific to PlotModeBase.
|
||||
"""
|
||||
|
||||
"""
|
||||
A list of the render styles. Do not modify.
|
||||
"""
|
||||
styles = {'wireframe': 1, 'solid': 2, 'both': 3}
|
||||
|
||||
"""
|
||||
style_override
|
||||
Always use this style if not blank.
|
||||
"""
|
||||
style_override = ''
|
||||
|
||||
"""
|
||||
default_wireframe_color
|
||||
default_solid_color
|
||||
Can be used when color is None or being calculated.
|
||||
Used by PlotCurve and PlotSurface, but not anywhere
|
||||
in PlotModeBase.
|
||||
"""
|
||||
|
||||
default_wireframe_color = (0.85, 0.85, 0.85)
|
||||
default_solid_color = (0.6, 0.6, 0.9)
|
||||
default_rot_preset = 'xy'
|
||||
|
||||
##
|
||||
## Instance-Level Attributes
|
||||
##
|
||||
|
||||
## 'Abstract' member functions
|
||||
def _get_evaluator(self):
|
||||
if self.use_lambda_eval:
|
||||
try:
|
||||
e = self._get_lambda_evaluator()
|
||||
return e
|
||||
except Exception:
|
||||
warnings.warn("\nWarning: creating lambda evaluator failed. "
|
||||
"Falling back on SymPy subs evaluator.")
|
||||
return self._get_sympy_evaluator()
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_calculate_verts(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _on_calculate_cverts(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
## Base member functions
|
||||
def __init__(self, *args, bounds_callback=None, **kwargs):
|
||||
self.verts = []
|
||||
self.cverts = []
|
||||
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0]]
|
||||
self.cbounds = [[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0]]
|
||||
|
||||
self._draw_lock = RLock()
|
||||
|
||||
self._calculating_verts = Event()
|
||||
self._calculating_cverts = Event()
|
||||
self._calculating_verts_pos = 0.0
|
||||
self._calculating_verts_len = 0.0
|
||||
self._calculating_cverts_pos = 0.0
|
||||
self._calculating_cverts_len = 0.0
|
||||
|
||||
self._max_render_stack_size = 3
|
||||
self._draw_wireframe = [-1]
|
||||
self._draw_solid = [-1]
|
||||
|
||||
self._style = None
|
||||
self._color = None
|
||||
|
||||
self.predraw = []
|
||||
self.postdraw = []
|
||||
|
||||
self.use_lambda_eval = self.options.pop('use_sympy_eval', None) is None
|
||||
self.style = self.options.pop('style', '')
|
||||
self.color = self.options.pop('color', 'rainbow')
|
||||
self.bounds_callback = bounds_callback
|
||||
|
||||
self._on_calculate()
|
||||
|
||||
def synchronized(f):
|
||||
def w(self, *args, **kwargs):
|
||||
self._draw_lock.acquire()
|
||||
try:
|
||||
r = f(self, *args, **kwargs)
|
||||
return r
|
||||
finally:
|
||||
self._draw_lock.release()
|
||||
return w
|
||||
|
||||
@synchronized
|
||||
def push_wireframe(self, function):
|
||||
"""
|
||||
Push a function which performs gl commands
|
||||
used to build a display list. (The list is
|
||||
built outside of the function)
|
||||
"""
|
||||
assert callable(function)
|
||||
self._draw_wireframe.append(function)
|
||||
if len(self._draw_wireframe) > self._max_render_stack_size:
|
||||
del self._draw_wireframe[1] # leave marker element
|
||||
|
||||
@synchronized
|
||||
def push_solid(self, function):
|
||||
"""
|
||||
Push a function which performs gl commands
|
||||
used to build a display list. (The list is
|
||||
built outside of the function)
|
||||
"""
|
||||
assert callable(function)
|
||||
self._draw_solid.append(function)
|
||||
if len(self._draw_solid) > self._max_render_stack_size:
|
||||
del self._draw_solid[1] # leave marker element
|
||||
|
||||
def _create_display_list(self, function):
|
||||
dl = pgl.glGenLists(1)
|
||||
pgl.glNewList(dl, pgl.GL_COMPILE)
|
||||
function()
|
||||
pgl.glEndList()
|
||||
return dl
|
||||
|
||||
def _render_stack_top(self, render_stack):
|
||||
top = render_stack[-1]
|
||||
if top == -1:
|
||||
return -1 # nothing to display
|
||||
elif callable(top):
|
||||
dl = self._create_display_list(top)
|
||||
render_stack[-1] = (dl, top)
|
||||
return dl # display newly added list
|
||||
elif len(top) == 2:
|
||||
if pgl.GL_TRUE == pgl.glIsList(top[0]):
|
||||
return top[0] # display stored list
|
||||
dl = self._create_display_list(top[1])
|
||||
render_stack[-1] = (dl, top[1])
|
||||
return dl # display regenerated list
|
||||
|
||||
def _draw_solid_display_list(self, dl):
|
||||
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT)
|
||||
pgl.glPolygonMode(pgl.GL_FRONT_AND_BACK, pgl.GL_FILL)
|
||||
pgl.glCallList(dl)
|
||||
pgl.glPopAttrib()
|
||||
|
||||
def _draw_wireframe_display_list(self, dl):
|
||||
pgl.glPushAttrib(pgl.GL_ENABLE_BIT | pgl.GL_POLYGON_BIT)
|
||||
pgl.glPolygonMode(pgl.GL_FRONT_AND_BACK, pgl.GL_LINE)
|
||||
pgl.glEnable(pgl.GL_POLYGON_OFFSET_LINE)
|
||||
pgl.glPolygonOffset(-0.005, -50.0)
|
||||
pgl.glCallList(dl)
|
||||
pgl.glPopAttrib()
|
||||
|
||||
@synchronized
|
||||
def draw(self):
|
||||
for f in self.predraw:
|
||||
if callable(f):
|
||||
f()
|
||||
if self.style_override:
|
||||
style = self.styles[self.style_override]
|
||||
else:
|
||||
style = self.styles[self._style]
|
||||
# Draw solid component if style includes solid
|
||||
if style & 2:
|
||||
dl = self._render_stack_top(self._draw_solid)
|
||||
if dl > 0 and pgl.GL_TRUE == pgl.glIsList(dl):
|
||||
self._draw_solid_display_list(dl)
|
||||
# Draw wireframe component if style includes wireframe
|
||||
if style & 1:
|
||||
dl = self._render_stack_top(self._draw_wireframe)
|
||||
if dl > 0 and pgl.GL_TRUE == pgl.glIsList(dl):
|
||||
self._draw_wireframe_display_list(dl)
|
||||
for f in self.postdraw:
|
||||
if callable(f):
|
||||
f()
|
||||
|
||||
def _on_change_color(self, color):
|
||||
Thread(target=self._calculate_cverts).start()
|
||||
|
||||
def _on_calculate(self):
|
||||
Thread(target=self._calculate_all).start()
|
||||
|
||||
def _calculate_all(self):
|
||||
self._calculate_verts()
|
||||
self._calculate_cverts()
|
||||
|
||||
def _calculate_verts(self):
|
||||
if self._calculating_verts.is_set():
|
||||
return
|
||||
self._calculating_verts.set()
|
||||
try:
|
||||
self._on_calculate_verts()
|
||||
finally:
|
||||
self._calculating_verts.clear()
|
||||
if callable(self.bounds_callback):
|
||||
self.bounds_callback()
|
||||
|
||||
def _calculate_cverts(self):
|
||||
if self._calculating_verts.is_set():
|
||||
return
|
||||
while self._calculating_cverts.is_set():
|
||||
sleep(0) # wait for previous calculation
|
||||
self._calculating_cverts.set()
|
||||
try:
|
||||
self._on_calculate_cverts()
|
||||
finally:
|
||||
self._calculating_cverts.clear()
|
||||
|
||||
def _get_calculating_verts(self):
|
||||
return self._calculating_verts.is_set()
|
||||
|
||||
def _get_calculating_verts_pos(self):
|
||||
return self._calculating_verts_pos
|
||||
|
||||
def _get_calculating_verts_len(self):
|
||||
return self._calculating_verts_len
|
||||
|
||||
def _get_calculating_cverts(self):
|
||||
return self._calculating_cverts.is_set()
|
||||
|
||||
def _get_calculating_cverts_pos(self):
|
||||
return self._calculating_cverts_pos
|
||||
|
||||
def _get_calculating_cverts_len(self):
|
||||
return self._calculating_cverts_len
|
||||
|
||||
## Property handlers
|
||||
def _get_style(self):
|
||||
return self._style
|
||||
|
||||
@synchronized
|
||||
def _set_style(self, v):
|
||||
if v is None:
|
||||
return
|
||||
if v == '':
|
||||
step_max = 0
|
||||
for i in self.intervals:
|
||||
if i.v_steps is None:
|
||||
continue
|
||||
step_max = max([step_max, int(i.v_steps)])
|
||||
v = ['both', 'solid'][step_max > 40]
|
||||
if v not in self.styles:
|
||||
raise ValueError("v should be there in self.styles")
|
||||
if v == self._style:
|
||||
return
|
||||
self._style = v
|
||||
|
||||
def _get_color(self):
|
||||
return self._color
|
||||
|
||||
@synchronized
|
||||
def _set_color(self, v):
|
||||
try:
|
||||
if v is not None:
|
||||
if is_sequence(v):
|
||||
v = ColorScheme(*v)
|
||||
else:
|
||||
v = ColorScheme(v)
|
||||
if repr(v) == repr(self._color):
|
||||
return
|
||||
self._on_change_color(v)
|
||||
self._color = v
|
||||
except Exception as e:
|
||||
raise RuntimeError("Color change failed. "
|
||||
"Reason: %s" % (str(e)))
|
||||
|
||||
style = property(_get_style, _set_style)
|
||||
color = property(_get_color, _set_color)
|
||||
|
||||
calculating_verts = property(_get_calculating_verts)
|
||||
calculating_verts_pos = property(_get_calculating_verts_pos)
|
||||
calculating_verts_len = property(_get_calculating_verts_len)
|
||||
|
||||
calculating_cverts = property(_get_calculating_cverts)
|
||||
calculating_cverts_pos = property(_get_calculating_cverts_pos)
|
||||
calculating_cverts_len = property(_get_calculating_cverts_len)
|
||||
|
||||
## String representations
|
||||
|
||||
def __str__(self):
|
||||
f = ", ".join(str(d) for d in self.d_vars)
|
||||
o = "'mode=%s'" % (self.primary_alias)
|
||||
return ", ".join([f, o])
|
||||
|
||||
def __repr__(self):
|
||||
f = ", ".join(str(d) for d in self.d_vars)
|
||||
i = ", ".join(str(i) for i in self.intervals)
|
||||
d = [('mode', self.primary_alias),
|
||||
('color', str(self.color)),
|
||||
('style', str(self.style))]
|
||||
|
||||
o = "'%s'" % ("; ".join("%s=%s" % (k, v)
|
||||
for k, v in d if v != 'None'))
|
||||
return ", ".join([f, i, o])
|
||||
@@ -0,0 +1,209 @@
|
||||
from sympy.utilities.lambdify import lambdify
|
||||
from sympy.core.numbers import pi
|
||||
from sympy.functions import sin, cos
|
||||
from sympy.plotting.pygletplot.plot_curve import PlotCurve
|
||||
from sympy.plotting.pygletplot.plot_surface import PlotSurface
|
||||
|
||||
from math import sin as p_sin
|
||||
from math import cos as p_cos
|
||||
|
||||
|
||||
def float_vec3(f):
|
||||
def inner(*args):
|
||||
v = f(*args)
|
||||
return float(v[0]), float(v[1]), float(v[2])
|
||||
return inner
|
||||
|
||||
|
||||
class Cartesian2D(PlotCurve):
|
||||
i_vars, d_vars = 'x', 'y'
|
||||
intervals = [[-5, 5, 100]]
|
||||
aliases = ['cartesian']
|
||||
is_default = True
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fy = self.d_vars[0]
|
||||
x = self.t_interval.v
|
||||
|
||||
@float_vec3
|
||||
def e(_x):
|
||||
return (_x, fy.subs(x, _x), 0.0)
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fy = self.d_vars[0]
|
||||
x = self.t_interval.v
|
||||
return lambdify([x], [x, fy, 0.0])
|
||||
|
||||
|
||||
class Cartesian3D(PlotSurface):
|
||||
i_vars, d_vars = 'xy', 'z'
|
||||
intervals = [[-1, 1, 40], [-1, 1, 40]]
|
||||
aliases = ['cartesian', 'monge']
|
||||
is_default = True
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fz = self.d_vars[0]
|
||||
x = self.u_interval.v
|
||||
y = self.v_interval.v
|
||||
|
||||
@float_vec3
|
||||
def e(_x, _y):
|
||||
return (_x, _y, fz.subs(x, _x).subs(y, _y))
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fz = self.d_vars[0]
|
||||
x = self.u_interval.v
|
||||
y = self.v_interval.v
|
||||
return lambdify([x, y], [x, y, fz])
|
||||
|
||||
|
||||
class ParametricCurve2D(PlotCurve):
|
||||
i_vars, d_vars = 't', 'xy'
|
||||
intervals = [[0, 2*pi, 100]]
|
||||
aliases = ['parametric']
|
||||
is_default = True
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fx, fy = self.d_vars
|
||||
t = self.t_interval.v
|
||||
|
||||
@float_vec3
|
||||
def e(_t):
|
||||
return (fx.subs(t, _t), fy.subs(t, _t), 0.0)
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fx, fy = self.d_vars
|
||||
t = self.t_interval.v
|
||||
return lambdify([t], [fx, fy, 0.0])
|
||||
|
||||
|
||||
class ParametricCurve3D(PlotCurve):
|
||||
i_vars, d_vars = 't', 'xyz'
|
||||
intervals = [[0, 2*pi, 100]]
|
||||
aliases = ['parametric']
|
||||
is_default = True
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fx, fy, fz = self.d_vars
|
||||
t = self.t_interval.v
|
||||
|
||||
@float_vec3
|
||||
def e(_t):
|
||||
return (fx.subs(t, _t), fy.subs(t, _t), fz.subs(t, _t))
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fx, fy, fz = self.d_vars
|
||||
t = self.t_interval.v
|
||||
return lambdify([t], [fx, fy, fz])
|
||||
|
||||
|
||||
class ParametricSurface(PlotSurface):
|
||||
i_vars, d_vars = 'uv', 'xyz'
|
||||
intervals = [[-1, 1, 40], [-1, 1, 40]]
|
||||
aliases = ['parametric']
|
||||
is_default = True
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fx, fy, fz = self.d_vars
|
||||
u = self.u_interval.v
|
||||
v = self.v_interval.v
|
||||
|
||||
@float_vec3
|
||||
def e(_u, _v):
|
||||
return (fx.subs(u, _u).subs(v, _v),
|
||||
fy.subs(u, _u).subs(v, _v),
|
||||
fz.subs(u, _u).subs(v, _v))
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fx, fy, fz = self.d_vars
|
||||
u = self.u_interval.v
|
||||
v = self.v_interval.v
|
||||
return lambdify([u, v], [fx, fy, fz])
|
||||
|
||||
|
||||
class Polar(PlotCurve):
|
||||
i_vars, d_vars = 't', 'r'
|
||||
intervals = [[0, 2*pi, 100]]
|
||||
aliases = ['polar']
|
||||
is_default = False
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.t_interval.v
|
||||
|
||||
def e(_t):
|
||||
_r = float(fr.subs(t, _t))
|
||||
return (_r*p_cos(_t), _r*p_sin(_t), 0.0)
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.t_interval.v
|
||||
fx, fy = fr*cos(t), fr*sin(t)
|
||||
return lambdify([t], [fx, fy, 0.0])
|
||||
|
||||
|
||||
class Cylindrical(PlotSurface):
|
||||
i_vars, d_vars = 'th', 'r'
|
||||
intervals = [[0, 2*pi, 40], [-1, 1, 20]]
|
||||
aliases = ['cylindrical', 'polar']
|
||||
is_default = False
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.u_interval.v
|
||||
h = self.v_interval.v
|
||||
|
||||
def e(_t, _h):
|
||||
_r = float(fr.subs(t, _t).subs(h, _h))
|
||||
return (_r*p_cos(_t), _r*p_sin(_t), _h)
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.u_interval.v
|
||||
h = self.v_interval.v
|
||||
fx, fy = fr*cos(t), fr*sin(t)
|
||||
return lambdify([t, h], [fx, fy, h])
|
||||
|
||||
|
||||
class Spherical(PlotSurface):
|
||||
i_vars, d_vars = 'tp', 'r'
|
||||
intervals = [[0, 2*pi, 40], [0, pi, 20]]
|
||||
aliases = ['spherical']
|
||||
is_default = False
|
||||
|
||||
def _get_sympy_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.u_interval.v
|
||||
p = self.v_interval.v
|
||||
|
||||
def e(_t, _p):
|
||||
_r = float(fr.subs(t, _t).subs(p, _p))
|
||||
return (_r*p_cos(_t)*p_sin(_p),
|
||||
_r*p_sin(_t)*p_sin(_p),
|
||||
_r*p_cos(_p))
|
||||
return e
|
||||
|
||||
def _get_lambda_evaluator(self):
|
||||
fr = self.d_vars[0]
|
||||
t = self.u_interval.v
|
||||
p = self.v_interval.v
|
||||
fx = fr * cos(t) * sin(p)
|
||||
fy = fr * sin(t) * sin(p)
|
||||
fz = fr * cos(p)
|
||||
return lambdify([t, p], [fx, fy, fz])
|
||||
|
||||
Cartesian2D._register()
|
||||
Cartesian3D._register()
|
||||
ParametricCurve2D._register()
|
||||
ParametricCurve3D._register()
|
||||
ParametricSurface._register()
|
||||
Polar._register()
|
||||
Cylindrical._register()
|
||||
Spherical._register()
|
||||
@@ -0,0 +1,17 @@
|
||||
class PlotObject:
|
||||
"""
|
||||
Base class for objects which can be displayed in
|
||||
a Plot.
|
||||
"""
|
||||
visible = True
|
||||
|
||||
def _draw(self):
|
||||
if self.visible:
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
"""
|
||||
OpenGL rendering code for the plot object.
|
||||
Override in base class.
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,68 @@
|
||||
try:
|
||||
from ctypes import c_float
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import pyglet.gl as pgl
|
||||
from math import sqrt as _sqrt, acos as _acos, pi
|
||||
|
||||
|
||||
def cross(a, b):
|
||||
return (a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0])
|
||||
|
||||
|
||||
def dot(a, b):
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
|
||||
|
||||
def mag(a):
|
||||
return _sqrt(a[0]**2 + a[1]**2 + a[2]**2)
|
||||
|
||||
|
||||
def norm(a):
|
||||
m = mag(a)
|
||||
return (a[0] / m, a[1] / m, a[2] / m)
|
||||
|
||||
|
||||
def get_sphere_mapping(x, y, width, height):
|
||||
x = min([max([x, 0]), width])
|
||||
y = min([max([y, 0]), height])
|
||||
|
||||
sr = _sqrt((width/2)**2 + (height/2)**2)
|
||||
sx = ((x - width / 2) / sr)
|
||||
sy = ((y - height / 2) / sr)
|
||||
|
||||
sz = 1.0 - sx**2 - sy**2
|
||||
|
||||
if sz > 0.0:
|
||||
sz = _sqrt(sz)
|
||||
return (sx, sy, sz)
|
||||
else:
|
||||
sz = 0
|
||||
return norm((sx, sy, sz))
|
||||
|
||||
rad2deg = 180.0 / pi
|
||||
|
||||
|
||||
def get_spherical_rotatation(p1, p2, width, height, theta_multiplier):
|
||||
v1 = get_sphere_mapping(p1[0], p1[1], width, height)
|
||||
v2 = get_sphere_mapping(p2[0], p2[1], width, height)
|
||||
|
||||
d = min(max([dot(v1, v2), -1]), 1)
|
||||
|
||||
if abs(d - 1.0) < 0.000001:
|
||||
return None
|
||||
|
||||
raxis = norm( cross(v1, v2) )
|
||||
rtheta = theta_multiplier * rad2deg * _acos(d)
|
||||
|
||||
pgl.glPushMatrix()
|
||||
pgl.glLoadIdentity()
|
||||
pgl.glRotatef(rtheta, *raxis)
|
||||
mat = (c_float*16)()
|
||||
pgl.glGetFloatv(pgl.GL_MODELVIEW_MATRIX, mat)
|
||||
pgl.glPopMatrix()
|
||||
|
||||
return mat
|
||||
@@ -0,0 +1,102 @@
|
||||
import pyglet.gl as pgl
|
||||
|
||||
from sympy.core import S
|
||||
from sympy.plotting.pygletplot.plot_mode_base import PlotModeBase
|
||||
|
||||
|
||||
class PlotSurface(PlotModeBase):
|
||||
|
||||
default_rot_preset = 'perspective'
|
||||
|
||||
def _on_calculate_verts(self):
|
||||
self.u_interval = self.intervals[0]
|
||||
self.u_set = list(self.u_interval.frange())
|
||||
self.v_interval = self.intervals[1]
|
||||
self.v_set = list(self.v_interval.frange())
|
||||
self.bounds = [[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0]]
|
||||
evaluate = self._get_evaluator()
|
||||
|
||||
self._calculating_verts_pos = 0.0
|
||||
self._calculating_verts_len = float(
|
||||
self.u_interval.v_len*self.v_interval.v_len)
|
||||
|
||||
verts = []
|
||||
b = self.bounds
|
||||
for u in self.u_set:
|
||||
column = []
|
||||
for v in self.v_set:
|
||||
try:
|
||||
_e = evaluate(u, v) # calculate vertex
|
||||
except ZeroDivisionError:
|
||||
_e = None
|
||||
if _e is not None: # update bounding box
|
||||
for axis in range(3):
|
||||
b[axis][0] = min([b[axis][0], _e[axis]])
|
||||
b[axis][1] = max([b[axis][1], _e[axis]])
|
||||
column.append(_e)
|
||||
self._calculating_verts_pos += 1.0
|
||||
|
||||
verts.append(column)
|
||||
for axis in range(3):
|
||||
b[axis][2] = b[axis][1] - b[axis][0]
|
||||
if b[axis][2] == 0.0:
|
||||
b[axis][2] = 1.0
|
||||
|
||||
self.verts = verts
|
||||
self.push_wireframe(self.draw_verts(False, False))
|
||||
self.push_solid(self.draw_verts(False, True))
|
||||
|
||||
def _on_calculate_cverts(self):
|
||||
if not self.verts or not self.color:
|
||||
return
|
||||
|
||||
def set_work_len(n):
|
||||
self._calculating_cverts_len = float(n)
|
||||
|
||||
def inc_work_pos():
|
||||
self._calculating_cverts_pos += 1.0
|
||||
set_work_len(1)
|
||||
self._calculating_cverts_pos = 0
|
||||
self.cverts = self.color.apply_to_surface(self.verts,
|
||||
self.u_set,
|
||||
self.v_set,
|
||||
set_len=set_work_len,
|
||||
inc_pos=inc_work_pos)
|
||||
self.push_solid(self.draw_verts(True, True))
|
||||
|
||||
def calculate_one_cvert(self, u, v):
|
||||
vert = self.verts[u][v]
|
||||
return self.color(vert[0], vert[1], vert[2],
|
||||
self.u_set[u], self.v_set[v])
|
||||
|
||||
def draw_verts(self, use_cverts, use_solid_color):
|
||||
def f():
|
||||
for u in range(1, len(self.u_set)):
|
||||
pgl.glBegin(pgl.GL_QUAD_STRIP)
|
||||
for v in range(len(self.v_set)):
|
||||
pa = self.verts[u - 1][v]
|
||||
pb = self.verts[u][v]
|
||||
if pa is None or pb is None:
|
||||
pgl.glEnd()
|
||||
pgl.glBegin(pgl.GL_QUAD_STRIP)
|
||||
continue
|
||||
if use_cverts:
|
||||
ca = self.cverts[u - 1][v]
|
||||
cb = self.cverts[u][v]
|
||||
if ca is None:
|
||||
ca = (0, 0, 0)
|
||||
if cb is None:
|
||||
cb = (0, 0, 0)
|
||||
else:
|
||||
if use_solid_color:
|
||||
ca = cb = self.default_solid_color
|
||||
else:
|
||||
ca = cb = self.default_wireframe_color
|
||||
pgl.glColor3f(*ca)
|
||||
pgl.glVertex3f(*pa)
|
||||
pgl.glColor3f(*cb)
|
||||
pgl.glVertex3f(*pb)
|
||||
pgl.glEnd()
|
||||
return f
|
||||
@@ -0,0 +1,144 @@
|
||||
from time import perf_counter
|
||||
|
||||
|
||||
import pyglet.gl as pgl
|
||||
|
||||
from sympy.plotting.pygletplot.managed_window import ManagedWindow
|
||||
from sympy.plotting.pygletplot.plot_camera import PlotCamera
|
||||
from sympy.plotting.pygletplot.plot_controller import PlotController
|
||||
|
||||
|
||||
class PlotWindow(ManagedWindow):
|
||||
|
||||
def __init__(self, plot, antialiasing=True, ortho=False,
|
||||
invert_mouse_zoom=False, linewidth=1.5, caption="SymPy Plot",
|
||||
**kwargs):
|
||||
"""
|
||||
Named Arguments
|
||||
===============
|
||||
|
||||
antialiasing = True
|
||||
True OR False
|
||||
ortho = False
|
||||
True OR False
|
||||
invert_mouse_zoom = False
|
||||
True OR False
|
||||
"""
|
||||
self.plot = plot
|
||||
|
||||
self.camera = None
|
||||
self._calculating = False
|
||||
|
||||
self.antialiasing = antialiasing
|
||||
self.ortho = ortho
|
||||
self.invert_mouse_zoom = invert_mouse_zoom
|
||||
self.linewidth = linewidth
|
||||
self.title = caption
|
||||
self.last_caption_update = 0
|
||||
self.caption_update_interval = 0.2
|
||||
self.drawing_first_object = True
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def setup(self):
|
||||
self.camera = PlotCamera(self, ortho=self.ortho)
|
||||
self.controller = PlotController(self,
|
||||
invert_mouse_zoom=self.invert_mouse_zoom)
|
||||
self.push_handlers(self.controller)
|
||||
|
||||
pgl.glClearColor(1.0, 1.0, 1.0, 0.0)
|
||||
pgl.glClearDepth(1.0)
|
||||
|
||||
pgl.glDepthFunc(pgl.GL_LESS)
|
||||
pgl.glEnable(pgl.GL_DEPTH_TEST)
|
||||
|
||||
pgl.glEnable(pgl.GL_LINE_SMOOTH)
|
||||
pgl.glShadeModel(pgl.GL_SMOOTH)
|
||||
pgl.glLineWidth(self.linewidth)
|
||||
|
||||
pgl.glEnable(pgl.GL_BLEND)
|
||||
pgl.glBlendFunc(pgl.GL_SRC_ALPHA, pgl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
if self.antialiasing:
|
||||
pgl.glHint(pgl.GL_LINE_SMOOTH_HINT, pgl.GL_NICEST)
|
||||
pgl.glHint(pgl.GL_POLYGON_SMOOTH_HINT, pgl.GL_NICEST)
|
||||
|
||||
self.camera.setup_projection()
|
||||
|
||||
def on_resize(self, w, h):
|
||||
super().on_resize(w, h)
|
||||
if self.camera is not None:
|
||||
self.camera.setup_projection()
|
||||
|
||||
def update(self, dt):
|
||||
self.controller.update(dt)
|
||||
|
||||
def draw(self):
|
||||
self.plot._render_lock.acquire()
|
||||
self.camera.apply_transformation()
|
||||
|
||||
calc_verts_pos, calc_verts_len = 0, 0
|
||||
calc_cverts_pos, calc_cverts_len = 0, 0
|
||||
|
||||
should_update_caption = (perf_counter() - self.last_caption_update >
|
||||
self.caption_update_interval)
|
||||
|
||||
if len(self.plot._functions.values()) == 0:
|
||||
self.drawing_first_object = True
|
||||
|
||||
iterfunctions = iter(self.plot._functions.values())
|
||||
|
||||
for r in iterfunctions:
|
||||
if self.drawing_first_object:
|
||||
self.camera.set_rot_preset(r.default_rot_preset)
|
||||
self.drawing_first_object = False
|
||||
|
||||
pgl.glPushMatrix()
|
||||
r._draw()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
# might as well do this while we are
|
||||
# iterating and have the lock rather
|
||||
# than locking and iterating twice
|
||||
# per frame:
|
||||
|
||||
if should_update_caption:
|
||||
try:
|
||||
if r.calculating_verts:
|
||||
calc_verts_pos += r.calculating_verts_pos
|
||||
calc_verts_len += r.calculating_verts_len
|
||||
if r.calculating_cverts:
|
||||
calc_cverts_pos += r.calculating_cverts_pos
|
||||
calc_cverts_len += r.calculating_cverts_len
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
for r in self.plot._pobjects:
|
||||
pgl.glPushMatrix()
|
||||
r._draw()
|
||||
pgl.glPopMatrix()
|
||||
|
||||
if should_update_caption:
|
||||
self.update_caption(calc_verts_pos, calc_verts_len,
|
||||
calc_cverts_pos, calc_cverts_len)
|
||||
self.last_caption_update = perf_counter()
|
||||
|
||||
if self.plot._screenshot:
|
||||
self.plot._screenshot._execute_saving()
|
||||
|
||||
self.plot._render_lock.release()
|
||||
|
||||
def update_caption(self, calc_verts_pos, calc_verts_len,
|
||||
calc_cverts_pos, calc_cverts_len):
|
||||
caption = self.title
|
||||
if calc_verts_len or calc_cverts_len:
|
||||
caption += " (calculating"
|
||||
if calc_verts_len > 0:
|
||||
p = (calc_verts_pos / calc_verts_len) * 100
|
||||
caption += " vertices %i%%" % (p)
|
||||
if calc_cverts_len > 0:
|
||||
p = (calc_cverts_pos / calc_cverts_len) * 100
|
||||
caption += " colors %i%%" % (p)
|
||||
caption += ")"
|
||||
if self.caption != caption:
|
||||
self.set_caption(caption)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,88 @@
|
||||
from sympy.external.importtools import import_module
|
||||
|
||||
disabled = False
|
||||
|
||||
# if pyglet.gl fails to import, e.g. opengl is missing, we disable the tests
|
||||
pyglet_gl = import_module("pyglet.gl", catch=(OSError,))
|
||||
pyglet_window = import_module("pyglet.window", catch=(OSError,))
|
||||
if not pyglet_gl or not pyglet_window:
|
||||
disabled = True
|
||||
|
||||
|
||||
from sympy.core.symbol import symbols
|
||||
from sympy.functions.elementary.exponential import log
|
||||
from sympy.functions.elementary.trigonometric import (cos, sin)
|
||||
x, y, z = symbols('x, y, z')
|
||||
|
||||
|
||||
def test_plot_2d():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(x, [x, -5, 5, 4], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_2d_discontinuous():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(1/x, [x, -1, 1, 2], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_3d():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(x*y, [x, -5, 5, 5], [y, -5, 5, 5], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_3d_discontinuous():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(1/x, [x, -3, 3, 6], [y, -1, 1, 1], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_2d_polar():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(1/x, [x, -1, 1, 4], 'mode=polar', visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_3d_cylinder():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(
|
||||
1/y, [x, 0, 6.282, 4], [y, -1, 1, 4], 'mode=polar;style=solid',
|
||||
visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_3d_spherical():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(
|
||||
1, [x, 0, 6.282, 4], [y, 0, 3.141,
|
||||
4], 'mode=spherical;style=wireframe',
|
||||
visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_2d_parametric():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(sin(x), cos(x), [x, 0, 6.282, 4], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_3d_parametric():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(sin(x), cos(x), x/5.0, [x, 0, 6.282, 4], visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def _test_plot_log():
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
p = PygletPlot(log(x), [x, 0, 6.282, 4], 'mode=polar', visible=False)
|
||||
p.wait_for_calculations()
|
||||
|
||||
|
||||
def test_plot_integral():
|
||||
# Make sure it doesn't treat x as an independent variable
|
||||
from sympy.plotting.pygletplot import PygletPlot
|
||||
from sympy.integrals.integrals import Integral
|
||||
p = PygletPlot(Integral(z*x, (x, 1, z), (z, 1, y)), visible=False)
|
||||
p.wait_for_calculations()
|
||||
@@ -0,0 +1,188 @@
|
||||
try:
|
||||
from ctypes import c_float, c_int, c_double
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import pyglet.gl as pgl
|
||||
from sympy.core import S
|
||||
|
||||
|
||||
def get_model_matrix(array_type=c_float, glGetMethod=pgl.glGetFloatv):
|
||||
"""
|
||||
Returns the current modelview matrix.
|
||||
"""
|
||||
m = (array_type*16)()
|
||||
glGetMethod(pgl.GL_MODELVIEW_MATRIX, m)
|
||||
return m
|
||||
|
||||
|
||||
def get_projection_matrix(array_type=c_float, glGetMethod=pgl.glGetFloatv):
|
||||
"""
|
||||
Returns the current modelview matrix.
|
||||
"""
|
||||
m = (array_type*16)()
|
||||
glGetMethod(pgl.GL_PROJECTION_MATRIX, m)
|
||||
return m
|
||||
|
||||
|
||||
def get_viewport():
|
||||
"""
|
||||
Returns the current viewport.
|
||||
"""
|
||||
m = (c_int*4)()
|
||||
pgl.glGetIntegerv(pgl.GL_VIEWPORT, m)
|
||||
return m
|
||||
|
||||
|
||||
def get_direction_vectors():
|
||||
m = get_model_matrix()
|
||||
return ((m[0], m[4], m[8]),
|
||||
(m[1], m[5], m[9]),
|
||||
(m[2], m[6], m[10]))
|
||||
|
||||
|
||||
def get_view_direction_vectors():
|
||||
m = get_model_matrix()
|
||||
return ((m[0], m[1], m[2]),
|
||||
(m[4], m[5], m[6]),
|
||||
(m[8], m[9], m[10]))
|
||||
|
||||
|
||||
def get_basis_vectors():
|
||||
return ((1, 0, 0), (0, 1, 0), (0, 0, 1))
|
||||
|
||||
|
||||
def screen_to_model(x, y, z):
|
||||
m = get_model_matrix(c_double, pgl.glGetDoublev)
|
||||
p = get_projection_matrix(c_double, pgl.glGetDoublev)
|
||||
w = get_viewport()
|
||||
mx, my, mz = c_double(), c_double(), c_double()
|
||||
pgl.gluUnProject(x, y, z, m, p, w, mx, my, mz)
|
||||
return float(mx.value), float(my.value), float(mz.value)
|
||||
|
||||
|
||||
def model_to_screen(x, y, z):
|
||||
m = get_model_matrix(c_double, pgl.glGetDoublev)
|
||||
p = get_projection_matrix(c_double, pgl.glGetDoublev)
|
||||
w = get_viewport()
|
||||
mx, my, mz = c_double(), c_double(), c_double()
|
||||
pgl.gluProject(x, y, z, m, p, w, mx, my, mz)
|
||||
return float(mx.value), float(my.value), float(mz.value)
|
||||
|
||||
|
||||
def vec_subs(a, b):
|
||||
return tuple(a[i] - b[i] for i in range(len(a)))
|
||||
|
||||
|
||||
def billboard_matrix():
|
||||
"""
|
||||
Removes rotational components of
|
||||
current matrix so that primitives
|
||||
are always drawn facing the viewer.
|
||||
|
||||
|1|0|0|x|
|
||||
|0|1|0|x|
|
||||
|0|0|1|x| (x means left unchanged)
|
||||
|x|x|x|x|
|
||||
"""
|
||||
m = get_model_matrix()
|
||||
# XXX: for i in range(11): m[i] = i ?
|
||||
m[0] = 1
|
||||
m[1] = 0
|
||||
m[2] = 0
|
||||
m[4] = 0
|
||||
m[5] = 1
|
||||
m[6] = 0
|
||||
m[8] = 0
|
||||
m[9] = 0
|
||||
m[10] = 1
|
||||
pgl.glLoadMatrixf(m)
|
||||
|
||||
|
||||
def create_bounds():
|
||||
return [[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0],
|
||||
[S.Infinity, S.NegativeInfinity, 0]]
|
||||
|
||||
|
||||
def update_bounds(b, v):
|
||||
if v is None:
|
||||
return
|
||||
for axis in range(3):
|
||||
b[axis][0] = min([b[axis][0], v[axis]])
|
||||
b[axis][1] = max([b[axis][1], v[axis]])
|
||||
|
||||
|
||||
def interpolate(a_min, a_max, a_ratio):
|
||||
return a_min + a_ratio * (a_max - a_min)
|
||||
|
||||
|
||||
def rinterpolate(a_min, a_max, a_value):
|
||||
a_range = a_max - a_min
|
||||
if a_max == a_min:
|
||||
a_range = 1.0
|
||||
return (a_value - a_min) / float(a_range)
|
||||
|
||||
|
||||
def interpolate_color(color1, color2, ratio):
|
||||
return tuple(interpolate(color1[i], color2[i], ratio) for i in range(3))
|
||||
|
||||
|
||||
def scale_value(v, v_min, v_len):
|
||||
return (v - v_min) / v_len
|
||||
|
||||
|
||||
def scale_value_list(flist):
|
||||
v_min, v_max = min(flist), max(flist)
|
||||
v_len = v_max - v_min
|
||||
return [scale_value(f, v_min, v_len) for f in flist]
|
||||
|
||||
|
||||
def strided_range(r_min, r_max, stride, max_steps=50):
|
||||
o_min, o_max = r_min, r_max
|
||||
if abs(r_min - r_max) < 0.001:
|
||||
return []
|
||||
try:
|
||||
range(int(r_min - r_max))
|
||||
except (TypeError, OverflowError):
|
||||
return []
|
||||
if r_min > r_max:
|
||||
raise ValueError("r_min cannot be greater than r_max")
|
||||
r_min_s = (r_min % stride)
|
||||
r_max_s = stride - (r_max % stride)
|
||||
if abs(r_max_s - stride) < 0.001:
|
||||
r_max_s = 0.0
|
||||
r_min -= r_min_s
|
||||
r_max += r_max_s
|
||||
r_steps = int((r_max - r_min)/stride)
|
||||
if max_steps and r_steps > max_steps:
|
||||
return strided_range(o_min, o_max, stride*2)
|
||||
return [r_min] + [r_min + e*stride for e in range(1, r_steps + 1)] + [r_max]
|
||||
|
||||
|
||||
def parse_option_string(s):
|
||||
if not isinstance(s, str):
|
||||
return None
|
||||
options = {}
|
||||
for token in s.split(';'):
|
||||
pieces = token.split('=')
|
||||
if len(pieces) == 1:
|
||||
option, value = pieces[0], ""
|
||||
elif len(pieces) == 2:
|
||||
option, value = pieces
|
||||
else:
|
||||
raise ValueError("Plot option string '%s' is malformed." % (s))
|
||||
options[option.strip()] = value.strip()
|
||||
return options
|
||||
|
||||
|
||||
def dot_product(v1, v2):
|
||||
return sum(v1[i]*v2[i] for i in range(3))
|
||||
|
||||
|
||||
def vec_sub(v1, v2):
|
||||
return tuple(v1[i] - v2[i] for i in range(3))
|
||||
|
||||
|
||||
def vec_mag(v):
|
||||
return sum(v[i]**2 for i in range(3))**(0.5)
|
||||
Reference in New Issue
Block a user