diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 6c902b5..ee38b33 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,8 +4,10 @@
-
+
+
+
@@ -334,7 +336,15 @@
1783272988957
-
+
+
+ 1783282570014
+
+
+
+ 1783282570014
+
+
@@ -378,6 +388,7 @@
-
+
+
\ No newline at end of file
diff --git a/src/fluency/ui/main_window.py b/src/fluency/ui/main_window.py
index 30fab5a..53b289c 100644
--- a/src/fluency/ui/main_window.py
+++ b/src/fluency/ui/main_window.py
@@ -80,66 +80,6 @@ from gui_ui import Ui_fluencyCAD # auto-generated Qt form (project root on sys.
logger = logging.getLogger(__name__)
-def _project_face_to_uv(
- face: Any,
- workplane: Tuple[Tuple[float, float, float], Tuple[float, float, float], Tuple[float, float, float]],
-) -> List[List[Tuple[float, float]]]:
- """Project a planar ``TopoDS_Face``'s boundary edges into the UV frame.
-
- *workplane* is (origin, normal, x_dir). Returns a list of polylines,
- each a list of (u, v) points, one per boundary edge (lines → endpoints,
- curves → sampled). Used by the 2D sketch widget to draw the face as an
- underlay when sketching on a surface.
- """
- import numpy as np
- from OCP.TopExp import TopExp_Explorer
- from OCP.TopAbs import TopAbs_EDGE, TopAbs_WIRE
- from OCP.TopoDS import TopoDS
- from OCP.BRepAdaptor import BRepAdaptor_Curve
- from OCP.GeomAbs import GeomAbs_Line
- from OCP.gp import gp_Pnt
-
- origin = np.asarray(workplane[0], dtype=float) # (x,y,z)
- normal = np.asarray(workplane[1], dtype=float) # plane normal
- x_dir = np.asarray(workplane[2], dtype=float) # in-plane x axis
- x_dir = x_dir / np.linalg.norm(x_dir)
- normal = normal / np.linalg.norm(normal)
- y_dir = np.cross(normal, x_dir)
- y_dir = y_dir / np.linalg.norm(y_dir)
-
- def world_to_uv(p: gp_Pnt) -> Tuple[float, float]:
- v = np.array([p.X() - origin[0], p.Y() - origin[1], p.Z() - origin[2]])
- return (float(np.dot(v, x_dir)), float(np.dot(v, y_dir)))
-
- polylines: List[List[Tuple[float, float]]] = []
-
- # Iterate wires of the face (outer + inner = holes), then edges.
- wire_expl = TopExp_Explorer(face, TopAbs_WIRE)
- while wire_expl.More():
- wire = wire_expl.Current()
- edge_expl = TopExp_Explorer(wire, TopAbs_EDGE)
- while edge_expl.More():
- edge = TopoDS.Edge_s(edge_expl.Current())
- try:
- crv = BRepAdaptor_Curve(edge)
- f = crv.FirstParameter()
- l = crv.LastParameter()
- is_line = crv.GetType() == GeomAbs_Line
- if is_line:
- pts = [crv.Value(f), crv.Value(l)]
- else:
- # Sample 32 segments across the parameter range.
- pts = [crv.Value(f + (l - f) * i / 32.0) for i in range(33)]
- poly = [world_to_uv(p) for p in pts]
- polylines.append(poly)
- except Exception:
- pass
- edge_expl.Next()
- wire_expl.Next()
-
- return polylines
-
-
def _project_body_to_workplane(
body_shape: Any,
workplane: Tuple[Tuple[float, float, float], Tuple[float, float, float], Tuple[float, float, float]],
diff --git a/src/fluency/ui/sketch_widget.py b/src/fluency/ui/sketch_widget.py
index 6ba7d80..a651c3a 100644
--- a/src/fluency/ui/sketch_widget.py
+++ b/src/fluency/ui/sketch_widget.py
@@ -28,6 +28,66 @@ from fluency.geometry_occ.sketch import OCCSketch, OCCSketchEntity
logger = logging.getLogger(__name__)
+def _project_face_to_uv(
+ face: Any,
+ workplane: Tuple[Tuple[float, float, float], Tuple[float, float, float], Tuple[float, float, float]],
+) -> List[List[Tuple[float, float]]]:
+ """Project a planar ``TopoDS_Face``'s boundary edges into the UV frame.
+
+ *workplane* is (origin, normal, x_dir). Returns a list of polylines,
+ each a list of (u, v) points, one per boundary edge (lines \u2192 endpoints,
+ curves \u2192 sampled). Used by the 2D sketch widget to draw the face as an
+ underlay when sketching on a surface.
+ """
+ import numpy as np
+ from OCP.TopExp import TopExp_Explorer
+ from OCP.TopAbs import TopAbs_EDGE, TopAbs_WIRE
+ from OCP.TopoDS import TopoDS
+ from OCP.BRepAdaptor import BRepAdaptor_Curve
+ from OCP.GeomAbs import GeomAbs_Line
+ from OCP.gp import gp_Pnt
+
+ origin = np.asarray(workplane[0], dtype=float) # (x,y,z)
+ normal = np.asarray(workplane[1], dtype=float) # plane normal
+ x_dir = np.asarray(workplane[2], dtype=float) # in-plane x axis
+ x_dir = x_dir / np.linalg.norm(x_dir)
+ normal = normal / np.linalg.norm(normal)
+ y_dir = np.cross(normal, x_dir)
+ y_dir = y_dir / np.linalg.norm(y_dir)
+
+ def world_to_uv(p: gp_Pnt) -> Tuple[float, float]:
+ v = np.array([p.X() - origin[0], p.Y() - origin[1], p.Z() - origin[2]])
+ return (float(np.dot(v, x_dir)), float(np.dot(v, y_dir)))
+
+ polylines: List[List[Tuple[float, float]]] = []
+
+ # Iterate wires of the face (outer + inner = holes), then edges.
+ wire_expl = TopExp_Explorer(face, TopAbs_WIRE)
+ while wire_expl.More():
+ wire = wire_expl.Current()
+ edge_expl = TopExp_Explorer(wire, TopAbs_EDGE)
+ while edge_expl.More():
+ edge = TopoDS.Edge_s(edge_expl.Current())
+ try:
+ crv = BRepAdaptor_Curve(edge)
+ f = crv.FirstParameter()
+ l = crv.LastParameter()
+ is_line = crv.GetType() == GeomAbs_Line
+ if is_line:
+ pts = [crv.Value(f), crv.Value(l)]
+ else:
+ # Sample 32 segments across the parameter range.
+ pts = [crv.Value(f + (l - f) * i / 32.0) for i in range(33)]
+ poly = [world_to_uv(p) for p in pts]
+ polylines.append(poly)
+ except Exception:
+ pass
+ edge_expl.Next()
+ wire_expl.Next()
+
+ return polylines
+
+
class Sketch2DWidget(QWidget):
"""2D sketching widget with SolveSpace constraint solving and drawing tools."""