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/cli.py
CHANGED
|
@@ -79,12 +79,17 @@ def init():
|
|
|
79
79
|
# on Windows and linux
|
|
80
80
|
wd = os.getcwd()
|
|
81
81
|
os.chdir(destination)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
log.
|
|
82
|
+
|
|
83
|
+
npm_cmd = shutil.which("npm")
|
|
84
|
+
if npm_cmd is None:
|
|
85
|
+
log.error("Cannot find npm to install MathJax for PreFigure")
|
|
86
|
+
else:
|
|
87
|
+
log.info(f"Installing MathJax libraries in {destination}")
|
|
88
|
+
try:
|
|
89
|
+
subprocess.run([npm_cmd, "install"]) #, f"--prefix={destination}"])
|
|
90
|
+
except:
|
|
91
|
+
log.warning("MathJax installation failed. Is npm installed on your system?")
|
|
92
|
+
log.setLevel(log_level)
|
|
88
93
|
os.chdir(wd)
|
|
89
94
|
|
|
90
95
|
log.info("Installing the Braille29 font")
|
|
@@ -347,21 +352,6 @@ def view(filename, ignore_annotations, port):
|
|
|
347
352
|
diagcess_dir = dir
|
|
348
353
|
break
|
|
349
354
|
|
|
350
|
-
# if we didn't find the diagcess tools, we'll install them here
|
|
351
|
-
if diagcess_dir is None:
|
|
352
|
-
prefig_root = Path(__file__).parent
|
|
353
|
-
source = prefig_root / 'resources' / 'diagcess'
|
|
354
|
-
cwd = Path(os.getcwd())
|
|
355
|
-
|
|
356
|
-
shutil.copytree(
|
|
357
|
-
source,
|
|
358
|
-
cwd,
|
|
359
|
-
dirs_exist_ok = True
|
|
360
|
-
)
|
|
361
|
-
diagcess_dir = cwd
|
|
362
|
-
|
|
363
|
-
diagcess_file = diagcess_dir / 'diagcess.html'
|
|
364
|
-
|
|
365
355
|
# Now we'll look for the output SVG file to view
|
|
366
356
|
# If we're given an xml file, we'll modify the filename
|
|
367
357
|
if filename.endswith('.xml'):
|
|
@@ -378,12 +368,26 @@ def view(filename, ignore_annotations, port):
|
|
|
378
368
|
log.warning(f"There is no directory {path.parent}")
|
|
379
369
|
return
|
|
380
370
|
|
|
381
|
-
for
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
371
|
+
# we're going to look for file to view in the current directory first
|
|
372
|
+
cwd_files = os.listdir('.')
|
|
373
|
+
if path.name in cwd_files:
|
|
374
|
+
view_path = path.parent / path.name
|
|
375
|
+
else:
|
|
376
|
+
# now we'll look in output
|
|
377
|
+
if 'output' in cwd_files:
|
|
378
|
+
cwd_files = os.listdir('output')
|
|
379
|
+
if path.name in cwd_files:
|
|
380
|
+
view_path = path.parent / 'output' / path.name
|
|
381
|
+
|
|
382
|
+
# if we still didn't find it, descend the file structure
|
|
383
|
+
if view_path is None:
|
|
384
|
+
for dir, dirs, files in os.walk(os.getcwd()):
|
|
385
|
+
files = set(files)
|
|
386
|
+
if path.name in files:
|
|
387
|
+
view_path = path.parent / dir / path.name
|
|
388
|
+
|
|
385
389
|
if view_path is None:
|
|
386
|
-
log.
|
|
390
|
+
log.error(f'Unable to find {filename}')
|
|
387
391
|
return
|
|
388
392
|
|
|
389
393
|
# We are going to start the server from the home directory
|
|
@@ -416,6 +420,22 @@ def view(filename, ignore_annotations, port):
|
|
|
416
420
|
webbrowser.open(url)
|
|
417
421
|
else:
|
|
418
422
|
# There are annotations so we'll open with diagcess
|
|
423
|
+
|
|
424
|
+
# if we didn't find the diagcess tools, we'll install them here
|
|
425
|
+
if diagcess_dir is None:
|
|
426
|
+
prefig_root = Path(__file__).parent
|
|
427
|
+
source = prefig_root / 'resources' / 'diagcess'
|
|
428
|
+
cwd = Path(os.getcwd())
|
|
429
|
+
|
|
430
|
+
shutil.copytree(
|
|
431
|
+
source,
|
|
432
|
+
cwd,
|
|
433
|
+
dirs_exist_ok = True
|
|
434
|
+
)
|
|
435
|
+
diagcess_dir = cwd
|
|
436
|
+
|
|
437
|
+
diagcess_file = diagcess_dir / 'diagcess.html'
|
|
438
|
+
|
|
419
439
|
file_rel_path = os.path.relpath(view_path, diagcess_dir)
|
|
420
440
|
file_rel_path = file_rel_path[:-4]
|
|
421
441
|
diagcess_rel_path = os.path.relpath(diagcess_file, home_dir)
|
prefig/core/CTM.py
CHANGED
|
@@ -71,6 +71,10 @@ class CTM:
|
|
|
71
71
|
else:
|
|
72
72
|
self.ctm = ctm
|
|
73
73
|
self.ctm_stack = []
|
|
74
|
+
self.scale_x = lambda x: x
|
|
75
|
+
self.scale_y = lambda y: y
|
|
76
|
+
self.inv_scale_x = lambda x: x
|
|
77
|
+
self.inv_scale_y = lambda y: y
|
|
74
78
|
|
|
75
79
|
def push(self):
|
|
76
80
|
self.ctm_stack.append([self.ctm, self.inverse])
|
|
@@ -81,6 +85,14 @@ class CTM:
|
|
|
81
85
|
return
|
|
82
86
|
self.ctm, self.inverse = self.ctm_stack.pop(-1)
|
|
83
87
|
|
|
88
|
+
def set_log_x(self):
|
|
89
|
+
self.scale_x = lambda x: np.log10(x)
|
|
90
|
+
self.inv_scale_x = lambda x: 10**x
|
|
91
|
+
|
|
92
|
+
def set_log_y(self):
|
|
93
|
+
self.scale_y = lambda y: np.log10(y)
|
|
94
|
+
self.inv_scale_y = lambda y: 10**y
|
|
95
|
+
|
|
84
96
|
def translate(self, x, y):
|
|
85
97
|
m = translation(x, y)
|
|
86
98
|
self.ctm = concat(self.ctm, m)
|
|
@@ -111,12 +123,15 @@ class CTM:
|
|
|
111
123
|
def inverse_transform(self, p):
|
|
112
124
|
p = list(p).copy()
|
|
113
125
|
p.append(1)
|
|
114
|
-
|
|
126
|
+
inv_point = [math_util.dot(self.inverse[i], p) for i in range(2)]
|
|
127
|
+
return np.array([self.inv_scale_x(inv_point[0]),
|
|
128
|
+
self.inv_scale_y(inv_point[1])])
|
|
115
129
|
|
|
116
130
|
def transform(self, p):
|
|
117
|
-
p =
|
|
131
|
+
p = [self.scale_x(p[0]), self.scale_y(p[1])]
|
|
118
132
|
p.append(1)
|
|
119
|
-
|
|
133
|
+
transformed_point = [math_util.dot(self.ctm[i], p) for i in range(2)]
|
|
134
|
+
return np.array(transformed_point)
|
|
120
135
|
|
|
121
136
|
def copy(self):
|
|
122
137
|
return copy.deepcopy(self) # CTM(copy.deepcopy(self.ctm))
|
prefig/core/__init__.py
CHANGED
prefig/core/annotations.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import lxml.etree as ET
|
|
2
2
|
import logging
|
|
3
|
+
import copy
|
|
3
4
|
|
|
4
5
|
log = logging.getLogger('prefigure')
|
|
5
6
|
|
|
@@ -32,14 +33,20 @@ def annotate(element, diagram, parent = None):
|
|
|
32
33
|
parent = diagram.get_annotations_root()
|
|
33
34
|
|
|
34
35
|
if element.get('ref', None) is not None:
|
|
35
|
-
|
|
36
|
+
ref = element.get('ref')
|
|
37
|
+
if not ref.startswith('pf__'):
|
|
38
|
+
ref = 'pf__' + ref
|
|
39
|
+
element.set('id', ref)
|
|
36
40
|
element.attrib.pop('ref')
|
|
37
41
|
else:
|
|
38
42
|
log.info(f"An annotation has an empty attribute ref")
|
|
39
43
|
element.attrib.pop('annotate', None)
|
|
40
44
|
|
|
41
45
|
# let's check to see if this is a reference to an annotation branch
|
|
42
|
-
|
|
46
|
+
id = element.get('id')
|
|
47
|
+
if not id.startswith('pf__'):
|
|
48
|
+
id = 'pf__' + id
|
|
49
|
+
annotation = diagram.get_annotation_branch(id)
|
|
43
50
|
if annotation is not None:
|
|
44
51
|
annotate(annotation, diagram, parent)
|
|
45
52
|
return
|
|
@@ -122,3 +129,109 @@ def annotate(element, diagram, parent = None):
|
|
|
122
129
|
ACTIVE = ET.Element('ACTIVE')
|
|
123
130
|
ACTIVE.text = element.get('id')
|
|
124
131
|
sonification.append(ACTIVE)
|
|
132
|
+
|
|
133
|
+
pronounciations = {
|
|
134
|
+
'de-solve': 'D E solve',
|
|
135
|
+
'define-shapes': 'define shapes',
|
|
136
|
+
'angle-marker': 'angle marker',
|
|
137
|
+
'area-between-curves': 'area between curves',
|
|
138
|
+
'area-under-curve': 'area under curve',
|
|
139
|
+
'grid-axes': 'grid axes',
|
|
140
|
+
'implicit-curve': 'implicit curve',
|
|
141
|
+
'parametric-curve': 'parametric curve',
|
|
142
|
+
'plot-de-solution': 'plot D E solution',
|
|
143
|
+
'riemann-sum': 'Riemann sum',
|
|
144
|
+
'slope-field': 'slope field',
|
|
145
|
+
'tick-mark': 'tick mark',
|
|
146
|
+
'tangent-line': 'tangent line',
|
|
147
|
+
'vector-field': 'vector field'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
labeled_elements = {
|
|
151
|
+
'label',
|
|
152
|
+
'point',
|
|
153
|
+
'xlabel',
|
|
154
|
+
'ylabel',
|
|
155
|
+
'angle-marker',
|
|
156
|
+
'tick-mark',
|
|
157
|
+
'item',
|
|
158
|
+
'node',
|
|
159
|
+
'edge'
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
label_subelements = {
|
|
163
|
+
'm': 'math',
|
|
164
|
+
'b': 'bold',
|
|
165
|
+
'it': 'italics',
|
|
166
|
+
'plain': 'plain',
|
|
167
|
+
'newline': 'new line'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def diagram_to_speech(diagram):
|
|
171
|
+
diagram = copy.deepcopy(diagram)
|
|
172
|
+
|
|
173
|
+
element_num = 0
|
|
174
|
+
for element in diagram.getiterator():
|
|
175
|
+
if element.tag in label_subelements.keys():
|
|
176
|
+
element.getparent().remove(element)
|
|
177
|
+
continue
|
|
178
|
+
attribs = copy.deepcopy(element.attrib)
|
|
179
|
+
for attrib_name in list(element.attrib.keys()):
|
|
180
|
+
element.attrib.pop(attrib_name)
|
|
181
|
+
|
|
182
|
+
if element.tag == "diagram":
|
|
183
|
+
element.set('ref', 'figure')
|
|
184
|
+
intro = "This prefigure source file begins with a diagram having these attributes: "
|
|
185
|
+
elif element.tag == "definition":
|
|
186
|
+
element.set('ref', 'element-'+str(element_num))
|
|
187
|
+
tag_speech = 'definition'
|
|
188
|
+
intro = "A definition element defining " + element.text.strip()
|
|
189
|
+
elif element.tag in labeled_elements:
|
|
190
|
+
element.set('ref', 'element-'+str(element_num))
|
|
191
|
+
tag_speech = pronounciations.get(element.tag, element.tag)
|
|
192
|
+
label_text = label_to_speech(element)
|
|
193
|
+
if len(label_text) > 0:
|
|
194
|
+
if len(attribs) == 0:
|
|
195
|
+
intro = f"A {tag_speech} element with label {label_text}. The element has no attributes."
|
|
196
|
+
else:
|
|
197
|
+
intro = f"A {tag_speech} element with label {label_text}. There are these attributes: "
|
|
198
|
+
else:
|
|
199
|
+
if len(attribs) == 0:
|
|
200
|
+
intro = f"A {tag_speech} element with no attributes."
|
|
201
|
+
else:
|
|
202
|
+
intro = f"A {tag_speech} element with these attributes: "
|
|
203
|
+
element.text = None
|
|
204
|
+
else:
|
|
205
|
+
element.set('ref', 'element-'+str(element_num))
|
|
206
|
+
tag_speech = pronounciations.get(element.tag, element.tag)
|
|
207
|
+
if len(attribs) == 0:
|
|
208
|
+
intro = f"A {tag_speech} element with no attributes"
|
|
209
|
+
else:
|
|
210
|
+
intro = f"A {tag_speech} element with these attributes: "
|
|
211
|
+
element.set("text", intro + attributes_to_speech(attribs))
|
|
212
|
+
element_num += 1
|
|
213
|
+
element.tag = "annotation"
|
|
214
|
+
|
|
215
|
+
log.error(ET.tostring(diagram, pretty_print=True))
|
|
216
|
+
return diagram
|
|
217
|
+
|
|
218
|
+
def attributes_to_speech(attribs):
|
|
219
|
+
strings = []
|
|
220
|
+
for key, value in attribs.items():
|
|
221
|
+
strings.append(f"{key} has value {value}")
|
|
222
|
+
return ', '.join(strings)
|
|
223
|
+
|
|
224
|
+
def label_to_speech(element):
|
|
225
|
+
strings = []
|
|
226
|
+
if (element.text is not None and
|
|
227
|
+
len(element.text.strip()) > 0):
|
|
228
|
+
strings.append(element.text.strip())
|
|
229
|
+
for child in element:
|
|
230
|
+
child_speech = label_subelements.get(child.tag, child.tag)
|
|
231
|
+
strings.append('begin ' + child_speech)
|
|
232
|
+
strings.append(child.text.strip())
|
|
233
|
+
strings.append('end ' + child_speech)
|
|
234
|
+
if (child.tail is not None and
|
|
235
|
+
child.tail.strip() is not None):
|
|
236
|
+
strings.append(child.tail.strip())
|
|
237
|
+
return ' '.join(strings)
|
prefig/core/area.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import lxml.etree as ET
|
|
4
4
|
import logging
|
|
5
|
+
import math
|
|
5
6
|
from . import user_namespace as un
|
|
6
7
|
from . import utilities as util
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ def area_between_curves(element, diagram, parent, outline_status):
|
|
|
13
14
|
finish_outline(element, diagram, parent)
|
|
14
15
|
return
|
|
15
16
|
|
|
17
|
+
polar = element.get('coordinates', 'cartesian') == 'polar'
|
|
16
18
|
util.set_attr(element, 'stroke', 'black')
|
|
17
19
|
util.set_attr(element, 'fill', 'lightgray')
|
|
18
20
|
util.set_attr(element, 'thickness', '2')
|
|
@@ -53,17 +55,32 @@ def area_between_curves(element, diagram, parent, outline_status):
|
|
|
53
55
|
else:
|
|
54
56
|
domain = un.valid_eval(domain)
|
|
55
57
|
|
|
58
|
+
if element.get('domain-degrees', 'no') == 'yes':
|
|
59
|
+
domain = [math.radians(d) for d in domain]
|
|
60
|
+
|
|
56
61
|
dx = (domain[1]-domain[0])/N
|
|
57
62
|
x = domain[0]
|
|
58
|
-
|
|
63
|
+
if polar:
|
|
64
|
+
r = f(x)
|
|
65
|
+
p = diagram.transform((r*math.cos(x), r*math.sin(x)))
|
|
66
|
+
else:
|
|
67
|
+
p = diagram.transform((x, f(x)))
|
|
59
68
|
cmds = ['M ' + util.pt2str(p)]
|
|
60
69
|
for _ in range(N+1):
|
|
61
|
-
|
|
70
|
+
if polar:
|
|
71
|
+
r = f(x)
|
|
72
|
+
p = diagram.transform((r*math.cos(x), r*math.sin(x)))
|
|
73
|
+
else:
|
|
74
|
+
p = diagram.transform((x, f(x)))
|
|
62
75
|
cmds.append('L ' + util.pt2str(p))
|
|
63
76
|
x += dx
|
|
64
77
|
for _ in range(N+1):
|
|
65
78
|
x -= dx
|
|
66
|
-
|
|
79
|
+
if polar:
|
|
80
|
+
r = g(x)
|
|
81
|
+
p = diagram.transform((r*math.cos(x), r*math.sin(x)))
|
|
82
|
+
else:
|
|
83
|
+
p = diagram.transform((x, g(x)))
|
|
67
84
|
cmds.append('L ' + util.pt2str(p))
|
|
68
85
|
cmds.append('Z')
|
|
69
86
|
d = ' '.join(cmds)
|
|
@@ -72,7 +89,6 @@ def area_between_curves(element, diagram, parent, outline_status):
|
|
|
72
89
|
diagram.add_id(path, element.get('id'))
|
|
73
90
|
path.set('d', d)
|
|
74
91
|
util.add_attr(path, util.get_2d_attr(element))
|
|
75
|
-
# path.set('type', 'area between curves')
|
|
76
92
|
|
|
77
93
|
if outline_status == 'add_outline':
|
|
78
94
|
diagram.add_outline(element, path, parent)
|
prefig/core/arrow.py
CHANGED
|
@@ -5,6 +5,7 @@ import numpy as np
|
|
|
5
5
|
from . import utilities as util
|
|
6
6
|
from . import CTM
|
|
7
7
|
from . import user_namespace as un
|
|
8
|
+
from . import repeat
|
|
8
9
|
|
|
9
10
|
log = logging.getLogger('prefigure')
|
|
10
11
|
|
|
@@ -21,7 +22,7 @@ def add_tactile_arrowhead_marker(diagram, path, mid=False):
|
|
|
21
22
|
# get the stroke width from the graphical component
|
|
22
23
|
stroke_width_str = path.get('stroke-width', '1')
|
|
23
24
|
stroke_width = int(stroke_width_str)
|
|
24
|
-
id = '
|
|
25
|
+
id = 'pf__arrow-head-'+stroke_width_str
|
|
25
26
|
|
|
26
27
|
# if we've seen this already, there's no need to create it again
|
|
27
28
|
if diagram.has_reusable(id):
|
|
@@ -179,16 +180,21 @@ def add_arrowhead_marker(diagram,
|
|
|
179
180
|
# end or in the middle of a path
|
|
180
181
|
id_data = f"_{arrow_width}_{arrow_angles[0]}_{arrow_angles[1]}"
|
|
181
182
|
if not mid:
|
|
182
|
-
id = '
|
|
183
|
+
id = 'pf__arrow-head-end-'+stroke_width_str+id_data+'-'+stroke_color
|
|
183
184
|
if arrow_width is None:
|
|
184
185
|
arrow_width = 4
|
|
185
186
|
dims = (1, arrow_width)
|
|
186
187
|
else:
|
|
187
|
-
id = '
|
|
188
|
+
id = 'pf__arrow-head-mid-'+stroke_width_str+id_data+'-'+stroke_color
|
|
188
189
|
if arrow_width is None:
|
|
189
190
|
arrow_width = 13/3
|
|
190
191
|
dims = (1, arrow_width) #11/3)
|
|
191
192
|
|
|
193
|
+
# EPUB does not allow some characters to appear in @id
|
|
194
|
+
# There is a possibility that we have # or . so we will
|
|
195
|
+
# replace them with this function call
|
|
196
|
+
id = repeat.epub_clean(id)
|
|
197
|
+
|
|
192
198
|
# If we've already created this one, we'll just move on
|
|
193
199
|
if diagram.has_reusable(id):
|
|
194
200
|
return id
|