Commit Graph
16 Commits
Author SHA1 Message Date
Irakli KourtisandClaude Opus 5.5 201e9d5f9f Patch fills non-planar edge loops with one smooth face (#250) (#472)
* Add a one-surface fill of closed 3D loops to geom

A closed loop of curves that does not lie in a plane has no exact surface;
the usual answer is one smooth face fitted over it. fill_loop does that:

- the loop's best-fit plane (Newell normal), with a frame chosen from the
  loop itself so the result does not depend on where the loop starts, its
  direction, or where the part sits
- the loop seen along that normal must be simple and must not fold or run
  along the normal; otherwise it is refused ("not supported yet"), never
  filled with wrong geometry
- one non-rational bicubic B-spline height field over the loop's box,
  fitted by least squares to the loop's samples (The NURBS Book 9.4.1,
  9.4.3, eq. 9.62/9.65) with a thin-plate smoothness term
  lambda*A*int(h_uu^2 + 2 h_uv^2 + h_vv^2), A one grid cell's area, so the
  fit is the same at any scale; only planes are reproduced exactly
- span counts per direction (long narrow loops fit), refined until the
  loop is within tolerance, else refused with the deviation reached

bspline: bisected span search, basis functions on a stack array, one
clamping helper shared by curves and surfaces, one binomial helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Patch fills non-planar loops of edges with one smooth face (#250)

surface.patch with edges refused any loop that is not planar ("not
supported yet: patching a loop that is not planar"). It now fills it:

- planar loops still get one exact planar face, but planarity is certified
  conservatively: lines by their ends, B-spline/NURBS edges by their control
  points, intersection curves by their samples lying on a support plane
  within tolerance; a loop that can't be certified is filled instead
- other loops get one face from geom's fill_loop, bounded by the body's own
  chained edges, whole (no split): its history is one generated face whose
  edges are the body's, and it stitches back into a closed solid
- intersection-curve edges are sampled by a seeded, guarded double
  projection shared by the planarity test and the fill (plane_fit)
- the face tolerance is 10x the planarity tolerance (documented: later
  operations take the face to contain its edges within it)
- edge picking runs inside the guard; the geom->truck conversion is shared
  with the T-spline sheets (spline_conv); the Newell plane fit is shared with
  STEP import

Tests: 3- to 8-sided, concave (dart, arrow, plus), flat-corner, two-edge and
imported rims fill as one face and stitch closed (volumes against an
independent integral); finite mesh normals, same result for any pick order
or placement; non-height-field loops and too-fine detail are refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Test the Patch dialog filling a non-planar loop of picked edges

The Patch dialog's only test used a sketch profile. Picking the four
curved rim edges of a box cut by a sideways cylinder now makes one
surface.patch command whose result is a single smooth face over the rim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:37:01 -04:00
bherbruckandClaude Opus 5.5 296683229d Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)
* WIP deep rename (not for push)

* WIP2

* WIP3

* WIP4 wxs fix

* rustfmt

* Rename the remaining identifiers in merged crash-report code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:17:01 -04:00
Iraklis KourtisandClaude Opus 5.5 a04ef4ca59 T-spline core library and sheet bodies (#250, step 1) (#337)
* Add B-spline basis, knot insertion and NURBS surfaces to geom

Freeform faces need local refinement and gap-free joins between patches
with different knot vectors, which tensor B-spline grids cannot give;
T-splines can, and the first user is an N-sided Boundary Fill of
non-planar loops. This lays the univariate groundwork they rest on, in
the L0 geometry crate so it stays independent of the B-rep kernel:

- Cox-de Boor basis of one function from its local knot vector, on
  half-open spans with a left-limit side for the domain end, with
  derivatives (NURBS Book eq. 2.5, 2.9)
- knot insertion into a single basis function (Sederberg et al. 2004
  section 3) and Bezier extraction of one function by repeated insertion
- Boehm knot insertion for curves (NURBS Book A5.1), de Casteljau
  evaluation and sub-segments
- a rational tensor NURBS surface with evaluation, knot insertion and
  clamping, and a rational Bezier patch type

All input is checked (finite, monotone knots, positive weights, size
caps) and returns SplineError; nothing indexes unchecked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Add T-spline surfaces: T-mesh, evaluation, AS check, refinement, extraction

Tensor B-spline faces refine only by whole rows and columns and cannot
join patches with different knot vectors without gaps. T-splines allow
T-junctions in the control net, which fixes both; the first user is an
N-sided Boundary Fill. This adds the maths only (no command, no change
in behaviour):

- T-mesh in index space with knot values per line, validated (outer
  rectangle, rectangular faces, no empty lines, size caps); local knot
  vectors read off rays from each vertex, rays leaving the mesh repeat
  the boundary (Sederberg et al. 2003 section 3)
- rational T-spline evaluation and first derivatives for odd bi-degree,
  with a complete (p-1)/2-line frame and the domain inside it
- T-junction extensions and the analysis-suitability check (Scott et al.
  2012 section 2)
- local refinement that keeps the surface exactly: requested knot lines
  are cut in, then every original blending function is carried as a
  record and split (violation 1) or the mesh gains edges (violations 2
  and 3) until each record is a blending function of the refined mesh
  (Sederberg et al. 2004 section 3); analysis-suitable input stays
  analysis-suitable by carrying conflicting T-junctions across faces
- NURBS <-> T-spline conversion for meshes without T-junctions, and
  Bezier extraction over the rectangles no blending-function knot line
  crosses

Measured: regular T-spline vs NURBS < 1e-12, round trip exact, partition
of unity with T-junctions < 1e-14, refinement deviation <= 1.4e-14 over
40 random refinements, Bezier patches vs surface < 1e-14.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

* Convert T-splines to sewn NURBS sheet bodies in the kernel

A T-spline is only useful to the modeller once it is a B-rep face set.
tspline_sheet builds an open shell: a mesh without T-junctions becomes
one clamped NURBS face; otherwise each Bezier element is a face, and
sides are split at the T-points where neighbours' corners land, so all
faces share vertices and edges and the sheet is one oriented shell
(V - E + F = 1). Every truck call runs under guard. No command yet;
Boundary Fill will be the first caller.

Tests: one face for a regular surface, sewn Bezier faces with
T-junctions, tessellated points on the surface to ~1e-9 mm, and hostile
control nets (coincident, 1e300, needle-thin) never panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:46:22 -04:00
Iraklis KourtisandClaude Opus 5.5 08d797235d feat(kernel): history across coalesced stepped walls (#136)
A cut of stepped prisms sews a wall over one segment through successive
slabs as one face, and the generated planar walls are then joined on
their common supports. Until now such a piece carried no history.

- prism_lineage: a sewn wall records what it is over in each slab it
  spans (Spec::Wall { levels, slabs }), so a coalesced wall comes from
  the operand walls under it in every slab
- the sewn piece's history is attached before the joining and carried
  through it (Built::Traced); planar_regularize composes the history of
  the body it joins with the join itself
- a composed face is Split only where record::mark_shared_split finds an
  origin under several faces; an inner Split alone no longer makes one
- Body::same ignores history: an operation that leaves a body as it is
  gives the same body, carrying that operation's history

The two boolean history tests that waited for this run again, and the
coalesced and regularized step cuts are covered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 08d7037176 feat(kernel): modifiers report exact face and edge history
Fillets (local, loop, rolling ball, corner, variable, chord), chamfers
(cutters, local corners, loops, two sides), shell, draft, offset faces,
split face/body, delete face, patch/copy/stitch/trim/extend and
transforms record their history over [body] while they build: faces
rebuilt from the body's, blends in place of edges (corner patches in
place of the edges at the corner), shell walls as offsets, edges as
pieces of the body's or where two faces meet. Multi-step operations
compose each step. The push-face offset fallback now has history, and
the bore union composes its narrowed tool.

A reflection reorders a body's edges, so transforms now match edges by
identity (mirrored copies reported the wrong edges).

Refs #136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 d69ebbee1b refactor(kernel): number profile loops once and record face history as faces are built
One numbered loop type (crate::numbered) carries each segment's profile
numbers through orientation, snapping, circle re-seaming, re-sampling and
rotation; every builder orients its regions through it once. Builders that
make their own side faces pair each face with its provenance as it is made
(ruled walls by the wire edges each face holds, smooth lofts, coils and path
sweeps by segment), and cap_and_close appends the caps itself. Column
following is kept only for faces truck sweeps internally (rsweep, tsweep
along paths, cones, where an apex vertex stays put). Extrude derives face
sources and history in one topology pass. One attach entry point takes
operands with their edges already indexed.

Geometry, face order, face sources and recorded origins are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 8c81bc60d0 feat(kernel): creation builders report exact face history
Extrude, revolve (partial and full turn), tapered extrude, loft (incl. to a
point), sweep, path sweep, coil, primitives, lip, emboss and thicken attach
the exact history of the bodies they build: each face's profile segment, cap
or operand face, and each edge's operand edge, recorded by topology identity
inside the building call. Nothing is sampled; a body whose faces do not follow
the expected construction carries no history. Face sources are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
Iraklis KourtisandClaude Opus 5.5 c87efa1f35 feat(kernel): exact face and edge history model
Bodies can now carry the exact history of the operation that made them:
for each result face its origins among the operation's operands (kept,
modified, split, merged, or generated from a profile segment, cap, edge or
offset) and for each edge the faces it separates and the operand edge or
face pair it came from. History is recorded by the operation, never
sampled, and checked against the body when attached.

- history.rs: the vocabulary (moved from provenance.rs), History::check,
  identity, substitute (composes an operation with the one that made an
  operand, including bodies made from a sketch) and face-source propagation.
- One edge order for a body (body::edge_order), shared by tessellation and
  history, so history edge indices are the mesh's edge indices.
- Transforms report an identity history; placed_topology documents the
  face/edge order guarantee it relies on.
- provenance.rs keeps the geometric trace for operations that do not yet
  report history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
2026-10-12 11:52:53 -04:00
bherbruck c53aff7592 Fix artificial Shell wall splits on stepped bodies 2026-10-11 22:32:53 -04:00
bherbruck c1d819f235 feat: preserve body history and placed modelling workflows
Record ownership transfers in the timeline so earlier Join/Cut membership,
world placement, machining data and styles survive component moves, edits,
independent copying, undo and reopening. Preserve pending positions in history
snapshots and keep constructor transfers ordered during feature redefinition.

Retain shallow sheet pockets and exact planar intersection islands through
unfold/refold, with depth-aware flat previews and DXF export. Store explicit
3D wire coordinate spaces and migrate known legacy provenance without moving
saved points or analytic fits.

Infer unique eligible cylindrical Emboss targets, align Revolve manipulators
with actual extent geometry, and discard obsolete preview errors after input
correction. Certify constructed and immutable imported face styles using exact
source metadata while preserving legacy face names. Upgrade designs to format10
with bounded validation and geometry-preserving legacy migration.
2026-10-11 03:59:12 -04:00
bherbruck 4b7e1d317d fix: load designs off-thread and cache recent thumbnails as PNGs 2026-10-10 20:55:14 -04:00
bherbruckandClaude Opus 5.5 4c8f080691 Faces split on purpose stay split: the body remembers its Split Face lines, export and healing keep the pieces apart
Split Face records the new edges as polylines on the body (Body::split_lines); the document carries them through later features. STEP export no longer joins pieces of one surface across such a line, and healing (fillets, chamfers, booleans, shell, draft) no longer merges coplanar pieces there, while incidental pieces (boolean leftovers, seam halves) still merge as before. Tests: plane and cylinder splits keep their face count through a STEP round trip (and join without the record), a second split keeps both lines, and an engine test exports a split box after a fillet with 8 faces.

Fixes #11

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT
2026-10-09 21:31:16 +00:00
bherbruckandClaude Opus 5.5 c0e3e3d216 Variable-radius and chord-length fillets
New kernel file blend_variable.rs. The hooks into blend.rs are visibility
only: plane_normal, vtx, rebuild_face and unsupported are now pub(crate).
- fillet_variable(body, edge, start, r0, r1) works on the same edge
  configuration as the constant fillet (straight edge, planar faces, end
  faces square to it). The radius runs linearly from the end nearest
  `start`, and every section square to the edge is a circular arc. The
  blend is an exact degree (2, 1) rational B-spline: arc control points
  move linearly.
- fillet_chord(body, edges, chord): the radius per edge comes from its
  face angle (chord = 2 r cos(phi/2)).

Fillet feature: style constant | chord | variable {radius2, start};
FusionFilletEdgesCommand takes type, radius2 and start.

Tests:
- Variable 2 -> 5 on a box edge removes (1 - pi/4) L (r0^2 + r0 r1 + r1^2)/3.
  Starting from the other end gives the same solid; equal radii give the
  constant fillet; too large is refused.
- Chord 4 on a right angle equals radius 4/sqrt(2).
- Engine: both types by command.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 21:30:58 +00:00
bherbruckandClaude Opus 5.5 7027382667 split_faces: exact sketch segments, a face map, and misses named
For solvecraft-params' Split Face feature:
- Sketch tools are chains of Seg2. A single line splits by its plane and a
  single arc by its cylinder, exactly (a circular edge on a planar face);
  longer chains and splines follow the curve.
- split_faces_with_map gives the source face of each face of the result.
- A listed face the tool misses is an error naming it (split_any keeps
  the lenient form for Trim).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 19:49:50 +00:00
bherbruckandClaude Opus 5.5 b8d00aea13 Surface Extend: a planar surface's straight edges pushed out (FusionExtendCommand)
Kernel extend(body, edges, distance) moves the picked straight edges of a
planar surface outward in its plane, with neighbours meeting them. Doc
feature SurfaceExtend, engine FusionExtendCommand. Tests: 10x10 extended
by 5 past one edge gives 150, past two gives 225; the engine patch, trim,
extend and thicken gives 240.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 17:31:57 +00:00
bherbruckandClaude Opus 5.5 dd6c8e1a6b Surface bodies: Patch, Stitch, Trim and Thicken
Kernel (surfaces):
- Bodies with an open shell are surface bodies (Body::is_surface).
- patch_region: a planar patch of a sketch region. patch_edges: a planar
  patch filling a closed loop of a body's edges.
- copy_faces: faces copied out as a surface.
- stitch: sew surfaces along coinciding edges, turning faces to agree; the
  result is a solid when it closes.
- trim: cut a surface by a plane or face and keep one side.
- thicken: offset planar and circular-cylindrical faces along their
  normals (one side or symmetric) into a solid. Plane copies slide;
  cylinder copies scale about the axis, exact for NURBS circles. Free
  edges get ruled walls. Faces meeting at a sharp edge are refused.

Doc features Patch, Stitch, Thicken (with operations) and SurfaceTrim;
engine commands FusionSurfacePatchCommand, FusionSurfaceStitchCommand,
FusionSurfaceThickenCommand and FusionSurfaceTrimCommand.

Tests (volumes):
- A 10x10 patch thickened by 2 gives 200, also symmetric.
- Half a cylinder side thickened outward gives half a ring; both halves
  inward give the full ring.
- Six faces of a box stitched back give 6000; an open box with its top
  patched from the edges, stitched, gives 6000.
- Trim keeps 30 or 70 of 100.
- Engine: patch, trim, thicken gives 140.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
2026-10-08 17:31:57 +00:00