Excalidraw-Interface 0.0.1__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.
- Excalidraw_Interface/LICENSE +9 -0
- Excalidraw_Interface/__init__.py +6 -0
- Excalidraw_Interface/defaults.py +54 -0
- Excalidraw_Interface/examples/advanced_groups.py +16 -0
- Excalidraw_Interface/examples/flowchart.py +20 -0
- Excalidraw_Interface/fonts/Assistant-VariableFont.ttf +0 -0
- Excalidraw_Interface/fonts/CascadiaCode.ttf +0 -0
- Excalidraw_Interface/fonts/Virgil.ttf +0 -0
- Excalidraw_Interface/primitives.py +206 -0
- Excalidraw_Interface/sketch_builder.py +241 -0
- Excalidraw_Interface-0.0.1.dist-info/LICENSE +19 -0
- Excalidraw_Interface-0.0.1.dist-info/METADATA +60 -0
- Excalidraw_Interface-0.0.1.dist-info/RECORD +15 -0
- Excalidraw_Interface-0.0.1.dist-info/WHEEL +5 -0
- Excalidraw_Interface-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Modified by Robert Nies
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Aditya Ganeshan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Specify the default values for excalidraw primitives."""
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
# Font family for estimating font size
|
|
5
|
+
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
|
6
|
+
_fonts = {
|
|
7
|
+
1: "fonts/Virgil.ttf",
|
|
8
|
+
2: "fonts/Assistant-VariableFont.ttf",
|
|
9
|
+
3: "fonts/CascadiaCode.ttf",
|
|
10
|
+
}
|
|
11
|
+
FONT_FAMILY = {x: os.path.join(current_file_dir, y) for x, y in _fonts.items()}
|
|
12
|
+
|
|
13
|
+
# Settings for Rectangle, Ellipse, Diamond
|
|
14
|
+
BOX_DEFAULTS = dict(
|
|
15
|
+
angle=0,
|
|
16
|
+
strokeColor="#000000",
|
|
17
|
+
backgroundColor="transparent",
|
|
18
|
+
fillStyle="hachure",
|
|
19
|
+
strokeStyle="solid",
|
|
20
|
+
strokeWidth=1,
|
|
21
|
+
roughness=0,
|
|
22
|
+
opacity=100,
|
|
23
|
+
roundness={"type": 3},
|
|
24
|
+
version=1,
|
|
25
|
+
versionNonce=0,
|
|
26
|
+
isDeleted=False,
|
|
27
|
+
boundElements=[],
|
|
28
|
+
updated=0,
|
|
29
|
+
link=None,
|
|
30
|
+
locked=False,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Extra properties for Text
|
|
34
|
+
TEXT_DEFAULTS = dict(
|
|
35
|
+
fontSize=20,
|
|
36
|
+
fontFamily=3,
|
|
37
|
+
textAlign="left",
|
|
38
|
+
verticalAlign="top",
|
|
39
|
+
baseline=18,
|
|
40
|
+
containerId=None,
|
|
41
|
+
originalText="default text",
|
|
42
|
+
roundness=None,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Extra properties for Line
|
|
47
|
+
LINE_DEFAULTS = dict(
|
|
48
|
+
lastCommitedPoint=None,
|
|
49
|
+
startBinding=None,
|
|
50
|
+
endBinding=None,
|
|
51
|
+
startArrowhead=None,
|
|
52
|
+
endArrowhead=None,
|
|
53
|
+
roundess={"type": 2}, # It has only two configurations None and type 2
|
|
54
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from Excalidraw_Interface import SketchBuilder, Group
|
|
2
|
+
|
|
3
|
+
sb = SketchBuilder()
|
|
4
|
+
|
|
5
|
+
tb_1 = sb.TextBox('Start Here', 0.5, 0.5)
|
|
6
|
+
tb_2 = sb.TextBox('End Here', 100, 100)
|
|
7
|
+
|
|
8
|
+
group_1 = Group([tb_1, tb_2])
|
|
9
|
+
bound_1 = sb.create_bounding_element(group_1)
|
|
10
|
+
|
|
11
|
+
hcb = sb.HeaderContentBox('Header', 'Some Extremely Long Content', 200, 200)
|
|
12
|
+
|
|
13
|
+
group_2 = Group([bound_1, hcb])
|
|
14
|
+
sb.create_bounding_element(group_2, strokeColor="#FF0000",)
|
|
15
|
+
|
|
16
|
+
sb.export_to_file('out.excalidraw')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from Excalidraw_Interface import SketchBuilder
|
|
2
|
+
|
|
3
|
+
flowchart_items = ['First Step', 'Second Step', 'Third Step']
|
|
4
|
+
|
|
5
|
+
sb = SketchBuilder() # Create a Sketch
|
|
6
|
+
|
|
7
|
+
prev_item = sb.TextBox("Start Here", x = 0, y = 0) # Create a Text Box
|
|
8
|
+
for index, item in enumerate(flowchart_items):
|
|
9
|
+
new_item = sb.TextBox(item, x = 0, y = (index+1) * 150) # Create a Text Box
|
|
10
|
+
sb.create_binding_arrows(prev_item, new_item) # Create arrows between boxes
|
|
11
|
+
prev_item = new_item
|
|
12
|
+
|
|
13
|
+
hcb = sb.HeaderContentBox("Header", "Content", x = -200, y = 400) # Create a multiline text box
|
|
14
|
+
circle = sb.Ellipse(200, 400, width=50, height=50, backgroundColor = 'red',
|
|
15
|
+
roughness=1) # Create a red circle in hand drawn style
|
|
16
|
+
|
|
17
|
+
sb.create_binding_arrows(prev_item, hcb, sb.DoubleArrow) # Create a double headed arrow
|
|
18
|
+
sb.create_binding_arrows(prev_item, circle, strokeColor = 'blue') # Create an blue arrow
|
|
19
|
+
|
|
20
|
+
sb.export_to_file('out.excalidraw')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from Excalidraw_Interface.defaults import FONT_FAMILY
|
|
2
|
+
from PIL import ImageFont
|
|
3
|
+
import random
|
|
4
|
+
import uuid
|
|
5
|
+
import math
|
|
6
|
+
import copy
|
|
7
|
+
|
|
8
|
+
class ExcaliDrawPrimitive:
|
|
9
|
+
"""
|
|
10
|
+
Base class for all excalidraw primitives
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, excal_type: str, default_config: dict,
|
|
14
|
+
x: float, y: float, width: float, height: float, **kwargs):
|
|
15
|
+
"""
|
|
16
|
+
Initialize the excalidraw primitive
|
|
17
|
+
:param excal_type: type of the excalidraw primitive
|
|
18
|
+
:param default_config: default values
|
|
19
|
+
:param x: top left x
|
|
20
|
+
:param y: top left y
|
|
21
|
+
:param width: width
|
|
22
|
+
:param height: height
|
|
23
|
+
:param kwargs: additional config to override defaults
|
|
24
|
+
"""
|
|
25
|
+
self.excal_type = excal_type
|
|
26
|
+
self.excal_id = str(uuid.uuid4())
|
|
27
|
+
self.excal_seed = random.randint(0, 100000)
|
|
28
|
+
|
|
29
|
+
self.x = x
|
|
30
|
+
self.y = y
|
|
31
|
+
self.width = width
|
|
32
|
+
self.height = height
|
|
33
|
+
self._groupIds = []
|
|
34
|
+
self.apply_recursive_groups = []
|
|
35
|
+
|
|
36
|
+
self.config = copy.deepcopy(default_config)
|
|
37
|
+
for key, value in kwargs.items():
|
|
38
|
+
if key not in self.config:
|
|
39
|
+
raise Exception(f"Unexpected key for shape {excal_type}: {key}")
|
|
40
|
+
self.config[key] = value
|
|
41
|
+
|
|
42
|
+
def export(self):
|
|
43
|
+
""" Convert the excalidraw primitive specifications to a dictionary """
|
|
44
|
+
export_dict = {'id': self.excal_id, 'type': self.excal_type, 'seed': self.excal_seed,
|
|
45
|
+
'x': self.x, 'y': self.y, 'width': self.width, 'height': self.height,
|
|
46
|
+
'groupIds': self._groupIds}
|
|
47
|
+
export_dict.update(self.config)
|
|
48
|
+
return export_dict
|
|
49
|
+
|
|
50
|
+
def add_to_group(self, group_id):
|
|
51
|
+
self._groupIds.append(group_id)
|
|
52
|
+
for elem in self.apply_recursive_groups:
|
|
53
|
+
elem.add_to_group(group_id)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def center(self):
|
|
57
|
+
""" Return the center of the excalidraw primitive """
|
|
58
|
+
return [self.x + self.width / 2, self.y + self.height / 2]
|
|
59
|
+
|
|
60
|
+
def get_edge_midpoint(self, theta, padding=5):
|
|
61
|
+
# given angle theta measured from the positive x-axis, return the boundary mapped to the center of the edges
|
|
62
|
+
theta_lim = math.atan2(self.height, self.width)
|
|
63
|
+
if abs(theta) < theta_lim or abs(theta) > math.pi - theta_lim:
|
|
64
|
+
sign_indicator = (abs(theta) < math.pi / 2)
|
|
65
|
+
x = self.x + self.width * sign_indicator + \
|
|
66
|
+
padding * (-1 + 2 * sign_indicator)
|
|
67
|
+
y = self.y + (self.height / 2)
|
|
68
|
+
else:
|
|
69
|
+
sign_indicator = (theta > 0)
|
|
70
|
+
x = self.x + self.width / 2
|
|
71
|
+
y = self.y + self.height * sign_indicator + \
|
|
72
|
+
padding * (-1 + 2 * sign_indicator)
|
|
73
|
+
return [x, y]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class Shape(ExcaliDrawPrimitive):
|
|
77
|
+
def __init__(self, shape: str, default_config, x, y, width, height, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Creates a rectangle, ellipse, or diamond
|
|
80
|
+
:param shape: "rectangle", "ellipse", "diamond"
|
|
81
|
+
:param default_config: default values
|
|
82
|
+
:param x: center x
|
|
83
|
+
:param y: center y
|
|
84
|
+
:param width: width
|
|
85
|
+
:param height: height
|
|
86
|
+
:param kwargs: additional config to override defaults
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
x -= width/2
|
|
90
|
+
y -= height/2
|
|
91
|
+
|
|
92
|
+
super().__init__(shape, default_config, x=x, y=y, width=width, height=height, **kwargs)
|
|
93
|
+
|
|
94
|
+
#TODO - fix fonts
|
|
95
|
+
class Text(ExcaliDrawPrimitive):
|
|
96
|
+
def __init__(self, text: str, default_config, x, y, **kwargs):
|
|
97
|
+
"""
|
|
98
|
+
Creates text item, width and height set automatically
|
|
99
|
+
:param text: text
|
|
100
|
+
:param default_config: default values
|
|
101
|
+
:param x: center x
|
|
102
|
+
:param y: center y
|
|
103
|
+
:param kwargs: additional config to override defaults
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
super().__init__('text', default_config, x, y, 0, 0, **kwargs)
|
|
107
|
+
self.text = text
|
|
108
|
+
|
|
109
|
+
d = super().export()
|
|
110
|
+
_font_file = FONT_FAMILY[d['fontFamily']]
|
|
111
|
+
_font = ImageFont.truetype(_font_file, d['fontSize'])
|
|
112
|
+
left, top, right, bottom = _font.getbbox(self.text)
|
|
113
|
+
|
|
114
|
+
self.width = right - left
|
|
115
|
+
self.height = bottom - top
|
|
116
|
+
self.x -= self.width / 2
|
|
117
|
+
self.y -= self.height / 2
|
|
118
|
+
|
|
119
|
+
def export(self):
|
|
120
|
+
d = super().export()
|
|
121
|
+
d['text'] = self.text
|
|
122
|
+
return d
|
|
123
|
+
|
|
124
|
+
class Line(ExcaliDrawPrimitive):
|
|
125
|
+
def __init__(self, excal_type, default_config, start_pt: tuple, end_pt: tuple, **kwargs):
|
|
126
|
+
"""
|
|
127
|
+
Creates a line or arrow
|
|
128
|
+
:param excal_type: 'line' or 'arrow'
|
|
129
|
+
:param default_config: default values
|
|
130
|
+
:param start_pt: start pt | tuple[float, float]
|
|
131
|
+
:param end_pt: end pt | tuple[float, float]
|
|
132
|
+
:param kwargs: additional config to override defaults
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
start_x = start_pt[0]
|
|
136
|
+
start_y = start_pt[1]
|
|
137
|
+
end_x = end_pt[0]
|
|
138
|
+
end_y = end_pt[1]
|
|
139
|
+
width = abs(end_x - start_x)
|
|
140
|
+
height = abs(end_y - start_y)
|
|
141
|
+
self.points = [[0, 0], [end_x - start_x, end_y - start_y]]
|
|
142
|
+
|
|
143
|
+
super().__init__(excal_type, default_config, x=start_x, y=start_y, width=width, height=height, **kwargs)
|
|
144
|
+
|
|
145
|
+
def export(self):
|
|
146
|
+
d = super().export()
|
|
147
|
+
d['points'] = self.points
|
|
148
|
+
return d
|
|
149
|
+
|
|
150
|
+
def set_start_binding(self, element: ExcaliDrawPrimitive, padding=10):
|
|
151
|
+
""" Set binding in both the line as well as the element."""
|
|
152
|
+
self.config["startBinding"] = {
|
|
153
|
+
"elementId": element.excal_id,
|
|
154
|
+
"focus": 0,
|
|
155
|
+
"gap": padding
|
|
156
|
+
}
|
|
157
|
+
bound_e = element.config.get('boundElements', [])
|
|
158
|
+
bound_e.append({
|
|
159
|
+
"id": self.excal_id,
|
|
160
|
+
"type": self.excal_type
|
|
161
|
+
})
|
|
162
|
+
element.config['boundElements'] = bound_e
|
|
163
|
+
|
|
164
|
+
def set_end_binding(self, element, padding=10):
|
|
165
|
+
""" Set binding in both the line as well as the element."""
|
|
166
|
+
self.config["endBinding"] = {
|
|
167
|
+
"elementId": element.excal_id,
|
|
168
|
+
"focus": 0,
|
|
169
|
+
"gap": padding
|
|
170
|
+
}
|
|
171
|
+
bound_e = element.config.get('boundElements', [])
|
|
172
|
+
bound_e.append({
|
|
173
|
+
"id": self.excal_id,
|
|
174
|
+
"type": self.excal_type
|
|
175
|
+
})
|
|
176
|
+
element.config['boundElements'] = bound_e
|
|
177
|
+
|
|
178
|
+
class Group(ExcaliDrawPrimitive):
|
|
179
|
+
def __init__(self, elems: list[ExcaliDrawPrimitive], first_is_group = False):
|
|
180
|
+
"""
|
|
181
|
+
Create a group (a fake element for handling group logic)
|
|
182
|
+
:param elems: list of excalidraw objects to group
|
|
183
|
+
:param first_is_group: treat the first element of the list as representing the whole group
|
|
184
|
+
"""
|
|
185
|
+
group_id = str(uuid.uuid4())
|
|
186
|
+
|
|
187
|
+
left_x = min([elem.x for elem in elems])
|
|
188
|
+
top_y = min([elem.y for elem in elems])
|
|
189
|
+
width = max([elem.x + elem.width - left_x for elem in elems])
|
|
190
|
+
height = max([elem.y + elem.height - top_y for elem in elems])
|
|
191
|
+
|
|
192
|
+
for elem in elems:
|
|
193
|
+
elem.add_to_group(group_id)
|
|
194
|
+
|
|
195
|
+
super().__init__('group', {}, left_x, top_y, width, height)
|
|
196
|
+
self.elems = elems
|
|
197
|
+
|
|
198
|
+
if first_is_group:
|
|
199
|
+
elems[0].apply_recursive_groups.extend(elems[1:])
|
|
200
|
+
|
|
201
|
+
def export(self):
|
|
202
|
+
raise Exception("Group should not be exported - a group was incorrectly added to sketch.")
|
|
203
|
+
|
|
204
|
+
def add_to_group(self, group_id):
|
|
205
|
+
for elem in self.elems:
|
|
206
|
+
elem.add_to_group(group_id)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
from Excalidraw_Interface import primitives, defaults
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
import copy
|
|
5
|
+
|
|
6
|
+
def none_to_empty_dict(x):
|
|
7
|
+
if x is None:
|
|
8
|
+
return {}
|
|
9
|
+
return x
|
|
10
|
+
|
|
11
|
+
class SketchBuilder:
|
|
12
|
+
def __init__(self, **custom_defaults):
|
|
13
|
+
"""
|
|
14
|
+
Create a sketch
|
|
15
|
+
:param custom_defaults: overrides normal defaults
|
|
16
|
+
"""
|
|
17
|
+
self._draw_objs: list[primitives.ExcaliDrawPrimitive] = []
|
|
18
|
+
|
|
19
|
+
self.BOX_DEFAULTS = copy.deepcopy(defaults.BOX_DEFAULTS)
|
|
20
|
+
|
|
21
|
+
self.LINE_DEFAULTS = copy.deepcopy(defaults.BOX_DEFAULTS)
|
|
22
|
+
self.LINE_DEFAULTS.update(copy.deepcopy(defaults.LINE_DEFAULTS))
|
|
23
|
+
|
|
24
|
+
self.TEXT_DEFAULTS = copy.deepcopy(defaults.BOX_DEFAULTS)
|
|
25
|
+
self.TEXT_DEFAULTS.update(copy.deepcopy(defaults.TEXT_DEFAULTS))
|
|
26
|
+
|
|
27
|
+
for key, value in custom_defaults.items():
|
|
28
|
+
used = False
|
|
29
|
+
|
|
30
|
+
if key in self.BOX_DEFAULTS:
|
|
31
|
+
self.BOX_DEFAULTS[key] = value
|
|
32
|
+
used = True
|
|
33
|
+
|
|
34
|
+
if key in self.LINE_DEFAULTS:
|
|
35
|
+
self.BOX_DEFAULTS[key] = value
|
|
36
|
+
used = True
|
|
37
|
+
|
|
38
|
+
if key in self.TEXT_DEFAULTS:
|
|
39
|
+
self.BOX_DEFAULTS[key] = value
|
|
40
|
+
used = True
|
|
41
|
+
|
|
42
|
+
if not used:
|
|
43
|
+
raise Exception(f"Key <{key}> not used.")
|
|
44
|
+
|
|
45
|
+
def add_element(self, element: primitives.ExcaliDrawPrimitive):
|
|
46
|
+
self._draw_objs.append(element)
|
|
47
|
+
return element
|
|
48
|
+
|
|
49
|
+
def Rectangle(self, x: float, y: float, width: float = 100, height: float = 100, **kwargs):
|
|
50
|
+
"""
|
|
51
|
+
Creates a rectangle
|
|
52
|
+
:param x: center x
|
|
53
|
+
:param y: center y
|
|
54
|
+
:param width: width
|
|
55
|
+
:param height: height
|
|
56
|
+
:param kwargs: additional config to override defaults
|
|
57
|
+
"""
|
|
58
|
+
return self.add_element(primitives.Shape('rectangle', self.BOX_DEFAULTS, x, y, width, height, **kwargs))
|
|
59
|
+
|
|
60
|
+
def Diamond(self, x: float, y: float, width: float = 100, height: float = 100, **kwargs):
|
|
61
|
+
"""
|
|
62
|
+
Creates a diamond
|
|
63
|
+
:param x: center x
|
|
64
|
+
:param y: center y
|
|
65
|
+
:param width: width
|
|
66
|
+
:param height: height
|
|
67
|
+
:param kwargs: additional config to override defaults
|
|
68
|
+
"""
|
|
69
|
+
return self.add_element(primitives.Shape('diamond', self.BOX_DEFAULTS, x, y, width, height, **kwargs))
|
|
70
|
+
|
|
71
|
+
def Ellipse(self, x: float, y: float, width: float = 100, height: float = 100, **kwargs):
|
|
72
|
+
"""
|
|
73
|
+
Creates a ellipse
|
|
74
|
+
:param x: center x
|
|
75
|
+
:param y: center y
|
|
76
|
+
:param width: width
|
|
77
|
+
:param height: height
|
|
78
|
+
:param kwargs: additional config to override defaults
|
|
79
|
+
"""
|
|
80
|
+
return self.add_element(primitives.Shape('ellipse', self.BOX_DEFAULTS, x, y, width, height, **kwargs))
|
|
81
|
+
|
|
82
|
+
def Text(self, text: str, x: float, y: float, **kwargs):
|
|
83
|
+
"""
|
|
84
|
+
Creates text item, width and height set automatically
|
|
85
|
+
:param text: text
|
|
86
|
+
:param x: center x
|
|
87
|
+
:param y: center y
|
|
88
|
+
:param kwargs: additional config to override defaults
|
|
89
|
+
"""
|
|
90
|
+
return self.add_element(primitives.Text(text, self.TEXT_DEFAULTS, x, y, **kwargs))
|
|
91
|
+
|
|
92
|
+
def Line(self, start_pt: tuple, end_pt: tuple, **kwargs):
|
|
93
|
+
"""
|
|
94
|
+
Creates a line
|
|
95
|
+
:param start_pt: start pt | tuple[float, float]
|
|
96
|
+
:param end_pt: end pt | tuple[float, float]
|
|
97
|
+
:param kwargs: additional config to override defaults
|
|
98
|
+
"""
|
|
99
|
+
return self.add_element(primitives.Line('line', self.LINE_DEFAULTS, start_pt, end_pt, **kwargs))
|
|
100
|
+
|
|
101
|
+
def Arrow(self, start_pt: tuple, end_pt: tuple, **kwargs):
|
|
102
|
+
"""
|
|
103
|
+
Creates an arrow
|
|
104
|
+
:param start_pt: start pt | tuple[float, float]
|
|
105
|
+
:param end_pt: end pt | tuple[float, float]
|
|
106
|
+
:param kwargs: additional config to override defaults
|
|
107
|
+
"""
|
|
108
|
+
kwargs['endArrowhead'] = 'arrow'
|
|
109
|
+
arrow = primitives.Line('arrow', self.LINE_DEFAULTS, start_pt, end_pt, **kwargs)
|
|
110
|
+
self.add_element(arrow)
|
|
111
|
+
return arrow
|
|
112
|
+
|
|
113
|
+
def DoubleArrow(self, start_pt: tuple, end_pt: tuple, **kwargs):
|
|
114
|
+
"""
|
|
115
|
+
Creates a bi-directional arrow
|
|
116
|
+
:param start_pt: start pt | tuple[float, float]
|
|
117
|
+
:param end_pt: end pt | tuple[float, float]
|
|
118
|
+
:param kwargs: additional config to override defaults
|
|
119
|
+
"""
|
|
120
|
+
kwargs['startArrowhead'] = 'arrow'
|
|
121
|
+
return self.Arrow(start_pt, end_pt, **kwargs)
|
|
122
|
+
|
|
123
|
+
def export_to_file(self, save_path):
|
|
124
|
+
"""Export the sketch to a excalidraw file."""
|
|
125
|
+
data = {
|
|
126
|
+
"type": "excalidraw",
|
|
127
|
+
"version": 1,
|
|
128
|
+
"source": "Excalidraw.py",
|
|
129
|
+
"elements": [],
|
|
130
|
+
"appState": {
|
|
131
|
+
"viewBackgroundColor": "#ffffff",
|
|
132
|
+
"gridSize": None,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for element in self._draw_objs:
|
|
137
|
+
json_obj = element.export()
|
|
138
|
+
data['elements'].append(json_obj)
|
|
139
|
+
|
|
140
|
+
if save_path.split(".")[-1] != "excalidraw":
|
|
141
|
+
save_path += ".excalidraw"
|
|
142
|
+
|
|
143
|
+
with open(save_path, "w") as file_reader:
|
|
144
|
+
json.dump(data, file_reader, indent=4)
|
|
145
|
+
|
|
146
|
+
def create_binding_arrows(self, start: primitives.ExcaliDrawPrimitive, end: primitives.ExcaliDrawPrimitive,
|
|
147
|
+
arrow = None, padding:int=10, **kwargs):
|
|
148
|
+
"""
|
|
149
|
+
Create a binding arrow between two elements.
|
|
150
|
+
:param start: primitives.ExcaliDrawPrimitives
|
|
151
|
+
:param end: primitives.ExcaliDrawPrimitives
|
|
152
|
+
:param arrow: self.Arrow or self.DoubleArrow
|
|
153
|
+
:param padding: defaults to 10
|
|
154
|
+
:param kwargs: passed to arrow
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
if arrow is None:
|
|
158
|
+
arrow = self.Arrow
|
|
159
|
+
|
|
160
|
+
center_start = start.center
|
|
161
|
+
center_end = end.center
|
|
162
|
+
dx = center_end[0] - center_start[0]
|
|
163
|
+
dy = center_end[1] - center_start[1]
|
|
164
|
+
theta = math.atan2(dy, dx)
|
|
165
|
+
theta = (theta + math.pi) % (2*math.pi) - math.pi
|
|
166
|
+
inverted_theta = theta % (2*math.pi) - math.pi
|
|
167
|
+
start_pt = start.get_edge_midpoint(theta, padding=padding)
|
|
168
|
+
end_pt = end.get_edge_midpoint(inverted_theta, padding=padding)
|
|
169
|
+
|
|
170
|
+
line = arrow(start_pt, end_pt, **kwargs)
|
|
171
|
+
line.set_start_binding(start, padding=padding)
|
|
172
|
+
line.set_end_binding(end, padding=padding)
|
|
173
|
+
|
|
174
|
+
def create_bounding_element(self, element: primitives.ExcaliDrawPrimitive, function = None, padding = 10, **kwargs):
|
|
175
|
+
"""
|
|
176
|
+
Create a bounding box around the given element/group.
|
|
177
|
+
|
|
178
|
+
:param element: primitives.ExcaliDrawPrimitives
|
|
179
|
+
:param function: self.Rectangle or similar, defaults to self.Rectangle
|
|
180
|
+
:param padding: defaults to 10
|
|
181
|
+
:param kwargs: passed to arrow
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if function is None:
|
|
185
|
+
function = self.Rectangle
|
|
186
|
+
|
|
187
|
+
element_center = element.center
|
|
188
|
+
new_x = element_center[0]
|
|
189
|
+
new_y = element_center[1]
|
|
190
|
+
|
|
191
|
+
new_width = element.width + 2 * padding
|
|
192
|
+
new_height = element.height + 2 * padding
|
|
193
|
+
|
|
194
|
+
bounding_elem = function(x=new_x, y=new_y, width=new_width, height=new_height, **kwargs)
|
|
195
|
+
primitives.Group([bounding_elem, element], first_is_group=True)
|
|
196
|
+
|
|
197
|
+
return bounding_elem
|
|
198
|
+
|
|
199
|
+
def TextBox(self, text: str, x: float, y: float, txt_kwargs = None, rect_kwargs = None):
|
|
200
|
+
"""
|
|
201
|
+
Creates a text box
|
|
202
|
+
:param text: text
|
|
203
|
+
:param x: center x
|
|
204
|
+
:param y: center y
|
|
205
|
+
:param txt_kwargs: other properties for text
|
|
206
|
+
:param rect_kwargs: other properties for rect
|
|
207
|
+
"""
|
|
208
|
+
txt_kwargs = none_to_empty_dict(txt_kwargs)
|
|
209
|
+
rect_kwargs = none_to_empty_dict(rect_kwargs)
|
|
210
|
+
|
|
211
|
+
txt = self.Text(text, x, y, **txt_kwargs)
|
|
212
|
+
return self.create_bounding_element(txt, self.Rectangle, **rect_kwargs)
|
|
213
|
+
|
|
214
|
+
def HeaderContentBox(self, header: str, content: str, x: float, y: float, padding = 10,
|
|
215
|
+
header_kwargs = None, content_kwargs = None, rect_kwargs = None):
|
|
216
|
+
"""
|
|
217
|
+
Creates a multiline text box
|
|
218
|
+
:param header: text
|
|
219
|
+
:param content: text
|
|
220
|
+
:param x: center x
|
|
221
|
+
:param y: center y
|
|
222
|
+
:param padding: defaults to 10
|
|
223
|
+
:param header_kwargs: other properties for text
|
|
224
|
+
:param content_kwargs: other properties for text
|
|
225
|
+
:param rect_kwargs: other properties for rect
|
|
226
|
+
"""
|
|
227
|
+
header_kwargs = none_to_empty_dict(header_kwargs)
|
|
228
|
+
content_kwargs = none_to_empty_dict(content_kwargs)
|
|
229
|
+
rect_kwargs = none_to_empty_dict(rect_kwargs)
|
|
230
|
+
|
|
231
|
+
fontSize = int(self.TEXT_DEFAULTS['fontSize'] * 2/3)
|
|
232
|
+
|
|
233
|
+
header_item = self.Text(header, x, y, **header_kwargs)
|
|
234
|
+
content_item = self.Text(content, x, y, fontSize=fontSize, **content_kwargs)
|
|
235
|
+
header_item.y -= header_item.height/2 + padding/2
|
|
236
|
+
content_item.y += content_item.height/2 + padding/2
|
|
237
|
+
|
|
238
|
+
return self.create_bounding_element(primitives.Group([header_item, content_item]),
|
|
239
|
+
self.Rectangle, **rect_kwargs)
|
|
240
|
+
|
|
241
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2024 Robert Nies
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to
|
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
8
|
+
so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: Excalidraw_Interface
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Create excalidraw drawings with python
|
|
5
|
+
Home-page: https://github.com/RobertJN64/Excalidraw_Interface
|
|
6
|
+
Author: Robert Nies
|
|
7
|
+
Author-email: robertjnies@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Python: >=3.8
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: pillow
|
|
21
|
+
|
|
22
|
+
# Excalidraw_Interface
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
A pythonic interface for creating diagrams in Excalidraw.
|
|
29
|
+
|
|
30
|
+
Based on https://github.com/BardOfCodes/procXD by Aditya Ganeshan (MIT License) and updated with modern python support
|
|
31
|
+
and API improvements.
|
|
32
|
+
|
|
33
|
+
## Example: Flowchart
|
|
34
|
+
|
|
35
|
+
[flowchart.py](Excalidraw_Interface/examples/flowchart.py)
|
|
36
|
+
|
|
37
|
+

|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from Excalidraw_Interface import SketchBuilder
|
|
41
|
+
|
|
42
|
+
flowchart_items = ['First Step', 'Second Step', 'Third Step']
|
|
43
|
+
|
|
44
|
+
sb = SketchBuilder() # Create a Sketch
|
|
45
|
+
|
|
46
|
+
prev_item = sb.TextBox("Start Here", x = 0, y = 0) # Create a Text Box
|
|
47
|
+
for index, item in enumerate(flowchart_items):
|
|
48
|
+
new_item = sb.TextBox(item, x = 0, y = (index+1) * 150) # Create a Text Box
|
|
49
|
+
sb.create_binding_arrows(prev_item, new_item) # Create arrows between boxes
|
|
50
|
+
prev_item = new_item
|
|
51
|
+
|
|
52
|
+
hcb = sb.HeaderContentBox("Header", "Content", x = -200, y = 400) # Create a multiline text box
|
|
53
|
+
circle = sb.Ellipse(200, 400, width=50, height=50, backgroundColor = 'red',
|
|
54
|
+
roughness=1) # Create a red circle in hand drawn style
|
|
55
|
+
|
|
56
|
+
sb.create_binding_arrows(prev_item, hcb, sb.DoubleArrow) # Create a double headed arrow
|
|
57
|
+
sb.create_binding_arrows(prev_item, circle, strokeColor = 'blue') # Create an blue arrow
|
|
58
|
+
|
|
59
|
+
sb.export_to_file('out.excalidraw')
|
|
60
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Excalidraw_Interface/LICENSE,sha256=vYmfhy4gf2coErkxHSgUP7zb6WENqyG41dsyYIujqtE,1091
|
|
2
|
+
Excalidraw_Interface/__init__.py,sha256=l22sr6eOlpP_npl_bmQVPoWRiF5YEzdMAs2TmW4sgks,211
|
|
3
|
+
Excalidraw_Interface/defaults.py,sha256=QoRj79j0d_P1ibTlBHuhkKsC_sSPQTnAYwQP4v8kCa0,1242
|
|
4
|
+
Excalidraw_Interface/primitives.py,sha256=uFqVRkpvvH7rBDW5l7hZdFolOEYFpD4_q7ISKvsGWXA,7287
|
|
5
|
+
Excalidraw_Interface/sketch_builder.py,sha256=zoa9iP06utfqBRvCnv2ZOQZQTElql5844mxDDLCWjU4,8878
|
|
6
|
+
Excalidraw_Interface/examples/advanced_groups.py,sha256=diQqtt89FvN-MOLFm0-nirv00nGRC1b1VwTg9XemE0Y,443
|
|
7
|
+
Excalidraw_Interface/examples/flowchart.py,sha256=rEHzX-pCx48mY8msKjpiZiwE-EGowxlYqzPhnaCaDUE,916
|
|
8
|
+
Excalidraw_Interface/fonts/Assistant-VariableFont.ttf,sha256=zXRaFBMUUNiaDC8P4RtWeZpV1prWpIsvWexaf_I34m0,79660
|
|
9
|
+
Excalidraw_Interface/fonts/CascadiaCode.ttf,sha256=bMImJUcgotsFnWr0s_37IR6k8JQNzuscb_UsWpSinzs,648732
|
|
10
|
+
Excalidraw_Interface/fonts/Virgil.ttf,sha256=VvZN1QlKTwyUk7R7U4NMQgXvv9bAljLOfEj32qh1v1k,80720
|
|
11
|
+
Excalidraw_Interface-0.0.1.dist-info/LICENSE,sha256=KHnSJvDPomXIpSt5dhDeRVUkjv2pX-igPsphn1sPDyI,1054
|
|
12
|
+
Excalidraw_Interface-0.0.1.dist-info/METADATA,sha256=PJxWaOSWyOCRNl9tNZQzeU7N9kBYrjq41CRhfZebolk,2311
|
|
13
|
+
Excalidraw_Interface-0.0.1.dist-info/WHEEL,sha256=Mdi9PDNwEZptOjTlUcAth7XJDFtKrHYaQMPulZeBCiQ,91
|
|
14
|
+
Excalidraw_Interface-0.0.1.dist-info/top_level.txt,sha256=XCwxnUO1EE0YkS-IVQpi8D320gGVWWcDbh3TDCqzkTs,21
|
|
15
|
+
Excalidraw_Interface-0.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Excalidraw_Interface
|