Integrate wasm binary
This commit is contained in:
parent
9e2ae8cdf8
commit
400cd9582d
3 changed files with 110 additions and 38 deletions
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
35
uv.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue