- added "measurement lines"
This commit is contained in:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user