diff --git a/openspec/ROADMAP.md b/openspec/ROADMAP.md index d42d85f..013ee04 100644 --- a/openspec/ROADMAP.md +++ b/openspec/ROADMAP.md @@ -51,7 +51,7 @@ | 38 | `38-webmcp` | 31a, 29 | v1.9.0. Инструменты WebMCP для браузерных агентов: чтение и запись параметров активного режима (baseplate, box, rack, keychain shape) с валидацией, сводка модели, экспорт и сброс только с подтверждением пользователя, журнал и отмена действий агента, переключатель; без эффекта, если браузер не поддерживает WebMCP | | 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 | +| 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, измерения, анализ нависаний | | 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 | diff --git a/openspec/changes/41-editor-op-stack/.openspec.yaml b/openspec/changes/41-editor-op-stack/.openspec.yaml new file mode 100644 index 0000000..97e3642 --- /dev/null +++ b/openspec/changes/41-editor-op-stack/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-27 diff --git a/openspec/changes/41-editor-op-stack/design.md b/openspec/changes/41-editor-op-stack/design.md new file mode 100644 index 0000000..6fd9c80 --- /dev/null +++ b/openspec/changes/41-editor-op-stack/design.md @@ -0,0 +1,41 @@ +# Design + +## Context + +`39` stores objects `{source, transform}`; `40` adds sketch-built bodies with a feature list. Rule 2a (`openspec/EDITOR.md`): descriptions are the source, meshes are derived. A single mechanism must now cover sketch features, booleans with other objects and body operations. Blender's modifier stack and Fusion's timeline are the references; Blender's *order matters, non-destructive, apply when done* model is the simplest to explain and implement. + +## Goals / Non-Goals + +**Goals**: one stack model for everything; fast edits through caching; safe dependency handling; clear messages for open meshes and failures. +**Non-Goals**: parametric constraints between bodies, formulas, assembly joints, a separate timeline bar. + +## Decisions + +1. **Model.** `EditorObject = {…, source: Source, stack: Op[]}`; `Source` = `mesh | primitive | empty` (an empty source is a sketch-built body whose first op is Extrude/Revolve with `op: 'new'`). `Op = {id, kind, params, enabled, label?}` with kinds: `extrude`, `revolve` (from `40`), `boolean`, `mirror`, `linearPattern`, `circularPattern`, `offset`, `hollow`. Later changes add `planeCut`, `hole`, `text` (43), `pushPull`, `edgeBreak` (44), `decimate`, `smooth` (42, as ops on a mesh source). +2. **Evaluation.** `evaluateObject(obj, ctx)` folds the enabled ops over the source result. Each op is a pure function `(solid | null, op, ctx) → solid` where `ctx` gives sketches, other objects' evaluated results (for tools), the quality and the abort signal. A failed op returns `{solid: previous, failed: {index, reason}}` and evaluation stops for that object (as in `40`). The rollback marker is `obj.rollback` (index or `null`); ops after it are not evaluated and the result is the state at that step. +3. **Cache.** Key of step *k* = hash of `(source id/params, ops[0..k] params + enabled, ids and versions of tools used up to k, quality)`. The worker keeps a map from key to `Manifold` with an LRU bound by triangle count (default 6 M) and deletes evicted manifolds (`.delete()`); editing op *k* reuses result *k−1*. Only the final solid of each visible object is turned into a mesh and sent; intermediate solids never leave the worker. +4. **Graph.** A `boolean` op references tool objects by id. `buildGraph(objects)` gives dependencies; evaluation order is a topological order; a cycle is detected when the op is created or edited and refused with the cycle named (`stack.error.cycle`). The tool is evaluated *with its own stack and its own transform*, then brought into the target's local space by `inverse(targetMatrix) · toolMatrix` (the worker receives matrices for this reason; `39` already carries them). Deleting a tool object marks the dependent ops failed (reason `toolMissing`); undo restores both. +5. **Closed-solid guard.** Booleans, offset and hollow need valid Manifold input. The source/tool mesh is converted with `toManifold`; on failure the op fails with `stack.error.notSolid` naming the object and the health summary of `39` (`openEdges`, …), with an action link to repair once `42` exists (until then the message is plain). +6. **Combine command.** For objects A, B, C selected with A active: Union → A gets `boolean(union, [B, C])`; Subtract → `boolean(subtract, [B, C])`; Intersect likewise. B and C become tools: hidden by default, `toolOf` shown in the outliner. This is one history step. The reverse (Release tool) removes the op and shows the tool again. +7. **Tools in the view and export.** An object that is a tool of an enabled op is a *tool object*: if visible it is drawn as a ghost (translucent, the existing tool colour tokens, no outline pick priority), otherwise hidden. Export skips tool objects unless they are explicitly selected with "Selection only". The 3D measurements bar uses the result bodies only. +8. **Mirror.** `mirror {plane: 'XY'|'YZ'|'XZ' (object local), origin: 'object'|'bbox'|number, merge}`: `merge` unions the body with its mirror image (default), otherwise both shells are kept in one solid (disjoint union); Manifold's `mirror(normal)` plus a translation. +9. **Patterns.** `linearPattern {axis, count (2–200), spacing | total, symmetric, merge}` and `circularPattern {axis, count, angle (full or partial), merge}` copy the body so far with Manifold transforms; `merge` unions, otherwise disjoint union (with an overlap warning). Axis: `x|y|z` of the object, a sketch line (plane frame), or two points later. Counts above a limit (triangles × count > 6 M) are refused. Pattern of the body is the only mode; patterns of a single earlier feature are out of scope, and hole arrays are done by patterning the *tool* object and subtracting it. +10. **Offset and Hollow.** `offset {distance}` (positive grows, negative shrinks) and `hollow {wall, drains: [{point, normal, diameter}] }`. Manifold Minkowski operations with a sphere approximation: the sphere is built as a polyhedron whose *circumscribed* radius is the requested distance for the minimum wall guarantee (shrink) and *inscribed* for the maximum size guarantee (grow), segment count from the tolerance (16–48). Hollow = `body − offset(body, −wall)` with drains as cylinders from the cavity to the outside along the given normal; a body that cannot hold a cavity of that wall (empty offset) fails with `stack.error.wallTooThick`. The spike (1.1) measures Minkowski time and robustness on real meshes; the limit on triangles and the fallback message are set from its results. +11. **Apply and Flatten.** *Apply* replaces ops[0..k] with the baked mesh as a new `mesh` source only if k is the first op; applying an op in the middle is not allowed (the UI offers Flatten). *Flatten* evaluates the stack, stores the mesh as a new immutable mesh (content hash) and resets the source to `mesh` with an empty stack; keeps an undo step. Flatten is the bridge to destructive mesh edits of `42`, which operate on mesh sources. +12. **Migration.** `migrateScene` turns `source: {kind:'feature', features}` of `40` into `source: {kind:'empty'}` with `stack: features.map(toOp)`; unknown op kinds are kept but disabled and shown as "unknown operation" so newer files do not lose data. +13. **UI.** The properties panel of an object shows a "Stack" section: a list of rows (drag handle, eye, kind icon, summary, failed mark) with rollback marker between rows, an Add menu (Boolean, Mirror, Pattern, Offset, Hollow, Extrude, Revolve), edit-in-place forms per op using `LengthField`/`NumberField`, and Flatten. The outliner shows chips for the number of ops and the tool relation. No artboard exists: built from `ui/` components and tokens. +14. **History.** Stack edits go through `dispatch(label, patch, mergeKey)`; drag reordering is one step; typing merges. + +## Risks / Trade-offs + +- **Minkowski cost.** Offsetting a non-convex 500k-triangle mesh with a sphere may take many seconds or memory. Mitigations: triangle limit with a message, progress/abort in the log, coarse sphere in the preview, cached result. The spike decides the limits. +- **Cache memory.** Many cached solids on large meshes; LRU by triangles plus explicit `delete()`; tested for leaks (counting live manifolds). +- **Order sensitivity.** Reordering ops can make later ones fail; the UI shows the failed row and keeps the state valid instead of blocking. +- **Floating point.** Booleans of nearly coplanar faces produce slivers; tools are not auto-grown (a user-facing "overlap" is the user's choice), and the Offset op exists to adjust. +- **Scope creep toward a full CAD timeline.** Parameters, formulas and groups are left out deliberately. + +## Open Questions + +- Spike 1.1: Minkowski limits and sphere resolution. +- Whether Mirror should default to `merge` for symmetric printing parts (assumed yes). +- Whether the rollback marker should also pause the display of dependent objects (assumed no: dependants see the full result). diff --git a/openspec/changes/41-editor-op-stack/proposal.md b/openspec/changes/41-editor-op-stack/proposal.md new file mode 100644 index 0000000..0aa0a28 --- /dev/null +++ b/openspec/changes/41-editor-op-stack/proposal.md @@ -0,0 +1,32 @@ +# Proposal + +## Why + +After `40` a body built from sketches keeps a short list of features, and `39` objects are independent of each other. Real parts need operations *between* bodies and *on* bodies: subtract a hole pattern, add a boss, mirror a half, hollow a model to save resin or filament, grow a part by 0.2 mm for clearance. Desktop tools solve this with a non-destructive history (Fusion's timeline, Blender's modifier stack) where each operation stays editable. `openspec/EDITOR.md` (section 2) fixes the model: objects with a source and a stack of operations evaluated in the worker. This change turns the feature list of `40` into that general stack and adds the first body operations. + +## What Changes + +- **Operation stack.** Every object gets an ordered `stack` of operations applied to its source (imported mesh, primitive, or a sketch-built base). Each operation can be edited, reordered by drag, suppressed (eye), deleted, applied (baked into the result) or the whole stack flattened. A rollback marker shows the object as it was after a chosen step. The sketch features Extrude and Revolve of `40` become stack operations; stored scenes of `40` are migrated automatically. +- **Booleans between objects.** Union, Subtract and Intersect with one or more *tool* objects, in the target's local space. Tools stay in the scene (hidden or shown as translucent ghosts) and keep their own transform and stack, so moving the tool moves the cut. A **Combine** command (select several objects → Union / Subtract / Intersect) creates the operation in one step. Requires closed solids; open meshes are refused with the reason and a pointer to repair (`42`). +- **Mirror** (about a plane through the object origin, a bounding-box centre plane or a plane along an axis; merged or kept as separate shells). +- **Linear and circular patterns** of the body so far, with count, spacing or total length, axis from a world axis, a sketch line or the object origin. A pattern on a *tool* object followed by a Subtract gives hole arrays. +- **Offset** (grow or shrink by a distance) for clearances and wall changes, and **Hollow** (shell with a wall thickness and optional drain holes), both built with Minkowski offsets of Manifold. +- **Dependency graph.** Objects can depend on other objects as tools; cycles are refused; a deleted tool turns the operation into a failed one without losing the rest; the worker evaluates the graph in order with a per-prefix cache so editing step *k* recomputes only steps *k…n*. +- **Display of tools and export.** Objects used as tools are drawn as ghosts when visible, are not exported unless selected explicitly, and the outliner shows the relation (tool icon, "used by"). + +## Capabilities + +### New Capabilities +- `editor-op-stack`: stack model, editing, reordering, suppression, rollback, apply and flatten, migration of `40` features, dependency graph and evaluation. +- `editor-booleans`: Union, Subtract, Intersect, Combine, tools and ghosts. +- `editor-body-ops`: Mirror, patterns, Offset, Hollow. + +### Modified Capabilities + +## Impact + +- `src/editor/stack.ts` (types, validation, dependency graph, migration `migrateStack`), `geometry/features/ops/{boolean,mirror,pattern,offset,hollow}.ts`, `geometry/features/evaluate.ts` generalised (prefix cache, graph order), `geometry/model.ts` (`generate('editor')` takes the whole graph), outliner and properties get the stack panel (`features/editor/stack/*`), tool ghosts in `Viewport` (existing tool colour tokens), strings in en and ru, tests. +- Scenes saved by `39`/`40` load unchanged; `source: {kind:'feature'}` is rewritten to an empty source plus stack entries by `migrateScene`. +- Out of scope: plane cut, split, connectors, holes, text on faces (`43`); face and edge based operations, fillet and chamfer of body edges, push/pull (`44`); mesh repair, decimate, smooth (`42`); project files (`45`); parameters with formulas; suppress by group/configuration; feature patterns of a single earlier operation (only whole-body patterns and tool patterns); true timeline UI as a separate bottom bar (the stack panel with a rollback marker covers it). +- Version 1.12.0 (minor: a new capability). +- Depends on: `40-editor-sketch`. diff --git a/openspec/changes/41-editor-op-stack/specs/editor-body-ops/spec.md b/openspec/changes/41-editor-op-stack/specs/editor-body-ops/spec.md new file mode 100644 index 0000000..532f28c --- /dev/null +++ b/openspec/changes/41-editor-op-stack/specs/editor-body-ops/spec.md @@ -0,0 +1,62 @@ +# Spec Delta + +## Purpose +Defines mirror, patterns, offset and hollow as stack operations. + +## ADDED Requirements + +### Requirement: Mirror +The Editor SHALL mirror the body about a plane through the object origin, the bounding-box centre or a given offset, merged into one solid or as separate shells. + +#### Scenario: Merge +- **WHEN** a half of a symmetric part touching the plane is mirrored with merge on +- **THEN** the result is one solid whose volume is twice the half + +#### Scenario: Separate shells +- **WHEN** merge is off +- **THEN** both shells are in the object and no union is computed + +### Requirement: Patterns +The Editor SHALL repeat the body linearly (count, spacing or total length, optionally symmetric about the origin) or circularly (count, axis, full or partial angle), merged or as separate shells. + +#### Scenario: Linear +- **WHEN** a 10 mm cube is repeated 4 times with spacing 20 mm along X +- **THEN** the bounding box is 70 mm long and the volume is 4000 mm³ + +#### Scenario: Circular +- **WHEN** a peg is repeated 6 times over 360° about Z +- **THEN** the pegs are 60° apart + +#### Scenario: Hole array +- **WHEN** a cylinder tool has a linear pattern and is subtracted from a plate +- **THEN** the plate has one hole per copy + +#### Scenario: Too many +- **WHEN** the repeat would exceed the triangle limit +- **THEN** the operation fails with the limit and the reached count + +### Requirement: Offset +The Editor SHALL grow or shrink the body by a distance in millimetres, guaranteeing that a shrink does not leave less than the requested wall and a grow does not exceed the requested size. + +#### Scenario: Grow for clearance +- **WHEN** a 20 mm cube is offset by +0.2 mm +- **THEN** its bounding box is 20.4 mm on each axis + +#### Scenario: Shrink to nothing +- **WHEN** the shrink distance removes the whole body +- **THEN** the operation fails with a message that the distance is too large + +### Requirement: Hollow +The Editor SHALL hollow a body with a wall thickness and optional drain holes with a position, direction and diameter. + +#### Scenario: Hollow cube +- **WHEN** a 30 mm cube is hollowed with a 2 mm wall +- **THEN** the cavity is a 26 mm cube and the volume is 27000 − 17576 mm³ within tolerance + +#### Scenario: Drain hole +- **WHEN** a drain of 3 mm is added on the bottom face +- **THEN** the cavity is open to the outside through that hole + +#### Scenario: Wall too thick +- **WHEN** the wall is larger than half the thinnest part +- **THEN** the operation fails with the largest allowed wall diff --git a/openspec/changes/41-editor-op-stack/specs/editor-booleans/spec.md b/openspec/changes/41-editor-op-stack/specs/editor-booleans/spec.md new file mode 100644 index 0000000..284471d --- /dev/null +++ b/openspec/changes/41-editor-op-stack/specs/editor-booleans/spec.md @@ -0,0 +1,54 @@ +# Spec Delta + +## Purpose +Defines boolean operations between objects, tool objects and the Combine command. + +## ADDED Requirements + +### Requirement: Boolean operations +The Editor SHALL provide Union, Subtract and Intersect of an object with one or more tool objects, evaluated in the target's local space with the tools' own stacks and transforms applied. + +#### Scenario: Subtract +- **WHEN** a cylinder tool is subtracted from a box +- **THEN** the box has a hole where the cylinder was and the volume decreases by the overlap volume + +#### Scenario: Move the tool +- **WHEN** the user moves the tool cylinder afterwards +- **THEN** the hole moves with it + +#### Scenario: Transformed target +- **WHEN** the target box is rotated 30° and the tool is not +- **THEN** the cut stays in the same place in the scene + +#### Scenario: Intersect +- **WHEN** two overlapping boxes are intersected +- **THEN** the result is their common volume + +### Requirement: Closed solids only +Boolean operations SHALL require closed solid operands and SHALL explain what is wrong otherwise. + +#### Scenario: Open mesh +- **WHEN** a tool mesh has open edges +- **THEN** the operation fails with the object name and the number of open edges + +### Requirement: Combine command +The Editor SHALL provide Combine for the selection (Union, Subtract, Intersect) using the active object as the target, as one history step. + +#### Scenario: Combine three +- **WHEN** three objects are selected with the first active and Union is chosen +- **THEN** the first object gets a Union operation with the other two as tools, and they are hidden as tools + +#### Scenario: Release +- **WHEN** the user releases a tool +- **THEN** the operation is removed and the tool object is visible again at its place + +### Requirement: Tool display and export +Tool objects SHALL be drawn as translucent ghosts when visible, marked in the outliner, and left out of exports unless explicitly selected. + +#### Scenario: Export +- **WHEN** the scene is exported with all visible objects +- **THEN** the result bodies are exported and the tool objects are not + +#### Scenario: Ghost +- **WHEN** a tool object is visible +- **THEN** it is translucent and clicking through it selects the body behind it first diff --git a/openspec/changes/41-editor-op-stack/specs/editor-op-stack/spec.md b/openspec/changes/41-editor-op-stack/specs/editor-op-stack/spec.md new file mode 100644 index 0000000..a6fc6ba --- /dev/null +++ b/openspec/changes/41-editor-op-stack/specs/editor-op-stack/spec.md @@ -0,0 +1,79 @@ +# Spec Delta + +## Purpose +Defines the operation stack of an object: editing, ordering, suppression, rollback, apply and flatten, dependencies and recomputation. + +## ADDED Requirements + +### Requirement: Operation stack +Every object SHALL keep an ordered list of operations applied to its source in order, shown in the properties with one row per operation. + +#### Scenario: Add operation +- **WHEN** the user adds Offset 0.2 mm to a box +- **THEN** the object grows by 0.2 mm on every side and a row "Offset 0.2" appears at the end of the stack + +#### Scenario: Edit an earlier operation +- **WHEN** the user changes the distance of the first of three operations +- **THEN** all three are evaluated again in order and the object updates + +#### Scenario: Reorder +- **WHEN** the user drags an operation above another +- **THEN** the order changes in one history step and the object is recomputed; if an operation now fails it is marked and the rest is skipped + +#### Scenario: Suppress +- **WHEN** the user turns the eye of an operation off +- **THEN** the object is evaluated without it and the row is dimmed + +#### Scenario: Delete +- **WHEN** the user deletes an operation +- **THEN** the object is recomputed without it and undo restores it + +### Requirement: Rollback marker +The stack SHALL have a rollback marker that shows the object as it was after a chosen operation, without deleting the later ones. + +#### Scenario: Roll back +- **WHEN** the user moves the marker above the last operation +- **THEN** the view shows the object before that operation, the later rows are dimmed, and exporting uses the shown state with a warning + +### Requirement: Failed operations +A failed operation SHALL be marked with its reason, the object SHALL show the result before it, and operations after it SHALL not be evaluated. + +#### Scenario: Not a solid +- **WHEN** a Boolean uses an imported mesh with holes as its tool +- **THEN** the row is marked failed, the message names the object and says it is not a closed solid, and the target keeps its previous shape + +### Requirement: Dependencies +Operations that use other objects SHALL be evaluated after those objects, cycles SHALL be refused, and deleting a used object SHALL not remove the dependent operations. + +#### Scenario: Cycle +- **WHEN** the user makes A subtract B while B already subtracts A +- **THEN** the operation is refused and the message names the cycle + +#### Scenario: Deleted tool +- **WHEN** a tool object is deleted +- **THEN** the dependent operation is marked failed with "tool missing" and the rest of the scene is unaffected; undo restores both + +### Requirement: Apply and Flatten +The Editor SHALL let the user flatten an object's stack into a single stored mesh, after which it is an ordinary mesh object without operations. + +#### Scenario: Flatten +- **WHEN** the user flattens a body with three operations +- **THEN** the visible shape and its volume are unchanged, the stack is empty, the source is a mesh, and undo restores the stack + +### Requirement: Recompute performance +Editing operation *k* SHALL re-evaluate only operations *k* to the last, reusing the result of operation *k−1*. + +#### Scenario: Cached prefix +- **WHEN** the user edits the last of five operations +- **THEN** only that operation is computed again + +### Requirement: Migration of stored sketch features +Scenes stored by the sketch change SHALL load with their Extrude and Revolve features as operations of the stack, and unknown operation kinds SHALL be kept and shown as unknown. + +#### Scenario: Old scene +- **WHEN** a scene with a sketch-built plate and a cut hole is loaded +- **THEN** the plate looks the same and the stack lists the two features + +#### Scenario: Unknown kind +- **WHEN** a stored operation has an unknown kind +- **THEN** it is shown disabled as "unknown operation" and saved back unchanged diff --git a/openspec/changes/41-editor-op-stack/tasks.md b/openspec/changes/41-editor-op-stack/tasks.md new file mode 100644 index 0000000..5fee08b --- /dev/null +++ b/openspec/changes/41-editor-op-stack/tasks.md @@ -0,0 +1,36 @@ +# Tasks + +## 1. Spikes + +- [ ] 1.1 Minkowski: check `minkowskiSum`/`minkowskiDifference` in the installed `manifold-3d`; time and memory on a cube, a rounded bracket and a 200k-triangle STL with spheres of 16/24/48 segments; compare the circumscribed/inscribed guarantee against analytic values. Record limits (max triangles, sphere segments, fallback message) in `design.md` Decision 10. If Offset/Hollow cannot be done within reasonable limits, stop for `/opsx:update`. +- [ ] 1.2 Cache: prototype the per-prefix LRU in a worker test (live-manifold counting, `delete()` on eviction), measure a 5-op stack edit; record in `design.md`. + +## 2. Stack model (React-free, `src/editor/`) + +- [ ] 2.1 `stack.ts`: `Op` types and params normalisation, `emptyStack`, add/remove/move/enable/rollback reducers, op summaries as keys. Test: reducers, normalisation of corrupt params, rollback bounds. +- [ ] 2.2 Graph: `buildGraph`, topological order, cycle detection with the cycle path, dependants of an object. Test: chains, diamonds, cycles named, deleted tool detection. +- [ ] 2.3 `migrateStack` in `migrateScene`: `feature` sources of `40` → empty source + ops, unknown kinds preserved disabled, idempotent. Test with stored scenes from `40` fixtures and unknown kinds. + +## 3. Geometry operations (`geometry/features/ops/`) + +- [ ] 3.1 `boolean.ts` (union/subtract/intersect, closed-solid guard with health info, tool placed by relative matrix) and Combine reducer. Test with `expectValidSolid`: volumes of box − cylinder, intersect, rotated target, open tool refused with counts. +- [ ] 3.2 `mirror.ts` and `pattern.ts` (linear with spacing/total/symmetric, circular with angle, merge or disjoint, triangle limit). Test: half-to-full volume, 4×20 mm bbox and volume, 6 pegs 60° apart, hole array volume, limit error. +- [ ] 3.3 `offset.ts` and `hollow.ts` per the spike (circumscribed/inscribed sphere, drains). Test: grown cube bbox, shrunk cube, shrink to empty error, hollow cube volume and drain connectivity (genus), wall too thick message value. +- [ ] 3.4 `evaluate.ts` generalised: fold over ops, rollback, failed ops with reason, tool evaluation in the target frame, prefix cache with LRU and `delete()`. Test: cache hits on repeated evaluation, edit of op *k* recomputes only *k…n* (counting calls), eviction frees manifolds, failed op keeps previous solid, missing tool. +- [ ] 3.5 `generate('editor')` takes the whole graph (objects, matrices, stacks), evaluates visible objects and tools, returns meshes per object plus per-op status (`ok`/`failed`+reason) in `ModelResult`. Test: result statuses, tools not returned as parts unless visible, stale cache after an edit. + +## 4. UI (no artboard exists: built from `ui/` components and tokens) + +- [ ] 4.1 Stack section in Properties: rows with drag handle, eye, summary, failed mark, rollback marker, Add menu, per-op forms (`LengthField` / `NumberField`, axis and plane selectors), Flatten with confirmation. Component tests for add, edit, reorder (keyboard and pointer), suppress, delete, rollback, flatten; `stepperAssert` passes. +- [ ] 4.2 Combine command (menu and context menu) and Release tool; outliner chips (ops count, tool of); ghost drawing of visible tools in `Viewport` with the existing tool colour tokens; export skips tool objects unless selected. Tests: Combine builds the op, release restores, export set, outliner chips. +- [ ] 4.3 Messages for `stack.error.*` (notSolid, cycle, toolMissing, wallTooThick, tooMany, offsetEmpty) as keys with parameters; log entries per failed op. Test: key parity en/ru and message parameters. +- [ ] 4.4 History: all stack commands through `dispatch` with merge for typing and one step per reorder. Test: undo/redo each command, merge, redo cleared. + +## 5. Version + +- [ ] 5.1 Bump `package.json` to 1.12.0 and add `1.12.0 — 41-editor-op-stack` to `CHANGELOG.md`. Test: `site.test.ts`. + +## 6. Verification + +- [ ] 6.1 `npm test` and `npm run build` are green, the dev server starts, no console errors; scenes of `39` and `40` load and look the same. +- [ ] 6.2 In the browser, en and ru, desktop and 390 px: subtract a patterned cylinder from a plate, move the tool and watch the holes move, mirror a half bracket, hollow an imported STL with drains, grow a part by 0.2 mm, reorder and suppress operations, roll back, flatten and undo, force a cycle and an open-mesh tool and read the messages; export and check the bbox/volume against the numbers.