# Copyright 2026 Hugging Face # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Self-contained "Metrics" explainer page for the Space. Builds one static, dependency-free HTML document explaining how a candidate STEP is scored: the validity gate, the three orthogonal axes (shape / topology / interface), and the editing renormalization. It is curated (a Space-tailored summary, deliberately a little duplicated from the canonical ``docs/metrics*`` in the code repo) rather than rendered from those markdown files, because the docs use repo-relative links + local illustration images that don't resolve when hosted. The page links out to the GitHub deep-dives for the full derivations, so the canonical source of truth stays there. The page is served two ways from the same builder (:func:`build_metrics_page`): - as a standalone route ``/metrics`` (so the per-submission report's headline metric pills can deep-link to ``/metrics#``), and - embedded in the "Metrics" Gradio tab via an iframe. Formulas are plain monospace blocks (no MathJax / KaTeX), so the page renders identically online and offline with no network dependency. The anchor ids are a published contract the report links against; see :data:`METRIC_ANCHORS`. """ from __future__ import annotations # Section anchor ids. The per-submission report's headline pills link to # ``/metrics#``; keep these stable (and in sync with the # report's pill links in cadgenbench's single_run.py). METRIC_ANCHORS = { "cad_score": "cad-score", "shape": "shape-similarity", "interface": "interface-match", "topology": "topology-match", "validity": "validity", "editing": "editing", } # Canonical deep-dive docs live in the code repo; linked from each # section so the Space page stays a summary and the full derivations # have one source of truth. _DOCS_BASE = "https://github.com/huggingface/cadgenbench/blob/main/docs" # Bundled illustration served by the Space (see app.py's /metrics-assets # route). Relative so it resolves same-origin whether the page is the # standalone /metrics route or the iframe in the Metrics tab. _MATING_GROUP_IMG = "/metrics-assets/mating_group.webp" _CSS = """\ * { box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; max-width: 960px; margin: 0 auto; padding: 24px 20px 80px; background: #f8f9fa; color: #1f2430; line-height: 1.55; } a { color: #1565c0; } h1 { font-size: 1.7em; margin: 0 0 4px; } .lede { color: #5b6170; margin: 0 0 20px; } .card { background: #fff; border: 1px solid #e3e5ea; border-radius: 12px; padding: 20px 24px; margin: 16px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.05); scroll-margin-top: 16px; } .card h2 { margin: 0 0 10px; font-size: 1.2em; display: flex; align-items: baseline; gap: 10px; } .card h3 { font-size: 0.98em; margin: 16px 0 4px; color: #37474f; } .axis-tag { font-family: monospace; font-size: 0.62em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 3px 8px; border-radius: 6px; } .t-cad { border-left: 5px solid #37474f; } .t-cad .axis-tag { background: #eceff1; color: #37474f; } .t-shape { border-left: 5px solid #1565c0; } .t-shape .axis-tag { background: #e3f2fd; color: #1565c0; } .t-iface { border-left: 5px solid #4527a0; } .t-iface .axis-tag { background: #ede7f6; color: #4527a0; } .t-topo { border-left: 5px solid #006d77; } .t-topo .axis-tag { background: #d8f3f4; color: #006d77; } .t-gate { border-left: 5px solid #c62828; } .t-gate .axis-tag { background: #ffebee; color: #c62828; } .t-edit { border-left: 5px solid #9e7700; } .t-edit .axis-tag { background: #fff9c4; color: #9e7700; } pre.formula { background: #0f1525; color: #e7ecf5; border-radius: 8px; padding: 14px 16px; overflow-x: auto; font-size: 0.86em; line-height: 1.5; margin: 10px 0; } code { background: #eef0f4; padding: 1px 5px; border-radius: 4px; font-size: 0.88em; } table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 0.92em; } th, td { border: 1px solid #e3e5ea; padding: 7px 10px; text-align: left; } th { background: #f5f7fa; } .deep { font-size: 0.9em; color: #5b6170; margin: 20px 0 0; } .endspace { height: 60vh; } .toc { background: #fff; border: 1px solid #e3e5ea; border-radius: 12px; padding: 14px 20px; margin: 16px 0; } .toc ul { margin: 6px 0 0; padding-left: 18px; } .note { color: #5b6170; font-size: 0.92em; } figure.fig { margin: 14px 0; } figure.fig img { display: block; width: 100%; max-width: 520px; height: auto; border: 1px solid #e3e5ea; border-radius: 10px; background: #fff; } figure.fig figcaption { font-size: 0.84em; color: #5b6170; margin-top: 6px; max-width: 560px; } .weight-pill { font-family: monospace; font-size: 0.8em; padding: 1px 7px; border-radius: 6px; background: #eceff1; color: #37474f; } @media (max-width: 760px) { /* Tighten the reading frame for phones. The end-spacer stays (vh of the bounded mobile iframe) so the last "On this page" target can scroll to the top of the box. */ body { padding: 16px 14px 28px; } h1 { font-size: 1.5em; } .card { padding: 16px 16px; } .card h2 { font-size: 1.12em; } pre.formula { font-size: 0.78em; padding: 12px 13px; } table { font-size: 0.86em; } th, td { padding: 6px 8px; } } """ def _section( *, anchor: str, css_class: str, tag: str, title: str, body: str, ) -> str: return ( f'
' f'

{tag}{title}

' f"{body}" "
" ) def build_metrics_page() -> str: """Return the full self-contained Metrics explainer HTML document.""" a = METRIC_ANCHORS overview = _section( anchor=a["cad_score"], css_class="t-cad", tag="CAD Score", title="How one part is scored", body=( "

CADGenBench scores a generated part (a STEP file) against one " "ground-truth STEP. First a hard validity gate; if it " "passes, the CAD Score is a weighted mean of three " "independent metrics, each in [0, 1].

" '
'
            "cad_score = 0                                                if not valid\n"
            "          = 0.4*shape + 0.4*interface + 0.2*topology          otherwise"
            "
" "

(This is the generation composition. " "Editing tasks renormalize the shape axis and reweight; " f'see Editing tasks below.)

' "" "" f'' "" f'' "" f'' "" f'' "" "
ComponentRangeWhat it asks
CAD Validity (gate){0, 1}Is the geometry valid?
Shape Similarity[0, 1]Does the bulk geometry match?
Topology Match[0, 1]Same pieces / holes / voids?
Interface Match[0, 1]Does it bolt up to the same fixture?
" "

Why three axes

" "

They are orthogonal by construction: each catches errors the " "others are blind to:

" "" "

Outputs are rigidly aligned to the ground truth " "(rotation + translation only, never scale) before scoring.

" ), ) validity = _section( anchor=a["validity"], css_class="t-gate", tag="Gate", title="CAD Validity", body=( "

Runs before every other metric on the raw candidate STEP. Any " "failure sets is_valid = False and forces " "cad_score = 0, so an invalid solid never beats a worse " "but valid one. Passing requires all of:

" "
    " "
  1. Well-formed BREP: no per-face / edge / vertex errors " "(self-intersecting wires, edges off their surface, etc.).
  2. " "
  3. Watertight: every shell is closed; no naked or free " "edges.
  4. " "
  5. Meshable as a closed orientable manifold: tessellates " "to a manifold, closed (3F = 2E), orientation-consistent triangle " "mesh.
  6. " "
" ), ) shape = _section( anchor=a["shape"], css_class="t-shape", tag="Shape", title="Shape Similarity", body=( "

Does the bulk geometry match? The mean of two complementary " "sub-metrics, each in [0, 1]:

" '
'
            "shape_similarity = 0.5 * (surface_distance_F1 + volume_IoU)"
            "
" "

Surface Distance F1

" "

Checks the candidate's surface sits where the GT's does and " "faces the same way. Points are sampled across both surfaces with " "their outward normals; a point matches when the closest point on " "the other mesh's surface is within 0.5% of the GT bounding-box " "diagonal and the normals agree to within 20°. Precision and " "recall combine into F1.

" "

Volume IoU

" "

Shared volume of the two solids over their combined volume " "(intersection over union).

" "

Both use a tolerance proportional to part size, so " "small features can move without shifting the score; those are " f'covered by interface match.

' ), ) topology = _section( anchor=a["topology"], css_class="t-topo", tag="Topo", title="Topology Match", body=( "

Does the candidate have the same number of pieces, " "through-holes, and internal voids? It compares the three " "Betti numbers of the solid:

" "" "

Each axis gets a fuzzy log-ratio against GT, sharpened by " "α = 2, and the three are multiplied:

" '
'
            "s_i = ((min(cand,gt) + 1) / (max(cand,gt) + 1)) ^ 2\n"
            "topology_match = s_0 * s_1 * s_2"
            "
" "

The product means one wrong count collapses the " "score: topology is discrete, so two of three right is not a partial " "match. Example: GT (1,2,0) vs candidate (1,4,0) scores " "(3/5)² = 0.36. Blind features (blind pockets, fillets, " "chamfers) are topologically trivial and covered by the other " "axes.

" ), ) interface = _section( anchor=a["interface"], css_class="t-iface", tag="Interface", title="Interface Match", body=( "

Would it bolt up to the same fixture? Each mating feature is a " "region of space the candidate must match in shape, size, and " "position:

" "" "

Mating groups

" "

The features that must seat together against a single fixture " "form one mating group: here, two bolt holes and a slot that " "one jig drops into. A part can have several independent groups (say " "a bolt pattern on one face and a boss on another), and each group " "is scored on its own.

" '
' f'' "
A mating group: a jig with two pins and a slot key " "seats into the part's two bolt holes and slot. The candidate has " "to fit the same fixture.
" "
" "

Scoring

" "

Per group:

" "
    " "
  1. Per-feature fit: volumetric IoU against the region " "(with a thin shell of opposite material, so both oversize and " "undersize lose points).
  2. " "
  3. Bounded pose search: ±1° and ±1% of part " "size per axis, so a feature isn't penalized for the residual of " "whole-part alignment.
  4. " "
  5. Pass/fail ramp: IoU ≥ 0.95 → 1, ≤ 0.80 " "→ 0, linear between; a sloppy fit scores 0.
  6. " "
" "

A group scores as its worst feature (the minimum); the " "fixture scores as the mean over its groups, so nailing one " "interface and missing another still earns partial credit.

" ), ) editing = _section( anchor=a["editing"], css_class="t-edit", tag="Editing", title="Editing tasks: no-op renormalization", body=( "

Editing fixtures ship an input.step plus an edit " "request; the GT is a small change to that input. Since all three " "axes measure global similarity, submitting the input unchanged " "(the no-op) already scores high, so the raw composition " "would reward doing nothing.

" "

The fix renormalizes the shape axis against the no-op " "baseline b = shape_similarity(input, GT):

" '
'
            "s_renorm  = max(0, (shape_similarity - b) / (1 - b))\n"
            "cad_score = 0.6*s_renorm + 0.3*interface + 0.1*topology   (0 if not valid)"
            "
" "

This maps the no-op to 0 and a perfect candidate to 1. Topology " "and interface stay raw (most edits leave them unchanged). A no-op " "therefore caps at 0.3 + 0.1 = 0.4, and any real shape improvement " "clears it.

" ), ) toc = ( '" ) footer = ( '

For the full definitions and derivations, see the ' f'metrics reference in the code: ' f'' "docs/metrics.md.

" # Trailing space so the last section can scroll to the top of the # viewport when reached via an in-page anchor (e.g. the "Editing # tasks" link); without it a near-bottom target lands mid-screen. '' ) return ( "" "" "" "CADGenBench Metrics" f"" "" "

Metrics

" "

How CADGenBench scores one generated CAD part against " "the ground truth. These metrics are new, so this page explains each " "one.

" f"{toc}{overview}{validity}{shape}{topology}{interface}{editing}" f"{footer}" f"" "" ) # Mobile sizing for the embedding iframe. # # We deliberately give the phone iframe its OWN bounded height (an internal # scroll) rather than auto-sizing it to the full content. Reason: when embedded # on huggingface.co the Space sits in a cross-origin outer frame that owns the # real scroll, so a content-sized iframe can't be scrolled from inside -- which # silently breaks the "On this page" anchor links (scrollIntoView has nothing it # is allowed to move). A bounded iframe scrolls its own document, so the anchors # work again. Desktop keeps its fixed CSS height (reads well there). # # Anchor clicks use scrollIntoView (smooth, scrolls whichever ancestor actually # scrolls -- the bounded iframe when embedded, the window on the standalone # /metrics route). All wrapped in try/catch: frameElement access is fine for # this same-origin iframe but must never throw if that ever changes. _JS = """ (function () { var narrow = (window.innerWidth || 1000) < 760; function fit() { if (!narrow) return; try { var fe = window.frameElement; if (!fe) return; // standalone /metrics: window scrolls var avail = (window.screen && window.screen.availHeight) || 740; var h = Math.max(340, Math.min(640, Math.round(avail - 360))); fe.style.height = h + 'px'; // bounded -> the iframe scrolls itself } catch (e) { /* keep the CSS fallback height */ } } document.addEventListener('click', function (e) { var a = e.target && e.target.closest ? e.target.closest('a[href^="#"]') : null; if (!a) return; var el = document.getElementById(a.getAttribute('href').slice(1)); if (el) { e.preventDefault(); el.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); window.addEventListener('load', fit); window.addEventListener('resize', function () { narrow = (window.innerWidth || 1000) < 760; fit(); }); fit(); })(); """