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/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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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"
|
|
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"
|
|
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('
|
|
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('
|
|
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
|
+
|
prefig/core/math_utilities.py
CHANGED
|
@@ -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
|
prefig/core/parametric_curve.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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:
|