Skip to content

Commit 0edc10d

Browse files
Merge pull request #109 from lightningpixel/release/v0.3.3
Release/v0.3.3
2 parents e1c73ec + 5eb5f87 commit 0edc10d

5 files changed

Lines changed: 182 additions & 2 deletions

File tree

api/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def filter(self, record):
3131

3232
app = FastAPI(
3333
title="Modly API",
34-
version="0.3.2",
34+
version="0.3.3",
3535
lifespan=lifespan,
3636
)
3737

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "modly",
3-
"version": "0.3.2",
3+
"version": "0.3.3",
44
"description": "Local AI-powered 3D mesh generation from images",
55
"main": "./out/main/index.js",
66
"author": "Modly",

scripts/build-builtins.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ for (const id of readdirSync(srcDir)) {
4949
})
5050
console.log(`[build-builtins] ${id}: npm install done`)
5151
}
52+
53+
// Copy any Python processor files
54+
for (const file of readdirSync(extSrcDir)) {
55+
if (file.endsWith('.py')) {
56+
cpSync(join(extSrcDir, file), join(extOutDir, file))
57+
console.log(`[build-builtins] ${id}: ${file} copied`)
58+
}
59+
}
5260
}
5361

5462
console.log('[build-builtins] Done.')
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"id": "mesh-remesher",
3+
"name": "Mesh Remesher",
4+
"type": "process",
5+
"entry": "processor.py",
6+
"version": "1.0.0",
7+
"author": "Modly",
8+
"description": "Remeshes a mesh to triangle or quad topology using isotropic remeshing.",
9+
"nodes": [
10+
{
11+
"id": "remesh",
12+
"name": "Remesh",
13+
"input": "mesh",
14+
"output": "mesh",
15+
"params_schema": [
16+
{
17+
"id": "mode",
18+
"label": "Mode",
19+
"type": "select",
20+
"default": "triangle",
21+
"options": [
22+
{ "value": "triangle", "label": "Triangle" },
23+
{ "value": "quad", "label": "Quad" },
24+
{ "value": "none", "label": "None" }
25+
],
26+
"tooltip": "Triangle produces a clean uniform triangulation. Quad attempts a quad-dominant mesh. None passes the mesh through unchanged."
27+
},
28+
{
29+
"id": "target_edge_length",
30+
"label": "Target Edge Length",
31+
"type": "float",
32+
"default": 0.0,
33+
"min": 0.0,
34+
"max": 1.0,
35+
"step": 0.001,
36+
"tooltip": "Target edge length in world units. Set to 0 for automatic (uses average edge length of the input mesh)."
37+
}
38+
]
39+
}
40+
]
41+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""
2+
Mesh Remesher — built-in process extension.
3+
4+
Protocol: reads one JSON line from stdin, writes JSON lines to stdout.
5+
stdin : { input, params, workspaceDir, tempDir }
6+
stdout: { type: "progress"|"log"|"done"|"error", ... }
7+
"""
8+
import json
9+
import os
10+
import shutil
11+
import sys
12+
import tempfile
13+
from pathlib import Path
14+
15+
16+
def emit(obj: dict) -> None:
17+
print(json.dumps(obj), flush=True)
18+
19+
20+
def progress(pct: int, label: str) -> None:
21+
emit({"type": "progress", "percent": pct, "label": label})
22+
23+
24+
def log(msg: str) -> None:
25+
emit({"type": "log", "message": msg})
26+
27+
28+
def done(file_path: str) -> None:
29+
emit({"type": "done", "result": {"filePath": file_path}})
30+
31+
32+
def error(msg: str) -> None:
33+
emit({"type": "error", "message": msg})
34+
35+
36+
def main() -> None:
37+
raw = sys.stdin.readline()
38+
data = json.loads(raw)
39+
40+
input_data = data.get("input", {})
41+
params = data.get("params", {})
42+
workspace_dir = data.get("workspaceDir", "")
43+
44+
input_path = input_data.get("filePath")
45+
if not input_path or not Path(input_path).is_file():
46+
error(f"mesh-remesher: input file not found: {input_path}")
47+
return
48+
49+
mode = str(params.get("mode", "triangle"))
50+
target_edge_length = float(params.get("target_edge_length", 0.0))
51+
52+
out_dir = Path(workspace_dir) / "Workflows"
53+
out_dir.mkdir(parents=True, exist_ok=True)
54+
from time import time
55+
out_path = str(out_dir / f"mesh-remesher-{int(time() * 1000)}.glb")
56+
57+
log(f"Mode: {mode}, edge length: {target_edge_length or 'auto'}")
58+
59+
if mode == "none":
60+
progress(50, "Passing through…")
61+
shutil.copy2(input_path, out_path)
62+
progress(100, "Done")
63+
done(out_path)
64+
return
65+
66+
try:
67+
import pymeshlab
68+
except ImportError:
69+
error("mesh-remesher: pymeshlab is not available on this system")
70+
return
71+
72+
import trimesh
73+
74+
progress(10, "Loading mesh…")
75+
loaded = trimesh.load(input_path)
76+
if isinstance(loaded, trimesh.Scene):
77+
geoms = list(loaded.geometry.values())
78+
geom = trimesh.util.concatenate(geoms) if len(geoms) > 1 else geoms[0]
79+
else:
80+
geom = loaded
81+
82+
tmp_dir = tempfile.mkdtemp()
83+
try:
84+
ply_in = os.path.join(tmp_dir, "input.ply")
85+
ply_out = os.path.join(tmp_dir, "output.ply")
86+
geom.export(ply_in)
87+
88+
ms = pymeshlab.MeshSet()
89+
ms.load_new_mesh(ply_in)
90+
91+
if target_edge_length <= 0:
92+
measures = ms.get_geometric_measures()
93+
target_edge_length = float(measures.get("avg_edge_length", 0.02))
94+
log(f"Auto edge length: {target_edge_length:.6f}")
95+
96+
progress(30, f"Remeshing ({mode})…")
97+
98+
if mode == "triangle":
99+
ms.meshing_isotropic_explicit_remeshing(
100+
targetlen=pymeshlab.PureValue(target_edge_length),
101+
iterations=3,
102+
)
103+
elif mode == "quad":
104+
ms.meshing_isotropic_explicit_remeshing(
105+
targetlen=pymeshlab.PureValue(target_edge_length),
106+
iterations=3,
107+
)
108+
try:
109+
ms.generate_polygonal_mesh()
110+
ms.meshing_poly_to_tri()
111+
except Exception:
112+
pass
113+
114+
progress(80, "Exporting…")
115+
ms.save_current_mesh(ply_out)
116+
result = trimesh.load(ply_out, force="mesh")
117+
finally:
118+
shutil.rmtree(tmp_dir, ignore_errors=True)
119+
120+
result.export(out_path)
121+
log(f"Output: {out_path} ({len(result.faces)} faces)")
122+
progress(100, "Done")
123+
done(out_path)
124+
125+
126+
if __name__ == "__main__":
127+
try:
128+
main()
129+
except Exception as exc:
130+
import traceback
131+
error(f"{exc}\n{traceback.format_exc()}")

0 commit comments

Comments
 (0)