- Improved sketching
This commit is contained in:
Generated
-11
@@ -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>
|
||||
Generated
+1
-1
@@ -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>
|
||||
Generated
+35
-62
@@ -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">{
|
||||
"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.vtk_widget.executor": "Run",
|
||||
"Python.vulkan.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"git-widget-placeholder": "feature/occ-migration",
|
||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||
}
|
||||
}]]></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>
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user