yapCAD 0.5.0__py2.py3-none-any.whl → 0.5.1__py2.py3-none-any.whl
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.
- yapcad/boolean/__init__.py +21 -0
- yapcad/boolean/native.py +1012 -0
- yapcad/boolean/trimesh_engine.py +155 -0
- yapcad/combine.py +52 -14
- yapcad/drawable.py +404 -26
- yapcad/geom.py +116 -0
- yapcad/geom3d.py +237 -7
- yapcad/geom3d_util.py +486 -30
- yapcad/geom_util.py +160 -61
- yapcad/io/__init__.py +2 -1
- yapcad/io/step.py +323 -0
- yapcad/spline.py +232 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/METADATA +60 -14
- yapcad-0.5.1.dist-info/RECORD +32 -0
- yapcad-0.5.0.dist-info/RECORD +0 -27
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/WHEEL +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/AUTHORS.rst +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/licenses/LICENSE.txt +0 -0
- {yapcad-0.5.0.dist-info → yapcad-0.5.1.dist-info}/top_level.txt +0 -0
yapcad/geom3d.py
CHANGED
@@ -6,7 +6,21 @@ from yapcad.geom import *
|
|
6
6
|
from yapcad.geom_util import *
|
7
7
|
from yapcad.xform import *
|
8
8
|
from functools import reduce
|
9
|
+
import os
|
10
|
+
from yapcad.octtree import NTree
|
11
|
+
|
9
12
|
from yapcad.triangulator import triangulate_polygon
|
13
|
+
import yapcad.boolean.native as _boolean_native
|
14
|
+
_DEFAULT_RAY_TOL = _boolean_native._DEFAULT_RAY_TOL
|
15
|
+
invalidate_surface_octree = _boolean_native.invalidate_surface_octree
|
16
|
+
surface_octree = _boolean_native.surface_octree
|
17
|
+
_surface_from_triangles = _boolean_native._surface_from_triangles
|
18
|
+
_iter_triangles_from_surface = _boolean_native._iter_triangles_from_surface
|
19
|
+
_iter_triangles_from_solid = _boolean_native._iter_triangles_from_solid
|
20
|
+
stitch_open_edges = _boolean_native.stitch_open_edges
|
21
|
+
stitch_solid = _boolean_native.stitch_solid
|
22
|
+
solid_contains_point = _boolean_native.solid_contains_point
|
23
|
+
solids_intersect = _boolean_native.solids_intersect
|
10
24
|
|
11
25
|
|
12
26
|
"""
|
@@ -125,7 +139,7 @@ function call with parameters for algorithmically-generated geometry.
|
|
125
139
|
|
126
140
|
``solid = ['solid', surfaces, material, construction ]``, where:
|
127
141
|
|
128
|
-
``surfaces`` is a list of surfaces with contiguous boundaries
|
142
|
+
``surfaces`` is a list of surfaces with contiguous boundaries
|
129
143
|
that completely encloses an interior space,
|
130
144
|
|
131
145
|
``material`` is a list of domain-specific representation of
|
@@ -136,6 +150,30 @@ function call with parameters for algorithmically-generated geometry.
|
|
136
150
|
``construction`` is a list that contains information about
|
137
151
|
how the solid was constructed, and may be empty
|
138
152
|
|
153
|
+
Topology Analysis
|
154
|
+
-----------------
|
155
|
+
|
156
|
+
Two key functions are provided for analyzing solid topology:
|
157
|
+
|
158
|
+
``issolidclosed(solid)`` -- Verifies that a solid is topologically closed
|
159
|
+
by ensuring every edge is shared by exactly two faces across all surfaces.
|
160
|
+
This is essential for determining if a solid properly encloses a volume
|
161
|
+
without gaps or holes. Returns True if closed, False otherwise.
|
162
|
+
|
163
|
+
``volumeof(solid)`` -- Calculates the volume enclosed by a closed solid
|
164
|
+
using the divergence theorem. Requires that the solid be topologically
|
165
|
+
closed (verified by calling ``issolidclosed()``). Returns the volume
|
166
|
+
as a non-negative floating point number.
|
167
|
+
|
168
|
+
Example usage::
|
169
|
+
|
170
|
+
from yapcad.geom3d_util import prism
|
171
|
+
from yapcad.geom3d import issolidclosed, volumeof
|
172
|
+
|
173
|
+
cube = prism(2, 2, 2)
|
174
|
+
if issolidclosed(cube):
|
175
|
+
vol = volumeof(cube) # Returns 8.0
|
176
|
+
|
139
177
|
|
140
178
|
Assembly
|
141
179
|
--------
|
@@ -475,7 +513,25 @@ def surfacebbox(s):
|
|
475
513
|
if not issurface(s):
|
476
514
|
raise ValueError('bad surface passed to surfacebbox')
|
477
515
|
return polybbox(s[1])
|
478
|
-
|
516
|
+
|
517
|
+
|
518
|
+
|
519
|
+
|
520
|
+
def solid_boolean(a, b, operation, tol=_DEFAULT_RAY_TOL, *, stitch=False, engine=None):
|
521
|
+
selected_raw = engine or os.environ.get('YAPCAD_BOOLEAN_ENGINE', 'native')
|
522
|
+
backend = None
|
523
|
+
if selected_raw and ':' in selected_raw:
|
524
|
+
selected, backend = selected_raw.split(':', 1)
|
525
|
+
else:
|
526
|
+
selected = selected_raw
|
527
|
+
if selected == 'native':
|
528
|
+
return _boolean_native.solid_boolean(a, b, operation, tol=tol, stitch=stitch)
|
529
|
+
if selected == 'trimesh':
|
530
|
+
from yapcad.boolean import trimesh_engine
|
531
|
+
backend = backend or os.environ.get('YAPCAD_TRIMESH_BACKEND')
|
532
|
+
return trimesh_engine.solid_boolean(a, b, operation, tol=tol, stitch=stitch, backend=backend)
|
533
|
+
raise ValueError(f'unknown boolean engine {selected_raw!r}')
|
534
|
+
|
479
535
|
def issurface(s,fast=True):
|
480
536
|
"""
|
481
537
|
Check to see if ``s`` is a valid surface.
|
@@ -487,8 +543,8 @@ def issurface(s,fast=True):
|
|
487
543
|
return (len(list(filter(lambda x: not (isinstance(x,int) or
|
488
544
|
x < 0 or x >= l),
|
489
545
|
inds))) == 0)
|
490
|
-
|
491
|
-
if not isinstance(s,list) or
|
546
|
+
|
547
|
+
if not isinstance(s,list) or len(s) not in (6,7) or s[0] != 'surface':
|
492
548
|
return False
|
493
549
|
if fast:
|
494
550
|
return True
|
@@ -633,7 +689,7 @@ def issolid(s,fast=True):
|
|
633
689
|
of surfaces completely bounds a volume of space without holes
|
634
690
|
"""
|
635
691
|
|
636
|
-
if not isinstance(s,list) or
|
692
|
+
if not isinstance(s,list) or len(s) not in (4,5) or s[0] != 'solid':
|
637
693
|
return False
|
638
694
|
if fast:
|
639
695
|
return True
|
@@ -653,8 +709,17 @@ def solidbbox(sld):
|
|
653
709
|
raise ValueError('bad argument to solidbbox')
|
654
710
|
|
655
711
|
box = []
|
656
|
-
for
|
657
|
-
|
712
|
+
for surf in sld[1]:
|
713
|
+
sb = surfacebbox(surf)
|
714
|
+
if not box:
|
715
|
+
box = sb
|
716
|
+
else:
|
717
|
+
box = [point(min(box[0][0], sb[0][0]),
|
718
|
+
min(box[0][1], sb[0][1]),
|
719
|
+
min(box[0][2], sb[0][2])),
|
720
|
+
point(max(box[1][0], sb[1][0]),
|
721
|
+
max(box[1][1], sb[1][1]),
|
722
|
+
max(box[1][2], sb[1][2]))]
|
658
723
|
|
659
724
|
return box
|
660
725
|
|
@@ -691,6 +756,171 @@ def mirrorsolid(x,plane,preserveNormal=True):
|
|
691
756
|
s2[1] = surfs
|
692
757
|
return s2
|
693
758
|
|
759
|
+
def _point_to_key(p):
|
760
|
+
"""
|
761
|
+
Convert a point to a hashable key for edge/vertex identification.
|
762
|
+
Uses rounded coordinates to handle floating point precision issues.
|
763
|
+
"""
|
764
|
+
# Round to a reasonable precision to handle floating point comparison
|
765
|
+
return (round(p[0] / epsilon) * epsilon,
|
766
|
+
round(p[1] / epsilon) * epsilon,
|
767
|
+
round(p[2] / epsilon) * epsilon)
|
768
|
+
|
769
|
+
def _canonical_edge_key(p1, p2):
|
770
|
+
"""
|
771
|
+
Create a canonical edge key from two points.
|
772
|
+
Returns tuple of keys in sorted order so (p1,p2) and (p2,p1) map to same edge.
|
773
|
+
"""
|
774
|
+
k1 = _point_to_key(p1)
|
775
|
+
k2 = _point_to_key(p2)
|
776
|
+
return (min(k1, k2), max(k1, k2))
|
777
|
+
|
778
|
+
def issolidclosed(x):
|
779
|
+
"""
|
780
|
+
Check if solid x is topologically closed.
|
781
|
+
|
782
|
+
A solid is closed if and only if every edge is shared by exactly two
|
783
|
+
faces across all surfaces. This ensures no holes or gaps exist in the
|
784
|
+
solid's boundary.
|
785
|
+
|
786
|
+
The function analyzes face adjacency by:
|
787
|
+
1. Building a global edge map using vertex positions (not indices)
|
788
|
+
2. Counting how many faces share each edge
|
789
|
+
3. Verifying that every edge is shared by exactly 2 faces
|
790
|
+
|
791
|
+
Args:
|
792
|
+
x: A solid data structure
|
793
|
+
|
794
|
+
Returns:
|
795
|
+
True if the solid is topologically closed, False otherwise
|
796
|
+
|
797
|
+
Raises:
|
798
|
+
ValueError: if x is not a valid solid
|
799
|
+
|
800
|
+
Example:
|
801
|
+
>>> from yapcad.geom3d_util import prism, sphere
|
802
|
+
>>> cube = prism(2, 2, 2)
|
803
|
+
>>> issolidclosed(cube)
|
804
|
+
True
|
805
|
+
"""
|
806
|
+
# First verify this is a valid solid
|
807
|
+
if not issolid(x, fast=False):
|
808
|
+
raise ValueError('invalid solid passed to issolidclosed')
|
809
|
+
|
810
|
+
surfaces = x[1]
|
811
|
+
|
812
|
+
# Empty solid is trivially closed
|
813
|
+
if not surfaces:
|
814
|
+
return True
|
815
|
+
|
816
|
+
# Build a global edge map across all surfaces
|
817
|
+
# Key: canonical edge tuple (point_key1, point_key2)
|
818
|
+
# Value: count of faces that share this edge
|
819
|
+
global_edge_count = {}
|
820
|
+
|
821
|
+
for surf_idx, surf in enumerate(surfaces):
|
822
|
+
faces = surf[3]
|
823
|
+
vertices = surf[1]
|
824
|
+
|
825
|
+
# Process each face in this surface
|
826
|
+
for face_idx, face in enumerate(faces):
|
827
|
+
if len(face) != 3:
|
828
|
+
raise ValueError(f'non-triangular face in surface {surf_idx}, face {face_idx}')
|
829
|
+
|
830
|
+
# Get the three vertex positions
|
831
|
+
p0 = vertices[face[0]]
|
832
|
+
p1 = vertices[face[1]]
|
833
|
+
p2 = vertices[face[2]]
|
834
|
+
|
835
|
+
# Extract the three edges of this triangular face
|
836
|
+
edges = [
|
837
|
+
_canonical_edge_key(p0, p1),
|
838
|
+
_canonical_edge_key(p1, p2),
|
839
|
+
_canonical_edge_key(p2, p0)
|
840
|
+
]
|
841
|
+
|
842
|
+
# Count this face's contribution to each edge
|
843
|
+
for edge in edges:
|
844
|
+
if edge not in global_edge_count:
|
845
|
+
global_edge_count[edge] = 0
|
846
|
+
global_edge_count[edge] += 1
|
847
|
+
|
848
|
+
# Check that every edge is shared by exactly 2 faces
|
849
|
+
for edge, count in global_edge_count.items():
|
850
|
+
if count != 2:
|
851
|
+
return False
|
852
|
+
|
853
|
+
return True
|
854
|
+
|
855
|
+
def volumeof(x):
|
856
|
+
"""
|
857
|
+
Calculate the volume enclosed by a solid.
|
858
|
+
|
859
|
+
Uses the divergence theorem to compute volume from the surface triangulation.
|
860
|
+
For each triangular face with vertices (p0, p1, p2), the signed volume
|
861
|
+
contribution is: V_i = (1/6) * dot(p0, cross(p1-p0, p2-p0))
|
862
|
+
|
863
|
+
The total volume is the sum of absolute values of all face contributions.
|
864
|
+
|
865
|
+
Args:
|
866
|
+
x: A solid data structure (must be topologically closed)
|
867
|
+
|
868
|
+
Returns:
|
869
|
+
float: The volume of the solid (always non-negative)
|
870
|
+
|
871
|
+
Raises:
|
872
|
+
ValueError: if x is not a valid solid or is not closed
|
873
|
+
|
874
|
+
Example:
|
875
|
+
>>> from yapcad.geom3d_util import prism
|
876
|
+
>>> cube = prism(2, 2, 2)
|
877
|
+
>>> abs(volumeof(cube) - 8.0) < 0.001
|
878
|
+
True
|
879
|
+
"""
|
880
|
+
# Verify this is a valid, closed solid
|
881
|
+
if not issolid(x, fast=False):
|
882
|
+
raise ValueError('invalid solid passed to volumeof')
|
883
|
+
|
884
|
+
if not issolidclosed(x):
|
885
|
+
raise ValueError('solid must be topologically closed to compute volume')
|
886
|
+
|
887
|
+
surfaces = x[1]
|
888
|
+
|
889
|
+
# Handle empty solid
|
890
|
+
if not surfaces:
|
891
|
+
return 0.0
|
892
|
+
|
893
|
+
total_volume = 0.0
|
894
|
+
|
895
|
+
# Accumulate signed volume contributions from all faces
|
896
|
+
for surf in surfaces:
|
897
|
+
vertices = surf[1]
|
898
|
+
faces = surf[3]
|
899
|
+
|
900
|
+
for face in faces:
|
901
|
+
if len(face) != 3:
|
902
|
+
raise ValueError('non-triangular face encountered')
|
903
|
+
|
904
|
+
# Get the three vertices of this face and normalise to w=1 if needed
|
905
|
+
p0 = point(vertices[face[0]])
|
906
|
+
p1 = point(vertices[face[1]])
|
907
|
+
p2 = point(vertices[face[2]])
|
908
|
+
|
909
|
+
# Compute vectors from p0 to other vertices
|
910
|
+
v1 = sub(p1, p0)
|
911
|
+
v2 = sub(p2, p0)
|
912
|
+
|
913
|
+
# Signed volume contribution: (1/6) * dot(p0, cross(v1, v2))
|
914
|
+
# This is the volume of the tetrahedron formed by the origin
|
915
|
+
# and the three face vertices
|
916
|
+
cross_product = cross(v1, v2)
|
917
|
+
signed_volume = dot(p0, cross_product) / 6.0
|
918
|
+
|
919
|
+
total_volume += signed_volume
|
920
|
+
|
921
|
+
# Return absolute value (orientation might cause negative result)
|
922
|
+
return abs(total_volume)
|
923
|
+
|
694
924
|
def normfunc(tri):
|
695
925
|
"""
|
696
926
|
utility funtion to compute normals for a flat facet triangle
|