9abeb6266a
- assembly forward proagation
150 lines
5.2 KiB
Python
150 lines
5.2 KiB
Python
"""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")
|