clean-geometry
Make the currently selected Rhino curves CAD-clean. Sibling to clean-similar-curves
(which only removes duplicates/overlaps). Same architecture: two scripts next to this file,
invoked via a one-line loader; a non-destructive cleangeo_diagnose.py stashes a plan in
sc.sticky; a destructive cleangeo_apply.py applies ONE category chosen by a KEY variable.
Checks
- A simplify —
Curve.Simplify (merge colinear segs, straighten near-lines, arcs, cull knots). Lossless-ish. KEY="simplify".
- B short → join / orphan — curves shorter than
SHORT_LEN; if an endpoint meets another curve → JOIN cluster; if isolated → ORPHAN. KEY="join" (safe); KEY="orphan" deletes isolated shorts (opt-in).
- C messy → tidy — curves with >
CP_MAX control points fit to line/arc/circle/ellipse within FIT_TOL (conservative; freeforms protected by a residual gate). KEY="tidy".
- D near-orthogonal — lines within
ORTHO_ANG of world X/Y but not exact. REPORT-ONLY in v1 (no apply).
- E snap-gap / burst corner — endpoint near-misses (
tol < dist ≤ GAP_TOL) that should meet. Fixed the user's way: Fillet radius=0, trim=true, join=true (Curve.CreateFilletCurves). KEY="gap".
Procedure (do in order)
- Resolve paths. Glob
**/clean-geometry/cleangeo_diagnose.py and **/clean-geometry/cleangeo_apply.py. Do NOT hardcode.
- Diagnose (non-destructive).
exec(open(r"<ABS>\cleangeo_diagnose.py").read())
Prints per-check counts + a length histogram. SHORT_LEN defaults to None → check B is skipped until set.
- Set
SHORT_LEN from the histogram (pick a value below the main length cluster) and re-run diagnose to populate B:SHORT_LEN=2.0; exec(open(r"<ABS>\cleangeo_diagnose.py").read())
- Confirm scope with AskUserQuestion. Note risk: A/C use
Replace (guid-stable, ≤ tol / ≤ FIT_TOL); B join is lossless topology change; orphan deletes (opt-in); E gap moves endpoints (extend/trim). D is report-only.
- Apply, one KEY at a time, recommended order:
KEY="simplify"; exec(open(r"<ABS>\cleangeo_apply.py").read())
KEY="tidy"; exec(open(r"<ABS>\cleangeo_apply.py").read())
KEY="join"; exec(open(r"<ABS>\cleangeo_apply.py").read())
KEY="gap"; exec(open(r"<ABS>\cleangeo_apply.py").read())
# optional: KEY="orphan"; ... (deletes isolated short curves)
Re-diagnose after the Replace stages (simplify, tidy) and before endpoint-moving stages (join, gap) for a clean plan — Replace shifts endpoints by ≤ tol; a fresh plan avoids stale clusters.
- Report created/modified/deleted/failed. Recommend re-running
clean-similar-curves after gap (fillet may create tiny overlaps). Offer memory update.
Critical gotchas (baked in — do not "fix" out)
Simplify angle arg is RADIANS (ModelAngleToleranceRadians), despite Rhinoscript's degrees API.
- A must run before C. Simplify lowers control-point count; the C "too many CPs" gate is evaluated on the simplified curve (diagnose does this internally). Applying
tidy before simplify still works (tidy is self-contained) but re-diagnosing keeps counts honest.
- C is conservative by design: CP_MAX gate +
FIT_TOL + GetDistancesBetweenCurves residual gate + new-CP-count-must-drop. A wandering freeform near no primitive is correctly LEFT ALONE. No Rebuild/Fit CP-reduction fallback in v1.
- B join edges are short-incident only — two long curves sharing an endpoint are NOT auto-joined; only clusters containing a short curve are. Prevents over-joining the whole network.
- Orphan = no neighbor within the selection. A real continuation that isn't selected looks orphaned. Never auto-deleted;
KEY="orphan" is opt-in.
- E excludes settled endpoints (already coincident within
tol) so closing a gap never rips apart an existing join. GAP_TOL > SNAP_TOL keeps the join band (B) and gap band (E) disjoint.
- A/C use live
rg.Curve objects stashed in sc.sticky (cleangeo_geo_simplify, cleangeo_geo_tidy) — general curves can't be expressed as p0/p1. Apply = sc.doc.Objects.Replace(guid, curve); keeps Id, layer, attributes, selection. Diagnose==apply.
- Created objects inherit donor attributes (
Attributes.Duplicate()) and are re-selected; join/gap donor = the longest member.
- IronPython 2.7:
TryGet* / GetDistancesBetweenCurves / CreateFilletCurves return tuples/arrays; use [0] for the bool. Float division. rg.CurveSimplifyOptions.All.
Parameters
Top of cleangeo_diagnose.py, # ---- params ----: SHORT_LEN (None→histogram, TUNE), SIMPLIFY_DIST/ANG,
SNAP_TOL, CP_MAX (30), FIT_TOL, ORTHO_ANG (2°), GAP_TOL (20×tol; must be > SNAP_TOL). None → doc tolerance.
Not yet implemented (deferred to v2)
- D near-orthogonal APPLY (report-only in v1 — endpoint snap-to-axis breaks networks; design is stashed in
plan['ortho']).
- E extend-to-intersection is via Fillet; parallel/collinear near-miss uses a midpoint-pull fallback.
- Non-curve types (surface/brep/mesh); T-junction / mid-curve endpoint snapping; aggressive Rebuild CP reduction; multi-primitive decomposition of one messy curve.
Reference
Built 2026-07-01 as a sibling to clean-similar-curves. v1 scope decided with user: A/B/C + E fix, D report-only,
conservative tidy, histogram-driven SHORT_LEN, opt-in orphan delete. See memory rhino-clean-geometry-method
and the dedup sibling rhino-curve-dedup-method. Verified by an in-doc synthetic test (per-check exact-geometry asserts).
1---2name: clean-geometry3description: Make the currently-selected Rhino curves into clean, CAD-usable geometry. Five checks on the selection — (A) simplify curves, (B) resolve too-short curves by joining them to a neighbor or flagging them as orphans, (C) tidy messy over-noded curves into primitives (line/arc/circle/ellipse) within tolerance, (D) flag lines that are almost-but-not orthogonal (report-only), (E) close burst/open corners where a snap is slightly off (via Fillet radius=0 / trim / join). Non-destructive diagnose+dry-run first, reports each category, confirms scope, then applies per category. Trigger when the user wants to clean up / tidy / fix / simplify geometry, close open corners, join short segments, or make CAD-clean linework on the current Rhino selection. Fire on Korean or English requests, e.g. "지오메트리 정리", "geometry 정리", "깔끔하게 정리", "커브 심플리파이", "짧은 선 정리", "터진 모서리 붙여줘", "모서리 스냅 맞춰줘", "clean geometry", "tidy curves", "simplify curves", "close open corners", "join short segments". For pure duplicate/overlap removal use the sibling4---56# clean-geometry78Make the **currently selected** Rhino curves CAD-clean. Sibling to `clean-similar-curves`9(which only removes duplicates/overlaps). Same architecture: two scripts next to this file,10invoked via a one-line loader; a non-destructive `cleangeo_diagnose.py` stashes a plan in11`sc.sticky`; a destructive `cleangeo_apply.py` applies ONE category chosen by a `KEY` variable.1213## Checks14- **A simplify** — `Curve.Simplify` (merge colinear segs, straighten near-lines, arcs, cull knots). Lossless-ish. `KEY="simplify"`.15- **B short → join / orphan** — curves shorter than `SHORT_LEN`; if an endpoint meets another curve → JOIN cluster; if isolated → ORPHAN. `KEY="join"` (safe); `KEY="orphan"` deletes isolated shorts (opt-in).16- **C messy → tidy** — curves with > `CP_MAX` control points fit to line/arc/circle/ellipse within `FIT_TOL` (conservative; freeforms protected by a residual gate). `KEY="tidy"`.17- **D near-orthogonal** — lines within `ORTHO_ANG` of world X/Y but not exact. **REPORT-ONLY in v1** (no apply).18- **E snap-gap / burst corner** — endpoint near-misses (`tol < dist ≤ GAP_TOL`) that should meet. Fixed the user's way: **Fillet radius=0, trim=true, join=true** (`Curve.CreateFilletCurves`). `KEY="gap"`.1920## Procedure (do in order)211. **Resolve paths.** Glob `**/clean-geometry/cleangeo_diagnose.py` and `**/clean-geometry/cleangeo_apply.py`. Do NOT hardcode.222. **Diagnose (non-destructive).**23 ```python24 exec(open(r"<ABS>\cleangeo_diagnose.py").read())25 ```26 Prints per-check counts + a **length histogram**. `SHORT_LEN` defaults to `None` → check B is skipped until set.273. **Set `SHORT_LEN` from the histogram** (pick a value below the main length cluster) and re-run diagnose to populate B:28 ```python29 SHORT_LEN=2.0; exec(open(r"<ABS>\cleangeo_diagnose.py").read())30 ```314. **Confirm scope** with AskUserQuestion. Note risk: A/C use `Replace` (guid-stable, ≤ tol / ≤ FIT_TOL); B `join` is lossless topology change; `orphan` deletes (opt-in); E `gap` moves endpoints (extend/trim). D is report-only.325. **Apply, one KEY at a time**, recommended order:33 ```python34 KEY="simplify"; exec(open(r"<ABS>\cleangeo_apply.py").read())35 KEY="tidy"; exec(open(r"<ABS>\cleangeo_apply.py").read())36 KEY="join"; exec(open(r"<ABS>\cleangeo_apply.py").read())37 KEY="gap"; exec(open(r"<ABS>\cleangeo_apply.py").read())38 # optional: KEY="orphan"; ... (deletes isolated short curves)39 ```40 **Re-diagnose after the Replace stages (simplify, tidy) and before endpoint-moving stages (join, gap)** for a clean plan — Replace shifts endpoints by ≤ tol; a fresh plan avoids stale clusters.416. **Report** created/modified/deleted/failed. Recommend re-running `clean-similar-curves` after `gap` (fillet may create tiny overlaps). Offer memory update.4243## Critical gotchas (baked in — do not "fix" out)44- **`Simplify` angle arg is RADIANS** (`ModelAngleToleranceRadians`), despite Rhinoscript's degrees API.45- **A must run before C.** Simplify lowers control-point count; the C "too many CPs" gate is evaluated on the *simplified* curve (diagnose does this internally). Applying `tidy` before `simplify` still works (tidy is self-contained) but re-diagnosing keeps counts honest.46- **C is conservative by design:** CP_MAX gate + `FIT_TOL` + `GetDistancesBetweenCurves` residual gate + new-CP-count-must-drop. A wandering freeform near no primitive is correctly LEFT ALONE. No `Rebuild`/`Fit` CP-reduction fallback in v1.47- **B join edges are short-incident only** — two long curves sharing an endpoint are NOT auto-joined; only clusters containing a short curve are. Prevents over-joining the whole network.48- **Orphan = no neighbor *within the selection*.** A real continuation that isn't selected looks orphaned. Never auto-deleted; `KEY="orphan"` is opt-in.49- **E excludes settled endpoints** (already coincident within `tol`) so closing a gap never rips apart an existing join. `GAP_TOL > SNAP_TOL` keeps the join band (B) and gap band (E) disjoint.50- **A/C use live `rg.Curve` objects stashed in `sc.sticky`** (`cleangeo_geo_simplify`, `cleangeo_geo_tidy`) — general curves can't be expressed as `p0/p1`. Apply = `sc.doc.Objects.Replace(guid, curve)`; keeps Id, layer, attributes, selection. Diagnose==apply.51- **Created objects inherit donor attributes** (`Attributes.Duplicate()`) and are re-selected; join/gap donor = the longest member.52- **IronPython 2.7:** `TryGet*` / `GetDistancesBetweenCurves` / `CreateFilletCurves` return tuples/arrays; use `[0]` for the bool. Float division. `rg.CurveSimplifyOptions.All`.5354## Parameters55Top of `cleangeo_diagnose.py`, `# ---- params ----`: `SHORT_LEN` (None→histogram, TUNE), `SIMPLIFY_DIST/ANG`,56`SNAP_TOL`, `CP_MAX` (30), `FIT_TOL`, `ORTHO_ANG` (2°), `GAP_TOL` (20×tol; must be > SNAP_TOL). `None` → doc tolerance.5758## Not yet implemented (deferred to v2)59- **D near-orthogonal APPLY** (report-only in v1 — endpoint snap-to-axis breaks networks; design is stashed in `plan['ortho']`).60- **E extend-to-intersection is via Fillet**; parallel/collinear near-miss uses a midpoint-pull fallback.61- Non-curve types (surface/brep/mesh); T-junction / mid-curve endpoint snapping; aggressive Rebuild CP reduction; multi-primitive decomposition of one messy curve.6263## Reference64Built 2026-07-01 as a sibling to `clean-similar-curves`. v1 scope decided with user: A/B/C + E fix, D report-only,65conservative tidy, histogram-driven SHORT_LEN, opt-in orphan delete. See memory `rhino-clean-geometry-method`66and the dedup sibling `rhino-curve-dedup-method`. Verified by an in-doc synthetic test (per-check exact-geometry asserts).