mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
develop
1370 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
481687fff3 |
Modeled threads: the thread runs down to a flat face with no uncut block, so a nut can bottom out (#11782)
* Modeled threads: run down to a flat shoulder so a nut can bottom out An external thread running the whole face stopped with its groove clear of a shoulder, leaving up to a turn uncut above it. The groove now runs on past the plane of the face's end and is trimmed there, so its root reaches the shoulder at every angle and nothing past the plane is cut. A shoulder rising back along the face keeps the square stop, as do internal threads, tapers, runouts and lengths stopping short. Where the kernel refuses the trimmed cut, the groove also keeps the square stop and the feature warns. Closes #11610 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Kernel ladder: record the thread cases that now reach their shoulder Six cases gain the faces of the trimmed last turn; every other case is unchanged apart from its timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Thread at a shoulder: review fixes, and a test of the fallback The shoulder check starts just off the face, as the free-end check does, so a slope rising from the face counts. A groove the trim leaves nothing of now falls back to the square stop too. A test forces the fallback and reads its warning. The ladder's order and report go back to main's, changed only for the cases this moved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Thread at a shoulder: check all the radii the groove cuts The check for a shoulder rising along the face looked a fixed distance out, which falls short of the groove on an undersized shaft. It now covers the groove's reach. A test reads how each kind of shoulder is classified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Thread at a shoulder: test the trim that leaves nothing of the groove The test now reaches the path where the half-space keeps nothing, which is the one that must fall back to the square stop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Thread at a shoulder: say so when the shoulder can't be checked If the kernel can't make the rising-shoulder check, the groove keeps the square stop as before, and the feature now warns that its last turn is left uncut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f74c6605c4 |
Fillet: Mitre or Swept corners where two rounds meet (#11498)
* SMLib wrapper: a swept or mitred turn for constant rounds (Turn) Brep::blend_dropping takes a Turn: Mitre (the default, as before) or Swept, which sets solidcore's FilletSession::set_swept_turns before the corners are made. A swept turn solidcore refuses at CreateFilletCorners comes back as Error::TurnRefused with its reason and place (NoRoom at a concave side edge, else CornerUnsupported). Swept is for Blend::Round only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Kernel: fillet rounds turn in a mitre or swept (FilletTurn) fillet_removing, fillet_max, fillet_max_each and fillet_each take a FilletTurn and hand it to SMLib, through the reblend of earlier rounds at a later corner too. A swept corner solidcore does not build is refused as invalid input with its reason and place: no smaller radius builds it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Design: a fillet saves its turn, mitre (default) or swept FeatureKind::Fillet gets `turn` (FilletTurn: mitre, the default and not saved, or swept), one pending format file. Evaluation hands it to the kernel, pattern copies included, and refuses a swept turn with a style, fit, corner or setbacks the kernel's swept corner does not take (SWEPT_TAKES). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * solid.fillet: the turn parameter, and the command's input and output schemas `turn: mitre|swept` on solid.fillet, refused when unknown or with options a swept corner does not take. solid.fillet now has its contract (FilletParams) and reply type (FilletResult), registered with checked!, and leaves schema/unconverted.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Fillet dialog: a Turns choice, Mitre or Swept A Constant radius fillet's Turns row sends `turn: swept`; choosing a narrowed radius or a curvature corner puts it back to Mitre, and Swept puts those back. Its tooltip says what each does where two rounds turn round a corner edge. New strings in a pending locale file (en, de, ja, ko, zh); glossary entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Fillet turn: the schema tests' fixes and the complete catalogs' strings The no-contract example in the schema tests is solid.chamfer now that solid.fillet has one; the reply test checks the mid-radii form against the contract (solidcore refuses that law's corner yet, #430) instead of building it; Arabic, Hebrew and Vietnamese, complete catalogs, get the new strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Fillet schema test: variable and spline forms checked against the contract solidcore fails a variable round on a box edge at this pin (status 1001, as variable_radius_on_a_box_edge), so the reply test checks those forms against the contract instead of building them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Fillet turn: the swept rig cases pass; the MCP no-contract example is solid.chamfer cov-fillet-turn-swept-boss-base-r1 and cov-fillet-turn-swept-pocket-sharp-corner-refused-r1 pass now that solid.fillet takes `turn`: off the rig's known-failing list. The MCP bridge test's example of a command without a contract was solid.fillet, which has one now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e12afd11cc |
Ellipse and spline extrudes make one wall; coplanar faces never lose their names to a budget (#11687)
* Planar sources: decide coplanar faces by support group, no budgets that drop sources map_planar_sources kept two budgets, `metadata` (2 MB) and `identity_bytes` (16 MB), charged by each face's source descriptor and by every extra candidate on its plane. When one ran out, faces silently lost their source and with it their persistent name: a 300-hole plate whose top a comb of grooves cuts into 62 strips lost the cap source from strip ~47 on. The budgets bounded the real cost: each result face looked at every candidate on its plane, faces x candidates when many faces are coplanar. The candidates are now grouped by support once (`same_support` against each group's first plane, through the existing PlaneIndex), each group's chosen source, ambiguity and inner flag decided once, and the faces without a role together with the competing tool faces grouped the same way. Each result face then looks up its plane's few groups. The decision is the old one: blocked by an unknown or competing support, else the first candidate on the plane unless another candidate there has a different plane, source or role. Work: grouping looks at each candidate's index window once per group (each candidate is assigned on its group's first look), and each face looks at the group representatives near its plane, so the planes looked at grow with faces + candidates. A counter (`looked`) checks it: n pieces of one cap and n faces on its plane take exactly 2n looks (n*n before), 3n with n blocking supports. Tests: the holed plate keeps the cap source on all 62 strips and the bottom (fails on the old code: face 352 lost its source); the existing planar source tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Extrude: an ellipse or a spline sketch curve sweeps one wall An extruded ellipse had four side faces (`F2:side:e1#1` to `#4`), one per conic quarter, and a spline one per span: the profile holds such a curve as several exact segments and SMLib got each as its own curve. The Extrude feature's builder (`extrude_whole_sides`) now takes a `one_curve` test from the caller. Each run of free-form segments of one degree that it says are one curve goes to SMLib as one B-spline: the Béziers' control points in a row, knots repeated the degree times at each joint, each span as long as keeps the speed through the joint the same (so the curve is C1 there and SMLib makes one face over it, as it would not over a C0 parametrisation), weights scaled to meet. Exact: the same curve, reparametrised. A loop starts at a run's start, so no run wraps round it. The wall's history lists every piece, and its construction source is its first piece's (as joined collinear lines). The document says which segments are one curve: ExactSketchCurves now also holds each free-form sketch curve's exact segments (`find_free_form`: a segment that is one of them, either way round), and the profile roles use it, so the wall is named `side:<curve>` from its history rather than by the curve nearest its mesh centre. Saved designs rebuild with these walls renamed (`F2:side:e1#k` -> `F2:side:e1`); accepted, no alias kept. Tests: kernel `pieces_of_one_curve_extrude_into_one_wall` (an ellipse in four conics is 3 faces, its volume pi a b h to 1e-6, the wall generated by all four pieces; a 3-span spline closed by lines is one wall; the same solid as a face per piece); doc `an_extruded_ellipse_or_spline_is_one_wall_named_by_its_curve` (one `F2:side:e1`, no piece names, the same names on a second build and after reopening). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Tests: a tray's far rim name is checked on the exact B-rep face The tray test took the point `sheet:...:rim.far` finds as right when it lay within 1e-3 mm of the strip z = 30, 199 <= y <= 200: a slack sized to the mesh. It now asserts on the face the name finds, on exact geometry: the point's distance to that face's surface (FaceSurface::nearest) is within rounding at the part's scale (1e-12 x its size), and the point is inside the face's boundary, counted by crossings of its record edges (each asserted to be a line, the rim being planar). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Tests: delete tests/naming/, the records no test reads since the naming corpus stopped comparing against them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Viewport test: smooth wall joins drawn from eight G2 spline walls The PR extrudes each spline or ellipse sketch curve as one wall, so the offset-ellipse ring the test built has no joins between faces any more. The test now extrudes a circle drawn as eight cubic splines: eight walls meeting with one curvature along eight upright edges (24 edges in all). It still checks that those joins are left out of the shaded view, drawn in the wireframe and selectable as edges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ * Kernel ladder: 009 and 012 record the one-wall extrude (an extruded spline or ellipse sweeps one wall) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bcac191847 |
Typed schemas for Change Design Units (part of #2829) (#10934)
document.units gets input and output schemas: `units` (mm, cm, m, in or ft, with the handler's messages for a missing or unknown unit) and its reply, `units` and `kept`, which the handler now builds from the reply type. Its line leaves unconverted.txt. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c019e2555c |
Threads: a 12 mm hole now takes an M12 thread, and a modelled M12 builds in it (#11651)
* Threads: a 12 mm hole takes M12, and a modelled thread narrows a bore that is too wide (#11609) Automatic sizing of a hole now takes the size whose nominal diameter the hole has (a 12 mm hole is M12, not M14, the size whose tap drill is 12 mm); a hole near no nominal diameter is still matched by tap drill. A modelled internal thread in a hole as wide as the thread or wider no longer refuses with "too wide": the bore is filled in to the thread's minor diameter over the threaded length and the groove cut there, and the feature's warning says so. A size that does not fit the hole names the size that does. A saved thread with no designation keeps the old rule through a pending format step, so an existing design rebuilds with the size it had. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Threads: keep an old automatic size through edits; kernel-rig cases expect the narrowed bore (#11609) Editing a saved automatic thread keeps `old_auto_size`, so it does not change size. The five kernel-rig cases that expected a modelled thread in a hole as wide as the thread to be refused now expect the narrowed bore, with the volume worked out analytically in each case. The thread dialog's reopen pattern names the new field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Thread dialog test: a 5 mm hole is suggested M5, its nominal size (#11609) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2741df3629 |
Typed schemas for Appearance, Appearance Library and List Appearances (part of #2829) (#10976)
* Typed schemas for Appearance, Appearance Library and List Appearances (part of #2829) appearance.assign, appearance.library and appearance.list get contracts that read what their handlers read, with the handlers' messages, and reply types the handlers build their replies from. Their lines leave unconverted.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Appearance library: the opacity is already a number Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
bfb32cd6e1 |
Align: move bodies by frames that follow sketch and model geometry (#6257) (#7123)
* claim: #6257 * Align: move bodies by frames that follow sketch and model geometry (#6257) solid.align takes from_frame and to_frame: a joint origin on the model (a face's or round end's centre, an edge's middle, a corner, found again by name) or a sketch point or a sketch circle's, arc's or ellipse's centre. Both are resolved on every evaluation, so the bodies follow their target when it changes. An optional angle turns the bodies about the target's direction after the move. The Align dialog takes sketch points and curves as From and To, offers the Joint Origin snap and an Angle row. * Align frames: box the joint origin (clippy large_enum_variant) * Align frames: every moved body on the frame's component, placed once * Align frames: a sketch id's length in characters * Align frames: the dialog test's sketch curve hit carries its point * Align frames: the dialog test takes Vec2 from solidcore-record --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> |
||
|
|
fbd30b0bd0 |
Panels: inspect and change layouts through commands (#11304) (#11454)
* claim: #11218 * Panels: add shared layout rules and retained content runtime * claim: #11304 * Panels: inspect and change layouts through shared commands (#11304) * Panels: account for layout commands in geometry coverage |
||
|
|
dc38349cbf |
Layer split stage 1: the B-rep record, native body file and Vec/Plane come from solidcore-record (#11496)
* Kernel: the B-rep record and the native body file come from solidcore-record SolidCraft's portable B-rep record (`solidcraft_geom::brep`: brep.rs, topology, history, envelope, part, curve2) and the native body file (`solidcraft_kernel::native`: the tagged binary codec and the container) move to solidcore's `solidcore-record` crate: geometry as data, with structural validation and no maths (solidcore's docs/layers.md, stage 1). Every caller uses `solidcore_record::brep` and `solidcore_record::native`; the old copies are deleted. What computes from coordinates stays with the kernel, in geom's `brep`: - `brep::check`: the record's geometric checks (orthonormal frames, non-degenerate planes, unit revolution axes, non-singular placements). `check::validate` and `check::validate_built` are the record's validation and these together, and every caller of `Brep::validate` uses them, so what is accepted is unchanged; - `brep::label`: a surface's natural ranges (a cone's apex), `curve_interval`, `edge_curve_interval` and `form_turns`; - `brep::pcurve::eval`: a pcurve's point; - `geometry::check`, `placement::moved` and `split_edges` become functions on the record. The kernel's native module keeps the conversion between a body and its saved record (metadata.rs) and checks the decoded record's geometry. `native/analytic.rs`, which no module declared, is deleted. solidcore-record is pinned to its rev on solidcore's `layer-record` branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Geometry: Vec2, Vec3, Plane and the profile types come from solidcore-record The plain data of vectors and points, sketch planes and planar profiles (`Seg2`, `Loop2`, `Region2`) moves to `solidcore-record`, with its constants and component-wise operators only (owner decision, solidcore layer split). Their geometry stays in geom as traits: `Vec2Geom`, `Vec3Geom`, `PlaneGeom`, `Seg2Geom`, `Loop2Geom`, `Region2Geom`, `Seg2ArcFit`, `Loop2ArcFit` and `Region2ArcFit`, in scope with `use solidcraft_geom::prelude::*`. Every caller names the types from `solidcore_record`; geom no longer defines or re-exports them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT * Kernel: pin solidcore-record to solidcore main f5e8e9a29 solidcore-record is merged on solidcore main (tangency-labs/solidcore#2519); its crate there is identical to the rev pinned before but for its clippy configuration. The lock is regenerated against the merged rev. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NvoxKMbMFSQU8tzRtoJhtT --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
01abf8af17 |
Engine test: the later round at a mitred corner, as solidcore 8ddce9016 builds it
At solidcore 8ddce9016 (pinned) the r2, r2, r3 corner at (20, 0, 20) is one patch: 10 faces, 7928.743041456134 mm³, the r3 round and the patch the second fillet's. The test is pinned to that and leaves the known-failing list. The fork's setback corner (15 faces, 7928.142563938) waits for solidcore#2310; solidcore#2382, not merged, moves the case to 7927.724282512234. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
ac3e6e0105 |
A panic solidcore's guard catches reaches the engine as EngineError::Internal
smlib's run() catches a solidcore panic and replaces the kernel (kept: the app does not die natively), returning Error::Internal; the kernel turned it into KernelError::Failed and the engine into Other, so hostile_params_never_panic and the fuzzer no longer saw kernel panics. Now the kernel's wrapper errors go through KernelError::smlib, which keeps Internal; a body the wrapper could not take because of a panic is Internal too. A feature whose evaluation hit it records the result as internal (not cached: the kernel was replaced), and the command that made it, or any command whose kernel call returns it, fails with EngineError::Internal under its own id. Crash reports mark a panic inside a solidcore call recovered (kernel::guarded), and the crash test's kernel mode panics inside a solidcore call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
d9310b2389 |
Crash test: a panic inside the kernel is the command's internal error
The kernel crash test's command must fail with EngineError::Internal naming diagnostics.crash_test, as a panic in the command itself does: the panic detectors (hostile_params_never_panic, the fuzzer) look for Internal. Today it is EngineError::Other. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
440fdaed13 |
Kernel work runs on threads with solidcore's 8 MiB stack
solidcore's algorithms recurse (subdivision, intersection, filleting) and its README asks for 8 MiB, a native main thread's stack; Rust starts other threads with 2 MiB, and only the web build set more. kernel::KERNEL_STACK is 8 MiB and kernel::kernel_thread a thread builder with it. The kernel's parallel map runs on a pool of its own with that stack (on a worker of a pool already, as in map_with_local_pools, it stays on that pool as before), the one-thread mesh pools and map_on_threads' helpers have it, and so do the engine's interference worker and the evaluator's plan-mesh thread. A test recurses 5 MiB deep on each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
3131b933c7 |
Engine blend-fit test: solidcore is the only kernel, so the refusal always names its edge
The cherry-picked test asked kernel_route which kernel ran; that choice is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
8a730c6273 |
Fillets on solidcore: the largest size that fits is still the one the search builds; the kernel's names the edge
solidcore's fit is the largest size less a margin (34.99 where the L bracket's bend builds 35, 4.29 where #3435's ring builds 4.3), so the feature's search keeps naming the size, and solidcore's is the answer only where the search finds none. The refusal keeps its words, "larger than the faces beside its edges allow", with the edge after them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz |
||
|
|
881899bee1 |
Fillets on solidcore: a blend too large names the largest size that fits and its edge, from the kernel; the crossing-rails mixed corner is not supported (solidcore#2256)
solidcore sizes a blend it refuses as too large (the session's fit, SolidError::FilletRefused under fillet_gn_section_generator). The smlib crate gives it as Error::TooLarge with the size, what it sizes and a point halfway along its edge (an equal chamfer's ball radius turned back into its distance); the kernel words it as the fillet feature's search does (largest_fit_note), and the feature runs no search when the refusal names it. The 3x2 mixed corner whose rails cross, which solidcore refuses (fillet_refuse_mixed_corner_crossing_rails), is not supported yet, citing solidcore#2256. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SdcgEu4Lr3moCCisdZShKz |
||
|
|
a59c14678f |
licences test: the bundled third-party text is checked for wgpu and egui, not truck
Truck is gone from SolidCraft's dependencies, and main's regenerated THIRD-PARTY-LICENSES.txt no longer lists it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5M41G8Nua7HvuFPZkmJ |
||
|
|
b4a3a9baae |
Wave 45/45: smlib export test: a rounded cube's corners checked on solidcore's spheres (and 3 more)
4 commits, squashed; the originals stay reachable from branch jyviko/merge-main-develop-solidcore (#11360). - |
||
|
|
c5ccb9201d |
Wave 43/45: smlib: rustfmt after the C++ kernel's removal (and 1 more)
2 commits, squashed; the originals stay reachable from branch jyviko/merge-main-develop-solidcore (#11360). - |
||
|
|
28e983e191 |
Wave 39/45: diagnostics.kernel takes a typed contract, as main's other diagnostics commands do
1 commits, squashed; the originals stay reachable from branch jyviko/merge-main-develop-solidcore (#11360).
-
|
||
|
|
ea772a0438 |
Merge develop into main: SolidCraft on the SMLib kernel (v1.1.0-alpha) (#11351)
* UI: rim and wall picking tests for a circle of arcs that is one wall A circle drawn as two half arcs now extrudes to one full-turn wall with its seam edge, and each rim is one closed edge (tangency-labs/smlib#67). The tests keep their intent on that body: a rim clicked once is one item, measured and highlighted whole and picked from either side; a wall picked either side of its seam is the same surface, lit whole, and toggles off from the other side; every face is its own patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fillets: a later round at a corner where earlier rounds meet blends them again with it The owner's ruling of 2026-10-30: a later fillet on an edge at a vertex where earlier rounds already meet in a mitre makes the corner rounding all of them at once makes, one smooth patch, the mitre gone. A body made by constant-radius rounds keeps what they were made on and which edges they rounded at which radius; a later round reaching such a corner blends the earlier rounds' edges (from the earliest of those meeting there on) with the new edges in one SMLib blend of the body before them. The result's history is put over the body the later round was given: an earlier round's face is that face trimmed anew (its name kept), the corner patch and the new rounds are the later fillet's. A picked mitre seam has no round of its own: it is refused unless a new edge of its radius meets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: a later r3 round at an r2 mitre, through the Fillet feature, makes the all-at-once corner Two box edges rounded r2 in one fillet, then the upright edge at their mitre r3 in a second fillet (tangent chain on, so the seam comes with it): 10 faces, 7928.743041 mm³ as fillet_radii of the box gives, the r2 rounds' names kept, the r3 round and the corner patch the second fillet's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Kernel: a body's record of rounds goes with its history, and can be read back The rounds that made a body are over the body its history is over: a new history (an operation carrying a step's result back to its own operands) leaves the record out, so a later round never reads one history against another body. Body::rounds gives the record and Body::with_rounds_of takes one back, checked (edges of the body before them, a fillet's radii, the body's history over that body), for a saved model to keep it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Model cache: a body made by rounds keeps them, so a later round blends them the same after opening A design gave another body when opened from its model cache: the rounds a body was made of lived only in memory, so a later round at their mitre was built sequentially (or refused, as too large) after opening and blended with them when evaluated anew. The saved model now records, per kernel body made by rounds, the body they were made on (a place in the bodies, stored once though no state holds it) and the edges with their radii; reading it attaches them back down each chain, and a thinned model keeps those bodies with the bodies made from them. Model cache format 7. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: a corner patch of rounds 2/3/4 takes no more than the strips leave in its block three_rounds_at_a_corner bounded the patch's extra cut at 3 mm³ beyond the strips, a figure with no derivation; SMLib's all-at-once body at radii 2, 3, 4 takes 3.099 (V 7878.579574 against strips 7881.678). That body is the strips' outside the block its patch lies in: its rounds are the strips' cylinders ending at a's section y = rc, b's x = 20 − rc and c's rails where they meet a's and b's (the patch's four corners, now asserted), so the patch takes at most what the strips leave in that block (11.86 at 2/2/3, 32.69 at 2/3/4). The lower bound is that block; the upper one, at most the strips' volume, stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the io crate's hand-built saved models list their rounds, as format 7 records do Model cache format 7 ( |
||
|
|
59ffd38019 |
Timeline: selecting a joining extrude no longer lights round walls an earlier feature made (#11263) (#11286)
A join that reports no history names faces by comparing the meshes before and after. When the join makes the body bigger, round walls are meshed differently and miss their old mesh at the naming tolerance, so they were named as the new feature's faces. A curved face with no match now looks again within the two meshes' chord tolerances. An edge name saved while a wall had the wrong name falls back to the edge at its stored point. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5e54681c80 |
Sketch: instances stepped in from an arc stop where the arc runs out, not turn round (#11068)
* Sketch: instances stepped in from an arc stop where the arc runs out, not turn round Each instance after the first takes its side from a point one more step past the last one. Stepping toward an arc's centre past an arc smaller than the distance, that point lands beyond the centre, and the side was read by comparing the point with the two offsets at the full distance: the one toward the centre doesn't exist there, so the other won and the next instance went back out onto an earlier one. The side is now read a hair from the curve, where both offsets exist, and the point stops halfway to the centre. The arc then runs out of room and the command is refused there, as for a ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Offset side: read off the picked line or arc itself A tiny step compared at large coordinates rounds away, and an arc left smaller than that step by an earlier instance still has no offset toward its centre: both turned the side round. A line's side is now its half-plane and an arc's inside or outside its circle, as a circle's already was; other curves compare both offsets' distance as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7bdaec8f4d |
Joints: large assemblies with many closed loops no longer leave a new joint in conflict (#9687) (#11146)
* claim: #9687 * Joints: Edit Joint and Unsuppress meet a conflicting loop joint as Joint does (#9687) joint.create (#10371) turns an angle a half turn when a new loop-closing joint comes out as a conflict, so a second planar joint whose faces point the same way meets. joint.edit never did, so a design saved before that (the report's rail on two planar joints) kept the conflict however the joint was edited, and a joint unsuppressed after its part was turned away stayed one too, though Move's hint says to edit the conflicting joint. An edit that places the joint again (type, offset, angle, flip, a side, or unsuppressing it) now tries the same half turns. A rename alone moves nothing. * Joints: a loop group over the solver's values still moves the loops that need to (#9687) A frame held by many planar joints (the report's printer: six planar joints between two profiles, and more) ties its loops into one group through the shared values. Past MAX_LOOP_VALUES (64) such a group was only checked, so a new joint closing a loop in it (Joint35, the rail on the 450 mm profile, which took the group from 63 to 69 values) stayed a conflict, and nothing in the group could be driven or dragged any more. Such a group now moves the values of the loops that need to move, those open at the start or reading a held value that moves, when they are few enough, while every loop of the group stays a residual, so the others stay closed. Groups within the cap solve as before. * Joints: closed loops through a moving value follow on their own closing joint (#9687) In a loop group over the solver's values, a closed loop that runs through a value the open or driven loops move (a part slid on one planar joint and held by a second) kept its closing joint's values, so its gap opened and the solve failed. Those closing joints' values move too: they are on no tree path, so freeing them pulls in no further loop. * Joints: loops a motion link ties to a following closing joint follow too (#9687) A followed closing joint whose value a motion link sets moves the value its link chain starts from; closed loops through that value now follow on their own closing joints as well, until no more do or the values pass the solver's cap. * Joints: say what the narrowed loop solve does not find (#9687) * Joints test: measure the peg's 30 degree turn from where the loop left P0 (#9687) --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> |
||
|
|
861fb8ad56 |
Hole: a countersink cuts nothing outside its diameter above the face (#5041) (#8261)
The countersink tool was a cone started at the run-on height above the face, so above the face it was wider than the countersink and kept widening: a wall or step just past the rim was notched. The tool is now one revolved profile: the cone from just above the face down into the hole, and above it a cylinder of the countersink's own diameter. The cone is the profile's first segment, so its face keeps the name csink:side:0. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f7d66d2b5b |
Sketch Break: break at a shared point separates the curves that meet there (#7410) (#7950)
* claim: #7410 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sketch Break: break at a shared point separates the curves that meet there (#7410) Two lines drawn as one chain share their corner point. There was no way to pull them apart: Break there said "nothing to break at there", and deleting the point deleted both lines. sketch.break aimed at a point where the picked curve ends and another curve also meets now gives the picked curve its own end there (`<curve>.start` or `.end` when that id is free), at the same place: both curves stay where they are and are no longer joined. The Break tool snaps a click on the corner to that point, so clicking it works the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
780c1163f9 |
Typed schemas for the crash report commands (#10909) (#10910)
* claim: #10909 * Crash report commands: input and output schemas (#10909) diagnostics.crash_reports, diagnostics.crash_report, diagnostics.crash_reports.clear and diagnostics.crash_test get typed contracts and reply types in schema/diagnostics.rs and leave unconverted.txt. Strict now refuses an `id` that isn't text on clear, which the handler reads as no id and so deletes every report. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
fa1eb57e3b |
Typed schemas for the standard views and Normal To (#10898) (#10899)
* claim: #10898 * Standard views and Normal To: input and output schemas (#10898) view.front, view.back, view.left, view.right, view.top, view.bottom, view.isometric and view.normal_to get typed contracts (an optional `camera`, read by the new shared CameraParam type with the commands' own reader) and a CameraReply output, and leave unconverted.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6602ab823f |
Typed schemas for Undo, Redo and the checkpoints (part of #2829) (#10945)
edit.undo, edit.redo, edit.checkpoint and edit.restore_checkpoint get input and output schemas in schema/edit.rs: the first three take no keys, the last takes `checkpoint` (with the handler's message for a bad id) and `forget`. The handlers build their replies from the reply types. Their lines leave unconverted.txt. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
ccdb470e01 |
Sheet metal rules: typed input contracts and replies (#6581) (#10921)
* claim: sheet rule contracts (#6581) * feat(engine): add typed sheet-rule contracts (#6581) |
||
|
|
a54d0517f7 |
Typed schemas for the local library commands (#10938) (#10939)
* claim: #10938 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Library commands: input and output schemas (#10938) library.inspect, library.export and library.apply get typed contracts and reply types in schema/libraries.rs and leave unconverted.txt. The handlers' own serde parameter types move there and become the contracts, so the contract is exactly what each handler reads; the catalog, provenance and apply result schemas are written by hand, since solidcraft-io doesn't use schemars. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
7015c296ea |
Typed schemas for List Edges, List Faces and List Threads (#10942) (#10943)
* claim: #10942 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * List Edges, List Faces and List Threads: input and output schemas (#10942) model.edges and model.faces take a typed `body`; model.threads takes no parameters. Their replies are built from ModelEdges, ModelFaces and ModelThreads in schema/inspect.rs, with the same JSON as before, and the three commands leave unconverted.txt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
b73eb6c243 |
Typed schemas for the curvature comb, minimum radius, isocurve and centre of mass commands (#10949) (#10950)
* claim: #10949 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Curvature comb, minimum radius, isocurve, centre of mass: schemas (#10949) The four analysis commands get typed contracts and reply types in schema/sketch_inspect.rs and leave unconverted.txt. Each point list has its own type with its handler's messages and reads only as many points as the handler does; `count`, `density` and `scale` read numbers as params::num does. The replies are built from the types, with the same JSON as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
38cc3507db |
Typed schemas for the configuration table, rules, activate and preview commands (#11059) (#11060)
* claim: #11059 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Configuration table, rules, activate and preview: schemas (#11059) config.table, config.rules, config.activate and config.preview get typed contracts and reply types in schema/config.rs and leave unconverted.txt. A new `table` builds the table from ConfigTable; show and the activation build their replies from it, so the unconverted config commands still return the same JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
90e30126ea |
Typed schemas for Boundary Surface (#11083) (#11084)
* claim: #11083 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Boundary Surface: input and output schemas (#11083) surface.boundary gets a typed contract in schema/surface_boundary.rs and the shared FeatureResult as its output, and leaves unconverted.txt. One struct holds both forms; each key reads as the handler reads it, with its messages, and `u` and `v` check their items where the handler does without the design, index included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e3bbd8258a |
Typed schemas for Loose Ends and Tie Loose Ends (#10959) (#10960)
* claim: #10959 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Loose Ends and Tie Loose Ends: input and output schemas (#10959) sketch.loose_ends and sketch.loose_ends.tie get typed contracts and reply types in schema/sketch_ends.rs and leave unconverted.txt. SketchTarget reads `sketch` as sketch_store::target does and PointIds reads `points` as the tie does, each with the handler's messages. The replies are built from the types, with the same JSON as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
0f8b971ef0 |
Typed schemas for Bill of Materials (#11075) (#11076)
* claim: #11075 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bill of Materials: input and output schemas (#11075) assembly.bom gets a typed contract and reply types in schema/bom.rs and leaves unconverted.txt. Each key reads as the handler reads it, with its message; the reply is built from Bom, BomRow and BomChild, in the old field order, so the reply and the files it writes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
9432cdfee8 |
Sheet metal: material joined in the plane of a panel is kept (#6843) (#6855)
* claim: #6843 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsiqyXWBFGTCX9J51bhysc * Sheet metal: material joined in the plane of a panel is kept (#6843) An extrude join through sheet metal went through the generic boolean: the body grew, but the sheet model didn't, so the flat pattern, DXF and bend table were refused, and the next sheet feature (a refold) dropped the material ("what ... changed is not kept"). An untapered extrude join one sheet thick in the plane of a panel (the base or a flange's panel, unfolded or folded) now joins the sheet model: each region of its sketch becomes part of the one panel it is one piece with, apart from every bend zone and the other panels, so the flat pattern, its DXF and a refold keep it, and it follows its sketch's parameters as a cut made there does. Cut-outs it covers are filled. A join of another depth, out of every panel's plane, touching a bend zone or another panel, or over a pocket, label, bore or perforation keeps the generic path and its warning. The extrude records its sketch plane and depth for a join too (its profile grows into the material it joins, so it never counted as untrimmed). The test that refused a joined tab beside a plain base now refuses a block standing on the sheet instead; the tab is kept. Fixes #6843. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
dadefec702 |
Joint dialog: Find Limits fills the limits from where parts collide (#5968) (#7047)
* claim: #5968 * Joint dialog: Find Limits fills the limits from where parts collide (#5968) joint.find_limits sweeps each value of a joint both ways from where it is, on a copy and with its own limits left out, and answers where the parts it moves first touch another part, where a closed loop stops closing, or that nothing is met within a turn (angles) or the parts' reach (distances). Edit Joint's Limits section gets a Find Limits button that runs it on the joint as the dialog has it, off the window thread, and fills the rows. joint.sweep now tells touching bodies apart from the other bodies of the same two parts: on the Hinge Assembly sample the pins touch where it starts, and the plates used to pass through each other unnoticed. * find_limits: read the session only * find_limits: a joint with nothing to meet, edits during a search, tiny ranges * find_limits: keep touching at the start per pair of parts Per body, the Hinge Assembly sample's plates, which only come flush in the end plane the two leaves share, stopped the search. The leaves never pass into each other: the sample's answer is a whole turn. * find_limits: touching boxes cost no collision work; stale results and rounding * find_limits: each pair of bodies of resting parts held to its own depth; loops without bodies * find_limits: a 32-bit budget it can pass, a repaint with the result, a bad form says so first * find_limits: each pair of resting bodies gets its own margin * find_limits: Arabic, Hebrew and Vietnamese strings (complete catalogs on main) * find_limits: search the joint with Edit Joint's side picks Edit Joint now keeps the joint's sides as picks in its inputs, and its OK commands compare them with what it opened on. Built without them, the commands Find Limits applies first asked for the sides again, so every search failed. --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> |
||
|
|
686056e4de |
Opening: a design saved by 0.24 opens with its sketches in later versions (#8151) (#10379)
* claim: #8151 * Opening: a design saved by 0.24 keeps the body a Join To Object runs to (#8151) Since 0.24.1 a Join extrude To Object also joins the body it runs to (#6424). Designs saved by 0.24.0 and earlier were built without that, so on 0.25 the reported box swallowed its lid: the lid's sketch, the holes, the cut and the moves after it failed. An extent may now save `to_body_apart: true`, which leaves that body apart. One pending format change sets it on every Join extrude To Object of a design of format 39 or older as it opens; designs of format 40 and later keep the join. Editing the extrude keeps the field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Markus Kaltenbrunner <kaltenbrunnermarkus@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d7ad69e64f |
Pattern and Mirror: a hole placed by edge distances can be copied (#8055) (#8231)
A pattern or mirror replays its source hole's definition, so a hole with reference edges finds its centre again from them. That happens after the hole is drilled: through the part, the centre is then in the opening, and the check that the centre lies on the face refused every copy. The replay now skips that check; placing the hole itself still makes it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f6e932f61a |
Rounded Rectangle: corners take parameter expressions, as a 2-point rectangle's do (#10956)
`sketch.rectangle.rounded` read `p0` and `p1` with `req_vec2`, so a corner typed as an expression (`["W / 2", "H / 2"]`) was refused with "`p0` must be a point [x, y]", although the 2-point rectangle it hands the corners to already places expression corners and keeps them following their parameters. The corners are now read with `corner_arg`, as the 2-point and center rectangles read theirs; the worked-out values still check the radius against the sides. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e33b8cfb07 |
Sheet metal convert: trays made in SolidCraft convert back (#7529) (#7580)
Convert to Sheet Metal refused a tray made with SolidCraft's own sheet metal, exported and
imported again: its walls run on past each bend's end (notch, tear and round corner reliefs), so
the edges beside the bend cylinder's edge never reach the other face's plane and the face could
not be trimmed ("a face beside a chamfer picked as a bend does not reach the face across it").
Where that edge is round or runs on along the bend's line, the face now keeps its corner and
steps down square to the other plane, a step the bend's setback takes up again; the inset drops
an edge the setback leaves shorter than the tolerance.
Test: trays_of_its_own_sheet_metal_convert_back (notch, tear and round, the walls made in one
flange feature, and notch and round with the short walls in a second one): the volume, the flat's
size and outline area and every bend's length are the source's.
Fixes #7529.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
768dc253dd |
Fix mirror/pattern feature copy dropping bottom slice on hollow parts (#6452) (#7994)
* claim: #6452 * Engine: a pattern or mirror copy of a join keeps its ends as drawn (#6452) The copy's tool is rebuilt against the current state, which already holds the source feature's own material; the embedded-join-end trim (#2059) read that material as the target already filling the copy's bottom band and trimmed it away before the copy matrix was applied. Mirroring or patterning a join on a hollow part silently lost the floor-level slice. Copies now skip the trim: their ends are where the source drew them. * chore: re-trigger duplicate PR check --------- Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com> |
||
|
|
1d98b59883 |
Sketch Break: a length becomes the distance between the old ends; no piece of no length (#6826) (#6834)
Breaking a dimensioned line left the length on the first piece, which the solver stretched back to the whole length, skewing the line. A cut on (or a hair from) an end, as where a tangent arc meets a line, made a zero-length piece with its coincident and collinear ties every time Break was clicked there. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6649cf4cfe |
Project: a refusal that names no body names the kind alone (#10626) (#10859)
The refusal of sketch.project ended in "face of " with nothing after the
"of" when the reference named no body: a picked face, edge or vertex is
found by its geometry, so the body it belongs to is not in the reference.
LinkSource::describe() now drops the "of <body>" when there is no body to
name and names the kind alone ("face", "edge", "vertex", "spun
profile", "isoparametric curve"). A source that names a body keeps its
name.
Only the text changes: the command refuses exactly what it refused before.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
9b431fea16 |
Hole: on a cone or torus face, drill along the face's normal, not a facet's (#10698) (#10885)
A hole placed on the side of a cone with no direction drilled along the nearest display-mesh triangle's normal, up to 2.6° off the surface's normal and sideways. Planes, cylinders and spheres already gave their exact normal; cones and tori now do too, from the surface the kernel reports for the face. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2339510e41 |
Sketch Trim: keep endpoint tangency on the surviving arc (#10851) (#10853)
* claim: #10851 * Sketch Trim: keep endpoint tangency on the surviving arc (#10851) |
||
|
|
ac9d4e8a1d |
Move to Component: a sketch on a face inside the moved part goes along (#11063) (#11072)
* Move to Component: a sketch on a face inside the moved part goes along (#11063) A face attachment stores the placement path it was found through. Moving the placement into another component changes that path, the stored one no longer starts at the design, and the move was refused as breaking the sketch. Face attachments of sketches and construction planes, and an emboss's face, now follow the move as the session's paths already do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Move to Component: attachments made through any placement of the old parent follow A parent placed more than once is one definition: a face attachment made through another placement of it than the one named also ran through the moved placement and broke. Every stored path through the moved placement now runs through its new place. A path that would grow past the 64 placements a design keeps is refused instead of saved unreadable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
39163148d9 |
Sheet metal: an edge flange on part of a stepped outline spans that part (#11034) (#11046)
A flange picked on one segment of a stepped base outline ran the base face's whole length: trimming the picked edge back to the bend line merged it with the outline's own edge on that line beyond the step, and the flange took the merged edge. It now stays off the base's edges that already lay on the bend line, so it spans the segment picked, with no relief there: the base is already back at the bend line. The same outline made by cutting notches into a rectangle gave the right flange already. Assisted-by: LLM |