- Tons of addtions
This commit is contained in:
@@ -64,6 +64,78 @@ class OCCSketch(SketchInterface):
|
||||
# Track first point as dragged/fixed for solver stability
|
||||
self._first_point_id: Optional[int] = None
|
||||
|
||||
# ── Workplane ───────────────────────────────────────────────────
|
||||
# The sketch lives in a 2D UV frame on this plane. UV coordinates
|
||||
# map to world via: P = origin + u*x_dir + v*y_dir
|
||||
# where y_dir = normal × x_dir. Defaults to the world XY plane so
|
||||
# existing XY-only behaviour is unchanged.
|
||||
self._wp_origin: Tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
self._wp_normal: Tuple[float, float, float] = (0.0, 0.0, 1.0)
|
||||
self._wp_x_dir: Tuple[float, float, float] = (1.0, 0.0, 0.0)
|
||||
self._wp_y_dir: Tuple[float, float, float] = (0.0, 1.0, 0.0)
|
||||
|
||||
# ─── Workplane management ──────────────────────────────────────────────
|
||||
|
||||
def set_workplane(
|
||||
self,
|
||||
origin: Tuple[float, float, float],
|
||||
normal: Tuple[float, float, float],
|
||||
x_dir: Tuple[float, float, float],
|
||||
) -> None:
|
||||
"""Place this sketch on an arbitrary plane in 3D.
|
||||
|
||||
*normal* and *x_dir* need not be unit/perpendicular — they are
|
||||
orthonormalised here. ``y_dir`` is derived as ``normal × x_dir``.
|
||||
Existing UV coordinates are unchanged; only their world mapping moves.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
n = np.asarray(normal, dtype=float)
|
||||
x = np.asarray(x_dir, dtype=float)
|
||||
n = n / np.linalg.norm(n)
|
||||
# Remove any component of x along n, then renormalise.
|
||||
x = x - np.dot(x, n) * n
|
||||
x_norm = np.linalg.norm(x)
|
||||
if x_norm < 1e-9:
|
||||
# x_dir is parallel to normal — pick any orthogonal basis vector.
|
||||
fallback = np.array([1.0, 0.0, 0.0]) if abs(n[0]) < 0.9 else np.array([0.0, 1.0, 0.0])
|
||||
x = fallback - np.dot(fallback, n) * n
|
||||
x_norm = np.linalg.norm(x)
|
||||
x = x / x_norm
|
||||
y = np.cross(n, x)
|
||||
y = y / np.linalg.norm(y)
|
||||
|
||||
self._wp_origin = tuple(float(v) for v in origin)
|
||||
self._wp_normal = tuple(float(v) for v in n)
|
||||
self._wp_x_dir = tuple(float(v) for v in x)
|
||||
self._wp_y_dir = tuple(float(v) for v in y)
|
||||
|
||||
def get_workplane(self) -> Tuple[Tuple[float, float, float], ...]:
|
||||
"""Return the (origin, normal, x_dir, y_dir) of this sketch's plane."""
|
||||
return (self._wp_origin, self._wp_normal, self._wp_x_dir, self._wp_y_dir)
|
||||
|
||||
def _uv_to_world(self, u: float, v: float):
|
||||
"""Map a UV point to a world ``gp_Pnt`` on the workplane."""
|
||||
from OCP.gp import gp_Pnt
|
||||
ox, oy, oz = self._wp_origin
|
||||
xx, xy, xz = self._wp_x_dir
|
||||
yx, yy, yz = self._wp_y_dir
|
||||
return gp_Pnt(
|
||||
ox + u * xx + v * yx,
|
||||
oy + u * xy + v * yy,
|
||||
oz + u * xz + v * yz,
|
||||
)
|
||||
|
||||
def _circle_axis(self, u: float, v: float):
|
||||
"""Return a ``gp_Ax2`` for a circle centred at UV on the workplane."""
|
||||
from OCP.gp import gp_Ax2, gp_Dir
|
||||
center = self._uv_to_world(u, v)
|
||||
return gp_Ax2(
|
||||
center,
|
||||
gp_Dir(*self._wp_normal),
|
||||
gp_Dir(*self._wp_x_dir),
|
||||
)
|
||||
|
||||
@property
|
||||
def solver(self) -> SolverSystem:
|
||||
"""Access the underlying SolveSpace solver."""
|
||||
@@ -71,7 +143,13 @@ class OCCSketch(SketchInterface):
|
||||
|
||||
@property
|
||||
def workplane(self) -> Any:
|
||||
"""Get the solver workplane entity."""
|
||||
"""Get the SolveSpace 2D solver workplane entity.
|
||||
|
||||
Note: this is the solver's internal 2D base, not the 3D placement
|
||||
plane — see :meth:`set_workplane` / :meth:`workplane` (no underscore)
|
||||
for the 3D plane. The solver always runs in UV regardless of the
|
||||
3D placement.
|
||||
"""
|
||||
return self._wp
|
||||
|
||||
def _next_id(self) -> int:
|
||||
@@ -576,42 +654,39 @@ class OCCSketch(SketchInterface):
|
||||
# ─── Geometry extraction for operations ────────────────────────────────
|
||||
|
||||
def get_geometry(self) -> GeometryObject:
|
||||
"""Get the solved geometry for operations using CadQuery.
|
||||
"""Get the solved geometry as an OCC ``TopoDS_Face`` on the workplane.
|
||||
|
||||
If the sketch has exactly one detected face (outer boundary + optional
|
||||
holes) that face is returned as a combined face-with-holes Workplane.
|
||||
Otherwise falls back to returning a single circle or polygon suitable
|
||||
for extrude/revolve.
|
||||
holes) that face is returned. Otherwise falls back to returning a
|
||||
single circle or polygon as a face. The returned object carries the
|
||||
workplane normal in ``metadata["normal"]`` so the kernel can extrude
|
||||
along the plane normal (not a hardcoded +Z).
|
||||
"""
|
||||
import cadquery as cq
|
||||
|
||||
faces = self.detect_faces()
|
||||
if len(faces) == 1:
|
||||
return self.build_face_geometry(faces[0])
|
||||
|
||||
# Fallback: return the first circle, or a polygon, or None.
|
||||
# Fallback: wrap the first circle, or the polygon, as a single-loop face.
|
||||
if self._circles:
|
||||
for entity_id, (center_id, radius) in self._circles.items():
|
||||
center_entity = self._entities.get(center_id)
|
||||
if center_entity and center_entity.geometry:
|
||||
cx, cy = center_entity.geometry
|
||||
wp = cq.Workplane("XY").center(cx, cy).circle(radius)
|
||||
obj = OCCGeometryObject(wp.val())
|
||||
obj._cadquery_obj = wp
|
||||
return obj
|
||||
face_dict = {
|
||||
"outer": {"type": "circle", "center": (cx, cy), "radius": radius},
|
||||
"holes": [],
|
||||
}
|
||||
return self.build_face_geometry(face_dict)
|
||||
|
||||
points = self.get_polygon_points()
|
||||
if not points:
|
||||
return OCCGeometryObject(None)
|
||||
|
||||
wp = cq.Workplane("XY").moveTo(points[0].x, points[0].y)
|
||||
for pt in points[1:]:
|
||||
wp = wp.lineTo(pt.x, pt.y)
|
||||
wp = wp.close()
|
||||
|
||||
obj = OCCGeometryObject(wp.val())
|
||||
obj._cadquery_obj = wp
|
||||
return obj
|
||||
face_dict = {
|
||||
"outer": {"type": "polygon", "points": [(p.x, p.y) for p in points]},
|
||||
"holes": [],
|
||||
}
|
||||
return self.build_face_geometry(face_dict)
|
||||
|
||||
def get_points(self) -> List[Point2D]:
|
||||
"""Get all point positions from solved solver data."""
|
||||
@@ -910,34 +985,33 @@ class OCCSketch(SketchInterface):
|
||||
return best
|
||||
|
||||
def build_face_geometry(self, face: Dict[str, Any]) -> OCCGeometryObject:
|
||||
"""Build an OCC face (outer boundary + inner holes) wrapped in a Workplane.
|
||||
"""Build an OCC face (outer boundary + inner holes) on the workplane.
|
||||
|
||||
The returned object feeds ``OCGeometryKernel.extrude`` directly: its
|
||||
``_cadquery_obj`` is a Workplane whose stack holds the face, so cadquery's
|
||||
``Workplane.extrude`` lifts it into a solid — inner wires become
|
||||
through-holes.
|
||||
Wires are constructed from UV coordinates mapped through
|
||||
:meth:`_uv_to_world`, so the resulting ``TopoDS_Face`` lies on this
|
||||
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.
|
||||
"""
|
||||
import cadquery as cq
|
||||
from OCP.BRepBuilderAPI import (
|
||||
BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace,
|
||||
BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeEdge,
|
||||
)
|
||||
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):
|
||||
if loop["type"] == "polygon":
|
||||
mp = BRepBuilderAPI_MakePolygon()
|
||||
for (px, py) in loop["points"]:
|
||||
mp.Add(gp_Pnt(px, py, 0.0))
|
||||
for (pu, pv) in loop["points"]:
|
||||
mp.Add(self._uv_to_world(pu, pv))
|
||||
mp.Close()
|
||||
mp.Build()
|
||||
w = mp.Wire()
|
||||
else:
|
||||
cx, cy = loop["center"]
|
||||
cu, cv = loop["center"]
|
||||
r = loop["radius"]
|
||||
circ = gp_Circ(gp_Ax2(gp_Pnt(cx, cy, 0.0), gp_Dir(0, 0, 1)), r)
|
||||
circ = gp_Circ(self._circle_axis(cu, cv), r)
|
||||
me = BRepBuilderAPI_MakeEdge(circ)
|
||||
me.Build()
|
||||
mw = BRepBuilderAPI_MakeWire()
|
||||
@@ -945,7 +1019,7 @@ class OCCSketch(SketchInterface):
|
||||
mw.Build()
|
||||
w = mw.Wire()
|
||||
if is_hole:
|
||||
w = _TopoDS.Wire_s(w.Reversed()) # reverse orientation so OCC treats it as a hole
|
||||
w = _TopoDS.Wire_s(w.Reversed()) # reverse so OCC treats it as a hole
|
||||
return w
|
||||
|
||||
outer_wire = wire_loop(face["outer"], is_hole=False)
|
||||
@@ -955,10 +1029,11 @@ class OCCSketch(SketchInterface):
|
||||
face_maker.Build()
|
||||
occ_face = face_maker.Face()
|
||||
|
||||
wp = cq.Workplane("XY")
|
||||
wp = wp.add(cq.Face(occ_face))
|
||||
obj = OCCGeometryObject(wp.val())
|
||||
obj._cadquery_obj = wp
|
||||
obj = OCCGeometryObject(occ_face, {
|
||||
"type": "sketch_face",
|
||||
"normal": self._wp_normal,
|
||||
"origin": self._wp_origin,
|
||||
})
|
||||
return obj
|
||||
|
||||
def get_solver_dof(self) -> int:
|
||||
|
||||
Reference in New Issue
Block a user