- sketch enhacements
This commit is contained in:
Generated
+15
-8
@@ -4,13 +4,10 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- Basic operations">
|
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="- removed cadquery deoendency">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" 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$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" 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$/src/fluency/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/__init__.py" 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$/uv.lock" beforeDir="false" afterPath="$PROJECT_DIR$/uv.lock" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -63,6 +60,7 @@
|
|||||||
"Python.occ_renderer.executor": "Run",
|
"Python.occ_renderer.executor": "Run",
|
||||||
"Python.side_fluency.executor": "Run",
|
"Python.side_fluency.executor": "Run",
|
||||||
"Python.simple_mesh.executor": "Run",
|
"Python.simple_mesh.executor": "Run",
|
||||||
|
"Python.sketch.executor": "Run",
|
||||||
"Python.vtk_widget.executor": "Run",
|
"Python.vtk_widget.executor": "Run",
|
||||||
"Python.vulkan.executor": "Run",
|
"Python.vulkan.executor": "Run",
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
@@ -288,7 +286,15 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1782768610475</updated>
|
<updated>1782768610475</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="23" />
|
<task id="LOCAL-00023" summary="- removed cadquery deoendency">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1782928990792</created>
|
||||||
|
<option name="number" value="00023" />
|
||||||
|
<option name="presentableId" value="LOCAL-00023" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1782928990792</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="24" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -328,6 +334,7 @@
|
|||||||
<MESSAGE value="- added sdf folder ( doesnt work via pip or git=)" />
|
<MESSAGE value="- added sdf folder ( doesnt work via pip or git=)" />
|
||||||
<MESSAGE value="- Tons of addtions" />
|
<MESSAGE value="- Tons of addtions" />
|
||||||
<MESSAGE value="- Basic operations" />
|
<MESSAGE value="- Basic operations" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="- Basic operations" />
|
<MESSAGE value="- removed cadquery deoendency" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="- removed cadquery deoendency" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -74,6 +74,14 @@ class OCCSketch(SketchInterface):
|
|||||||
# • clear them as a group when the source face is removed
|
# • clear them as a group when the source face is removed
|
||||||
self._external_entity_ids: set = set()
|
self._external_entity_ids: set = set()
|
||||||
|
|
||||||
|
# Centerline entity ids (X and Y reference axes through origin).
|
||||||
|
# These are construction lines that span the sketch and are used
|
||||||
|
# as reference axes for constraining geometry. They are:
|
||||||
|
# • fixed in the solver (never move)
|
||||||
|
# • marked is_construction (excluded from profile detection)
|
||||||
|
# • non-deletable
|
||||||
|
self._centerline_ids: set = set()
|
||||||
|
|
||||||
# Track first point as dragged/fixed for solver stability
|
# Track first point as dragged/fixed for solver stability
|
||||||
self._first_point_id: Optional[int] = None
|
self._first_point_id: Optional[int] = None
|
||||||
|
|
||||||
@@ -259,8 +267,14 @@ class OCCSketch(SketchInterface):
|
|||||||
radius: float,
|
radius: float,
|
||||||
start_point: SketchEntity,
|
start_point: SketchEntity,
|
||||||
end_point: SketchEntity,
|
end_point: SketchEntity,
|
||||||
|
sweep: Optional[float] = None,
|
||||||
) -> OCCSketchEntity:
|
) -> OCCSketchEntity:
|
||||||
"""Add an arc (tracked only)."""
|
"""Add an arc (tracked only).
|
||||||
|
|
||||||
|
*sweep* is the signed angular span in radians (positive = CCW, negative = CW).
|
||||||
|
When *None* the rendering will infer the shortest path between start and end.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
entity_id = self._next_id()
|
entity_id = self._next_id()
|
||||||
|
|
||||||
center_entity = self._entities.get(center.id)
|
center_entity = self._entities.get(center.id)
|
||||||
@@ -274,10 +288,26 @@ class OCCSketch(SketchInterface):
|
|||||||
sx, sy = start_entity.geometry
|
sx, sy = start_entity.geometry
|
||||||
ex, ey = end_entity.geometry
|
ex, ey = end_entity.geometry
|
||||||
|
|
||||||
|
# Infer sweep from geometry when not provided.
|
||||||
|
if sweep is None:
|
||||||
|
sa = math.atan2(sy - cy, sx - cx)
|
||||||
|
ea = math.atan2(ey - cy, ex - cx)
|
||||||
|
sweep = ea - sa
|
||||||
|
while sweep > math.pi:
|
||||||
|
sweep -= 2 * math.pi
|
||||||
|
while sweep < -math.pi:
|
||||||
|
sweep += 2 * math.pi
|
||||||
|
|
||||||
entity = OCCSketchEntity(
|
entity = OCCSketchEntity(
|
||||||
entity_id=entity_id,
|
entity_id=entity_id,
|
||||||
entity_type="arc",
|
entity_type="arc",
|
||||||
geometry={"center": (cx, cy), "radius": radius, "start": (sx, sy), "end": (ex, ey)},
|
geometry={
|
||||||
|
"center": (cx, cy),
|
||||||
|
"radius": radius,
|
||||||
|
"start": (sx, sy),
|
||||||
|
"end": (ex, ey),
|
||||||
|
"sweep": sweep,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._entities[entity_id] = entity
|
self._entities[entity_id] = entity
|
||||||
@@ -286,6 +316,7 @@ class OCCSketch(SketchInterface):
|
|||||||
"start": start_point.id,
|
"start": start_point.id,
|
||||||
"end": end_point.id,
|
"end": end_point.id,
|
||||||
"radius": radius,
|
"radius": radius,
|
||||||
|
"sweep": sweep,
|
||||||
}
|
}
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
@@ -432,6 +463,67 @@ class OCCSketch(SketchInterface):
|
|||||||
"""Return the set of external (underlay) entity ids currently in the sketch."""
|
"""Return the set of external (underlay) entity ids currently in the sketch."""
|
||||||
return set(self._external_entity_ids)
|
return set(self._external_entity_ids)
|
||||||
|
|
||||||
|
# ── Centerlines (X and Y reference axes) ────────────────────────────
|
||||||
|
|
||||||
|
_CENTERLINE_EXTENT: float = 10000.0 # large enough to span any sketch
|
||||||
|
|
||||||
|
def add_centerlines(self) -> None:
|
||||||
|
"""Add X (horizontal) and Y (vertical) centerlines through the origin.
|
||||||
|
|
||||||
|
These construction lines serve as reference axes for constraining
|
||||||
|
sketch geometry. They are:
|
||||||
|
• Fixed in the solver (never move)
|
||||||
|
• Marked is_construction (excluded from profile / face detection)
|
||||||
|
• Non-deletable
|
||||||
|
• Available as constraint targets (snap, coincident, distance,
|
||||||
|
symmetric, horizontal, vertical, parallel, perpendicular)
|
||||||
|
|
||||||
|
The X centerline runs horizontal (left→right) through origin and
|
||||||
|
the Y centerline runs vertical (bottom→top) through origin. Both
|
||||||
|
extend to ``_CENTERLINE_EXTENT`` in both directions so they span
|
||||||
|
any realistic sketch. If centerlines have already been added this
|
||||||
|
is a no-op.
|
||||||
|
"""
|
||||||
|
if self._centerline_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
HALF = self._CENTERLINE_EXTENT
|
||||||
|
|
||||||
|
# Origin point — auto-fixed as the first point in the solver
|
||||||
|
origin = self.add_point(0.0, 0.0)
|
||||||
|
origin.is_construction = True
|
||||||
|
|
||||||
|
# X centerline (horizontal)
|
||||||
|
xl = self.add_point(-HALF, 0.0)
|
||||||
|
xl.is_construction = True
|
||||||
|
xr = self.add_point(HALF, 0.0)
|
||||||
|
xr.is_construction = True
|
||||||
|
xline = self.add_line(xl, xr)
|
||||||
|
xline.is_construction = True
|
||||||
|
self.constrain_horizontal(xline)
|
||||||
|
self.constrain_fixed(xl)
|
||||||
|
|
||||||
|
# Y centerline (vertical)
|
||||||
|
yb = self.add_point(0.0, -HALF)
|
||||||
|
yb.is_construction = True
|
||||||
|
yt = self.add_point(0.0, HALF)
|
||||||
|
yt.is_construction = True
|
||||||
|
yline = self.add_line(yb, yt)
|
||||||
|
yline.is_construction = True
|
||||||
|
self.constrain_vertical(yline)
|
||||||
|
self.constrain_fixed(yb)
|
||||||
|
|
||||||
|
self._centerline_ids = {
|
||||||
|
origin.id, xl.id, xr.id, xline.id,
|
||||||
|
yb.id, yt.id, yline.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.solve()
|
||||||
|
|
||||||
|
def get_centerline_ids(self) -> set:
|
||||||
|
"""Return the set of centerline entity ids currently in the sketch."""
|
||||||
|
return set(self._centerline_ids)
|
||||||
|
|
||||||
def add_rectangle(
|
def add_rectangle(
|
||||||
self, corner1: Tuple[float, float], corner2: Tuple[float, float]
|
self, corner1: Tuple[float, float], corner2: Tuple[float, float]
|
||||||
) -> List[OCCSketchEntity]:
|
) -> List[OCCSketchEntity]:
|
||||||
@@ -838,7 +930,11 @@ class OCCSketch(SketchInterface):
|
|||||||
if entity_id in self._external_entity_ids:
|
if entity_id in self._external_entity_ids:
|
||||||
continue
|
continue
|
||||||
center_entity = self._entities.get(center_id)
|
center_entity = self._entities.get(center_id)
|
||||||
|
circle_ent = self._entities.get(entity_id)
|
||||||
if center_entity and center_entity.geometry and not center_entity.is_external:
|
if center_entity and center_entity.geometry and not center_entity.is_external:
|
||||||
|
# Skip construction circles — they're reference geometry.
|
||||||
|
if circle_ent is not None and circle_ent.is_construction:
|
||||||
|
continue
|
||||||
cx, cy = center_entity.geometry
|
cx, cy = center_entity.geometry
|
||||||
face_dict = {
|
face_dict = {
|
||||||
"outer": {"type": "circle", "center": (cx, cy), "radius": radius},
|
"outer": {"type": "circle", "center": (cx, cy), "radius": radius},
|
||||||
@@ -880,13 +976,13 @@ class OCCSketch(SketchInterface):
|
|||||||
def get_polygon_points(self) -> List[Point2D]:
|
def get_polygon_points(self) -> List[Point2D]:
|
||||||
"""Get ordered polygon points from connected lines (uses solved positions).
|
"""Get ordered polygon points from connected lines (uses solved positions).
|
||||||
|
|
||||||
External (underlay) lines are skipped — they are reference geometry
|
External (underlay) and construction lines are skipped — they are
|
||||||
only, not part of the sketch profile.
|
reference geometry only, not part of the sketch profile.
|
||||||
"""
|
"""
|
||||||
adjacency: Dict[Tuple[float, float], List[Tuple[float, float]]] = {}
|
adjacency: Dict[Tuple[float, float], List[Tuple[float, float]]] = {}
|
||||||
|
|
||||||
for entity in self._entities.values():
|
for entity in self._entities.values():
|
||||||
if entity.entity_type == "line" and entity.geometry and not entity.is_external:
|
if entity.entity_type == "line" and entity.geometry and not entity.is_external and not entity.is_construction:
|
||||||
p1, p2 = entity.geometry
|
p1, p2 = entity.geometry
|
||||||
if p1 not in adjacency:
|
if p1 not in adjacency:
|
||||||
adjacency[p1] = []
|
adjacency[p1] = []
|
||||||
@@ -926,21 +1022,63 @@ class OCCSketch(SketchInterface):
|
|||||||
def _line_segments(self) -> List[Tuple[Tuple[float, float], Tuple[float, float]]]:
|
def _line_segments(self) -> List[Tuple[Tuple[float, float], Tuple[float, float]]]:
|
||||||
"""Current line segments as world-coordinate tuples (uses solved positions).
|
"""Current line segments as world-coordinate tuples (uses solved positions).
|
||||||
|
|
||||||
External (underlay) lines are excluded: they're reference geometry
|
Returns both straight line segments AND tessellated arc segments so
|
||||||
projected from a 3D face, not user-drawn sketch profile, and must
|
that arcs participate in closed-loop / face detection. Construction
|
||||||
not contribute to detect_faces / get_geometry / extrusion.
|
and external entities are excluded — they're reference geometry and
|
||||||
|
must not affect the sketch profile.
|
||||||
|
|
||||||
|
Tessellation density: roughly 12 segments per π radians of arc sweep,
|
||||||
|
which gives smooth-looking closed loops for face detection.
|
||||||
"""
|
"""
|
||||||
segs: List[Tuple[Tuple[float, float], Tuple[float, float]]] = []
|
segs: List[Tuple[Tuple[float, float], Tuple[float, float]]] = []
|
||||||
|
|
||||||
|
# ── Straight line segments ──
|
||||||
for line_id, (sid, eid2) in self._lines.items():
|
for line_id, (sid, eid2) in self._lines.items():
|
||||||
# Skip external/underlay lines — they live in the solver for
|
|
||||||
# constraint referencing, but are not part of the sketch profile.
|
|
||||||
if line_id in self._external_entity_ids:
|
if line_id in self._external_entity_ids:
|
||||||
continue
|
continue
|
||||||
|
line_ent = self._entities.get(line_id)
|
||||||
|
if line_ent is not None and line_ent.is_construction:
|
||||||
|
continue
|
||||||
s_ent = self._entities.get(sid)
|
s_ent = self._entities.get(sid)
|
||||||
e_ent = self._entities.get(eid2)
|
e_ent = self._entities.get(eid2)
|
||||||
if s_ent and e_ent and s_ent.geometry and e_ent.geometry:
|
if s_ent and e_ent and s_ent.geometry and e_ent.geometry:
|
||||||
segs.append(((float(s_ent.geometry[0]), float(s_ent.geometry[1])),
|
segs.append(((float(s_ent.geometry[0]), float(s_ent.geometry[1])),
|
||||||
(float(e_ent.geometry[0]), float(e_ent.geometry[1]))))
|
(float(e_ent.geometry[0]), float(e_ent.geometry[1]))))
|
||||||
|
|
||||||
|
# ── Arc segments (tessellated) ──
|
||||||
|
for arc_id, arc_data in self._arcs.items():
|
||||||
|
arc_ent = self._entities.get(arc_id)
|
||||||
|
if arc_ent is not None and arc_ent.is_construction:
|
||||||
|
continue
|
||||||
|
center_id = arc_data.get("center")
|
||||||
|
start_id = arc_data.get("start")
|
||||||
|
end_id = arc_data.get("end")
|
||||||
|
radius = arc_data.get("radius", 0.0)
|
||||||
|
sweep = arc_data.get("sweep")
|
||||||
|
if sweep is None or radius <= 0:
|
||||||
|
continue
|
||||||
|
c_ent = self._entities.get(center_id)
|
||||||
|
s_ent = self._entities.get(start_id)
|
||||||
|
e_ent = self._entities.get(end_id)
|
||||||
|
if not (c_ent and s_ent and e_ent
|
||||||
|
and c_ent.geometry and s_ent.geometry and e_ent.geometry):
|
||||||
|
continue
|
||||||
|
cx, cy = c_ent.geometry
|
||||||
|
sx, sy = s_ent.geometry
|
||||||
|
start_angle = math.atan2(sy - cy, sx - cx)
|
||||||
|
# ~12 segments per π radians
|
||||||
|
n = max(4, int(abs(sweep) / (math.pi / 12)))
|
||||||
|
for i in range(n):
|
||||||
|
t1 = i / n
|
||||||
|
t2 = (i + 1) / n
|
||||||
|
a1 = start_angle + t1 * sweep
|
||||||
|
a2 = start_angle + t2 * sweep
|
||||||
|
p1 = (cx + radius * math.cos(a1),
|
||||||
|
cy + radius * math.sin(a1))
|
||||||
|
p2 = (cx + radius * math.cos(a2),
|
||||||
|
cy + radius * math.sin(a2))
|
||||||
|
segs.append((p1, p2))
|
||||||
|
|
||||||
return segs
|
return segs
|
||||||
|
|
||||||
def get_closed_loops(self) -> List[Dict[str, Any]]:
|
def get_closed_loops(self) -> List[Dict[str, Any]]:
|
||||||
@@ -1240,6 +1378,7 @@ class OCCSketch(SketchInterface):
|
|||||||
self._constraint_count = 0
|
self._constraint_count = 0
|
||||||
self._constraint_log.clear()
|
self._constraint_log.clear()
|
||||||
self._external_entity_ids.clear()
|
self._external_entity_ids.clear()
|
||||||
|
self._centerline_ids.clear()
|
||||||
self._first_point_id = None
|
self._first_point_id = None
|
||||||
|
|
||||||
def _prune_log_for(self, removed_ids: set) -> None:
|
def _prune_log_for(self, removed_ids: set) -> None:
|
||||||
@@ -1259,10 +1398,17 @@ class OCCSketch(SketchInterface):
|
|||||||
constraint that referenced it, rebuilds the whole solver system from
|
constraint that referenced it, rebuilds the whole solver system from
|
||||||
the surviving points/lines + pruned log, and re-solves. The line's
|
the surviving points/lines + pruned log, and re-solves. The line's
|
||||||
endpoint points are NOT removed — only the line segment.
|
endpoint points are NOT removed — only the line segment.
|
||||||
|
|
||||||
|
Note: centerlines (reference axes through origin) cannot be deleted.
|
||||||
"""
|
"""
|
||||||
if line.id not in self._lines or line.id not in self._entities:
|
if line.id not in self._lines or line.id not in self._entities:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Centerlines are permanent reference axes — refuse deletion.
|
||||||
|
if line.id in self._centerline_ids:
|
||||||
|
logger.debug("Refusing to delete centerline")
|
||||||
|
return False
|
||||||
|
|
||||||
del self._lines[line.id]
|
del self._lines[line.id]
|
||||||
if line.id in self._entities:
|
if line.id in self._entities:
|
||||||
del self._entities[line.id]
|
del self._entities[line.id]
|
||||||
@@ -1298,10 +1444,17 @@ class OCCSketch(SketchInterface):
|
|||||||
All constraints that reference the point OR the removed lines are
|
All constraints that reference the point OR the removed lines are
|
||||||
pruned from the log, the solver is rebuilt from survivors, labels are
|
pruned from the log, the solver is rebuilt from survivors, labels are
|
||||||
re-derived, and the system is re-solved.
|
re-derived, and the system is re-solved.
|
||||||
|
|
||||||
|
Note: centerlines (reference axes through origin) cannot be deleted.
|
||||||
"""
|
"""
|
||||||
if point.id not in self._entities or point.id not in self._points:
|
if point.id not in self._entities or point.id not in self._points:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Centerline points are permanent reference anchors — refuse deletion.
|
||||||
|
if point.id in self._centerline_ids:
|
||||||
|
logger.debug("Refusing to delete centerline point")
|
||||||
|
return False
|
||||||
|
|
||||||
removed_ids: set = {point.id}
|
removed_ids: set = {point.id}
|
||||||
# Remove lines that use this point as an endpoint.
|
# Remove lines that use this point as an endpoint.
|
||||||
removed_line_keys: List[int] = [
|
removed_line_keys: List[int] = [
|
||||||
@@ -1327,6 +1480,18 @@ class OCCSketch(SketchInterface):
|
|||||||
del self._circles[cid]
|
del self._circles[cid]
|
||||||
if cid in self._entities:
|
if cid in self._entities:
|
||||||
del self._entities[cid]
|
del self._entities[cid]
|
||||||
|
# Arcs referencing this point (as centre, start, or end) are invalid.
|
||||||
|
removed_arc_keys: List[int] = [
|
||||||
|
aid for aid, adata in list(self._arcs.items())
|
||||||
|
if adata.get("center") == point.id
|
||||||
|
or adata.get("start") == point.id
|
||||||
|
or adata.get("end") == point.id
|
||||||
|
]
|
||||||
|
for aid in removed_arc_keys:
|
||||||
|
removed_ids.add(aid)
|
||||||
|
del self._arcs[aid]
|
||||||
|
if aid in self._entities:
|
||||||
|
del self._entities[aid]
|
||||||
|
|
||||||
self._prune_log_for(removed_ids)
|
self._prune_log_for(removed_ids)
|
||||||
self._rebuild_solver()
|
self._rebuild_solver()
|
||||||
|
|||||||
+742
-39
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user