- Operation highlighting, body highlighting

This commit is contained in:
bklronin
2026-08-08 13:46:12 +02:00
parent b29bc11b42
commit 3d63f033f2
8 changed files with 488 additions and 43 deletions
+37 -16
View File
@@ -1949,7 +1949,7 @@ class OCCSketch(SketchInterface):
_SNAP_TOL: float = 1e-2 # world-unit tolerance for snapping line endpoints in loop detection
def _line_segments(self) -> List[Tuple[Tuple[float, float], Tuple[float, float]]]:
def _line_segments(self) -> List[Tuple[float, float, float, float, int]]:
"""Current line segments as world-coordinate tuples (uses solved positions).
Returns both straight line segments AND tessellated arc segments so
@@ -1957,10 +1957,13 @@ class OCCSketch(SketchInterface):
and external entities are excluded — they're reference geometry and
must not affect the sketch profile.
Each segment is ``(x1, y1, x2, y2, entity_id)`` — the entity_id lets
callers trace which sketch entity produced each segment.
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[float, float, float, float, int]] = []
# ── Straight line segments ──
for line_id, (sid, eid2) in self._lines.items():
@@ -1974,8 +1977,9 @@ class OCCSketch(SketchInterface):
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])),
float(s_ent.geometry[0]), float(s_ent.geometry[1]),
float(e_ent.geometry[0]), float(e_ent.geometry[1]),
line_id,
)
)
@@ -2010,7 +2014,7 @@ class OCCSketch(SketchInterface):
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))
segs.append((p1[0], p1[1], p2[0], p2[1], arc_id))
return segs
@@ -2018,8 +2022,8 @@ class OCCSketch(SketchInterface):
"""Detect closed loops: polygon cycles from connected lines + each circle.
Each loop is one of:
{"type": "polygon", "points": [(x,y), ...]} (closed, last == first)
{"type": "circle", "center": (x,y), "radius": r}
{"type": "polygon", "points": [(x,y), ...], "entity_ids": [int, ...]} (closed, last == first)
{"type": "circle", "center": (x,y), "radius": r, "entity_ids": [int]}
Line endpoint coordinates are snapped to ``_SNAP_TOL`` so a closed
rectangle's four corners join into one cycle even after solver floating
point jitter. Only connected components where every node has degree 2
@@ -2035,11 +2039,13 @@ class OCCSketch(SketchInterface):
reprs: Dict[Any, Tuple[float, float]] = {} # key -> averaged world pt
edges: List[Tuple[Any, Any]] = []
for p1, p2 in segs:
k1, k2 = key(p1), key(p2)
reprs.setdefault(k1, p1)
reprs.setdefault(k2, p2)
edge_eids: Dict[Tuple[Any, Any], int] = {} # (k1,k2) -> entity_id
for x1, y1, x2, y2, eid in segs:
k1, k2 = key((x1, y1)), key((x2, y2))
reprs.setdefault(k1, (x1, y1))
reprs.setdefault(k2, (x2, y2))
edges.append((k1, k2))
edge_eids[(k1, k2) if k1 < k2 else (k2, k1)] = eid
# Undirected adjacency.
adj: Dict[Any, List[Any]] = {}
@@ -2066,8 +2072,9 @@ class OCCSketch(SketchInterface):
if nb not in comp_seen:
stack.append(nb)
if all(len(adj[n]) == 2 for n in comp) and len(comp) >= 3:
# Order the cycle by following each node's neighbor not yet visited.
# Order the cycle by following each node's neighbour not yet visited.
ordered: List[Any] = []
eids: List[int] = []
cur = comp[0]
prev = None
for _ in range(len(comp)):
@@ -2075,16 +2082,26 @@ class OCCSketch(SketchInterface):
nbrs = [nb for nb in adj[cur] if nb != prev]
if not nbrs:
break
ekey = (cur, nbrs[0]) if cur < nbrs[0] else (nbrs[0], cur)
if ekey in edge_eids:
eids.append(edge_eids[ekey])
prev = cur
cur = nbrs[0]
if len(ordered) == len(comp):
pts = [reprs[k] for k in ordered]
pts.append(pts[0])
loops.append({"type": "polygon", "points": pts})
loops.append(
{"type": "polygon", "points": pts, "entity_ids": sorted(set(eids))}
)
seen |= comp_seen
# Circles are closed loops of their own.
for cid, (center_id, r) in self._circles.items():
circle_ent = self._entities.get(cid)
if circle_ent is not None and circle_ent.is_construction:
continue
if cid in self._external_entity_ids:
continue
c_ent = self._entities.get(center_id)
if c_ent and c_ent.geometry and r > 0:
loops.append(
@@ -2092,6 +2109,7 @@ class OCCSketch(SketchInterface):
"type": "circle",
"center": (float(c_ent.geometry[0]), float(c_ent.geometry[1])),
"radius": float(r),
"entity_ids": [cid],
}
)
return loops
@@ -2242,8 +2260,7 @@ class OCCSketch(SketchInterface):
that is the rectangle minus the circle — exactly the
"shape within a shape = closed without inner" behavior. A shape nested
inside a hole (depth 2) becomes its own solid face again.
Returns a list of ``{"outer": loop, "holes": [loop, ...], "depth": int}``.
Returns a list of ``{"outer": loop, "holes": [loop, ...], "depth": int, "entity_ids": [int, ...]}``.
"""
loops = self.get_closed_loops()
if not loops:
@@ -2267,7 +2284,11 @@ class OCCSketch(SketchInterface):
# directly nested: depth one greater, and outer contains inner.
if depths[j] == depths[i] + 1 and OCCSketch._loop_contains(inner, outer):
holes.append(inner)
faces.append({"outer": outer, "holes": holes, "depth": depths[i]})
# Face entity_ids = union of outer + hole loop entity_ids.
eids = set(outer.get("entity_ids", []))
for h in holes:
eids.update(h.get("entity_ids", []))
faces.append({"outer": outer, "holes": holes, "depth": depths[i], "entity_ids": sorted(eids)})
return faces
def find_face_at(self, x: float, y: float) -> Optional[Dict[str, Any]]: