Integrate wasm binary

This commit is contained in:
jaseg 2025-12-15 14:55:48 +01:00
parent 9e2ae8cdf8
commit 400cd9582d
3 changed files with 110 additions and 38 deletions

View file

@ -11,7 +11,9 @@ dependencies = [
"gerbonara>=1.6.0",
"kicad-python>=0.5.0",
"lxml>=6.0.2",
"platformdirs>=4.5.1",
"py-straight-skeleton>=0.1.0",
"wasmtime>=39.0.0",
]
authors = [{ name = "jaseg" }]
maintainers = [

View file

@ -2,10 +2,18 @@ import math
import itertools
import subprocess
import os
from tempfile import NamedTemporaryFile
from dataclasses import dataclass
from pathlib import Path
import importlib.resources
import lzma
import sys
import hashlib
import platformdirs
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import wasmtime
def interpolate(p1, p2, t, t_start=0, t_end=1):
@ -37,6 +45,53 @@ def edge_cycle(points):
return itertools.pairwise(itertools.chain(points, points[:1]))
class WasmApp:
def __init__(self, wasm_filename, cachedir="kicoil"):
module_binary = importlib.resources.read_binary(__package__, wasm_filename)
module_path_digest = hashlib.sha256(__file__.encode()).hexdigest()
module_digest = hashlib.sha256(module_binary).hexdigest()
cache_path = Path(os.getenv("KICOIL_CACHE_DIR", platformdirs.user_cache_dir(cachedir)))
cache_path.mkdir(parents=True, exist_ok=True)
cache_filename = (cache_path / f'{wasm_filename}-{module_path_digest[:8]}-{module_digest[:16]}')
self.engine = wasmtime.Engine()
try:
with cache_filename.open("rb") as cache_file:
self.module = wasmtime.Module.deserialize(self.engine, lzma.decompress(cache_file.read()))
except:
print("Preparing to run {}. This might take a while...".format(wasm_filename), file=sys.stderr)
self.module = wasmtime.Module(self.engine, module_binary)
with cache_filename.open("wb") as cache_file:
cache_file.write(lzma.compress(self.module.serialize(), preset=0))
def run(self, stdin='', argv=[]):
with NamedTemporaryFile('r') as stdout_f, NamedTemporaryFile('w') as stdin_f:
stdin_f.write(stdin)
stdin_f.flush()
wasi_cfg = wasmtime.WasiConfig()
wasi_cfg.argv = argv
wasi_cfg.stdin_file = stdin_f.name
wasi_cfg.stdout_file = stdout_f.name
wasi_cfg.inherit_stderr()
linker = wasmtime.Linker(self.engine)
linker.define_wasi()
store = wasmtime.Store(self.engine)
store.set_wasi(wasi_cfg)
self.app = linker.instantiate(store, self.module)
linker.define_instance(store, "app", self.app)
try:
self.app.exports(store)["_start"](store)
except wasmtime.ExitTrap as trap:
if trap.code != 0:
raise
return 0, stdout_f.read()
@dataclass(frozen=True)
class SkeletonNode:
x: float
@ -56,38 +111,22 @@ def polygon_is_clockwise(points):
return det < 0
def compute_skeleton_cli(exterior):
# Find the skeleton_cli binary
# Look in project root directory
cli_path = Path(__file__).parent.parent.parent / 'skeleton_cli'
if not cli_path.exists():
raise FileNotFoundError(f"skeleton_cli binary not found at {cli_path}")
skeleton_wasm = WasmApp('skeleton.wasm')
# Prepare input: one point per line
def compute_skeleton(exterior):
points_deduplicated = []
for p1, p2 in edge_cycle(exterior):
if p2 != p1:
points_deduplicated.append(p1)
input_data = '\n'.join(f'{x} {y}' for x, y in points_deduplicated)
Path('/tmp/debug.txt').write_text(input_data)
# Run the CLI program
try:
result = subprocess.run(
[str(cli_path)],
input=input_data,
capture_output=True,
text=True,
check=True
)
except subprocess.CalledProcessError as e:
raise ValueError(f'Error computing polygon straight skeleton. CGAL says: {e.stdout.rstrip()}\n{e.stderr.rstrip()}')
rc, data = skeleton_wasm.run(input_data)
# Parse output: each line is "x1 y1 x2 y2 t1 t2"
node_dict = {} # Map (x, y, t) to SkeletonNode
node_map = {} # Map (x, y, t) to SkeletonNode
edges = []
for line in result.stdout.strip().split('\n'):
for line in data.strip().split('\n'):
if not line:
continue
@ -97,21 +136,17 @@ def compute_skeleton_cli(exterior):
x1, y1, x2, y2, t1, t2 = map(float, parts)
# Create or get nodes
key1 = (x1, y1, t1)
key2 = (x2, y2, t2)
n1 = (x1, y1, t1)
if n1 not in node_map:
node_map[n1] = SkeletonNode(*n1)
n2 = (x2, y2, t2)
if n2 not in node_map:
node_map[n2] = SkeletonNode(*n2)
if key1 not in node_dict:
node_dict[key1] = SkeletonNode(x1, y1, t1)
if key2 not in node_dict:
node_dict[key2] = SkeletonNode(x2, y2, t2)
edges.append((node_map[n1], node_map[n2]))
node1 = node_dict[key1]
node2 = node_dict[key2]
edges.append((node1, node2))
nodes = list(node_dict.values())
nodes = list(node_map.values())
return nodes, edges
@ -120,7 +155,7 @@ class Skeletonator:
self.poly = poly
self.poly_edges = list(zip(poly, poly[1:] + poly[:1]))
self.circumference = sum(math.dist(a, b) for a, b in self.poly_edges)
self.skeleton_nodes, self.skeleton_edges = compute_skeleton_cli(exterior=poly, holes=[])
self.skeleton_nodes, self.skeleton_edges = compute_skeleton(exterior=poly)
self.arc_map = {}
self.divergent = set()
self.radius = max(n.time for n in self.skeleton_nodes)
@ -263,7 +298,7 @@ class Skeletonator:
elif n in self.arc_map:
ax.plot(n.x, n.y, 'ro', markersize=3, alpha=0.5)
else:
ax.plot(node.x, node.y, 'o', color='magenta', markersize=6)
ax.plot(n.x, n.y, 'o', color='magenta', markersize=6)
ax.set_aspect('equal', adjustable='box')
ax.grid(True, alpha=0.3)
@ -271,6 +306,6 @@ class Skeletonator:
ax.set_title(f'Polygon Skeleton (radius: {self.radius:.3f}, min_radius: {self.min_radius:.3f})')
ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.inver_yaxis()
ax.invert_yaxis()
pdf.savefig(fig, bbox_inches='tight')
plt.close(fig)

35
uv.lock generated
View file

@ -394,6 +394,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
]
[[package]]
name = "importlib-resources"
version = "6.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
]
[[package]]
name = "ipykernel"
version = "7.1.0"
@ -536,7 +545,9 @@ dependencies = [
{ name = "gerbonara" },
{ name = "kicad-python" },
{ name = "lxml" },
{ name = "platformdirs" },
{ name = "py-straight-skeleton" },
{ name = "wasmtime" },
]
[package.dev-dependencies]
@ -555,7 +566,9 @@ requires-dist = [
{ name = "gerbonara", specifier = ">=1.6.0" },
{ name = "kicad-python", specifier = ">=0.5.0" },
{ name = "lxml", specifier = ">=6.0.2" },
{ name = "platformdirs", specifier = ">=4.5.1" },
{ name = "py-straight-skeleton", specifier = ">=0.1.0" },
{ name = "wasmtime", specifier = ">=39.0.0" },
]
[package.metadata.requires-dev]
@ -1273,6 +1286,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "wasmtime"
version = "39.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-resources" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/7c/da1dff86d6d66cd95ab17241e6aa3aef5f8fb316eec8fb956ca23c000347/wasmtime-39.0.0.tar.gz", hash = "sha256:30a27221b3fac84bc6247b34339ff6f417b989728513fa4b957a26742651ff7c", size = 117253, upload-time = "2025-11-20T21:13:01.363Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/2d/820cc89e430e97bc2760b96f2728feb049ec625bbcf0ec1be9c949f65019/wasmtime-39.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:8ddd8905b7786b791bae5413d86c42e89e2f846bdbc66b307a1d56841bf97b2b", size = 6839712, upload-time = "2025-11-20T21:12:41.853Z" },
{ url = "https://files.pythonhosted.org/packages/da/1c/8bef06fc7c0ab4c521f5f3864f362ddde99294cfcca21bb621a8d7b61241/wasmtime-39.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:1b699b59a443f4688b49f2e4d19895b08783ca1a0151c4009e5fa6e06766c869", size = 7672122, upload-time = "2025-11-20T21:12:43.842Z" },
{ url = "https://files.pythonhosted.org/packages/5a/69/48abeb238baa42e7cfc41fc3e67676130804842e7269169af963d91d02f1/wasmtime-39.0.0-py3-none-any.whl", hash = "sha256:d5e60ffb196bac6e96f4f7c796aa592e647179ff8aa7da97b3c77a40a59dfde7", size = 6255336, upload-time = "2025-11-20T21:12:45.125Z" },
{ url = "https://files.pythonhosted.org/packages/7d/85/1c53a16c39de3dbcfa70342d3550e162bc5fa347ab5eb8c55478d40b5702/wasmtime-39.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:78bd4965b66d98ffae444784fcd70c0390c59fb0a04813c5526731a8bc80c029", size = 7492414, upload-time = "2025-11-20T21:12:46.983Z" },
{ url = "https://files.pythonhosted.org/packages/9b/56/211bb7b1eeb949406854ae22d838d4ffab97e683958420dd08369394933b/wasmtime-39.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:24525b09e077f67311310503b0e5d08d9887f4eb79ac1b9ffe5cb5c348f8a412", size = 6492408, upload-time = "2025-11-20T21:12:48.787Z" },
{ url = "https://files.pythonhosted.org/packages/52/ca/eaa71d487fe87d342d26de5186587a31fc978ed42d8c44087cf45351b528/wasmtime-39.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:bc5a9dfeeb692877bb5c38439e11253d1553a9d2631e8421552f9bba04af6360", size = 7753991, upload-time = "2025-11-20T21:12:50.792Z" },
{ url = "https://files.pythonhosted.org/packages/eb/03/49284533cb9331f3d906de80893e5750b661cd45e1923b5628da4abe45c8/wasmtime-39.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:b1572becb900e50f63c604fba53d10ce58877d122c802f6302e07dbcb4dd8ca6", size = 6754932, upload-time = "2025-11-20T21:12:52.569Z" },
{ url = "https://files.pythonhosted.org/packages/26/b1/93745d0d3b5d1a1481f9826f530d03e2f338c4e7d5cfe21857bddd114d97/wasmtime-39.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e82d4b1a406cd34c19bd3c531084347f0fc7a0b4b4393530e833c9eaad459bbc", size = 6818651, upload-time = "2025-11-20T21:12:54.545Z" },
{ url = "https://files.pythonhosted.org/packages/e0/ce/576077a17e48f6645943c7c607ac22b9d51521261a6dafa7881a9a151506/wasmtime-39.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:adb94db5b013ebcbd27fb891015de22e21352c5a7a3b28d3d08fc627bdb082f4", size = 7779203, upload-time = "2025-11-20T21:12:56.393Z" },
{ url = "https://files.pythonhosted.org/packages/20/40/24af9eab59a4169f390e3e00b09998943bf22ee2f67eca4e7b11560601d1/wasmtime-39.0.0-py3-none-win_amd64.whl", hash = "sha256:b3db32e65660bc3f245636b2919455af69bd8e754458bc18a5126565b0cd3d9a", size = 6255344, upload-time = "2025-11-20T21:12:57.686Z" },
{ url = "https://files.pythonhosted.org/packages/86/c1/4ac0e00183cce085e44ea0cf78f628c9ef33cb8f9bf8fe6f97e3118be4b1/wasmtime-39.0.0-py3-none-win_arm64.whl", hash = "sha256:d4254bae165b71d1dd344dbec3b465206467319d17220d60b3efefb72a5483a8", size = 5371096, upload-time = "2025-11-20T21:12:59.976Z" },
]
[[package]]
name = "wcwidth"
version = "0.2.14"