castletool 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {castletool-0.2.2 → castletool-0.2.3}/PKG-INFO +1 -1
- {castletool-0.2.2 → castletool-0.2.3}/castletool/castletool.py +430 -12
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/PKG-INFO +1 -1
- {castletool-0.2.2 → castletool-0.2.3}/pyproject.toml +1 -1
- {castletool-0.2.2 → castletool-0.2.3}/LICENSE +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/README.md +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool/__init__.py +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/SOURCES.txt +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/dependency_links.txt +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/entry_points.txt +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/requires.txt +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/castletool.egg-info/top_level.txt +0 -0
- {castletool-0.2.2 → castletool-0.2.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: castletool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Interactive Castle deck editor for injecting images/GIFs and MIDI into Castle blueprint JSON files.
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -9,12 +9,15 @@ import bisect
|
|
|
9
9
|
import copy
|
|
10
10
|
import io
|
|
11
11
|
import json
|
|
12
|
+
import math
|
|
12
13
|
import os
|
|
13
14
|
import platform
|
|
15
|
+
import re
|
|
14
16
|
import shutil
|
|
15
17
|
import subprocess
|
|
16
18
|
import sys
|
|
17
19
|
import uuid
|
|
20
|
+
import xml.etree.ElementTree as ET
|
|
18
21
|
from pathlib import Path
|
|
19
22
|
|
|
20
23
|
# ── optional deps ────────────────────────────────────────────────────────────
|
|
@@ -39,12 +42,6 @@ except ImportError:
|
|
|
39
42
|
HAS_PT = False
|
|
40
43
|
|
|
41
44
|
HAS_FZF = shutil.which("fzf") is not None
|
|
42
|
-
|
|
43
|
-
try:
|
|
44
|
-
from svg_to_castle import svg_to_path_data, build_drawing2_vector
|
|
45
|
-
HAS_SVG = True
|
|
46
|
-
except ImportError:
|
|
47
|
-
HAS_SVG = False
|
|
48
45
|
HAS_FFMPEG = shutil.which("ffmpeg") is not None
|
|
49
46
|
|
|
50
47
|
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -303,9 +300,6 @@ def quantize_frame(png_bytes: bytes, colors: int) -> bytes:
|
|
|
303
300
|
result.save(buf, format="PNG", optimize=True)
|
|
304
301
|
return buf.getvalue()
|
|
305
302
|
|
|
306
|
-
import base64
|
|
307
|
-
import math
|
|
308
|
-
|
|
309
303
|
def build_drawing2(
|
|
310
304
|
frames: list[bytes],
|
|
311
305
|
fps: float,
|
|
@@ -384,6 +378,433 @@ def build_drawing2(
|
|
|
384
378
|
"disabled": False,
|
|
385
379
|
}
|
|
386
380
|
|
|
381
|
+
# ── svg → Drawing2 (vector) ──────────────────────────────────────────────────
|
|
382
|
+
# Merged from svg_to_castle.py: flattens all SVG paths to straight line
|
|
383
|
+
# segments (s:1, f:false). Bezier curves are sampled at `steps` intervals.
|
|
384
|
+
|
|
385
|
+
# Castle coordinate scale: SVG px → Castle units
|
|
386
|
+
# fillPixelsPerUnit = 25.6 by default
|
|
387
|
+
SVG_DEFAULT_PPU = 25.6
|
|
388
|
+
SVG_NS = "http://www.w3.org/2000/svg"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _svg_lerp(a, b, t): return a + (b - a) * t
|
|
392
|
+
|
|
393
|
+
def _svg_quadratic_point(p0, p1, p2, t):
|
|
394
|
+
x = (1-t)**2*p0[0] + 2*(1-t)*t*p1[0] + t**2*p2[0]
|
|
395
|
+
y = (1-t)**2*p0[1] + 2*(1-t)*t*p1[1] + t**2*p2[1]
|
|
396
|
+
return (x, y)
|
|
397
|
+
|
|
398
|
+
def _svg_cubic_point(p0, p1, p2, p3, t):
|
|
399
|
+
x = (1-t)**3*p0[0] + 3*(1-t)**2*t*p1[0] + 3*(1-t)*t**2*p2[0] + t**3*p3[0]
|
|
400
|
+
y = (1-t)**3*p0[1] + 3*(1-t)**2*t*p1[1] + 3*(1-t)*t**2*p2[1] + t**3*p3[1]
|
|
401
|
+
return (x, y)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _svg_arc_to_lines(x1, y1, rx, ry, x_rot, large, sweep, x2, y2, steps):
|
|
405
|
+
"""Convert SVG arc to polyline points."""
|
|
406
|
+
if x1 == x2 and y1 == y2:
|
|
407
|
+
return []
|
|
408
|
+
if rx == 0 or ry == 0:
|
|
409
|
+
return [(x1, y1), (x2, y2)]
|
|
410
|
+
|
|
411
|
+
phi = math.radians(x_rot)
|
|
412
|
+
cos_phi, sin_phi = math.cos(phi), math.sin(phi)
|
|
413
|
+
|
|
414
|
+
dx, dy = (x1 - x2) / 2, (y1 - y2) / 2
|
|
415
|
+
x1p = cos_phi * dx + sin_phi * dy
|
|
416
|
+
y1p = -sin_phi * dx + cos_phi * dy
|
|
417
|
+
|
|
418
|
+
rx, ry = abs(rx), abs(ry)
|
|
419
|
+
x1p2, y1p2, rx2, ry2 = x1p**2, y1p**2, rx**2, ry**2
|
|
420
|
+
lam = x1p2/rx2 + y1p2/ry2
|
|
421
|
+
if lam > 1:
|
|
422
|
+
sq = math.sqrt(lam)
|
|
423
|
+
rx, ry = sq * rx, sq * ry
|
|
424
|
+
rx2, ry2 = rx**2, ry**2
|
|
425
|
+
|
|
426
|
+
num = max(0, rx2*ry2 - rx2*y1p2 - ry2*x1p2)
|
|
427
|
+
den = rx2*y1p2 + ry2*x1p2
|
|
428
|
+
sq = math.sqrt(num / den) if den else 0
|
|
429
|
+
if large == sweep:
|
|
430
|
+
sq = -sq
|
|
431
|
+
|
|
432
|
+
cxp = sq * rx * y1p / ry
|
|
433
|
+
cyp = -sq * ry * x1p / rx
|
|
434
|
+
|
|
435
|
+
cx = cos_phi*cxp - sin_phi*cyp + (x1+x2)/2
|
|
436
|
+
cy = sin_phi*cxp + cos_phi*cyp + (y1+y2)/2
|
|
437
|
+
|
|
438
|
+
def angle(ux, uy, vx, vy):
|
|
439
|
+
n = math.sqrt(ux**2+uy**2) * math.sqrt(vx**2+vy**2)
|
|
440
|
+
if n == 0: return 0
|
|
441
|
+
c = max(-1, min(1, (ux*vx+uy*vy)/n))
|
|
442
|
+
a = math.acos(c)
|
|
443
|
+
if ux*vy - uy*vx < 0: a = -a
|
|
444
|
+
return a
|
|
445
|
+
|
|
446
|
+
theta1 = angle(1, 0, (x1p-cxp)/rx, (y1p-cyp)/ry)
|
|
447
|
+
dtheta = angle((x1p-cxp)/rx, (y1p-cyp)/ry, (-x1p-cxp)/rx, (-y1p-cyp)/ry)
|
|
448
|
+
|
|
449
|
+
if not sweep and dtheta > 0: dtheta -= 2*math.pi
|
|
450
|
+
if sweep and dtheta < 0: dtheta += 2*math.pi
|
|
451
|
+
|
|
452
|
+
pts = []
|
|
453
|
+
for i in range(steps + 1):
|
|
454
|
+
t = i / steps
|
|
455
|
+
theta = theta1 + t * dtheta
|
|
456
|
+
x = cos_phi*rx*math.cos(theta) - sin_phi*ry*math.sin(theta) + cx
|
|
457
|
+
y = sin_phi*rx*math.cos(theta) + cos_phi*ry*math.sin(theta) + cy
|
|
458
|
+
pts.append((x, y))
|
|
459
|
+
return pts
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _svg_parse_numbers(s: str) -> list[float]:
|
|
463
|
+
return [float(x) for x in re.findall(r'[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?', s)]
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _svg_path_to_polylines(d: str, steps: int = 16) -> list[list[tuple]]:
|
|
467
|
+
"""
|
|
468
|
+
Parse SVG path 'd' attribute, return list of polylines.
|
|
469
|
+
Each polyline is a list of (x, y) tuples.
|
|
470
|
+
Beziers are sampled at 'steps' intervals.
|
|
471
|
+
"""
|
|
472
|
+
tokens = re.findall(r'[MmZzLlHhVvCcSsQqTtAa]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?', d)
|
|
473
|
+
cmd = None
|
|
474
|
+
pos = (0.0, 0.0)
|
|
475
|
+
start = (0.0, 0.0)
|
|
476
|
+
last_ctrl = None
|
|
477
|
+
polylines = []
|
|
478
|
+
current = []
|
|
479
|
+
|
|
480
|
+
def flush():
|
|
481
|
+
nonlocal current
|
|
482
|
+
if len(current) >= 2:
|
|
483
|
+
polylines.append(current)
|
|
484
|
+
current = []
|
|
485
|
+
|
|
486
|
+
i = 0
|
|
487
|
+
while i < len(tokens):
|
|
488
|
+
t = tokens[i]
|
|
489
|
+
if re.match(r'[MmZzLlHhVvCcSsQqTtAa]', t):
|
|
490
|
+
cmd = t
|
|
491
|
+
i += 1
|
|
492
|
+
if cmd in ('Z', 'z'):
|
|
493
|
+
if current and current[-1] != start:
|
|
494
|
+
current.append(start)
|
|
495
|
+
flush()
|
|
496
|
+
pos = start
|
|
497
|
+
cmd = None
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
if cmd in ('Z', 'z'):
|
|
501
|
+
if current and current[-1] != start:
|
|
502
|
+
current.append(start)
|
|
503
|
+
flush()
|
|
504
|
+
pos = start
|
|
505
|
+
cmd = None
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
nparams = {
|
|
509
|
+
'M':2,'m':2,'L':2,'l':2,'H':1,'h':1,'V':1,'v':1,
|
|
510
|
+
'C':6,'c':6,'S':4,'s':4,'Q':4,'q':4,'T':2,'t':2,
|
|
511
|
+
'A':7,'a':7,
|
|
512
|
+
}.get(cmd, 0)
|
|
513
|
+
|
|
514
|
+
if nparams == 0:
|
|
515
|
+
i += 1
|
|
516
|
+
continue
|
|
517
|
+
|
|
518
|
+
args = []
|
|
519
|
+
while len(args) < nparams and i < len(tokens):
|
|
520
|
+
tok = tokens[i]
|
|
521
|
+
if re.match(r'[MmZzLlHhVvCcSsQqTtAa]', tok):
|
|
522
|
+
break
|
|
523
|
+
args.append(float(tok))
|
|
524
|
+
i += 1
|
|
525
|
+
|
|
526
|
+
if len(args) < nparams:
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
rel = cmd.islower()
|
|
530
|
+
base = pos if rel else (0.0, 0.0)
|
|
531
|
+
|
|
532
|
+
if cmd in ('M', 'm'):
|
|
533
|
+
flush()
|
|
534
|
+
nx, ny = base[0]+args[0], base[1]+args[1]
|
|
535
|
+
pos = (nx, ny)
|
|
536
|
+
start = pos
|
|
537
|
+
current = [pos]
|
|
538
|
+
last_ctrl = None
|
|
539
|
+
cmd = 'l' if rel else 'L'
|
|
540
|
+
|
|
541
|
+
elif cmd in ('L', 'l'):
|
|
542
|
+
nx, ny = base[0]+args[0], base[1]+args[1]
|
|
543
|
+
pos = (nx, ny)
|
|
544
|
+
current.append(pos)
|
|
545
|
+
last_ctrl = None
|
|
546
|
+
|
|
547
|
+
elif cmd in ('H', 'h'):
|
|
548
|
+
nx = (pos[0] if rel else 0) + args[0]
|
|
549
|
+
pos = (nx, pos[1])
|
|
550
|
+
current.append(pos)
|
|
551
|
+
last_ctrl = None
|
|
552
|
+
|
|
553
|
+
elif cmd in ('V', 'v'):
|
|
554
|
+
ny = (pos[1] if rel else 0) + args[0]
|
|
555
|
+
pos = (pos[0], ny)
|
|
556
|
+
current.append(pos)
|
|
557
|
+
last_ctrl = None
|
|
558
|
+
|
|
559
|
+
elif cmd in ('C', 'c'):
|
|
560
|
+
p1 = (base[0]+args[0], base[1]+args[1])
|
|
561
|
+
p2 = (base[0]+args[2], base[1]+args[3])
|
|
562
|
+
p3 = (base[0]+args[4], base[1]+args[5])
|
|
563
|
+
for s in range(1, steps+1):
|
|
564
|
+
current.append(_svg_cubic_point(pos, p1, p2, p3, s/steps))
|
|
565
|
+
last_ctrl = p2
|
|
566
|
+
pos = p3
|
|
567
|
+
|
|
568
|
+
elif cmd in ('S', 's'):
|
|
569
|
+
p1 = (2*pos[0]-last_ctrl[0], 2*pos[1]-last_ctrl[1]) if last_ctrl else pos
|
|
570
|
+
p2 = (base[0]+args[0], base[1]+args[1])
|
|
571
|
+
p3 = (base[0]+args[2], base[1]+args[3])
|
|
572
|
+
for s in range(1, steps+1):
|
|
573
|
+
current.append(_svg_cubic_point(pos, p1, p2, p3, s/steps))
|
|
574
|
+
last_ctrl = p2
|
|
575
|
+
pos = p3
|
|
576
|
+
|
|
577
|
+
elif cmd in ('Q', 'q'):
|
|
578
|
+
p1 = (base[0]+args[0], base[1]+args[1])
|
|
579
|
+
p2 = (base[0]+args[2], base[1]+args[3])
|
|
580
|
+
for s in range(1, steps+1):
|
|
581
|
+
current.append(_svg_quadratic_point(pos, p1, p2, s/steps))
|
|
582
|
+
last_ctrl = p1
|
|
583
|
+
pos = p2
|
|
584
|
+
|
|
585
|
+
elif cmd in ('T', 't'):
|
|
586
|
+
p1 = (2*pos[0]-last_ctrl[0], 2*pos[1]-last_ctrl[1]) if last_ctrl else pos
|
|
587
|
+
p2 = (base[0]+args[0], base[1]+args[1])
|
|
588
|
+
for s in range(1, steps+1):
|
|
589
|
+
current.append(_svg_quadratic_point(pos, p1, p2, s/steps))
|
|
590
|
+
last_ctrl = p1
|
|
591
|
+
pos = p2
|
|
592
|
+
|
|
593
|
+
elif cmd in ('A', 'a'):
|
|
594
|
+
rx,ry,xr = args[0],args[1],args[2]
|
|
595
|
+
large,sweep = int(args[3]),int(args[4])
|
|
596
|
+
ex,ey = base[0]+args[5], base[1]+args[6]
|
|
597
|
+
pts = _svg_arc_to_lines(pos[0],pos[1],rx,ry,xr,large,sweep,ex,ey,steps)
|
|
598
|
+
if pts:
|
|
599
|
+
current.extend(pts[1:])
|
|
600
|
+
last_ctrl = None
|
|
601
|
+
pos = (ex, ey)
|
|
602
|
+
|
|
603
|
+
flush()
|
|
604
|
+
return polylines
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
SVG_COLORS = {
|
|
608
|
+
"black":(0,0,0),"white":(1,1,1),"red":(1,0,0),"green":(0,0.502,0),
|
|
609
|
+
"blue":(0,0,1),"yellow":(1,1,0),"cyan":(0,1,1),"magenta":(1,0,1),
|
|
610
|
+
"gray":(0.502,0.502,0.502),"grey":(0.502,0.502,0.502),
|
|
611
|
+
"none":None,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
def parse_svg_color(s: str, opacity: float = 1.0):
|
|
615
|
+
if not s or s == "none": return None
|
|
616
|
+
s = s.strip().lower()
|
|
617
|
+
if s in SVG_COLORS:
|
|
618
|
+
c = SVG_COLORS[s]
|
|
619
|
+
return [*c, opacity] if c else None
|
|
620
|
+
if s.startswith("#"):
|
|
621
|
+
h = s[1:]
|
|
622
|
+
if len(h) == 3: h = h[0]*2+h[1]*2+h[2]*2
|
|
623
|
+
r,g,b = int(h[0:2],16)/255, int(h[2:4],16)/255, int(h[4:6],16)/255
|
|
624
|
+
return [r, g, b, opacity]
|
|
625
|
+
m = re.match(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)', s)
|
|
626
|
+
if m:
|
|
627
|
+
return [int(m.group(i))/255 for i in (1,2,3)] + [opacity]
|
|
628
|
+
return [0, 0, 0, opacity]
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def svg_to_path_data(svg_path, steps: int = 16, scale: float = 1.0,
|
|
632
|
+
ppu: float = SVG_DEFAULT_PPU, color=None):
|
|
633
|
+
"""
|
|
634
|
+
Parse an SVG file and return a Castle pathDataList and framesBounds.
|
|
635
|
+
scale: multiplier applied to Castle units (1.0 = default)
|
|
636
|
+
ppu: pixels per unit (default 25.6)
|
|
637
|
+
color: override color [r,g,b,a], None = use SVG colors
|
|
638
|
+
"""
|
|
639
|
+
tree = ET.parse(svg_path)
|
|
640
|
+
root = tree.getroot()
|
|
641
|
+
|
|
642
|
+
def tag(el): return el.tag.split('}')[-1] if '}' in el.tag else el.tag
|
|
643
|
+
|
|
644
|
+
vb = root.get("viewBox")
|
|
645
|
+
if vb:
|
|
646
|
+
parts = _svg_parse_numbers(vb)
|
|
647
|
+
vb_x, vb_y, vb_w, vb_h = parts
|
|
648
|
+
else:
|
|
649
|
+
vb_x, vb_y = 0, 0
|
|
650
|
+
vb_w = float(root.get("width", 100))
|
|
651
|
+
vb_h = float(root.get("height", 100))
|
|
652
|
+
|
|
653
|
+
# SVG Y is flipped vs Castle Y
|
|
654
|
+
def to_castle_raw(x, y):
|
|
655
|
+
return (x / ppu * scale, -(y / ppu) * scale)
|
|
656
|
+
|
|
657
|
+
_cx_offset = [0.0]
|
|
658
|
+
_cy_offset = [0.0]
|
|
659
|
+
|
|
660
|
+
def to_castle(x, y):
|
|
661
|
+
rx, ry = to_castle_raw(x, y)
|
|
662
|
+
return (rx - _cx_offset[0], ry - _cy_offset[0])
|
|
663
|
+
|
|
664
|
+
path_data = []
|
|
665
|
+
default_color = color or [0, 0, 0, 1]
|
|
666
|
+
all_xs, all_ys = [], []
|
|
667
|
+
|
|
668
|
+
def get_color(el):
|
|
669
|
+
if color: return color
|
|
670
|
+
stroke = el.get("stroke") or el.get("style", "")
|
|
671
|
+
m = re.search(r'stroke\s*:\s*([^;]+)', stroke)
|
|
672
|
+
stroke_val = m.group(1).strip() if m else (el.get("stroke") or "black")
|
|
673
|
+
op_m = re.search(r'stroke-opacity\s*:\s*([^;]+)', el.get("style",""))
|
|
674
|
+
opacity = float(op_m.group(1)) if op_m else float(el.get("stroke-opacity", 1))
|
|
675
|
+
c = parse_svg_color(stroke_val, opacity)
|
|
676
|
+
return c if c else default_color
|
|
677
|
+
|
|
678
|
+
def process_element(el):
|
|
679
|
+
t = tag(el)
|
|
680
|
+
c = get_color(el)
|
|
681
|
+
|
|
682
|
+
polylines = []
|
|
683
|
+
|
|
684
|
+
if t == "path":
|
|
685
|
+
d = el.get("d", "")
|
|
686
|
+
polylines = _svg_path_to_polylines(d, steps)
|
|
687
|
+
|
|
688
|
+
elif t == "line":
|
|
689
|
+
x1,y1 = float(el.get("x1",0)), float(el.get("y1",0))
|
|
690
|
+
x2,y2 = float(el.get("x2",0)), float(el.get("y2",0))
|
|
691
|
+
polylines = [[(x1,y1),(x2,y2)]]
|
|
692
|
+
|
|
693
|
+
elif t == "polyline" or t == "polygon":
|
|
694
|
+
pts_raw = _svg_parse_numbers(el.get("points",""))
|
|
695
|
+
pts = [(pts_raw[i],pts_raw[i+1]) for i in range(0,len(pts_raw)-1,2)]
|
|
696
|
+
if t == "polygon" and pts: pts.append(pts[0])
|
|
697
|
+
polylines = [pts]
|
|
698
|
+
|
|
699
|
+
elif t == "rect":
|
|
700
|
+
x,y = float(el.get("x",0)), float(el.get("y",0))
|
|
701
|
+
w,h = float(el.get("width",0)), float(el.get("height",0))
|
|
702
|
+
polylines = [[(x,y),(x+w,y),(x+w,y+h),(x,y+h),(x,y)]]
|
|
703
|
+
|
|
704
|
+
elif t == "circle":
|
|
705
|
+
cx2,cy2,r = float(el.get("cx",0)),float(el.get("cy",0)),float(el.get("r",1))
|
|
706
|
+
pts = [(cx2+r*math.cos(2*math.pi*i/steps),
|
|
707
|
+
cy2+r*math.sin(2*math.pi*i/steps)) for i in range(steps+1)]
|
|
708
|
+
polylines = [pts]
|
|
709
|
+
|
|
710
|
+
elif t == "ellipse":
|
|
711
|
+
cx2,cy2 = float(el.get("cx",0)),float(el.get("cy",0))
|
|
712
|
+
rx2,ry2 = float(el.get("rx",1)),float(el.get("ry",1))
|
|
713
|
+
pts = [(cx2+rx2*math.cos(2*math.pi*i/steps),
|
|
714
|
+
cy2+ry2*math.sin(2*math.pi*i/steps)) for i in range(steps+1)]
|
|
715
|
+
polylines = [pts]
|
|
716
|
+
|
|
717
|
+
for poly in polylines:
|
|
718
|
+
if len(poly) < 2: continue
|
|
719
|
+
castle_pts = [to_castle(x, y) for x, y in poly]
|
|
720
|
+
for j in range(len(castle_pts)-1):
|
|
721
|
+
x1,y1 = castle_pts[j]
|
|
722
|
+
x2,y2 = castle_pts[j+1]
|
|
723
|
+
path_data.append({
|
|
724
|
+
"p": [round(x1,5), round(y1,5), round(x2,5), round(y2,5)],
|
|
725
|
+
"s": 1,
|
|
726
|
+
"f": False,
|
|
727
|
+
"c": [round(v,5) for v in c],
|
|
728
|
+
})
|
|
729
|
+
all_xs.extend([x1, x2])
|
|
730
|
+
all_ys.extend([y1, y2])
|
|
731
|
+
|
|
732
|
+
for child in el:
|
|
733
|
+
process_element(child)
|
|
734
|
+
|
|
735
|
+
process_element(root)
|
|
736
|
+
|
|
737
|
+
if not all_xs:
|
|
738
|
+
return path_data, {"minX":-5,"maxX":5,"minY":-5,"maxY":5}, {"minX":-32,"maxX":32,"minY":-32,"maxY":32}
|
|
739
|
+
|
|
740
|
+
mid_x = (min(all_xs) + max(all_xs)) / 2
|
|
741
|
+
mid_y = (min(all_ys) + max(all_ys)) / 2
|
|
742
|
+
for seg in path_data:
|
|
743
|
+
seg["p"][0] = round(seg["p"][0] - mid_x, 5)
|
|
744
|
+
seg["p"][1] = round(seg["p"][1] - mid_y, 5)
|
|
745
|
+
seg["p"][2] = round(seg["p"][2] - mid_x, 5)
|
|
746
|
+
seg["p"][3] = round(seg["p"][3] - mid_y, 5)
|
|
747
|
+
all_xs = [x - mid_x for x in all_xs]
|
|
748
|
+
all_ys = [y - mid_y for y in all_ys]
|
|
749
|
+
|
|
750
|
+
bounds = {
|
|
751
|
+
"minX": round(min(all_xs), 5),
|
|
752
|
+
"maxX": round(max(all_xs), 5),
|
|
753
|
+
"minY": round(min(all_ys), 5),
|
|
754
|
+
"maxY": round(max(all_ys), 5),
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
fill_bounds = {
|
|
758
|
+
"minX": round(min(all_xs) * ppu / scale),
|
|
759
|
+
"maxX": round(max(all_xs) * ppu / scale),
|
|
760
|
+
"minY": round(-max(all_ys) * ppu / scale),
|
|
761
|
+
"maxY": round(-min(all_ys) * ppu / scale),
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return path_data, bounds, fill_bounds
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def build_drawing2_vector(path_data, bounds, fill_bounds,
|
|
768
|
+
scale=10, ppu=SVG_DEFAULT_PPU) -> dict:
|
|
769
|
+
frame = {
|
|
770
|
+
"isLinked": False,
|
|
771
|
+
"pathDataList": path_data,
|
|
772
|
+
"fillImageBounds": fill_bounds,
|
|
773
|
+
"avatarX": 0, "avatarY": 0, "avatarRadius": 5,
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
"initialFrame": 1, "currentFrame": 1,
|
|
777
|
+
"framesPerSecond": 4,
|
|
778
|
+
"playMode": "still",
|
|
779
|
+
"loopStartFrame": -1, "loopEndFrame": -1,
|
|
780
|
+
"opacity": 1,
|
|
781
|
+
"hash": str(abs(hash(str(path_data))))[:19],
|
|
782
|
+
"playing": False, "loop": False,
|
|
783
|
+
"drawData": {
|
|
784
|
+
"color": [1,1,1,1], "lineColor": [0,0,0,1],
|
|
785
|
+
"gridSize": 0.71428, "scale": scale, "version": 3,
|
|
786
|
+
"fillPixelsPerUnit": ppu,
|
|
787
|
+
"numTotalLayers": 1,
|
|
788
|
+
"framesBounds": [bounds],
|
|
789
|
+
"colors": [], "selectedFrame": 1,
|
|
790
|
+
"layers": [{
|
|
791
|
+
"title": "Layer 1", "id": "layer1",
|
|
792
|
+
"isVisible": True, "isBitmap": False, "isAvatar": False,
|
|
793
|
+
"frames": [frame],
|
|
794
|
+
}],
|
|
795
|
+
},
|
|
796
|
+
"physicsBodyData": {
|
|
797
|
+
"shapes": [{
|
|
798
|
+
"p1": {"x": 5, "y": 5}, "p2": {"x": -5, "y": -5},
|
|
799
|
+
"p3": {"x": 0, "y": 0}, "radius": 0, "x": 0, "y": 0,
|
|
800
|
+
"type": "rectangle",
|
|
801
|
+
}],
|
|
802
|
+
"scale": scale, "version": 2, "zeroShapesInV1": False,
|
|
803
|
+
},
|
|
804
|
+
"disabled": False,
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
|
|
387
808
|
# ── midi → Music ─────────────────────────────────────────────────────────────
|
|
388
809
|
|
|
389
810
|
def beat_key(b): return f"{b:.6f}"
|
|
@@ -667,9 +1088,6 @@ def main():
|
|
|
667
1088
|
|
|
668
1089
|
p()
|
|
669
1090
|
if is_vector:
|
|
670
|
-
if not HAS_SVG:
|
|
671
|
-
pe("svg_to_castle.py not found — place it in the same folder as castle_tool.py")
|
|
672
|
-
sys.exit(1)
|
|
673
1091
|
pb(f"Loading SVG: {img_path.name}")
|
|
674
1092
|
svg_scale = 1.0
|
|
675
1093
|
if yn("Would you like to scale the SVG output?", default="n"):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: castletool
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Interactive Castle deck editor for injecting images/GIFs and MIDI into Castle blueprint JSON files.
|
|
5
5
|
Author-email: Your Name <your.email@example.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "castletool"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Interactive Castle deck editor for injecting images/GIFs and MIDI into Castle blueprint JSON files."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Your Name", email = "your.email@example.com" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|