"""Headless test: update_external_entities handles circle/arc dict entries. Reproduces the crash where a re-projected face contains a circular edge (e.g. an instance cut hole): _project_face_to_uv returns a mixed list of polylines + curve dicts, and update_external_entities used to unpack the dict entries as (u, v) tuples. Covers: 1. mixed projection (polylines + circle dict) -> rebuild + rebind path, no crash, no duplicate/orphan curve entities, user geometry re-anchored. 2. repeated update with the same mixed projection -> stable (idempotent). 3. polylines-only same-topology projection -> in-place path still works (external ids preserved). """ import math from fluency.geometry_occ.sketch import OCCSketch RECT = [ [(0.0, 0.0), (10.0, 0.0)], [(10.0, 0.0), (10.0, 10.0)], [(10.0, 10.0), (0.0, 10.0)], [(0.0, 10.0), (0.0, 0.0)], ] def _counts(sk): ents = list(sk._entities.values()) return { "ext_points": sum( 1 for e in ents if e.entity_type == "point" and getattr(e, "is_external", False) ), "ext_lines": sum( 1 for e in ents if e.entity_type == "line" and getattr(e, "is_external", False) ), "circles": sum(1 for e in ents if e.entity_type == "circle"), "arcs": sum(1 for e in ents if e.entity_type == "arc"), "user_points": sum( 1 for e in ents if e.entity_type == "point" and not getattr(e, "is_external", False) ), } def test_mixed_projection_rebuild(): sk = OCCSketch() sk.add_external_polylines([list(p) for p in RECT]) center = sk.add_external_point(5.0, 5.0) sk.add_circle(center, 2.0) user = sk.add_point(5.0, 5.0) assert sk.constrain_coincident(user, center) assert sk.solve() # Re-projection: same rectangle, circle moved + resized -> mixed list. new_proj = [ list(p) for p in RECT ] + [ {"type": "circle", "center": [6.0, 6.0], "radius": 1.5}, ] old_ext_ids = set(sk._external_entity_ids) assert sk.update_external_entities(new_proj), "rebuild + rebind solve failed" c = _counts(sk) assert c["circles"] == 1, f"duplicate circle entities: {c}" assert c["ext_points"] == 5, f"ext point count wrong (expect 4 corners + 1 centre): {c}" assert c["ext_lines"] == 4, f"ext line count wrong (expect 4): {c}" assert c["user_points"] == 1 # The coincident rebind must anchor the user point to the NEW centre. ux, uy = user.geometry assert math.hypot(ux - 6.0, uy - 6.0) < 1e-6, f"user point at {(ux, uy)}" # Rebuild path: fresh external ids. assert not (old_ext_ids & sk._external_entity_ids) # Idempotent second pass with the same projection. assert sk.update_external_entities(list(new_proj)), "second pass failed" c2 = _counts(sk) assert c2 == c, f"counts changed on second pass: {c} -> {c2}" ux, uy = user.geometry assert math.hypot(ux - 6.0, uy - 6.0) < 1e-6 print("test_mixed_projection_rebuild OK") def test_polylines_only_inplace(): sk = OCCSketch() sk.add_external_polylines([list(p) for p in RECT]) corner = None for eid in sk._external_entity_ids: ent = sk._entities[eid] if ent.entity_type == "point" and ent.geometry == (0.0, 0.0): corner = ent break assert corner is not None user = sk.add_point(0.0, 0.0) assert sk.constrain_coincident(user, corner) assert sk.solve() # Same topology, slightly shifted rectangle -> in-place move. moved = [[(u + 1.0, v + 2.0) for (u, v) in poly] for poly in RECT] old_ext_ids = set(sk._external_entity_ids) assert sk.update_external_entities(moved), "in-place solve failed" assert sk._external_entity_ids == old_ext_ids, "in-place path must keep ids" ux, uy = user.geometry assert math.hypot(ux - 1.0, uy - 2.0) < 1e-6, f"user point at {(ux, uy)}" print("test_polylines_only_inplace OK") def test_arc_import_shares_corners(): """_import_external_curves must merge arc endpoints with existing polyline corner points (no floating duplicate endpoints).""" sk = OCCSketch() # Rectangle with the top-right corner filleted: the arc endpoints must # land on the truncated-edge corner points, not create new ones. r = 2.0 sk.add_external_polylines([ [(0.0, 0.0), (10.0, 0.0)], [(10.0, 0.0), (10.0, 10.0 - r)], [(10.0 - r, 10.0), (0.0, 10.0)], [(0.0, 10.0), (0.0, 0.0)], ]) sk._import_external_curves( [], [ { "type": "arc", "center": [10.0 - r, 10.0 - r], "start": [10.0, 10.0 - r], "end": [10.0 - r, 10.0], "radius": r, }, ], ) c = _counts(sk) # 5 corners + 1 arc centre, NO extra endpoint entities. assert c["ext_points"] == 6, f"expected 6 ext points, got {c}" assert c["arcs"] == 1, f"expected 1 arc, got {c}" assert sk.solve() print("test_arc_import_shares_corners OK") if __name__ == "__main__": test_mixed_projection_rebuild() test_polylines_only_inplace() test_arc_import_shares_corners() print("UNDERLAY_CURVES_OK")