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/core/image.py ADDED
@@ -0,0 +1,127 @@
1
+ import lxml.etree as ET
2
+ import logging
3
+ import numpy as np
4
+ import base64
5
+ import lxml.etree as ET
6
+ from . import user_namespace as un
7
+ from . import group
8
+ from . import utilities as util
9
+ from . import CTM
10
+
11
+ log = logging.getLogger('prefigure')
12
+
13
+ type_dict = {'jpg':'jpeg',
14
+ 'jpeg':'jpeg',
15
+ 'png':'png',
16
+ 'gif':'gif',
17
+ 'svg':'svg'}
18
+
19
+ # Allows a block of XML to repeat with a changing parameter
20
+
21
+ def image(element, diagram, parent, outline_status):
22
+ if len(element) == 0:
23
+ log.error("An <image> must contain content to replace the image in a tactile build")
24
+ return;
25
+
26
+ if diagram.output_format() == 'tactile':
27
+ element.tag = 'group'
28
+ group.group(element, diagram, parent, outline_status)
29
+ return
30
+
31
+ source = element.get('source', None)
32
+ if source is None:
33
+ log.error("An <image> needs a @source attribute")
34
+ return
35
+ try:
36
+ ll = un.valid_eval(element.get('lower-left', '(0,0)'))
37
+ dims = un.valid_eval(element.get('dimensions', '(1,1)'))
38
+ center = element.get('center', None)
39
+ if center is not None:
40
+ center = un.valid_eval(center)
41
+ ll = center - 0.5 * dims
42
+ else:
43
+ center = ll + 0.5*dims
44
+ rotation = un.valid_eval(element.get('rotate', '0'))
45
+ scale = un.valid_eval(element.get('scale', '1'))
46
+ except:
47
+ log.error("Error parsing placement data in an <image>")
48
+ return
49
+
50
+ file_type = None
51
+ if element.get('filetype', None) is not None:
52
+ file_type = type_dict.get(element.get('filetype'), None)
53
+ if file_type is None:
54
+ suffix = source.split('.')[-1]
55
+ file_type = type_dict.get(suffix, None)
56
+ if file_type is None:
57
+ log.error(f"Cannot determine the type of image in {source}")
58
+ return
59
+
60
+ ll_svg = diagram.transform(ll)
61
+ ur_svg = diagram.transform(ll + dims)
62
+ center_svg = diagram.transform(center)
63
+ width, height = ur_svg - ll_svg
64
+ height = -height
65
+
66
+ if diagram.get_environment() == 'pretext':
67
+ source = 'data/' + source
68
+ else:
69
+ assets_dir = diagram.get_external()
70
+ if assets_dir is not None:
71
+ assets_dir = assets_dir.strip()
72
+ if assets_dir[-1] != '/':
73
+ assets_dir += '/'
74
+ source = assets_dir + source
75
+
76
+ opacity = element.get('opacity', None)
77
+ if opacity is not None:
78
+ opacity = un.valid_eval(opacity)
79
+ if file_type == 'svg':
80
+ svg_tree = ET.parse(source)
81
+ svg_root = svg_tree.getroot()
82
+ svg_width = svg_root.get('width', None)
83
+ svg_height = svg_root.get('height', None)
84
+
85
+ object = ET.SubElement(parent, 'foreignObject')
86
+ object.set('x', util.float2str(center_svg[0]-width/2))
87
+ object.set('y', util.float2str(center_svg[1]-height/2))
88
+ object.set('width', util.float2str(width))
89
+ object.set('height', util.float2str(height))
90
+ svg_root.set('width', '100%')
91
+ svg_root.set('height', '100%')
92
+ if svg_root.get('viewBox', None) is None:
93
+ svg_root.set('viewBox', f"0 0 {svg_width} {svg_height}")
94
+ object.append(svg_root)
95
+ diagram.add_id(object, element.get('id'))
96
+ svg_root.attrib.pop('id', None)
97
+ if opacity is not None:
98
+ object.set('opacity', util.float2str(opacity))
99
+ return
100
+
101
+ image_el = ET.SubElement(parent, 'image')
102
+ image_el.set('x', util.float2str(-width/2))
103
+ image_el.set('y', util.float2str(-height/2))
104
+ image_el.set('width', util.float2str(width))
105
+ image_el.set('height', util.float2str(height))
106
+ if opacity is not None:
107
+ image_el.set('opacity', util.float2str(opacity))
108
+ diagram.add_id(image_el, element.get('id'))
109
+
110
+ with open(source, "rb") as image_file:
111
+ encoded_string = base64.b64encode(image_file.read())
112
+ encoded_string = encoded_string.decode('utf-8')
113
+ ref = f"data:image/{file_type};base64,{encoded_string}"
114
+ image_el.set('href', ref)
115
+
116
+ transform_pieces = [CTM.translatestr(*center_svg)]
117
+
118
+ if isinstance(scale, np.ndarray):
119
+ transform_pieces.append(CTM.scalestr(*scale))
120
+ elif scale != '1':
121
+ transform_pieces.append(f"scale({scale})")
122
+
123
+ if rotation != 0:
124
+ transform_pieces.append(f"rotate({-rotation})")
125
+
126
+ image_el.set('transform', ' '.join(transform_pieces))
127
+
prefig/core/label.py CHANGED
@@ -15,6 +15,8 @@ import tempfile
15
15
 
16
16
  log = logging.getLogger('prefigure')
17
17
 
18
+ allowed_fonts = {'serif','sans-serif','monospace'}
19
+
18
20
  def init(format, environment):
19
21
  global math_labels
20
22
  global text_measurements
@@ -465,10 +467,18 @@ def position_svg_label(element, diagram, ctm, group):
465
467
  # These lists contain: font, size, italics, bold, color
466
468
  label_color = element.get("color", None)
467
469
 
468
- std_font_face = ['sans', 14, False, False, label_color]
469
- it_font_face = ['sans', 14, True, False, label_color]
470
- b_font_face = ['sans', 14, False, True, label_color]
471
- it_b_font_face = ['sans', 14, True, True, label_color]
470
+ diagram.apply_defaults('label', element)
471
+
472
+ font_family = element.get('font', 'sans-serif').lower().strip()
473
+ if font_family not in allowed_fonts:
474
+ font_family = 'sans-serif'
475
+
476
+ font_size = un.valid_eval(element.get('font-size', '14'))
477
+
478
+ std_font_face = [font_family, font_size, False, False, label_color]
479
+ it_font_face = [font_family, font_size, True, False, label_color]
480
+ b_font_face = [font_family, font_size, False, True, label_color]
481
+ it_b_font_face = [font_family, font_size, True, True, label_color]
472
482
 
473
483
  label = element
474
484
 
prefig/core/legend.py CHANGED
@@ -64,7 +64,7 @@ class Legend:
64
64
  # first we'll create the label
65
65
  label_el = copy.deepcopy(li)
66
66
  label_el.tag = 'label'
67
- label_el.set('id', f"legend-label-{num}")
67
+ label_el.set('id', f"pf__legend-label-{num}")
68
68
  label_el.set('alignment', 'se')
69
69
  label_el.set('anchor', element.get('anchor', anchor_str))
70
70
  label_el.set('abs-offset', '(0,0)')
@@ -87,7 +87,7 @@ class Legend:
87
87
  key = copy.deepcopy(key)
88
88
  key.set('p', anchor_str)
89
89
  key.set('size', '4')
90
- key.set('id', f"legend-point-{num}")
90
+ key.set('id', f"pf__legend-point-{num}")
91
91
  key_width = max(key_width, point_width)
92
92
  else:
93
93
  fill = key.get('fill')
@@ -219,11 +219,11 @@ class Legend:
219
219
  id = group.get('id', 'none')
220
220
  if id == 'background-group':
221
221
  for rectangle in group:
222
- if rectangle.get('id', 'none').startswith('legend-label'):
222
+ if rectangle.get('id', 'none').startswith('pf__legend-label'):
223
223
  group.remove(rectangle)
224
224
  if id == 'braille-group':
225
225
  for label in group:
226
- if label.get('id','none').startswith('legend-label'):
226
+ if label.get('id','none').startswith('pf__legend-label'):
227
227
  label_groups.append(label)
228
228
  group.remove(label)
229
229
 
prefig/core/line.py CHANGED
@@ -5,7 +5,11 @@ import logging
5
5
  from . import utilities as util
6
6
  from . import math_utilities as math_util
7
7
  from . import user_namespace as un
8
+ import copy
8
9
  from . import arrow
10
+ from . import group
11
+ from . import label
12
+ from . import CTM
9
13
 
10
14
  log = logging.getLogger('prefigure')
11
15
 
@@ -50,12 +54,24 @@ def line(element, diagram, parent, outline_status):
50
54
 
51
55
  line = mk_line(p1, p2, diagram, element.get('id', None),
52
56
  endpoint_offsets=endpoint_offsets)
57
+
58
+ # we need to hold on to the endpoints in case the line is labelled
59
+ # these are endpoints in SVG coordinates
60
+ x1 = float(line.get('x1'))
61
+ x2 = float(line.get('x2'))
62
+ y1 = float(line.get('y1'))
63
+ y2 = float(line.get('y2'))
64
+
65
+ q1 = np.array((x1, y1))
66
+ q2 = np.array((x2, y2))
67
+ diagram.save_data(element, {'q1': q1, 'q2': q2})
68
+
69
+ # now add the graphical attributes
53
70
  util.set_attr(element, 'stroke', 'black')
54
71
  util.set_attr(element, 'thickness', '2')
55
72
  if diagram.output_format() == 'tactile':
56
73
  element.set('stroke', 'black')
57
74
  util.add_attr(line, util.get_1d_attr(element))
58
- # line.set('type', 'line')
59
75
 
60
76
  arrows = int(element.get('arrows', '0'))
61
77
  forward = 'marker-end'
@@ -113,15 +129,38 @@ def line(element, diagram, parent, outline_status):
113
129
  diagram.add_outline(element, line, parent)
114
130
  finish_outline(element, diagram, parent)
115
131
  else:
132
+ original_parent = parent
133
+ parent = add_label(element, diagram, parent)
116
134
  parent.append(line)
117
135
 
136
+ # if no label has been added, then we're done
137
+ if original_parent == parent:
138
+ return
139
+
140
+ # if there is a label, then the id is on the outer <g> element
141
+ # so we need to remove it from the children
142
+ remove_id(parent)
143
+
118
144
  def finish_outline(element, diagram, parent):
145
+ original_parent = parent
146
+ parent = add_label(element, diagram, parent)
147
+
148
+ # if we've added a label, remove the id's from element under the parent <g>
149
+ if original_parent != parent:
150
+ remove_id(parent)
151
+
119
152
  diagram.finish_outline(element,
120
153
  element.get('stroke'),
121
154
  element.get('thickness'),
122
155
  element.get('fill', 'none'),
123
156
  parent)
124
157
 
158
+ def remove_id(el):
159
+ for child in el:
160
+ if child.get('id', None) is not None:
161
+ child.attrib.pop('id')
162
+ remove_id(child)
163
+
125
164
  # We'll be adding lines in other places so we'll use this more widely
126
165
  def mk_line(p0, p1, diagram, id = None, endpoint_offsets = None, user_coords = True):
127
166
  line = ET.Element('line')
@@ -176,3 +215,55 @@ def infinite_line(p0, p1, diagram, slope = None):
176
215
  if t_min > t_max:
177
216
  return None, None
178
217
  return [p + t * v for t in [t_min, t_max]]
218
+
219
+ def add_label(element, diagram, parent):
220
+ # Is there a label associated with point?
221
+ text = element.text
222
+
223
+ # is there a label here?
224
+ has_text = text is not None and len(text.strip()) > 0
225
+ all_comments = all([subel.tag is ET.Comment for subel in element])
226
+ if has_text or not all_comments:
227
+ # If there's a label, we'll bundle the label and point in a group
228
+ parent_group = ET.SubElement(parent, 'g')
229
+ diagram.add_id(parent_group, element.get('id'))
230
+
231
+ # Now we'll create a new XML element describing the label
232
+ el = copy.deepcopy(element)
233
+ el.tag = 'label'
234
+
235
+ data = diagram.retrieve_data(element)
236
+ q1 = data['q1']
237
+ q2 = data['q2']
238
+
239
+ label_location = un.valid_eval(element.get("label-location", "0.5"))
240
+ if label_location < 0:
241
+ label_location = -label_location
242
+ q1, q2 = q2, q1
243
+
244
+ el.set('user-coords', 'no')
245
+ diff = q2 - q1
246
+ d = math_util.length(diff)
247
+ angle = math.degrees(math.atan2(diff[1], diff[0]))
248
+ if diagram.output_format() == "tactile":
249
+ anchor = q1 + label_location * diff
250
+ el.set("anchor", f"({anchor[0]}, {anchor[1]})")
251
+ direction = (diff[1], diff[0])
252
+ alignment = label.get_alignment_from_direction(direction)
253
+ el.set("alignment", alignment)
254
+ label.label(el, diagram, parent_group)
255
+ else:
256
+ tform = CTM.translatestr(*q1)
257
+ tform += ' ' + CTM.rotatestr(-angle)
258
+ distance = d * label_location
259
+ g = ET.SubElement(parent_group, "g")
260
+ g.set("transform", tform)
261
+ el.set("anchor", f"({distance},0)")
262
+ el.set("alignment", "north")
263
+ label.label(el, diagram, g)
264
+
265
+ return parent_group
266
+
267
+ else:
268
+ return parent
269
+
@@ -1,16 +1,39 @@
1
1
  import numpy as np
2
2
  import math
3
3
  from . import user_namespace as un
4
+ from . import calculus
4
5
 
5
6
  import logging
6
7
  logger = logging.getLogger('prefigure')
7
8
 
9
+ diagram = None
10
+
11
+ def set_diagram(d):
12
+ global diagram
13
+ diagram = d
14
+
8
15
  # introduce some useful mathematical operations
9
16
  # that are meant to be available to authors
10
17
 
18
+ def ln(x):
19
+ return math.log(x)
20
+
21
+ #Add other three trig functions for convenience
22
+ def sec(x):
23
+ return 1/math.cos(x)
24
+
25
+ def csc(x):
26
+ return 1/math.sin(x)
27
+
28
+ def cot(x):
29
+ return 1/math.tan(x)
30
+
11
31
  def dot(u, v):
12
32
  return np.dot(np.array(u), np.array(v))
13
33
 
34
+ def distance(p, q):
35
+ return length(np.array(p) - np.array(q))
36
+
14
37
  def length(u):
15
38
  return np.linalg.norm(np.array(u))
16
39
 
@@ -67,6 +90,20 @@ def rotate(v, theta):
67
90
  s = math.sin(theta)
68
91
  return np.array([c*v[0]-s*v[1], s*v[0]+c*v[1]])
69
92
 
93
+ def deriv(f, a):
94
+ return calculus.derivative(f, a)
95
+
96
+ def grad(f, a):
97
+ a = list(a)
98
+ grad = []
99
+ for j in range(len(a)):
100
+ def f_trace(x):
101
+ b = a[:]
102
+ b[j] = x
103
+ return f(*b)
104
+ grad.append(calculus.derivative(f_trace, a[j]))
105
+ return np.array(grad)
106
+
70
107
  def zip_lists(a, b):
71
108
  return list(zip(a, b))
72
109
 
@@ -116,3 +153,121 @@ def delta(t, a):
116
153
 
117
154
  return 0
118
155
 
156
+ def line_intersection(lines):
157
+ p1, p2 = [np.array(c) for c in lines[0]]
158
+ q1, q2 = [np.array(c) for c in lines[1]]
159
+
160
+ diff = p2-p1
161
+ normal = [-diff[1], diff[0]]
162
+
163
+ v = q2 - q1
164
+ denom = dot(normal, v)
165
+ if abs(denom) < 1e-10:
166
+ bbox = diagram.bbox()
167
+ return np.array([(bbox[0]+bbox[2])/2, (bbox[1]+bbox[3])/2])
168
+ t = dot(normal, q1-p1) / denom
169
+ return q1 - t*v
170
+
171
+ # find the intersection of two graphs or the zero of just one
172
+ def intersect(functions, seed=None, interval=None):
173
+ bbox = diagram.bbox()
174
+ width = bbox[2] - bbox[0]
175
+ height = bbox[3] - bbox[1]
176
+ tolerance = 1e-06 * height
177
+
178
+ upper = bbox[3] + height
179
+ lower = bbox[1] - height
180
+
181
+ # we want to allow for some flexibility. We can find the intersection
182
+ # of
183
+ # -- two graphs f(x), g(x): intersect((f,g), seed)
184
+ # -- zero of f(x): intersect(f, seed)
185
+ # -- solve f(x) = y: intersect((f, y), seed)
186
+ # -- two lines: intersect((p1,p2),(q1,q2),seed)
187
+ if isinstance(functions, np.ndarray):
188
+ if isinstance(functions[0], np.ndarray):
189
+ return line_intersection(functions)
190
+ try:
191
+ y_value = float(functions[1])
192
+ f = lambda x: functions[0](x) - y_value
193
+ except:
194
+ f = lambda x: functions[0](x) - functions[1](x)
195
+ else:
196
+ f = functions
197
+
198
+ if interval is None:
199
+ interval = (bbox[0], bbox[2])
200
+
201
+ x0 = seed
202
+ y0 = f(x0)
203
+
204
+ if abs(y0) < tolerance:
205
+ return x0
206
+
207
+ dx = 0.002 * width
208
+ x = x0
209
+ x_left = -np.inf
210
+ while x >= interval[0]:
211
+ x -= dx
212
+ try:
213
+ y = f(x)
214
+ except:
215
+ break
216
+ if y > upper or y < lower:
217
+ break
218
+ if abs(y) < tolerance:
219
+ x_left = x
220
+ break
221
+ if y * y0 < 0:
222
+ x_left = x
223
+ break
224
+ if x_left != -np.inf and abs(f(x_left) - f(x_left + dx)) > height:
225
+ x_left = -np.inf
226
+
227
+ x = x0
228
+ x_right = np.inf
229
+ while x <= interval[1]:
230
+ x += dx
231
+ try:
232
+ y = f(x)
233
+ except:
234
+ break
235
+ if y > upper or y < lower:
236
+ break
237
+ if abs(y) < tolerance:
238
+ x_right = x
239
+ break
240
+ if y * y0 < 0:
241
+ x_right = x
242
+ break
243
+
244
+ if x_right != np.inf and abs(f(x_right) - f(x_right - dx)) > height:
245
+ x_right = np.inf
246
+
247
+ if x_left < interval[0] and x_right > interval[1]:
248
+ # we didn't find anything
249
+ return x0
250
+
251
+ if x_left < interval[0]:
252
+ x2 = x_right
253
+ x1 = x_right - dx
254
+
255
+ if x_right > interval[1]:
256
+ x2 = x_left + dx
257
+ x1 = x_left
258
+
259
+ if abs(x0 - x_right) < abs(x0 - x_left):
260
+ x1 = x_right - dx
261
+ x2 = x_right
262
+ else:
263
+ x1 = x_left
264
+ x2 = x_left + dx
265
+
266
+ for _ in range(8):
267
+ mid = (x1 + x2) / 2
268
+ if f(mid) * f(x1) < 0:
269
+ x2 = mid
270
+ else:
271
+ x1 = mid
272
+ return (x1+x2)/2
273
+
prefig/core/network.py CHANGED
@@ -65,8 +65,8 @@ def network(element, diagram, parent, outline_status):
65
65
  positions = {}
66
66
  for node in element.findall('node'):
67
67
  handle = node.get('at', None)
68
- diagram.add_id(node, handle)
69
- handle = node.get('id')
68
+ # diagram.add_id(node, handle)
69
+ # handle = node.get('id')
70
70
  nodes[handle] = node
71
71
 
72
72
  # check for the position of this node
@@ -24,8 +24,9 @@ def parametric_curve(element, diagram, parent, outline_status):
24
24
  log.error(f"Error in <parametric-curve> defining domain={element.get('domain')}")
25
25
  return
26
26
 
27
- N = int(element.get('N', '100'))
27
+ arrows = int(element.get('arrows', '0'))
28
28
 
29
+ N = int(element.get('N', '100'))
29
30
  t = domain[0]
30
31
  dt = (domain[1]-domain[0])/N
31
32
  p = diagram.transform(f(t))
@@ -34,8 +35,21 @@ def parametric_curve(element, diagram, parent, outline_status):
34
35
  t += dt
35
36
  p = diagram.transform(f(t))
36
37
  points.append('L ' + util.pt2str(p))
38
+
37
39
  if element.get('closed', 'no') == 'yes':
38
40
  points.append('Z')
41
+
42
+ if arrows > 0 and element.get('arrow-location', None) is not None:
43
+ arrow_location = un.valid_eval(element.get('arrow-location'))
44
+ num_pts = 5
45
+ t = arrow_location - num_pts*dt
46
+ p = diagram.transform(f(t))
47
+ points.append('M ' + util.pt2str(p))
48
+ for _ in range(num_pts):
49
+ t += dt
50
+ p = diagram.transform(f(t))
51
+ points.append('L ' + util.pt2str(p))
52
+
39
53
  d = ' '.join(points)
40
54
 
41
55
  if diagram.output_format() == 'tactile':
@@ -51,12 +65,10 @@ def parametric_curve(element, diagram, parent, outline_status):
51
65
  diagram.add_id(path, element.get('id'))
52
66
  path.set('d', d)
53
67
  util.add_attr(path, util.get_2d_attr(element))
54
- # path.set('type', 'parametric curve')
55
68
 
56
69
  element.set('cliptobbox', element.get('cliptobbox', 'yes'))
57
70
  util.cliptobbox(path, element, diagram)
58
71
 
59
- arrows = int(element.get('arrows', '0'))
60
72
  forward = 'marker-end'
61
73
  backward = 'marker-start'
62
74
  if element.get('reverse', 'no') == 'yes':
prefig/core/path.py CHANGED
@@ -480,6 +480,31 @@ def decorate(child, diagram, current_point, cmds):
480
480
  cmds += ['L', util.pt2str(ctm.transform((x_pos,0)))]
481
481
  cmds += ['L', util.pt2str(ctm.transform((length, 0)))]
482
482
 
483
+ if decoration_data[0] == 'ragged':
484
+ # offset, step
485
+ try:
486
+ data = [d.split('=') for d in decoration_data[1:]]
487
+ data = {k:v for k, v in data}
488
+ except:
489
+ log.error("Error in wave decoration data")
490
+ return
491
+ try:
492
+ offset = un.valid_eval(data['offset'])
493
+ step = un.valid_eval(data['step'])
494
+ except:
495
+ log.error("Error in retrieving the step and an offset in a ragged decoration")
496
+
497
+ np.random.seed(int(data.get('seed','1')))
498
+ x_pos = 0
499
+ y_pos = 0
500
+ p = ctm.transform((0,0))
501
+ cmds += ['L', util.pt2str(ctm.transform((0,0)))]
502
+ while x_pos < length-step:
503
+ y_pos = 2 * offset * (np.random.random()-0.5)
504
+ x_pos += (0.5*np.random.random()+0.75)*step
505
+ cmds += ['L', util.pt2str(ctm.transform((x_pos,y_pos)))]
506
+ cmds += ['L', util.pt2str(ctm.transform((length, 0)))]
507
+
483
508
  if decoration_data[0] == 'capacitor':
484
509
  try:
485
510
  data = [d.split('=') for d in decoration_data[1:]]
prefig/core/point.py CHANGED
@@ -1,5 +1,7 @@
1
1
  import lxml.etree as ET
2
2
  import logging
3
+ import numpy as np
4
+ import math
3
5
  from . import user_namespace as un
4
6
  from . import math_utilities as math_util
5
7
  from . import utilities as util
@@ -18,13 +20,27 @@ def point(element, diagram, parent, outline_status = None):
18
20
 
19
21
  # determine the location and size of the point from the XML element
20
22
  try:
21
- p = diagram.transform(un.valid_eval(element.get('p')))
23
+ p = un.valid_eval(element.get('p'))
24
+ if element.get('coordinates', 'cartesian') == 'polar':
25
+ radial = p[0]
26
+ angle = p[1]
27
+ if element.get('degrees', 'no') == 'yes':
28
+ angle = math.radians(angle)
29
+ p = radial * np.array([math.cos(angle), math.sin(angle)])
30
+ element.set('p', util.pt2long_str(p, spacer=','))
31
+ p = diagram.transform(p)
22
32
  except:
23
33
  log.error(f"Error in <point> defining p={element.get('p')}")
24
34
  return
25
35
 
26
36
  if diagram.output_format() == 'tactile':
27
- element.set('size', element.get('size', '9'))
37
+ if element.get('size', None) is not None:
38
+ size = un.valid_eval(element.get('size'))
39
+ size = max(size, 9)
40
+ size = str(size)
41
+ else:
42
+ size = '9'
43
+ element.set('size', size)
28
44
  else:
29
45
  element.set('size', element.get('size', '4'))
30
46
  size = util.get_attr(element, 'size', '1')
prefig/core/polygon.py CHANGED
@@ -53,7 +53,10 @@ def polygon(element, diagram, parent,
53
53
  if element.get('stroke') is not None:
54
54
  element.set('stroke', 'black')
55
55
  if element.get('fill') is not None:
56
- element.set('fill', 'lightgray')
56
+ if element.get('fill').strip().lower() != 'none':
57
+ element.set('fill', 'lightgray')
58
+ else:
59
+ element.set('fill', 'none')
57
60
  util.set_attr(element, 'stroke', 'none')
58
61
  util.set_attr(element, 'fill', 'none')
59
62
  util.set_attr(element, 'thickness', '2')
@@ -188,6 +191,9 @@ def spline(element, diagram, parent, outline_status):
188
191
 
189
192
  cs = CubicSpline(t_vals, points, bc_type=bc)
190
193
 
194
+ if element.get('name', None) is not None:
195
+ un.enter_function(element.get('name'), cs)
196
+
191
197
  N = un.valid_eval(element.get('N', '100'))
192
198
  domain = element.get('domain', None)
193
199
  if domain is not None:
prefig/core/read.py CHANGED
@@ -13,9 +13,12 @@ def read(element, diagram, parent, outline_status):
13
13
  return
14
14
  filename = Path(filename)
15
15
  external_root = diagram.get_external()
16
- if external_root is not None:
17
- external_root = Path(external_root)
18
- filename = external_root / filename
16
+ if diagram.get_environment() == "pretext":
17
+ filename = "data" / filename
18
+ else:
19
+ if external_root is not None:
20
+ external_root = Path(external_root)
21
+ filename = external_root / filename
19
22
 
20
23
  name = element.get('name', None)
21
24
  if name is None: