- Assembly instaiated operations
- assembly forward proagation
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user