reverse3d 0.8.0__tar.gz → 0.8.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 (78) hide show
  1. {reverse3d-0.8.0/reverse3d.egg-info → reverse3d-0.8.1}/PKG-INFO +5 -4
  2. {reverse3d-0.8.0 → reverse3d-0.8.1}/README.md +4 -3
  3. reverse3d-0.8.1/reverse3d/_version.py +1 -0
  4. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/recipes.py +66 -41
  5. {reverse3d-0.8.0 → reverse3d-0.8.1/reverse3d.egg-info}/PKG-INFO +5 -4
  6. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/SOURCES.txt +1 -0
  7. reverse3d-0.8.1/tests/test_color.py +49 -0
  8. reverse3d-0.8.0/reverse3d/_version.py +0 -1
  9. {reverse3d-0.8.0 → reverse3d-0.8.1}/LICENSE +0 -0
  10. {reverse3d-0.8.0 → reverse3d-0.8.1}/NOTICE +0 -0
  11. {reverse3d-0.8.0 → reverse3d-0.8.1}/pyproject.toml +0 -0
  12. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/__init__.py +0 -0
  13. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/__main__.py +0 -0
  14. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/_about.py +0 -0
  15. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/__init__.py +0 -0
  16. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/compact.py +0 -0
  17. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/mcp_server.py +0 -0
  18. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/prompts.py +0 -0
  19. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/skill/SKILL.md +0 -0
  20. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/agent/tools.py +0 -0
  21. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/autopilot.py +0 -0
  22. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/cli.py +0 -0
  23. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/config.py +0 -0
  24. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/doctor.py +0 -0
  25. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/errors.py +0 -0
  26. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/hardware.py +0 -0
  27. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/imaging.py +0 -0
  28. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/logging_utils.py +0 -0
  29. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/mesh_io.py +0 -0
  30. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/offline.py +0 -0
  31. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/profiles.py +0 -0
  32. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/py.typed +0 -0
  33. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/__init__.py +0 -0
  34. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/depth_estimation.py +0 -0
  35. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/photogrammetry.py +0 -0
  36. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/pointcloud.py +0 -0
  37. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reconstruction/silhouette.py +0 -0
  38. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/__init__.py +0 -0
  39. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/_blender_job.py +0 -0
  40. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/blender_render.py +0 -0
  41. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/rendering/preview.py +0 -0
  42. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/__init__.py +0 -0
  43. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/geometry_fit.py +0 -0
  44. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/measure.py +0 -0
  45. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/mesh_cleaner.py +0 -0
  46. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/reverse_eng/parametric.py +0 -0
  47. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/scenes.py +0 -0
  48. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/security.py +0 -0
  49. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/setup_agents.py +0 -0
  50. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/userconfig.py +0 -0
  51. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/__init__.py +0 -0
  52. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/classifier.py +0 -0
  53. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/feature_matcher.py +0 -0
  54. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/mask_extractor.py +0 -0
  55. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/vision/segmenter.py +0 -0
  56. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d/watch.py +0 -0
  57. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/dependency_links.txt +0 -0
  58. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/entry_points.txt +0 -0
  59. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/requires.txt +0 -0
  60. {reverse3d-0.8.0 → reverse3d-0.8.1}/reverse3d.egg-info/top_level.txt +0 -0
  61. {reverse3d-0.8.0 → reverse3d-0.8.1}/setup.cfg +0 -0
  62. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_agent_tools.py +0 -0
  63. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_authorship.py +0 -0
  64. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_automation.py +0 -0
  65. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_cli.py +0 -0
  66. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_cli_automation.py +0 -0
  67. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_config.py +0 -0
  68. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_geometry_fit.py +0 -0
  69. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_heic.py +0 -0
  70. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_mesh.py +0 -0
  71. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_offline_autopilot.py +0 -0
  72. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_optional_extras.py +0 -0
  73. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_reconstruction.py +0 -0
  74. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_render.py +0 -0
  75. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_scenes.py +0 -0
  76. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_security.py +0 -0
  77. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_silhouette.py +0 -0
  78. {reverse3d-0.8.0 → reverse3d-0.8.1}/tests/test_vision.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reverse3d
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: AI-agent-ready toolkit for 3D reverse engineering: image analysis, photogrammetry, mesh repair, primitive fitting and headless rendering.
5
5
  Author-email: Voxel Enterprise <info@voxel.lat>
6
6
  Maintainer-email: Voxel Enterprise <info@voxel.lat>
@@ -215,10 +215,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
215
215
 
216
216
  | bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
217
217
  |---------------------|----------------------------|--------|
218
- | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA de profundidad, Blender y COLMAP |
219
- | `--lite` | `-Lite` | Todo menos la IA de profundidad (PyTorch); ahorra hasta ~2.5 GB |
220
- | `--minimal` | `-Minimal` | Solo núcleo + MCP, sin herramientas externas |
218
+ | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
219
+ | `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
220
+ | `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
221
221
  | `--no-tools` | `-NoTools` | No instalar Blender ni COLMAP |
222
+ | *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación y profundidad, unos 750 MB) para que todo funcione sin internet |
222
223
  | `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
223
224
  | `--with recon,depth` | `-With recon,depth` | Extras a elección |
224
225
  | `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
@@ -147,10 +147,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
147
147
 
148
148
  | bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
149
149
  |---------------------|----------------------------|--------|
150
- | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA de profundidad, Blender y COLMAP |
151
- | `--lite` | `-Lite` | Todo menos la IA de profundidad (PyTorch); ahorra hasta ~2.5 GB |
152
- | `--minimal` | `-Minimal` | Solo núcleo + MCP, sin herramientas externas |
150
+ | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
151
+ | `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
152
+ | `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
153
153
  | `--no-tools` | `-NoTools` | No instalar Blender ni COLMAP |
154
+ | *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación y profundidad, unos 750 MB) para que todo funcione sin internet |
154
155
  | `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
155
156
  | `--with recon,depth` | `-With recon,depth` | Extras a elección |
156
157
  | `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
@@ -0,0 +1 @@
1
+ __version__ = "0.8.1"
@@ -30,6 +30,8 @@ from dataclasses import dataclass, field
30
30
  from pathlib import Path
31
31
  from typing import Any, Literal, TypeVar
32
32
 
33
+ import numpy as np
34
+
33
35
  from reverse3d.config import Settings
34
36
  from reverse3d.errors import (
35
37
  CaptureQualityError,
@@ -135,29 +137,70 @@ def _clean(
135
137
  parts, and tiny debris is already dropped by the area filter."""
136
138
  from reverse3d.reverse_eng.mesh_cleaner import CleanOptions, clean_mesh_file
137
139
 
140
+ options = CleanOptions(
141
+ keep_largest_only=keep_largest,
142
+ fill_holes=fill_holes,
143
+ align=align,
144
+ ground=align != "none",
145
+ scale=scale,
146
+ max_hole_edges=2000 if repair == "strong" else 200,
147
+ )
138
148
  report = run.step(
139
149
  "clean",
140
- lambda: clean_mesh_file(
141
- src,
142
- dst,
143
- run.settings,
144
- CleanOptions(
145
- keep_largest_only=keep_largest,
146
- fill_holes=fill_holes,
147
- align=align,
148
- ground=align != "none",
149
- scale=scale,
150
- max_hole_edges=2000 if repair == "strong" else 200,
151
- ),
152
- overwrite=overwrite,
153
- ),
150
+ lambda: clean_mesh_file(src, dst, run.settings, options, overwrite=overwrite),
154
151
  )
155
152
  run.warnings.extend(report.warnings)
156
153
  run.outputs["model"] = str(dst)
157
154
  run.details["clean"] = report.to_dict()
155
+ if _has_vertex_colors(src):
156
+ # Same cleaning on the coloured mesh (deterministic, colours follow the
157
+ # vertices): model_color.ply for renders and model.glb for games and viewers.
158
+ try:
159
+ color_ply = dst.with_name("model_color.ply")
160
+ clean_mesh_file(src, color_ply, run.settings, options, overwrite=True)
161
+ import trimesh
162
+
163
+ glb = dst.with_name("model.glb")
164
+ trimesh.load(color_ply, process=False, force="mesh").export(glb)
165
+ run.outputs["model_color"] = str(color_ply)
166
+ run.outputs["model_glb"] = str(glb)
167
+ except Exception as exc: # colour is a bonus: never fail the model for it
168
+ logger.info("Coloured model skipped: %s", exc)
158
169
  return report.after
159
170
 
160
171
 
172
+ def _has_vertex_colors(path: Path) -> bool:
173
+ import trimesh
174
+
175
+ try:
176
+ mesh: Any = trimesh.load(path, process=False, force="mesh")
177
+ except Exception:
178
+ return False
179
+ colors = getattr(mesh.visual, "vertex_colors", None)
180
+ if colors is None or len(colors) != len(mesh.vertices):
181
+ return False
182
+ rgb = np.asarray(colors)[:, :3]
183
+ return float(np.max(np.std(rgb, axis=0))) > 1.0 # not just a uniform default colour
184
+
185
+
186
+ def _color_from_cloud(mesh_path: Path, cloud_path: Path, out: Path) -> Path | None:
187
+ """Paint a mesh with the colours of the point cloud it was built from."""
188
+ import trimesh
189
+ from scipy.spatial import cKDTree
190
+
191
+ cloud: Any = trimesh.load(cloud_path, process=False)
192
+ colors = getattr(cloud, "colors", None)
193
+ if colors is None or len(colors) != len(getattr(cloud, "vertices", [])):
194
+ return None
195
+ mesh: Any = trimesh.load(mesh_path, process=False, force="mesh")
196
+ _, idx = cKDTree(np.asarray(cloud.vertices)).query(np.asarray(mesh.vertices), k=4)
197
+ rgb = np.asarray(colors)[idx][..., :3].astype(np.float64).mean(axis=1)
198
+ rgba = np.column_stack([rgb, np.full(len(rgb), 255.0)]).clip(0, 255).astype(np.uint8)
199
+ mesh.visual = trimesh.visual.ColorVisuals(mesh, vertex_colors=rgba)
200
+ mesh.export(out)
201
+ return out
202
+
203
+
161
204
  def _render(run: _Run, mesh: Path, overwrite: bool) -> None:
162
205
  from reverse3d.rendering.blender_render import RenderOptions, preset_options, render_mesh
163
206
 
@@ -210,28 +253,6 @@ _DEFAULT_LOOK = {
210
253
  }
211
254
 
212
255
 
213
- def _color_outputs(run: _Run, colored: Path, model: Path, scale: float) -> None:
214
- """Give the final model the photo colours of the carved hull: model.glb (games,
215
- web, viewers) and model_color.ply (renders)."""
216
- import numpy as np
217
- import trimesh
218
- from scipy.spatial import cKDTree
219
-
220
- source: Any = trimesh.load(colored, process=False, force="mesh")
221
- target: Any = trimesh.load(model, process=False, force="mesh")
222
- colors = getattr(source.visual, "vertex_colors", None)
223
- if colors is None or len(colors) != len(source.vertices):
224
- return
225
- _, nearest = cKDTree(np.asarray(source.vertices) * scale).query(np.asarray(target.vertices))
226
- target.visual = trimesh.visual.ColorVisuals(target, vertex_colors=np.asarray(colors)[nearest])
227
- glb = run.out / "model.glb"
228
- ply = run.out / "model_color.ply"
229
- target.export(glb)
230
- target.export(ply)
231
- run.outputs["model_glb"] = str(glb)
232
- run.outputs["model_color"] = str(ply)
233
-
234
-
235
256
  def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
236
257
  from reverse3d.reconstruction.photogrammetry import (
237
258
  PhotogrammetryOptions,
@@ -352,6 +373,15 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
352
373
  "Point cloud created but no mesh could be built.",
353
374
  hint='Install the meshing extra: pip install "reverse3d[recon]" (Python ≤ 3.12).',
354
375
  )
376
+ cloud = photo.dense_points_path or photo.sparse_points_path
377
+ if not upright and cloud:
378
+ try:
379
+ colored = _color_from_cloud(
380
+ Path(mesh_path), Path(cloud), run.out / "reconstruction" / "mesh_color.ply"
381
+ )
382
+ mesh_path = str(colored) if colored else mesh_path
383
+ except Exception as exc: # colour is a bonus
384
+ logger.info("Colouring from the point cloud skipped: %s", exc)
355
385
  model = run.out / "model.stl"
356
386
  _clean(
357
387
  run,
@@ -364,11 +394,6 @@ def _photos_to_model(run: _Run, src: Path, opt: RecipeOptions) -> MeshStats:
364
394
  keep_largest=preset.keep_largest,
365
395
  repair=opt.repair,
366
396
  )
367
- if upright:
368
- try:
369
- _color_outputs(run, Path(mesh_path), model, opt.scale)
370
- except Exception as exc: # colour is a bonus: never fail the model for it
371
- logger.info("Colour transfer skipped: %s", exc)
372
397
  return _measure(run, model)
373
398
 
374
399
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reverse3d
3
- Version: 0.8.0
3
+ Version: 0.8.1
4
4
  Summary: AI-agent-ready toolkit for 3D reverse engineering: image analysis, photogrammetry, mesh repair, primitive fitting and headless rendering.
5
5
  Author-email: Voxel Enterprise <info@voxel.lat>
6
6
  Maintainer-email: Voxel Enterprise <info@voxel.lat>
@@ -215,10 +215,11 @@ Cada archivo modificado se **respalda antes** (`*.reverse3d-backup-<fecha>`). So
215
215
 
216
216
  | bash (`install.sh`) | PowerShell (`install.ps1`) | Efecto |
217
217
  |---------------------|----------------------------|--------|
218
- | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA de profundidad, Blender y COLMAP |
219
- | `--lite` | `-Lite` | Todo menos la IA de profundidad (PyTorch); ahorra hasta ~2.5 GB |
220
- | `--minimal` | `-Minimal` | Solo núcleo + MCP, sin herramientas externas |
218
+ | *(sin opciones)* | *(sin opciones)* | **Todo**: núcleo, MCP, render, mallado, IA local (con CUDA si hay NVIDIA), Blender y COLMAP |
219
+ | `--lite` | `-Lite` | Todo, con la IA local en la CPU (PyTorch CPU, ~250 MB en vez de ~2.5 GB) |
220
+ | `--minimal` | `-Minimal` | Núcleo + MCP + IA local, sin herramientas externas |
221
221
  | `--no-tools` | `-NoTools` | No instalar Blender ni COLMAP |
222
+ | *(en todos los modos)* | *(en todos los modos)* | **La IA local se instala y se descarga siempre** (clasificador, segmentación y profundidad, unos 750 MB) para que todo funcione sin internet |
222
223
  | `--no-auto-approve` | `-NoAutoApprove` | Que los agentes pregunten antes de cada herramienta |
223
224
  | `--with recon,depth` | `-With recon,depth` | Extras a elección |
224
225
  | `--device auto\|cpu\|gpu` | `-Device …` | Dispositivo por defecto |
@@ -60,6 +60,7 @@ tests/test_authorship.py
60
60
  tests/test_automation.py
61
61
  tests/test_cli.py
62
62
  tests/test_cli_automation.py
63
+ tests/test_color.py
63
64
  tests/test_config.py
64
65
  tests/test_geometry_fit.py
65
66
  tests/test_heic.py
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ import numpy as np
6
+ import trimesh
7
+
8
+ from reverse3d.config import Settings
9
+ from reverse3d.recipes import _clean, _color_from_cloud, _has_vertex_colors, _Run
10
+
11
+
12
+ def _two_tone_cloud(path: Path) -> Path:
13
+ """Points on a sphere: red on top, blue at the bottom (like a COLMAP fused.ply)."""
14
+ pts = trimesh.creation.icosphere(subdivisions=5).vertices
15
+ rgb = np.where(pts[:, 2:3] > 0, [220, 30, 30], [30, 30, 220])
16
+ trimesh.PointCloud(pts, colors=np.column_stack([rgb, np.full(len(pts), 255)])).export(path)
17
+ return path
18
+
19
+
20
+ def test_mesh_takes_the_colours_of_its_point_cloud(tmp_path: Path) -> None:
21
+ plain = tmp_path / "mesh.ply"
22
+ trimesh.creation.icosphere(subdivisions=3).export(plain)
23
+ assert not _has_vertex_colors(plain)
24
+ colored = _color_from_cloud(plain, _two_tone_cloud(tmp_path / "cloud.ply"), tmp_path / "c.ply")
25
+ assert colored is not None and _has_vertex_colors(colored)
26
+ mesh = trimesh.load(colored, process=False)
27
+ top = mesh.vertices[:, 2] > 0.3
28
+ assert mesh.visual.vertex_colors[top, 0].mean() > 150 # red on top
29
+ assert mesh.visual.vertex_colors[~top & (mesh.vertices[:, 2] < -0.3), 2].mean() > 150
30
+
31
+
32
+ def test_cleaning_a_coloured_mesh_also_writes_glb(tmp_path: Path, settings: Settings) -> None:
33
+ plain = tmp_path / "mesh.ply"
34
+ trimesh.creation.icosphere(subdivisions=3).export(plain)
35
+ colored = _color_from_cloud(plain, _two_tone_cloud(tmp_path / "cloud.ply"), tmp_path / "c.ply")
36
+ assert colored is not None
37
+ run = _Run(recipe="photos_to_model", settings=settings, out=tmp_path)
38
+ _clean(run, colored, tmp_path / "model.stl", scale=1.0, overwrite=True)
39
+ assert {"model", "model_color", "model_glb"} <= set(run.outputs)
40
+ glb = trimesh.load(run.outputs["model_glb"], force="mesh")
41
+ assert len(np.unique(np.asarray(glb.visual.vertex_colors)[:, :3], axis=0)) > 1
42
+
43
+
44
+ def test_plain_meshes_get_no_colour_outputs(tmp_path: Path, settings: Settings) -> None:
45
+ plain = tmp_path / "mesh.ply"
46
+ trimesh.creation.icosphere(subdivisions=3).export(plain)
47
+ run = _Run(recipe="mesh_report", settings=settings, out=tmp_path)
48
+ _clean(run, plain, tmp_path / "model.stl", scale=1.0, overwrite=True)
49
+ assert "model_glb" not in run.outputs
@@ -1 +0,0 @@
1
- __version__ = "0.8.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes