Skip to content
This repository was archived by the owner on Feb 1, 2023. It is now read-only.

Commit 831e4e2

Browse files
author
Release Manager
committed
Trac #30628: Change font of axis labels in Three.js
#30614 allows the font for text3d to be changed in the Three.js viewer. This ticket proposes allowing the font for the viewer's axis labels to be changed as well. URL: https://trac.sagemath.org/30628 Reported by: gh-jcamp0x2a Ticket author(s): Joshua Campbell Reviewer(s): Eric Gourgoulhon
2 parents 92dcaf2 + 3e3381c commit 831e4e2

File tree

4 files changed

+199
-75
lines changed

4 files changed

+199
-75
lines changed

src/doc/en/reference/plot3d/threejs.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ Options currently supported by the viewer:
3434
- ``axes_labels`` -- (default: ['x','y','z']) list or tuple of three strings;
3535
set to False to remove all labels
3636

37+
- ``axes_labels_style`` -- (default: None) list of three dicts, one per axis, or
38+
a single dict controlling all three axes; supports the same styling options as
39+
:func:`~sage.plot.plot3d.shapes2.text3d` such as ``color``, ``opacity``, ``fontsize``,
40+
``fontweight``, ``fontstyle``, and ``fontfamily``
41+
3742
- ``color`` -- (default: 'blue') color of the 3D object
3843

3944
- ``decimals`` -- (default: 2) integer determining decimals displayed in labels

src/sage/ext_data/threejs/threejs_template.html

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -98,41 +98,56 @@
9898
var d = options.decimals; // decimals
9999
var offsetRatio = 0.1;
100100
var al = options.axesLabels;
101+
var als = options.axesLabelsStyle || [{}, {}, {}];
101102

102103
var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
103104
var xm = xMid.toFixed(d);
104105
if ( /^-0.?0*$/.test(xm) ) xm = xm.substr(1);
105-
addLabel( al[0] + '=' + xm, a[0]*xMid, a[1]*b[1].y+offset, a[2]*b[0].z );
106-
addLabel( ( b[0].x ).toFixed(d), a[0]*b[0].x, a[1]*b[1].y+offset, a[2]*b[0].z );
107-
addLabel( ( b[1].x ).toFixed(d), a[0]*b[1].x, a[1]*b[1].y+offset, a[2]*b[0].z );
106+
addLabel( al[0] + '=' + xm, a[0]*xMid, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );
107+
addLabel( ( b[0].x ).toFixed(d), a[0]*b[0].x, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );
108+
addLabel( ( b[1].x ).toFixed(d), a[0]*b[1].x, a[1]*b[1].y+offset, a[2]*b[0].z, als[0] );
108109

109110
var offset = offsetRatio * a[0]*( b[1].x - b[0].x );
110111
var ym = yMid.toFixed(d);
111112
if ( /^-0.?0*$/.test(ym) ) ym = ym.substr(1);
112-
addLabel( al[1] + '=' + ym, a[0]*b[1].x+offset, a[1]*yMid, a[2]*b[0].z );
113-
addLabel( ( b[0].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[0].y, a[2]*b[0].z );
114-
addLabel( ( b[1].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[1].y, a[2]*b[0].z );
113+
addLabel( al[1] + '=' + ym, a[0]*b[1].x+offset, a[1]*yMid, a[2]*b[0].z, als[1] );
114+
addLabel( ( b[0].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[0].y, a[2]*b[0].z, als[1] );
115+
addLabel( ( b[1].y ).toFixed(d), a[0]*b[1].x+offset, a[1]*b[1].y, a[2]*b[0].z, als[1] );
115116

116117
var offset = offsetRatio * a[1]*( b[1].y - b[0].y );
117118
var zm = zMid.toFixed(d);
118119
if ( /^-0.?0*$/.test(zm) ) zm = zm.substr(1);
119-
addLabel( al[2] + '=' + zm, a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*zMid );
120-
addLabel( ( b[0].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[0].z );
121-
addLabel( ( b[1].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[1].z );
120+
addLabel( al[2] + '=' + zm, a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*zMid, als[2] );
121+
addLabel( ( b[0].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[0].z, als[2] );
122+
addLabel( ( b[1].z ).toFixed(d), a[0]*b[1].x, a[1]*b[0].y-offset, a[2]*b[1].z, als[2] );
122123

123124
}
124125

125-
function addLabel( text, x, y, z, color='black', fontSize=14, fontFamily='monospace',
126-
fontStyle='normal', fontWeight='normal', opacity=1 ) {
126+
function addLabel( text, x, y, z, style ) {
127+
128+
var color = style.color || 'black';
129+
var fontSize = style.fontSize || 14;
130+
var fontFamily = style.fontFamily || 'monospace';
131+
var fontStyle = style.fontStyle || 'normal';
132+
var fontWeight = style.fontWeight || 'normal';
133+
var opacity = style.opacity || 1;
127134

128135
if ( options.theme === 'dark' )
129136
if ( color === 'black' || color === '#000000' )
130137
color = 'white';
131138

139+
if ( Array.isArray( fontStyle ) ) {
140+
fontFamily = fontFamily.map( function( f ) {
141+
// Need to put quotes around fonts that have whitespace in their names.
142+
return /\s/.test( f ) ? '"' + f + '"' : f;
143+
}).join(', ');
144+
}
145+
132146
var canvas = document.createElement( 'canvas' );
133147
var context = canvas.getContext( '2d' );
134148
var pixelRatio = Math.round( window.devicePixelRatio );
135149

150+
// For example: italic bold 20px "Times New Roman", Georgia, serif
136151
var font = [fontStyle, fontWeight, fontSize + 'px', fontFamily].join(' ');
137152

138153
context.font = font;
@@ -278,13 +293,7 @@
278293
for ( var i=0 ; i < texts.length ; i++ ) addText( texts[i] );
279294

280295
function addText( json ) {
281-
json.fontFamily = json.fontFamily.map( function( f ) {
282-
// Need to put quotes around fonts that have whitespace in their names.
283-
return /\s/.test( f ) ? '"' + f + '"' : f;
284-
}).join(', ');
285-
var sprite = addLabel( json.text, a[0]*json.x, a[1]*json.y, a[2]*json.z, json.color,
286-
json.fontSize, json.fontFamily, json.fontStyle, json.fontWeight,
287-
json.opacity );
296+
var sprite = addLabel( json.text, a[0]*json.x, a[1]*json.y, a[2]*json.z, json );
288297
sprite.userData = json;
289298
}
290299

src/sage/plot/plot3d/base.pyx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ cdef class Graphics3d(SageObject):
417417
js_options['autoPlay'] = options.get('auto_play', True)
418418
js_options['axes'] = options.get('axes', False)
419419
js_options['axesLabels'] = options.get('axes_labels', ['x','y','z'])
420+
js_options['axesLabelsStyle'] = options.get('axes_labels_style')
420421
js_options['decimals'] = options.get('decimals', 2)
421422
js_options['delay'] = options.get('delay', 20)
422423
js_options['frame'] = options.get('frame', True)
@@ -455,6 +456,21 @@ cdef class Graphics3d(SageObject):
455456

456457
if not js_options['frame']:
457458
js_options['axesLabels'] = False
459+
js_options['axesLabelsStyle'] = None
460+
461+
if js_options['axesLabelsStyle'] is not None:
462+
from .shapes import _validate_threejs_text_style
463+
style = js_options['axesLabelsStyle']
464+
if isinstance(style, dict):
465+
style = _validate_threejs_text_style(style)
466+
style = [style, style, style]
467+
elif isinstance(style, list) and len(style) == 3 and all([isinstance(s, dict) for s in style]):
468+
style = [_validate_threejs_text_style(s) for s in style]
469+
else:
470+
import warnings
471+
warnings.warn("axes_labels_style must be a dict or a list of 3 dicts")
472+
style = [dict(), dict(), dict()]
473+
js_options['axesLabelsStyle'] = style
458474

459475
from sage.repl.rich_output import get_display_manager
460476
scripts = get_display_manager().threejs_scripts(options['online'])

src/sage/plot/plot3d/shapes.pyx

Lines changed: 151 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,65 +1209,10 @@ class Text(PrimitiveObject):
12091209
center = (float(0), float(0), float(0))
12101210
if render_params.transform is not None:
12111211
center = render_params.transform.transform_point(center)
1212-
12131212
color = '#' + str(self.texture.hex_rgb())
12141213
string = str(self.string)
1215-
1216-
default_size = 14.0
1217-
size = self._extra_kwds.get('fontsize', default_size)
1218-
try:
1219-
size = float(size)
1220-
except (TypeError, ValueError):
1221-
scale = str(size).lower()
1222-
if scale.endswith('%'):
1223-
try:
1224-
scale = float(scale[:-1]) / 100.0
1225-
size = default_size * scale
1226-
except ValueError:
1227-
import warnings
1228-
warnings.warn(f"invalid fontsize: {size}, using: {default_size}")
1229-
size = default_size
1230-
else:
1231-
from matplotlib.font_manager import font_scalings
1232-
try:
1233-
size = default_size * font_scalings[scale]
1234-
except KeyError:
1235-
import warnings
1236-
warnings.warn(f"unknown fontsize: {size}, using: {default_size}")
1237-
size = default_size
1238-
1239-
font = self._extra_kwds.get('fontfamily', ['monospace'])
1240-
if isinstance(font, str):
1241-
font = font.split(',')
1242-
font = [str(f).strip() for f in font]
1243-
1244-
default_style = 'normal'
1245-
style = str(self._extra_kwds.get('fontstyle', default_style))
1246-
if style not in ['normal', 'italic'] and not style.startswith('oblique'): # ex: oblique 30deg
1247-
import warnings
1248-
warnings.warn(f"unknown style: {style}, using: {default_style}")
1249-
style = default_style
1250-
1251-
default_weight = 'normal'
1252-
weight = self._extra_kwds.get('fontweight', default_weight)
1253-
if weight not in ['normal', 'bold']:
1254-
try:
1255-
weight = int(weight)
1256-
except:
1257-
from matplotlib.font_manager import weight_dict
1258-
try:
1259-
weight = weight_dict[weight]
1260-
except KeyError:
1261-
import warnings
1262-
warnings.warn(f"unknown fontweight: {weight}, using: {default_weight}")
1263-
weight = default_weight
1264-
1265-
opacity = float(self._extra_kwds.get('opacity', 1.0))
1266-
1267-
text = dict(text=string, x=center[0], y=center[1], z=center[2], color=color,
1268-
fontSize=size, fontFamily=font, fontStyle=style, fontWeight=weight,
1269-
opacity=opacity)
1270-
1214+
text = _validate_threejs_text_style(self._extra_kwds)
1215+
text.update(dict(text=string, x=center[0], y=center[1], z=center[2], color=color))
12711216
return [('text', text)]
12721217

12731218
def bounding_box(self):
@@ -1279,3 +1224,152 @@ class Text(PrimitiveObject):
12791224
((0, 0, 0), (0, 0, 0))
12801225
"""
12811226
return (0,0,0), (0,0,0)
1227+
1228+
1229+
def _validate_threejs_text_style(style):
1230+
"""
1231+
Validate a combination of text styles for use in the Three.js viewer.
1232+
1233+
INPUT:
1234+
1235+
- ``style`` -- a dict optionally containing keys: 'color', 'fontSize',
1236+
'fontFamily', 'fontStyle', 'fontWeight', and 'opacity'
1237+
1238+
OUTPUT:
1239+
1240+
A corrected version of the dict, having printed out warning messages for
1241+
any problems found
1242+
1243+
TESTS:
1244+
1245+
Default values::
1246+
1247+
sage: from sage.plot.plot3d.shapes import _validate_threejs_text_style as validate
1248+
sage: validate(dict())
1249+
{'color': '#000000',
1250+
'fontFamily': ['monospace'],
1251+
'fontSize': 14.0,
1252+
'fontStyle': 'normal',
1253+
'fontWeight': 'normal',
1254+
'opacity': 1.0}
1255+
1256+
Color by name or by HTML hex code::
1257+
1258+
sage: validate(dict(color='red'))
1259+
{'color': '#ff0000',...}
1260+
sage: validate(dict(color='#ff0000'))
1261+
{'color': '#ff0000',...}
1262+
sage: validate(dict(color='octarine'))
1263+
...UserWarning: invalid color: octarine...
1264+
1265+
Font size in absolute units or in percentage/keyword relative to default::
1266+
1267+
sage: validate(dict(fontsize=20.5))
1268+
{...'fontSize': 20.5...}
1269+
sage: validate(dict(fontsize="200%"))
1270+
{...'fontSize': 28...}
1271+
sage: validate(dict(fontsize="x%"))
1272+
...UserWarning: invalid fontsize: x%...
1273+
sage: validate(dict(fontsize="large"))
1274+
{...'fontSize': 16.8...}
1275+
sage: validate(dict(fontsize="ginormous"))
1276+
...UserWarning: unknown fontsize: ginormous...
1277+
1278+
Font family as list or comma-delimited string::
1279+
1280+
sage: validate(dict(fontfamily=[" Times New Roman ", " Georgia", "serif "]))
1281+
{...'fontFamily': ['Times New Roman', 'Georgia', 'serif']...}
1282+
sage: validate(dict(fontfamily=" Times New Roman , Georgia,serif "))
1283+
{...'fontFamily': ['Times New Roman', 'Georgia', 'serif']...}
1284+
1285+
Font style keywords including the special syntax for 'oblique' angle::
1286+
1287+
sage: validate(dict(fontstyle='italic'))
1288+
{...'fontStyle': 'italic'...}
1289+
sage: validate(dict(fontstyle='oblique 30deg'))
1290+
{...'fontStyle': 'oblique 30deg'...}
1291+
sage: validate(dict(fontstyle='garrish'))
1292+
...UserWarning: unknown style: garrish...
1293+
1294+
Font weight as keyword or integer::
1295+
1296+
sage: validate(dict(fontweight='bold'))
1297+
{...'fontWeight': 'bold'...}
1298+
sage: validate(dict(fontweight=500))
1299+
{...'fontWeight': 500...}
1300+
sage: validate(dict(fontweight='roman'))
1301+
{...'fontWeight': 500...}
1302+
sage: validate(dict(fontweight='bold & beautiful'))
1303+
...UserWarning: unknown fontweight: bold & beautiful...
1304+
1305+
Opacity::
1306+
1307+
sage: validate(dict(opacity=0.5))
1308+
{...'opacity': 0.5}
1309+
1310+
"""
1311+
default_color = '#000000' # black
1312+
color = style.get('color', default_color)
1313+
from .texture import Texture
1314+
try:
1315+
texture = Texture(color=color)
1316+
except ValueError:
1317+
import warnings
1318+
warnings.warn(f"invalid color: {color}, using: {default_color}")
1319+
color = default_color
1320+
else:
1321+
color = '#' + str(texture.hex_rgb())
1322+
1323+
default_size = 14.0
1324+
size = style.get('fontsize', default_size)
1325+
try:
1326+
size = float(size)
1327+
except (TypeError, ValueError):
1328+
scale = str(size).lower()
1329+
if scale.endswith('%'):
1330+
try:
1331+
scale = float(scale[:-1]) / 100.0
1332+
size = default_size * scale
1333+
except ValueError:
1334+
import warnings
1335+
warnings.warn(f"invalid fontsize: {size}, using: {default_size}")
1336+
size = default_size
1337+
else:
1338+
from matplotlib.font_manager import font_scalings
1339+
try:
1340+
size = default_size * font_scalings[scale]
1341+
except KeyError:
1342+
import warnings
1343+
warnings.warn(f"unknown fontsize: {size}, using: {default_size}")
1344+
size = default_size
1345+
1346+
font = style.get('fontfamily', ['monospace'])
1347+
if isinstance(font, str):
1348+
font = font.split(',')
1349+
font = [str(f).strip() for f in font]
1350+
1351+
default_style = 'normal'
1352+
fontstyle = str(style.get('fontstyle', default_style))
1353+
if fontstyle not in ['normal', 'italic'] and not fontstyle.startswith('oblique'): # ex: oblique 30deg
1354+
import warnings
1355+
warnings.warn(f"unknown style: {fontstyle}, using: {default_style}")
1356+
fontstyle = default_style
1357+
1358+
default_weight = 'normal'
1359+
weight = style.get('fontweight', default_weight)
1360+
if weight not in ['normal', 'bold']:
1361+
try:
1362+
weight = int(weight)
1363+
except:
1364+
from matplotlib.font_manager import weight_dict
1365+
try:
1366+
weight = weight_dict[weight]
1367+
except KeyError:
1368+
import warnings
1369+
warnings.warn(f"unknown fontweight: {weight}, using: {default_weight}")
1370+
weight = default_weight
1371+
1372+
opacity = float(style.get('opacity', 1.0))
1373+
1374+
return dict(color=color, fontSize=size, fontFamily=font, fontStyle=fontstyle,
1375+
fontWeight=weight, opacity=opacity)

0 commit comments

Comments
 (0)