# Clean Geometry

> 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 sibling

- Skill: `hongikarchi/clean-geometry` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add hongikarchi/clean-geometry`
- Raw SKILL.md: https://api.skillmd.com/api/skills/hongikarchi/clean-geometry/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: hongikarchi (https://skillmd.com/u/hongikarchi)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/hongikarchi/clean-geometry

---


# 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)
1. **Resolve paths.** Glob `**/clean-geometry/cleangeo_diagnose.py` and `**/clean-geometry/cleangeo_apply.py`. Do NOT hardcode.
2. **Diagnose (non-destructive).**
   ```python
   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.
3. **Set `SHORT_LEN` from the histogram** (pick a value below the main length cluster) and re-run diagnose to populate B:
   ```python
   SHORT_LEN=2.0; exec(open(r"<ABS>\cleangeo_diagnose.py").read())
   ```
4. **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.
5. **Apply, one KEY at a time**, recommended order:
   ```python
   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.
6. **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).

