Spaces:
Running
Running
User friendly scaling factors
Browse files- README.md +1 -1
- app.py +240 -83
- tests/test_app_scaling.py +17 -4
README.md
CHANGED
|
@@ -41,7 +41,7 @@ Then open the local Gradio URL in your browser, upload STL files or load the bun
|
|
| 41 |
- Loads bundled sample STL files
|
| 42 |
- Shows interactive 3D viewers for rotating each model
|
| 43 |
- Shows model extents, face count, vertex count, and watertight status
|
| 44 |
-
- Optionally scales loaded STLs per shape, either
|
| 45 |
- Lets you choose layer height and XY pixel size
|
| 46 |
- Produces one `.tif` image per slice
|
| 47 |
- Encodes material as black (`0`) and empty space as white (`255`) in each TIFF slice
|
|
|
|
| 41 |
- Loads bundled sample STL files
|
| 42 |
- Shows interactive 3D viewers for rotating each model
|
| 43 |
- Shows model extents, face count, vertex count, and watertight status
|
| 44 |
+
- Optionally scales loaded STLs per shape, either with independent target X/Y/Z dimensions or by keeping proportions while target dimensions update together
|
| 45 |
- Lets you choose layer height and XY pixel size
|
| 46 |
- Produces one `.tif` image per slice
|
| 47 |
- Encodes material as black (`0`) and empty space as white (`255`) in each TIFF slice
|
app.py
CHANGED
|
@@ -41,9 +41,9 @@ ViewerState = dict[str, Any]
|
|
| 41 |
SAMPLE_STL_FILENAMES = ("Hollow_Pyramid.stl", "Rounded_Cube_Through_Holes.stl", "halfsphere.stl")
|
| 42 |
SAMPLE_STL_DIR = Path(__file__).resolve().parent / "sample_stls"
|
| 43 |
DEFAULT_TARGET_EXTENTS = (20.0, 20.0, 20.0)
|
| 44 |
-
|
| 45 |
-
SCALE_MODE_TARGET_DIMENSIONS = "
|
| 46 |
-
SCALE_MODE_UNIFORM_FACTOR = "
|
| 47 |
FRONT_CAMERA = (90, 80, None)
|
| 48 |
APP_CSS = """
|
| 49 |
.gradio-container {
|
|
@@ -979,21 +979,40 @@ def _resolve_target_extents(
|
|
| 979 |
return (target[0], target[1], target[2])
|
| 980 |
|
| 981 |
|
| 982 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
scale_to_target: bool | None,
|
| 984 |
-
|
|
|
|
|
|
|
|
|
|
| 985 |
) -> float | None:
|
| 986 |
if not scale_to_target:
|
| 987 |
return None
|
| 988 |
|
| 989 |
-
|
| 990 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 991 |
|
| 992 |
-
|
| 993 |
-
if
|
| 994 |
-
|
|
|
|
| 995 |
|
| 996 |
-
return
|
| 997 |
|
| 998 |
|
| 999 |
def _normalize_scale_mode(scale_mode: str | None) -> str:
|
|
@@ -1020,14 +1039,6 @@ def _shape_target_values(
|
|
| 1020 |
)
|
| 1021 |
|
| 1022 |
|
| 1023 |
-
def _shape_uniform_values(
|
| 1024 |
-
uniform1: float | None,
|
| 1025 |
-
uniform2: float | None,
|
| 1026 |
-
uniform3: float | None,
|
| 1027 |
-
) -> tuple[float | None, float | None, float | None]:
|
| 1028 |
-
return (uniform1, uniform2, uniform3)
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
def _resolve_mesh_scale_factors(
|
| 1032 |
mesh: trimesh.Trimesh,
|
| 1033 |
scale_to_target: bool | None,
|
|
@@ -1035,13 +1046,19 @@ def _resolve_mesh_scale_factors(
|
|
| 1035 |
target_x: float | None,
|
| 1036 |
target_y: float | None,
|
| 1037 |
target_z: float | None,
|
| 1038 |
-
uniform_scale: float | None,
|
| 1039 |
) -> tuple[float, float, float] | None:
|
| 1040 |
if not scale_to_target:
|
| 1041 |
return None
|
| 1042 |
|
| 1043 |
if _normalize_scale_mode(scale_mode) == SCALE_MODE_UNIFORM_FACTOR:
|
| 1044 |
-
scale =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
return (scale, scale, scale)
|
| 1046 |
|
| 1047 |
target_extents = _resolve_target_extents(True, target_x, target_y, target_z)
|
|
@@ -1050,6 +1067,39 @@ def _resolve_mesh_scale_factors(
|
|
| 1050 |
return scale_factors_for_target_extents(mesh, target_extents)
|
| 1051 |
|
| 1052 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1053 |
def _load_model_mesh(
|
| 1054 |
stl_file: str | Path,
|
| 1055 |
scale_to_target: bool | None = False,
|
|
@@ -1057,7 +1107,6 @@ def _load_model_mesh(
|
|
| 1057 |
target_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1058 |
target_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1059 |
target_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1060 |
-
uniform_scale: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1061 |
) -> tuple[trimesh.Trimesh, tuple[float, float, float]]:
|
| 1062 |
mesh = load_mesh(stl_file)
|
| 1063 |
scale_factors = _resolve_mesh_scale_factors(
|
|
@@ -1067,7 +1116,6 @@ def _load_model_mesh(
|
|
| 1067 |
target_x,
|
| 1068 |
target_y,
|
| 1069 |
target_z,
|
| 1070 |
-
uniform_scale,
|
| 1071 |
)
|
| 1072 |
if scale_factors is None:
|
| 1073 |
return mesh, (1.0, 1.0, 1.0)
|
|
@@ -1078,6 +1126,55 @@ def _viewer_update(model_path: str | None) -> dict[str, Any]:
|
|
| 1078 |
return gr.update(value=model_path, camera_position=FRONT_CAMERA)
|
| 1079 |
|
| 1080 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
def _build_annotated_scene(mesh: trimesh.Trimesh, opacity: float = 1.0) -> str:
|
| 1082 |
"""Export a GLB containing the mesh, origin axes, and a Z=0 grid plane."""
|
| 1083 |
scene = trimesh.Scene()
|
|
@@ -1203,7 +1300,6 @@ def load_single_model(
|
|
| 1203 |
target_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1204 |
target_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1205 |
target_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1206 |
-
uniform_scale: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1207 |
) -> tuple[str | None, str]:
|
| 1208 |
if not stl_file:
|
| 1209 |
return _viewer_update(None), "No model loaded."
|
|
@@ -1214,7 +1310,6 @@ def load_single_model(
|
|
| 1214 |
target_x=target_x,
|
| 1215 |
target_y=target_y,
|
| 1216 |
target_z=target_z,
|
| 1217 |
-
uniform_scale=uniform_scale,
|
| 1218 |
)
|
| 1219 |
glb_path = _build_annotated_scene(mesh, opacity=_resolve_model_opacity(opacity))
|
| 1220 |
return _viewer_update(glb_path), _format_model_details(Path(stl_file).name, mesh, scale_factors)
|
|
@@ -1227,15 +1322,12 @@ def preload_sample_models(
|
|
| 1227 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1228 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1229 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1230 |
-
uniform1: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1231 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1232 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1233 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1234 |
-
uniform2: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1235 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1236 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1237 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1238 |
-
uniform3: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1239 |
) -> tuple:
|
| 1240 |
outputs: list[Any] = []
|
| 1241 |
resolved_opacity = _resolve_model_opacity(opacity)
|
|
@@ -1250,7 +1342,6 @@ def preload_sample_models(
|
|
| 1250 |
target3_y,
|
| 1251 |
target3_z,
|
| 1252 |
)
|
| 1253 |
-
uniform_values = _shape_uniform_values(uniform1, uniform2, uniform3)
|
| 1254 |
|
| 1255 |
for index, filename in enumerate(SAMPLE_STL_FILENAMES):
|
| 1256 |
stl_path = SAMPLE_STL_DIR / filename
|
|
@@ -1270,7 +1361,6 @@ def preload_sample_models(
|
|
| 1270 |
target_x=target_values[index][0],
|
| 1271 |
target_y=target_values[index][1],
|
| 1272 |
target_z=target_values[index][2],
|
| 1273 |
-
uniform_scale=uniform_values[index],
|
| 1274 |
)
|
| 1275 |
except Exception as exc:
|
| 1276 |
outputs.extend([
|
|
@@ -1299,15 +1389,12 @@ def refresh_all_model_viewers(
|
|
| 1299 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1300 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1301 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1302 |
-
uniform1: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1303 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1304 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1305 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1306 |
-
uniform2: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1307 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1308 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1309 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1310 |
-
uniform3: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1311 |
) -> tuple:
|
| 1312 |
outputs: list[Any] = []
|
| 1313 |
resolved_opacity = _resolve_model_opacity(opacity)
|
|
@@ -1322,9 +1409,8 @@ def refresh_all_model_viewers(
|
|
| 1322 |
target3_y,
|
| 1323 |
target3_z,
|
| 1324 |
)
|
| 1325 |
-
uniform_values = _shape_uniform_values(uniform1, uniform2, uniform3)
|
| 1326 |
|
| 1327 |
-
for stl_file, values
|
| 1328 |
if not stl_file:
|
| 1329 |
outputs.extend([_viewer_update(None), "No model loaded."])
|
| 1330 |
continue
|
|
@@ -1337,7 +1423,6 @@ def refresh_all_model_viewers(
|
|
| 1337 |
values[0],
|
| 1338 |
values[1],
|
| 1339 |
values[2],
|
| 1340 |
-
uniform_scale,
|
| 1341 |
)
|
| 1342 |
)
|
| 1343 |
return tuple(outputs)
|
|
@@ -1354,15 +1439,12 @@ def generate_all_stacks(
|
|
| 1354 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1355 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1356 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1357 |
-
uniform1: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1358 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1359 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1360 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1361 |
-
uniform2: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1362 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1363 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1364 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
| 1365 |
-
uniform3: float | None = DEFAULT_UNIFORM_SCALE,
|
| 1366 |
progress: gr.Progress = gr.Progress(),
|
| 1367 |
):
|
| 1368 |
files = [stl1, stl2, stl3]
|
|
@@ -1377,12 +1459,11 @@ def generate_all_stacks(
|
|
| 1377 |
target3_y,
|
| 1378 |
target3_z,
|
| 1379 |
)
|
| 1380 |
-
uniform_values = _shape_uniform_values(uniform1, uniform2, uniform3)
|
| 1381 |
valid_count = max(1, sum(1 for f in files if f))
|
| 1382 |
results: list = []
|
| 1383 |
completed = 0
|
| 1384 |
|
| 1385 |
-
for stl_file, values
|
| 1386 |
if not stl_file:
|
| 1387 |
results.extend([
|
| 1388 |
_empty_state(),
|
|
@@ -1411,7 +1492,6 @@ def generate_all_stacks(
|
|
| 1411 |
values[0],
|
| 1412 |
values[1],
|
| 1413 |
values[2],
|
| 1414 |
-
uniform_scale,
|
| 1415 |
)
|
| 1416 |
|
| 1417 |
stack = slice_stl_to_tiffs(
|
|
@@ -1930,7 +2010,7 @@ def build_demo() -> gr.Blocks:
|
|
| 1930 |
gr.Markdown(
|
| 1931 |
"""
|
| 1932 |
# STL to TIFF Slicer
|
| 1933 |
-
Upload up to three STL files,
|
| 1934 |
"""
|
| 1935 |
)
|
| 1936 |
|
|
@@ -1950,24 +2030,27 @@ def build_demo() -> gr.Blocks:
|
|
| 1950 |
)
|
| 1951 |
with gr.Column(scale=0, min_width=260):
|
| 1952 |
scale_to_target = gr.Checkbox(
|
| 1953 |
-
label="
|
| 1954 |
value=False,
|
| 1955 |
)
|
| 1956 |
-
|
| 1957 |
-
|
| 1958 |
-
|
| 1959 |
-
|
| 1960 |
-
|
| 1961 |
-
|
|
|
|
|
|
|
|
|
|
| 1962 |
|
| 1963 |
# --- Upload + 3D viewer row ---
|
| 1964 |
stl_files: list[gr.File] = []
|
| 1965 |
model_viewers: list[gr.Model3D] = []
|
| 1966 |
model_details_list: list[gr.Markdown] = []
|
|
|
|
| 1967 |
target_xs: list[gr.Number] = []
|
| 1968 |
target_ys: list[gr.Number] = []
|
| 1969 |
target_zs: list[gr.Number] = []
|
| 1970 |
-
uniform_scales: list[gr.Number] = []
|
| 1971 |
|
| 1972 |
with gr.Row():
|
| 1973 |
for i in range(3):
|
|
@@ -1985,38 +2068,33 @@ def build_demo() -> gr.Blocks:
|
|
| 1985 |
height=270,
|
| 1986 |
)
|
| 1987 |
model_details = gr.Markdown(f"No model {i + 1} loaded.")
|
| 1988 |
-
with gr.
|
| 1989 |
-
|
| 1990 |
-
|
| 1991 |
-
|
| 1992 |
-
|
| 1993 |
-
|
| 1994 |
-
|
| 1995 |
-
|
| 1996 |
-
|
| 1997 |
-
|
| 1998 |
-
|
| 1999 |
-
|
| 2000 |
-
|
| 2001 |
-
|
| 2002 |
-
|
| 2003 |
-
|
| 2004 |
-
|
| 2005 |
-
|
| 2006 |
-
|
| 2007 |
-
|
| 2008 |
-
label="Uniform Scale",
|
| 2009 |
-
value=DEFAULT_UNIFORM_SCALE,
|
| 2010 |
-
minimum=0.0001,
|
| 2011 |
-
step=0.01,
|
| 2012 |
-
)
|
| 2013 |
stl_files.append(stl_file)
|
| 2014 |
model_viewers.append(model_viewer)
|
| 2015 |
model_details_list.append(model_details)
|
|
|
|
| 2016 |
target_xs.append(target_x)
|
| 2017 |
target_ys.append(target_y)
|
| 2018 |
target_zs.append(target_z)
|
| 2019 |
-
uniform_scales.append(uniform_scale)
|
| 2020 |
|
| 2021 |
# --- Shared slicing controls ---
|
| 2022 |
with gr.Row():
|
|
@@ -2140,15 +2218,12 @@ def build_demo() -> gr.Blocks:
|
|
| 2140 |
target_xs[0],
|
| 2141 |
target_ys[0],
|
| 2142 |
target_zs[0],
|
| 2143 |
-
uniform_scales[0],
|
| 2144 |
target_xs[1],
|
| 2145 |
target_ys[1],
|
| 2146 |
target_zs[1],
|
| 2147 |
-
uniform_scales[1],
|
| 2148 |
target_xs[2],
|
| 2149 |
target_ys[2],
|
| 2150 |
target_zs[2],
|
| 2151 |
-
uniform_scales[2],
|
| 2152 |
]
|
| 2153 |
|
| 2154 |
for i in range(3):
|
|
@@ -2162,7 +2237,6 @@ def build_demo() -> gr.Blocks:
|
|
| 2162 |
target_xs[i],
|
| 2163 |
target_ys[i],
|
| 2164 |
target_zs[i],
|
| 2165 |
-
uniform_scales[i],
|
| 2166 |
],
|
| 2167 |
outputs=[model_viewers[i], model_details_list[i]],
|
| 2168 |
)
|
|
@@ -2186,7 +2260,7 @@ def build_demo() -> gr.Blocks:
|
|
| 2186 |
model_details_list[i],
|
| 2187 |
])
|
| 2188 |
|
| 2189 |
-
load_samples_button.click(
|
| 2190 |
fn=preload_sample_models,
|
| 2191 |
inputs=[model_opacity, scale_to_target, scale_mode, *all_scale_inputs],
|
| 2192 |
outputs=preload_outputs,
|
|
@@ -2211,7 +2285,90 @@ def build_demo() -> gr.Blocks:
|
|
| 2211 |
inputs=refresh_inputs,
|
| 2212 |
outputs=refresh_outputs,
|
| 2213 |
)
|
| 2214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2215 |
scale_control.change(
|
| 2216 |
fn=refresh_all_model_viewers,
|
| 2217 |
inputs=refresh_inputs,
|
|
|
|
| 41 |
SAMPLE_STL_FILENAMES = ("Hollow_Pyramid.stl", "Rounded_Cube_Through_Holes.stl", "halfsphere.stl")
|
| 42 |
SAMPLE_STL_DIR = Path(__file__).resolve().parent / "sample_stls"
|
| 43 |
DEFAULT_TARGET_EXTENTS = (20.0, 20.0, 20.0)
|
| 44 |
+
UNIFORM_TARGET_AXES = ("X", "Y", "Z")
|
| 45 |
+
SCALE_MODE_TARGET_DIMENSIONS = "Independent X/Y/Z"
|
| 46 |
+
SCALE_MODE_UNIFORM_FACTOR = "Keep Proportions"
|
| 47 |
FRONT_CAMERA = (90, 80, None)
|
| 48 |
APP_CSS = """
|
| 49 |
.gradio-container {
|
|
|
|
| 979 |
return (target[0], target[1], target[2])
|
| 980 |
|
| 981 |
|
| 982 |
+
def _axis_index(axis: str | None) -> int:
|
| 983 |
+
normalized = (axis or "X").upper()
|
| 984 |
+
if normalized not in UNIFORM_TARGET_AXES:
|
| 985 |
+
raise ValueError("Uniform target side must be X, Y, or Z.")
|
| 986 |
+
return UNIFORM_TARGET_AXES.index(normalized)
|
| 987 |
+
|
| 988 |
+
|
| 989 |
+
def _resolve_uniform_scale_from_targets(
|
| 990 |
+
mesh: trimesh.Trimesh,
|
| 991 |
scale_to_target: bool | None,
|
| 992 |
+
target_x: float | None,
|
| 993 |
+
target_y: float | None,
|
| 994 |
+
target_z: float | None,
|
| 995 |
+
anchor_axis: str | None = "X",
|
| 996 |
) -> float | None:
|
| 997 |
if not scale_to_target:
|
| 998 |
return None
|
| 999 |
|
| 1000 |
+
targets = (target_x, target_y, target_z)
|
| 1001 |
+
anchor_index = _axis_index(anchor_axis)
|
| 1002 |
+
target_size = targets[anchor_index]
|
| 1003 |
+
if target_size is None:
|
| 1004 |
+
raise ValueError("Target side length is required when uniform STL scaling is enabled.")
|
| 1005 |
+
|
| 1006 |
+
target_size = float(target_size)
|
| 1007 |
+
if not math.isfinite(target_size) or target_size <= 0:
|
| 1008 |
+
raise ValueError("Target side length must be greater than zero.")
|
| 1009 |
|
| 1010 |
+
current_size = float(mesh.extents[anchor_index])
|
| 1011 |
+
if current_size <= 0:
|
| 1012 |
+
axis = UNIFORM_TARGET_AXES[anchor_index]
|
| 1013 |
+
raise ValueError(f"Cannot scale uniformly from a zero-sized {axis} extent.")
|
| 1014 |
|
| 1015 |
+
return target_size / current_size
|
| 1016 |
|
| 1017 |
|
| 1018 |
def _normalize_scale_mode(scale_mode: str | None) -> str:
|
|
|
|
| 1039 |
)
|
| 1040 |
|
| 1041 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
def _resolve_mesh_scale_factors(
|
| 1043 |
mesh: trimesh.Trimesh,
|
| 1044 |
scale_to_target: bool | None,
|
|
|
|
| 1046 |
target_x: float | None,
|
| 1047 |
target_y: float | None,
|
| 1048 |
target_z: float | None,
|
|
|
|
| 1049 |
) -> tuple[float, float, float] | None:
|
| 1050 |
if not scale_to_target:
|
| 1051 |
return None
|
| 1052 |
|
| 1053 |
if _normalize_scale_mode(scale_mode) == SCALE_MODE_UNIFORM_FACTOR:
|
| 1054 |
+
scale = _resolve_uniform_scale_from_targets(
|
| 1055 |
+
mesh,
|
| 1056 |
+
True,
|
| 1057 |
+
target_x,
|
| 1058 |
+
target_y,
|
| 1059 |
+
target_z,
|
| 1060 |
+
anchor_axis="X",
|
| 1061 |
+
)
|
| 1062 |
return (scale, scale, scale)
|
| 1063 |
|
| 1064 |
target_extents = _resolve_target_extents(True, target_x, target_y, target_z)
|
|
|
|
| 1067 |
return scale_factors_for_target_extents(mesh, target_extents)
|
| 1068 |
|
| 1069 |
|
| 1070 |
+
def _uniform_target_extents_from_anchor(
|
| 1071 |
+
mesh: trimesh.Trimesh,
|
| 1072 |
+
anchor_axis: str | None,
|
| 1073 |
+
target_x: float | None,
|
| 1074 |
+
target_y: float | None,
|
| 1075 |
+
target_z: float | None,
|
| 1076 |
+
) -> tuple[float, float, float]:
|
| 1077 |
+
scale = _resolve_uniform_scale_from_targets(
|
| 1078 |
+
mesh,
|
| 1079 |
+
True,
|
| 1080 |
+
target_x,
|
| 1081 |
+
target_y,
|
| 1082 |
+
target_z,
|
| 1083 |
+
anchor_axis=anchor_axis,
|
| 1084 |
+
)
|
| 1085 |
+
extents = np.asarray(mesh.extents, dtype=float)
|
| 1086 |
+
return (
|
| 1087 |
+
float(extents[0] * scale),
|
| 1088 |
+
float(extents[1] * scale),
|
| 1089 |
+
float(extents[2] * scale),
|
| 1090 |
+
)
|
| 1091 |
+
|
| 1092 |
+
|
| 1093 |
+
def _dimension_update(current: float | None, target: float) -> dict[str, Any]:
|
| 1094 |
+
rounded = round(float(target), 6)
|
| 1095 |
+
try:
|
| 1096 |
+
if current is not None and math.isclose(float(current), rounded, rel_tol=1e-9, abs_tol=1e-6):
|
| 1097 |
+
return gr.update()
|
| 1098 |
+
except (TypeError, ValueError):
|
| 1099 |
+
pass
|
| 1100 |
+
return gr.update(value=rounded)
|
| 1101 |
+
|
| 1102 |
+
|
| 1103 |
def _load_model_mesh(
|
| 1104 |
stl_file: str | Path,
|
| 1105 |
scale_to_target: bool | None = False,
|
|
|
|
| 1107 |
target_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1108 |
target_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1109 |
target_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1110 |
) -> tuple[trimesh.Trimesh, tuple[float, float, float]]:
|
| 1111 |
mesh = load_mesh(stl_file)
|
| 1112 |
scale_factors = _resolve_mesh_scale_factors(
|
|
|
|
| 1116 |
target_x,
|
| 1117 |
target_y,
|
| 1118 |
target_z,
|
|
|
|
| 1119 |
)
|
| 1120 |
if scale_factors is None:
|
| 1121 |
return mesh, (1.0, 1.0, 1.0)
|
|
|
|
| 1126 |
return gr.update(value=model_path, camera_position=FRONT_CAMERA)
|
| 1127 |
|
| 1128 |
|
| 1129 |
+
def update_scaling_controls_visibility(
|
| 1130 |
+
scale_to_target: bool | None,
|
| 1131 |
+
scale_mode: str | None,
|
| 1132 |
+
) -> tuple[dict[str, Any], ...]:
|
| 1133 |
+
enabled = bool(scale_to_target)
|
| 1134 |
+
|
| 1135 |
+
return (
|
| 1136 |
+
gr.update(visible=enabled),
|
| 1137 |
+
gr.update(visible=enabled),
|
| 1138 |
+
gr.update(visible=enabled),
|
| 1139 |
+
gr.update(visible=enabled),
|
| 1140 |
+
)
|
| 1141 |
+
|
| 1142 |
+
|
| 1143 |
+
def sync_uniform_target_dimensions(
|
| 1144 |
+
stl_file: str | None,
|
| 1145 |
+
scale_to_target: bool | None,
|
| 1146 |
+
scale_mode: str | None,
|
| 1147 |
+
changed_axis: str,
|
| 1148 |
+
target_x: float | None,
|
| 1149 |
+
target_y: float | None,
|
| 1150 |
+
target_z: float | None,
|
| 1151 |
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
| 1152 |
+
if (
|
| 1153 |
+
not stl_file
|
| 1154 |
+
or not scale_to_target
|
| 1155 |
+
or _normalize_scale_mode(scale_mode) != SCALE_MODE_UNIFORM_FACTOR
|
| 1156 |
+
):
|
| 1157 |
+
return gr.update(), gr.update(), gr.update()
|
| 1158 |
+
|
| 1159 |
+
try:
|
| 1160 |
+
mesh = load_mesh(stl_file)
|
| 1161 |
+
x_value, y_value, z_value = _uniform_target_extents_from_anchor(
|
| 1162 |
+
mesh,
|
| 1163 |
+
changed_axis,
|
| 1164 |
+
target_x,
|
| 1165 |
+
target_y,
|
| 1166 |
+
target_z,
|
| 1167 |
+
)
|
| 1168 |
+
except Exception:
|
| 1169 |
+
return gr.update(), gr.update(), gr.update()
|
| 1170 |
+
|
| 1171 |
+
return (
|
| 1172 |
+
_dimension_update(target_x, x_value),
|
| 1173 |
+
_dimension_update(target_y, y_value),
|
| 1174 |
+
_dimension_update(target_z, z_value),
|
| 1175 |
+
)
|
| 1176 |
+
|
| 1177 |
+
|
| 1178 |
def _build_annotated_scene(mesh: trimesh.Trimesh, opacity: float = 1.0) -> str:
|
| 1179 |
"""Export a GLB containing the mesh, origin axes, and a Z=0 grid plane."""
|
| 1180 |
scene = trimesh.Scene()
|
|
|
|
| 1300 |
target_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1301 |
target_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1302 |
target_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1303 |
) -> tuple[str | None, str]:
|
| 1304 |
if not stl_file:
|
| 1305 |
return _viewer_update(None), "No model loaded."
|
|
|
|
| 1310 |
target_x=target_x,
|
| 1311 |
target_y=target_y,
|
| 1312 |
target_z=target_z,
|
|
|
|
| 1313 |
)
|
| 1314 |
glb_path = _build_annotated_scene(mesh, opacity=_resolve_model_opacity(opacity))
|
| 1315 |
return _viewer_update(glb_path), _format_model_details(Path(stl_file).name, mesh, scale_factors)
|
|
|
|
| 1322 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1323 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1324 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1325 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1326 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1327 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1328 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1329 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1330 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1331 |
) -> tuple:
|
| 1332 |
outputs: list[Any] = []
|
| 1333 |
resolved_opacity = _resolve_model_opacity(opacity)
|
|
|
|
| 1342 |
target3_y,
|
| 1343 |
target3_z,
|
| 1344 |
)
|
|
|
|
| 1345 |
|
| 1346 |
for index, filename in enumerate(SAMPLE_STL_FILENAMES):
|
| 1347 |
stl_path = SAMPLE_STL_DIR / filename
|
|
|
|
| 1361 |
target_x=target_values[index][0],
|
| 1362 |
target_y=target_values[index][1],
|
| 1363 |
target_z=target_values[index][2],
|
|
|
|
| 1364 |
)
|
| 1365 |
except Exception as exc:
|
| 1366 |
outputs.extend([
|
|
|
|
| 1389 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1390 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1391 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1392 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1393 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1394 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1395 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1396 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1397 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1398 |
) -> tuple:
|
| 1399 |
outputs: list[Any] = []
|
| 1400 |
resolved_opacity = _resolve_model_opacity(opacity)
|
|
|
|
| 1409 |
target3_y,
|
| 1410 |
target3_z,
|
| 1411 |
)
|
|
|
|
| 1412 |
|
| 1413 |
+
for stl_file, values in zip((stl1, stl2, stl3), target_values):
|
| 1414 |
if not stl_file:
|
| 1415 |
outputs.extend([_viewer_update(None), "No model loaded."])
|
| 1416 |
continue
|
|
|
|
| 1423 |
values[0],
|
| 1424 |
values[1],
|
| 1425 |
values[2],
|
|
|
|
| 1426 |
)
|
| 1427 |
)
|
| 1428 |
return tuple(outputs)
|
|
|
|
| 1439 |
target1_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1440 |
target1_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1441 |
target1_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1442 |
target2_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1443 |
target2_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1444 |
target2_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1445 |
target3_x: float | None = DEFAULT_TARGET_EXTENTS[0],
|
| 1446 |
target3_y: float | None = DEFAULT_TARGET_EXTENTS[1],
|
| 1447 |
target3_z: float | None = DEFAULT_TARGET_EXTENTS[2],
|
|
|
|
| 1448 |
progress: gr.Progress = gr.Progress(),
|
| 1449 |
):
|
| 1450 |
files = [stl1, stl2, stl3]
|
|
|
|
| 1459 |
target3_y,
|
| 1460 |
target3_z,
|
| 1461 |
)
|
|
|
|
| 1462 |
valid_count = max(1, sum(1 for f in files if f))
|
| 1463 |
results: list = []
|
| 1464 |
completed = 0
|
| 1465 |
|
| 1466 |
+
for stl_file, values in zip(files, target_values):
|
| 1467 |
if not stl_file:
|
| 1468 |
results.extend([
|
| 1469 |
_empty_state(),
|
|
|
|
| 1492 |
values[0],
|
| 1493 |
values[1],
|
| 1494 |
values[2],
|
|
|
|
| 1495 |
)
|
| 1496 |
|
| 1497 |
stack = slice_stl_to_tiffs(
|
|
|
|
| 2010 |
gr.Markdown(
|
| 2011 |
"""
|
| 2012 |
# STL to TIFF Slicer
|
| 2013 |
+
Upload up to three STL files, optionally scale each shape, choose layer height and XY pixel size, then generate TIFF stacks for all uploaded models.
|
| 2014 |
"""
|
| 2015 |
)
|
| 2016 |
|
|
|
|
| 2030 |
)
|
| 2031 |
with gr.Column(scale=0, min_width=260):
|
| 2032 |
scale_to_target = gr.Checkbox(
|
| 2033 |
+
label="Scale STLs",
|
| 2034 |
value=False,
|
| 2035 |
)
|
| 2036 |
+
|
| 2037 |
+
with gr.Group(visible=False) as scaling_details_group:
|
| 2038 |
+
with gr.Row():
|
| 2039 |
+
with gr.Column(scale=0, min_width=260):
|
| 2040 |
+
scale_mode = gr.Radio(
|
| 2041 |
+
choices=[SCALE_MODE_TARGET_DIMENSIONS, SCALE_MODE_UNIFORM_FACTOR],
|
| 2042 |
+
value=SCALE_MODE_TARGET_DIMENSIONS,
|
| 2043 |
+
label="Scaling Mode",
|
| 2044 |
+
)
|
| 2045 |
|
| 2046 |
# --- Upload + 3D viewer row ---
|
| 2047 |
stl_files: list[gr.File] = []
|
| 2048 |
model_viewers: list[gr.Model3D] = []
|
| 2049 |
model_details_list: list[gr.Markdown] = []
|
| 2050 |
+
target_groups: list[gr.Group] = []
|
| 2051 |
target_xs: list[gr.Number] = []
|
| 2052 |
target_ys: list[gr.Number] = []
|
| 2053 |
target_zs: list[gr.Number] = []
|
|
|
|
| 2054 |
|
| 2055 |
with gr.Row():
|
| 2056 |
for i in range(3):
|
|
|
|
| 2068 |
height=270,
|
| 2069 |
)
|
| 2070 |
model_details = gr.Markdown(f"No model {i + 1} loaded.")
|
| 2071 |
+
with gr.Group(visible=False) as target_group:
|
| 2072 |
+
with gr.Row():
|
| 2073 |
+
target_x = gr.Number(
|
| 2074 |
+
label="Target X (mm)",
|
| 2075 |
+
value=DEFAULT_TARGET_EXTENTS[0],
|
| 2076 |
+
minimum=0.0001,
|
| 2077 |
+
step=0.1,
|
| 2078 |
+
)
|
| 2079 |
+
target_y = gr.Number(
|
| 2080 |
+
label="Target Y (mm)",
|
| 2081 |
+
value=DEFAULT_TARGET_EXTENTS[1],
|
| 2082 |
+
minimum=0.0001,
|
| 2083 |
+
step=0.1,
|
| 2084 |
+
)
|
| 2085 |
+
target_z = gr.Number(
|
| 2086 |
+
label="Target Z (mm)",
|
| 2087 |
+
value=DEFAULT_TARGET_EXTENTS[2],
|
| 2088 |
+
minimum=0.0001,
|
| 2089 |
+
step=0.1,
|
| 2090 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2091 |
stl_files.append(stl_file)
|
| 2092 |
model_viewers.append(model_viewer)
|
| 2093 |
model_details_list.append(model_details)
|
| 2094 |
+
target_groups.append(target_group)
|
| 2095 |
target_xs.append(target_x)
|
| 2096 |
target_ys.append(target_y)
|
| 2097 |
target_zs.append(target_z)
|
|
|
|
| 2098 |
|
| 2099 |
# --- Shared slicing controls ---
|
| 2100 |
with gr.Row():
|
|
|
|
| 2218 |
target_xs[0],
|
| 2219 |
target_ys[0],
|
| 2220 |
target_zs[0],
|
|
|
|
| 2221 |
target_xs[1],
|
| 2222 |
target_ys[1],
|
| 2223 |
target_zs[1],
|
|
|
|
| 2224 |
target_xs[2],
|
| 2225 |
target_ys[2],
|
| 2226 |
target_zs[2],
|
|
|
|
| 2227 |
]
|
| 2228 |
|
| 2229 |
for i in range(3):
|
|
|
|
| 2237 |
target_xs[i],
|
| 2238 |
target_ys[i],
|
| 2239 |
target_zs[i],
|
|
|
|
| 2240 |
],
|
| 2241 |
outputs=[model_viewers[i], model_details_list[i]],
|
| 2242 |
)
|
|
|
|
| 2260 |
model_details_list[i],
|
| 2261 |
])
|
| 2262 |
|
| 2263 |
+
sample_load_event = load_samples_button.click(
|
| 2264 |
fn=preload_sample_models,
|
| 2265 |
inputs=[model_opacity, scale_to_target, scale_mode, *all_scale_inputs],
|
| 2266 |
outputs=preload_outputs,
|
|
|
|
| 2285 |
inputs=refresh_inputs,
|
| 2286 |
outputs=refresh_outputs,
|
| 2287 |
)
|
| 2288 |
+
|
| 2289 |
+
visibility_outputs = [scaling_details_group, *target_groups]
|
| 2290 |
+
scale_to_target.change(
|
| 2291 |
+
fn=update_scaling_controls_visibility,
|
| 2292 |
+
inputs=[scale_to_target, scale_mode],
|
| 2293 |
+
outputs=visibility_outputs,
|
| 2294 |
+
queue=False,
|
| 2295 |
+
)
|
| 2296 |
+
scale_mode.change(
|
| 2297 |
+
fn=update_scaling_controls_visibility,
|
| 2298 |
+
inputs=[scale_to_target, scale_mode],
|
| 2299 |
+
outputs=visibility_outputs,
|
| 2300 |
+
queue=False,
|
| 2301 |
+
)
|
| 2302 |
+
for i, stl_file in enumerate(stl_files):
|
| 2303 |
+
target_xs[i].change(
|
| 2304 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2305 |
+
stl, enabled, mode, "X", x, y, z
|
| 2306 |
+
),
|
| 2307 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2308 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2309 |
+
queue=False,
|
| 2310 |
+
).then(
|
| 2311 |
+
fn=refresh_all_model_viewers,
|
| 2312 |
+
inputs=refresh_inputs,
|
| 2313 |
+
outputs=refresh_outputs,
|
| 2314 |
+
)
|
| 2315 |
+
target_ys[i].change(
|
| 2316 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2317 |
+
stl, enabled, mode, "Y", x, y, z
|
| 2318 |
+
),
|
| 2319 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2320 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2321 |
+
queue=False,
|
| 2322 |
+
).then(
|
| 2323 |
+
fn=refresh_all_model_viewers,
|
| 2324 |
+
inputs=refresh_inputs,
|
| 2325 |
+
outputs=refresh_outputs,
|
| 2326 |
+
)
|
| 2327 |
+
target_zs[i].change(
|
| 2328 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2329 |
+
stl, enabled, mode, "Z", x, y, z
|
| 2330 |
+
),
|
| 2331 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2332 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2333 |
+
queue=False,
|
| 2334 |
+
).then(
|
| 2335 |
+
fn=refresh_all_model_viewers,
|
| 2336 |
+
inputs=refresh_inputs,
|
| 2337 |
+
outputs=refresh_outputs,
|
| 2338 |
+
)
|
| 2339 |
+
scale_to_target.change(
|
| 2340 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2341 |
+
stl, enabled, mode, "X", x, y, z
|
| 2342 |
+
),
|
| 2343 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2344 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2345 |
+
queue=False,
|
| 2346 |
+
)
|
| 2347 |
+
scale_mode.change(
|
| 2348 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2349 |
+
stl, enabled, mode, "X", x, y, z
|
| 2350 |
+
),
|
| 2351 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2352 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2353 |
+
queue=False,
|
| 2354 |
+
)
|
| 2355 |
+
stl_file.change(
|
| 2356 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2357 |
+
stl, enabled, mode, "X", x, y, z
|
| 2358 |
+
),
|
| 2359 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2360 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2361 |
+
queue=False,
|
| 2362 |
+
)
|
| 2363 |
+
sample_load_event.then(
|
| 2364 |
+
fn=lambda stl, enabled, mode, x, y, z: sync_uniform_target_dimensions(
|
| 2365 |
+
stl, enabled, mode, "X", x, y, z
|
| 2366 |
+
),
|
| 2367 |
+
inputs=[stl_file, scale_to_target, scale_mode, target_xs[i], target_ys[i], target_zs[i]],
|
| 2368 |
+
outputs=[target_xs[i], target_ys[i], target_zs[i]],
|
| 2369 |
+
queue=False,
|
| 2370 |
+
)
|
| 2371 |
+
for scale_control in (scale_to_target, scale_mode):
|
| 2372 |
scale_control.change(
|
| 2373 |
fn=refresh_all_model_viewers,
|
| 2374 |
inputs=refresh_inputs,
|
tests/test_app_scaling.py
CHANGED
|
@@ -7,10 +7,11 @@ from app import (
|
|
| 7 |
SCALE_MODE_TARGET_DIMENSIONS,
|
| 8 |
SCALE_MODE_UNIFORM_FACTOR,
|
| 9 |
_resolve_mesh_scale_factors,
|
|
|
|
| 10 |
)
|
| 11 |
|
| 12 |
|
| 13 |
-
def
|
| 14 |
mesh = trimesh.creation.box(extents=(2.0, 4.0, 8.0))
|
| 15 |
|
| 16 |
scale_factors = _resolve_mesh_scale_factors(
|
|
@@ -20,10 +21,9 @@ def test_resolve_mesh_scale_factors_uses_uniform_factor_for_all_axes() -> None:
|
|
| 20 |
target_x=10.0,
|
| 21 |
target_y=20.0,
|
| 22 |
target_z=30.0,
|
| 23 |
-
uniform_scale=1.5,
|
| 24 |
)
|
| 25 |
|
| 26 |
-
assert scale_factors == (
|
| 27 |
|
| 28 |
|
| 29 |
def test_resolve_mesh_scale_factors_fits_each_axis_in_target_mode() -> None:
|
|
@@ -36,7 +36,20 @@ def test_resolve_mesh_scale_factors_fits_each_axis_in_target_mode() -> None:
|
|
| 36 |
target_x=10.0,
|
| 37 |
target_y=20.0,
|
| 38 |
target_z=4.0,
|
| 39 |
-
uniform_scale=1.5,
|
| 40 |
)
|
| 41 |
|
| 42 |
np.testing.assert_allclose(scale_factors, (5.0, 5.0, 0.5))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
SCALE_MODE_TARGET_DIMENSIONS,
|
| 8 |
SCALE_MODE_UNIFORM_FACTOR,
|
| 9 |
_resolve_mesh_scale_factors,
|
| 10 |
+
_uniform_target_extents_from_anchor,
|
| 11 |
)
|
| 12 |
|
| 13 |
|
| 14 |
+
def test_resolve_mesh_scale_factors_uses_x_target_for_uniform_scaling() -> None:
|
| 15 |
mesh = trimesh.creation.box(extents=(2.0, 4.0, 8.0))
|
| 16 |
|
| 17 |
scale_factors = _resolve_mesh_scale_factors(
|
|
|
|
| 21 |
target_x=10.0,
|
| 22 |
target_y=20.0,
|
| 23 |
target_z=30.0,
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
+
assert scale_factors == (5.0, 5.0, 5.0)
|
| 27 |
|
| 28 |
|
| 29 |
def test_resolve_mesh_scale_factors_fits_each_axis_in_target_mode() -> None:
|
|
|
|
| 36 |
target_x=10.0,
|
| 37 |
target_y=20.0,
|
| 38 |
target_z=4.0,
|
|
|
|
| 39 |
)
|
| 40 |
|
| 41 |
np.testing.assert_allclose(scale_factors, (5.0, 5.0, 0.5))
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_uniform_target_extents_update_from_changed_side() -> None:
|
| 45 |
+
mesh = trimesh.creation.box(extents=(2.0, 4.0, 8.0))
|
| 46 |
+
|
| 47 |
+
target_extents = _uniform_target_extents_from_anchor(
|
| 48 |
+
mesh,
|
| 49 |
+
anchor_axis="Y",
|
| 50 |
+
target_x=10.0,
|
| 51 |
+
target_y=12.0,
|
| 52 |
+
target_z=30.0,
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
np.testing.assert_allclose(target_extents, (6.0, 12.0, 24.0))
|