- sketch enhacements
This commit is contained in:
@@ -1159,16 +1159,24 @@ class OCCSketch(SketchInterface):
|
||||
return loops
|
||||
|
||||
@staticmethod
|
||||
def _point_in_polygon(pt: Tuple[float, float], poly: List[Tuple[float, float]]) -> bool:
|
||||
def _point_in_polygon(
|
||||
pt: Tuple[float, float],
|
||||
poly: List[Tuple[float, float]],
|
||||
margin: float = 0.0,
|
||||
) -> bool:
|
||||
"""Ray-casting point-in-polygon test.
|
||||
|
||||
Returns *True* only for strictly interior points. Points on the
|
||||
boundary (within 1e-9) are considered *outside* so that the outer
|
||||
boundary of a nested shape doesn't falsely contain another loop whose
|
||||
representative point happens to land on that boundary.
|
||||
Returns *True* for points strictly inside the polygon. Points on
|
||||
the boundary (within eps=1e-9) are *outside* by default so the
|
||||
outer boundary of a nested shape doesn't falsely contain a hole's
|
||||
rep point. When *margin* > 0, points that are within that many
|
||||
world-unit of the boundary are also treated as inside — used by
|
||||
``_loop_contains`` to prevent float rounding from breaking
|
||||
thin-wall nesting detection.
|
||||
"""
|
||||
x, y = pt
|
||||
eps = 1e-9
|
||||
eps = 1e-9 # strict boundary rejection
|
||||
margin = float(margin)
|
||||
n = len(poly)
|
||||
inside = False
|
||||
j = n - 1
|
||||
@@ -1177,11 +1185,17 @@ class OCCSketch(SketchInterface):
|
||||
xj, yj = poly[j]
|
||||
# Point-on-segment test — exclude strict boundary hits.
|
||||
# First check bounding box of the segment.
|
||||
if min(xi, xj) - eps <= x <= max(xi, xj) + eps and min(yi, yj) - eps <= y <= max(yi, yj) + eps:
|
||||
bbox_tol = max(eps, margin)
|
||||
if min(xi, xj) - bbox_tol <= x <= max(xi, xj) + bbox_tol and min(yi, yj) - bbox_tol <= y <= max(yi, yj) + bbox_tol:
|
||||
# Check collinearity
|
||||
cross = (x - xi) * (yj - yi) - (y - yi) * (xj - xi)
|
||||
if abs(cross) < eps:
|
||||
return False # on boundary
|
||||
abs_cross = abs(cross)
|
||||
if abs_cross < eps:
|
||||
# Strictly on boundary — return False unless margin says otherwise.
|
||||
if margin > 0 and abs_cross < margin:
|
||||
pass # fall through to ray-cast below
|
||||
else:
|
||||
return False
|
||||
if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi + 1e-30) + xi):
|
||||
inside = not inside
|
||||
j = i
|
||||
@@ -1189,34 +1203,78 @@ class OCCSketch(SketchInterface):
|
||||
|
||||
@staticmethod
|
||||
def _loop_contains(inner: Dict[str, Any], outer: Dict[str, Any]) -> bool:
|
||||
"""Does ``outer`` fully enclose ``inner``? Uses a representative point +
|
||||
boundary tests on ``outer`` (only valid when ``outer`` != ``inner``)."""
|
||||
rep = OCCSketch._loop_rep_point(inner)
|
||||
if outer["type"] == "polygon":
|
||||
return OCCSketch._point_in_polygon(rep, outer["points"])
|
||||
else: # circle
|
||||
cx, cy = outer["center"]
|
||||
return math.hypot(rep[0] - cx, rep[1] - cy) < outer["radius"]
|
||||
"""Does ``outer`` fully enclose ``inner``?
|
||||
|
||||
For polygon-polygon: checks that ALL vertices of ``inner`` are strictly
|
||||
inside ``outer`` using ray-casting. This is robust for convex polygons
|
||||
and avoids the representative-point issue where a large nested loop's
|
||||
centroid lands inside an inner loop.
|
||||
|
||||
For circle-in-polygon: checks the circle centre is inside the polygon
|
||||
(vertex check would be too strict for tessellated arc segments).
|
||||
|
||||
For circle-in-circle: checks distance between centres + inner radius
|
||||
< outer radius + margin.
|
||||
|
||||
For polygon-in-circle: checks all polygon vertices are inside the
|
||||
circle.
|
||||
"""
|
||||
eps = 1e-3
|
||||
|
||||
if outer["type"] == "circle":
|
||||
ox, oy = outer["center"]
|
||||
orad = outer["radius"]
|
||||
if inner["type"] == "circle":
|
||||
# Two circles: centre distance + inner radius < outer radius
|
||||
dx = inner["center"][0] - ox
|
||||
dy = inner["center"][1] - oy
|
||||
return math.hypot(dx, dy) + inner["radius"] < orad + eps
|
||||
else:
|
||||
# Polygon in circle: all vertices inside
|
||||
pts = inner["points"]
|
||||
if len(pts) > 1 and pts[0] == pts[-1]:
|
||||
pts = pts[:-1]
|
||||
for pt in pts:
|
||||
if math.hypot(pt[0] - ox, pt[1] - oy) > orad - eps:
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
# outer is polygon
|
||||
if inner["type"] == "circle":
|
||||
# Circle in polygon: centre must be inside with margin
|
||||
cx, cy = inner["center"]
|
||||
return OCCSketch._point_in_polygon(
|
||||
(cx, cy), outer["points"], margin=1e-3
|
||||
)
|
||||
else:
|
||||
# Polygon in polygon: ALL inner vertices inside outer
|
||||
pts = inner["points"]
|
||||
if len(pts) > 1 and pts[0] == pts[-1]:
|
||||
pts = pts[:-1]
|
||||
for pt in pts:
|
||||
if not OCCSketch._point_in_polygon(
|
||||
pt, outer["points"], margin=eps
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _loop_rep_point(loop: Dict[str, Any]) -> Tuple[float, float]:
|
||||
"""An interior representative point inside a loop.
|
||||
|
||||
For polygons we use the midpoint between the centroid and the first
|
||||
vertex (而不是 centroid 本身): a nested shape centered on the polygon's
|
||||
centroid (e.g. a circle inside a rectangle, both centered on the same
|
||||
point) would otherwise make the polygon's rep point coincide with the
|
||||
hole and break containment tests. This midpoint stays inside convex
|
||||
loops and is unlikely to land on a nested feature's center.
|
||||
Only used for circle-in-polygon containment checks (polygon-in-polygon
|
||||
uses all-vertex containment). Returns the centroid for polygons and
|
||||
the centre for circles.
|
||||
"""
|
||||
if loop["type"] == "polygon":
|
||||
pts = loop["points"][:-1] if len(loop["points"]) > 1 and loop["points"][0] == loop["points"][-1] else loop["points"]
|
||||
n = len(pts)
|
||||
if n < 3:
|
||||
return loop.get("center", (0.0, 0.0))
|
||||
sx = sum(p[0] for p in pts) / n
|
||||
sy = sum(p[1] for p in pts) / n
|
||||
v0 = pts[0]
|
||||
return ((sx + v0[0]) / 2.0, (sy + v0[1]) / 2.0)
|
||||
return loop["center"]
|
||||
return (sx, sy)
|
||||
return loop.get("center", (0.0, 0.0))
|
||||
|
||||
@staticmethod
|
||||
def _loop_area(loop: Dict[str, Any]) -> float:
|
||||
@@ -1303,6 +1361,28 @@ class OCCSketch(SketchInterface):
|
||||
best = face
|
||||
return best
|
||||
|
||||
@staticmethod
|
||||
def _loop_signed_area(loop: Dict[str, Any]) -> float:
|
||||
"""Signed area of a loop. Positive = CCW, negative = CW.
|
||||
|
||||
Circles are treated as CCW (positive area) because
|
||||
``gp_Circ`` / ``gp_Ax2`` creates edges with CCW parametric
|
||||
direction when looking against the normal.
|
||||
"""
|
||||
if loop["type"] == "circle":
|
||||
r = loop.get("radius", 0.0)
|
||||
return math.pi * r * r # always positive (CCW)
|
||||
pts = loop["points"]
|
||||
if len(pts) < 3:
|
||||
return 0.0
|
||||
area = 0.0
|
||||
n = len(pts) - 1 # last point == first for closed loops
|
||||
for i in range(n):
|
||||
x1, y1 = pts[i]
|
||||
x2, y2 = pts[i + 1]
|
||||
area += x1 * y2 - x2 * y1
|
||||
return area / 2.0
|
||||
|
||||
def build_face_geometry(self, face: Dict[str, Any]) -> OCCGeometryObject:
|
||||
"""Build an OCC face (outer boundary + inner holes) on the workplane.
|
||||
|
||||
@@ -1311,6 +1391,13 @@ class OCCSketch(SketchInterface):
|
||||
sketch's 3D plane (not necessarily XY). The returned object stores
|
||||
the raw OCC face in ``.shape`` and the plane normal in
|
||||
``metadata["normal"]`` for the extrude kernel.
|
||||
|
||||
Hole wires are oriented to have OPPOSITE geometric winding relative
|
||||
to the outer wire, which is what OCC's face builder expects for
|
||||
proper hole treatment. Previous code unconditionally reversed ALL
|
||||
hole wires, which produced solid islands (not holes) whenever the
|
||||
outer loop had clockwise winding — e.g. after dragging a rectangle
|
||||
from top-left to bottom-right.
|
||||
"""
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace,
|
||||
@@ -1319,32 +1406,40 @@ class OCCSketch(SketchInterface):
|
||||
from OCP.gp import gp_Pnt, gp_Circ, gp_Ax2, gp_Dir
|
||||
from OCP.TopoDS import TopoDS as _TopoDS
|
||||
|
||||
def wire_loop(loop: Dict[str, Any], is_hole: bool = False):
|
||||
def _wire_from_loop(loop: Dict[str, Any]):
|
||||
"""Build a wire from a loop dict. No orientation adjustment."""
|
||||
if loop["type"] == "polygon":
|
||||
mp = BRepBuilderAPI_MakePolygon()
|
||||
for (pu, pv) in loop["points"]:
|
||||
mp.Add(self._uv_to_world(pu, pv))
|
||||
mp.Close()
|
||||
mp.Build()
|
||||
w = mp.Wire()
|
||||
else:
|
||||
cu, cv = loop["center"]
|
||||
r = loop["radius"]
|
||||
circ = gp_Circ(self._circle_axis(cu, cv), r)
|
||||
me = BRepBuilderAPI_MakeEdge(circ)
|
||||
me.Build()
|
||||
mw = BRepBuilderAPI_MakeWire()
|
||||
mw.Add(me.Edge())
|
||||
mw.Build()
|
||||
w = mw.Wire()
|
||||
if is_hole:
|
||||
w = _TopoDS.Wire_s(w.Reversed()) # reverse so OCC treats it as a hole
|
||||
return w
|
||||
return mp.Wire()
|
||||
cu, cv = loop["center"]
|
||||
r = loop["radius"]
|
||||
circ = gp_Circ(self._circle_axis(cu, cv), r)
|
||||
me = BRepBuilderAPI_MakeEdge(circ)
|
||||
me.Build()
|
||||
mw = BRepBuilderAPI_MakeWire()
|
||||
mw.Add(me.Edge())
|
||||
mw.Build()
|
||||
return mw.Wire()
|
||||
|
||||
outer_loop = face["outer"]
|
||||
outer_wire = _wire_from_loop(outer_loop)
|
||||
outer_winding = self._loop_signed_area(outer_loop)
|
||||
|
||||
outer_wire = wire_loop(face["outer"], is_hole=False)
|
||||
face_maker = BRepBuilderAPI_MakeFace(outer_wire, True)
|
||||
for h in face["holes"]:
|
||||
face_maker.Add(wire_loop(h, is_hole=True))
|
||||
hole_wire = _wire_from_loop(h)
|
||||
hole_winding = self._loop_signed_area(h)
|
||||
# OCC expects hole wires to have OPPOSITE winding to the outer
|
||||
# wire (material on the other side). We reverse the hole wire
|
||||
# only when its natural winding matches the outer's; if they
|
||||
# already differ the wire is left as-is.
|
||||
if (hole_winding >= 0 and outer_winding >= 0) or (hole_winding < 0 and outer_winding < 0):
|
||||
hole_wire = _TopoDS.Wire_s(hole_wire.Reversed())
|
||||
face_maker.Add(hole_wire)
|
||||
face_maker.Build()
|
||||
occ_face = face_maker.Face()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user