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 @@ -53,7 +53,7 @@
| 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` | 41 | v1.13.0. Диагностика и ремонт STL, Decimate, Smooth, Lay on Face, измерения, анализ нависаний |
| 43 | `43-editor-cut-split` | 41, 42 | v1.14.0 (план). Plane Cut, Split с соединителями, отверстия под магниты/винты, текст на грани |
| 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/43-editor-cut-split/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-27
37 changes: 37 additions & 0 deletions openspec/changes/43-editor-cut-split/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Design

## Context

The baseplate splitter (`geometry/split.ts`, `connectors.ts`) is grid-aware: cuts are between cells, connectors go into ribs. A general model has no ribs; connectors must be placed in the *section* of the cut. `41` supplies stack operations and booleans, `42` supplies planar region picking. Slicers (Cut tool with connectors), Meshmixer (Plane Cut, Separate Shells) and Fusion (Split Body, holes) are the references.

## Goals / Non-Goals

**Goals**: reliable cuts of closed solids, parts that print and assemble (clearances, depth limits), a hole tool with real fastener presets, editable and derived results.
**Non-Goals**: topology-aware smart cutting, threads, wrapping text.

## Decisions

1. **Derived parts.** `doc.cuts: CutGroup[]` where `CutGroup = {id, sourceObject, planes: Plane[] | grid {x[], y[], z[]}, connectors: ConnectorSet, bed?: {…}}`. Each resulting part is an ordinary object with `source: {kind:'cutPart', group, index}` (and its own transform = identity relative to the source frame, so the parts stay assembled in the scene by default, with an *Explode* slider like the baseplate's exploded view). The source object is hidden and listed under the group in the outliner; deleting the group restores the source. Editing the group recomputes all parts; parts may carry their own stack after the cut (`41`), applied to their derived solid.
2. **Cutting.** `cutSolid(solid, group)` splits with `Manifold.splitByPlane` (or `trimByPlane` for keep-one-side) for arbitrary planes; the grid variant is a sequence of axis splits ordered x, y, z. Requires a closed solid (guard from `41`). Both halves are closed by construction. Parts are labelled by column letter and row number as in `split.ts`; arbitrary single planes label A and B.
3. **Split to bed.** `autoCuts(size, usable)` per axis: fewest parts (`ceil(size / usable)`), cuts spread evenly, optionally shifted by a tolerance to align with section minima (sections with the smallest area are preferred within ±10 % of the evenly spaced position) — a cheap way to avoid cutting through large solid areas. The printer, bed and margin come from the export settings through `usableBed`. Parts bigger than the bed after the cut (diagonal parts of rotated objects are not rotated automatically) are marked `oversize` like the baseplate's.
4. **Section and placement.** The cut section is `CrossSection` from the solid sliced at the plane (`Manifold.slice`), expressed in plane coordinates. `placeConnectors(section, {kind, count, size, margin})` finds candidate positions on a grid whose cell is the connector pitch, keeps those whose clearance disc lies inside the section inset by `margin` (default connector diameter), chooses `count` positions by farthest-point sampling starting from the section's pole of inaccessibility (so pairs of pegs spread, singles go to the centre), and returns them in order. The user can drag, add and delete in a 2D preview of the section; manual positions are stored in plane coordinates on the cut definition so they survive edits of the source as long as they stay inside (otherwise warning `cut.connector.outside`, connector skipped).
5. **Connector solids.** Peg: cylinder of diameter *d* and length *l* on part A (union), socket of diameter *d + clearance* and depth *l + 0.3* on part B (difference), both chamfered at the tip. Pin hole: socket in both parts for a separate pin (diameter from the pin presets). Magnet pocket: in both parts, diameter *d + tolerance*, depth *h + tolerance* (polarity left to the user). Dovetail: a trapezoid profile (length, angle 15°, depth) extruded along the section polygon's chosen axis, key on A, socket on B grown by the clearance, depth limited by the material thickness. Depth for all is limited to the minimum of the configured depth and the available material behind the cut at that point minus 1.5 mm (ray distance through the solid along the normal); less than 2 mm available skips the connector with `cut.connector.thin`. Presets and clearances come from a shared `connectorSpec.ts` extracted from `connectors.ts` so the baseplate keeps its values.
6. **Collision checks.** After building, each connector solid is intersected with the part to verify it is fully inside (volume of connector ∩ part ≥ 99 % of the connector volume for the pocket, ≥ 99 % of the peg inside the opposite part's socket envelope); failures give `cut.connector.outside` or `cut.connector.overlap` with the connector index, shown in the preview in red.
7. **Hole tool.** Surface picking returns `(point, normal)` from the mesh raycaster (`42`); the hole op stores `{point, normal, up}` in object-local space plus parameters `{kind: 'simple'|'counterbore'|'countersink'|'magnet'|'insert', d, depth|through, cbD, cbDepth, csAngle, tolerance, preset?}`. The tool solid is a stack of cylinders/cones along the normal, starting slightly above the surface (+0.2 mm) and subtracted as a `boolean(subtract, generated tool)` in the evaluator (a generated tool is a solid, not a scene object). Presets: M2–M5 screw clearance (loose/normal), heads (DIN 912 and ISO 7380 sizes), countersink 90°, brass heat inserts M2–M5 (outer diameter and length table, documented from vendor ranges), magnets from the `06a` list plus common sizes (6×2, 8×3, 10×2); the table is data in `geometry/spec.ts` style, hand-written and cited in README. `through` extends along the normal to leave the solid (bbox diagonal). Offsets: numeric position fields move the point along the picked face's plane (face frame) relative to the object origin or to a picked edge.
8. **Text on a face.** `text` op: `{point, normal, up, text, font, size, spacing, align, mode: 'emboss'|'engrave', height|depth, rotation}`. The glyph outline polygons come from the rasteriser and `traceAlpha` outline code of Keychain, moved to a shared module `geometry/textShapes.ts`; polygons become a `CrossSection` (EvenOdd), extruded along the normal (emboss starts 0.05 mm inside the surface so the union is watertight; engrave starts 0.05 mm above), positioned by the frame. Only flat projection: on curved surfaces the text keeps its flat shape and the op reports `text.warning.curved` when the sampled surface deviates more than the text depth over the text extent. Fonts register via the existing lazy `FontFace` loaders.
9. **Viewport.** A cutting-plane widget (translucent quad with the gizmo of `39` attached, snaps to axes), cut section preview as a 2D inset panel, surface picking marker with the normal, ghosting of the hidden source. No new three.js dependency.
10. **History and persistence.** All edits go through `dispatch` with merging; `cuts` is a new optional field of the scene document (schema stays 1); normalisation drops connectors with missing groups.
11. **Export.** Derived parts export as ordinary parts with unique names `Name A1`; connectors that are loose pins (pin kind) are exported as separate small parts when "Include pins" is on (default on).

## Risks / Trade-offs

- **Connector placement quality** on odd sections (thin, branched) is heuristic; the preview and manual edit are the escape, and unplaceable connectors are reported rather than forced.
- **Dovetail on arbitrary sections** is only reliable on sections with a straight run of sufficient length; otherwise it is refused with the reason, and pegs are suggested.
- **Hole presets** are hand-written tables; wrong sizes would be annoying, so values are conservative, cited, and editable (custom values always possible).
- **Derived parts and stack ops** interplay: operations added to a part apply to the derived solid after the cut; editing the cut can invalidate them (they fail visibly, as everywhere in `41`).
- **Text quality** depends on font tracing; very small text or thin strokes warn using the same check as Keychain (`textResolution` logic).

## Open Questions

- Whether pegs should be part of A (union) or loose pins by default (assumed: integrated pegs by default, loose pins as an option).
- Whether to show the exploded gap slider in the editor viewport for all groups (assumed yes, per group).
31 changes: 31 additions & 0 deletions openspec/changes/43-editor-cut-split/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Proposal

## Why

Two of the most common reasons to open a model in an editor before printing are "it does not fit on my bed" and "I need a hole for a magnet/screw/insert here". Slicers have a Cut tool, Meshmixer has Plane Cut, and Gridfinity Baseplate already splits plates with connectors (`06`, `06a`) — but only for its own grid. This change brings the same capabilities to any model: cut with a plane, split to the printer bed, join the parts with pins or dovetails, drill fastener and magnet holes anywhere on a surface, and put text on a face.

## What Changes

- **Plane Cut.** A cutting plane defined by an axis and position, by the gizmo (any orientation), by a picked flat face, or by three points. Modes: keep both parts (as separate objects), keep one side, slice into N equal parts along an axis. The parts are *derived*: they keep a reference to the source object and the cut definition, so editing the plane or the source updates them (rule 2a). The cut faces are capped automatically (closed solids).
- **Split to bed.** One command computes the cuts needed so every part fits the printer bed of the export settings (bed minus margin), spread evenly, with a per-axis override and manual adjustment of each cut position; parts are labelled A1, B1… like the baseplate.
- **Connectors on cut faces.** Alignment pegs (cylindrical peg + socket with clearance), through-holes for filament or pins, magnet pockets, and dovetails, using the presets of `06a` (filament 1.75 → Ø1.9, pins 2/3 mm, magnets 3 × 2 and 2.8 × 0.75, dovetail clearance). Placement is automatic over the cut section (inset, even spacing, never closer than a margin to the outline) with click-to-move, add and remove in the preview; each connector is checked to lie inside both parts for its full depth.
- **Hole tool.** Click a surface to place a hole along the surface normal: simple, counterbore, countersink, magnet pocket, screw clearance and heat-insert pocket (M2–M5 and common magnet sizes from presets, with tolerance), blind or through, offsets typed numerically, arrays through the patterns of `41`. A hole is a stack operation with a stored position in object-local space.
- **Text on a face.** Emboss or engrave text at a picked surface point with font, size, spacing, depth/height and rotation, using the font list and rasteriser of Keychain (`19`) and the exact outline tracing, as a stack operation.
- **Plan checks.** Parts that still exceed the bed are marked; connectors that collide or leave a part produce warnings with keys, like the baseplate's `connector.*`.

## Capabilities

### New Capabilities
- `editor-plane-cut`: Plane Cut, split to bed, derived parts.
- `editor-cut-connectors`: pegs, pins, magnets, dovetails on cut faces, placement and checks.
- `editor-holes-text`: hole tool with presets and text on faces.

### Modified Capabilities

## Impact

- New `src/geometry/cut/{plane,split,section,connectors,place}.ts` (reusing `PRESETS`/clearance constants of `connectors.ts` and `fasteners.ts` through a shared module), `src/geometry/features/ops/{hole,text}.ts`, doc field `cuts` (`CutGroup`), derived-part object source `{kind:'cutPart', group, index}`, `features/editor/{CutTool,SplitDialog,ConnectorEditor,HoleTool,TextTool}`, viewport support for the cutting plane widget and surface picking markers, strings, tests.
- Reuse: `Manifold.trimByPlane/splitByPlane`, `CrossSection` of the cut, `rasterizeText` and `textOutline` of Keychain (moved to shared modules if they are feature-private), `usableBed` and printers of `09`.
- Out of scope: cutting with curved or sketch-shaped surfaces (cut tool = a Boolean with a tool object, available from `41`), automatic best-cut search that avoids features, interlocking joints beyond pegs and dovetails, threads (real helical threads), text along curves or wrapped on curved surfaces (flat projection only), per-part colours, assembly animation.
- Version 1.14.0 (minor: a new capability).
- Depends on: `41-editor-op-stack` (stack operations, booleans, patterns, closed-solid guard) and `42-editor-mesh-repair` (planar face region for plane-from-face and surface picking).
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Spec Delta

## Purpose
Defines connectors on cut faces: pegs, pin and magnet sockets, dovetails, their placement and checks.

## ADDED Requirements

### Requirement: Connector types
The Editor SHALL offer connectors on a cut face: integrated alignment pegs with sockets, holes for separate pins or filament, magnet pockets, and dovetails, with size, depth and clearance, using the presets of the baseplate connectors.

#### Scenario: Peg
- **WHEN** a peg connector of 4 mm diameter and 8 mm length is added
- **THEN** part A has a peg of 4 mm and part B a socket larger by the clearance and slightly deeper

#### Scenario: Filament hole
- **WHEN** the filament preset is chosen
- **THEN** both parts get a hole of 1.9 mm diameter and the default depth

#### Scenario: Magnet pockets
- **WHEN** the magnet preset 6 × 2 is chosen
- **THEN** both parts get a pocket of the magnet size plus tolerance

#### Scenario: Dovetail
- **WHEN** a dovetail is chosen on a section with a straight run of enough length
- **THEN** part A has the key and part B the socket with clearance, and the parts join by sliding

### Requirement: Placement
Connectors SHALL be placed automatically over the cut section, evenly and away from the outline, and SHALL be movable, addable and removable in a section preview.

#### Scenario: Two connectors
- **WHEN** count 2 is chosen on a rectangular section
- **THEN** the connectors are spread along the longer side with equal margins

#### Scenario: Drag
- **WHEN** the user drags a connector in the preview
- **THEN** it moves and the check runs again for the new position

#### Scenario: Thin section
- **WHEN** the section is too small for any connector
- **THEN** none is placed and the message says why

### Requirement: Fit checks
Each connector SHALL be checked to lie fully inside both parts for its depth; problems SHALL be reported by key with the connector number and shown in red, and the connector SHALL be skipped.

#### Scenario: Leaves the part
- **WHEN** a manually placed peg would leave the part
- **THEN** it is skipped with the "outside" message

#### Scenario: Too little material
- **WHEN** less than 2 mm of material lies behind the cut at a connector
- **THEN** it is skipped with the "thin" message

### Requirement: Loose pins
Connectors of the pin kind SHALL be exportable as separate small parts.

#### Scenario: Include pins
- **WHEN** Include pins is on and the scene is exported
- **THEN** one pin part per pin connector is included and named
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Spec Delta

## Purpose
Defines the hole tool with fastener presets and text on faces.

## ADDED Requirements

### Requirement: Hole tool
The Editor SHALL place a hole where the user clicks a surface, along the surface normal, with the types simple, counterbore, countersink, magnet pocket and heat-insert pocket, blind or through, using presets for common screws, inserts and magnets with a tolerance, and numeric position fields.

#### Scenario: Place a screw hole
- **WHEN** the user clicks a top face and chooses M3 clearance, through
- **THEN** a hole of the M3 clearance diameter goes through the part along the face normal

#### Scenario: Counterbore
- **WHEN** the counterbore type is chosen for an M3 socket head
- **THEN** the head recess has the preset diameter and depth over the clearance hole

#### Scenario: Magnet pocket
- **WHEN** a 6 × 2 magnet pocket is placed
- **THEN** the pocket is the magnet size plus tolerance and 2 mm deep plus tolerance

#### Scenario: Inclined face
- **WHEN** the face is tilted 30°
- **THEN** the hole axis follows the face normal

#### Scenario: Type the position
- **WHEN** the user types offsets of 10 and 5 mm in the face plane
- **THEN** the hole moves to that position

#### Scenario: Array
- **WHEN** the hole operation is combined with a linear pattern of `41`
- **THEN** the holes repeat with the given spacing

### Requirement: Hole is an operation
A hole SHALL be an editable operation in the object's stack with its position stored in object space, so changing the source or reordering recomputes it.

#### Scenario: Edit diameter
- **WHEN** the user changes the hole size later
- **THEN** the hole updates without losing its position

#### Scenario: Missing material
- **WHEN** the stored position no longer lies on the surface
- **THEN** the operation still cuts where the tool volume meets material and reports a warning if nothing is cut

### Requirement: Text on a face
The Editor SHALL emboss or engrave text at a picked surface point with font, size, spacing, alignment, rotation and depth or height, as an editable stack operation.

#### Scenario: Engrave
- **WHEN** the user engraves "A1" 1 mm deep on a flat top face
- **THEN** the text is cut 1 mm into the surface

#### Scenario: Emboss
- **WHEN** the user embosses text 0.8 mm high
- **THEN** the text stands 0.8 mm above the surface and is fused with the body

#### Scenario: Curved surface
- **WHEN** the text extends over a surface that deviates more than the text depth
- **THEN** a warning says the text is projected flat

#### Scenario: Cyrillic and Latin
- **WHEN** a font without the needed glyphs is chosen
- **THEN** the same fallback as in the keychain text applies and a notice is shown

#### Scenario: Fine strokes
- **WHEN** the strokes are thinner than the nozzle allows
- **THEN** the thin-stroke warning is shown
Loading
Loading