3d-base 0.3.2__tar.gz → 0.3.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 3d-base
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Headless pictures of 3D meshes with correct occlusion and smooth shading, and a health check for exported STL files
5
5
  Author: Konstantinos Georgiou
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "3d-base"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Headless pictures of 3D meshes with correct occlusion and smooth shading, and a health check for exported STL files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: 3d-base
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Headless pictures of 3D meshes with correct occlusion and smooth shading, and a health check for exported STL files
5
5
  Author: Konstantinos Georgiou
6
6
  License-Expression: MIT
@@ -188,7 +188,7 @@ def as_written(mesh: trimesh.Trimesh, file_type: str = "stl", loose_mm3: float =
188
188
  "touches": touches, "loose_mm3": float(loose)}
189
189
 
190
190
 
191
- def solid_from_skin(mesh: trimesh.Trimesh, centre, lean: float = 0.3, keep=None) -> trimesh.Trimesh:
191
+ def solid_from_skin(mesh: trimesh.Trimesh, centre, lean: float = 0.3, keep=None, skin: float = 0.5) -> trimesh.Trimesh:
192
192
  """A hollow part made solid from its outer skin, by removing its inner surface.
193
193
 
194
194
  The inner surface is the largest connected region, by area, of faces that point toward `centre`
@@ -200,6 +200,11 @@ def solid_from_skin(mesh: trimesh.Trimesh, centre, lean: float = 0.3, keep=None)
200
200
  the same outside and no inside, so a cavity cut into it afterwards (a bore for a part it sits on)
201
201
  is the only inside it has.
202
202
 
203
+ A face is hidden only by a face more than `skin` nearer the centre along its ray. Nearer than
204
+ that is a fold or a second copy of the surface, the kind a boolean or a vertex displacement
205
+ leaves (a cap's seams, pressed in by half a millimetre, folded its inner skin over itself at
206
+ the front and hid a patch of it), not a wall.
207
+
203
208
  `keep` names faces that stay whatever they face, as a boolean mask over the faces: a part the
204
209
  centre sees through the opening, such as the underside of a cap's bill, which faces the centre
205
210
  from outside the shell and would otherwise be taken for inner surface and have the air under it
@@ -223,7 +228,7 @@ def solid_from_skin(mesh: trimesh.Trimesh, centre, lean: float = 0.3, keep=None)
223
228
  if not cand.any():
224
229
  raise ValueError("no face points toward the centre")
225
230
  which = np.flatnonzero(cand)
226
- cand[which[~_seen_from(mesh, centre, which)]] = False
231
+ cand[which[~_seen_from(mesh, centre, which, skin=skin)]] = False
227
232
  if not cand.any():
228
233
  raise ValueError("no face that points toward the centre is seen from it")
229
234
  adj = np.asarray(mesh.face_adjacency)
@@ -265,8 +270,9 @@ def solid_from_skin(mesh: trimesh.Trimesh, centre, lean: float = 0.3, keep=None)
265
270
  return solid
266
271
 
267
272
 
268
- def _seen_from(mesh: trimesh.Trimesh, centre, which, cells: int = 64, batch: int = 4_000_000):
269
- """Which of the faces `which` a ray from `centre` to their middles meets first.
273
+ def _seen_from(mesh: trimesh.Trimesh, centre, which, cells: int = 64, batch: int = 4_000_000, skin: float = 0.0):
274
+ """Which of the faces `which` a ray from `centre` to their middles meets first, or within `skin`
275
+ of first: a face less than that nearer the centre is a fold or a second copy of the surface.
270
276
 
271
277
  Every ray starts at the one point, so the triangles are binned by their direction from it on a
272
278
  cube map (six faces of `cells` by `cells`), and each ray is tested only against the triangles in
@@ -313,7 +319,7 @@ def _seen_from(mesh: trimesh.Trimesh, centre, which, cells: int = 64, batch: int
313
319
  for i in range(0, len(pair_ray), batch):
314
320
  r, t_idx = pair_ray[i:i + batch], pair_tri[i:i + batch]
315
321
  t = _ray_t(mid[r] / dist[r][:, None], tri[t_idx])
316
- before = (t < dist[r] - 1e-7) & ~own[i:i + batch]
322
+ before = (t < dist[r] - max(float(skin), 1e-7)) & ~own[i:i + batch]
317
323
  hidden[np.unique(r[before])] = True
318
324
  return ~hidden
319
325
 
@@ -259,3 +259,29 @@ def test_a_bill_seen_through_the_opening_is_kept_when_named():
259
259
  assert told.is_watertight and told.is_winding_consistent and told.euler_number == 2
260
260
  assert not told.contains(under)[0]
261
261
  assert abs(told.bounds[1][0] - 20.0) < 1e-6
262
+
263
+
264
+ def test_a_sliver_just_inside_the_inner_surface_neither_hides_it_nor_stays():
265
+ from base3d.mesh import solid_from_skin
266
+ # a dome shell with a closed sliver, 0.2 mm thick, lying 0.05 mm under the top of its inner
267
+ # surface: the kind of second copy a boolean or a vertex displacement leaves. Its far side faces
268
+ # outward and stands in front of the dome's top as seen from the centre
269
+ t = np.linspace(0, np.pi / 2, 24)
270
+ outer = np.c_[10 * np.sin(t), 10 * np.cos(t)]
271
+ inner = np.c_[9 * np.sin(t), 9 * np.cos(t)][::-1]
272
+ dome = _revolved(np.vstack([outer, inner, outer[:1]]), sections=64)
273
+ sliver = trimesh.creation.box(extents=(2.0, 2.0, 0.2))
274
+ sliver.apply_translation([0, 0, 9.0 - 0.05 - 0.1])
275
+ both = trimesh.util.concatenate([dome, sliver])
276
+ # the dome's faces keep their indices in the joined mesh; the top of its inner surface lies in
277
+ # the sliver's shadow, hidden by 0.05 mm with no skin and seen through it with the default
278
+ from base3d.mesh import _seen_from
279
+ c = dome.triangles_center
280
+ top = np.flatnonzero((c[:, 2] > 8.5) & (c[:, 2] < 9.2) & (np.linalg.norm(c[:, :2], axis=1) < 0.6))
281
+ assert len(top) > 0
282
+ assert not _seen_from(both, (0, 0, 0), top, skin=0.0).any()
283
+ assert _seen_from(both, (0, 0, 0), top, skin=0.5).all()
284
+ solid = solid_from_skin(both, centre=(0, 0, 0))
285
+ assert solid.is_watertight and solid.is_winding_consistent and solid.euler_number == 2
286
+ assert solid.contains([[0.0, 0.0, 8.9], [0.0, 0.0, 5.0]]).all() # the top is solid through
287
+ assert abs(solid.volume - 2.0 / 3.0 * np.pi * 1000) < 0.03 * 2094
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