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.
Files changed (43) hide show
  1. prefig/cli.py +46 -26
  2. prefig/core/CTM.py +18 -3
  3. prefig/core/__init__.py +2 -0
  4. prefig/core/annotations.py +115 -2
  5. prefig/core/area.py +20 -4
  6. prefig/core/arrow.py +9 -3
  7. prefig/core/axes.py +909 -0
  8. prefig/core/circle.py +13 -4
  9. prefig/core/clip.py +1 -1
  10. prefig/core/coordinates.py +31 -4
  11. prefig/core/diagram.py +129 -6
  12. prefig/core/graph.py +236 -23
  13. prefig/core/grid_axes.py +181 -495
  14. prefig/core/image.py +127 -0
  15. prefig/core/label.py +14 -4
  16. prefig/core/legend.py +4 -4
  17. prefig/core/line.py +92 -1
  18. prefig/core/math_utilities.py +155 -0
  19. prefig/core/network.py +2 -2
  20. prefig/core/parametric_curve.py +15 -3
  21. prefig/core/path.py +25 -0
  22. prefig/core/point.py +18 -2
  23. prefig/core/polygon.py +7 -1
  24. prefig/core/read.py +6 -3
  25. prefig/core/rectangle.py +10 -4
  26. prefig/core/repeat.py +37 -3
  27. prefig/core/shape.py +12 -1
  28. prefig/core/slope_field.py +142 -0
  29. prefig/core/tags.py +8 -2
  30. prefig/core/tangent_line.py +36 -12
  31. prefig/core/user_namespace.py +8 -0
  32. prefig/core/utilities.py +9 -1
  33. prefig/engine.py +73 -28
  34. prefig/resources/diagcess/diagcess.js +7 -1
  35. prefig/resources/schema/pf_schema.rnc +89 -6
  36. prefig/resources/schema/pf_schema.rng +321 -5
  37. prefig/scripts/install_mj.py +10 -11
  38. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/METADATA +12 -16
  39. prefig-0.5.6.dev20260130060411.dist-info/RECORD +68 -0
  40. prefig-0.2.15.dev20250514053750.dist-info/RECORD +0 -66
  41. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/LICENSE +0 -0
  42. {prefig-0.2.15.dev20250514053750.dist-info → prefig-0.5.6.dev20260130060411.dist-info}/WHEEL +0 -0
  43. {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
- log.info(f"Installing MathJax libraries in {destination}")
83
- try:
84
- subprocess.run(["npm", "install"]) #, f"--prefix={destination}"])
85
- except:
86
- log.warning("MathJax installation failed. Is npm installed on your system?")
87
- log.setLevel(log_level)
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 dir, dirs, files in os.walk(os.getcwd()):
382
- files = set(files)
383
- if path.name in files:
384
- view_path = path.parent / dir / path.name
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.warning(f'Unable to find {filename}')
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
- return np.array([math_util.dot(self.inverse[i], p) for i in range(2)])
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 = list(p).copy()
131
+ p = [self.scale_x(p[0]), self.scale_y(p[1])]
118
132
  p.append(1)
119
- return np.array([math_util.dot(self.ctm[i], p) for i in range(2)])
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
@@ -2,6 +2,7 @@ from . import (
2
2
  annotations,
3
3
  area,
4
4
  arrow,
5
+ axes,
5
6
  calculus,
6
7
  circle,
7
8
  clip,
@@ -12,6 +13,7 @@ from . import (
12
13
  graph,
13
14
  grid_axes,
14
15
  group,
16
+ image,
15
17
  implicit,
16
18
  label,
17
19
  label_tools,
@@ -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
- element.set('id', element.get('ref'))
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
- annotation = diagram.get_annotation_branch(element.get('id'))
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
- p = diagram.transform((x, f(x)))
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
- p = diagram.transform((x,f(x)))
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
- p = diagram.transform((x, g(x)))
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 = 'arrow-head-'+stroke_width_str
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 = 'arrow-head-end-'+stroke_width_str+id_data+'-'+stroke_color
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 = 'arrow-head-mid-'+stroke_width_str+id_data+'-'+stroke_color
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