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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# kernelCAD v0.14.0
# kernelCAD v0.15.0

## Unreleased

- **NURBS Slice F curved-patch trim (#528).** `Surface.trimTo(by)` now sections the base/cutter surfaces and imprints the section edge with `BRepFeat_SplitShape` from the `replicad-opencascadejs` `kcad-v0.24.0` wasm build. Clean curved NURBS/Coons patches no longer fail with the Slice-E planar-only `feature.surface-trim.non-planar` guard; missed cutters still emit `feature.surface-trim.no-intersection`.
- **`Surface.split(by)` now returns both halves.** The public return type is `[Surface, Surface]`, ordered by descending face area. The old one-piece larger-half behavior and `feature.surface-trim.split-deferred` warning are legacy only.
- **`render_preview` MCP tool (#440).** First-class inline visual feedback: `{ code | file }` → deterministic PNG views on disk, **no studio / dev-server precondition**. Renders the canonical engineering views (`front`/`right`/`top`/`iso`, subset via `views`) plus an optional `pose: '<az>,<el>'`, honors `focus`/`hide` part isolation, and returns absolute image paths with per-view camera descriptions (`{ ok, images, out_dir, bounds, mechanism, render_source, render_ms, diagnostics }`). Pixels come from the same headless pipeline as `kernelcad render`; what's new is provisioning — a prebuilt static demo-player bundle (`npm run build:player`, shipped in the npm package at `dist/headless-player/`) is served from an ephemeral local port automatically, with a running studio dev server honored as fallback and `base_url` as an explicit override. Mechanism truth runs with full `kernelcad render` parity: broken mechanisms render watermarked MECHANISM BROKEN (refused under `KERNELCAD_RENDER_STRICT=1`). `no_mechanism_check: true` skips the (potentially expensive) probe for fast iteration and reports `mechanism: 'unverified'`; ignored under strict mode. Paths are local to the MCP server machine — hosted/remote clients should use `open_in_studio` instead.

## v0.14.0 — 2026-06-24 — NURBS Slice E: surface finishing
Expand Down
7 changes: 4 additions & 3 deletions docs/demos/v0.14/enclosure-half/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ now honor rational `weights` for exact conics (E1); `Surface.trimTo(by)` and
patches into a watertight solid via `BRepBuilderAPI_Sewing` +
`BRepBuilderAPI_MakeSolid` (E3); and `Shape.draft()` tapers analytic faces
via `BRepOffsetAPI_DraftAngle` (E4). `add_surface` gains kinds `trim`, `sew`,
and `draft`. Six new diagnostic codes cover the honest limitations: curved-patch
trim refused, split-into-both-halves deferred, named neutral-plane not yet
resolved. Full notes in `CHANGELOG.md`.
and `draft`. In v0.15, curved-patch trim moves to `BRepFeat_SplitShape` and
`Surface.split(by)` returns both halves as `[Surface, Surface]`; v0.14 demos
may still mention the earlier planar-only limitation. Full notes in
`CHANGELOG.md`.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "kernelcad",
"mcpName": "com.kernelcad/kernelcad",
"private": false,
"version": "0.14.0",
"version": "0.15.0",
"license": "MIT",
"author": "Andrii Shylenko <andrii@shylenko.com>",
"type": "module",
Expand Down Expand Up @@ -121,7 +121,7 @@
"react-dom": "^19.2.0",
"react-markdown": "^10.1.0",
"replicad": "^0.23.1",
"replicad-opencascadejs": "github:w1ne/replicad-opencascadejs#kcad-v0.23.2",
"replicad-opencascadejs": "github:w1ne/replicad-opencascadejs#kcad-v0.24.0",
"sharp": "^0.33.5",
"shiki": "^1.29.2",
"three": "^0.184.0",
Expand All @@ -132,7 +132,7 @@
"zod": "^4.3.6"
},
"overrides": {
"replicad-opencascadejs": "github:w1ne/replicad-opencascadejs#kcad-v0.23.2"
"replicad-opencascadejs": "github:w1ne/replicad-opencascadejs#kcad-v0.24.0"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20260504.1",
Expand Down
8 changes: 4 additions & 4 deletions src/agent/mcp/edits/surfaceTrim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// src/agent/mcp/edits/surfaceTrim.ts
//
// Insert a `const <binding> = <surface>.trimTo(<by>)` or
// `const <binding> = <surface>.split(<by>)` statement into a .kcad.ts script
// immediately before the last top-level return.
// `const <binding> = <surface>.split(<by>)` statement into a .kcad.ts script.
// For split, the binding is a `[Surface, Surface]` tuple.

import { addFeature } from './addFeature';
import type { AddFeatureResult } from './addFeature';
Expand All @@ -15,11 +15,11 @@ export interface SurfaceTrimInput {
code: string;
/** JS variable name of the surface to trim/split (must be declared in source). */
surface_binding: string;
/** JS variable name of the cutter (Surface, Shape, or Curve3D; must be declared in source). */
/** JS variable name of the cutter Surface (must be declared in source). */
by_binding: string;
/** Which op — 'trim' discards the smaller half; 'split' keeps both halves. */
op: 'trim' | 'split';
/** JS const name for the resulting Surface binding. Auto-derived if omitted. */
/** JS const name for the resulting Surface or `[Surface, Surface]` binding. Auto-derived if omitted. */
binding_name?: string;
}

Expand Down
4 changes: 2 additions & 2 deletions src/agent/mcp/toolRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ export const TOOL_REGISTRY: ToolRegistryEntry[] = [
'Use this when you need an organic, freeform, or swept shape — a body shell, panel, fairing, ergonomic curve, lens, or sculpted form — authored as a NURBS Surface into the user\'s .kcad.ts, OR when you need to finish surfaces into a watertight solid or taper faces for moldability. One authoring/finishing path, selected by `kind`:\n' +
"- 'nurbs' — insert a nurbsSurface(...) / surfaceFromCurves(...) call. Pass either { controls, degree, weights?, knots?, periodic? } for direct construction, OR { section_sketch_ids } for skinning. Weights are honored: supply rational weights to build exact circles/cylinders/spheres/conics (the surface becomes rational); omit weights for a non-rational surface.\n" +
"- 'boundary' — insert a surfaceFromBoundary([c1,c2,c3,c4], opts?) call: one NURBS face through 4 boundary Curve3D refs (bottom, right, top, left in loop order; adjacent endpoints must coincide within 1e-6 mm) via OCCT BRepOffsetAPI_MakeFilling.\n" +
"- 'trim' — insert a `<surface>.trimTo(<by>)` or `<surface>.split(<by>)` call. Pass `surface_binding` (the Surface variable name), `by_binding` (the cutter Surface variable name; Shape/Curve3D cutters are deferred to a later slice), and `op: 'trim'` (discard smaller half) or `op: 'split'` (returns larger piece; full split-into-N deferred, emits `feature.surface-trim.split-deferred`).\n" +
"- 'trim' — insert a `<surface>.trimTo(<by>)` or `<surface>.split(<by>)` call. Pass `surface_binding` (the Surface variable name), `by_binding` (the cutter Surface variable name; Shape/Curve3D cutters are deferred to a later slice), and `op: 'trim'` (keep the largest imprinted piece) or `op: 'split'` (return both halves as a `[Surface, Surface]` tuple).\n" +
"- 'sew' — insert a `sew([s0, s1, ...], opts?)` call to stitch N surfaces into a closed watertight solid via OCCT BRepBuilderAPI_Sewing. Pass `surface_bindings` (array of Surface variable names). Use after trim/boundary to close patches into a solid: trim → sew → solid pipeline. Optional `tolerance` (mm, default 1e-6) and `require_closed` (emits feature.surface-sew.open-shell if result is not watertight).\n" +
"- 'draft' — insert a `<shape>.draft(angleDeg, { face, neutralPlane?, pullDir? })` call to taper the selected face(s) for mold release. Pass `shape_binding`, `angle_deg` (0–90), and `face` (canonical name, label, or FaceQuery descriptor). Lowering emits feature.draft.failed on invalid geometry.\n" +
'The returned Surface produces no Shape until you chain .thicken(t) or .toShape() (do that via add_feature on the binding name). Returns the modified code + diagnostics. Each kind fails closed on its own missing required params.',
Expand Down Expand Up @@ -381,7 +381,7 @@ export const TOOL_REGISTRY: ToolRegistryEntry[] = [
},
by_binding: {
type: 'string',
description: "kind:'trim' — JS variable name of the cutter (another Surface, a solid Shape, or a Curve3D; must be declared in source).",
description: "kind:'trim' — JS variable name of the cutter Surface (must be declared in source). Shape/Curve3D cutters are deferred.",
},
op: {
type: 'string',
Expand Down
6 changes: 3 additions & 3 deletions src/agent/mcp/tools/listApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,12 +201,12 @@ export const SURFACE_METHODS: ApiEntry[] = [
{
name: 'trimTo',
signature: '(by: Surface) => Surface',
description: 'Trim this surface at its intersection with `by` (a Surface cutter) and return a new Surface representing the kept half. No geometry is computed at capture time — the lowerer runs BRepAlgoAPI_Section and discards the unwanted half. Emits `feature.surface-trim.no-intersection` when the cutter produces no section curve. Shape/Curve3D cutters are deferred to a later slice.',
description: 'Trim this surface at its intersection with `by` (a Surface cutter) and return a new Surface representing the kept half. No geometry is computed at capture time — the lowerer runs BRepAlgoAPI_Section, imprints the section curve with BRepFeat_SplitShape, and keeps the largest resulting face. Emits `feature.surface-trim.no-intersection` when the cutter produces no section curve. Shape/Curve3D cutters are deferred to a later slice.',
},
{
name: 'split',
signature: '(by: Surface) => Surface',
description: 'Split this surface at its intersection with `by` (a Surface cutter) and return the larger surviving piece. Full split-into-both-halves is deferred; the lowerer emits `feature.surface-trim.split-deferred` always. Emits `feature.surface-trim.no-intersection` when the cutter produces no section curve. Shape/Curve3D cutters are deferred to a later slice.',
signature: '(by: Surface) => [Surface, Surface]',
description: 'Split this surface at its intersection with `by` (a Surface cutter) and return both resulting Surface halves as `[first, second]`, ordered by descending face area. The lowerer uses BRepFeat_SplitShape, so curved base/cutter patches are supported for clean intersections. Emits `feature.surface-trim.no-intersection` when the cutter produces no section curve. Shape/Curve3D cutters are deferred to a later slice.',
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,9 @@ that together enclose a volume (front panel + back panel, top cap + side wall,
etc.), you must close them into a watertight solid before exporting or
boolean-ing:

1. **trimTo shared edges** — call `.trimTo(sharedCurve)` on each patch so
adjacent edges are cut to the same boundary, eliminating seam gaps.
1. **trimTo shared cutter surfaces** — call `.trimTo(cutterSurface)` on each
patch so adjacent edges are cut to the same imprinted boundary, eliminating
seam gaps. Use `.split(cutterSurface)` when both sides of a patch are needed.
2. **sew** — call `sew([...trimmedSurfaces], { requireClosed: true })` to fuse
the coincident edges into one closed shell. `requireClosed: true` throws
`feature.surface-sew.open-shell` immediately if the result is still open,
Expand All @@ -220,7 +221,7 @@ boolean-ing:
Do NOT call `.thicken()` on each patch individually and then union the
resulting solids — the seam discontinuity at the join creates visible shading
artifacts and the export mesher will sometimes fail the watertight gate. Use
`trimTo` + `sew` so the surfaces share topological edges.
`trimTo` / `split` + `sew` so the surfaces share topological edges.

## When NOT to apply a rule

Expand Down
2 changes: 1 addition & 1 deletion src/agent/skills/kernelcad-mcp/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ All edit tools return the modified source plus diagnostics. Re-run `kernelcad ev
- `add_surface({ kind, code, ... })` — author a NURBS Surface; returns modified code + diagnostics. Chain `.thicken(t)` / `.toShape()` via the existing `add_feature` tool on the returned binding name. Select the path with `kind`:
- `add_surface({ kind: 'nurbs', code, controls?, degree?, weights?, knots?, periodic?, section_sketch_ids?, binding_name? })` — insert a `nurbsSurface(...)` or `surfaceFromCurves(...)` call.
- `add_surface({ kind: 'boundary', code, curve_bindings, continuity?, sampling?, binding_name? })` — insert a `surfaceFromBoundary([c1, c2, c3, c4], opts?)` declaration: one NURBS filling face through 4 boundary Curve3D refs. `curve_bindings` must be a tuple of 4 Curve3D variable names declared earlier in exact loop order: `[0]` bottom, `[1]` right, `[2]` top, `[3]` left; each is regex-validated. `continuity` accepts a single grade (`'C0' | 'C1' | 'C2'`) applied to all 4 edges or a length-4 per-edge array; defaults to `'C0'`.
- `add_surface({ kind: 'trim', code, surface_binding, by_binding, binding_name? })` — insert a `<surface>.trimTo(<by>)` call. `surface_binding` must be an existing `Surface` variable; `by_binding` may be a `Surface`, a solid `Shape`, or a `Curve3D`. The kept half is the portion of the surface that lies on the same side of the cutter as the origin (OCCT convention). Use to align coincident patch edges before calling `sew`. Emits `feature.surface-trim.no-intersection` if the cutter does not intersect the surface.
- `add_surface({ kind: 'trim', code, surface_binding, by_binding, op?, binding_name? })` — insert a `<surface>.trimTo(<by>)` call (`op: 'trim'`) or `<surface>.split(<by>)` tuple (`op: 'split'`). `surface_binding` and `by_binding` must be existing `Surface` variables. `trimTo` keeps the largest imprinted piece; `split` returns both halves as `[Surface, Surface]`. Use to align coincident patch edges before calling `sew`. Emits `feature.surface-trim.no-intersection` if the cutter does not intersect the surface.
- `add_surface({ kind: 'sew', code, surface_bindings, tolerance?, require_closed?, binding_name? })` — insert a `sew([...], opts?)` top-level call. `surface_bindings` is an array of `Surface` variable names (from `nurbsSurface`, `surfaceFromBoundary`, or prior `.trimTo()` calls); edges within `tolerance` mm (default 1e-6) are merged. Set `require_closed: true` when authoring a closed solid — the lowerer emits `feature.surface-sew.open-shell` immediately if the result is still open, catching seam gaps early. The result is a `Shape` flowing into booleans and export.
- `add_surface({ kind: 'draft', code, target_binding, angle_deg, face, neutral_plane?, pull_dir?, binding_name? })` — insert a `<shape>.draft(angleDeg, opts)` chained call. `target_binding` must be an existing `Shape` variable. `face` accepts a canonical name, label, or FaceQuery (same selector as `shell()`). `neutral_plane` sets the parting-line face (defaults to `face`); `pull_dir` is the demoulding direction `[x, y, z]` (defaults to face normal at lower time). Use after `sew` or on any Shape whose faces must release from a mold. Emits `feature.draft.failed` if OCCT cannot draft the selected faces at the requested angle.
- `add_curve({ kind, code, ... })` — author a 3D Curve3D before the last top-level return; consumable by `variableSweep` as a spine and by `surfaceFromBoundary` as a boundary edge. Select the path with `kind`:
Expand Down
11 changes: 6 additions & 5 deletions src/agent/skills/kernelcad-nurbs/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const panel = surfaceFromCurves([s0, s1]).thicken(2);
|---|---|---|
| `.thicken(t)` | `Shape` (closed solid) | Offsets both sides by `t` mm via `BRepOffsetAPI_MakeThickSolid.MakeThickSolidBySimple`. `t` accepts `Editable<number>`. |
| `.toShape()` | `Shape` (zero-volume shell) | Single-face Shape; use as profile placeholder for future face-aware features. |
| `.trimTo(by)` | `Surface` | Trim this surface at its intersection with `by` (a `Surface` cutter) and return the kept half. No geometry computed at capture time — the lowerer runs BRepAlgoAPI_Section. Use before `sew` to align adjacent patch edges. Emits `feature.surface-trim.no-intersection` when the cutter misses. Shape/Curve3D cutters are deferred to a later slice. |
| `.split(by)` | `Surface` | Like `.trimTo()` but for splitting; this slice returns the larger piece and emits a `feature.surface-trim.split-deferred` warning (full split-into-N is deferred to a later slice). The cutter must be a `Surface`; Shape/Curve3D cutters are deferred. Emits `feature.surface-trim.no-intersection` when the cutter misses. |
| `.trimTo(by)` | `Surface` | Trim this surface at its intersection with `by` (a `Surface` cutter) and return the kept half. No geometry computed at capture time — the lowerer runs `BRepAlgoAPI_Section` and imprints the section curve with `BRepFeat_SplitShape`. Use before `sew` to align adjacent patch edges. Emits `feature.surface-trim.no-intersection` when the cutter misses. Shape/Curve3D cutters are deferred. |
| `.split(by)` | `[Surface, Surface]` | Split this surface at its intersection with `by` (a `Surface` cutter) and return both resulting halves, ordered by descending area. The cutter must be a `Surface`; Shape/Curve3D cutters are deferred. Emits `feature.surface-trim.no-intersection` when the cutter misses. |

Top-level finishing ops that consume `Surface` instances:

Expand Down Expand Up @@ -351,9 +351,10 @@ waypoints never converge. Derive the curves from a reference photo:
`weights` builds an exact rational surface — use them for exact circles,
cylinders, spheres, and conics rather than approximating with control points.
(3D `nurbsCurve` weights are still non-rational; that lane is deferred.)
- **Trim and sew freeform NURBS surfaces into a watertight solid** (Slice E).
`.trimTo(cutter)` aligns a patch to a shared boundary (planar patches only —
curved patches are refused with `feature.surface-trim.non-planar`), then
- **Trim and sew freeform NURBS surfaces into a watertight solid** (Slice E/F).
`.trimTo(cutter)` aligns a patch to a shared boundary by imprinting the
section curve; clean curved NURBS/Coons patches are supported. `.split(cutter)`
returns both halves as `[Surface, Surface]` when you need both sides. Then
`sew([...], { requireClosed: true })` fuses coincident-edged patches into a
closed solid, then optionally `.draft(angle, { face })` tapers a face for mold
release (analytic faces only; OCCT refuses to draft spline faces, emitting
Expand Down
16 changes: 1 addition & 15 deletions src/modeling/backends/occt/occtLowerer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -609,22 +609,8 @@ export class OcctLowerer implements FeatureLowerer {
const cutterFace = this.resolveTrimCutter(trimData, r, inputs, diagnostics, allRecords);
if (!cutterFace) return undefined;

// split-into-N is deferred: the lowerer returns only the larger piece
// (identical to trim). Surface that honestly as a warning rather than
// silently masquerading the larger half as the promised compound.
if (trimData.op === 'split') {
diagnostics.push({
target: this.target,
code: 'feature.surface-trim.split-deferred',
featureId: r.id,
severity: 'warn',
message: `surfaceTrim ${sid}: split currently returns only the larger piece; full split-into-both-halves is deferred to a later slice.`,
hint: HINT_TEMPLATES['feature.surface-trim.split-deferred'].template,
});
}

try {
const { face } = lowerSurfaceTrim(baseBuilt.face, cutterFace, trimData.op);
const { face } = lowerSurfaceTrim(baseBuilt.face, cutterFace, trimData.op, trimData.piece);
surface = { kind: 'face', face };
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
Expand Down
Loading
Loading