- added "measurement lines"

This commit is contained in:
bklronin
2026-07-26 21:39:47 +02:00
parent 9f1c29d319
commit 0daa6152ee
12 changed files with 3110 additions and 1675 deletions
+381 -98
View File
@@ -109,7 +109,6 @@ class OCCSketch(SketchInterface):
orthonormalised here. ``y_dir`` is derived as ``normal × x_dir``.
Existing UV coordinates are unchanged; only their world mapping moves.
"""
import numpy as np
n = np.asarray(normal, dtype=float)
x = np.asarray(x_dir, dtype=float)
@@ -138,6 +137,7 @@ class OCCSketch(SketchInterface):
def _uv_to_world(self, u: float, v: float):
"""Map a UV point to a world ``gp_Pnt`` on the workplane."""
from OCP.gp import gp_Pnt
ox, oy, oz = self._wp_origin
xx, xy, xz = self._wp_x_dir
yx, yy, yz = self._wp_y_dir
@@ -150,6 +150,7 @@ class OCCSketch(SketchInterface):
def _circle_axis(self, u: float, v: float):
"""Return a ``gp_Ax2`` for a circle centred at UV on the workplane."""
from OCP.gp import gp_Ax2, gp_Dir
center = self._uv_to_world(u, v)
return gp_Ax2(
center,
@@ -275,6 +276,7 @@ class OCCSketch(SketchInterface):
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)
@@ -345,8 +347,10 @@ class OCCSketch(SketchInterface):
self._solver.dragged(solver_handle, self._wp)
entity = OCCSketchEntity(
entity_id=entity_id, entity_type="point",
geometry=(x, y), handle=solver_handle,
entity_id=entity_id,
entity_type="point",
geometry=(x, y),
handle=solver_handle,
)
entity.is_external = True
entity.is_construction = True # external points are reference / dashed
@@ -377,7 +381,8 @@ class OCCSketch(SketchInterface):
x1, y1 = s_ent.geometry
x2, y2 = e_ent.geometry
entity = OCCSketchEntity(
entity_id=entity_id, entity_type="line",
entity_id=entity_id,
entity_type="line",
geometry=((x1, y1), (x2, y2)),
handle=solver_handle,
)
@@ -388,6 +393,10 @@ class OCCSketch(SketchInterface):
self._external_entity_ids.add(entity_id)
return entity
#: UV distance below which two projected points are considered the same
#: corner when importing / re-projecting external underlay geometry.
_EXTERNAL_MERGE_TOL: float = 1e-6
def add_external_polyline(
self, uv_points: List[Tuple[float, float]]
) -> Tuple[List[OCCSketchEntity], List[OCCSketchEntity]]:
@@ -398,33 +407,108 @@ class OCCSketch(SketchInterface):
``(points, lines)`` in the order they were created so the caller can
keep references (e.g. for rendering or for toggling).
Points very close to each other (within 1e-6 UV units) are merged
into a single shared point, so a closed rectangle becomes 4 unique
points and 4 line segments (not 4 points and 4 lines + 4 duplicates
at the corners).
Points very close to each other (within ``_EXTERNAL_MERGE_TOL`` UV
units) are merged into a single shared point, so a closed rectangle
becomes 4 unique points and 4 line segments (not 4 points and 4
lines + 4 duplicates at the corners). Merging also applies against
*previously imported* external points, so consecutive polylines that
share a corner (separate face edges meeting at a vertex) reuse one
point entity — the corner becomes a single connection hub for
coincident constraints instead of two stacked duplicates.
"""
if len(uv_points) < 2:
return [], []
# Deduplicate nearby points so shared corners (e.g. a rectangle's
# four vertices) are *one* point entity reused by two line segments.
eps = 1e-6
points: List[OCCSketchEntity] = []
coord_to_entity: Dict[Tuple[int, int], OCCSketchEntity] = {}
for (u, v) in uv_points:
key = (int(round(u / eps)), int(round(v / eps)))
ent = coord_to_entity.get(key)
if ent is None:
ent = self.add_external_point(float(u), float(v))
coord_to_entity[key] = ent
points.append(ent)
lines: List[OCCSketchEntity] = []
for i in range(len(points) - 1):
try:
ln = self.add_external_line(points[i], points[i + 1])
lines.append(ln)
except ValueError:
pass
return points, lines
points, lines = self.add_external_polylines([uv_points])
return (points[0] if points else []), lines
def add_external_polylines(
self, polylines: List[List[Tuple[float, float]]]
) -> Tuple[List[List[OCCSketchEntity]], List[OCCSketchEntity]]:
"""Bulk-import several polylines with corner dedup *across* polylines.
A face projection yields one polyline per boundary edge; edges that
meet at a vertex must share a single external point entity, otherwise
every corner ends up as two independent fixed points and user geometry
coincident to one duplicate is *not* connected to geometry coincident
to the other. Returns ``(points_per_polyline, all_lines)``.
"""
tol = self._EXTERNAL_MERGE_TOL
def find_or_create(u: float, v: float) -> OCCSketchEntity:
# Tolerance-based nearest lookup against existing external points
# (entity counts are small — a face boundary has tens of points).
best: Optional[OCCSketchEntity] = None
best_d = tol
for eid in self._external_entity_ids:
ent = self._entities.get(eid)
if ent is None or ent.entity_type != "point" or ent.geometry is None:
continue
d = math.hypot(ent.geometry[0] - u, ent.geometry[1] - v)
if d <= best_d:
best_d = d
best = ent
if best is None:
best = self.add_external_point(float(u), float(v))
return best
all_points: List[List[OCCSketchEntity]] = []
all_lines: List[OCCSketchEntity] = []
for uv_points in polylines:
if len(uv_points) < 2:
continue
points = [find_or_create(float(u), float(v)) for (u, v) in uv_points]
all_points.append(points)
for i in range(len(points) - 1):
if points[i] is points[i + 1]:
continue # degenerate zero-length segment after merging
# Skip duplicate segments (two edges projecting onto the
# same pair of corner points).
dupe = False
for lid, (sid, eid2) in self._lines.items():
if lid not in self._external_entity_ids:
continue
if (sid == points[i].id and eid2 == points[i + 1].id) or (
sid == points[i + 1].id and eid2 == points[i].id
):
dupe = True
break
if dupe:
continue
try:
ln = self.add_external_line(points[i], points[i + 1])
all_lines.append(ln)
except ValueError:
pass
return all_points, all_lines
def _drop_external_entities(self) -> set:
"""Remove external entities from local tracking + prune their constraints.
Does NOT rebuild the solver — the caller decides when to rebuild
(removal-only flows rebuild immediately; re-projection flows first
import the new externals so the rebuild sees them and doesn't
auto-anchor a user point instead).
"""
removed = set(self._external_entity_ids)
if not removed:
return removed
# Wipe external entities from local tracking.
for eid in list(removed):
if eid in self._entities:
del self._entities[eid]
self._points.pop(eid, None)
self._lines.pop(eid, None)
self._circles.pop(eid, None)
self._arcs.pop(eid, None)
# Also clean lines that USE an external point as an endpoint but
# somehow aren't themselves external (defensive — shouldn't happen
# via the public API, but rebuild_solver needs a clean graph).
for lid, (sid, eid2) in list(self._lines.items()):
if sid in removed or eid2 in removed:
del self._lines[lid]
if lid in self._entities:
del self._entities[lid]
self._external_entity_ids.clear()
self._prune_log_for(removed)
return removed
def remove_external_entities(self) -> None:
"""Remove every external / underlay entity and prune related constraints.
@@ -437,25 +521,7 @@ class OCCSketch(SketchInterface):
"""
if not self._external_entity_ids:
return
# Wipe external entities from local tracking.
for eid in list(self._external_entity_ids):
if eid in self._entities:
del self._entities[eid]
self._points.pop(eid, None)
self._lines.pop(eid, None)
self._circles.pop(eid, None)
self._arcs.pop(eid, None)
# Also clean lines that USE an external point as an endpoint but
# somehow aren't themselves external (defensive — shouldn't happen
# via the public API, but rebuild_solver needs a clean graph).
for lid, (sid, eid2) in list(self._lines.items()):
if sid in self._external_entity_ids or eid2 in self._external_entity_ids:
del self._lines[lid]
if lid in self._entities:
del self._entities[lid]
removed = set(self._external_entity_ids)
self._external_entity_ids.clear()
self._prune_log_for(removed)
self._drop_external_entities()
self._rebuild_solver()
self._rebuild_labels()
@@ -463,6 +529,184 @@ class OCCSketch(SketchInterface):
"""Return the set of external (underlay) entity ids currently in the sketch."""
return set(self._external_entity_ids)
def update_external_entities(self, polylines: List[List[Tuple[float, float]]]) -> bool:
"""Re-project external (underlay) entities from updated source geometry.
Called when the 3D body the underlay was projected from has been
rebuilt (e.g. its source sketch was edited and re-extruded) and the
face edges were re-projected to UV. The underlay must follow the
body so user geometry constrained to it propagates through the
solver.
Two paths:
* **In-place update** (same topology): when the new projection has
the same number of unique corner points and segments, each existing
external point is paired with the nearest new position (greedy
one-to-one) and moved via ``set_params``. Entity ids and every
constraint referencing them survive untouched, and the next
:meth:`solve` pulls the user geometry along.
* **Rebuild + rebind** (topology changed): external entities are
removed and re-imported; constraints that referenced external
entities are re-created against the nearest new external entity
(point-to-point for coincident on corners, point-on-line for
coincident on edges) so user geometry stays anchored.
Returns True when the underlay was updated and solved OK.
"""
# Flatten the new projection into unique corner positions + segments.
tol = self._EXTERNAL_MERGE_TOL
new_pts: List[Tuple[float, float]] = []
def new_index(u: float, v: float) -> int:
for i, (x, y) in enumerate(new_pts):
if math.hypot(x - u, y - v) <= tol:
return i
new_pts.append((float(u), float(v)))
return len(new_pts) - 1
new_segs: List[Tuple[int, int]] = []
for poly in polylines:
if len(poly) < 2:
continue
idx = [new_index(float(u), float(v)) for (u, v) in poly]
for i in range(len(idx) - 1):
if idx[i] == idx[i + 1]:
continue
seg = (min(idx[i], idx[i + 1]), max(idx[i], idx[i + 1]))
if seg not in new_segs:
new_segs.append(seg)
old_ext_points = [
self._entities[eid]
for eid in sorted(self._external_entity_ids)
if eid in self._entities and self._entities[eid].entity_type == "point"
]
old_ext_lines = [
lid for lid in sorted(self._lines.keys()) if lid in self._external_entity_ids
]
same_topology = len(new_pts) == len(old_ext_points) and len(new_segs) == len(old_ext_lines)
if same_topology and old_ext_points:
# Greedy one-to-one nearest matching old point -> new position.
pairs: List[Tuple[float, int, int]] = [] # (dist, old_idx, new_idx)
for oi, ent in enumerate(old_ext_points):
ox, oy = ent.geometry
for ni, (nx, ny) in enumerate(new_pts):
pairs.append((math.hypot(ox - nx, oy - ny), oi, ni))
pairs.sort()
match: Dict[int, int] = {}
used_new: set = set()
for d, oi, ni in pairs:
if oi in match or ni in used_new:
continue
match[oi] = ni
used_new.add(ni)
if len(match) == len(old_ext_points):
# Verify segment connectivity is preserved under the matching
# (same corners, but edges rewired -> rebuild instead).
mapped_segs = set()
for a, b in new_segs:
mapped_segs.add((a, b))
connectivity_ok = True
for lid in old_ext_lines:
sid, eid2 = self._lines[lid]
oi_s = next((i for i, e in enumerate(old_ext_points) if e.id == sid), None)
oi_e = next((i for i, e in enumerate(old_ext_points) if e.id == eid2), None)
if oi_s is None or oi_e is None:
connectivity_ok = False
break
seg = (min(match[oi_s], match[oi_e]), max(match[oi_s], match[oi_e]))
if seg not in mapped_segs:
connectivity_ok = False
break
if connectivity_ok:
for oi, ent in enumerate(old_ext_points):
nx, ny = new_pts[match[oi]]
self.set_entity_position(ent, nx, ny)
return self.solve()
# ── Rebuild + rebind path (topology changed, or no externals yet) ──
# Capture constraints that tie USER entities to external entities so
# they can be re-created against the nearest new external entity.
saved: List[Dict[str, Any]] = []
for entry in self._constraint_log:
ext_ids = [i for i in entry["ids"] if i in self._external_entity_ids]
user_ids = [i for i in entry["ids"] if i not in self._external_entity_ids]
if not ext_ids or not user_ids:
continue
for ext_id in ext_ids:
ent = self._entities.get(ext_id)
if ent is None:
continue
if ent.entity_type == "point" and ent.geometry is not None:
anchor: Any = ("point", ext_id, tuple(ent.geometry))
elif ent.entity_type == "line" and ext_id in self._lines:
sid, eid2 = self._lines[ext_id]
s_ent = self._entities.get(sid)
e_ent = self._entities.get(eid2)
if s_ent is None or e_ent is None:
continue
mx = (s_ent.geometry[0] + e_ent.geometry[0]) / 2.0
my = (s_ent.geometry[1] + e_ent.geometry[1]) / 2.0
anchor = ("line", ext_id, (mx, my))
else:
continue
saved.append({"type": entry["type"], "user_ids": list(user_ids), "anchor": anchor})
# Drop old externals, import the new projection, then rebuild the
# solver exactly once. The import MUST happen before the rebuild:
# with external ids present the rebuild re-fixes the new underlay
# points and (per the add_point guard) does not auto-anchor a user
# point — which would conflict with the re-bound coincidents below.
self._drop_external_entities()
self.add_external_polylines(polylines)
self._rebuild_solver()
self._rebuild_labels()
# Rebind saved constraints to the nearest new external entity.
rebound = 0
for item in saved:
kind, _old_id, pos = item["anchor"]
target: Optional[OCCSketchEntity] = None
best_d = float("inf")
if kind == "point":
for eid in self._external_entity_ids:
ent = self._entities.get(eid)
if ent is None or ent.entity_type != "point" or ent.geometry is None:
continue
d = math.hypot(ent.geometry[0] - pos[0], ent.geometry[1] - pos[1])
if d < best_d:
best_d = d
target = ent
else: # line: nearest segment midpoint
for lid, (sid, eid2) in self._lines.items():
if lid not in self._external_entity_ids:
continue
s_ent = self._entities.get(sid)
e_ent = self._entities.get(eid2)
if s_ent is None or e_ent is None:
continue
mx = (s_ent.geometry[0] + e_ent.geometry[0]) / 2.0
my = (s_ent.geometry[1] + e_ent.geometry[1]) / 2.0
d = math.hypot(mx - pos[0], my - pos[1])
if d < best_d:
best_d = d
target = self._entities.get(lid)
if target is None:
continue
for uid in item["user_ids"]:
user_ent = self._entities.get(uid)
if user_ent is None:
continue
if item["type"] == "coincident":
if self.constrain_coincident(user_ent, target):
rebound += 1
if rebound:
logger.info("Rebound %d constraint(s) to re-projected underlay", rebound)
return self.solve()
# ── Centerlines (X and Y reference axes) ────────────────────────────
_CENTERLINE_EXTENT: float = 10000.0 # large enough to span any sketch
@@ -514,8 +758,13 @@ class OCCSketch(SketchInterface):
self.constrain_fixed(yb)
self._centerline_ids = {
origin.id, xl.id, xr.id, xline.id,
yb.id, yt.id, yline.id,
origin.id,
xl.id,
xr.id,
xline.id,
yb.id,
yt.id,
yline.id,
}
self.solve()
@@ -557,7 +806,12 @@ class OCCSketch(SketchInterface):
"""Count and log a constraint so the solver can be rebuilt after deletions."""
self._constraint_count += 1
self._constraint_log.append(
{"type": ctype, "ids": tuple(int(i) for i in ids), "params": tuple(params), "labels": set(labels)}
{
"type": ctype,
"ids": tuple(int(i) for i in ids),
"params": tuple(params),
"labels": set(labels),
}
)
def _apply_constraint_log(self, entry: Dict[str, Any]) -> bool:
@@ -666,7 +920,16 @@ class OCCSketch(SketchInterface):
x, y = saved_pos.get(pid, (0.0, 0.0))
new_handle = self._solver.add_point_2d(x, y, self._wp)
ent.handle = new_handle
if self._first_point_id is None:
if pid in self._external_entity_ids:
# External (underlay) points are ALWAYS fixed — the dragged
# applied at creation isn't in the constraint log, so it must
# be re-applied here or the underlay becomes draggable after
# any solver rebuild (e.g. deleting an unrelated user point).
self._solver.dragged(new_handle, self._wp)
elif self._first_point_id is None and not self._external_entity_ids:
# Mirror add_point's guard: when the sketch carries external
# underlay points those are the natural anchors, and fixing a
# user point too would over-constrain the system.
self._first_point_id = pid
self._solver.dragged(new_handle, self._wp)
@@ -1008,7 +1271,12 @@ class OCCSketch(SketchInterface):
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 and not entity.is_construction:
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] = []
@@ -1043,7 +1311,7 @@ class OCCSketch(SketchInterface):
# ─── Closed-loop / face detection (for region selection + holes) ──────
_SNAP_TOL: float = 1e-4 # world-unit tolerance for snapping line endpoints
_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]]]:
"""Current line segments as world-coordinate tuples (uses solved positions).
@@ -1068,8 +1336,12 @@ class OCCSketch(SketchInterface):
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]))))
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():
@@ -1086,8 +1358,9 @@ class OCCSketch(SketchInterface):
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):
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
@@ -1099,10 +1372,8 @@ class OCCSketch(SketchInterface):
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))
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
@@ -1180,8 +1451,13 @@ class OCCSketch(SketchInterface):
for cid, (center_id, r) in self._circles.items():
c_ent = self._entities.get(center_id)
if c_ent and c_ent.geometry and r > 0:
loops.append({"type": "circle", "center": (float(c_ent.geometry[0]), float(c_ent.geometry[1])),
"radius": float(r)})
loops.append(
{
"type": "circle",
"center": (float(c_ent.geometry[0]), float(c_ent.geometry[1])),
"radius": float(r),
}
)
return loops
@staticmethod
@@ -1212,7 +1488,10 @@ class OCCSketch(SketchInterface):
# Point-on-segment test — exclude strict boundary hits.
# First check bounding box of the segment.
bbox_tol = max(eps, margin)
if min(xi, xj) - bbox_tol <= x <= max(xi, xj) + bbox_tol and min(yi, yj) - bbox_tol <= y <= max(yi, yj) + bbox_tol:
if (
min(xi, xj) - bbox_tol <= x <= max(xi, xj) + bbox_tol
and min(yi, yj) - bbox_tol <= y <= max(yi, yj) + bbox_tol
):
# Check collinearity
cross = (x - xi) * (yj - yi) - (y - yi) * (xj - xi)
abs_cross = abs(cross)
@@ -1269,18 +1548,14 @@ class OCCSketch(SketchInterface):
if inner["type"] == "circle":
# Circle in polygon: centre must be inside with margin
cx, cy = inner["center"]
return OCCSketch._point_in_polygon(
(cx, cy), outer["points"], margin=1e-3
)
return OCCSketch._point_in_polygon((cx, cy), outer["points"], margin=1e-3)
else:
# Polygon in polygon: ALL inner vertices inside outer
pts = inner["points"]
if len(pts) > 1 and pts[0] == pts[-1]:
pts = pts[:-1]
for pt in pts:
if not OCCSketch._point_in_polygon(
pt, outer["points"], margin=eps
):
if not OCCSketch._point_in_polygon(pt, outer["points"], margin=eps):
return False
return True
@@ -1293,7 +1568,11 @@ class OCCSketch(SketchInterface):
the centre for circles.
"""
if loop["type"] == "polygon":
pts = loop["points"][:-1] if len(loop["points"]) > 1 and loop["points"][0] == loop["points"][-1] else loop["points"]
pts = (
loop["points"][:-1]
if len(loop["points"]) > 1 and loop["points"][0] == loop["points"][-1]
else loop["points"]
)
n = len(pts)
if n < 3:
return loop.get("center", (0.0, 0.0))
@@ -1374,11 +1653,13 @@ class OCCSketch(SketchInterface):
for h in face["holes"]:
if h["type"] == "polygon":
if OCCSketch._point_in_polygon(pt, h["points"]):
in_hole = True; break
in_hole = True
break
else:
hcx, hcy = h["center"]
if math.hypot(pt[0] - hcx, pt[1] - hcy) < h["radius"]:
in_hole = True; break
in_hole = True
break
if in_hole:
continue
area = OCCSketch._loop_area(outer)
@@ -1426,17 +1707,19 @@ class OCCSketch(SketchInterface):
from top-left to bottom-right.
"""
from OCP.BRepBuilderAPI import (
BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeWire, BRepBuilderAPI_MakeEdge,
BRepBuilderAPI_MakePolygon,
BRepBuilderAPI_MakeFace,
BRepBuilderAPI_MakeWire,
BRepBuilderAPI_MakeEdge,
)
from OCP.gp import gp_Pnt, gp_Circ, gp_Ax2, gp_Dir
from OCP.gp import gp_Circ
from OCP.TopoDS import TopoDS as _TopoDS
def _wire_from_loop(loop: Dict[str, Any]):
"""Build a wire from a loop dict. No orientation adjustment."""
if loop["type"] == "polygon":
mp = BRepBuilderAPI_MakePolygon()
for (pu, pv) in loop["points"]:
for pu, pv in loop["points"]:
mp.Add(self._uv_to_world(pu, pv))
mp.Close()
mp.Build()
@@ -1463,17 +1746,22 @@ class OCCSketch(SketchInterface):
# wire (material on the other side). We reverse the hole wire
# only when its natural winding matches the outer's; if they
# already differ the wire is left as-is.
if (hole_winding >= 0 and outer_winding >= 0) or (hole_winding < 0 and outer_winding < 0):
if (hole_winding >= 0 and outer_winding >= 0) or (
hole_winding < 0 and outer_winding < 0
):
hole_wire = _TopoDS.Wire_s(hole_wire.Reversed())
face_maker.Add(hole_wire)
face_maker.Build()
occ_face = face_maker.Face()
obj = OCCGeometryObject(occ_face, {
"type": "sketch_face",
"normal": self._wp_normal,
"origin": self._wp_origin,
})
obj = OCCGeometryObject(
occ_face,
{
"type": "sketch_face",
"normal": self._wp_normal,
"origin": self._wp_origin,
},
)
return obj
def get_solver_dof(self) -> int:
@@ -1579,7 +1867,8 @@ class OCCSketch(SketchInterface):
removed_ids: set = {point.id}
# Remove lines that use this point as an endpoint.
removed_line_keys: List[int] = [
lid for lid, (sid, eid2) in list(self._lines.items())
lid
for lid, (sid, eid2) in list(self._lines.items())
if sid == point.id or eid2 == point.id
]
for lid in removed_line_keys:
@@ -1593,8 +1882,7 @@ class OCCSketch(SketchInterface):
del self._entities[point.id]
# Circles anchored on the point are also invalid.
removed_circle_keys: List[int] = [
cid for cid, (center_id, _r) in list(self._circles.items())
if center_id == point.id
cid for cid, (center_id, _r) in list(self._circles.items()) if center_id == point.id
]
for cid in removed_circle_keys:
removed_ids.add(cid)
@@ -1603,7 +1891,8 @@ class OCCSketch(SketchInterface):
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())
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
@@ -1820,9 +2109,7 @@ class OCCSketch(SketchInterface):
s_id = self._find_point_at(x1, y1)
e_id = self._find_point_at(x2, y2)
if s_id is None or e_id is None:
logger.warning(
"Skipping line %s during load: endpoints not found", eid
)
logger.warning("Skipping line %s during load: endpoints not found", eid)
continue
if is_external:
ent = self.add_external_line(entities_by_id[s_id], entities_by_id[e_id])
@@ -1832,9 +2119,7 @@ class OCCSketch(SketchInterface):
(cx, cy), radius = geom
c_id = self._find_point_at(cx, cy)
if c_id is None:
logger.warning(
"Skipping circle %s during load: center not found", eid
)
logger.warning("Skipping circle %s during load: center not found", eid)
continue
ent = self.add_circle(entities_by_id[c_id], float(radius))
elif etype == "arc":
@@ -1847,9 +2132,7 @@ class OCCSketch(SketchInterface):
s_id = self._find_point_at(*start_pos)
e_id = self._find_point_at(*end_pos)
if c_id is None or s_id is None or e_id is None:
logger.warning(
"Skipping arc %s during load: endpoints not found", eid
)
logger.warning("Skipping arc %s during load: endpoints not found", eid)
continue
ent = self.add_arc(
entities_by_id[c_id],