Port from snaporca: sketch-only projects save, scripted geometry arrives exact,

and a ladder that draws with the mouse

Three commits carried across (snaporca 4ffd60eacb, 421055c2ec, b71216ce0b):

1. A design made only of sketches must survive being saved. CadDocument::recompute
   returned false with "no solid-producing features" for a document that has no
   solid, and two callers read that as "unusable": the GUI syncs the 3MF recipe
   only after a successful recompute, so a sketch-only design was saved with no
   recipe at all, and deserialize_recipe ends with `return recompute()`, so even a
   project that carried one was refused on load. Having nothing to build is now a
   success; a feature that MEANT to build a solid and produced none still fails.
   DesignPanel::refresh_tree syncs the recipe too, for the paths that call
   m_doc.recompute() directly.

2. Scripted geometry arrives exact. The Horizontal/Vertical inference window and
   the endpoint weld window both close to zero for add_entities_scripted; void
   attribution probes from a point strictly inside each loop instead of from its
   first vertex. Corpus rung 39 graded / 39 fully clean, was 35 with 6 failures.

3. scripts/gui-ladder.py — 17 rungs, 84 properties, all driven by synthetic clicks
   and typed values rather than through the socket.

Parity 17 identical / 8 diverging as expected. Kernel suite here: 188 cases /
2532 assertions.

snaporca-mtav, snaporca-8xg1, snaporca-5hvl, snaporca-730j
This commit is contained in:
Tommaso Bianchi
2026-08-23 01:22:32 +02:00
parent 05ce2607a8
commit 82db99f337
7 changed files with 1211 additions and 11 deletions
File diff suppressed because it is too large Load Diff
+34 -1
View File
@@ -202,6 +202,33 @@ def point_in(pt, ring):
return inside
def interior_point(ring):
"""A point strictly inside a simple closed ring (first == last).
The lowest vertex of a simple polygon is always convex, so stepping from it along the
bisector of its two edges goes inward; the step is a small fraction of the shorter edge so
it stays inside however sharp the corner is.
"""
q = ring[:-1] if len(ring) > 1 and ring[0] == ring[-1] else ring
if len(q) < 3:
return ring[0]
k = min(range(len(q)), key=lambda i: (q[i][1], q[i][0]))
v = q[k]
a = (q[(k - 1) % len(q)][0] - v[0], q[(k - 1) % len(q)][1] - v[1])
b = (q[(k + 1) % len(q)][0] - v[0], q[(k + 1) % len(q)][1] - v[1])
la, lb = math.hypot(*a), math.hypot(*b)
if la < 1e-12 or lb < 1e-12:
return v
a = (a[0] / la, a[1] / la)
b = (b[0] / lb, b[1] / lb)
bx, by = a[0] + b[0], a[1] + b[1]
n = math.hypot(bx, by)
if n < 1e-12:
return v
step = 1e-3 * min(la, lb)
return (v[0] + bx / n * step, v[1] + by / n * step)
# ── one drawing ──────────────────────────────────────────────────────────────
def grade(pdf, name, report):
segs = drawing_segments(pdf)
@@ -258,11 +285,17 @@ def grade(pdf, name, report):
# void. So compute the same rule here, independently, and compare the whole attribution.
if got:
rings = [outer] + voids
# Probe from a point STRICTLY INSIDE each ring, never from one of its vertices — the
# same rule the engine now uses (DesignSketchTool::region_loops). A vertex is exactly
# where two loops touch in a real drawing, and a ray cast from a point lying ON the
# polygon under test answers by rounding: that alone accounted for every one of the 6
# sheets where the two attributions used to disagree. snaporca-5hvl.
probes = [interior_point(r) for r in rings]
mine_parent = {}
for i, r in enumerate(rings):
best, best_a = -1, 0.0
for j, q in enumerate(rings):
if i == j or not point_in(r[0], q):
if i == j or not point_in(probes[i], q):
continue
a = shoelace(q)
if best < 0 or a < best_a: