Spaces:
Sleeping
Sleeping
Commit ·
92bb652
1
Parent(s): 04b33e7
Lots of updates (x-y origin, formatting, combined TIFFS)
Browse files
README.md
CHANGED
|
@@ -31,6 +31,10 @@ Then open the local Gradio URL in your browser, upload an STL file, and generate
|
|
| 31 |
- Lets you step through the slice stack in the browser
|
| 32 |
- Exports a ZIP containing the generated TIFF images
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
## Test
|
| 35 |
|
| 36 |
```powershell
|
|
|
|
| 31 |
- Lets you step through the slice stack in the browser
|
| 32 |
- Exports a ZIP containing the generated TIFF images
|
| 33 |
|
| 34 |
+
## Notes
|
| 35 |
+
|
| 36 |
+
- G-code generation uses the slicer's `Pixel Size/Fill Width` for XY step distance by passing `fil_width=pixel_size` into `generate_snake_path_gcode()`.
|
| 37 |
+
|
| 38 |
## Test
|
| 39 |
|
| 40 |
```powershell
|
app.py
CHANGED
|
@@ -18,7 +18,7 @@ from tiff_to_gcode import generate_snake_path_gcode
|
|
| 18 |
ViewerState = dict[str, Any]
|
| 19 |
SAMPLE_STL_FILENAMES = ("Hollow_Pyramid.stl", "Rounded_Cube_Through_Holes.stl", "halfsphere.stl")
|
| 20 |
SAMPLE_STL_DIR = Path(__file__).resolve().parent / "sample_stls"
|
| 21 |
-
FRONT_CAMERA = (
|
| 22 |
APP_CSS = """
|
| 23 |
.gradio-container {
|
| 24 |
font-size: 90%;
|
|
@@ -360,17 +360,30 @@ def _opacity_to_alpha(opacity: float) -> int:
|
|
| 360 |
return int(round(255 * bounded))
|
| 361 |
|
| 362 |
|
| 363 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
"""Export a GLB containing the mesh, origin axes, and a Z=0 grid plane."""
|
| 365 |
scene = trimesh.Scene()
|
| 366 |
display_transform = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
|
| 367 |
|
| 368 |
-
# --- Model (
|
| 369 |
model_copy = mesh.copy()
|
| 370 |
model_copy.apply_transform(display_transform)
|
|
|
|
| 371 |
mat = trimesh.visual.material.PBRMaterial(
|
| 372 |
-
baseColorFactor=[
|
| 373 |
-
alphaMode="BLEND",
|
| 374 |
metallicFactor=0.0,
|
| 375 |
roughnessFactor=0.6,
|
| 376 |
)
|
|
@@ -476,23 +489,24 @@ def _build_annotated_scene(mesh: trimesh.Trimesh, opacity: float = 0.96) -> str:
|
|
| 476 |
return str(out_path)
|
| 477 |
|
| 478 |
|
| 479 |
-
def load_single_model(stl_file: str | None, opacity: float =
|
| 480 |
if not stl_file:
|
| 481 |
-
return None, "No model loaded."
|
| 482 |
mesh = load_mesh(stl_file)
|
| 483 |
-
glb_path = _build_annotated_scene(mesh, opacity=opacity)
|
| 484 |
-
return glb_path, _format_model_details(Path(stl_file).name, mesh)
|
| 485 |
|
| 486 |
|
| 487 |
-
def preload_sample_models(opacity: float =
|
| 488 |
outputs: list[Any] = []
|
|
|
|
| 489 |
|
| 490 |
for filename in SAMPLE_STL_FILENAMES:
|
| 491 |
stl_path = SAMPLE_STL_DIR / filename
|
| 492 |
if not stl_path.exists():
|
| 493 |
outputs.extend([
|
| 494 |
None,
|
| 495 |
-
None,
|
| 496 |
f"Sample file not found: {stl_path}",
|
| 497 |
])
|
| 498 |
continue
|
|
@@ -502,14 +516,14 @@ def preload_sample_models(opacity: float = 0.96) -> tuple:
|
|
| 502 |
except Exception as exc:
|
| 503 |
outputs.extend([
|
| 504 |
str(stl_path),
|
| 505 |
-
None,
|
| 506 |
f"Failed to load sample model: {stl_path.name} ({exc})",
|
| 507 |
])
|
| 508 |
continue
|
| 509 |
|
| 510 |
outputs.extend([
|
| 511 |
str(stl_path),
|
| 512 |
-
_build_annotated_scene(mesh, opacity=
|
| 513 |
_format_model_details(stl_path.name, mesh),
|
| 514 |
])
|
| 515 |
|
|
@@ -523,11 +537,12 @@ def refresh_all_model_viewers(
|
|
| 523 |
opacity: float,
|
| 524 |
) -> tuple:
|
| 525 |
outputs: list[Any] = []
|
|
|
|
| 526 |
for stl_file in (stl1, stl2, stl3):
|
| 527 |
if not stl_file:
|
| 528 |
-
outputs.extend([None, "No model loaded."])
|
| 529 |
continue
|
| 530 |
-
outputs.extend(load_single_model(stl_file,
|
| 531 |
return tuple(outputs)
|
| 532 |
|
| 533 |
|
|
@@ -848,6 +863,11 @@ def build_demo() -> gr.Blocks:
|
|
| 848 |
min_width=140,
|
| 849 |
scale=0,
|
| 850 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 851 |
|
| 852 |
# --- Upload + 3D viewer row ---
|
| 853 |
stl_files: list[gr.File] = []
|
|
@@ -876,15 +896,13 @@ def build_demo() -> gr.Blocks:
|
|
| 876 |
|
| 877 |
# --- Shared slicing controls ---
|
| 878 |
with gr.Row():
|
| 879 |
-
model_opacity = gr.Slider(
|
| 880 |
-
label="3D Model Opacity",
|
| 881 |
-
minimum=0.2,
|
| 882 |
-
maximum=1.0,
|
| 883 |
-
value=0.96,
|
| 884 |
-
step=0.05,
|
| 885 |
-
)
|
| 886 |
layer_height = gr.Number(label="Layer Height", value=0.8, minimum=0.0001, step=0.01)
|
| 887 |
-
pixel_size = gr.Number(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 888 |
generate_button = gr.Button("Generate TIFF Stacks", variant="primary")
|
| 889 |
|
| 890 |
# --- Per-object slice browsers ---
|
|
@@ -1030,7 +1048,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1030 |
for i in range(3):
|
| 1031 |
refresh_outputs.extend([model_viewers[i], model_details_list[i]])
|
| 1032 |
|
| 1033 |
-
model_opacity.
|
| 1034 |
fn=refresh_all_model_viewers,
|
| 1035 |
inputs=[stl_files[0], stl_files[1], stl_files[2], model_opacity],
|
| 1036 |
outputs=refresh_outputs,
|
|
|
|
| 18 |
ViewerState = dict[str, Any]
|
| 19 |
SAMPLE_STL_FILENAMES = ("Hollow_Pyramid.stl", "Rounded_Cube_Through_Holes.stl", "halfsphere.stl")
|
| 20 |
SAMPLE_STL_DIR = Path(__file__).resolve().parent / "sample_stls"
|
| 21 |
+
FRONT_CAMERA = (90, 80, None)
|
| 22 |
APP_CSS = """
|
| 23 |
.gradio-container {
|
| 24 |
font-size: 90%;
|
|
|
|
| 360 |
return int(round(255 * bounded))
|
| 361 |
|
| 362 |
|
| 363 |
+
def _resolve_model_opacity(setting: float | bool | None) -> float:
|
| 364 |
+
if isinstance(setting, bool):
|
| 365 |
+
return 0.75 if setting else 1.0
|
| 366 |
+
if setting is None:
|
| 367 |
+
return 1.0
|
| 368 |
+
return max(0.05, min(float(setting), 1.0))
|
| 369 |
+
|
| 370 |
+
|
| 371 |
+
def _viewer_update(model_path: str | None) -> dict[str, Any]:
|
| 372 |
+
return gr.update(value=model_path, camera_position=FRONT_CAMERA)
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _build_annotated_scene(mesh: trimesh.Trimesh, opacity: float = 1.0) -> str:
|
| 376 |
"""Export a GLB containing the mesh, origin axes, and a Z=0 grid plane."""
|
| 377 |
scene = trimesh.Scene()
|
| 378 |
display_transform = trimesh.transformations.rotation_matrix(-np.pi / 2, [1, 0, 0])
|
| 379 |
|
| 380 |
+
# --- Model (muted orange to match the Gradio theme accent) ---
|
| 381 |
model_copy = mesh.copy()
|
| 382 |
model_copy.apply_transform(display_transform)
|
| 383 |
+
bounded_opacity = _resolve_model_opacity(opacity)
|
| 384 |
mat = trimesh.visual.material.PBRMaterial(
|
| 385 |
+
baseColorFactor=[230, 150, 90, _opacity_to_alpha(bounded_opacity)],
|
| 386 |
+
alphaMode="OPAQUE" if bounded_opacity >= 0.999 else "BLEND",
|
| 387 |
metallicFactor=0.0,
|
| 388 |
roughnessFactor=0.6,
|
| 389 |
)
|
|
|
|
| 489 |
return str(out_path)
|
| 490 |
|
| 491 |
|
| 492 |
+
def load_single_model(stl_file: str | None, opacity: float = 1.0) -> tuple[str | None, str]:
|
| 493 |
if not stl_file:
|
| 494 |
+
return _viewer_update(None), "No model loaded."
|
| 495 |
mesh = load_mesh(stl_file)
|
| 496 |
+
glb_path = _build_annotated_scene(mesh, opacity=_resolve_model_opacity(opacity))
|
| 497 |
+
return _viewer_update(glb_path), _format_model_details(Path(stl_file).name, mesh)
|
| 498 |
|
| 499 |
|
| 500 |
+
def preload_sample_models(opacity: float = 1.0) -> tuple:
|
| 501 |
outputs: list[Any] = []
|
| 502 |
+
resolved_opacity = _resolve_model_opacity(opacity)
|
| 503 |
|
| 504 |
for filename in SAMPLE_STL_FILENAMES:
|
| 505 |
stl_path = SAMPLE_STL_DIR / filename
|
| 506 |
if not stl_path.exists():
|
| 507 |
outputs.extend([
|
| 508 |
None,
|
| 509 |
+
_viewer_update(None),
|
| 510 |
f"Sample file not found: {stl_path}",
|
| 511 |
])
|
| 512 |
continue
|
|
|
|
| 516 |
except Exception as exc:
|
| 517 |
outputs.extend([
|
| 518 |
str(stl_path),
|
| 519 |
+
_viewer_update(None),
|
| 520 |
f"Failed to load sample model: {stl_path.name} ({exc})",
|
| 521 |
])
|
| 522 |
continue
|
| 523 |
|
| 524 |
outputs.extend([
|
| 525 |
str(stl_path),
|
| 526 |
+
_viewer_update(_build_annotated_scene(mesh, opacity=resolved_opacity)),
|
| 527 |
_format_model_details(stl_path.name, mesh),
|
| 528 |
])
|
| 529 |
|
|
|
|
| 537 |
opacity: float,
|
| 538 |
) -> tuple:
|
| 539 |
outputs: list[Any] = []
|
| 540 |
+
resolved_opacity = _resolve_model_opacity(opacity)
|
| 541 |
for stl_file in (stl1, stl2, stl3):
|
| 542 |
if not stl_file:
|
| 543 |
+
outputs.extend([_viewer_update(None), "No model loaded."])
|
| 544 |
continue
|
| 545 |
+
outputs.extend(load_single_model(stl_file, resolved_opacity))
|
| 546 |
return tuple(outputs)
|
| 547 |
|
| 548 |
|
|
|
|
| 863 |
min_width=140,
|
| 864 |
scale=0,
|
| 865 |
)
|
| 866 |
+
with gr.Column(scale=0, min_width=240):
|
| 867 |
+
model_opacity = gr.Checkbox(
|
| 868 |
+
label="Use 75% 3D Model Opacity",
|
| 869 |
+
value=False,
|
| 870 |
+
)
|
| 871 |
|
| 872 |
# --- Upload + 3D viewer row ---
|
| 873 |
stl_files: list[gr.File] = []
|
|
|
|
| 896 |
|
| 897 |
# --- Shared slicing controls ---
|
| 898 |
with gr.Row():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
layer_height = gr.Number(label="Layer Height", value=0.8, minimum=0.0001, step=0.01)
|
| 900 |
+
pixel_size = gr.Number(
|
| 901 |
+
label="Pixel Size/Fill Width",
|
| 902 |
+
value=0.8,
|
| 903 |
+
minimum=0.0001,
|
| 904 |
+
step=0.01,
|
| 905 |
+
)
|
| 906 |
generate_button = gr.Button("Generate TIFF Stacks", variant="primary")
|
| 907 |
|
| 908 |
# --- Per-object slice browsers ---
|
|
|
|
| 1048 |
for i in range(3):
|
| 1049 |
refresh_outputs.extend([model_viewers[i], model_details_list[i]])
|
| 1050 |
|
| 1051 |
+
model_opacity.change(
|
| 1052 |
fn=refresh_all_model_viewers,
|
| 1053 |
inputs=[stl_files[0], stl_files[1], stl_files[2], model_opacity],
|
| 1054 |
outputs=refresh_outputs,
|