Skip to content

tessellation: non-manifold edges in dovetail baseplate STL exports (2.89.2) #696

@andymai

Description

@andymai

Summary

brepkit-wasm 2.89.2 produces tessellated meshes with non-manifold edges for dovetail-connector baseplate exports that are watertight under brepjs-opencascade (OCCT). PR #692's compound-boolean variance fix is in this release and behaves correctly — but the tessellation/winding pathology is independent and still present.

The downstream consumer is gridfinity-layout-tool; the failing tests live in src/features/generation/worker/generators/baseplateGenerator.scenario.dovetail.test.ts (originally written against issue gridfinity-layout-tool#1407).

Observed under 2.89.2

Scenario Non-manifold edges
5×4 middle-column tile (3 join edges, tongue protrusion) 142
4×4 interior tile (4 join edges) 73

OCCT (default brepjs kernel) for the same params: 0.

The non-manifold count comes from edge-shared-by-≥3-triangles in the binary STL. The diagnostic also surfaces in stderr as [repairMeshWinding] Skipped non-manifold edges during winding repair { nonManifoldEdges: N, triangleCount: M } from the consumer's defensive winding-repair pass (which is a no-op under OCCT — see src/shared/generation/repairMeshWinding.ts).

Minimal repro

The full repro path is exportBaseplate(params, 'stl') → tessellate → binary STL. The BaseplateParams shapes that fail:

// 5×4 middle-column — 142 non-manifold edges
{
  width: 5, depth: 4, gridUnitMm: 42,
  magnetHoles: false,
  paddingLeft: 0, paddingRight: 0, paddingFront: 0, paddingBack: 0,
  fractionalEdgeX: 'end', fractionalEdgeY: 'end',
  lightweight: true,
  connectorNubs: true,
  edges: { left: 'exterior', right: 'join', front: 'join', back: 'join' },
}

// 4×4 interior — 73 non-manifold edges
{
  width: 4, depth: 4, gridUnitMm: 42,
  magnetHoles: false,
  paddingLeft: 0, paddingRight: 0, paddingFront: 0, paddingBack: 0,
  fractionalEdgeX: 'end', fractionalEdgeY: 'end',
  lightweight: true,
  connectorNubs: true,
  edges: { left: 'join', right: 'join', front: 'join', back: 'join' },
}

To reproduce in the consumer repo:

git checkout main  # brepkit-wasm pinned at ^2.87.1 — bump to ^2.89.2 first
BREPJS_KERNEL=brepkit pnpm exec vitest run \
  src/features/generation/worker/generators/baseplateGenerator.scenario.dovetail.test.ts

Expected: all 9 tests pass.
Actual under brepkit-wasm@2.89.2: at least 2 (middle-column tile, interior tile) fail with non-manifold edges: expected 142 to be +0 // Object.is equality. Older 2.89.1 run failed all 9.

Hypothesis

The 9-fail signature in 2.89.1 narrowed to 2-fail (early in the file) in 2.89.2, but the type of failure is unchanged: triangle winding emitted across face boundaries is inconsistent, causing edges to appear shared by ≥3 triangles in the merged mesh. PR #683 introduced deterministic face order in face_components; PR #692 fixed HashMap iteration in the GFA pipeline. Either:

  1. A remaining non-deterministic iteration site in the tessellation/edge-emission path is producing different winding for adjacent faces, or
  2. The boolean output topology is non-manifold (T-junction or shared coplanar face residue), which the tessellator then faithfully passes through.

The gridfinity-layout-tool fix that motivated these tests (TONGUE_PROTRUSION — extending the tongue's base edge a small amount INTO the slab to give the fuse shared volume rather than a degenerate coplanar face) was specifically to avoid hypothesis (2) under OCCT. If brepkit still produces non-manifold output even with the protrusion, hypothesis (1) or a separate residue case in the boolean is more likely.

Companion observation: perf

PR #692's variance fix landed cleanly — the same consumer's 4×4 with lip bench went from max 142ms / RME ±51.7% on 2.87.1 to max 21.5ms / RME ±4.0% on 2.89.2. Multi-boolean determinism is solved.

Independent of variance, the 2.89.1 test run had two outlier wall-times that are worth a sanity-check on 2.89.2 once the manifold issue is fixed:

Test Wall-time on 2.89.1
binExporter.split-noexport > exports successfully after split preview — magnet+screw base with lip 1,139 s
baseplateGenerator.scenario.winding > magnet baseplate with lightweight floor — winding consistent 649 s

Both are likely consequences of the failure path (broken topology cascading into expensive mesh repair), so they should clear up alongside the manifold fix — flagging here in case they don't.

Environment

  • brepkit-wasm@2.89.2 via npm
  • brepjs@18.5.3 with BrepkitAdapter from brepjs.registerKernel('brepkit', new BrepkitAdapter(kernel))
  • Node 24.11.0, vitest 4.1.6, Linux x86_64

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions