Skip to content

Commit

Permalink
Trac #22670: Make Three.js work offline redux
Browse files Browse the repository at this point in the history
The Three.js scripts used by various backends for offline viewing should
be generated by methods in the backends, not in `plot3d/base.pyx`.

Continuation of discussion after #22488 was closed.

URL: https://trac.sagemath.org/22670
Reported by: paulmasson
Ticket author(s): Paul Masson, Andrey Novoseltsev
Reviewer(s): Andrey Novoseltsev, Paul Masson
  • Loading branch information
Release Manager authored and vbraun committed Apr 7, 2017
2 parents fc8ed14 + 668eeee commit e399a53
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 44 deletions.
57 changes: 16 additions & 41 deletions src/sage/plot/plot3d/base.pyx
Expand Up @@ -356,49 +356,23 @@ cdef class Graphics3d(SageObject):
EXAMPLES::
sage: sphere()._rich_repr_threejs()
sage: sphere(online=True)._rich_repr_threejs()
OutputSceneThreejs container
"""
options = {}
options['aspect_ratio'] = [float(i) for i in kwds.get('aspect_ratio', [1,1,1])]
options['axes'] = kwds.get('axes', False)
options['axes_labels'] = kwds.get('axes_labels', ['x','y','z'])
options['decimals'] = int(kwds.get('decimals', 2))
options['frame'] = kwds.get('frame', True)
options['online'] = kwds.get('online', False)
options = self._process_viewing_options(kwds)
# Threejs specific options
options.setdefault('axes_labels', ['x','y','z'])
options.setdefault('decimals', 2)
options.setdefault('online', False)
# Normalization of options values for proper JSONing
options['aspect_ratio'] = [float(i) for i in options['aspect_ratio']]
options['decimals'] = int(options['decimals'])

if not options['frame']:
options['axes_labels'] = False

from sage.repl.rich_output import get_display_manager
backend = get_display_manager()._backend
from sage.repl.rich_output.backend_sagenb import BackendSageNB
if isinstance(backend, BackendSageNB):
options['online'] = True

if options['online']:
scripts = ( """
<script src="https://cdn.rawgit.com/mrdoob/three.js/r80/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/r80/examples/js/controls/OrbitControls.js"></script>
""" )
else:
from sage.repl.rich_output.backend_ipython import BackendIPythonNotebook
if isinstance(backend, BackendIPythonNotebook):
scripts = ( """
<script src="/nbextensions/threejs/three.min.js"></script>
<script src="/nbextensions/threejs/OrbitControls.js"></script>
<script>
if ( !window.THREE ) document.write('\
<script src="https://cdn.rawgit.com/mrdoob/three.js/r80/build/three.min.js"><\/script>\
<script src="https://cdn.rawgit.com/mrdoob/three.js/r80/examples/js/controls/OrbitControls.js"><\/script>');
</script>
""" )
else:
from sage.env import SAGE_SHARE
scripts = ( """
<script src="{0}/threejs/three.min.js"></script>
<script src="{0}/threejs/OrbitControls.js"></script>
""".format( SAGE_SHARE ) )
scripts = get_display_manager().threejs_scripts(options['online'])

b = self.bounding_box()
bounds = '[{{"x":{}, "y":{}, "z":{}}}, {{"x":{}, "y":{}, "z":{}}}]'.format(
Expand Down Expand Up @@ -440,13 +414,14 @@ cdef class Graphics3d(SageObject):
surfaces = '[' + ','.join(surfaces) + ']'

from sage.env import SAGE_EXTCODE
filename = os.path.join(SAGE_EXTCODE, 'threejs', 'threejs_template.html')
f = open(filename, 'r')
html = f.read()
f.close()
with open(os.path.join(
SAGE_EXTCODE, 'threejs', 'threejs_template.html')) as f:
html = f.read()

html = html.replace('SAGE_SCRIPTS', scripts)
html = html.replace('SAGE_OPTIONS', json.dumps(options))
js_options = dict((key, options[key]) for key in
['aspect_ratio', 'axes', 'axes_labels', 'decimals', 'frame'])
html = html.replace('SAGE_OPTIONS', json.dumps(js_options))
html = html.replace('SAGE_BOUNDS', bounds)
html = html.replace('SAGE_LIGHTS', lights)
html = html.replace('SAGE_AMBIENT', ambient)
Expand Down
4 changes: 2 additions & 2 deletions src/sage/repl/rich_output/backend_base.py
@@ -1,9 +1,9 @@
# -*- encoding: utf-8 -*-
r"""
Base class for Backends
Base Class for Backends
The display backends are the commandline, the SageNB notebook, the
ipython notebook, the Emacs sage mode, the Sage doctester, .... All of
IPython notebook, the Emacs sage mode, the Sage doctester, .... All of
these have different capabilities for what they can display.
To implement a new display backend, you need to subclass
Expand Down
46 changes: 45 additions & 1 deletion src/sage/repl/rich_output/backend_ipython.py
Expand Up @@ -396,6 +396,27 @@ def is_in_terminal(self):
"""
return True

def threejs_offline_scripts(self):
"""
Three.js scripts for the IPython command line
OUTPUT:
String containing script tags
EXAMPLES::
sage: from sage.repl.rich_output.backend_ipython import BackendIPythonCommandline
sage: backend = BackendIPythonCommandline()
sage: backend.threejs_offline_scripts()
'...<script ...</script>...'
"""
from sage.env import SAGE_SHARE
return """
<script src="{0}/threejs/three.min.js"></script>
<script src="{0}/threejs/OrbitControls.js"></script>
""".format(SAGE_SHARE)


IFRAME_TEMPLATE = \
"""
Expand Down Expand Up @@ -554,4 +575,27 @@ def displayhook(self, plain_text, rich_output):
else:
raise TypeError('rich_output type not supported')


def threejs_offline_scripts(self):
"""
Three.js scripts for the IPython notebook
OUTPUT:
String containing script tags
EXAMPLES::
sage: from sage.repl.rich_output.backend_ipython import BackendIPythonNotebook
sage: backend = BackendIPythonNotebook()
sage: backend.threejs_offline_scripts()
'...<script src="/nbextensions/threejs/three.min...<\\/script>...'
"""
from sage.repl.rich_output import get_display_manager
CDN_scripts = get_display_manager().threejs_scripts(online=True)
return """
<script src="/nbextensions/threejs/three.min.js"></script>
<script src="/nbextensions/threejs/OrbitControls.js"></script>
<script>
if ( !window.THREE ) document.write('{}');
</script>
""".format(CDN_scripts.replace('</script>', r'<\/script>'))
18 changes: 18 additions & 0 deletions src/sage/repl/rich_output/backend_sagenb.py
Expand Up @@ -452,3 +452,21 @@ def embed_video(self, video_output):
url='cell://' + filename,
link_attrs='class="file_link"',
))

def threejs_offline_scripts(self):
"""
Three.js scripts for the Sage notebook
OUTPUT:
String containing script tags
EXAMPLES::
sage: from sage.repl.rich_output.backend_sagenb import BackendSageNB
sage: backend = BackendSageNB()
sage: backend.threejs_offline_scripts()
'...<script ...</script>...'
"""
from sage.repl.rich_output import get_display_manager
return get_display_manager().threejs_scripts(online=True)
42 changes: 42 additions & 0 deletions src/sage/repl/rich_output/display_manager.py
Expand Up @@ -713,6 +713,48 @@ def graphics_from_save(self, save_function, save_kwds,
buf = OutputBuffer.from_file(filename)
return output_container(buf)

def threejs_scripts(self, online):
"""
Return Three.js script tags for the current backend.
INPUT:
- ``online`` -- Boolean determining script usage context
OUTPUT:
String containing script tags
.. NOTE::
This base method handles ``online=True`` case only, serving CDN
script tags. Location of scripts for offline usage is
backend-specific.
EXAMPLES::
sage: from sage.repl.rich_output import get_display_manager
sage: get_display_manager().threejs_scripts(online=True)
'...<script src="https://cdn.rawgit.com/mrdoob/three.js/...'
sage: get_display_manager().threejs_scripts(online=False)
Traceback (most recent call last):
...
ValueError: current backend does not support
offline threejs graphics
"""
if online:
from sage.misc.package import installed_packages
version = installed_packages()['threejs']
return """
<script src="https://cdn.rawgit.com/mrdoob/three.js/{0}/build/three.min.js"></script>
<script src="https://cdn.rawgit.com/mrdoob/three.js/{0}/examples/js/controls/OrbitControls.js"></script>
""".format(version)
try:
return self._backend.threejs_offline_scripts()
except AttributeError:
raise ValueError(
'current backend does not support offline threejs graphics')

def supported_output(self):
"""
Return the output container classes that can be used.
Expand Down

0 comments on commit e399a53

Please sign in to comment.