LINT(nged)

Detect and report problems with subsets or all of a .g database.

πŸ“… published 7.44.0, from nixpkgs unstable

πŸ’… LINT (nged) β€” the vibe check for your .g database

okay bestie, so this one is literally the lint command from the nged BRL-CAD user commands β€” that's the manual name, don't @ me β€” and it's about to become your new favorite thing. It detects and reports problems in a .g database, and honestly? iconic behavior.


✨ SYNOPSIS ✨

lint [-h] [-C] [-M] [-I] [-F "filters"] [ObjectName]
lint --raytrace [--perturb] [--raytrace-tol <frac>] [-j <jsonfile>] [ObjectName ...]

🌸 DESCRIPTION 🌸

So like. lint looks at whichever geometry database you currently have open (or just a subset of it, if you pass object names as arguments β€” yes you can do that, we love a targeted investigation πŸ’…) and it goes hunting for a whole buffet of problems.

By default? It searches for all the defect types. Every single one. But the second you hand it one or more of the options that turn on a search for a specific type of problem β€” that's it, only those searches run. It's giving "you asked for this, so this is what you get" energy.

Also important, no cap: the --raytrace mode is a special, mutually exclusive operating mode. It disables all the other checks and instead runs a multi-level raytrace consistency validation against every reachable object. Scroll down to the RAYTRACE MODE section for the whole story 🫢

πŸŽ€ OPTIONS πŸŽ€

  • -h, --help β€” prints the help message. that's it. that's the whole gig ✨
  • -v, --verbose β€” more reporting verbosity, more diagnostics, more detail. and the best part? You can repeat the flag to keep increasing the amount of diagnostic detail. stack it, queen πŸ‘‘
  • -C, --cyclic β€” checks for full paths that are cyclic. Translation: a high level object has a reference to itself somewhere up in its definition tree. Those definitions are always considered invalid, and sometimes they make tools that work with geometry hierarchy act real weird about it. πŸ’€
  • -M, --missing β€” checks for references to objects or files that are missing β€” like, not in the database, not on the file system, simply not there. Mainly it's a check for comb objects pointing at missing objects, but it'll also catch and report invalid object and file references inside extrude and dsp primitives. thorough queen 🌸
  • -I, --invalid-shape [check [check ...]] β€” hunts for shapes in the database that don't satisfy one or more of the properties required for them to be valid definitions β€” specifically the kind where fixing it means changing the object's definition. (As of this writing, the only implemented check is for BoT object solidity, but lots more are possible and planned πŸ‘€.) And hey, quick clarification: objects that are invalid because something's missing might be perfectly valid without touching their own definition β€” e.g. the missing object/file could just be added back in. That category lives over in the -M check.
  • -F, --filter β€” applies search command style filterz to the set of objects being tested. So like, you can test only brep objects and not BoT objects, or only objects matching a particular name pattern. Super handy. But β€” and this is the important part β€” it does not affect the checks for missing or cyclic geometry. The underlying search mechanism assumes the inputs don't have those particular categories of problem, so it just can't be reliably applied in those cases. periodt.
  • -j, --json-file fname β€” writes the full lint report data out to a JSON file. for all your programmatic little needs 🧚
  • -V, --visualize β€” when a detected problem has a visual representation, this draws that representation in the current display. see it to believe it πŸ¦‹
  • -t, --tol # β€” overrides the numerical tolerance used by the checks that depend on tolerances. Defaults to VUNITIZE_TOL when unspecified. write that down.
  • -g, --group name β€” creates or updates a combination named name containing the shape objects that reported issues. Heads up though: cyclic paths and missing references do not get added to this grouping object.
  • --min-tri-area # β€” units are mm^2. If you specify it, lint will not report sampling problems whose seed triangle is smaller than the area threshold you supplied. tiny triangle, no drama πŸ”
  • --raytrace β€” turns on raytrace validation mode. All standard lint checks get disabled; instead the command runs the raytrace consistency pipeline described in the RAYTRACE MODE section. ⚠️ Cannot be combined with -C, -M, or -I. it's giving mutually exclusive, pick a lane πŸ’…
  • --perturb β€” modifier for --raytrace mode. When it's present, combs get facetized using the facetize default perturbation pass (the same perturbation that facetize normally applies to dodge coplanar-face boolean artifacts) rather than the --no-perturb mode that is the default for raytrace validation.

So why do two modes even exist? Glad you asked, bestie:

Without --perturb, the facetized BoT reference is geometrically consistent with the Crofton CSG measurement β€” both of them are seeing the exact original geometry. Which makes any mismatch super easy to spot, but it can also report coplanar-face artifacts as failures even though facetize handles them correctly in practice. 😭

With --perturb, the reference BoT is the actual output that facetize would produce for a model. Mismatches that clear up under --perturb are coplanar-face artifacts that the perturbation pass resolves (the default facetize behavior is correct for these objects). Mismatches that persist under --perturb are genuine modeling topology issues that perturbation cannot resolve β€” those ones require actual geometry correction. See the COPLANAR INTERFACE PATTERNS section below for the details πŸ‘‘

  • --raytrace-tol frac β€” the fractional tolerance for raytrace validation comparisons (default: 0.10 = 10%). A value of 0.05 requires the two independent estimates to agree within 5%. Lower values are stricter. Only meaningful when --raytrace is also specified. no --raytrace? it's just sitting there vibing, doing nothing.

πŸ”₯ RAYTRACE MODE πŸ”₯

Raytrace validation mode (--raytrace) cross-checks surface area and volume estimates from two independent methods against each other, so it can detect problems in raytracing or boolean evaluation. It is significantly more expensive than ordinary lint checks β€” like, highkey expensive β€” so the move is to use it on targeted subsets of a database rather than entire large assemblies. targeted, not greedy πŸ’…

🧚 Algorithm

  1. Universe collection. Starting from the explicitly named objects (or all top-level objects if none are named), every reachable object in the hierarchy is collected into the candidate set.
  2. Samplability classification. Any object whose subtree contains a halfspace (or other unbounded primitive) is marked as non-samplable and excluded from metric evaluation. Why? Because excluding those objects prevents the Cauchy-Crofton estimator from operating on infinite solids where it would diverge. we do not let the math run off to infinity, thank you 🌈
  3. Leaf-to-root processing order. Objects are processed in dependency order so that leaf primitives get validated before the combs that contain them. This ensures any failure is always attributed to the lowest failing node in the tree rather than getting masked by higher-level aggregate results. main character = the actual problem node πŸŽ€
  4. Failure propagation. If any child of a comb fails, or is skipped in a way that makes its result untrustworthy, the parent comb is automatically skipped with the reason child_validation_failed. This stops a single bad leaf from producing misleading cascade results upward. one bad apple does not get to ruin the whole tree, periodt.
  • Crofton SA/volume β€” Cauchy-Crofton estimate obtained by firing random chord rays against the CSG raytracer.
  • Analytic SA/volume β€” closed-form result from the primitive's function table (ft_surf_area / ft_volume), when available. If analytic values aren't available (e.g. BoT, BREP), the Crofton result is recorded informatively without a pass/fail verdict. no verdict, just vibes πŸ“
  • CSG Crofton SA/volume β€” ray-sampled against the original CSG boolean tree.
  • BoT SA/volume β€” the comb is facetized to a solid triangle mesh using the facetize command, and the mesh SA/volume is measured exactly via the divergence theorem.

Without --perturb, facetize is called with --no-perturb so the BoT reference reflects the exact original geometry. With --perturb, the default perturbation pass is used, producing the same BoT that would result from a normal facetize run.

A significant discrepancy (beyond --raytrace-tol) indicates either a problem with boolean evaluation or an anomalous raytrace result for that combination of geometry. 😬

πŸ’– Output

Results get reported in three categories:

  • Passed – both estimates agree within --raytrace-tol. slay ✨
  • Failed (raytrace_mismatch) – the two estimates disagree beyond --raytrace-tol. The output includes SA and volume error percentages and the reason for failure.
  • Skipped – the object could not be validated. Reasons include:
    • contains_unbounded_primitive – a halfspace is present in the subtree.
    • child_validation_failed – a child object failed or was skipped.
    • crofton_prep_failed – the raytrace instance could not be prepared.
    • raytrace_facetize_failed – the facetize step failed for a comb.

With -v (verbose), skipped objects and passing objects are also listed in full detail. With -j, all results get written to a JSON file for programmatic analysis. data queen πŸ“

⭐ Performance Considerations

Raytrace validation uses 2 000 Crofton rays per object by default. For a database with many objects this can be slow β€” so the recommendation is to name specific assembly trees as arguments rather than relying on the tops-object default scan. Facetizing combs is inherently more expensive than sampling primitives. plan accordingly, bestie 🫢

πŸ‘‘ Interpretation of Results

A raytrace_mismatch on a leaf primitive suggests a bug in the raytrace shot routine for that primitive type (the Crofton ray count is sufficient for basic primitives at 10% tolerance).

A raytrace_mismatch on a comb when all children passed suggests either:

  • a coplanar-face artifact (see COPLANAR INTERFACE PATTERNS below),
  • a problem with boolean evaluation producing incorrect geometry,
  • or a raytrace behavior anomaly specific to that combination of primitives.

🩺 Using --perturb to triage comb mismatches

okay bestie, here's the whole vibe: run the same check twice β€” once without --perturb (that's the default, no thoughts needed) and once with --perturb. Then read your two results like you're decoding a group chat:

  • pass without --perturb, pass with --perturb β€” Clean object. that's it, that's the girl πŸ’…
  • fail without --perturb, pass with --perturb β€” Coplanar-face artifact that the perturbation pass resolves. The default facetize output is correct; no modeling change is needed. Common patterns: adjacent sub end-caps sharing a plane (Example 1 below); sub with multiple faces flush with the base (Example 2 below).
  • fail without --perturb, fail with --perturb β€” Persistent mismatch β€” one of three known patterns:
  • pass without --perturb, fail with --perturb β€” Unexpected: perturbation introduced a problem. Likely a very marginal tolerance case; try re-running with a looser --raytrace-tol value, or examine the mesh with nirt for thin-volume artifacts near the tolerance boundary.

Because the check propagates failure upward, the lowest failing node reported is always the most actionable finding. lowkey the most useful sentence on this whole page, ngl 🫢


πŸ¦‹ Coplanar Interface Patterns

When lint --raytrace is run without --perturb, a raytrace_mismatch on a comb is often caused by coplanar faces between adjacent primitives that Manifold cannot resolve cleanly. Running lint --raytrace --perturb then distinguishes two situations:

  • Mismatch clears with --perturb: The coplanar-face artifact is eliminated by expanding each primitive slightly. The underlying model is correct; no geometry change is needed. πŸŽ€
  • Mismatch persists with --perturb: The geometry contains a structural problem that perturbation cannot fix. This requires correcting the source model. 😭

The four examples below use real regions from the BRL-CAD test model m35.g and show step-by-step how each type of mismatch is introduced and how it behaves. Examples 1 and 2 are resolved by --perturb; Examples 3 and 4 persist regardless.

πŸ’… Example 1: Sub-Sub Coplanar End-Cap (region r321, TGC cylinders)

This region is a hollow annular cylinder built from three TGC (truncated general cone) primitives: a solid outer cylinder (s321) with two coaxial cylinders subtracted (s320 and s323). The key defect is that s320's top cap and s323's bottom cap sit at the exact same plane with the same radius. like, exactly the same. suspicious behavior fr fr πŸ‘€

πŸ“ Step 1 β€” Source geometry

Shown as a radial cross-section (Y = position along the shared axis, r = radial distance from the axis):

             r
             |
           8.4 +===========================+   s321 outer wall
               |        s321 (BASE)        |   Y = 26.1 .. 27.9 in, radius 8.4
           7.4 +─────────┬─────────────────+─────────────────────────────+
               | s320    |    s323 (SUB)                                  |
               | (SUB)   |    Y = 26.85 .. 33.2 in, radius 7.4           |
           7.4 +─────────┴─────────────────+─────────────────────────────+
           8.4 +===========================+
               |         |
             Y=26.1    Y=26.85                          Y=27.9       Y=33.2
                         ^
               s320 top = s323 bottom
               both at Y=26.85, both radius 7.4 (exact coincidence)

✨ Step 2 β€” Correct boolean result (r321 = s321 - s320 - s323)

Because s320 spans Y=26.1..26.85 and s323 spans Y=26.85..27.9 (within s321), together they fill the full inner cylinder r=0..7.4 across the entire height of s321. The correct result is a simple annular shell (r=7.4..8.4):

             r
             |
           8.4 +===========================+
               | hollow shell (r=7.4..8.4) |
           7.4 +===========================+
               (void -- both subs span the full height of s321)
               Y=26.1                     Y=27.9

           CSG Crofton SA = 177,481 mmΒ²  (two annular caps + outer + inner wall)
           No disc face exists at Y=26.85: s320 and s323 meet seamlessly there.

πŸ’€ Step 3 β€” Without --perturb: how the extra faces appear

facetize tessellates each primitive independently. After computing (s321 - s320), Manifold places a closing disc D1 (radius 7.4) at Y=26.85 β€” the open top of the s320 cavity. When it then subtracts s323 (whose own bottom cap is also at Y=26.85, radius 7.4), those two discs are geometrically identical and exactly coplanar. Manifold cannot determine whether D1 is interior (should be removed) or exterior (should be kept), so D1 survives as a spurious face in the output mesh. The same problem occurs at Y=26.1, where s320's bottom cap is coplanar with s321's bottom cap.

             After (s321 - s320), mesh contains disc D1 at Y=26.85, area = pi*7.4^2 = 172 in^2

             When subtracting s323 (bottom cap also at Y=26.85, same radius):
               D1 and s323 boundary are at the SAME PLANE  -->  D1 survives
               Similar spurious disc D0 forms at Y=26.1 from sub-base coincidence

             Two spurious discs * 172 in^2 * 645 mm^2/in^2 = ~222,000 mm^2 extra SA

             Measured:  BoT SA = 397,840 mm^2
             Correct:   CSG SA = 177,481 mm^2
             Ratio:     2.24x inflation  -->  FAIL

🧚 Step 4 β€” With --perturb: how perturbation fixes it

Each sub is expanded outward by a deterministic amount chosen from the primitive name and bounding-box scale. s320 and s323 receive different expansion factors (0.065 mm and 0.070 mm respectively), so their cap planes no longer coincide:

             s320' (factor 0.065 mm):
               bottom cap moves to Y = 26.1 - 0.065 = 26.035  (below s321 bottom)
               top    cap moves to Y = 26.85 + 0.065 = 26.915

             s323' (factor 0.070 mm):
               bottom cap moves to Y = 26.85 - 0.070 = 26.780
               top    cap moves to Y = 33.2  + 0.070 = 33.270

             After (s321 - s320'): closing disc D1 is now at Y=26.915
             s323' spans Y=26.780..33.270, so Y=26.915 is inside s323' -->
               Manifold correctly removes D1 when subtracting s323'.

             Measured:  BoT SA = 178,262 mm^2
             Correct:   CSG SA = 177,481 mm^2
             Error:     0.4%  -->  PASS

Remedy for sub-sub coplanar: This is a diagnostic artifact β€” no geometry change is needed. Running lint --raytrace --perturb confirms the model is clean. If the region must facetize correctly without --perturb (e.g. for legacy pipelines), merge the adjacent sub-sections into a single primitive or introduce at least 0.01 mm clearance between end caps. 🫢

🌸 Example 2: Sub-Base Flush Faces (region r794, ARB8 box-in-box)

This region is an ARB8 box (s794) with a slightly smaller ARB8 subtracted (s795). Three of s795's faces are exactly flush with three faces of s794. not near flush. flush flush. πŸ™ˆ

πŸ“ Step 1 β€” Source geometry

Shown in cross-section in the XZ plane (looking along the Y axis):

             Z
             ^
           20.21 +============================+  <-- flush: s795 top = s794 top (Z=20.21)
                 |        s794 (BASE)         |
                 |  +────────────────────+   |  <- s795 X walls INSIDE s794
                 |  |   s795 (SUB)       |   |
           18.96 |  +────────────────────+   |  <- s794 bottom
           18.835   +────────────────────+      <- s795 bottom (BELOW s794!)
                 |                        |
             X=79.66                  X=83.66   (s794 X range)
                X=79.785            X=83.535    (s795 X range -- narrower)

             Also flush along the full Y extent:
               Y = -41.44  (both s794 and s795 front face)
               Y = -17.125 (both s794 and s795 back face)

             Three flush face planes total: Z=20.21, Y=-41.44, Y=-17.125

✨ Step 2 β€” Correct boolean result (r794 = s794 - s795)

The result is a thin frame: the base material on the left/right X sides and top/bottom Z strips that s795 does not reach. The correct shape has 24 triangular faces.

Thin frame cross-section at any Y slice:

+--+────────────────────────+--+

void (s795 removed)
thin X strips (0.125 in each)
+────────────────────────+
Z=18.96 bottomthin Z strip at bottom

+──────────────────────────────+

CSG Crofton SA = 86,684 mmΒ² (24 triangles, 16 vertices)


### Step 3 β€” without `--perturb`: bestie, the extra triangles 😭

At `Z=20.21`: s794's top face and s795's top face are *literally the same plane*. Manifold cannot resolve the coplanar pair β†’ so it just... inserts **4 extra triangles** at the three flush face planes.

Mesh: 28 triangles (correct: 24) --> extra SA = 117,651 mm^2

Measured: BoT SA = 204,335 mm^2 Correct: CSG SA = 86,684 mm^2 Ratio: 2.36x inflation --> FAIL


Yeah. 2.36x inflated. Not the vibe. πŸ’€

### Step 4 β€” with `--perturb`: flush faces get pushed outside the base and it slays ✨

s795' (factor `0.060 mm`):
- Z top moves to `Z = 20.21 + 0.060 = 20.270` (past s794 top at `Z=20.21`)
- Y front moves to `Y = -41.44 - 0.060 = -41.500` (past s794 front)
- Y back moves to `Y = -17.125 + 0.060 = -17.065` (past s794 back)
- Z bottom moves to `Z = 18.835 - 0.060 = 18.775` (still below s794, irrelevant)
- X walls move to `X = 79.725` and `83.595` (still inside s794 X range)

No coplanar face planes remain. Periodt.

Mesh: 24 triangles (correct) Measured: BoT SA = 86,531 mm^2 Correct: CSG SA = 86,684 mm^2 Error: 0.2% --> PASS


<pink>PASS, bestie!! 0.2%!! we love to see it!!</pink> πŸŽ€

> **Remedy for multi-face flush:** this is a diagnostic artifact β€” the model is correct and `--perturb` confirms it cleanly. If flush-face artifacts are undesirable without `--perturb`, offset the flush faces of the sub at least `0.01 mm` inward.

---

### Example 3: Sub Engulfs Base (region r604, ARB8 β€” PERSISTENT with `--perturb`)

Okay so this region has a **very thin** ARB8 slab (`s604`, only `0.312 in` thick in Y), and a much larger ARB8 (`s178`) gets subtracted from it. `s178` *nearly completely* covers `s604`; the correct result is a tiny exposed wedge only `~3 mm` wide where `s604` protrudes slightly past the edge of `s178`. πŸ¦‹

#### Step 1 β€” source geometry

Shown as a cross-section in the XZ plane at `Y=17` (looking along the Y axis toward `Y=17.312`):

Z ^ 31.125 +─────────────────────────────────────────────+

s178 (SUB) cross-section
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
s604 (BASE) cross-section
(almost entirely inside s178)

26.125 | β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ <-- s604 bottom

↑ thin exposed wedge here
(at Zβ‰ˆ26.125, s604 X=Β±6.64"
but s178 only reaches X=Β±6.52")

23.184 +─────────────────────────────────────────────+

X=-12.08 X=+12.08 (s178) X=-12 X=+12 (s604 top) X=-6.52 X=-6.64 X=+6.64 X=+6.52 (near Z=26.125) ↑ exposed wedge ↑

At Y=17.312 (flush face plane): s178 face (X=Β±12.08, Z=23.184..31.125) completely CONTAINS s604 face (X=Β±12, Z=26.125..31.125)


#### Step 2 β€” correct boolean result (`r604 = s604 - s178`)

The correct result is only the thin wedge at the bottom corners of `s604` that `s178` does not reach: approximately `3 mm` wide, running the full `0.312 in` depth of `s604` in Y.

CSG Crofton SA = 3,991 mm^2 CSG Crofton vol = 1,821 mm^3 (a small but real sliver -- the tiny exposed corner of s604)


It's giving tiny but mighty. πŸ’…

#### Step 3 β€” without `--perturb`: massive spurious inflation πŸ™ˆ

`s178`'s `Y=17.312` face completely covers `s604`'s `Y=17.312` face. Manifold cannot correctly subtract a sub whose face engulfs the base face at that plane β†’ so it inserts **28 spurious triangles** covering most of `s604`'s area.

Measured: BoT SA = 123,798 mm^2 Correct: CSG SA = 3,991 mm^2 Ratio: 31x inflation --> FAIL


31x. THIRTY-ONE TIMES. not me crying over a mesh 😭

#### Step 4 β€” with `--perturb`: improves but still fails (volume error)

`s178'` (factor `0.080 mm`) expands outward in all directions. The `Y=17.312` face moves to `Y=17.392` β†’ coplanar degeneracy resolved. SA is now nearly correct (`5%` off β€” passes).

**BUT** (and it's a big but): `s178'` also expands in XZ, growing by `~0.08 mm` at `Z=26.125`. The exposed wedge was only `~3 mm` wide; after perturbation `s178'` covers an extra `~0.16 mm` (both sides), leaving `~2.7 mm`. The result volume is `14%` too small:

Measured: BoT SA = 3,790 mm^2 (5% off -- passes) Measured: BoT vol = 1,564 mm^3 (14% off -- FAILS tolerance) Correct: CSG vol = 1,821 mm^3


Result: **FAIL even with `--perturb`.** `lint` reports `"persistent SA/volume mismatch under perturbation"`. 🫠

> **Remedy for sub-engulfs-base:** this does require geometry correction, fr fr. The sub (`s178`) is larger than the base (`s604`) in nearly every direction. If the intent is a thin exposed sliver, the model is correct as designed but `facetize` will always produce a volume error proportional to the perturbation magnitude relative to the sliver thickness. Options:

- Redesign so the sub leaves a clearly larger exposed region (at least `1 mm` clearance on all sides), making the result insensitive to the perturbation amount.
- If the sub is truly meant to completely remove the base, verify the CSG tree: if the region should be empty, restructure the boolean so that result is modeled directly.

---

### Example 4: Fully-Engulfed Base (regions r937, r522, r1114 β€” PERSISTENT with `--perturb`)

In this pattern the subtractive primitives *collectively* remove all of the base solid. The correct CSG boolean result is **empty**. Crofton ray tracing correctly measures `SA=0` and `volume=0` for these regions. However, `facetize` produces a nonzero BoT mesh with spurious faces in both the default and `--perturb` modes. πŸ‘‘

#### Step 1 β€” source geometry (r937 example)

`r937` uses a small TGC connector (`s937`) as the base, subtracted by a larger TGC tube (`s939`) that passes through it, a very large TGC structural member (`s528`), and a small fillet TGC (`s989`):

Longitudinal cross-section (simplified):

s939 (SUB) ─────────────────────────────> β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ s528 (SUB, very large) β”‚ β”‚ +──────────+ β”‚ β”‚ | s937 BASE| (small TGC connector) β”‚ β”‚ +──────────+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

s939: SA = 74,026 mmΒ² (long tube, completely spans s937) s528: SA = 293,293 mmΒ² (large member, also spans s937) s937: SA = 5,057 mmΒ² (small base fully inside both subs)

At every point on s937's surface, either s939 or s528 occupies that space --> the CSG intersection of s937 with NOT(s939 βˆͺ s528) is the empty set.


#### Step 2 β€” correct boolean result (`r937 = s937 - s939 - s528 - s989`)

CSG Crofton SA = 0 mmΒ² (region is empty) CSG Crofton vol = 0 mmΒ³ (region is empty)


This is correct: `s937` is a small fitting completely inside two larger structural members that are subtracted from it. The region exists in the database to assign material/LOS attributes to that volume; the geometry itself is properly void. ✨

#### Step 3 β€” without `--perturb`: spurious BoT faces

`facetize` tessellates `s937` first. When subtracting `s939` and `s528`, some faces of `s937`'s mesh are exactly coplanar with interface planes of the larger subs. Manifold cannot resolve these zero-width intersections and leaves residual closing triangles on the output.

Measured: BoT SA = 1,946 mmΒ² (spurious -- correct is 0) Measured: BoT vol = 3,366 mmΒ³ (spurious -- correct is 0) Correct: CSG SA = 0 mmΒ² Ratio: infinity --> FAIL


Ratio: infinity. That's not even a number bestie, that's a *lifestyle*. πŸ’€

#### Step 4 β€” with `--perturb`: still fails (subs still engulf base)

Expanding `s939'` and `s528'` by their perturbation factors does not help: both subs were already *much* larger than `s937`. The perturbed subs still completely engulf the perturbed `s937`. Manifold still encounters coplanar closing faces and the BoT remains nonzero:

Measured: BoT SA = 1,937 mmΒ² (still spurious) Measured: BoT vol = 3,340 mmΒ³ (still spurious) Correct: CSG SA = 0 mmΒ² Ratio: infinity --> FAIL (identical pattern to without --perturb)


#### Interpreting the zero-crofton signal

A `crofton_sa=0` result is **not** an error in the raytrace β€” it is the correct measurement for an empty CSG region. The mismatch is that the facetized BoT is wrong (nonzero). The region is effectively empty and the BoT mesh has no geometric meaning.

> **Remedy for fully-engulfed base:**

- If the region is intentionally empty (e.g. used only for attribute assignment): the geometry is correct. The `lint --raytrace` mismatch is a known limitation of the `facetize` path. No model change is required. Consider suppressing raytrace lint for this region using `-F` filters if the report is noisy.
- If the region is accidentally empty (modeling error): inspect the CSG tree with `mged l` and adjust the sub extents so that at least a small portion of the base is exposed.

---

# 🌈 EXAMPLES 🌈

### Example 1. Examine entire database for all defect categories (using `lint_tests.g` from BRL-CAD repository)

mged>lint

Checking for cyclic paths... Checking for references to non-extant objects... Checking for invalid objects... Found cyclic paths: /c1/c2/c1 /c2/c1/c2 Found references to missing objects or files: invalid_dsp_missing_binary_obj.s/missing_binary_object.s invalid_dsp_missing_file.s/missing_file.data invalid_extrude_missing_sketch.s/missing_sketch.s missingentry.c/missing.s Found invalid objects: nonsolid.bot [bot:not_solid]


### Example 2. vibe-check one single geometry object (this one's from `lint_tests.g` in the BRL-CAD repository) πŸŽ€

mged>lint -I nonsolid.bot

Checking for invalid objects... Found invalid objects: nonsolid.bot [bot:not_solid]


### Example 3. snoop every non-BoT object matching the name pattern `_1.` for invalidity (this one's the VIA OpenBook part d STEP conversion) πŸ”

mged>lint -F "! -type bot -name _1."

Checking for cyclic paths... Checking for references to non-extant objects... Checking for invalid objects... brep.m_L[1332] loop is not valid. end of brep.m_T[loop.m_ti[0]=6991]=(8.79673,0.628202) and start of brep.m_T[loop.m_ti[1]=6990]=(8.79673,0.630829) do not match. brep.m_F[1180] face is not valid. brep.m_L[face.m_li[0]=1332] is not valid. ON_Brep.m_F[1180] is invalid. Found invalid objects: D.s [brep:opennurbs_invalid]


### Example 4. hunting for empty bots (havoc model, facetized with the `-r` option β€” bestie, remember that detail) πŸ’€

mged>lint -I bot:empty havoc.bot

Checking for invalid objects... Found invalid objects: r.rot137.bot [bot:empty] r.rot237.bot [bot:empty] r.rot337.bot [bot:empty] r.rot37.bot [bot:empty] r.rot437.bot [bot:empty]


### Example 5. run raytrace validation on one specific assembly ✨

mged>lint --raytrace wing_assembly

Running raytrace validation checks... Raytrace validation: 12 passed, 1 failed, 3 skipped Raytrace mismatches (CSG raytracer disagrees with reference): r.spar.c [comb] SA_err=38.2% VOL_err=0.4% (CSG Crofton SA disagrees with BoT mesh; coplanar-face artifact possible - re-run with --perturb to distinguish artifact from modeling issue)


### Example 6. triage a mismatch with `--perturb` to figure out if it's a facetize artifact or an actual modeling issue πŸ•΅οΈβ€β™€οΈ

First pass, no `--perturb` (that's the default):

mged>lint --raytrace r.pocket.c

Raytrace validation: 0 passed, 1 failed, 0 skipped Raytrace mismatches (CSG raytracer disagrees with reference): r.pocket.c [comb] SA_err=130.2% VOL_err=0.3% (CSG Crofton SA disagrees with BoT mesh; coplanar-face artifact possible - re-run with --perturb to distinguish artifact from modeling issue)


Now re-run with `--perturb`:

mged>lint --raytrace --perturb r.pocket.c

Raytrace validation: 0 passed, 1 failed, 0 skipped Raytrace mismatches (CSG raytracer disagrees with reference): r.pocket.c [comb] SA_err=130.5% VOL_err=0.3% (persistent SA mismatch under perturbation; probable coplanar sub-sub or sub-base interface - geometry correction required (see lint man page))


The mismatch *persists* under `--perturb`, which means it's a coplanar sub-base interface (that's Pattern 2 above, iykyk). The remedy: offset the flush faces of the subtractive primitive inward by **at least `0.001` mm**. Periodt.

### Example 7. raytrace validation with a strict 5% tolerance plus JSON output 🌈

mged>lint --raytrace --raytrace-tol 0.05 -j results.json entire_model

Running raytrace validation checks... Raytrace validation: 47 passed, 3 failed, 8 skipped Raytrace mismatches (CSG raytracer disagrees with reference): r.frame.c [comb] SA_err=74.5% VOL_err=0.0% (CSG Crofton SA disagrees with BoT mesh; coplanar-face artifact possible - re-run with --perturb to distinguish artifact from modeling issue) sub_box.s [primitive] SA_err=6.1% VOL_err=0.8% (Crofton SA disagrees with analytic formula) r.window.c [comb] SA_err=5.5% VOL_err=0.1% (CSG Crofton SA disagrees with BoT mesh; coplanar-face artifact possible - re-run with --perturb to distinguish artifact from modeling issue) Raytrace checks skipped (facetize failed): r.complex.c [comb]


---

# πŸ› οΈ REPAIRING INVALID GEOMETRY πŸ› οΈ

Okay so like β€” how do you actually *fix* this stuff? The procedures for repairing invalid geometry are specific to the problem and the object, sorry bestie, there's no one-size-fits-all glow-up here. For cyclic paths and invalid references the answer is generally straightforward. For cyclic paths, the cycle must be broken by editing comb definitions so the hierarchy is not self-referencial. For invalid references, either the reference is edited or the missing object is added. When it comes to invalid objects, however, matters become more challenging. 😭

Generally speaking, BRL-CAD (as of 2024) does not have a lot of capability when it comes to repairing NURBS B-Rep objects β€” the best option there is going to be to work with the object in an external editor (individual brep objects can be written out to 3dm files with the `brep dump` subcommand.) πŸŽ€

For empty BoTs, there are two possibilities. One is the original geometric definition has an error causing it to be interpreted as empty, and the other is that the modeling intent was indeed to remove a region and the definitions just weren't adjusted accordingly. If the "empty" object really shouldn't be there, the best course is to remove the original definitions and delete the empty BoT. If the BoT's emptiness is a result of geometric error, the original CSG definition needs to be fixed.

Non-solid BoTs are a consequence of topological errors. Sometimes `bot repair` can perform hole filling to close such meshes, but there are more subtle categories of failures such as t-junctions that need to be fixed in mesh editing tools (rebuilding the object may also be easier than repairing a badly mangled mesh, in severe cases.) πŸ’…

Unexpected misses and hits in BoTs can be caused by a number of issues, but one frequent encountered problem is a mesh with triangles too small for BRL-CAD's intersection solver to resolve. Generally such triangles are artifacts rather than intentional features, and can frequently be removed by decimation:

bot decimate -f 0.005 in.bot out.bot


If decimation does clear the error, the user must manually replace the original BoT with the new one. No auto-swap magic, ngl.

Thin volumes and close faces are a frequent consequence of coplanar faces being present in the original CSG inputs β€” the inherent limits of floating point math cause difficulty for coplanar boolean evaluation. The Manifold library is able to successfully produce meshes with them, but it often does so by introducing near-degenerate thin triangle pairings along formerly coplanar faces. As a rule these boolean artifacts are undesirable for BRL-CAD purposes β€” the former introduce visual artifacts, particularly in shaded viewing. For NIRT and other solid shotlining tools, close faces can "break up" what should be a single solid segment into multiple separate segments with a near-zero length "gap" between them. Not the vibe. πŸ™ˆ

For BoTs where BRL-CAD's internal tools are inadequate and it is worthwhile to attempt mesh repairs, the `bot_dump` command can write out obj or stl files. After editing, importers for obj, stl and other mesh formats can import a repaired mesh back into BRL-CAD. Generally speaking the imported mesh names will have to be adjusted and the original invalid objects removed manually. (Converter-generated names will usually be different.) πŸ¦‹

Facetize (when run with the `-r` option) already tries to clear thin volumes automatically β€” occasionally it is possible, but in many cases the mesh cannot be automatically repaired without manual intervention in a mesh editing tool. In situtations where coplanar faces are the cause of thin volumes or close faces, the recommended solution is to adjust the original source geometry to avoid coplanarity β€” slightly "bumping" faces in or out tiny amounts to avoid the mathematically problematic alignments. If a mesh does need to be exported, the `bot strip` command can remove some of the problematic faces as a preliminary step. (Note that there may still be problem triangles present β€” lint's current BoT testing samples only the center of each triangle, so triangles that are coplanar but not overlapping in the center will not be detected.) πŸ‘‘

When `--raytrace` finds mismatches, the first step is to determine whether the source is a geometric model issue or a genuine raytrace bug:

- <pink>**Large SA error with small volume error**</pink> is the classic signature of a sub-tolerance sliver (a face pair separated by less than `BN_TOL_DIST`). The CSG boolweave discards the sliver for near-perpendicular rays but admits it for oblique angles, inflating the Crofton SA estimate while leaving volume nearly unchanged. The fix is to adjust the source geometry so that coplanar or near-coplanar faces are clearly separated or clearly coincident. Running facetize with its perturb pass (the default) will also eliminate the sliver artifact in the tessellated representation, unless the coplanar interfaces match one of the persistent patterns described in <purple>**COPLANAR INTERFACE PATTERNS**</purple> above.
- <hotpink>**Large volume error**</hotpink> points to a more fundamental raytrace problem, such as an un-modeled interior volume or an incorrect boolean result. Examining the object with `nirt` and comparing with hand calculations or analytic formulas is a good next step.
- <cyan>**Failures only at deep nodes, with parents skipped**</cyan>: the chain of child-blocked parent skips shows you exactly where to focus investigation. Fix the deepest failing node first; re-running `lint --raytrace` after each fix will confirm whether the repair resolved the issue and whether any additional failures are now visible.
- <lavender>**Persistent mismatch under `--perturb`**</lavender>: if re-running with `--perturb` does not clear the mismatch, the geometry contains a coplanar sub-sub or sub-base interface pattern (see <purple>**COPLANAR INTERFACE PATTERNS**</purple>) that requires manual geometry correction. Perturbation-based workarounds (varying the perturbation magnitude, post-boolean degenerate-triangle removal, etc.) are not reliable for these patterns because the artifact triangles are geometrically valid (non-degenerate) slivers, not numerically zero-area faces.

---

# πŸ‘‘ AUTHOR πŸ‘‘

BRL-CAD Team

# πŸ’Œ BUG REPORTS πŸ’Œ

okay bestie, if you find a bug or run into a problem, don't just scream into the void β€” <hotpink>actually tell someone</hotpink> 🫢. Reports of bugs or problems should be submitted via electronic mail to `devs@brlcad.org`. That's it. That's the whole vibe. Slide into their inbox with the details and let the devs cook πŸ”₯

---

and that's a wrap on `LINT(nged)`, bestie!! you made it to the end, <rainbow>iconic behavior honestly</rainbow> ✨ this little masterpiece comes courtesy of <purple>BRL-CAD</purple> and it's the `LINT(nged)` page you've been living in this whole time πŸ’… go forth, lint responsibly, and remember: clean geometry is main character energy πŸ‘‘πŸ’–

> ngl if you skipped down here without reading the rest… bestie. go back up. the lore matters πŸ˜­πŸ™ˆ
πŸ“¦ where this page comes from