prefig 0.2.15.dev20250514053750__py3-none-any.whl → 0.5.6.dev20260130060411__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefig/cli.py +46 -26
- prefig/core/CTM.py +18 -3
- prefig/core/__init__.py +2 -0
- prefig/core/annotations.py +115 -2
- prefig/core/area.py +20 -4
- prefig/core/arrow.py +9 -3
- prefig/core/axes.py +909 -0
- prefig/core/circle.py +13 -4
- prefig/core/clip.py +1 -1
- prefig/core/coordinates.py +31 -4
- prefig/core/diagram.py +129 -6
- prefig/core/graph.py +236 -23
- prefig/core/grid_axes.py +181 -495
- prefig/core/image.py +127 -0
- prefig/core/label.py +14 -4
- prefig/core/legend.py +4 -4
- prefig/core/line.py +92 -1
- prefig/core/math_utilities.py +155 -0
- prefig/core/network.py +2 -2
- prefig/core/parametric_curve.py +15 -3
- prefig/core/path.py +25 -0
- prefig/core/point.py +18 -2
- prefig/core/polygon.py +7 -1
- prefig/core/read.py +6 -3
- prefig/core/rectangle.py +10 -4
- prefig/core/repeat.py +37 -3
- prefig/core/shape.py +12 -1
- prefig/core/slope_field.py +142 -0
- prefig/core/tags.py +8 -2
- prefig/core/tangent_line.py +36 -12
- prefig/core/user_namespace.py +8 -0
- prefig/core/utilities.py +9 -1
- prefig/engine.py +73 -28
- prefig/resources/diagcess/diagcess.js +7 -1
- prefig/resources/schema/pf_schema.rnc +89 -6
- prefig/resources/schema/pf_schema.rng +321 -5
- prefig/scripts/install_mj.py +10 -11
- {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/METADATA +12 -16
- prefig-0.5.6.dev20260130060411.dist-info/RECORD +68 -0
- prefig-0.2.15.dev20250514053750.dist-info/RECORD +0 -66
- {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/LICENSE +0 -0
- {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/WHEEL +0 -0
- {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/entry_points.txt +0 -0
prefig/core/rectangle.py
CHANGED
|
@@ -71,10 +71,15 @@ def rectangle(element, diagram, parent, outline_status):
|
|
|
71
71
|
path.set('d', ' '.join(cmds))
|
|
72
72
|
|
|
73
73
|
if diagram.output_format() == 'tactile':
|
|
74
|
-
|
|
74
|
+
stroke = element.get('stroke')
|
|
75
|
+
fill = element.get('fill')
|
|
76
|
+
if stroke is not None and stroke != 'none':
|
|
75
77
|
element.set('stroke', 'black')
|
|
76
|
-
if
|
|
77
|
-
|
|
78
|
+
if fill is not None:
|
|
79
|
+
if fill.strip().lower() != 'none':
|
|
80
|
+
element.set('fill', 'lightgray')
|
|
81
|
+
else:
|
|
82
|
+
element.set('fill', 'none')
|
|
78
83
|
else:
|
|
79
84
|
util.set_attr(element, 'stroke', 'none')
|
|
80
85
|
util.set_attr(element, 'fill', 'none')
|
|
@@ -97,5 +102,6 @@ def finish_outline(element, diagram, parent):
|
|
|
97
102
|
diagram.finish_outline(element,
|
|
98
103
|
element.get('stroke'),
|
|
99
104
|
element.get('thickness'),
|
|
100
|
-
element.get('fill', 'none'),
|
|
105
|
+
# element.get('fill', 'none'),
|
|
106
|
+
'none',
|
|
101
107
|
parent)
|
prefig/core/repeat.py
CHANGED
|
@@ -2,6 +2,7 @@ import lxml.etree as ET
|
|
|
2
2
|
import logging
|
|
3
3
|
from . import user_namespace as un
|
|
4
4
|
import copy
|
|
5
|
+
import re
|
|
5
6
|
import numpy as np
|
|
6
7
|
from . import group
|
|
7
8
|
from . import label
|
|
@@ -9,6 +10,25 @@ from . import utilities
|
|
|
9
10
|
|
|
10
11
|
log = logging.getLogger('prefigure')
|
|
11
12
|
|
|
13
|
+
# EPUB restricts the characters that can appear in an id to
|
|
14
|
+
# a-z|A-Z|0-9|_|-
|
|
15
|
+
# This is an issue here since <repeat> elements can create id's
|
|
16
|
+
# This regular expression checks characters to see if they are allowed
|
|
17
|
+
epub_check = re.compile(r'[A-Za-z0-9_-]')
|
|
18
|
+
|
|
19
|
+
# We also define substitutions for the most common disallowed characters
|
|
20
|
+
epub_dict = {'(': 'p',
|
|
21
|
+
')': 'q',
|
|
22
|
+
'[': 'p',
|
|
23
|
+
']': 'q',
|
|
24
|
+
'{': 'p',
|
|
25
|
+
'}': 'q',
|
|
26
|
+
',': 'c',
|
|
27
|
+
'.': 'd',
|
|
28
|
+
'=': '_',
|
|
29
|
+
r'#': 'h'}
|
|
30
|
+
|
|
31
|
+
|
|
12
32
|
# Allows a block of XML to repeat with a changing parameter
|
|
13
33
|
|
|
14
34
|
def repeat(element, diagram, parent, outline_status):
|
|
@@ -40,7 +60,10 @@ def repeat(element, diagram, parent, outline_status):
|
|
|
40
60
|
if outline is not None:
|
|
41
61
|
element.set('outline', outline)
|
|
42
62
|
if id is not None:
|
|
63
|
+
if not id.startswith('pf__'):
|
|
64
|
+
id = 'pf__' + id
|
|
43
65
|
element.set('id', id)
|
|
66
|
+
element_cp.set('id', id)
|
|
44
67
|
|
|
45
68
|
for num, k in enumerate(iterator):
|
|
46
69
|
if isinstance(k, np.ndarray):
|
|
@@ -48,11 +71,12 @@ def repeat(element, diagram, parent, outline_status):
|
|
|
48
71
|
else:
|
|
49
72
|
k_str = str(k)
|
|
50
73
|
|
|
51
|
-
|
|
74
|
+
k_str_clean = epub_clean(k_str)
|
|
75
|
+
# This is a change since we use the syntax "var_str" for the id suffix
|
|
52
76
|
if count:
|
|
53
|
-
suffix_str = var + "
|
|
77
|
+
suffix_str = var + "_" + k_str_clean
|
|
54
78
|
else:
|
|
55
|
-
suffix_str = var + "
|
|
79
|
+
suffix_str = var + "_" + str(num)
|
|
56
80
|
|
|
57
81
|
definition = ET.SubElement(element, 'definition')
|
|
58
82
|
definition.text = var + '=' + k_str
|
|
@@ -78,3 +102,13 @@ def repeat(element, diagram, parent, outline_status):
|
|
|
78
102
|
|
|
79
103
|
if annotation is not None:
|
|
80
104
|
diagram.pop_from_annotation_branch()
|
|
105
|
+
|
|
106
|
+
def epub_clean(s):
|
|
107
|
+
epub_clean = [bool(epub_check.fullmatch(ch)) for ch in s]
|
|
108
|
+
chars = []
|
|
109
|
+
for index, ch in enumerate(s):
|
|
110
|
+
if epub_clean[index]:
|
|
111
|
+
chars.append(ch)
|
|
112
|
+
continue
|
|
113
|
+
chars.append(epub_dict.get(ch, '_'))
|
|
114
|
+
return "".join(chars)
|
prefig/core/shape.py
CHANGED
|
@@ -36,7 +36,10 @@ def define(element, diagram, parent, outline_status):
|
|
|
36
36
|
log.error(f"In <define-shapes>, {child.tag} does not define a shape")
|
|
37
37
|
continue
|
|
38
38
|
if child.get('at', None) is not None:
|
|
39
|
-
|
|
39
|
+
id = child.get('at')
|
|
40
|
+
if not id.startswith('pf__'):
|
|
41
|
+
id = 'pf__' + id
|
|
42
|
+
child.set('id', id)
|
|
40
43
|
dummy_parent = ET.Element('group')
|
|
41
44
|
# this is kind of a hack, but we only need to construct the shape
|
|
42
45
|
# so we stash the format temporarily in case we're building a
|
|
@@ -69,6 +72,12 @@ def shape(element, diagram, parent, outline_status):
|
|
|
69
72
|
return
|
|
70
73
|
|
|
71
74
|
shape_refs = [r.strip() for r in reference.split(',')]
|
|
75
|
+
shape_edit = []
|
|
76
|
+
for shape_ref in shape_refs:
|
|
77
|
+
if not shape_ref.startswith('pf__'):
|
|
78
|
+
shape_ref = 'pf__' + shape_ref
|
|
79
|
+
shape_edit.append(shape_ref)
|
|
80
|
+
shape_refs = shape_edit
|
|
72
81
|
shapes = []
|
|
73
82
|
for ref in shape_refs:
|
|
74
83
|
shapes.append(diagram.recall_shape(ref))
|
|
@@ -82,6 +91,8 @@ def shape(element, diagram, parent, outline_status):
|
|
|
82
91
|
operation = 'union'
|
|
83
92
|
else:
|
|
84
93
|
path = ET.SubElement(parent, 'use')
|
|
94
|
+
if not reference.startswith('pf__'):
|
|
95
|
+
reference = 'pf__' + reference
|
|
85
96
|
path.set('href', r'#' + reference)
|
|
86
97
|
|
|
87
98
|
if operation is not None:
|
prefig/core/slope_field.py
CHANGED
|
@@ -8,8 +8,10 @@ from . import utilities
|
|
|
8
8
|
from . import grid_axes
|
|
9
9
|
from . import group
|
|
10
10
|
from . import math_utilities as math_util
|
|
11
|
+
from . import calculus
|
|
11
12
|
|
|
12
13
|
log = logging.getLogger('prefigure')
|
|
14
|
+
np.seterr(divide="ignore", invalid="ignore")
|
|
13
15
|
|
|
14
16
|
# Add a graphical element for slope fields
|
|
15
17
|
def slope_field(element, diagram, parent, outline_status):
|
|
@@ -24,6 +26,8 @@ def slope_field(element, diagram, parent, outline_status):
|
|
|
24
26
|
return
|
|
25
27
|
bbox = diagram.bbox()
|
|
26
28
|
|
|
29
|
+
if element.get('id', None) is None:
|
|
30
|
+
diagram.add_id(element, None)
|
|
27
31
|
# We're going to turn this element into a group and add lines to it
|
|
28
32
|
element.tag = "group"
|
|
29
33
|
if element.get('outline', 'no') == 'yes':
|
|
@@ -112,3 +116,141 @@ def slope_field(element, diagram, parent, outline_status):
|
|
|
112
116
|
|
|
113
117
|
group.group(element, diagram, parent, outline_status)
|
|
114
118
|
|
|
119
|
+
# Add a graphical element for slope fields
|
|
120
|
+
def vector_field(element, diagram, parent, outline_status):
|
|
121
|
+
if outline_status == 'finish_outline':
|
|
122
|
+
finish_outline(element, diagram, parent)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
f = un.valid_eval(element.get('function'))
|
|
127
|
+
except:
|
|
128
|
+
log.error(f"Error retrieving slope-field function={element.get('function')}")
|
|
129
|
+
return
|
|
130
|
+
bbox = diagram.bbox()
|
|
131
|
+
|
|
132
|
+
if element.get('id', None) is None:
|
|
133
|
+
diagram.add_id(element, None)
|
|
134
|
+
|
|
135
|
+
# We're going to turn this element into a group and add lines to it
|
|
136
|
+
element.tag = "group"
|
|
137
|
+
if element.get('outline', 'no') == 'yes':
|
|
138
|
+
element.set('outline', 'always')
|
|
139
|
+
|
|
140
|
+
# Now we'll construct a line with all the graphical information
|
|
141
|
+
# and make copies of it
|
|
142
|
+
line_template = ET.Element('line')
|
|
143
|
+
|
|
144
|
+
if diagram.output_format() == 'tactile':
|
|
145
|
+
line_template.set('stroke', 'black')
|
|
146
|
+
else:
|
|
147
|
+
line_template.set('stroke', element.get('stroke', 'blue'))
|
|
148
|
+
line_template.set('thickness', element.get('thickness', '2'))
|
|
149
|
+
line_template.set('arrows', '1')
|
|
150
|
+
|
|
151
|
+
if element.get('arrow-width', None) is not None:
|
|
152
|
+
line_template.set('arrow-width', element.get('arrow-width'))
|
|
153
|
+
if element.get('arrow-angles', None) is not None:
|
|
154
|
+
line_template.set('arrow-angles', element.get('arrow-angles'))
|
|
155
|
+
|
|
156
|
+
field_data = []
|
|
157
|
+
if element.get('curve', None) is not None:
|
|
158
|
+
curve = un.valid_eval(element.get('curve'))
|
|
159
|
+
try:
|
|
160
|
+
domain = un.valid_eval(element.get('domain'))
|
|
161
|
+
except:
|
|
162
|
+
log.error('A @domain is needed if adding a vector field to a curve')
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
N = un.valid_eval(element.get('N'))
|
|
166
|
+
except:
|
|
167
|
+
log.error('A @N is needed if adding a vector field to a curve')
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
t = domain[0]
|
|
171
|
+
# if "f" a function of t or (x,y)?
|
|
172
|
+
one_variable = True
|
|
173
|
+
try:
|
|
174
|
+
f(t)
|
|
175
|
+
except TypeError:
|
|
176
|
+
one_variable = False
|
|
177
|
+
|
|
178
|
+
dt = (domain[1]-domain[0])/(N-1)
|
|
179
|
+
for _ in range(N):
|
|
180
|
+
position = curve(t)
|
|
181
|
+
if one_variable:
|
|
182
|
+
field_data.append([position, f(t)])
|
|
183
|
+
else:
|
|
184
|
+
field_data.append([position, f(*position)])
|
|
185
|
+
t += dt
|
|
186
|
+
scale_factor = un.valid_eval(element.get('scale', '1'))
|
|
187
|
+
|
|
188
|
+
else:
|
|
189
|
+
spacings = element.get('spacings', None)
|
|
190
|
+
if spacings is not None:
|
|
191
|
+
try:
|
|
192
|
+
spacings = un.valid_eval(spacings)
|
|
193
|
+
rx, ry = spacings
|
|
194
|
+
except:
|
|
195
|
+
log.error(f"Error parsing slope-field attribute @spacings={element.get('spacings')}")
|
|
196
|
+
return
|
|
197
|
+
else:
|
|
198
|
+
rx = grid_axes.find_gridspacing((bbox[0], bbox[2]))
|
|
199
|
+
ry = grid_axes.find_gridspacing((bbox[1], bbox[3]))
|
|
200
|
+
|
|
201
|
+
# we will go through and generate the vectors first
|
|
202
|
+
# since we'll need to scale them
|
|
203
|
+
max_scale = 0
|
|
204
|
+
exponent = un.valid_eval(element.get('exponent', '1'))
|
|
205
|
+
x = rx[0]
|
|
206
|
+
while x <= rx[2]:
|
|
207
|
+
y = ry[0]
|
|
208
|
+
while y <= ry[2]:
|
|
209
|
+
f_value = f(x, y)
|
|
210
|
+
if any(np.isnan(f_value)):
|
|
211
|
+
y += ry[1]
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if len(f_value) != 2:
|
|
216
|
+
log.error("Only two-dimensional vector fields are supported")
|
|
217
|
+
return;
|
|
218
|
+
except:
|
|
219
|
+
pass
|
|
220
|
+
norm = math_util.length(f_value)
|
|
221
|
+
if norm < 1e-10:
|
|
222
|
+
f_value = np.array((0,0))
|
|
223
|
+
else:
|
|
224
|
+
# we will scale the length by length**exponent
|
|
225
|
+
# to promote the length of shorter vectors
|
|
226
|
+
f_value = norm**exponent * (1/norm * f_value)
|
|
227
|
+
max_scale = max(max_scale,
|
|
228
|
+
abs((f_value[0])/rx[1]),
|
|
229
|
+
abs((f_value[1])/ry[1]))
|
|
230
|
+
field_data.append([np.array([x,y]), f_value])
|
|
231
|
+
y += ry[1]
|
|
232
|
+
x += rx[1]
|
|
233
|
+
|
|
234
|
+
scale_factor = min(1, 0.75 / max_scale)
|
|
235
|
+
if element.get('scale') is not None:
|
|
236
|
+
scale = un.valid_eval(element.get('scale'))
|
|
237
|
+
scale_factor = scale
|
|
238
|
+
|
|
239
|
+
for datum in field_data:
|
|
240
|
+
p, v = datum
|
|
241
|
+
v = scale_factor * v
|
|
242
|
+
# is this long enough to add?
|
|
243
|
+
tail = p
|
|
244
|
+
tip = p+v
|
|
245
|
+
p0 = diagram.transform(tail)
|
|
246
|
+
p1 = diagram.transform(tip)
|
|
247
|
+
if math_util.distance(p0, p1) < 2:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
line_el = copy.deepcopy(line_template)
|
|
251
|
+
line_el.set('p1', utilities.pt2long_str(tail, spacer=','))
|
|
252
|
+
line_el.set('p2', utilities.pt2long_str(tip, spacer=','))
|
|
253
|
+
element.append(line_el)
|
|
254
|
+
|
|
255
|
+
group.group(element, diagram, parent, outline_status)
|
|
256
|
+
|
prefig/core/tags.py
CHANGED
|
@@ -2,6 +2,7 @@ import logging
|
|
|
2
2
|
import lxml.etree as ET
|
|
3
3
|
from . import annotations
|
|
4
4
|
from . import area
|
|
5
|
+
from . import axes
|
|
5
6
|
from . import clip
|
|
6
7
|
from . import circle
|
|
7
8
|
from . import coordinates
|
|
@@ -10,6 +11,7 @@ from . import definition
|
|
|
10
11
|
from . import graph
|
|
11
12
|
from . import grid_axes
|
|
12
13
|
from . import group
|
|
14
|
+
from . import image
|
|
13
15
|
from . import implicit
|
|
14
16
|
from . import label
|
|
15
17
|
from . import legend
|
|
@@ -36,10 +38,11 @@ tag_dict = {
|
|
|
36
38
|
'arc': circle.arc,
|
|
37
39
|
'area-between-curves': area.area_between_curves,
|
|
38
40
|
'area-under-curve': area.area_under_curve,
|
|
39
|
-
'axes':
|
|
41
|
+
'axes': axes.axes,
|
|
40
42
|
'caption': label.caption,
|
|
41
43
|
'circle': circle.circle,
|
|
42
44
|
'clip': clip.clip,
|
|
45
|
+
'contour': implicit.implicit_curve,
|
|
43
46
|
'coordinates': coordinates.coordinates,
|
|
44
47
|
'definition': definition.definition,
|
|
45
48
|
'derivative': definition.derivative,
|
|
@@ -49,6 +52,7 @@ tag_dict = {
|
|
|
49
52
|
'grid-axes': grid_axes.grid_axes,
|
|
50
53
|
'group': group.group,
|
|
51
54
|
'histogram': statistics.histogram,
|
|
55
|
+
'image': image.image,
|
|
52
56
|
'implicit-curve': implicit.implicit_curve,
|
|
53
57
|
'label': label.label,
|
|
54
58
|
'legend': legend.legend,
|
|
@@ -61,6 +65,7 @@ tag_dict = {
|
|
|
61
65
|
'rectangle': rectangle.rectangle,
|
|
62
66
|
'repeat': repeat.repeat,
|
|
63
67
|
'riemann-sum': riemann_sum.riemann_sum,
|
|
68
|
+
'tick-mark': axes.tick_mark,
|
|
64
69
|
'transform': CTM.transform_group,
|
|
65
70
|
'rotate': CTM.transform_rotate,
|
|
66
71
|
'scale': CTM.transform_scale,
|
|
@@ -70,7 +75,8 @@ tag_dict = {
|
|
|
70
75
|
'tangent-line': tangent_line.tangent,
|
|
71
76
|
'translate': CTM.transform_translate,
|
|
72
77
|
'triangle': polygon.triangle,
|
|
73
|
-
'vector': vector.vector
|
|
78
|
+
'vector': vector.vector,
|
|
79
|
+
'vector-field': slope_field.vector_field
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
log = logging.getLogger('prefigure')
|
prefig/core/tangent_line.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import lxml.etree as ET
|
|
3
|
+
import numpy as np
|
|
2
4
|
from . import user_namespace as un
|
|
3
5
|
from . import utilities as util
|
|
4
6
|
from . import calculus
|
|
@@ -31,6 +33,10 @@ def tangent(element, diagram, parent, outline_status):
|
|
|
31
33
|
def tangent(x):
|
|
32
34
|
return y0 + m*(x-a)
|
|
33
35
|
|
|
36
|
+
name = element.get('name', None)
|
|
37
|
+
if name is not None:
|
|
38
|
+
un.enter_namespace(name, tangent)
|
|
39
|
+
|
|
34
40
|
# determine the interval over which we'll draw the tangent line
|
|
35
41
|
bbox = diagram.bbox()
|
|
36
42
|
domain = element.get('domain', None)
|
|
@@ -39,19 +45,38 @@ def tangent(element, diagram, parent, outline_status):
|
|
|
39
45
|
else:
|
|
40
46
|
domain = un.valid_eval(domain)
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
scales = diagram.get_scales()
|
|
43
49
|
x1, x2 = domain
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if scales[0] == 'linear' and scales[1] == 'linear':
|
|
51
|
+
# find the endpoints of the tangent line
|
|
52
|
+
y1 = tangent(x1)
|
|
53
|
+
y2 = tangent(x2)
|
|
54
|
+
p1 = (x1, y1)
|
|
55
|
+
p2 = (x2, y2)
|
|
56
|
+
if element.get('infinite') == 'yes' or element.get('domain') is None:
|
|
57
|
+
p1, p2 = line.infinite_line(p1, p2, diagram)
|
|
58
|
+
if p1 is None:
|
|
59
|
+
return
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
# construct the graphical line element from those points and attributes
|
|
62
|
+
line_el = line.mk_line(p1, p2, diagram, element.get('id'))
|
|
63
|
+
|
|
64
|
+
else:
|
|
65
|
+
line_el = ET.Element('path')
|
|
66
|
+
if scales[0] == 'log':
|
|
67
|
+
x_positions = np.logspace(np.log10(x1), np.log(x2), 101)
|
|
68
|
+
else:
|
|
69
|
+
x_positions = np.linspace(x1, x2, 101)
|
|
70
|
+
cmds = []
|
|
71
|
+
next_cmd = 'M'
|
|
72
|
+
for x in x_positions:
|
|
73
|
+
y = tangent(x)
|
|
74
|
+
if y < 0 and scales[1] == 'log':
|
|
75
|
+
next_cmd = 'M'
|
|
76
|
+
continue
|
|
77
|
+
cmds += [next_cmd, util.pt2str(diagram.transform((x, y)))]
|
|
78
|
+
next_cmd = 'L'
|
|
79
|
+
line_el.set('d', ' '.join(cmds))
|
|
55
80
|
|
|
56
81
|
if diagram.output_format() == 'tactile':
|
|
57
82
|
element.set('stroke', 'black')
|
|
@@ -60,7 +85,6 @@ def tangent(element, diagram, parent, outline_status):
|
|
|
60
85
|
util.set_attr(element, 'thickness', '2')
|
|
61
86
|
|
|
62
87
|
util.add_attr(line_el, util.get_1d_attr(element))
|
|
63
|
-
# line_el.set('type', 'tangent line')
|
|
64
88
|
element.set('cliptobbox', 'yes')
|
|
65
89
|
util.cliptobbox(line_el, element, diagram)
|
|
66
90
|
|
prefig/core/user_namespace.py
CHANGED
|
@@ -24,6 +24,7 @@ __delta_on = False
|
|
|
24
24
|
functions = {x for x in dir(math) + dir(math_utilities) if not "__" in x}.difference({'e', 'pi'})
|
|
25
25
|
functions.add('max')
|
|
26
26
|
functions.add('min')
|
|
27
|
+
functions.add('round')
|
|
27
28
|
variables = {'e', 'pi', 'inf'}
|
|
28
29
|
|
|
29
30
|
# Transforms an AST by wrapping any List or Tuple inside a numpy array
|
|
@@ -66,6 +67,8 @@ def validate_node(node, args=None):
|
|
|
66
67
|
return True
|
|
67
68
|
if isinstance(node, ast.Expression):
|
|
68
69
|
return validate_node(node.body, args)
|
|
70
|
+
if isinstance(node, ast.Starred):
|
|
71
|
+
return validate_node(node.value)
|
|
69
72
|
if isinstance(node, ast.Name):
|
|
70
73
|
if node.id in variables:
|
|
71
74
|
return True
|
|
@@ -170,6 +173,11 @@ def derivative(f, name):
|
|
|
170
173
|
functions.add(name)
|
|
171
174
|
variables.add(name)
|
|
172
175
|
|
|
176
|
+
def enter_function(name, f):
|
|
177
|
+
globals()[name] = f
|
|
178
|
+
functions.add(name)
|
|
179
|
+
variables.add(name)
|
|
180
|
+
|
|
173
181
|
def enter_namespace(name, value):
|
|
174
182
|
globals()[name] = value
|
|
175
183
|
variables.add(name)
|
prefig/core/utilities.py
CHANGED
|
@@ -2,6 +2,14 @@ import numpy as np
|
|
|
2
2
|
from . import user_namespace as un
|
|
3
3
|
from . import label
|
|
4
4
|
|
|
5
|
+
import logging
|
|
6
|
+
logger = logging.getLogger('prefigure')
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
with warnings.catch_warnings():
|
|
10
|
+
warnings.filterwarnings('ignore', 'legacy print')
|
|
11
|
+
np.set_printoptions(legacy="1.25")
|
|
12
|
+
|
|
5
13
|
colors = {'gray': r'#777', 'lightgray': r'#ccc', 'darkgray': r'#333'}
|
|
6
14
|
|
|
7
15
|
# Some utilities to handle XML elements
|
|
@@ -20,7 +28,7 @@ def get_attr(element, attr, default):
|
|
|
20
28
|
try:
|
|
21
29
|
attribute = un.valid_eval(element.get(attr, default))
|
|
22
30
|
if isinstance(attribute, np.ndarray):
|
|
23
|
-
return
|
|
31
|
+
return ','.join([float2longstr(a) for a in attribute])
|
|
24
32
|
return str(attribute)
|
|
25
33
|
except (TypeError, SyntaxError): # this is a string that's not in the namespace
|
|
26
34
|
return element.get(attr, default)
|
prefig/engine.py
CHANGED
|
@@ -40,7 +40,7 @@ def build(
|
|
|
40
40
|
):
|
|
41
41
|
pub_requested = not ignore_publication and publication is not None
|
|
42
42
|
path = Path(filename)
|
|
43
|
-
if path.suffix
|
|
43
|
+
if path.suffix == '':
|
|
44
44
|
filename = str(path.parent / (path.stem + '.xml'))
|
|
45
45
|
|
|
46
46
|
# We're going to look for a publication, possibly in a parent directory
|
|
@@ -84,20 +84,40 @@ def build(
|
|
|
84
84
|
def build_from_string(format, input_string, environment="pyodide"):
|
|
85
85
|
tree = ET.fromstring(input_string)
|
|
86
86
|
log.setLevel(logging.DEBUG)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
ns = {'pf': 'https://prefigure.org'}
|
|
88
|
+
diagrams_with_ns = tree.xpath('//pf:diagram', namespaces=ns)
|
|
89
|
+
diagrams_without_ns = tree.xpath('//diagram', namespaces=ns)
|
|
90
|
+
diagrams = diagrams_with_ns + diagrams_without_ns
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
diagram = diagrams[0]
|
|
94
|
+
except:
|
|
95
|
+
return ''
|
|
96
|
+
|
|
97
|
+
# put all elements in default namespace
|
|
98
|
+
for elem in diagram.getiterator():
|
|
99
|
+
# Skip comments and processing instructions,
|
|
100
|
+
# because they do not have names
|
|
101
|
+
if not (
|
|
102
|
+
isinstance(elem, ET._Comment)
|
|
103
|
+
or isinstance(elem, ET._ProcessingInstruction)
|
|
104
|
+
):
|
|
105
|
+
# Remove a namespace URI in the element's name
|
|
106
|
+
elem.tag = ET.QName(elem).localname
|
|
107
|
+
|
|
108
|
+
core.parse.check_duplicate_handles(diagram, set())
|
|
109
|
+
|
|
110
|
+
output_string = core.parse.mk_diagram(
|
|
111
|
+
diagram,
|
|
112
|
+
format,
|
|
113
|
+
None, # publication file
|
|
114
|
+
"prefig", # filename needed for label generation
|
|
115
|
+
False, # supress caption
|
|
116
|
+
None, # diagram number
|
|
117
|
+
environment,
|
|
118
|
+
return_string = True
|
|
119
|
+
)
|
|
120
|
+
return output_string
|
|
101
121
|
|
|
102
122
|
def pdf(
|
|
103
123
|
format,
|
|
@@ -121,23 +141,35 @@ def pdf(
|
|
|
121
141
|
filename = Path(filename)
|
|
122
142
|
|
|
123
143
|
if filename.suffix != '.svg':
|
|
124
|
-
filename = filename.parent / (filename.
|
|
144
|
+
filename = filename.parent / (filename.stem + '.svg')
|
|
125
145
|
|
|
126
146
|
if build_path is None:
|
|
127
147
|
filename_str = str(filename)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
|
|
149
|
+
# fist look in current directory
|
|
150
|
+
cwd_files = os.listdir('.')
|
|
151
|
+
if filename_str in cwd_files:
|
|
152
|
+
build_path = filename
|
|
153
|
+
else:
|
|
154
|
+
if 'output' in cwd_files:
|
|
155
|
+
output_files = os.listdir('output')
|
|
156
|
+
if filename_str in output_files:
|
|
157
|
+
build_path = 'output' / filename
|
|
158
|
+
|
|
159
|
+
if build_path is None:
|
|
160
|
+
for dir, dirs, files in os.walk(os.getcwd()):
|
|
161
|
+
files = set(files)
|
|
162
|
+
if filename_str in files:
|
|
163
|
+
build_path = dir / filename
|
|
132
164
|
if build_path is None:
|
|
133
|
-
log.
|
|
165
|
+
log.error(f"Unable to find {filename}")
|
|
134
166
|
return
|
|
135
167
|
|
|
136
168
|
dpi = str(dpi)
|
|
137
169
|
executable = shutil.which('rsvg-convert')
|
|
138
170
|
if executable is None:
|
|
139
|
-
log.
|
|
140
|
-
log.
|
|
171
|
+
log.error("rsvg-convert is required to create PDFs.")
|
|
172
|
+
log.error("See the installation instructions at https://prefigure.org")
|
|
141
173
|
return
|
|
142
174
|
|
|
143
175
|
log.info(f"Converting {build_path} to PDF")
|
|
@@ -184,12 +216,25 @@ def png(
|
|
|
184
216
|
|
|
185
217
|
if build_path is None:
|
|
186
218
|
filename_str = str(filename)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
219
|
+
# look first in current directory
|
|
220
|
+
cwd_files = os.listdir('.')
|
|
221
|
+
if filename_str in cwd_files:
|
|
222
|
+
build_path = filename
|
|
223
|
+
else:
|
|
224
|
+
# now look in output
|
|
225
|
+
if 'output' in cwd_files:
|
|
226
|
+
output_files = os.listdir('output')
|
|
227
|
+
if filename_str in output_files:
|
|
228
|
+
build_path = 'output' / filename
|
|
229
|
+
|
|
230
|
+
# we still haven't found it so descend in the file system
|
|
231
|
+
if build_path is None:
|
|
232
|
+
for dir, dirs, files in os.walk(os.getcwd()):
|
|
233
|
+
files = set(files)
|
|
234
|
+
if filename_str in files:
|
|
235
|
+
build_path = dir / filename
|
|
191
236
|
if build_path is None:
|
|
192
|
-
log.
|
|
237
|
+
log.error(f"Unable to find {filename}")
|
|
193
238
|
return
|
|
194
239
|
|
|
195
240
|
log.info(f"Converting {build_path} to PDF")
|