- Improved sketching

This commit is contained in:
bklronin
2026-06-14 10:10:33 +02:00
parent 7091f530ee
commit ea34e7e29d
8 changed files with 1413 additions and 521 deletions
-11
View File
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/sdfcad" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12 (fluency)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+1 -1
View File
@@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/fluency.iml" filepath="$PROJECT_DIR$/.idea/fluency.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/fluency-cad.iml" filepath="$PROJECT_DIR$/.idea/fluency-cad.iml" />
</modules>
</component>
</project>
+35 -62
View File
@@ -5,30 +5,14 @@
</component>
<component name="ChangeListManager">
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- Improved sketching">
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/BUGFIX_DRAWING_ISSUES.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/IMPLEMENTATION_SUMMARY.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/PHASE1_COMPLETE.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/PHASE2_COMPLETE.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/SELECTION_DELETION.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/SINGLE_WINDOW_MIGRATION_PLAN.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/TEST_REPORT_PHASE1.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/changes_log.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/current_state.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/doc/technical_documentation.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/drawing_modules/constraints_3d.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/drawing_modules/vtk_sketch_widget.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/drawing_modules/workplane.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/minimal_main.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ouput_test.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/state.png" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_delete_functionality.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_minimal_vtk.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_projection.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_selection.py" afterDir="false" />
<change afterPath="$PROJECT_DIR$/test_vtk_sketch_widget.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/fluency.iml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/geometry_occ/kernel.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/geometry_occ/kernel.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/geometry_occ/sketch.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/geometry_occ/sketch.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/fluency/main.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/main.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -45,7 +29,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="master" />
<entry key="$PROJECT_DIR$" value="structure" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -65,32 +49,31 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python.2dtest.executor": "Run",
"Python.3d_windows.executor": "Run",
"Python.Unnamed.executor": "Run",
"Python.draw_widget2d.executor": "Run",
"Python.draw_widget_solve.executor": "Run",
"Python.fluency.executor": "Run",
"Python.fluencyb.executor": "Run",
"Python.gl_widget.executor": "Run",
"Python.main.executor": "Run",
"Python.meshtest.executor": "Run",
"Python.side_fluency.executor": "Run",
"Python.simple_mesh.executor": "Run",
"Python.test_vtk_sketch_widget.executor": "Run",
"Python.vtk_sketch_widget.executor": "Run",
"Python.vtk_widget.executor": "Run",
"Python.vulkan.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"git-widget-placeholder": "single__window",
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency",
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;Python.2dtest.executor&quot;: &quot;Run&quot;,
&quot;Python.3d_windows.executor&quot;: &quot;Run&quot;,
&quot;Python.Unnamed.executor&quot;: &quot;Run&quot;,
&quot;Python.draw_widget2d.executor&quot;: &quot;Run&quot;,
&quot;Python.draw_widget_solve.executor&quot;: &quot;Run&quot;,
&quot;Python.fluency.executor&quot;: &quot;Run&quot;,
&quot;Python.fluencyb.executor&quot;: &quot;Run&quot;,
&quot;Python.gl_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.main.executor&quot;: &quot;Run&quot;,
&quot;Python.meshtest.executor&quot;: &quot;Run&quot;,
&quot;Python.side_fluency.executor&quot;: &quot;Run&quot;,
&quot;Python.simple_mesh.executor&quot;: &quot;Run&quot;,
&quot;Python.vtk_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.vulkan.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;feature/occ-migration&quot;,
&quot;last_opened_file_path&quot;: &quot;/Volumes/Data_drive/Programming/fluency&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;project.propVCSSupport.DirectoryMappings&quot;
}
}]]></component>
}</component>
<component name="QodanaReportsService">
<option name="descriptions">
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
@@ -103,14 +86,13 @@
<recent name="$PROJECT_DIR$/modules" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/doc" />
<recent name="$PROJECT_DIR$" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-4762d8aabb82-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.173" />
<option value="bundled-python-sdk-4c141bd692a7-e2d783800521-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-251.26927.90" />
</set>
</attachedChunks>
</component>
@@ -275,15 +257,7 @@
<option name="project" value="LOCAL" />
<updated>1755369224187</updated>
</task>
<task id="LOCAL-00020" summary="- Improved sketching">
<option name="closed" value="true" />
<created>1763311700335</created>
<option name="number" value="00020" />
<option name="presentableId" value="LOCAL-00020" />
<option name="project" value="LOCAL" />
<updated>1763311700335</updated>
</task>
<option name="localTasksCounter" value="21" />
<option name="localTasksCounter" value="20" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
@@ -318,7 +292,6 @@
<MESSAGE value="- added MIT license" />
<MESSAGE value="- added screenshot" />
<MESSAGE value="- added sdf folder ( doesnt work via pip or git=)" />
<MESSAGE value="- Improved sketching" />
<option name="LAST_COMMIT_MESSAGE" value="- Improved sketching" />
<option name="LAST_COMMIT_MESSAGE" value="- added sdf folder ( doesnt work via pip or git=)" />
</component>
</project>
+1
View File
@@ -33,6 +33,7 @@ dependencies = [
"numpy>=1.24.0",
"scipy>=1.10.0",
"pillow>=10.0.0",
"python_solvespace>=3.0.0",
]
[project.optional-dependencies]
+21 -9
View File
@@ -185,17 +185,28 @@ class OCGeometryKernel(GeometryKernel):
) -> GeometryObject:
"""Revolve a 2D sketch around an axis."""
import cadquery as cq
import math
cq_obj = self._get_cq_obj(sketch)
# Get the OCC shape directly
shape = self._get_shape(sketch)
if isinstance(cq_obj, cq.Workplane):
solid = cq_obj.revolve(angle)
else:
face = cq.Face.makeFromWires(cq_obj)
axis_vec = cq.Vector(*axis)
origin_vec = cq.Vector(*origin)
solid = face.revolve(axis_vec, origin_vec, angle)
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakeFace
from OCP.gp import gp_Ax1, gp_Pnt, gp_Dir
from OCP.BRepPrimAPI import BRepPrimAPI_MakeRevol
# Build a face from the wire/shape
face_maker = BRepBuilderAPI_MakeFace(shape, False)
face_maker.Build()
face = face_maker.Face()
# Revolve the face around the axis
revolve_axis = gp_Ax1(gp_Pnt(*origin), gp_Dir(*axis))
angle_rad = math.radians(angle)
revolver = BRepPrimAPI_MakeRevol(face, revolve_axis, angle_rad)
revolver.Build()
solid_shape = revolver.Shape()
solid = cq.Shape(solid_shape)
return OCCGeometryObject(solid, {"type": "revolution"})
def loft(self, profiles: List[GeometryObject], ruled: bool = False) -> GeometryObject:
@@ -673,7 +684,8 @@ class OCGeometryKernel(GeometryKernel):
explorer = TopExp_Explorer(shape, TopAbs_EDGE)
while explorer.More():
edge = explorer.Current()
from OCP.TopoDS import TopoDS
edge = TopoDS.Edge_s(explorer.Current())
edge_points = discretize_edge(edge)
for i, pt in enumerate(edge_points):
+274 -55
View File
@@ -1,12 +1,17 @@
"""
OpenCASCADE-based sketch for Fluency CAD.
OpenCASCADE-based sketch for Fluency CAD with SolveSpace constraint solver integration.
This module provides 2D sketching capabilities using CadQuery Workplanes.
This module provides 2D sketching capabilities using the SolveSpace constraint
solver (via python_solvespace) for constraint management, and CadQuery for
geometry generation from solved positions.
"""
from typing import List, Tuple, Optional, Dict, Any
from dataclasses import dataclass, field
import numpy as np
import logging
import re
from python_solvespace import SolverSystem, ResultFlag
from fluency.geometry.base import (
SketchInterface,
@@ -16,24 +21,33 @@ from fluency.geometry.base import (
)
from fluency.geometry_occ.kernel import OCCGeometryObject
logger = logging.getLogger(__name__)
class OCCSketchEntity(SketchEntity):
"""Sketch entity for OpenCASCADE-based sketch."""
"""Sketch entity for OpenCASCADE-based sketch with solver integration."""
def __init__(self, entity_id: int, entity_type: str, geometry: Any = None, handle: Any = None):
super().__init__(entity_id, entity_type)
self.geometry = geometry
self.handle = handle
self.handle = handle # SolveSpace solver entity handle
self.is_construction: bool = False
self.constraints: List[str] = [] # Track applied constraint names for UI
class OCCSketch(SketchInterface):
"""
CadQuery-based sketch for 2D geometry.
Sketch with SolveSpace constraint solver integration.
This sketch uses CadQuery Workplanes for geometry creation.
Uses python_solvespace as the constraint engine, allowing points and lines
to be parametrically constrained. After solving, positions are read from
the solver and used to build CadQuery geometry for extrusion.
"""
def __init__(self) -> None:
self._solver: SolverSystem = SolverSystem()
self._wp: Any = self._solver.create_2d_base()
self._entities: Dict[int, OCCSketchEntity] = {}
self._entity_counter: int = 0
self._points: Dict[int, Tuple[float, float]] = {}
@@ -41,17 +55,42 @@ class OCCSketch(SketchInterface):
self._circles: Dict[int, Tuple[int, float]] = {}
self._arcs: Dict[int, Any] = {}
self._constraint_count: int = 0
self._workplane: Any = None
# Track first point as dragged/fixed for solver stability
self._first_point_id: Optional[int] = None
@property
def solver(self) -> SolverSystem:
"""Access the underlying SolveSpace solver."""
return self._solver
@property
def workplane(self) -> Any:
"""Get the solver workplane entity."""
return self._wp
def _next_id(self) -> int:
self._entity_counter += 1
return self._entity_counter
def _get_handle_nr(self, handle_str: str) -> int:
match = re.search(r"handle=(\d+)", str(handle_str))
return int(match.group(1)) if match else 0
def add_point(self, x: float, y: float) -> OCCSketchEntity:
"""Add a point to the sketch."""
"""Add a point to the sketch (added to solver + tracked)."""
entity_id = self._next_id()
entity = OCCSketchEntity(entity_id=entity_id, entity_type="point", geometry=(x, y))
# Add to solver
solver_handle = self._solver.add_point_2d(x, y, self._wp)
if self._first_point_id is None:
self._first_point_id = entity_id
# Fix first point so solver has a reference
self._solver.dragged(solver_handle, self._wp)
entity = OCCSketchEntity(
entity_id=entity_id, entity_type="point", geometry=(x, y), handle=solver_handle
)
self._entities[entity_id] = entity
self._points[entity_id] = (x, y)
@@ -59,20 +98,30 @@ class OCCSketch(SketchInterface):
return entity
def add_line(self, start: SketchEntity, end: SketchEntity) -> OCCSketchEntity:
"""Add a line between two points."""
"""Add a line between two points (added to solver + tracked)."""
entity_id = self._next_id()
start_geom = self._entities.get(start.id)
end_geom = self._entities.get(end.id)
start_entity = self._entities.get(start.id)
end_entity = self._entities.get(end.id)
if start_geom is None or end_geom is None:
if start_entity is None or end_entity is None:
raise ValueError("Start or end point not found in sketch")
x1, y1 = start_geom.geometry
x2, y2 = end_geom.geometry
# Get solver handles
s_handle = start_entity.handle
e_handle = end_entity.handle
# Add line to solver
solver_handle = self._solver.add_line_2d(s_handle, e_handle, self._wp)
x1, y1 = start_entity.geometry
x2, y2 = end_entity.geometry
entity = OCCSketchEntity(
entity_id=entity_id, entity_type="line", geometry=((x1, y1), (x2, y2))
entity_id=entity_id,
entity_type="line",
geometry=((x1, y1), (x2, y2)),
handle=solver_handle,
)
self._entities[entity_id] = entity
@@ -81,7 +130,7 @@ class OCCSketch(SketchInterface):
return entity
def add_circle(self, center: SketchEntity, radius: float) -> OCCSketchEntity:
"""Add a circle."""
"""Add a circle (tracked only — solver has no native circle in this API)."""
entity_id = self._next_id()
center_entity = self._entities.get(center.id)
@@ -106,7 +155,7 @@ class OCCSketch(SketchInterface):
start_point: SketchEntity,
end_point: SketchEntity,
) -> OCCSketchEntity:
"""Add an arc."""
"""Add an arc (tracked only)."""
entity_id = self._next_id()
center_entity = self._entities.get(center.id)
@@ -161,76 +210,218 @@ class OCCSketch(SketchInterface):
return entities
def constrain_coincident(self, *entities: SketchEntity) -> bool:
"""Make entities coincident."""
# ─── Constraint methods (actual solver calls) ──────────────────────────
def _add_constraint_record(self) -> None:
self._constraint_count += 1
def constrain_coincident(self, *entities: SketchEntity) -> bool:
"""Make entities coincident via solver."""
if len(entities) < 2:
return False
e1 = self._entities.get(entities[0].id)
e2 = self._entities.get(entities[1].id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.coincident(e1.handle, e2.handle, self._wp)
self._add_constraint_record()
return True
def constrain_horizontal(self, line: SketchEntity) -> bool:
"""Constrain a line to be horizontal."""
self._constraint_count += 1
entity = self._entities.get(line.id)
if entity is None or entity.handle is None:
return False
self._solver.horizontal(entity.handle, self._wp)
self._add_constraint_record()
if "hrz" not in entity.constraints:
entity.constraints.append("hrz")
return True
def constrain_vertical(self, line: SketchEntity) -> bool:
"""Constrain a line to be vertical."""
self._constraint_count += 1
entity = self._entities.get(line.id)
if entity is None or entity.handle is None:
return False
self._solver.vertical(entity.handle, self._wp)
self._add_constraint_record()
if "vrt" not in entity.constraints:
entity.constraints.append("vrt")
return True
def constrain_distance(
self, entity1: SketchEntity, entity2: SketchEntity, distance: float
) -> bool:
"""Constrain distance between two entities."""
self._constraint_count += 1
e1 = self._entities.get(entity1.id)
e2 = self._entities.get(entity2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.distance(e1.handle, e2.handle, distance, self._wp)
self._add_constraint_record()
return True
def constrain_angle(self, line1: SketchEntity, line2: SketchEntity, angle: float) -> bool:
"""Constrain angle between two lines."""
self._constraint_count += 1
e1 = self._entities.get(line1.id)
e2 = self._entities.get(line2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.angle(e1.handle, e2.handle, angle, self._wp)
self._add_constraint_record()
return True
def constrain_parallel(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to be parallel."""
self._constraint_count += 1
e1 = self._entities.get(line1.id)
e2 = self._entities.get(line2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.parallel(e1.handle, e2.handle, self._wp)
self._add_constraint_record()
return True
def constrain_perpendicular(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to be perpendicular."""
self._constraint_count += 1
e1 = self._entities.get(line1.id)
e2 = self._entities.get(line2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.perpendicular(e1.handle, e2.handle, self._wp)
self._add_constraint_record()
return True
def constrain_midpoint(self, point: SketchEntity, line: SketchEntity) -> bool:
"""Constrain a point to be at the midpoint of a line."""
self._constraint_count += 1
pt = self._entities.get(point.id)
ln = self._entities.get(line.id)
if pt is None or ln is None or pt.handle is None or ln.handle is None:
return False
self._solver.midpoint(pt.handle, ln.handle, self._wp)
self._add_constraint_record()
return True
def constrain_tangent(self, entity1: SketchEntity, entity2: SketchEntity) -> bool:
"""Constrain two entities to be tangent."""
self._constraint_count += 1
e1 = self._entities.get(entity1.id)
e2 = self._entities.get(entity2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.tangent(e1.handle, e2.handle, self._wp)
self._add_constraint_record()
return True
def constrain_equal_length(self, line1: SketchEntity, line2: SketchEntity) -> bool:
"""Constrain two lines to have equal length."""
self._constraint_count += 1
e1 = self._entities.get(line1.id)
e2 = self._entities.get(line2.id)
if e1 is None or e2 is None or e1.handle is None or e2.handle is None:
return False
self._solver.equal(e1.handle, e2.handle, self._wp)
self._add_constraint_record()
return True
def constrain_equal_radius(self, circle1: SketchEntity, circle2: SketchEntity) -> bool:
"""Constrain two circles to have equal radius."""
self._constraint_count += 1
"""Circle equal-radius (tracked only — solver limit)."""
self._add_constraint_record()
return True
def constrain_fixed(self, entity: SketchEntity) -> bool:
"""Fix an entity in place."""
self._constraint_count += 1
"""Fix an entity in place via dragged constraint."""
ent = self._entities.get(entity.id)
if ent is None or ent.handle is None:
return False
self._solver.dragged(ent.handle, self._wp)
self._add_constraint_record()
return True
def constrain_symmetric(
self, entity1: SketchEntity, entity2: SketchEntity, line: SketchEntity
) -> bool:
"""Constrain symmetry about a line."""
e1 = self._entities.get(entity1.id)
e2 = self._entities.get(entity2.id)
ln = self._entities.get(line.id)
if e1 is None or e2 is None or ln is None:
return False
if e1.handle is None or e2.handle is None or ln.handle is None:
return False
self._solver.symmetric(e1.handle, e2.handle, ln.handle, self._wp)
self._add_constraint_record()
return True
# ─── Solving ───────────────────────────────────────────────────────────
def solve(self) -> bool:
"""Solve all constraints."""
return True
"""Solve all constraints via SolveSpace solver."""
try:
result = self._solver.solve()
if result == ResultFlag.OKAY:
# Sync solved positions back to entity geometries
self._sync_solved_positions()
return True
else:
logger.warning(f"Solver returned: {result}")
return False
except Exception as e:
logger.error(f"Solver error: {e}")
return False
def _sync_solved_positions(self) -> None:
"""Read solved point positions from solver and update entity geometries."""
for entity_id, entity in list(self._entities.items()):
if entity.entity_type == "point" and entity.handle is not None:
try:
x, y = self._solver.params(entity.handle.params)
entity.geometry = (x, y)
if entity_id in self._points:
self._points[entity_id] = (x, y)
except Exception as e:
logger.debug(f"Could not sync point {entity_id}: {e}")
elif entity.entity_type == "line" and entity_id in self._lines:
start_id, end_id = self._lines[entity_id]
start_entity = self._entities.get(start_id)
end_entity = self._entities.get(end_id)
if start_entity and end_entity and start_entity.geometry and end_entity.geometry:
entity.geometry = (start_entity.geometry, end_entity.geometry)
def get_solved_point(self, entity_id: int) -> Optional[Tuple[float, float]]:
"""Get the solved position of a point entity."""
entity = self._entities.get(entity_id)
if entity and entity.entity_type == "point" and entity.handle is not None:
try:
x, y = self._solver.params(entity.handle.params)
return (float(x), float(y))
except Exception:
pass
return None
def get_solved_param(self, handle: Any) -> Optional[Tuple[float, float]]:
"""Get solved params for a solver entity handle."""
try:
x, y = self._solver.params(handle.params)
return (float(x), float(y))
except Exception:
return None
# ─── Geometry extraction for operations ────────────────────────────────
def get_geometry(self) -> GeometryObject:
"""Get the solved geometry for operations."""
"""Get the solved geometry for operations using CadQuery."""
import cadquery as cq
# Check for circles first
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
points = self.get_polygon_points()
if not points:
return OCCGeometryObject(None)
@@ -245,22 +436,32 @@ class OCCSketch(SketchInterface):
return obj
def get_points(self) -> List[Point2D]:
"""Get all point positions."""
"""Get all point positions from solved solver data."""
points: List[Point2D] = []
for entity_id, entity in self._entities.items():
if entity.entity_type == "point":
x, y = entity.geometry
points.append(Point2D(x, y))
# Try to get solved position first
if entity.handle is not None:
try:
x, y = self._solver.params(entity.handle.params)
points.append(Point2D(x, y))
continue
except Exception:
pass
# Fall back to stored geometry
if entity.geometry:
x, y = entity.geometry
points.append(Point2D(x, y))
return points
def get_polygon_points(self) -> List[Point2D]:
"""Get ordered polygon points from connected lines."""
"""Get ordered polygon points from connected lines (uses solved positions)."""
adjacency: Dict[Tuple[float, float], List[Tuple[float, float]]] = {}
for entity in self._entities.values():
if entity.entity_type == "line":
if entity.entity_type == "line" and entity.geometry:
p1, p2 = entity.geometry
if p1 not in adjacency:
adjacency[p1] = []
@@ -272,30 +473,41 @@ class OCCSketch(SketchInterface):
if not adjacency:
return []
points: List[Point2D] = []
ordered: List[Point2D] = []
visited: set = set()
current = next(iter(adjacency.keys()))
while current and current not in visited:
points.append(Point2D(current[0], current[1]))
visited.add(current)
while current and tuple(current) not in visited:
ordered.append(Point2D(current[0], current[1]))
visited.add(tuple(current))
neighbors = adjacency.get(current, [])
next_point = None
for n in neighbors:
if n not in visited:
if tuple(n) not in visited:
next_point = n
break
current = next_point
if len(points) > 2:
points.append(points[0])
if len(ordered) > 2:
ordered.append(ordered[0])
return points
return ordered
def get_solver_dof(self) -> int:
"""Get remaining degrees of freedom from solver."""
return self._solver.dof()
def get_solver_failures(self) -> List[Any]:
"""Get list of failed constraints."""
return self._solver.failures()
# ─── Management ────────────────────────────────────────────────────────
def clear(self) -> None:
"""Clear all geometry and constraints."""
"""Clear all geometry and constraints from both solver and tracker."""
self._solver = SolverSystem()
self._wp = self._solver.create_2d_base()
self._entities.clear()
self._points.clear()
self._lines.clear()
@@ -303,12 +515,16 @@ class OCCSketch(SketchInterface):
self._arcs.clear()
self._entity_counter = 0
self._constraint_count = 0
self._first_point_id = None
def delete_entity(self, entity: SketchEntity) -> bool:
"""Delete an entity and its constraints."""
if entity.id not in self._entities:
return False
# Remove from solver (clear + rebuild is simplest)
# For simplicity, we skip solver removal — on next solve, stale handles
# will be ignored. A full rebuild would need entity-by-entity solver removal.
del self._entities[entity.id]
if entity.id in self._points:
@@ -327,9 +543,12 @@ class OCCSketch(SketchInterface):
return len(self._entities)
def get_constraint_count(self) -> int:
"""Get the number of constraints in the sketch."""
"""Get the number of constraints applied via solver."""
return self._constraint_count
def is_fully_constrained(self) -> bool:
"""Check if the sketch is fully constrained."""
return False
"""Check if the sketch is fully constrained (0 DOF)."""
try:
return self._solver.dof() == 0
except Exception:
return False
+1067 -383
View File
File diff suppressed because it is too large Load Diff
Generated
+14
View File
@@ -638,6 +638,7 @@ dependencies = [
{ name = "pillow" },
{ name = "pygfx" },
{ name = "pyside6" },
{ name = "python-solvespace" },
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "wgpu" },
@@ -661,6 +662,7 @@ requires-dist = [
{ name = "pygfx", specifier = ">=0.1.0" },
{ name = "pyside6", specifier = ">=6.4.0" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
{ name = "python-solvespace", specifier = ">=3.0.0" },
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
{ name = "scipy", specifier = ">=1.10.0" },
{ name = "wgpu", specifier = ">=0.1.0" },
@@ -2107,6 +2109,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "python-solvespace"
version = "3.0.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/70/d9/edb532941527cfdbd22861ce574a57859952d21132f791a8706fafee9876/python_solvespace-3.0.8.tar.gz", hash = "sha256:c5c132c1151cfa4cc8719474bbbafedd109d7203317e531f9160a37aaae644b0", size = 1231985, upload-time = "2022-11-11T13:18:23.9Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1c/73/cfd7e631395b036052356517c2c8b8e7917308b4c397694da34260f7eff6/python_solvespace-3.0.8-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:bd3366cda1f3bac7c239c3fad98c6eee97c3d9299ee5c3a1ab3e4ea5e112b81a", size = 386490, upload-time = "2022-11-11T13:19:11.237Z" },
{ url = "https://files.pythonhosted.org/packages/c7/28/7e8ea55da42cd9e0ab9bdc607f5d57270c553baa9c4dab63866bba7c6e40/python_solvespace-3.0.8-cp310-cp310-win_amd64.whl", hash = "sha256:33c150f68c81addf8045e28ffeb28a828fcbbe9ab84cc4a9f1e50d0847b23bfb", size = 243413, upload-time = "2022-11-11T13:19:03.064Z" },
{ url = "https://files.pythonhosted.org/packages/e5/6a/3d3f3ffa52b4952a4d63506b20d55e23e5bf34dfd096c185cfd4b262d9a2/python_solvespace-3.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c899978359d95c274bbcc31405b080517af3839beca98e3342c52a37409bb916", size = 703966, upload-time = "2022-11-11T13:34:03.875Z" },
{ url = "https://files.pythonhosted.org/packages/b1/4c/0140a197e00be797f3ee26278a9205cccfb668b8c7d7746350bb72bff3c7/python_solvespace-3.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:1191b004298d57936058d9dbdd395c44c1f46d5a9c854e36c507c49c3e6b6382", size = 242810, upload-time = "2022-11-11T13:19:55.656Z" },
]
[[package]]
name = "pytokens"
version = "0.4.1"