mirror of
https://github.com/gumyr/build123d.git
synced 2026-05-10 22:23:10 -07:00
Reorder STL primitive detection to defer normal planes to enable more cylinders to be found
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.14) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.14) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.14) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.14) (push) Waiting to run
Run type checking / typecheck (3.10) (push) Waiting to run
Run type checking / typecheck (3.14) (push) Waiting to run
Some checks are pending
benchmarks / benchmarks (macos-14, 3.12) (push) Waiting to run
benchmarks / benchmarks (macos-15-intel, 3.12) (push) Waiting to run
benchmarks / benchmarks (ubuntu-latest, 3.12) (push) Waiting to run
benchmarks / benchmarks (windows-latest, 3.12) (push) Waiting to run
Upload coverage reports to Codecov / run (push) Waiting to run
pylint / lint (3.10) (push) Waiting to run
Wheel building and publishing / Build wheel on ubuntu-latest (push) Waiting to run
Wheel building and publishing / upload_pypi (push) Blocked by required conditions
tests / tests (macos-14, 3.10) (push) Waiting to run
tests / tests (macos-14, 3.14) (push) Waiting to run
tests / tests (macos-15-intel, 3.10) (push) Waiting to run
tests / tests (macos-15-intel, 3.14) (push) Waiting to run
tests / tests (ubuntu-latest, 3.10) (push) Waiting to run
tests / tests (ubuntu-latest, 3.14) (push) Waiting to run
tests / tests (windows-latest, 3.10) (push) Waiting to run
tests / tests (windows-latest, 3.14) (push) Waiting to run
Run type checking / typecheck (3.10) (push) Waiting to run
Run type checking / typecheck (3.14) (push) Waiting to run
This commit is contained in:
parent
a72776d466
commit
771a00d9eb
2 changed files with 161 additions and 28 deletions
|
|
@ -11,9 +11,9 @@ desc:
|
|||
|
||||
The user-facing entry point is ``detect_primitives``. The reconstruction
|
||||
pipeline first builds a mesh index of face centers, normals, and adjacency.
|
||||
It then searches for planes, spheres, and cylinders in that order so
|
||||
simpler and more stable primitives claim faces before more ambiguous curved
|
||||
regions are processed.
|
||||
It then searches for clean proxy planes, spheres, cylinders, and fallback
|
||||
normal-grouped planes in that order so stronger primitive evidence claims
|
||||
faces before more ambiguous regions are processed.
|
||||
|
||||
Each detector uses a broad classification step to identify candidate faces,
|
||||
sews or connects them into regions, fits a local analytic primitive, and
|
||||
|
|
@ -1249,24 +1249,29 @@ def _intersect_2d_lines(
|
|||
)
|
||||
|
||||
|
||||
def detect_planes(
|
||||
def detect_planes_from_clean_proxy(
|
||||
mesh,
|
||||
mesh_index: MeshIndex,
|
||||
) -> list[PlanePatch]:
|
||||
"""Detect high-confidence planar regions from cleaned proxy faces."""
|
||||
|
||||
return _detect_planes_from_clean_proxy(mesh, mesh_index)
|
||||
|
||||
|
||||
def detect_planes_from_normals(
|
||||
mesh,
|
||||
mesh_index: MeshIndex,
|
||||
blocked_indices: set[int] | None = None,
|
||||
normal_digits: int = 3,
|
||||
plane_tolerance_factor: float = 0.003,
|
||||
min_component_size: int = 2,
|
||||
min_two_face_area_factor: float = 0.05,
|
||||
) -> list[PlanePatch]:
|
||||
"""Detect planar regions in a mesh."""
|
||||
"""Detect planar regions by grouping connected faces with matching normals."""
|
||||
|
||||
shape_scale = mesh.bounding_box().diagonal
|
||||
plane_patches = _detect_planes_from_clean_proxy(mesh, mesh_index)
|
||||
claimed = (
|
||||
set().union(*(patch.face_indices for patch in plane_patches))
|
||||
if plane_patches
|
||||
else set()
|
||||
)
|
||||
remaining = set(range(len(mesh_index.faces))) - claimed
|
||||
plane_patches: list[PlanePatch] = []
|
||||
remaining = set(range(len(mesh_index.faces))) - (blocked_indices or set())
|
||||
|
||||
for component_indices in _plane_like_face_components(
|
||||
mesh_index,
|
||||
|
|
@ -1292,6 +1297,35 @@ def detect_planes(
|
|||
return plane_patches
|
||||
|
||||
|
||||
def detect_planes(
|
||||
mesh,
|
||||
mesh_index: MeshIndex,
|
||||
normal_digits: int = 3,
|
||||
plane_tolerance_factor: float = 0.003,
|
||||
min_component_size: int = 2,
|
||||
min_two_face_area_factor: float = 0.05,
|
||||
) -> list[PlanePatch]:
|
||||
"""Detect planar regions in a mesh."""
|
||||
|
||||
clean_plane_patches = detect_planes_from_clean_proxy(mesh, mesh_index)
|
||||
clean_plane_indices = (
|
||||
set().union(*(patch.face_indices for patch in clean_plane_patches))
|
||||
if clean_plane_patches
|
||||
else set()
|
||||
)
|
||||
normal_plane_patches = detect_planes_from_normals(
|
||||
mesh,
|
||||
mesh_index,
|
||||
blocked_indices=clean_plane_indices,
|
||||
normal_digits=normal_digits,
|
||||
plane_tolerance_factor=plane_tolerance_factor,
|
||||
min_component_size=min_component_size,
|
||||
min_two_face_area_factor=min_two_face_area_factor,
|
||||
)
|
||||
|
||||
return [*clean_plane_patches, *normal_plane_patches]
|
||||
|
||||
|
||||
# Cylinder detection
|
||||
def _cylinder_like_face_indices(
|
||||
mesh_index: MeshIndex,
|
||||
|
|
@ -1849,12 +1883,12 @@ def detect_primitives(
|
|||
throughout the pipeline.
|
||||
|
||||
Detection proceeds in stages:
|
||||
1. Planes are found first from cleaned proxy faces and coplanar connected
|
||||
components.
|
||||
1. High-confidence planes are found first from cleaned proxy faces.
|
||||
2. Spheres are found next from broad radius-signature classification,
|
||||
connected or sewn regions, local sphere fitting, and region growth.
|
||||
3. Cylinders are detected last from area-grouped sewn regions and local
|
||||
3. Cylinders are detected from area-grouped sewn regions and local
|
||||
cylinder seeds, then grown, refit, and validated.
|
||||
4. Remaining coplanar connected components are detected as fallback planes.
|
||||
|
||||
Each accepted patch is converted into a build123d Face, unmatched mesh
|
||||
faces are returned as leftovers, and the generated code strings are sorted
|
||||
|
|
@ -1864,14 +1898,14 @@ def detect_primitives(
|
|||
mesh_index = MeshIndex.from_shape(mesh)
|
||||
# shape_scale = mesh.bounding_box().diagonal
|
||||
|
||||
plane_patches = detect_planes(mesh, mesh_index)
|
||||
plane_indices = (
|
||||
set().union(*(patch.face_indices for patch in plane_patches))
|
||||
if plane_patches
|
||||
clean_plane_patches = detect_planes_from_clean_proxy(mesh, mesh_index)
|
||||
clean_plane_indices = (
|
||||
set().union(*(patch.face_indices for patch in clean_plane_patches))
|
||||
if clean_plane_patches
|
||||
else set()
|
||||
)
|
||||
|
||||
sphere_patches = detect_spheres(mesh, mesh_index, plane_indices)
|
||||
sphere_patches = detect_spheres(mesh, mesh_index, clean_plane_indices)
|
||||
sphere_indices = (
|
||||
set().union(*(patch.face_indices for patch in sphere_patches))
|
||||
if sphere_patches
|
||||
|
|
@ -1881,15 +1915,26 @@ def detect_primitives(
|
|||
cylinder_patches = detect_cylinders(
|
||||
mesh,
|
||||
mesh_index,
|
||||
plane_indices | sphere_indices,
|
||||
clean_plane_indices | sphere_indices,
|
||||
)
|
||||
cylinder_indices = (
|
||||
set().union(*(patch.face_indices for patch in cylinder_patches))
|
||||
if cylinder_patches
|
||||
else set()
|
||||
)
|
||||
# cylinder_indices = (
|
||||
# set().union(*(patch.face_indices for patch in cylinder_patches))
|
||||
# if cylinder_patches
|
||||
# else set()
|
||||
# )
|
||||
|
||||
patches: list[DetectedPatch] = [*plane_patches, *cylinder_patches, *sphere_patches]
|
||||
normal_plane_patches = detect_planes_from_normals(
|
||||
mesh,
|
||||
mesh_index,
|
||||
blocked_indices=clean_plane_indices | sphere_indices | cylinder_indices,
|
||||
)
|
||||
|
||||
patches: list[DetectedPatch] = [
|
||||
*clean_plane_patches,
|
||||
*cylinder_patches,
|
||||
*sphere_patches,
|
||||
*normal_plane_patches,
|
||||
]
|
||||
|
||||
# primitives: list[tuple[Face, Shell]] = []
|
||||
primitives: list[Face] = []
|
||||
|
|
|
|||
|
|
@ -1426,7 +1426,10 @@ def test_detect_primitives_empty_sort_pair_path(monkeypatch):
|
|||
mesh_index = make_mesh_index(faces=[sample_face], samples=[], face_key_lookup={})
|
||||
|
||||
monkeypatch.setattr(bfs.MeshIndex, "from_shape", lambda _mesh: mesh_index)
|
||||
monkeypatch.setattr(bfs, "detect_planes", lambda *_args, **_kwargs: [])
|
||||
monkeypatch.setattr(bfs, "detect_planes_from_clean_proxy", lambda *_args: [])
|
||||
monkeypatch.setattr(
|
||||
bfs, "detect_planes_from_normals", lambda *_args, **_kwargs: []
|
||||
)
|
||||
monkeypatch.setattr(bfs, "detect_spheres", lambda *_args, **_kwargs: [])
|
||||
monkeypatch.setattr(bfs, "detect_cylinders", lambda *_args, **_kwargs: [])
|
||||
monkeypatch.setattr(bfs, "shapes_to_code", lambda primitives: [])
|
||||
|
|
@ -1436,3 +1439,88 @@ def test_detect_primitives_empty_sort_pair_path(monkeypatch):
|
|||
assert list(primitives) == []
|
||||
assert list(leftovers) == [sample_face]
|
||||
assert code_lines == []
|
||||
|
||||
|
||||
def test_detect_primitives_blocks_normal_planes_after_curved_patches(monkeypatch):
|
||||
mesh = SimpleNamespace()
|
||||
sample_faces = [Rectangle(1, 1).face() for _ in range(4)]
|
||||
mesh_index = make_mesh_index(faces=sample_faces, samples=[], face_key_lookup={})
|
||||
clean_patch = bfs.PlanePatch(
|
||||
kind="plane",
|
||||
face_indices=frozenset({0}),
|
||||
origin=Vector(0, 0, 0),
|
||||
normal=Vector(0, 0, 1),
|
||||
u_min=0.0,
|
||||
u_max=1.0,
|
||||
v_min=0.0,
|
||||
v_max=1.0,
|
||||
residual=0.0,
|
||||
)
|
||||
sphere_patch = bfs.SpherePatch(
|
||||
kind="sphere",
|
||||
face_indices=frozenset({1}),
|
||||
center=Vector(0, 0, 0),
|
||||
radius=1.0,
|
||||
residual=0.0,
|
||||
)
|
||||
cylinder_patch = bfs.CylinderPatch(
|
||||
kind="cylinder",
|
||||
face_indices=frozenset({2}),
|
||||
axis_point=Vector(0, 0, 0),
|
||||
axis_direction=Vector(0, 0, 1),
|
||||
radius=1.0,
|
||||
normal_sign=1,
|
||||
residual=0.0,
|
||||
)
|
||||
normal_patch = bfs.PlanePatch(
|
||||
kind="plane",
|
||||
face_indices=frozenset({3}),
|
||||
origin=Vector(0, 0, 0),
|
||||
normal=Vector(0, 0, 1),
|
||||
u_min=0.0,
|
||||
u_max=1.0,
|
||||
v_min=0.0,
|
||||
v_max=1.0,
|
||||
residual=0.0,
|
||||
)
|
||||
calls = []
|
||||
|
||||
def fake_detect_spheres(_mesh, _mesh_index, blocked_indices):
|
||||
calls.append(("spheres", blocked_indices))
|
||||
return [sphere_patch]
|
||||
|
||||
def fake_detect_cylinders(_mesh, _mesh_index, blocked_indices):
|
||||
calls.append(("cylinders", blocked_indices))
|
||||
return [cylinder_patch]
|
||||
|
||||
def fake_detect_planes_from_normals(_mesh, _mesh_index, blocked_indices):
|
||||
calls.append(("normal_planes", blocked_indices))
|
||||
return [normal_patch]
|
||||
|
||||
monkeypatch.setattr(bfs.MeshIndex, "from_shape", lambda _mesh: mesh_index)
|
||||
monkeypatch.setattr(
|
||||
bfs, "detect_planes_from_clean_proxy", lambda *_args: [clean_patch]
|
||||
)
|
||||
monkeypatch.setattr(bfs, "detect_spheres", fake_detect_spheres)
|
||||
monkeypatch.setattr(bfs, "detect_cylinders", fake_detect_cylinders)
|
||||
monkeypatch.setattr(
|
||||
bfs, "detect_planes_from_normals", fake_detect_planes_from_normals
|
||||
)
|
||||
monkeypatch.setattr(bfs, "build_plane_face", lambda _patch: sample_faces[0])
|
||||
monkeypatch.setattr(
|
||||
bfs, "build_cylinder_face", lambda _patch, _support_faces: sample_faces[0]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
bfs, "build_sphere_face", lambda _patch, _support_faces: sample_faces[0]
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
bfs, "shapes_to_code", lambda primitives: ["Rectangle(1, 1)"] * len(primitives)
|
||||
)
|
||||
|
||||
bfs.detect_primitives(mesh)
|
||||
|
||||
assert calls == [
|
||||
("spheres", {0}),
|
||||
("cylinders", {0, 1}),
|
||||
("normal_planes", {0, 1, 2}),
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue