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.
- {3d_base-0.3.2/src/3d_base.egg-info → 3d_base-0.3.3}/PKG-INFO +1 -1
- {3d_base-0.3.2 → 3d_base-0.3.3}/pyproject.toml +1 -1
- {3d_base-0.3.2 → 3d_base-0.3.3/src/3d_base.egg-info}/PKG-INFO +1 -1
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/mesh.py +11 -5
- {3d_base-0.3.2 → 3d_base-0.3.3}/tests/test_mesh.py +26 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/LICENSE +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/README.md +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/setup.cfg +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/3d_base.egg-info/SOURCES.txt +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/3d_base.egg-info/dependency_links.txt +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/3d_base.egg-info/entry_points.txt +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/3d_base.egg-info/requires.txt +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/3d_base.egg-info/top_level.txt +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/__init__.py +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/cli.py +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/show.py +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/skill/SKILL.md +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/src/base3d/thickness.py +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/tests/test_show.py +0 -0
- {3d_base-0.3.2 → 3d_base-0.3.3}/tests/test_thickness.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "3d-base"
|
|
7
|
-
version = "0.3.
|
|
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"
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|