chore: 添加虚拟环境到仓库
- 添加 backend_service/venv 虚拟环境 - 包含所有Python依赖包 - 注意:虚拟环境约393MB,包含12655个文件
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)
|
||||
@@ -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)
|
||||
@@ -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