- Tons of addtions

This commit is contained in:
bklronin
2026-06-28 22:51:52 +02:00
parent f8f16ea800
commit f6422e0847
7 changed files with 1010 additions and 149 deletions
+111 -36
View File
@@ -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: