- 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" /> <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 @@
&quot;Python.occ_renderer.executor&quot;: &quot;Run&quot;, &quot;Python.occ_renderer.executor&quot;: &quot;Run&quot;,
&quot;Python.side_fluency.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.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.vtk_widget.executor&quot;: &quot;Run&quot;,
&quot;Python.vulkan.executor&quot;: &quot;Run&quot;, &quot;Python.vulkan.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;, &quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
@@ -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>
+175 -10
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff