Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openspec/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
| 39 | `39-editor-foundation` | 29, 31a | v1.10.0. **Свободный редактор** (главный режим, см. `openspec/EDITOR.md`): продукт `/editor`, оболочка (outliner, свойства, панель инструментов), сцена из объектов, импорт STL, примитивы, выбор, гизмо и числовые трансформации, привязка, undo/redo, экспорт сцены; Viewport переиспользуется и расширяется |
| 40 | `40-editor-sketch` | 39 | v1.11.0. Эскиз на плоскости: инструменты (прямоугольник с радиусом углов), Fillet/Chamfer 2D, ограничения и размеры, степени свободы, импорт SVG, Extrude/Revolve в тела (join/cut/intersect, фаска/скругление краёв); тело = эскиз + список операций, сетка производная (`EDITOR.md` 2a) |
| 41 | `41-editor-op-stack` | 40 | v1.12.0. Неразрушающий стек операций (пересчёт от источников, `EDITOR.md` 2a), дерево/timeline, булевы, Mirror, Pattern, Hollow, Offset |
| 42 | `42-editor-mesh-repair` | 39 | v1.13.0 (план). Диагностика и ремонт STL, Decimate, Smooth, Lay on Face, измерения, анализ нависаний |
| 42 | `42-editor-mesh-repair` | 41 | v1.13.0. Диагностика и ремонт STL, Decimate, Smooth, Lay on Face, измерения, анализ нависаний |
| 43 | `43-editor-cut-split` | 41, 42 | v1.14.0 (план). Plane Cut, Split с соединителями, отверстия под магниты/винты, текст на грани |
| 44 | `44-editor-face-tools` | 41 | v1.15.0 (план). Выбор граней/рёбер, Push/Pull, Fillet/Chamfer, эскиз на грани, Hole wizard |
| 45 | `45-editor-project-default` | 43, 44 | v2.0.0 (план, major: `/` ведёт в редактор). Файл проекта `.pkit` (ZIP: scene.json + сетки), автосохранение, редактор — вход по умолчанию, генераторы как вставляемые объекты |
Expand Down
2 changes: 2 additions & 0 deletions openspec/changes/42-editor-mesh-repair/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-27
44 changes: 44 additions & 0 deletions openspec/changes/42-editor-mesh-repair/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Design

## Context

`39` provides `meshHealth` (counts), mesh sources in IndexedDB addressed by content hash, and `41` refuses booleans on open meshes. Repairs are *destructive* mesh edits (they change the source), unlike stack operations; the stack model of `41` handles that by Flatten and by applying repairs only to mesh sources: a repair produces a new immutable mesh and swaps the object's `source.meshId`, with undo restoring the old id. The reference tools are Meshmixer (Inspector, Make Solid, Reduce, Smooth), netfabb/3D Builder (auto repair) and slicer repair, plus the analysis views of PrusaSlicer/Bambu Studio.

## Goals / Non-Goals

**Goals**: reliable repair of typical downloaded STLs; honest reports (what was fixed, what was not); everything responsive on 500k-triangle meshes.
**Non-Goals**: geometry-preserving guarantees for broken input, quality remeshing, mesh sculpting.

## Decisions

1. **Half-edge-free analysis.** `analyze(mesh)` builds an edge map keyed by sorted vertex pairs (typed arrays, no objects per edge): per edge the incident triangle count and orientation consistency → open edges (1), manifold (2 consistent), non-manifold (>2), inconsistent winding (2 same direction). Boundary loops come from linking open edges; shells from union-find over triangles; volume from the signed tetrahedra; degenerate = area < 1e-12 mm² or duplicate vertex ids; duplicates = same sorted vertex triple. Output is a plain `Report` with index lists (for highlighting), no geometry objects.
2. **Self-intersections.** Triangle–triangle tests with a uniform grid broad phase (cell ≈ average edge length), Möller test, ignoring pairs that share a vertex or edge; capped at 5 M candidate pairs with a progress job and abort; reports count and the first 1000 pairs for highlighting. Large meshes can skip the check ("analyse intersections" is a button, not automatic).
3. **Repairs as pure functions** `(mesh, args) → {mesh, report}` in `geometry/mesh/`:
- `weld(tolerance)`: spatial hash welding; default 1e-4 mm relative to the bounding diagonal.
- `removeDegenerate`, `removeDuplicates`.
- `fixOrientation`: BFS over shells propagating consistent winding across manifold edges, then flip the shell if its signed volume is negative; non-manifold edges break the propagation (reported).
- `fillHoles(loops, {mode: 'flat'|'smooth', maxPerimeter})`: ear clipping on the loop projected on its best-fit plane for 'flat' (checking for self-overlap and falling back to a fan around the centroid); 'smooth' adds interior vertices by subdivision and relaxes them (Laplacian with the boundary fixed); a loop larger than the limit is skipped and reported; the existing `fillHoles` of `stl-import.ts` becomes the simple fallback.
- `removeShells({maxTriangles, maxVolume, keepLargest})`.
- `makeSolid({resolution})`: the winding-number field of the (possibly broken) mesh sampled on a grid with cell = resolution (default bbox diagonal / 400, min 0.05 mm), then `Manifold.levelSet` extraction at 0.5 — closed, manifold, self-intersection-free, slightly softened sharp edges; the memory estimate (cells × 4 bytes) is checked before starting and the resolution is raised or the job refused with the number. Marked in the UI as "rebuilds the surface".
- `autoRepair`: weld → remove degenerate/duplicates → fix orientation → fill holes (default limit 20 mm perimeter) → remove tiny shells (< 0.1 % of the largest) → re-analyse; if still not manifold and the user allowed it, Make Solid. A report lists each step with counts.
4. **Decimate.** The manifold solid path uses Manifold's `simplify(tolerance)`; the UI takes a target ratio and searches the tolerance by bisection (≤ 8 evaluations) showing the achieved triangle count and the maximum deviation estimate (≤ tolerance). A non-manifold mesh is first run through `autoRepair` or refused with the reason. The result is a new mesh; the deviation preview colours vertices by distance to the original (sampled).
5. **Smooth.** Taubin (λ|μ) smoothing on the vertex graph with cotangent-free uniform weights; parameters strength 0–1, iterations 1–50; vertices on boundary loops and on sharp edges (dihedral > angle threshold, default 40°) are fixed; area-preserving by construction (μ < 0). Applied to mesh sources only.
6. **Orientation.** `layOnFace(mesh, triIndex, tol)` finds the connected planar region (normal within tolerance 1°, dihedral growth) and returns the rotation taking its normal to −Z; the same region function `planarRegion` is exported for `44`. `bestOrientations(mesh)` evaluates up to 24 candidates (the six axis-aligned flips plus the largest flat regions' normals) and scores `a·overhangArea + b·height − c·contactArea` (weights from slicer practice, documented), returning the top three as selectable previews; applying is one history step (a transform, not a mesh change).
7. **Measure.** Picking uses the raycaster against the mesh (BVH from `three-mesh-bvh` is added here if the foundation did not need it; MIT) and snaps to the nearest vertex, edge midpoint or face centre within 8 px. Circle fit is a least-squares fit in the plane of the picked points (≥ 3 points), reported with the fit residual; wall thickness casts a ray from the picked point along the inward normal to the first back-face hit. Results are `Measurement {kind, value, points}` in mm in world space, displayed with the unit setting and rendered as lines and labels; not stored in the document.
8. **Analysis overlays.** Overhang: per-triangle angle between the face normal and the bed normal after the object's matrix, coloured by a ramp beyond the threshold, drawn as a vertex-colour attribute (non-indexed copy only for the shown object; recomputed on transform commit, not during drag). Thin wall: per-vertex inward ray distance sampled on a subset (≤ 100k rays, worker), threshold = 2 × nozzle from the export settings. Open edges/flipped faces: line segments and colour set from the report. Section view: a clipping plane on the scene materials with a slider and an axis selector; no cap geometry.
9. **Worker.** `mesh.worker.ts` runs analyses and repairs with a `MeshClient` (latest wins per lane, cancel recreates the worker, progress through the job API of `31a`), like the photo worker. Meshes are transferred, not copied, and the result mesh is stored and registered like imports.
10. **Document.** A repair changes `source.meshId`; `source.originMeshId` (new optional field) remembers the first imported id so "Revert to imported" works across sessions; unused meshes are collected at load time except those referenced by `originMeshId`.
11. **UI.** A "Mesh" section in Properties for mesh objects (Health summary → Details expands lists with Show, Repair actions with options, Decimate, Smooth, Orientation, Revert); Measure and Analysis are tool bar toggles with their own small panels. No artboard exists: built from `ui/` components and tokens.

## Risks / Trade-offs

- **Self-intersection and Make Solid cost** on big meshes: limits, estimates, abort and clear messages; the first is optional, the second needs a confirmation with the estimate.
- **Fill holes quality** on large or twisted loops: size limit and the 'smooth' option; the report lists skipped loops.
- **Make Solid softens details**: the UI says so and recommends the finest resolution that fits the budget; it is never applied automatically unless Auto repair is allowed to.
- **Decimate on non-manifold input** can fail or tear; repair first, refuse otherwise.
- **Overhang map is orientation dependent** and ignores bridges; it is a visual aid, not a print simulation.

## Open Questions

- Whether `three-mesh-bvh` is needed already (decided by the performance task in `39`/here).
- Default weights of the best-orientation score (tuned on three reference models in task 4.3).
33 changes: 33 additions & 0 deletions openspec/changes/42-editor-mesh-repair/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Proposal

## Why

Most STL files found online are not clean: holes, flipped faces, duplicate vertices, tiny floating shells, self-intersections. They print badly or cannot be used in booleans (`41` refuses open meshes). Desktop tools (Meshmixer, 3D Builder, netfabb, slicer repair) exist largely for this. The foundation (`39`) already shows a health summary; this change adds the diagnosis in detail, the repairs, the mesh-reducing operations (Decimate, Smooth), the orientation helpers (Lay on Face, best orientation) and the measuring and analysis tools that every printing workflow uses (distance, angle, hole diameter, wall thickness, overhangs, cross-section view).

## What Changes

- **Detailed diagnosis** on a mesh object: open edges grouped into boundary loops (count, perimeter), non-manifold edges and vertices, degenerate and duplicate triangles, flipped triangles and inverted volume, disconnected shells (count, volumes), self-intersections (count and a progress/abort job), small shells under a threshold. Problems are listed with *Show* (highlighted in the view and framed).
- **Repairs** (each one undoable, each a new immutable mesh): Weld vertices (tolerance), Remove degenerate / duplicate triangles, Fix orientation (consistent winding, outward by volume), Fill holes (flat or smoothed fill, per loop or all, size limit), Remove small shells, Resolve self-intersections and **Make Solid** (remesh through a winding-number level set at a chosen resolution, as the last resort), and **Auto repair** (the sequence with a report and a before/after summary).
- **Decimate** (target triangle count or tolerance in mm, with before/after and a preview of the deviation) and **Smooth** (Taubin smoothing with strength and iterations, boundaries and sharp edges preserved).
- **Orientation:** **Lay on Face** (click a flat region, it goes to the bed), **Best orientation** (suggests up to three orientations scored by overhang area, height and contact area), Drop to bed is already in `39`.
- **Measure tool:** distance between two picked points (vertex, edge midpoint, face centre or free), angle between three points or two faces, circle fit (diameter of a hole or boss from three or more points), wall thickness at a point; results in a list in mm/in with copy. Measurements are session-only.
- **Analysis overlays:** overhang map for the current orientation (angle threshold, default 45°), thin-wall map below the nozzle-based threshold taken from the export settings, open-edge and flipped-face highlights, a **section view** (clipping plane along X/Y/Z with a slider).
- **Heavy work in a worker** (`mesh.worker.ts` with progress and cancel through the log jobs of `31a`), so the UI stays responsive on large meshes.

## Capabilities

### New Capabilities
- `editor-mesh-health`: diagnosis, listing, highlighting, shells and self-intersections.
- `editor-mesh-repair`: repairs, Make Solid, Auto repair, Decimate, Smooth, orientation helpers.
- `editor-measure-analysis`: measuring tool, overhang and thin-wall maps, section view.

### Modified Capabilities

## Impact

- New `src/geometry/mesh/{analyze,weld,orient,fillHoles,shells,intersections,makeSolid,decimate,smooth,orientation}.ts` (React-free, tested with fixtures), `mesh.worker.ts` + `MeshClient`, `src/features/editor/{HealthPanel,RepairPanel,MeasureTool,AnalysisPanel,SectionSlider}`, viewport overlay hooks (highlight sets, clipping plane, per-vertex colours for maps), strings in en and ru, tests. Repaired meshes replace the object's source mesh id (the previous id stays in history; `originMeshId` keeps the import for "Revert to imported").
- `fillHoles` of `stl-import.ts` is reused and generalised; `describeMesh`/`flatFaces` are reused for flat regions.
- The "not a closed solid" messages of `41` get a "Repair" action pointing here.
- Out of scope: sculpting-type edits, remeshing for quality (isotropic), texture/colour repair, face and edge editing (`44`), cutting and splitting (`43`), automatic support generation, slicing-based analysis, AI repair.
- Version 1.13.0 (minor: a new capability).
- Depends on: `41-editor-op-stack` (the not-solid messages and Flatten link; mesh sources and stacks), which depends on the foundation `39`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Spec Delta

## Purpose
Defines measuring and the analysis views.

## ADDED Requirements

### Requirement: Measure tool
The Editor SHALL measure the distance between two picked points, the angle between three points, the diameter of a circle fitted to three or more points, and the wall thickness at a point, snapping to vertices, edge midpoints and face centres, and showing results in the selected unit with a copy action.

#### Scenario: Distance
- **WHEN** the user picks two opposite corners of a 40 × 30 × 20 mm block
- **THEN** the distance is shown as 53.85 mm

#### Scenario: Hole diameter
- **WHEN** the user picks three points on the edge of a 6 mm hole
- **THEN** the diameter is shown as 6.0 mm with the fit residual

#### Scenario: Thickness
- **WHEN** the user picks a point on the outer face of a 2 mm wall
- **THEN** the thickness is shown as 2.0 mm

#### Scenario: Inches
- **WHEN** the unit is inches
- **THEN** the values are shown in inches and the stored millimetre values are unchanged

### Requirement: Overhang map
The Editor SHALL colour surfaces that overhang more than a chosen angle for the current orientation, updating after a transform is committed.

#### Scenario: Tilted cube
- **WHEN** a cube is tilted and the map is on at 45°
- **THEN** the downward faces beyond 45° are coloured and the others are not

### Requirement: Thin-wall map
The Editor SHALL colour regions whose wall thickness is below a threshold that defaults to twice the nozzle diameter from the export settings.

#### Scenario: Thin rib
- **WHEN** a model has a 0.5 mm rib and the nozzle is 0.4 mm
- **THEN** the rib is coloured as too thin

### Requirement: Section view
The Editor SHALL clip the view with a plane along X, Y or Z controlled by a slider, without changing the model or exports.

#### Scenario: Section
- **WHEN** the user moves the Z section slider to the middle of a hollow part
- **THEN** the cavity is visible and exporting still produces the whole part

### Requirement: Analysis is view-only
Measurements and overlays SHALL NOT be saved in the document or create history steps.

#### Scenario: Reload
- **WHEN** the page is reloaded
- **THEN** no measurements or overlays remain
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Spec Delta

## Purpose
Defines the detailed diagnosis of a mesh and how problems are listed and shown.

## ADDED Requirements

### Requirement: Detailed diagnosis
For a mesh object the Editor SHALL report the number of triangles and vertices, open edges grouped into boundary loops with their perimeter, non-manifold edges and vertices, degenerate and duplicate triangles, triangles with inconsistent or flipped orientation, whether the volume is negative, the number of separate shells with their volumes, and the surface area and volume.

#### Scenario: Cube with a hole
- **WHEN** a cube with one triangle missing is analysed
- **THEN** one boundary loop of three edges is reported and the mesh is marked open

#### Scenario: Two shells
- **WHEN** a mesh consists of a cube and a floating small sphere
- **THEN** two shells are reported with their volumes

#### Scenario: Clean mesh
- **WHEN** the mesh is a closed manifold solid
- **THEN** the panel says it is a valid solid and shows volume and area

### Requirement: Show problems
Each reported problem SHALL be listed and, when selected, highlighted in the view and framed by the camera.

#### Scenario: Show a hole
- **WHEN** the user clicks a boundary loop in the list
- **THEN** its edges are drawn highlighted, and the camera frames the loop

### Requirement: Self-intersections
The Editor SHALL detect self-intersecting triangles on request, report their count and highlight them, showing progress and allowing cancellation.

#### Scenario: Cancel
- **WHEN** the user cancels a running intersection check
- **THEN** the job ends, the log says cancelled, and the editor stays responsive

#### Scenario: Large mesh
- **WHEN** the candidate pair count exceeds the limit
- **THEN** the check stops with a message that gives the limit and does not freeze the page

### Requirement: Diagnosis does not change the mesh
Analysis SHALL NOT modify the mesh or create history steps.

#### Scenario: Analyse
- **WHEN** the user opens the health details
- **THEN** the undo history is unchanged
Loading
Loading