autoforge 2.0.0__tar.gz → 2.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. {autoforge-2.0.0 → autoforge-2.0.1}/PKG-INFO +6 -1
  2. {autoforge-2.0.0 → autoforge-2.0.1}/README.md +5 -0
  3. {autoforge-2.0.0 → autoforge-2.0.1}/pyproject.toml +4 -1
  4. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/PKG-INFO +6 -1
  5. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/SOURCES.txt +4 -0
  6. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/ImageHelper.py +29 -0
  7. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OtherHelper.py +4 -2
  8. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OutputHelper.py +128 -145
  9. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/PruningHelper.py +45 -28
  10. autoforge-2.0.1/src/autoforge/Helper/filamentcolors_library.py +345 -0
  11. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/auto_forge.py +38 -16
  12. autoforge-2.0.1/src/autoforge/data/filamentcolors_catalog.json +10216 -0
  13. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/filaments.py +49 -0
  14. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/init.py +13 -3
  15. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/optimization.py +8 -0
  16. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/preview.py +8 -2
  17. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/pruning.py +60 -4
  18. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/config.py +6 -0
  19. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/pipeline_runner.py +8 -34
  20. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/slider_render.py +10 -1
  21. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/server.py +7 -0
  22. autoforge-2.0.1/src/autoforge/webui/services/catalog_service.py +150 -0
  23. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/filament_service.py +60 -0
  24. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/optimization_service.py +15 -0
  25. autoforge-2.0.1/tests/test_bugfixes_2026_09_26.py +413 -0
  26. autoforge-2.0.1/tests/test_filamentcolors_library.py +345 -0
  27. autoforge-2.0.0/src/autoforge/Helper/filamentcolors_library.py +0 -164
  28. {autoforge-2.0.0 → autoforge-2.0.1}/setup.cfg +0 -0
  29. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/dependency_links.txt +0 -0
  30. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/entry_points.txt +0 -0
  31. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/requires.txt +0 -0
  32. {autoforge-2.0.0 → autoforge-2.0.1}/src/AutoForge.egg-info/top_level.txt +0 -0
  33. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/AmpUtils.py +0 -0
  34. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/CAdamW.py +0 -0
  35. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/DeviceUtils.py +0 -0
  36. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/FilamentHelper.py +0 -0
  37. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/ChristofidesHeightMap.py +0 -0
  38. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/DepthEstimateHeightMap.py +0 -0
  39. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/FastTSPHeightMap.py +0 -0
  40. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/Heightmaps/__init__.py +0 -0
  41. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/OptimizerHelper.py +0 -0
  42. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Helper/__init__.py +0 -0
  43. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/__init__.py +0 -0
  44. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/auto_forge_init_check.py +0 -0
  45. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Init_comparer/plot_results.py +0 -0
  46. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/LossFunctions.py +0 -0
  47. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/PerceptionLoss.py +0 -0
  48. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Loss/__init__.py +0 -0
  49. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Misc/__init__.py +0 -0
  50. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Misc/color_fit_all.py +0 -0
  51. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Modules/Optimizer.py +0 -0
  52. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/Modules/__init__.py +0 -0
  53. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/__init__.py +0 -0
  54. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/__main__.py +0 -0
  55. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/priority_mask_editor.py +0 -0
  56. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/__init__.py +0 -0
  57. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/__init__.py +0 -0
  58. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/images.py +0 -0
  59. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/outputs.py +0 -0
  60. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/project.py +0 -0
  61. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/settings.py +0 -0
  62. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/sliders.py +0 -0
  63. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/state.py +0 -0
  64. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/system.py +0 -0
  65. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/api/ws.py +0 -0
  66. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/__init__.py +0 -0
  67. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/colored_mesh.py +0 -0
  68. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/gpu_memory.py +0 -0
  69. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/mesh_persist.py +0 -0
  70. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/sliders.py +0 -0
  71. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/helpers/telemetry.py +0 -0
  72. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/models.py +0 -0
  73. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/__init__.py +0 -0
  74. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/image_service.py +0 -0
  75. {autoforge-2.0.0 → autoforge-2.0.1}/src/autoforge/webui/services/project_service.py +0 -0
  76. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_amp_utils.py +0 -0
  77. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_auto_forge_cli.py +0 -0
  78. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_bug_regressions.py +0 -0
  79. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_cadamw.py +0 -0
  80. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_christofides_heightmap.py +0 -0
  81. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_composite_image.py +0 -0
  82. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_depth_adjusted_height_map.py +0 -0
  83. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_depth_estimate_height_map.py +0 -0
  84. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_device_utils.py +0 -0
  85. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_end_to_end_optimizer.py +0 -0
  86. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_fasttsp_heightmap.py +0 -0
  87. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_filament_helper.py +0 -0
  88. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_flatforge.py +0 -0
  89. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_image_helper.py +0 -0
  90. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_loss_functions.py +0 -0
  91. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_optimizer_module.py +0 -0
  92. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_optimizer_training.py +0 -0
  93. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_other_helper.py +0 -0
  94. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_output_helper.py +0 -0
  95. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_perception_loss.py +0 -0
  96. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_pipeline_smoke.py +0 -0
  97. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_property_based.py +0 -0
  98. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_prune_repeatability.py +0 -0
  99. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_pruning_helper.py +0 -0
  100. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_remove_height_spikes.py +0 -0
  101. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_webui_backend.py +0 -0
  102. {autoforge-2.0.0 → autoforge-2.0.1}/tests/test_webui_regressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoForge
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: AutoForge - 3D color images made easy
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -98,6 +98,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
98
98
 
99
99
  If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
100
100
 
101
+ To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
102
+
101
103
  The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
102
104
 
103
105
  4. **Update to the latest release** whenever you want, from the project folder:
@@ -248,6 +250,8 @@ conda activate forge
248
250
  pip install -e .
249
251
  ```
250
252
 
253
+ To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
254
+
251
255
  If the installed pytorch version has no cuda support execute the following:
252
256
 
253
257
  ```bash
@@ -276,6 +280,7 @@ I would love to see what you have done with the software, so it would be great i
276
280
  First and foremost:
277
281
  - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
278
282
  Without it, this project would not have been possible.
283
+ - [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
279
284
 
280
285
  AutoForge makes use of several open source libraries:
281
286
 
@@ -61,6 +61,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
61
61
 
62
62
  If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
63
63
 
64
+ To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
65
+
64
66
  The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
65
67
 
66
68
  4. **Update to the latest release** whenever you want, from the project folder:
@@ -211,6 +213,8 @@ conda activate forge
211
213
  pip install -e .
212
214
  ```
213
215
 
216
+ To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
217
+
214
218
  If the installed pytorch version has no cuda support execute the following:
215
219
 
216
220
  ```bash
@@ -239,6 +243,7 @@ I would love to see what you have done with the software, so it would be great i
239
243
  First and foremost:
240
244
  - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
241
245
  Without it, this project would not have been possible.
246
+ - [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
242
247
 
243
248
  AutoForge makes use of several open source libraries:
244
249
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "AutoForge"
7
- version="2.0.0"
7
+ version="2.0.1"
8
8
  description = "AutoForge - 3D color images made easy"
9
9
  authors = [{ name = "Hendric Voss", email = "hvoss@techfak.uni-bielefeld.de" }]
10
10
  readme = "README.md"
@@ -50,6 +50,9 @@ test = [
50
50
  [tool.setuptools]
51
51
  package-dir = {"" = "src"}
52
52
 
53
+ [tool.setuptools.package-data]
54
+ autoforge = ["data/*.json"]
55
+
53
56
  [project.scripts]
54
57
  autoforge = "autoforge.auto_forge:main"
55
58
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AutoForge
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: AutoForge - 3D color images made easy
5
5
  Author-email: Hendric Voss <hvoss@techfak.uni-bielefeld.de>
6
6
  License: CC BY-NC-SA 4.0
@@ -98,6 +98,8 @@ The easiest way to use AutoForge is the web UI, a local app with drag-and-drop i
98
98
 
99
99
  If HueForge is installed on the same computer, the web UI finds your HueForge personal filament library and offers to import it once, on its first start.
100
100
 
101
+ To find a filament you own without measuring it yourself, click **Catalog** in the filament library. It searches every filament on [filamentcolors.xyz](https://filamentcolors.xyz) that has a measured TD (search by brand, color name, type or hex code; filter by type, brand or color family; or pick a color to see the closest matches first), and **Add** puts it straight into your library.
102
+
101
103
  The web UI sends anonymous usage telemetry to the project via [PostHog](https://posthog.com/) by default, to notify me of problems and any bugs. This includes crash reports (unhandled errors from both the browser frontend and the backend server, with the error type, message, and stack trace) so bugs can get fixed faster. No image data, filament data, or personal information is sent. To disable it, pass `--no-telemetry` (e.g. `./run_webui.sh --no-telemetry` / `run_webui.bat --no-telemetry`), or set `AUTOFORGE_WEBUI_TELEMETRY_ENABLED=false` permanently in your environment.
102
104
 
103
105
  4. **Update to the latest release** whenever you want, from the project folder:
@@ -248,6 +250,8 @@ conda activate forge
248
250
  pip install -e .
249
251
  ```
250
252
 
253
+ To refresh the bundled filamentcolors.xyz catalog (`src/autoforge/data/filamentcolors_catalog.json`) before a release, run `./generate_filamentcolors_library.sh`. It downloads the whole catalog once, pausing 2 seconds between pages (`--delay` sets the pause; requests are never less than 1 second apart), and keeps only the filaments that have a TD.
254
+
251
255
  If the installed pytorch version has no cuda support execute the following:
252
256
 
253
257
  ```bash
@@ -276,6 +280,7 @@ I would love to see what you have done with the software, so it would be great i
276
280
  First and foremost:
277
281
  - [Hueforge](https://shop.thehueforge.com/) for providing the inspiration for this project.
278
282
  Without it, this project would not have been possible.
283
+ - [filamentcolors.xyz](https://filamentcolors.xyz) and its community for the measured filament swatches and TD values in the web UI's filament catalog.
279
284
 
280
285
  AutoForge makes use of several open source libraries:
281
286
 
@@ -35,6 +35,7 @@ src/autoforge/Misc/__init__.py
35
35
  src/autoforge/Misc/color_fit_all.py
36
36
  src/autoforge/Modules/Optimizer.py
37
37
  src/autoforge/Modules/__init__.py
38
+ src/autoforge/data/filamentcolors_catalog.json
38
39
  src/autoforge/webui/__init__.py
39
40
  src/autoforge/webui/config.py
40
41
  src/autoforge/webui/models.py
@@ -62,6 +63,7 @@ src/autoforge/webui/helpers/slider_render.py
62
63
  src/autoforge/webui/helpers/sliders.py
63
64
  src/autoforge/webui/helpers/telemetry.py
64
65
  src/autoforge/webui/services/__init__.py
66
+ src/autoforge/webui/services/catalog_service.py
65
67
  src/autoforge/webui/services/filament_service.py
66
68
  src/autoforge/webui/services/image_service.py
67
69
  src/autoforge/webui/services/optimization_service.py
@@ -69,6 +71,7 @@ src/autoforge/webui/services/project_service.py
69
71
  tests/test_amp_utils.py
70
72
  tests/test_auto_forge_cli.py
71
73
  tests/test_bug_regressions.py
74
+ tests/test_bugfixes_2026_09_26.py
72
75
  tests/test_cadamw.py
73
76
  tests/test_christofides_heightmap.py
74
77
  tests/test_composite_image.py
@@ -78,6 +81,7 @@ tests/test_device_utils.py
78
81
  tests/test_end_to_end_optimizer.py
79
82
  tests/test_fasttsp_heightmap.py
80
83
  tests/test_filament_helper.py
84
+ tests/test_filamentcolors_library.py
81
85
  tests/test_flatforge.py
82
86
  tests/test_image_helper.py
83
87
  tests/test_loss_functions.py
@@ -18,6 +18,35 @@ def imwrite(filename: str, img: MatLike, params: Sequence[int] = ()) -> None:
18
18
  encoded_img.tofile(filename)
19
19
 
20
20
 
21
+ def to_bgr_or_bgra_uint8(img) -> np.ndarray:
22
+ """Bring any image OpenCV decodes into the 8-bit BGR/BGRA layout the
23
+ pipeline is written for (used by both the CLI and the webui).
24
+
25
+ ``cv2.imread(..., IMREAD_UNCHANGED)`` returns whatever the file holds, but
26
+ the pipeline indexed ``img.shape[2]`` and treated values as 0-255: a
27
+ grayscale PNG/JPEG (2-D array) failed with "tuple index out of range",
28
+ grayscale+alpha had two channels and failed in ``cvtColor``, and a
29
+ 16-bit PNG ran on values up to 65535 — a nonsense target the optimizer
30
+ could never match."""
31
+ if img is None:
32
+ raise ValueError("The input image could not be read.")
33
+ if img.dtype == np.uint16:
34
+ img = (img.astype(np.float32) / 257.0).round().astype(np.uint8)
35
+ elif img.dtype in (np.float32, np.float64):
36
+ img = (np.clip(img, 0.0, 1.0) * 255.0).round().astype(np.uint8)
37
+ elif img.dtype != np.uint8:
38
+ img = np.clip(img, 0, 255).astype(np.uint8)
39
+ if img.ndim == 2:
40
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
41
+ channels = img.shape[2]
42
+ if channels == 1:
43
+ return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
44
+ if channels == 2:
45
+ gray, alpha = img[:, :, 0], img[:, :, 1:2]
46
+ return np.concatenate([cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR), alpha], axis=2)
47
+ return img
48
+
49
+
21
50
  def resize_image(img, max_size):
22
51
  h_img, w_img, _ = img.shape
23
52
 
@@ -23,8 +23,10 @@ def set_seed(args) -> Any:
23
23
 
24
24
 
25
25
  def perform_basic_check(args):
26
- # Basic checks
27
- if not (args.background_height / args.layer_height).is_integer():
26
+ # Basic checks. With a tolerance: in floating point 0.28 / 0.04 is
27
+ # 7.000000000000001, so an exact is_integer() rejected valid settings.
28
+ ratio = args.background_height / args.layer_height
29
+ if abs(ratio - round(ratio)) > 1e-6:
28
30
  print(
29
31
  "Error: Background height must be a multiple of layer height.",
30
32
  file=sys.stderr,
@@ -48,6 +48,56 @@ def extract_filament_swaps(disc_global, disc_height_image, background_layers):
48
48
  return filament_indices, slider_values
49
49
 
50
50
 
51
+ def _is_blank(value) -> bool:
52
+ """Missing, None, or the NaN pandas reads an empty CSV cell as."""
53
+ if value is None:
54
+ return True
55
+ if isinstance(value, float) and np.isnan(value):
56
+ return True
57
+ return isinstance(value, str) and value.strip() == ""
58
+
59
+
60
+ def _hfp_filament(mat, brand="", color="#000000", name=""):
61
+ """One ``filament_set`` entry of a .hfp file from a material record.
62
+
63
+ The records come from pandas, which reads an empty CSV cell as NaN -
64
+ and ``json.dump`` writes that as a bare ``NaN``, which is not JSON, so
65
+ HueForge (or any strict parser) could not open the project. The webui
66
+ hits this for every filament without a brand. Blank fields get the
67
+ given defaults (a fresh uuid for ``uuid``) instead."""
68
+
69
+ def text(key, default):
70
+ value = mat.get(key)
71
+ return default if _is_blank(value) else str(value)
72
+
73
+ td_raw = mat.get("Transmissivity")
74
+ try:
75
+ td = float(td_raw)
76
+ except (TypeError, ValueError):
77
+ td = float("nan")
78
+ if not np.isfinite(td):
79
+ td = 0.1
80
+ return {
81
+ "Brand": text("Brand", brand),
82
+ "Color": text("Color", color),
83
+ "Name": text("Name", name),
84
+ "Owned": str(mat.get("Owned", False)).strip().lower() == "true",
85
+ "Transmissivity": int(td) if td.is_integer() else td,
86
+ "Type": text("Type", "PLA"),
87
+ "uuid": text("Uuid", str(uuid.uuid4())),
88
+ }
89
+
90
+
91
+ def model_size_mm(width_px, height_px, maximum_x_y_size):
92
+ """Physical (width, height) in mm of a model built by ``generate_stl``
93
+ from a ``width_px`` x ``height_px`` height map: its longer side spans
94
+ ``maximum_x_y_size`` mm across the pixel centres (``max(W, H) - 1``
95
+ steps)."""
96
+ steps = max(int(width_px) - 1, int(height_px) - 1, 1)
97
+ scale = float(maximum_x_y_size) / steps
98
+ return (max(int(width_px) - 1, 0) * scale, max(int(height_px) - 1, 0) * scale)
99
+
100
+
51
101
  def generate_project_file(
52
102
  project_filename,
53
103
  args,
@@ -78,7 +128,7 @@ def generate_project_file(
78
128
  csv_filename (str): Path to the CSV file containing material data.
79
129
  """
80
130
  # Compute the number of background layers (as in your main())
81
- background_layers = int(args.background_height / args.layer_height)
131
+ background_layers = int(round(args.background_height / args.layer_height))
82
132
 
83
133
  # Load full material data from CSV
84
134
  material_data = load_materials_data(args)
@@ -95,21 +145,13 @@ def generate_project_file(
95
145
  # Use actual background filament data if auto selected and index stored
96
146
  bg_idx = getattr(args, "background_material_index", None)
97
147
  if bg_idx is not None and 0 <= bg_idx < len(material_data):
98
- bg_mat = material_data[bg_idx]
99
148
  filament_set.append(
100
- {
101
- "Brand": bg_mat.get("Brand", "Autoforge"),
102
- "Color": bg_mat.get("Color", args.background_color),
103
- "Name": bg_mat.get("Name", "Background"),
104
- "Owned": str(bg_mat.get("Owned", False)).strip().lower() == "true",
105
- "Transmissivity": (
106
- int(bg_mat["Transmissivity"])
107
- if float(bg_mat["Transmissivity"]).is_integer()
108
- else float(bg_mat["Transmissivity"])
109
- ),
110
- "Type": bg_mat.get("Type", "PLA"),
111
- "uuid": bg_mat.get("Uuid", str(uuid.uuid4())),
112
- }
149
+ _hfp_filament(
150
+ material_data[bg_idx],
151
+ brand="Autoforge",
152
+ color=args.background_color,
153
+ name="Background",
154
+ )
113
155
  )
114
156
  else:
115
157
  filament_set.append(
@@ -125,22 +167,7 @@ def generate_project_file(
125
167
  )
126
168
 
127
169
  for idx in filament_indices:
128
- mat = material_data[idx]
129
- filament_set.append(
130
- {
131
- "Brand": mat["Brand"],
132
- "Color": mat["Color"],
133
- "Name": mat["Name"],
134
- "Owned": str(mat.get("Owned", False)).strip().lower() == "true",
135
- "Transmissivity": (
136
- int(mat["Transmissivity"])
137
- if float(mat["Transmissivity"]).is_integer()
138
- else float(mat["Transmissivity"])
139
- ),
140
- "Type": mat.get("Type", "PLA"),
141
- "uuid": mat.get("Uuid", str(uuid.uuid4())),
142
- }
143
- )
170
+ filament_set.append(_hfp_filament(material_data[idx]))
144
171
 
145
172
  filament_set = filament_set[::-1]
146
173
 
@@ -204,7 +231,9 @@ def generate_project_file(
204
231
 
205
232
  # Write out the project file as JSON
206
233
  with open(project_filename, "w") as f:
207
- json.dump(project_data, f, indent=4)
234
+ # allow_nan=False: a NaN that slipped through must fail here, loudly,
235
+ # rather than produce a file HueForge cannot parse.
236
+ json.dump(project_data, f, indent=4, allow_nan=False)
208
237
 
209
238
 
210
239
  def generate_stl(
@@ -526,93 +555,69 @@ def generate_flatforge_stls(
526
555
  )
527
556
  print(f"Selected clear material: {material_names[most_transparent_idx]} (TD: {material_TDs_np[most_transparent_idx]:.2f})")
528
557
 
529
- # Create a 3D array: [layer, height, width] where each entry indicates which material is at that position
530
- # Initialize with -1 (no material)
531
- layer_materials = np.full((max_layer, H, W), -1, dtype=int)
532
-
533
- # For each pixel, assign materials to layers based on disc_height_image and disc_global
534
- for i in range(H):
535
- for j in range(W):
536
- if not valid_mask[i, j]:
537
- continue
538
-
539
- pixel_height = int(disc_height_image[i, j])
540
-
541
- # Assign materials from layer 0 to min(pixel_height, max_layer)-1
542
- # Fill any gaps in disc_global by extending the last color from below
543
- # This ensures there's no clear between two colored layers
544
- last_color = -1
545
- for layer in range(min(pixel_height, max_layer)):
546
- material = int(disc_global[layer])
547
- if material >= 0:
548
- # This layer has a color assigned
549
- last_color = material
550
- layer_materials[layer, i, j] = material
551
- elif last_color >= 0:
552
- # This layer is a gap in disc_global, extend the color from below
553
- # This prevents clear from being placed between two colored layers
554
- layer_materials[layer, i, j] = last_color
555
- # else: both material and last_color are -1, leave as -1
556
-
558
+ heights = np.asarray(disc_height_image).astype(np.int64)
559
+
560
+ # The material printed at each layer. A gap in disc_global (-1) takes the
561
+ # color from below, so there is never clear between two colored layers.
562
+ layer_color = np.full(max_layer, -1, dtype=np.int64)
563
+ last_color = -1
564
+ for layer in range(max_layer):
565
+ material = int(disc_global[layer])
566
+ if material >= 0:
567
+ last_color = material
568
+ layer_color[layer] = last_color
569
+
570
+ # Contiguous runs of one material: (material, first_layer, end_layer).
571
+ bands = []
572
+ for layer in range(max_layer):
573
+ material = int(layer_color[layer])
574
+ if material < 0:
575
+ continue
576
+ if bands and bands[-1][0] == material and bands[-1][2] == layer:
577
+ bands[-1] = (material, bands[-1][1], layer + 1)
578
+ else:
579
+ bands.append((material, layer, layer + 1))
580
+
557
581
  # Get unique materials used (excluding background)
558
- unique_materials = np.unique(disc_global[:max_layer])
559
- unique_materials = [int(m) for m in unique_materials if m >= 0]
560
-
582
+ unique_materials = sorted({material for material, _, _ in bands})
583
+
561
584
  # Calculate the total height of the print (including cap layers)
562
585
  total_height = background_height + (max_layer + cap_layers) * layer_height
563
-
586
+
564
587
  stl_files = []
565
-
566
- # Helper function to create a flat box STL for a given material at specific layers
588
+
567
589
  def create_color_stl(material_idx, material_name, color_hex):
568
- """Create an STL for a specific material/color."""
569
- # Find which layers and pixels use this material
570
- material_mask_3d = (layer_materials == material_idx)
571
-
572
- # Create a height map for this material
573
- # For each pixel, find the maximum layer that uses this material
574
- height_map = np.zeros((H, W), dtype=float)
575
- min_height_map = np.full((H, W), max_layer + cap_layers, dtype=float)
576
-
577
- has_material = False
578
- for layer in range(max_layer):
579
- for i in range(H):
580
- for j in range(W):
581
- if material_mask_3d[layer, i, j]:
582
- has_material = True
583
- # Track the highest layer this material appears at this pixel
584
- if layer + 1 > height_map[i, j]:
585
- height_map[i, j] = layer + 1
586
- # Track the lowest layer this material appears at this pixel
587
- if layer < min_height_map[i, j]:
588
- min_height_map[i, j] = layer
589
-
590
- if not has_material:
590
+ """Create the STL for one material: one solid per band it prints in.
591
+
592
+ A material can recur in several separate bands (e.g. white low down
593
+ and again as a highlight). A single solid spanning the lowest to the
594
+ highest of its layers used to swallow every other material printed in
595
+ between, so the per-color STLs overlapped. At a pixel of height z, the
596
+ band [a, b) is filled from layer a up to min(b, z)."""
597
+ pieces = []
598
+ for material, first, end in bands:
599
+ if material != material_idx:
600
+ continue
601
+ band_mask = heights > first
602
+ if not np.any(band_mask & valid_mask):
603
+ continue
604
+ top_mm = np.minimum(heights, end).astype(float) * layer_height
605
+ bottom_mm = np.full((H, W), first * layer_height, dtype=float)
606
+ mesh_data = _create_flatforge_box_mesh(
607
+ top_mm, bottom_mm, background_height,
608
+ maximum_x_y_size, valid_mask, band_mask
609
+ )
610
+ if mesh_data is not None:
611
+ pieces.append(mesh_data)
612
+
613
+ if not pieces:
591
614
  return None
592
-
593
- # Convert to mm (layers to mm)
594
- height_map_mm = height_map * layer_height
595
- min_height_map_mm = min_height_map * layer_height
596
-
597
- # Create a 2D mask indicating which pixels have this material (at any layer)
598
- material_mask_2d = np.any(material_mask_3d, axis=0)
599
-
600
- # Create vertices for the box
601
- # We need to create a rectangular box where z goes from min to max for each pixel
615
+
602
616
  filename = os.path.join(output_folder, f"{material_name}_{color_hex.lstrip('#')}.stl")
603
-
604
- # Build mesh with per-pixel min and max heights
605
- mesh_data = _create_flatforge_box_mesh(
606
- height_map_mm, min_height_map_mm, background_height,
607
- maximum_x_y_size, valid_mask, material_mask_2d
608
- )
609
-
610
- if mesh_data is not None:
611
- _save_stl_with_manifold_fix(mesh_data, filename)
612
- stl_files.append(filename)
613
- return filename
614
- return None
615
-
617
+ _save_stl_with_manifold_fix(np.concatenate(pieces, axis=0), filename)
618
+ stl_files.append(filename)
619
+ return filename
620
+
616
621
  # Generate STL for each unique material
617
622
  for mat_idx in unique_materials:
618
623
  material_name = material_names[mat_idx].replace(" ", "_").replace("/", "-")
@@ -621,42 +626,20 @@ def generate_flatforge_stls(
621
626
  color_hex = "#{:02x}{:02x}{:02x}".format(
622
627
  int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)
623
628
  )
624
-
629
+
625
630
  print(f"Generating FlatForge STL for {material_name}...")
626
631
  create_color_stl(mat_idx, material_name, color_hex)
627
-
632
+
628
633
  # Generate STL for clear/transparent areas
629
- # Clear should ONLY be placed above the topmost colored material, never between colors
630
- # Since gaps between colors are filled by extending the color from below during material assignment,
631
- # we only need to check if the pixel height is less than max_layer (unfilled top layers)
634
+ # Clear is only placed above the topmost colored layer of a pixel, never
635
+ # between colors (gaps take the color from below, see layer_color).
632
636
  print("Generating FlatForge STL for clear areas...")
633
-
634
- # Find positions where clear is needed (above all colored materials)
635
- clear_height_map = np.zeros((H, W), dtype=float)
636
- clear_min_height_map = np.full((H, W), max_layer, dtype=float)
637
-
638
- has_clear = False
639
- for i in range(H):
640
- for j in range(W):
641
- if not valid_mask[i, j]:
642
- continue
643
-
644
- pixel_height = int(disc_height_image[i, j])
645
-
646
- # If pixel height is less than max_layer, fill the top with clear
647
- if pixel_height < max_layer:
648
- has_clear = True
649
- clear_min_height_map[i, j] = pixel_height
650
- clear_height_map[i, j] = max_layer
651
- # Note: We don't check layer_materials because gaps are already filled with colors
652
-
653
- if has_clear:
654
- clear_height_map_mm = clear_height_map * layer_height
655
- clear_min_height_map_mm = clear_min_height_map * layer_height
656
-
657
- # Create mask for clear areas
658
- clear_mask = (clear_height_map > 0) & valid_mask
659
-
637
+
638
+ clear_mask = valid_mask & (heights < max_layer)
639
+ if np.any(clear_mask):
640
+ clear_height_map_mm = np.full((H, W), max_layer * layer_height, dtype=float)
641
+ clear_min_height_map_mm = heights.astype(float) * layer_height
642
+
660
643
  filename = os.path.join(output_folder, f"Clear_{clear_material_name}_{clear_color_hex}.stl")
661
644
  mesh_data = _create_flatforge_box_mesh(
662
645
  clear_height_map_mm, clear_min_height_map_mm, background_height,
@@ -667,7 +650,7 @@ def generate_flatforge_stls(
667
650
  stl_files.append(filename)
668
651
  else:
669
652
  print("No clear areas needed - all layers filled with colored materials.")
670
-
653
+
671
654
  # Generate cap layer if requested
672
655
  if cap_layers > 0:
673
656
  print(f"Generating FlatForge STL for cap layer ({cap_layers} layers)...")