Skip to content

Commit 1b7d968

Browse files
committed
add Chem437 block set
1 parent 8a5d2e1 commit 1b7d968

File tree

10,733 files changed

+621347
-44122
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

10,733 files changed

+621347
-44122
lines changed

src/assets/blocks/10x10x10palette/smi/A_10_L.smi renamed to scripts/generate_assets.py

File renamed without changes.

scripts/process_block_svg.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import os
2+
import re
3+
from typing import List, Tuple
4+
from xml.dom.minidom import parse, Text, Element
5+
6+
import numpy as np
7+
8+
workdir = "./src/assets/blocks/chem-437"
9+
10+
11+
def tokenize(expression: str) -> List[str]:
12+
return re.findall(r"[+-]?(?:\d*\.\d+|\d+)|[A-Za-z]+", expression)
13+
14+
15+
def parse_transform(node: Element) -> np.array:
16+
if "transform" not in node.attributes:
17+
return np.identity(3)
18+
19+
tokens = tokenize(node.getAttribute("transform"))
20+
if tokens[0] != "matrix" or len(tokens) != 7:
21+
raise SyntaxError("transform matrix must have 6 components")
22+
23+
a, b, c, d, tx, ty = [float(n) for n in tokens[1:]]
24+
return np.array([[a, c, tx], [b, d, ty], [0, 0, 1]])
25+
26+
27+
def get_arc_bbox(
28+
x1: float,
29+
y1: float,
30+
x2: float,
31+
y2: float,
32+
rx: float,
33+
ry: float,
34+
theta: float,
35+
large_arc_flag: bool,
36+
sweep_flag: bool,
37+
) -> List[Tuple[float, float]]:
38+
"""
39+
See: https://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
40+
"""
41+
42+
x1_ = (np.cos(theta) * (x1 - x2)) / 2 + (np.sin(theta) * (y1 - y2)) / 2
43+
y1_ = (-np.sin(theta) * (x1 - x2)) / 2 + (np.cos(theta) * (y1 - y2)) / 2
44+
45+
factor = np.sqrt(
46+
max(
47+
0,
48+
(rx**2 * ry**2 - rx**2 * y1_**2 - ry**2 * x1_**2)
49+
/ (
50+
rx**2 * y1_**2 + ry**2 * x1_**2
51+
), # this could become negative, probably due to rounding error
52+
)
53+
)
54+
if large_arc_flag == sweep_flag:
55+
factor = -factor
56+
57+
cx_ = (factor * rx * y1_) / ry
58+
cy_ = (-factor * ry * x1_) / rx
59+
60+
cx = np.cos(theta) * cx_ - np.sin(theta) * cy_ + (x1 + x2) / 2
61+
cy = np.sin(theta) * cx_ + np.cos(theta) * cy_ + (y1 + y2) / 2
62+
63+
def angle(ux: float, uy: float, vx: float, vy: float) -> float:
64+
sign = 1 if ux * vy - uy * vx > 0 else -1
65+
return (
66+
np.arccos(
67+
(ux * vx + uy * vy) / (np.sqrt(ux**2 + uy**2) * np.sqrt(vx**2 + vy**2))
68+
)
69+
* sign
70+
)
71+
72+
start_angle = angle(1, 0, x1_ - cx_, y1_ - cy_)
73+
delta = angle(x1_ - cx_, y1_ - cy_, -x1_ - cx_, -y1_ - cy_)
74+
75+
if sweep_flag and delta < 0:
76+
delta += 2 * np.pi
77+
elif not sweep_flag and delta > 0:
78+
delta -= 2 * np.pi
79+
end_angle = start_angle + delta
80+
81+
counterclockwise = not sweep_flag
82+
83+
r = max(rx, ry)
84+
85+
# the bounding box of the circumcircle of the (entire) ellipse
86+
return [(cx - r, cy - r), (cx - r, cy + r), (cx + r, cy - r), (cx + r, cy + r)]
87+
88+
89+
def parse_path(node: Element) -> List[Tuple[float, float]]:
90+
if "d" not in node.attributes:
91+
return []
92+
93+
points = []
94+
tokens = tokenize(node.getAttribute("d"))
95+
96+
while tokens:
97+
if tokens[0] == "M" or tokens[0] == "L":
98+
i = 1
99+
while not tokens[i].isalpha():
100+
points.append((float(tokens[i]), float(tokens[i + 1])))
101+
i += 2
102+
tokens = tokens[i:]
103+
elif tokens[0] == "A":
104+
i = 1
105+
while not tokens[i].isalpha():
106+
rx, ry, theta, large_arc_flag, sweep_flag, x2, y2 = [
107+
float(n) for n in tokens[i : i + 7]
108+
]
109+
x1, y1 = points[-1]
110+
points += get_arc_bbox(
111+
x1,
112+
y1,
113+
x2,
114+
y2,
115+
rx,
116+
ry,
117+
theta,
118+
bool(large_arc_flag),
119+
bool(sweep_flag),
120+
)
121+
points.append((x2, y2))
122+
i += 7
123+
tokens = tokens[i:]
124+
elif tokens[0] == "Z":
125+
break
126+
else:
127+
raise SyntaxError(f"path command `{tokens[0]}` not supported")
128+
129+
return points
130+
131+
132+
def process_path(node: Element, parent_transform=np.identity(3)):
133+
if "fill" in node.attributes:
134+
node.removeAttribute("fill")
135+
136+
transform = parse_transform(node)
137+
points = parse_path(node)
138+
return [parent_transform @ transform @ (p[0], p[1], 1) for p in points]
139+
140+
141+
def process_text(node: Element, parent_transform=np.identity(3)):
142+
if "fill" in node.attributes:
143+
node.removeAttribute("fill")
144+
145+
text = ""
146+
for child in node.childNodes:
147+
if isinstance(child, Text):
148+
text += child.wholeText
149+
150+
font_size = node.getAttribute("font-size")
151+
152+
if font_size.endswith("px"):
153+
font_size = font_size[:-2]
154+
font_size = float(font_size) if font_size else 100
155+
156+
# estimate text dimensions using font size (TODO: this is not accurate at all)
157+
h = font_size * 0.8
158+
w = font_size * 0.5 * len(text)
159+
160+
x = float(node.getAttribute("x"))
161+
y = float(node.getAttribute("y"))
162+
163+
points = [(x, y - h), (x + w, y - h), (x + w, y), (x, y)]
164+
165+
transform = parse_transform(node)
166+
167+
return [parent_transform @ transform @ (p[0], p[1], 1) for p in points]
168+
169+
170+
def process_group(group: Element, parent_transform=np.identity(3)):
171+
all_points = []
172+
173+
transform = parent_transform @ parse_transform(group)
174+
175+
for node in group.childNodes:
176+
if isinstance(node, Element):
177+
if node.tagName == "g":
178+
all_points += process_group(node, transform)
179+
elif node.tagName == "path":
180+
all_points += process_path(node, transform)
181+
elif node.tagName == "text":
182+
all_points += process_text(node, transform)
183+
else:
184+
raise SyntaxError(f"tag name {node.tagName} not supported")
185+
186+
return all_points
187+
188+
189+
def get_svg_bbox(node: Element):
190+
all_points = process_group(node)
191+
192+
min_x = np.min([p[0] / p[2] for p in all_points])
193+
max_x = np.max([p[0] / p[2] for p in all_points])
194+
min_y = np.min([p[1] / p[2] for p in all_points])
195+
max_y = np.max([p[1] / p[2] for p in all_points])
196+
197+
return min_x, min_y, max_x - min_x, max_y - min_y
198+
199+
200+
def add_class_names(group: Element, class_names: List[str]):
201+
for node in group.childNodes:
202+
if isinstance(node, Element):
203+
if node.tagName == "g":
204+
add_class_names(node, class_names)
205+
elif node.tagName == "path":
206+
if "d" in node.attributes and "A" in node.getAttribute("d"):
207+
# assume elliptical arcs are actually circles (i.e. connection points)
208+
node.setAttribute("stroke-width", "18px")
209+
node.setAttribute("class", class_names[0])
210+
class_names.pop(0)
211+
212+
213+
def process_svg(filename, class_names):
214+
xml = parse(filename)
215+
svg = xml.getElementsByTagName("svg")[0]
216+
217+
add_class_names(svg, class_names)
218+
x, y, width, height = get_svg_bbox(svg)
219+
220+
padding = 2
221+
x -= padding
222+
y -= padding
223+
width += 2 * padding
224+
height += 2 * padding
225+
226+
if "width" in svg.attributes:
227+
svg.removeAttribute("width")
228+
if "height" in svg.attributes:
229+
svg.removeAttribute("height")
230+
231+
svg.setAttribute("viewBox", f"0 0 {width} {height}")
232+
233+
if x and y:
234+
g = xml.createElement("g")
235+
g.childNodes = svg.childNodes
236+
g.setAttribute("transform", f"matrix(1 0 0 1 {-x} {-y})")
237+
238+
newline = xml.createTextNode("\n")
239+
svg.childNodes = [newline, g, newline]
240+
241+
with open(filename, "w") as out_file:
242+
svg.writexml(out_file)
243+
print(f"saved to {filename}")
244+
245+
246+
if __name__ == "__main__":
247+
for filename in os.listdir(os.path.join(workdir, "block_svg")):
248+
filepath = os.path.join(workdir, "block_svg", filename)
249+
process_svg(filepath, ["connection_in", "connection_out"])
File renamed without changes.

0 commit comments

Comments
 (0)