- sketch enhacements

This commit is contained in:
bklronin
2026-07-03 21:49:05 +02:00
parent f860ff3e77
commit 01833e4af2
3 changed files with 935 additions and 60 deletions
+15 -8
View File
@@ -4,13 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<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$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" 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" />
<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" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -63,6 +60,7 @@
&quot;Python.occ_renderer.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.sketch.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;,
@@ -288,7 +286,15 @@
<option name="project" value="LOCAL" />
<updated>1782768610475</updated>
</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 />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -328,6 +334,7 @@
<MESSAGE value="- added sdf folder ( doesnt work via pip or git=)" />
<MESSAGE value="- Tons of addtions" />
<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>
</project>
+175 -10
View File
@@ -74,6 +74,14 @@ class OCCSketch(SketchInterface):
# • clear them as a group when the source face is removed
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
self._first_point_id: Optional[int] = None
@@ -259,8 +267,14 @@ class OCCSketch(SketchInterface):
radius: float,
start_point: SketchEntity,
end_point: SketchEntity,
sweep: Optional[float] = None,
) -> 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()
center_entity = self._entities.get(center.id)
@@ -274,10 +288,26 @@ class OCCSketch(SketchInterface):
sx, sy = start_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_id=entity_id,
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
@@ -286,6 +316,7 @@ class OCCSketch(SketchInterface):
"start": start_point.id,
"end": end_point.id,
"radius": radius,
"sweep": sweep,
}
return entity
@@ -432,6 +463,67 @@ class OCCSketch(SketchInterface):
"""Return the set of external (underlay) entity ids currently in the sketch."""
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(
self, corner1: Tuple[float, float], corner2: Tuple[float, float]
) -> List[OCCSketchEntity]:
@@ -838,7 +930,11 @@ class OCCSketch(SketchInterface):
if entity_id in self._external_entity_ids:
continue
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:
# Skip construction circles — they're reference geometry.
if circle_ent is not None and circle_ent.is_construction:
continue
cx, cy = center_entity.geometry
face_dict = {
"outer": {"type": "circle", "center": (cx, cy), "radius": radius},
@@ -880,13 +976,13 @@ class OCCSketch(SketchInterface):
def get_polygon_points(self) -> List[Point2D]:
"""Get ordered polygon points from connected lines (uses solved positions).
External (underlay) lines are skipped — they are reference geometry
only, not part of the sketch profile.
External (underlay) and construction lines are skipped — they are
reference geometry only, not part of the sketch profile.
"""
adjacency: Dict[Tuple[float, float], List[Tuple[float, float]]] = {}
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
if p1 not in adjacency:
adjacency[p1] = []
@@ -926,21 +1022,63 @@ class OCCSketch(SketchInterface):
def _line_segments(self) -> List[Tuple[Tuple[float, float], Tuple[float, float]]]:
"""Current line segments as world-coordinate tuples (uses solved positions).
External (underlay) lines are excluded: they're reference geometry
projected from a 3D face, not user-drawn sketch profile, and must
not contribute to detect_faces / get_geometry / extrusion.
Returns both straight line segments AND tessellated arc segments so
that arcs participate in closed-loop / face detection. Construction
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]]] = []
# ── Straight line segments ──
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:
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)
e_ent = self._entities.get(eid2)
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])),
(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
def get_closed_loops(self) -> List[Dict[str, Any]]:
@@ -1240,6 +1378,7 @@ class OCCSketch(SketchInterface):
self._constraint_count = 0
self._constraint_log.clear()
self._external_entity_ids.clear()
self._centerline_ids.clear()
self._first_point_id = 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
the surviving points/lines + pruned log, and re-solves. The line's
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:
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]
if line.id in self._entities:
del self._entities[line.id]
@@ -1298,10 +1444,17 @@ class OCCSketch(SketchInterface):
All constraints that reference the point OR the removed lines are
pruned from the log, the solver is rebuilt from survivors, labels are
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:
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}
# Remove lines that use this point as an endpoint.
removed_line_keys: List[int] = [
@@ -1327,6 +1480,18 @@ class OCCSketch(SketchInterface):
del self._circles[cid]
if cid in self._entities:
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._rebuild_solver()
+745 -42
View File
File diff suppressed because it is too large Load Diff