mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-14 12:40:43 +00:00
ec31745f0a
* Preferences: preserve Navigation and 3D mouse alongside other panels * Tests: check preference search visibility after scrolling settles * Tests: identify preference search results through indexed row handles
590 lines
24 KiB
Python
590 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Compile docs/design/preferences/figures/*.svg into ui_kit/preferences_figure_data.rs.
|
|
|
|
Each figure becomes a list of drawing operations (rectangles, circles, flattened polygons, text) in
|
|
the figure's own coordinates, with colours as roles (`Ink`) instead of hex values. A role is the
|
|
light/dark colour pair the board used at that position; docs/design/preferences/tokens.md says which
|
|
`theme::Tokens` field each role reads. Run from the repository root:
|
|
|
|
python3 tools/preferences-figures/generate.py
|
|
"""
|
|
import collections
|
|
import json
|
|
import math
|
|
import os
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
|
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
|
FIGS = os.path.join(ROOT, 'docs/design/preferences/figures')
|
|
OUT = os.path.join(ROOT, 'crates/ui-egui/src/ui_kit/preferences_figure_data.rs')
|
|
|
|
# Skipped: the Orientation figures already exist in ui_kit and stay as they are.
|
|
SKIP = {'orientation-z-up', 'orientation-y-up', 'style-shaded-edges'}
|
|
THEMES = ['theme-light', 'theme-dark', 'theme-gruvbox', 'theme-imported']
|
|
|
|
PAIRS = {
|
|
('#F7F8FA', '#1C1F26'): 'Ground',
|
|
('#6F7C8F', '#A3AEC2'): 'Line',
|
|
('#236EAD', '#8CBEF5'): 'Accent',
|
|
('#FFFFFF', '#6B7790'): 'Top',
|
|
('#4A5868', '#B4BBC8'): 'Dim',
|
|
('#E4E9F0', '#515B72'): 'Left',
|
|
('#C9D2DE', '#394256'): 'Right',
|
|
('#D5DBE3', '#3F4552'): 'Border',
|
|
('#C3CBD6', '#4A5262'): 'ControlBorder',
|
|
('#FFFFFF', '#2F343E'): 'Card',
|
|
('#1B2430', '#DEE2E8'): 'Text',
|
|
('#8A5300', '#E8C24A'): 'Reference',
|
|
('#2F6FD0', '#6CA8F7'): 'AxisZ',
|
|
('#D64545', '#F27A7A'): 'AxisX',
|
|
('#2E9E5B', '#62D48E'): 'AxisY',
|
|
('#FFFFFF', '#FFFFFF'): 'White',
|
|
('#B84A00', '#F0A27A'): 'Note',
|
|
('#FBEBDD', '#4A3326'): 'NoteFill',
|
|
('#2F7D5B', '#7FD1AE'): 'Ok',
|
|
('#C98A00', '#E8C24A'): 'Warn',
|
|
('#FFFFFF', '#1C1F26'): 'Control',
|
|
('#0072B2', '#56B4E9'): 'Cv0',
|
|
('#E69F00', '#F0C060'): 'Cv1',
|
|
('#E69F00', '#E69F00'): 'Cv1',
|
|
('#D55E00', '#F0A27A'): 'Cv2',
|
|
('#56B4E9', '#8CBEF5'): 'Cv3',
|
|
('#56B4E9', '#56B4E9'): 'Cv3',
|
|
('#CC79A7', '#CC79A7'): 'Cv4',
|
|
}
|
|
|
|
|
|
def num(v, d=0.0):
|
|
return d if v is None else float(re.match(r'-?[\d.]+(?:e-?\d+)?', v.strip()).group(0))
|
|
|
|
|
|
def parse_color(v):
|
|
"""(hex6 upper, alpha) or None for none/url."""
|
|
if v is None or v == 'none':
|
|
return None
|
|
v = v.strip()
|
|
if v.startswith('url('):
|
|
return ('url', re.search(r'#([\w-]+)', v).group(1))
|
|
m = re.fullmatch(r'#([0-9A-Fa-f]{3,8})', v)
|
|
if not m:
|
|
raise ValueError(v)
|
|
h = m.group(1)
|
|
if len(h) == 3:
|
|
h = ''.join(c * 2 for c in h)
|
|
a = 1.0
|
|
if len(h) == 8:
|
|
a = int(h[6:], 16) / 255.0
|
|
h = h[:6]
|
|
return ('#' + h.upper(), a)
|
|
|
|
|
|
def mat_mul(a, b):
|
|
return (a[0] * b[0] + a[2] * b[1], a[1] * b[0] + a[3] * b[1], a[0] * b[2] + a[2] * b[3], a[1] * b[2] + a[3] * b[3],
|
|
a[0] * b[4] + a[2] * b[5] + a[4], a[1] * b[4] + a[3] * b[5] + a[5])
|
|
|
|
|
|
def parse_transform(s):
|
|
m = (1, 0, 0, 1, 0, 0)
|
|
for name, args in re.findall(r'(\w+)\(([^)]*)\)', s or ''):
|
|
v = [float(x) for x in re.split(r'[ ,]+', args.strip())]
|
|
if name == 'translate':
|
|
t = (1, 0, 0, 1, v[0], v[1] if len(v) > 1 else 0)
|
|
elif name == 'scale':
|
|
t = (v[0], 0, 0, v[1] if len(v) > 1 else v[0], 0, 0)
|
|
elif name == 'matrix':
|
|
t = tuple(v)
|
|
else:
|
|
raise ValueError(name)
|
|
m = mat_mul(m, t)
|
|
return m
|
|
|
|
|
|
def ap(m, x, y):
|
|
return (m[0] * x + m[2] * y + m[4], m[1] * x + m[3] * y + m[5])
|
|
|
|
|
|
def flatten_path(d, m):
|
|
toks = re.findall(r'[MmLlHhVvZzAaQqCc]|-?[\d.]+(?:e-?\d+)?', d)
|
|
i = 0
|
|
subs = []
|
|
cur = None
|
|
x = y = sx = sy = 0.0
|
|
cmd = None
|
|
|
|
def nxt():
|
|
nonlocal i
|
|
v = float(toks[i])
|
|
i += 1
|
|
return v
|
|
|
|
def add(px, py):
|
|
cur.append(ap(m, px, py))
|
|
|
|
while i < len(toks):
|
|
if re.fullmatch(r'[A-Za-z]', toks[i]):
|
|
cmd = toks[i]
|
|
i += 1
|
|
if cmd in 'Zz':
|
|
if cur:
|
|
subs.append((cur, True))
|
|
cur = None
|
|
x, y = sx, sy
|
|
continue
|
|
c = cmd
|
|
if c in 'Mm':
|
|
if cur and len(cur) > 0:
|
|
subs.append((cur, False))
|
|
nx, ny = nxt(), nxt()
|
|
if c == 'm':
|
|
nx, ny = x + nx, y + ny
|
|
x, y = sx, sy = nx, ny
|
|
cur = []
|
|
add(x, y)
|
|
cmd = 'L' if c == 'M' else 'l'
|
|
elif c in 'Ll':
|
|
nx, ny = nxt(), nxt()
|
|
if c == 'l':
|
|
nx, ny = x + nx, y + ny
|
|
x, y = nx, ny
|
|
if cur is None:
|
|
cur = []
|
|
add(sx, sy)
|
|
add(x, y)
|
|
elif c in 'Hh':
|
|
nx = nxt()
|
|
x = x + nx if c == 'h' else nx
|
|
if cur is None:
|
|
cur = []
|
|
add(sx, sy)
|
|
add(x, y)
|
|
elif c in 'Vv':
|
|
ny = nxt()
|
|
y = y + ny if c == 'v' else ny
|
|
if cur is None:
|
|
cur = []
|
|
add(sx, sy)
|
|
add(x, y)
|
|
elif c in 'Qq':
|
|
a, b, e, f = nxt(), nxt(), nxt(), nxt()
|
|
if c == 'q':
|
|
a, b, e, f = x + a, y + b, x + e, y + f
|
|
for k in range(1, 13):
|
|
t = k / 12
|
|
add((1 - t) ** 2 * x + 2 * (1 - t) * t * a + t * t * e, (1 - t) ** 2 * y + 2 * (1 - t) * t * b + t * t * f)
|
|
x, y = e, f
|
|
elif c in 'Cc':
|
|
a, b, e, f, g, h = (nxt() for _ in range(6))
|
|
if c == 'c':
|
|
a, b, e, f, g, h = x + a, y + b, x + e, y + f, x + g, y + h
|
|
for k in range(1, 17):
|
|
t = k / 16
|
|
u = 1 - t
|
|
add(u ** 3 * x + 3 * u * u * t * a + 3 * u * t * t * e + t ** 3 * g, u ** 3 * y + 3 * u * u * t * b + 3 * u * t * t * f + t ** 3 * h)
|
|
x, y = g, h
|
|
elif c in 'Aa':
|
|
rx, ry, rot, la, sw, ex, ey = (nxt() for _ in range(7))
|
|
if c == 'a':
|
|
ex, ey = x + ex, y + ey
|
|
pts = arc(x, y, rx, ry, rot, int(la), int(sw), ex, ey)
|
|
for px, py in pts:
|
|
add(px, py)
|
|
x, y = ex, ey
|
|
else:
|
|
raise ValueError(c)
|
|
if cur:
|
|
subs.append((cur, False))
|
|
return subs
|
|
|
|
|
|
def arc(x1, y1, rx, ry, phi, fa, fs, x2, y2):
|
|
if rx == 0 or ry == 0:
|
|
return [(x2, y2)]
|
|
ph = math.radians(phi)
|
|
cp, sp = math.cos(ph), math.sin(ph)
|
|
dx, dy = (x1 - x2) / 2, (y1 - y2) / 2
|
|
x1p, y1p = cp * dx + sp * dy, -sp * dx + cp * dy
|
|
rx, ry = abs(rx), abs(ry)
|
|
lam = x1p ** 2 / rx ** 2 + y1p ** 2 / ry ** 2
|
|
if lam > 1:
|
|
rx *= math.sqrt(lam)
|
|
ry *= math.sqrt(lam)
|
|
num_ = rx ** 2 * ry ** 2 - rx ** 2 * y1p ** 2 - ry ** 2 * x1p ** 2
|
|
den = rx ** 2 * y1p ** 2 + ry ** 2 * x1p ** 2
|
|
co = math.sqrt(max(0, num_ / den)) * (-1 if fa == fs else 1)
|
|
cxp, cyp = co * rx * y1p / ry, -co * ry * x1p / rx
|
|
cx, cy = cp * cxp - sp * cyp + (x1 + x2) / 2, sp * cxp + cp * cyp + (y1 + y2) / 2
|
|
|
|
def ang(ux, uy, vx, vy):
|
|
a = math.atan2(ux * vy - uy * vx, ux * vx + uy * vy)
|
|
return a
|
|
|
|
th1 = ang(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry)
|
|
dth = ang((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry)
|
|
if not fs and dth > 0:
|
|
dth -= 2 * math.pi
|
|
if fs and dth < 0:
|
|
dth += 2 * math.pi
|
|
n = max(6, int(abs(dth) / (math.pi / 16)))
|
|
out = []
|
|
for k in range(1, n + 1):
|
|
t = th1 + dth * k / n
|
|
out.append((cp * rx * math.cos(t) - sp * ry * math.sin(t) + cx, sp * rx * math.cos(t) + cp * ry * math.sin(t) + cy))
|
|
return out
|
|
|
|
|
|
def rr_poly(x, y, w, h, r, m, seg=6):
|
|
pts = []
|
|
if r <= 0:
|
|
for px, py in ((x, y), (x + w, y), (x + w, y + h), (x, y + h)):
|
|
pts.append(ap(m, px, py))
|
|
return pts
|
|
r = min(r, w / 2, h / 2)
|
|
for cx, cy, a0 in ((x + w - r, y + r, -90), (x + w - r, y + h - r, 0), (x + r, y + h - r, 90), (x + r, y + r, 180)):
|
|
for k in range(seg + 1):
|
|
a = math.radians(a0 + 90 * k / seg)
|
|
pts.append(ap(m, cx + r * math.cos(a), cy + r * math.sin(a)))
|
|
return pts
|
|
|
|
|
|
class Walker:
|
|
"""Walks one SVG into a flat op list; colours stay as hex until paired."""
|
|
|
|
def __init__(self, root):
|
|
self.defs = {}
|
|
self.clips = {}
|
|
self.ops = []
|
|
self.root = root
|
|
|
|
@staticmethod
|
|
def tag(e):
|
|
return e.tag.split('}')[-1]
|
|
|
|
def collect(self, e):
|
|
for c in e:
|
|
t = self.tag(c)
|
|
if t in ('linearGradient', 'radialGradient'):
|
|
self.defs[c.get('id')] = c
|
|
elif t == 'clipPath':
|
|
self.clips[c.get('id')] = c
|
|
elif t == 'defs':
|
|
self.collect(c)
|
|
else:
|
|
self.collect(c)
|
|
|
|
def run(self):
|
|
self.collect(self.root)
|
|
vb = [float(v) for v in self.root.get('viewBox').split()]
|
|
self.size = (vb[2], vb[3])
|
|
self.walk(self.root, (1, 0, 0, 1, -vb[0], -vb[1]), None, root=True)
|
|
return self.ops
|
|
|
|
def style(self, e, mscale):
|
|
f = parse_color(e.get('fill', '#000000'))
|
|
s = parse_color(e.get('stroke'))
|
|
fo = num(e.get('fill-opacity'), 1.0) * num(e.get('opacity'), 1.0)
|
|
so = num(e.get('stroke-opacity'), 1.0) * num(e.get('opacity'), 1.0)
|
|
sw = num(e.get('stroke-width'), 1.0) * mscale if s else 0
|
|
dash = None
|
|
if e.get('stroke-dasharray'):
|
|
dv = [float(v) for v in re.split(r'[ ,]+', e.get('stroke-dasharray').strip())]
|
|
if len(dv) >= 2 and dv[1] > 0:
|
|
dash = (dv[0] * mscale, dv[1] * mscale)
|
|
round_cap = e.get('stroke-linecap') == 'round'
|
|
return f, fo, s, so, sw, dash, round_cap
|
|
|
|
def walk(self, e, m, clip, root=False):
|
|
for c in e:
|
|
t = self.tag(c)
|
|
if t in ('defs', 'linearGradient', 'radialGradient', 'clipPath', 'stop'):
|
|
continue
|
|
mc = m
|
|
if t == 'svg':
|
|
x, y = num(c.get('x'), 0), num(c.get('y'), 0)
|
|
w, h = num(c.get('width')), num(c.get('height'))
|
|
vb = [float(v) for v in c.get('viewBox').split()]
|
|
mc = mat_mul(m, (1, 0, 0, 1, x, y))
|
|
mc = mat_mul(mc, (w / vb[2], 0, 0, h / vb[3], -vb[0] * w / vb[2], -vb[1] * h / vb[3]))
|
|
self.collect_clips(c)
|
|
# A nested svg clips its content to its own viewport.
|
|
self.ops.append(('clip', rr_poly(x, y, w, h, 0, m)))
|
|
self.walk(c, mc, clip)
|
|
self.ops.append(('unclip',))
|
|
continue
|
|
if t == 'g':
|
|
mc = mat_mul(m, parse_transform(c.get('transform')))
|
|
cl = clip
|
|
if c.get('clip-path'):
|
|
cid = re.search(r'#([\w-]+)', c.get('clip-path')).group(1)
|
|
cp = self.clips[cid]
|
|
pts = self.clip_points(cp, mc)
|
|
self.ops.append(('clip', pts))
|
|
self.walk(c, mc, cl)
|
|
self.ops.append(('unclip',))
|
|
else:
|
|
self.walk(c, mc, cl)
|
|
continue
|
|
if c.get('transform'):
|
|
mc = mat_mul(m, parse_transform(c.get('transform')))
|
|
det = abs(mc[0] * mc[3] - mc[1] * mc[2])
|
|
ms = math.sqrt(det)
|
|
f, fo, s, so, sw, dash, rc = self.style(c, ms)
|
|
axis = abs(mc[1]) < 1e-9 and abs(mc[2]) < 1e-9
|
|
if t == 'rect':
|
|
x, y, w, h = num(c.get('x'), 0), num(c.get('y'), 0), num(c.get('width')), num(c.get('height'))
|
|
r = num(c.get('rx'), 0)
|
|
if f and f[0] == 'url' and f[1] in self.defs:
|
|
g = self.defs[f[1]]
|
|
stops = [(num(st.get('offset')), parse_color(st.get('stop-color'))[0]) for st in g]
|
|
self.ops.append(('poly', rr_poly(x, y, w, h, r, mc), True, ('grad', stops[0][1], stops[-1][1], ap(mc, 0, y)[1], ap(mc, 0, y + h)[1]), fo, s, so, sw, dash, rc))
|
|
elif axis and mc[0] > 0 and mc[3] > 0:
|
|
p0 = ap(mc, x, y)
|
|
self.ops.append(('rect', p0[0], p0[1], w * mc[0], h * mc[3], r * mc[0], f, fo, s, so, sw, dash))
|
|
else:
|
|
self.ops.append(('poly', rr_poly(x, y, w, h, r, mc), True, f and ('solid', f), fo, s, so, sw, dash, rc))
|
|
elif t == 'circle':
|
|
cx, cy, r = num(c.get('cx')), num(c.get('cy')), num(c.get('r'))
|
|
if abs(mc[1]) < 1e-9 and abs(mc[2]) < 1e-9 and abs(mc[0] - mc[3]) < 1e-9:
|
|
p0 = ap(mc, cx, cy)
|
|
self.ops.append(('circle', p0[0], p0[1], r * mc[0], f, fo, s, so, sw, dash))
|
|
else:
|
|
pts = [ap(mc, cx + r * math.cos(a * math.pi / 12), cy + r * math.sin(a * math.pi / 12)) for a in range(24)]
|
|
self.ops.append(('poly', pts, True, f and ('solid', f), fo, s, so, sw, dash, rc))
|
|
elif t == 'ellipse':
|
|
cx, cy, rx, ry = num(c.get('cx')), num(c.get('cy')), num(c.get('rx')), num(c.get('ry'))
|
|
if f and f[0] == 'url':
|
|
g = self.defs[f[1]]
|
|
stops = [num(st.get('stop-opacity'), 1.0) for st in g]
|
|
p0 = ap(mc, cx, cy)
|
|
self.ops.append(('glow', p0[0], p0[1], rx * mc[0], ry * mc[3], stops[0] * fo))
|
|
else:
|
|
pts = [ap(mc, cx + rx * math.cos(a * math.pi / 16), cy + ry * math.sin(a * math.pi / 16)) for a in range(32)]
|
|
self.ops.append(('poly', pts, True, f and ('solid', f), fo, s, so, sw, dash, rc))
|
|
elif t in ('polygon', 'polyline'):
|
|
nums = [float(v) for v in re.split(r'[ ,]+', c.get('points').strip())]
|
|
pts = [ap(mc, nums[k], nums[k + 1]) for k in range(0, len(nums), 2)]
|
|
self.ops.append(('poly', pts, t == 'polygon', f and ('solid', f), fo, s, so, sw, dash, rc))
|
|
elif t == 'path' and s and s[0] == 'url':
|
|
g = self.defs[s[1]]
|
|
stops = [(num(st.get('offset')), parse_color(st.get('stop-color')), num(st.get('stop-opacity'), 1.0)) for st in g]
|
|
for pts, closed in flatten_path(c.get('d'), mc):
|
|
(x0, y0), (x1, y1) = pts[0], pts[-1]
|
|
self.ops.append(('gline', x0, y0, x1, y1, stops[0][1], stops[0][2], stops[-1][2], sw))
|
|
elif t == 'path':
|
|
for pts, closed in flatten_path(c.get('d'), mc):
|
|
if len(pts) >= 2:
|
|
fill = f and ('solid', f) if (closed or len(pts) >= 3) else None
|
|
self.ops.append(('poly', pts, closed, fill, fo, s, so, sw, dash, rc))
|
|
elif t == 'text':
|
|
x, y = num(c.get('x')), num(c.get('y'))
|
|
p0 = ap(mc, x, y)
|
|
self.ops.append(('text', p0[0], p0[1], num(c.get('font-size')) * ms, c.get('text-anchor', 'start'), f, fo, ''.join(c.itertext()), num(c.get('font-weight'), 400) >= 600))
|
|
elif t == 'g':
|
|
pass
|
|
else:
|
|
raise ValueError('unhandled ' + t)
|
|
|
|
def collect_clips(self, e):
|
|
for c in e:
|
|
if self.tag(c) == 'clipPath':
|
|
self.clips[c.get('id')] = c
|
|
|
|
def clip_points(self, cp, m):
|
|
out = []
|
|
for c in cp:
|
|
t = self.tag(c)
|
|
if t == 'path':
|
|
for pts, _ in flatten_path(c.get('d'), m):
|
|
out.extend(pts)
|
|
elif t == 'rect':
|
|
out.extend(rr_poly(num(c.get('x'), 0), num(c.get('y'), 0), num(c.get('width')), num(c.get('height')), 0, m))
|
|
else:
|
|
raise ValueError('clip ' + t)
|
|
return out
|
|
|
|
|
|
def load(name, theme):
|
|
tree = ET.parse(os.path.join(FIGS, '%s.%s.svg' % (name, theme)))
|
|
w = Walker(tree.getroot())
|
|
return w.run(), w.size
|
|
|
|
|
|
def color_fields(op):
|
|
"""Positions of (colour, alpha) tuples in an op, as (index) list."""
|
|
k = op[0]
|
|
if k == 'rect':
|
|
return [6, 9]
|
|
if k == 'circle':
|
|
return [4, 7]
|
|
if k == 'poly':
|
|
return [None, 5]
|
|
if k == 'text':
|
|
return [5]
|
|
return []
|
|
|
|
|
|
def ink_of(l, d, slots=None):
|
|
if slots is not None:
|
|
key = tuple(slots)
|
|
if key not in SLOTS:
|
|
SLOTS[key] = len(SLOTS)
|
|
return 'Slot(%d)' % SLOTS[key]
|
|
if l == '#000000':
|
|
return 'Black'
|
|
r = PAIRS.get((l, d))
|
|
if r is None:
|
|
raise KeyError((l, d))
|
|
return r
|
|
|
|
|
|
SLOTS = collections.OrderedDict()
|
|
|
|
|
|
def f32(v):
|
|
s = ('%.3f' % v).rstrip('0').rstrip('.')
|
|
if s in ('', '-0'):
|
|
s = '0'
|
|
return s + ('' if '.' in s else '.0')
|
|
|
|
|
|
def paint(l, d, alpha, slots=None):
|
|
"""Option<Paint> source."""
|
|
if l is None:
|
|
return 'None'
|
|
return 'Some(P { ink: Ink::%s, a: %s })' % (ink_of(l[0], d[0], slots), f32(alpha))
|
|
|
|
|
|
def line(sl, sd, so, sw, dash, rc, slots=None):
|
|
if sl is None or sw <= 0:
|
|
return 'None'
|
|
ds = dash or (0, 0)
|
|
return 'Some(L { ink: Ink::%s, a: %s, w: %s, dash: [%s, %s], round: %s })' % (ink_of(sl[0], sd[0], slots), f32(so), f32(sw), f32(ds[0]), f32(ds[1]), 'true' if rc else 'false')
|
|
|
|
|
|
def emit(name, ops_l, ops_d, slotsets=None):
|
|
out = []
|
|
assert len(ops_l) == len(ops_d), (name, len(ops_l), len(ops_d))
|
|
for idx, (a, b) in enumerate(zip(ops_l, ops_d)):
|
|
assert a[0] == b[0], (name, idx, a[0], b[0])
|
|
sl = None
|
|
if slotsets is not None:
|
|
sl = [s[idx] for s in slotsets]
|
|
k = a[0]
|
|
if k == 'rect':
|
|
_, x, y, w, h, r, f, fo, s, so, sw, dash = a
|
|
_, _, _, _, _, _, fd, _, sd, _, _, _ = b
|
|
fs = fsl = None
|
|
if slotsets is not None:
|
|
fs = [t[6][0] if t[6] else None for t in sl]
|
|
ss = [t[8][0] if t[8] else None for t in sl]
|
|
out.append('Op::Rect { r: [%s, %s, %s, %s], radius: %s, fill: %s, line: %s }' % (
|
|
f32(x), f32(y), f32(w), f32(h), f32(r),
|
|
paint(f, fd, fo, fs if slotsets is not None and f else None),
|
|
line(s, sd, so, sw, dash, False, ss if slotsets is not None and s else None)))
|
|
elif k == 'circle':
|
|
_, x, y, r, f, fo, s, so, sw, dash = a
|
|
fd, sd = b[4], b[6]
|
|
fs = ss = None
|
|
if slotsets is not None:
|
|
fs = [t[4][0] if t[4] else None for t in sl]
|
|
ss = [t[6][0] if t[6] else None for t in sl]
|
|
out.append('Op::Circle { c: [%s, %s, %s], fill: %s, line: %s }' % (
|
|
f32(x), f32(y), f32(r), paint(f, fd, fo, fs if f and slotsets is not None else None),
|
|
line(s, sd, so, sw, dash, False, ss if s and slotsets is not None else None)))
|
|
elif k == 'poly':
|
|
_, pts, closed, fill, fo, s, so, sw, dash, rc = a
|
|
fd, sd = b[3], b[5]
|
|
pts_s = ', '.join('[%s, %s]' % (f32(px), f32(py)) for px, py in pts)
|
|
fs = ss = None
|
|
if slotsets is not None:
|
|
fs = [t[3][1][0] if t[3] and t[3][0] == 'solid' else None for t in sl]
|
|
ss = [t[5][0] if t[5] else None for t in sl]
|
|
grad = 'None'
|
|
fill_s = 'None'
|
|
if fill and fill[0] == 'grad':
|
|
_, top, bot, y0, y1 = fill
|
|
gd = b[3]
|
|
if slotsets is not None:
|
|
tops = [t[3][1] for t in sl]
|
|
bots = [t[3][2] for t in sl]
|
|
grad = 'Some(G { top: Ink::%s, bottom: Ink::%s, y: [%s, %s] })' % (ink_of(top, gd[1], tops), ink_of(bot, gd[2], bots), f32(y0), f32(y1))
|
|
else:
|
|
grad = 'Some(G { top: Ink::%s, bottom: Ink::%s, y: [%s, %s] })' % (ink_of(top, gd[1]), ink_of(bot, gd[2]), f32(y0), f32(y1))
|
|
elif fill:
|
|
fill_s = paint(fill[1], fd[1], fo, fs if slotsets is not None else None)
|
|
out.append('Op::Poly { pts: &[%s], closed: %s, fill: %s, grad: %s, line: %s }' % (
|
|
pts_s, 'true' if closed else 'false', fill_s, grad,
|
|
line(s, sd, so, sw, dash, rc, ss if s and slotsets is not None else None)))
|
|
elif k == 'gline':
|
|
_, x0, y0, x1, y1, col, a0, a1, sw = a
|
|
out.append('Op::Gradient { pts: [[%s, %s], [%s, %s]], w: %s, ink: Ink::%s, a: [%s, %s] }' % (
|
|
f32(x0), f32(y0), f32(x1), f32(y1), f32(sw), ink_of(col[0], b[5][0]), f32(a0), f32(a1)))
|
|
elif k == 'glow':
|
|
_, x, y, rx, ry, al = a
|
|
out.append('Op::Glow { c: [%s, %s, %s, %s], a: %s }' % (f32(x), f32(y), f32(rx), f32(ry), f32(al)))
|
|
elif k == 'text':
|
|
_, x, y, size, anchor, f, fo, txt, bold = a
|
|
fd = b[5]
|
|
fs = [t[5][0] for t in sl] if slotsets is not None else None
|
|
out.append('Op::Text { at: [%s, %s], size: %s, anchor: %d, ink: Ink::%s, a: %s, s: %s, bold: %s }' % (
|
|
f32(x), f32(y), f32(size), {'start': 0, 'middle': 1, 'end': 2}[anchor], ink_of(f[0], fd[0], fs), f32(fo), '"' + txt.replace('\\', '\\\\').replace('"', '\\"') + '"', 'true' if bold else 'false'))
|
|
elif k == 'clip':
|
|
pts_s = ', '.join('[%s, %s]' % (f32(px), f32(py)) for px, py in a[1])
|
|
out.append('Op::Clip(&[%s])' % pts_s)
|
|
elif k == 'unclip':
|
|
out.append('Op::Unclip')
|
|
else:
|
|
raise ValueError(k)
|
|
return out
|
|
|
|
|
|
def const_name(n):
|
|
return n.upper().replace('-', '_')
|
|
|
|
|
|
def fn_name(n):
|
|
return 'paint_' + n.replace('-', '_')
|
|
|
|
|
|
def main():
|
|
man = sorted({f.split('.')[0] for f in os.listdir(FIGS) if f.endswith('.svg')})
|
|
parts = []
|
|
parts.append('//! Generated by `tools/preferences-figures/generate.py` from `docs/design/preferences/figures/*.svg`.\n'
|
|
'//! Do not edit by hand: change the SVG and run the script again.\n'
|
|
'#![allow(clippy::approx_constant, clippy::excessive_precision)]\n'
|
|
'use super::preferences_figure_ops::{Figure, G, Ink, L, Op, P};\n')
|
|
fns = []
|
|
names = []
|
|
for name in man:
|
|
if name in SKIP or name.startswith('theme-'):
|
|
continue
|
|
ol, size = load(name, 'light')
|
|
od, size2 = load(name, 'dark')
|
|
assert size == size2
|
|
ops = emit(name, ol, od)
|
|
parts.append('pub(super) const %s: Figure = Figure { size: [%s, %s], ops: &[\n %s,\n] };\n' % (const_name(name), f32(size[0]), f32(size[1]), ',\n '.join(ops)))
|
|
names.append(name)
|
|
# theme thumbnail: slots from the four themes
|
|
loaded = [load(t, 'light')[0] for t in THEMES]
|
|
size = load('theme-light', 'light')[1]
|
|
ops = emit('theme-thumbnail', loaded[0], loaded[0], slotsets=loaded)
|
|
parts.append('pub(super) const THEME_THUMBNAIL: Figure = Figure { size: [%s, %s], ops: &[\n %s,\n] };\n' % (f32(size[0]), f32(size[1]), ',\n '.join(ops)))
|
|
slot_table = ['// Theme thumbnail slots (light, dark, gruvbox, imported sample colours):']
|
|
for key, i in SLOTS.items():
|
|
slot_table.append('// %d: %s' % (i, ' '.join(str(k) for k in key)))
|
|
parts.append('\n'.join(slot_table) + '\n')
|
|
parts.append('use egui::{Painter, Rect};\nuse super::preferences_figure_ops::paint;\n')
|
|
for n in names:
|
|
if n == 'mouse-mapping':
|
|
parts.append('/// The mouse figure, with its own slightly heavier lettering.\npub(crate) fn paint_mouse_mapping(p: &Painter, rect: Rect) {\n super::preferences_figure_ops::paint_mouse(p, rect);\n}\n')
|
|
continue
|
|
parts.append('/// The `%s` figure of `docs/design/preferences/figures`.\npub(crate) fn %s(p: &Painter, rect: Rect) {\n paint(p, rect, &%s, None, None);\n}\n' % (n, fn_name(n), const_name(n)))
|
|
parts.append('/// Every figure painter by file name, for the gallery and the tests.\npub(crate) const FIGURES: &[(&str, [f32; 2], fn(&Painter, Rect))] = &[\n%s];\n' % ''.join(' ("%s", %s.size, %s),\n' % (n, const_name(n), fn_name(n)) for n in names))
|
|
open(OUT, 'w', encoding='utf8', newline='\n').write('\n'.join(parts))
|
|
print('figures', len(names), 'slots', len(SLOTS), file=sys.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|