yapCAD 0.3.1__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/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 s[0] != 'surface' or len(s) not in (6,7):
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 s[0] != 'solid' or len(s) not in (4,5):
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 s in sld[1]:
657
- box = surfacebbox(s + box)
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