mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
develop
678 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> |
||
|
|
1e0bfca819 |
Release 0.40.0-rc.2 (#11751)
Bumps the version to 0.40.0-rc.2 and folds the pending format changes into format 50. A published pre-release no longer reaches the Flatpak feed. 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
2aa7e3e51c |
Release 0.40.0-rc.1 (#11457)
Bumps the version to 0.40.0-rc.1 and folds the pending format changes into format 49. The default-layout header test now expects the room the version really takes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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> |
||
|
|
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> |
||
|
|
0d8eebbea6 |
Sheet metal: a hole leaves sheet parts it cannot reach untouched (#10226)
A hole in a design with sheet metal first tried to bore every sheet of its component, building each panel and running a boolean on it, and then checked every sheet again to see whether the drill met it at all. For a sheet far from the hole both only found nothing: with 50 sheet parts that was most of a hole's time. Now a sheet whose box lies clear of the drill's box is left out of both, with the same grown boxes the other sheet cuts use, so a drill into a sheet's rim still bores it. A drill a sheet would refuse whatever it meets (an invalid one, or one more bore than a sheet may hold) is still put to every sheet as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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> |
||
|
|
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> |
||
|
|
ca4796921f |
Tapped holes: read only the bodies along the drill line for the wall (#10227)
A tapped hole looked for the cylindrical wall its marking lies on in every face of every body of the design, measuring each face over all of its body's triangles. A body is now passed over when the drill line, widened by the hole's radius and a margin, misses its box (from the mesh it keeps, which the wall's triangles are part of): no face there can be the wall, so the marking stays the same. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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> |
||
|
|
a0ec7dbdf0 |
Sheet metal: a cut leaves sheets it cannot reach untouched without a check (#10301)
Before a cut ran, every sheet metal body of its component went through a boolean with the tool, to make sure the cut would not take material the flat pattern cannot keep. For a sheet the tool does not reach, that check only found nothing: in a design of 50 sheet parts it was about an eighth of a hole's time. Now a sheet whose box lies clear of every tool's box is left out of the check, with the same grown boxes the cut itself uses (#10225), so a tool that touches or grazes a sheet is still checked. Sheets shown in another frame and Intersect are checked as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6cee7a91b9 |
Release 0.30.1 (#11176)
Bumps the version to 0.30.1 and folds the pending format changes into format 48. 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> |
||
|
|
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> |
||
|
|
f484457c3b | Expressions: sign(0) is 0, not 1 (#10996) | ||
|
|
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 |
||
|
|
ef939b52eb |
Parameters: one meaning for value in every reply; round reported numbers (#10747) (#10812)
* claim: #10747 * Parameters: value is mm/degrees in every reply; round reported numbers (#10747) --------- Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com> |
||
|
|
29a6bbdb43 |
Hole: say when a hole removes no material, or all of a body (#10701, #10702) (#10836)
* Hole: a hole moved off the material says it removes no material (#10701) A hole whose position, parameter or sketch point moves it off the part cut nothing and said nothing: the feature had no warning, and a parameter sweep listed the value as fine. A hole whose tool is clear of every body it cuts now warns "the hole removes no material" ("1 of 2 holes remove no material" for several), as a pattern does of its copies. The check compares boxes only, from the meshes the hole already found its face on, so a large hole pattern costs no extra booleans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hole: a hole wider than the part says it removes all of it (#10702) A through hole wider than the part removed the whole body with no message, also when a parameter widened it later. The hole now warns "the hole removes all of Body1" when a body it cuts is gone after the cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
073bbc3f00 |
Assemblies: a construction axis follows its component when the component rotates (#10127) (#10212)
* claim: #10127 * Assemblies: a construction axis follows its component when the component rotates (#10127) A construction axis or point built on a body now goes with that body when Move/Copy moves or turns it. Each construction records the bodies its picks lie on; the Move feature (not a copy) carries along every construction whose bodies it all moved, with the same transform in the construction's component frame. One that also reads something fixed stays where it is. 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> |
||
|
|
9e5e143ac3 |
Sheet metal: a fold crosses slanted or curved edges of any panel (#10621) (#10922)
sheet.fold refused any fold whose bend zone wasn't a rectangle. It now crosses the sheet's own outer edges, slanted or on any curve: the outline is cut exactly across the zone, the panels keep their real outlines, and flanges and other folds still go on them. The flat pattern's DXF keeps the curve as drawn. A sharp corner in the zone stays refused. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1cbebe21bf |
Edit Hole: seed the face where the hole is now (#10689) (#10860)
* claim: #10689 Co-authored-by: Cursor <cursoragent@cursor.com> * Edit Hole: seed the face where the hole is now (#10689) Project the stored position onto the drilled face along the hole direction when opening the editor, matching evaluation, so OK works after a parameter moves the face. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
12ebc80e15 |
Cosmetic tap markings: look for their cylinder only in nearby bodies (#10303)
* Holes and cuts: count the bodies a cut works on in the cost tests A test helper, cut_work, adds a feature at the end of a design and reports how many bodies its cut ran booleans on and which bodies it left exactly as they were (same geometry, key and face names). A new test pins that a cut still goes through the boolean where its tool touches a face, runs along an edge, passes a hair beside a body or drills into a dome. The sheet metal hole (#10226) and cosmetic thread (#10227) work reuse the helper for their own shortcuts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Holes and cuts: leave bodies the tool cannot reach untouched without a full cut A cut ran a whole boolean against every body of its component, and for a body its tool does not reach that boolean only handed the body back: about 7 ms per body, so a hole in a design of 200 blocks took 1.3 s. Now a body whose box lies clear of every tool's box is left as it is. Both boxes are their meshes' grown past how far the exact faces can curve out from them, the same test sheet metal cuts already use, so a tool that touches a body, grazes it or drills into a dome still goes through the boolean. Bodies shown in another frame, mesh bodies and Intersect (which removes the bodies it misses) are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cosmetic tap markings: look for their cylinder only in nearby bodies A tap marking (a Thread feature, or the marking of a tapped hole) looked for the cylinder at its point in every body of the design, and each search meshes the body afresh: with many bodies and many markings that grew quadratically (200 markings on 200 cylinders: 33 s of a 33 s run). A body is now passed over when its box, from the mesh it keeps and grown past that mesh's deflection, lies farther from the point than the search itself would look (a twentieth of the body's size, plus its own mesh's deflection). The search finds no face there, so the marking and everything else stay the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
96509412e9 |
Extrude: a Cut To the far side of a curved wall takes the wall (#8634)
A Cut To Object on a curved face ran the tool through the target and kept the tool less the body, the piece at the start. From outside a curved wall to its inside face that piece is the air before the wall: the cut took nothing, or the tool lay in the curved face and the boolean failed on the coincident faces. Where the target's material ends at the picked face under the whole profile (rays over the profile cross it once, material before, none after), the tool now ends flat halfway across the gap beyond the face. It takes what lies before the face and meets no face of the body. Otherwise the trim is as before. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e91f5d73bd |
Joints: a typed joint that closes a loop starts where its parts are (#6061) (#10336)
A relation given no values already started from where its parts sit (relation_values), so the part moves no further than it must. A typed joint (planar, revolute, cylindrical ...) added between parts another joint already relates started its values at 0 instead, so the loop solver began far from the current placement and could report a conflict for a joint that agrees with how the parts are placed. Seed every loop-closing joint, typed or relation, from the current placement when no values are given. Joints in an open chain and joints with explicit values keep their initial values as before. |
||
|
|
a735620aa8 |
Circular pattern: the axis origin takes length expressions and follows them (#10852) (#10855)
* claim: #10852 * Circular pattern: the axis origin takes length expressions and follows them (#10852) solid.pattern.circular with axis {origin, dir} kept its origin as plain numbers, so a pattern about a parameter-driven point stayed where the point was when the pattern was made: the seed moved with the parameter and the copies did not. Expressions were refused (#248). The pattern's origin is now a point of expressions, as a Mirror plane's origin is: numbers or length expressions, evaluated on each rebuild, and renamed with their parameters. Plain numbers are saved as numbers, as before. timeline.edit sets pattern.origin as expressions, and editing the pattern in its dialog keeps them. One pending format change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e016c40f43 |
Sketch Intersect: picked faces meet as their whole surface, not the span under the pick (#9009) (#9107)
A surface extruded from a fit-point spline is one B-rep face per span, joined by hidden seams; the viewport picks and highlights it as one face (Mesh::surface_patch). sketch.intersection_curve turned a face pick into the single nearest face, so the curve stopped at that span's ends. project3d::mesh_of now returns the picked face's surface patch and mesh_intersection keeps the triangles of every face in it. Body sources and Project to Surface are unchanged. |
||
|
|
d7a17de3d9 |
Sheet metal: a perforation field bounded by a sketch's outline (#732) (#10754)
`sheet.perforate` takes an optional `sketch` (and `profiles`): the field's outline. Holes go only inside its closed profiles, out of their inner loops, `margin` clear of its edges, on a grid from the outline's lower-left corner. On a folded sheet the outline maps through each panel the sketch lies on and is clipped to it, so it never reaches a flange in the flat; a sketch on no face of the sheet is refused. A mirrored copy mirrors the outline, and an edit that names no sketch (the Perforate dialog has no outline) keeps it. Without a sketch the field covers the whole sheet as before. The feature saves `sketch` and `profiles`, a sheet's perforation its outline in flat coordinates: one pending format note, no migration. Assisted-by: LLM |
||
|
|
a33619fd2b |
Tangent Plane: line it up with a reference plane, at an angle (#8822) (#9002)
* claim: #8822 * Tangent Plane: line it up with a reference plane, at an angle (#8822) `construct.plane.tangent` takes an optional `reference` (a plane or planar face) and `angle`. On a cylinder, cone or torus the plane touches where the face turns towards the reference, turned by the angle about the face's axis, on the side of the pick; on a sphere it is parallel to the reference. The plane follows the reference when it moves. The Tangent Plane dialog gets a Reference plane input and, once one is picked, an Angle field; editing the plane brings both back. Saved as a pending format change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5ad2503503 |
Timeline folders: editing a feature in a folder leaves no stale id, and such designs open again (#9373) (#9393)
* claim: #9373 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Timeline folders: editing a feature in a folder leaves no stale id, and such designs open again (#9373) timeline.redefine runs the edited feature's command on a scratch copy with the marker just after the feature, and takes the new definition from the feature that run makes. Inside a timeline folder that trial feature joined the folder too; it was removed from the feature list but not from the folder, and its id, handed back, was the next feature's. Each edit left that id in the folder (the same one, edit after edit), naming no feature or, once a later feature took it, a feature twice, and the reader then refused the design: "damaged design: invalid feature in a timeline folder". The trial feature now leaves the folders as it leaves the feature list. The reader drops a folder entry naming a feature or joint the design doesn't have, or one a folder already holds, and opens the design: folders only group the timeline, so nothing else is lost. A folder damaged in itself is still refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
6f73bd95c4 |
Assemblies: dragging a part with mirrored copies no longer meshes them on every frame (#10101)
A mirror of placed bodies follows its source, so each frame of a drag evaluated it and made new copies with empty mesh cells. A reflection can't share its source's triangles, so every copy was tessellated from nothing, and so was anything an evaluated feature after the mirror made. Keep the first copy of a body with its source: each later copy is a proper rigid move of it, so a world state draws them from one set of triangles. The model keeps each copy's own lazy mesh. A feature evaluated again that gives the same solids (an import) keeps its meshes. A mirror of placed bodies no longer searches its copies' revolved faces when no later feature can build on bodies. That depends on the design alone and is part of the mirror's placement digest. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3125a79827 |
Release 0.30.0 (#10894)
Bump the workspace to 0.30.0 and fold the pending design format changes into format 47. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
eaf34d2528 |
Construction planes: refuse duplicate feature names (#10707) (#10743)
* claim: #10707 * Fix construction plane name collisions (#10707) |
||
|
|
eac1425661 |
Hole: a face name shared by two bodies is no split face (#10699) (#10872)
* Face names: one name on two bodies is no split face (#10699) A hole placed on the top of one of two bodies an extrude made, or on a piece of a split body, warned "face `F2:top` was split; using the piece nearest the picked point": the face name is on both bodies, and any name found more than once counted as split. A name on several bodies, one face on each, now finds the face the point lies on without the note; pieces on one body, or a point on none of them, still say split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hole: look for a face name shared by two bodies on the body the point is on (#10699) The lookup over every body counts a name on several bodies as split on purpose (a copy's face is not a piece of the source's, see `the_same_face_on_a_copy_is_not_a_piece_of_the_named_bodys_face`), so the first change, which made it unique there, is undone. A hole whose face name is found split while its point lies on one of those faces now looks again on that body alone: one face of the name there is the face, whole; pieces on that body still say split. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
cfc46448a8 |
Joints: an edge origin shares the joint faces' measurement (#10866)
`face_of` called `Body::faces`, which tessellates the whole body, on every solve. Face origins went through the cached `ModelBody::joint_faces`, at the same tolerance, but edge origins never did, so one edge origin on a large body cost a drag or a Drive Joints step far more than the solve. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
446590dca1 |
fix(engine): report design variant in timeline.inspect and accept in timeline.add (#10748) (#10792)
* fix(engine): report design variant in timeline.inspect and accept in timeline.add (#10748) * ci: refresh checks to clear cancelled workflow run |
||
|
|
0ad74f2577 |
Revolve: keep the angle grab point when the sweep is zero (#9601) (#9602)
A revolve of 0° still builds no solid. The handle stays on the profile so the angle can be dragged back into range. |
||
|
|
89cb651084 |
Warnings: quote a long generated name by its start and end (#8673)
A chamfer's warning quoted the full generated names of fourteen split edges, 122,701 characters (#8541). Names over about 80 characters are now quoted by their first 48 and last 24 characters. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3017641b41 |
fix: Projection d'une silhouette à congés coniques fermée en un profil (#10773)
- snap_tangent_arcs : chaque extrémité d'arc prend la ligne la plus proche - une ligne ne prend qu'une extrémité d'un arc ; l'autre passe à sa ligne suivante - une ligne plus loin sur le contour est ignorée quand l'extrémité touche déjà une autre pièce - tests : 4 unitaires (project_order.rs) et the_silhouette_of_conic_rounds_closes Co-authored-by: P'tite Tête <asso@ptitetete.com> |