mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 20:41:50 +00:00
develop
60 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
38ca98f88b |
Meshing curved faces: evaluate spline basis tables in place (#11648) (#11649)
Every point on a spline surface returned its basis tables by value, up to 272 bytes several times over. Filling them in place takes about a quarter off meshing a round extrude's wall, with the same numbers. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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> |
||
|
|
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 ( |
||
|
|
5e0e168ae9 |
Properties: moment of inertia, principal axes and first moments of a part or assembly (#8095) (#8103)
* claim: #8095 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Properties: moment of inertia, principal axes and first moments of a part or assembly (#8095) component.properties and occurrence.properties report each closed body's inertia tensor about its centre of mass, and in total the tensor about the total centre of mass (world axes, g·mm²), its principal moments and axes, the first moments, and with `about: [x, y, z]` the tensor about any point. The tensor comes from each body's welded export mesh, tessellated once per body, scaled to the measured mass and turned by each placement; bodies add by parallel axes. The maths is a new crates/geom inertia module. The Properties window shows the first moments, the tensor, the principal moments and axes, and the view marks the centre of mass with the principal axes while it is open. Inspect → Measure → Properties now opens that window (it ran the command and showed nothing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Properties: Copy matrix, and the whole design's card titled just Properties (#8095) Copy matrix, under the tensor, puts it on the clipboard as three lines of tab-separated values at full precision, in the units shown. The card for the whole design is titled Properties; one opened on a component, placement or bodies keeps its name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Properties: measure beside the window, so a large assembly doesn't freeze it (#8095) Properties ran on the window thread: 18 s on a 144-body design, with the window answering nothing meanwhile (#5203). In the desktop app the card now measures on a copy of the session on a worker, the way #4614 made agents' measures work, says "Measuring…" until the reply is in, and Close drops it. A quick measure still shows at once; headless, tests and the web measure in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8e6a107f6e |
Lists many PRs add to: no written lengths, one file per theme (#7747) (#7898)
* Lists many PRs add to: no written lengths, one file per theme (#7747) * xtask lists: the fixed-table allowlist in order, each with its own reason * Lists: the wide-character blocks and the drawing scale presets as slices too (#7747) * Lists: the drawing units test reads UNITS as a slice (#7747) |
||
|
|
9c5b3beb73 |
geom: cover inside_loop and segments_meet in the fill predicates (#6162)
Add unit tests for fill.rs's two ray-casting predicates, which fill_general.rs relies on to decide which parts of a surface a loop covers: a point inside/outside a square and an L-shaped (non-convex) loop, documenting how inside_loop resolves points on an edge or vertex; and segments that cross, stay apart, touch at an endpoint, or are collinear with and without overlap for segments_meet. Closes #38 Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
14acdc2a6d |
Render: one sRGB transfer function (#7161) (#7162)
* Render: one sRGB transfer function, geom::srgb, for the hand-written copies (#7161) Twelve places wrote the sRGB encode or decode formula out by hand (doc's glass recipe, engine's render library, glTF import, the PNG film, photographic and presentation output, the ray tracer's image, the stage, the surface decoder, texture normalisation and the theme's contrast check). They now call solidcraft_geom::srgb: encode/decode on 0..1 values, encode8/decode8 for 8-bit codes. render::surface::srgb8_to_linear stays as a re-export, so its callers are unchanged. Same results: the module's tests compare it bit for bit with the formula as the callers wrote it, over all 256 codes and a dense sweep of linear values plus the knees, out-of-range values, infinities and NaN, for each way the callers clamped (after encoding, before it, or not at all), and decoded values either side of the decode knee, where no 8-bit code falls. One change: glTF import encoded in f32. Over every f32 in 0..1, 218 of 1,065,353,217 values sit on a rounding tie that f32 got wrong and now give the neighbouring code (f64 gives the nearer one). Left as they are: render/job/image.rs (replaced by exact tables in its own change), the tests and the starter-pack validator that keep their own copy as an independent reference, and the WGSL shaders (they can't call Rust). Fixes #7161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tests: the sRGB codes of five callers that had no test of their output (#7161) The glass swatch, a library surface applied as a plain appearance, glTF's fallback for a colour channel that isn't a number, a stage panel and an empty pixel in presentation output each get an assertion with their sRGB codes worked out by hand. A planted wrong conversion at each is now caught by that test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
9bcb2ffe68 |
Drawings: first landing from the drawings branch (#8249)
* Drawing sheets: sizes, the ISO 5457 border and its marks (#3875) (#5108) * Drawing sheets: sizes and the ISO 5457 border (#3875) A sheet is a paper size and an orientation: ISO A0 to A5, Letter, Legal, ANSI A to F, architectural A to E1, or a custom size (finite, above 0 and at most 5,000 mm a side), landscape by default and either way for every size. A border profile is the rule for its border, and layout() works it out for a sheet: the frame inside the margins, and the reference fields with their labels. The ISO profile has a 20 mm filing margin on the left and 10 mm elsewhere, a 0.7 mm frame, fields of 50 mm out from the centre lines with the corner fields taking what's left, rows lettered from the top without I and O (AA, AB and on past Z), columns numbered from the left, and an A4 sheet labelled on its top and right only. A profile can divide the frame equally instead, or have no grid. The records sit in geom, next to the section records, so saving, rendering and export can all use them. Nothing is saved or drawn yet: there is no command and no format change. Margins that leave no drawing space, values that aren't finite, fields outside 1 to 1000 mm, more than 200 equal fields and unknown JSON fields are refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing sheets: ISO 5457's centring and trimming marks (#3875) A drawing border profile now also holds the grid reference border and the centring and trimming marks, either of which can be left out. The ISO profile follows ISO 5457:1999 (read in its identical adoption, IS 10711:2001): - the grid reference border 5 mm outside the drawing border on every side, with the field labels between the two; - four centring marks on the sheet's centre lines, from the grid reference border to 10 mm inside the drawing border; - at each corner a trimming mark of two overlapping 10 x 5 mm rectangles, in the margin outside the grid reference border. The labelled sides are now a rule worked out for each sheet: ISO 5457's (the top and right of A4, all four sides of the others) or fixed sides. So one profile labels every size correctly. validate() checks what doesn't depend on the sheet, for when a profile is read; layout() calls it, then checks the rest against the sheet. Refused: - a grid reference border that isn't finite, is negative or is wider than a margin; - a centring mark that isn't above 0, would reach past the sheet's centre lines, or is on a drawing border those lines miss; - a trimming mark that isn't above 0, is wider than it is long, is wider than the margin left outside the grid reference border, or is longer than half the sheet's shorter side. The grid rule's JSON is now externally tagged ("none", {"from_centre": {"field_mm": 50}}), as the sheet size is, so an unknown field is refused in every form; the internally tagged "none" ignored one. Nothing is saved yet, so the shape change costs nothing. The names follow the glossary, and ISO 5457's "drawing sheets": the module is drawing_sheet (was sheet, which means sheet metal elsewhere here), DrawingBorderProfile and SheetLayout (were BorderProfile and BorderLayout), sheet_border and drawing_border (were paper and frame), and Sheet::size_mm (was paper_mm). The first part of S1 is still a draft, so the renames are made here rather than in a commit of their own. The extra centring marks ISO 5457 asks for on sheets larger than A0 aren't here yet. Still nothing is saved or drawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing sheets: a lighter built-in border, the filing margin a choice (#3875) Cameron's decisions after seeing the sheet in the app, 2026-10-23: - No trimming marks in the built-in profile: the filled corner blocks take a lot of ink. trimming_mark_mm stays, so a profile can still ask for them. - Centring marks stop at the drawing border: from the grid reference border to the drawing border, never into the drawing area (on A4 they crossed the tables). centring_mark_mm keeps its meaning, how far a mark reaches inside the drawing border, and now takes 0, the built-in value, as grid_border_mm already takes 0 for "on the drawing border". A mark of no length (a reach of 0 with no grid reference border) is refused. - Margins of 10 mm all round. ISO 5457's 20 mm filing margin on the left, for drawings that get bound, is a named choice: filing_margin, off when not given. On, it stands in for margins.left_mm, which is kept, and still checked, for when it's off again. The 20 mm is FILING_MARGIN_MM, and validate() and layout() both take the margins from one helper, laid_out_margins(). A3 landscape is now 400 x 277 at (10, 10), with eight 50 mm columns. With the filing margin it is 390 x 277 at (20, 10) again, the same as a 20 mm left margin set by hand. A profile with all three choices on (ISO 5457's full border) lays out exactly as the built-in profile did before, and the tests keep the old hand values for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing W23a workshop plate oracle fixtures (#4950) (#4962) * test(oracle): reviewer-aligned parameterized workshop plate fixtures (#4950) Independent analytic W23 expected values and seven verified models, including four R10 corners and the 2 mm tangent top chamfer. Document accepted Drawing states and commit regenerated docs/oracle.md from successful workflow run #29 (90/90 pass). * fix(oracle): correct W23 chamfer centroid and clarify drawing session ownership * Paper scene: plain records of what is drawn on a sheet (#4890 A1) (#5513) geom::paper holds what a sheet shows as plain data: groups of strokes and fills, each group tagged with the object that drew it (an opaque id) and its kind, with one validation pass over a bounded record count. Seg2's checks and the coordinate rule move into profile.rs so section and paper share them, and drawing_sheet takes its side and line limits from paper, so each limit is written once. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing sheets: centring marks across the whole margin (#3875) (#5554) The built-in sheet's centring marks now run from the trimmed sheet's edge to the drawing border. From the grid reference border, as before, a mark was the same length as a grid field mark, and on A3 it sat on one (the sheet's centre falls on a field edge), so it no longer read as a mark once the grid reference border's outline is left out (Cameron, after the prototype on #3875). - DrawingBorderProfile.centring_mark_start: sheet_edge (the built-in's, and when not given) or grid_border (ISO 5457's figure 5); ISO's full border in the tests asks for grid_border. - A mark still needs a length: one that stops on the drawing border needs room outside it on the side it starts from, every margin for sheet_edge, the grid reference border for grid_border. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing sheets: no centring or trimming mark longer than the largest sheet's side (#3875) (#6410) A border profile capped every length at 5000 mm (the largest sheet's side) but two: a centring mark's reach inside the drawing border and a trimming mark's length. A file could give either a huge whole number, such as 18446744073709551615. The read's number check passes whole numbers, so the profile took it as 1.8e19 mm, and saving wrote it as a decimal the next read refused: a drawing that saved but didn't open again. K2's fuzz test of drawing files (#3890) found it. Both now take the cap the margins have, `MAX_SIDE_MM`, in `validate`, so the file is refused when it's read. Layout's own checks against the sheet are unchanged, so a 5000 mm mark still validates and is refused only on a sheet too small for it. Tests: the hostile-profile test refuses 5000.000001 mm and 1.8e19 mm for each mark, and takes 5000 mm on its own; 4 planted faults (each cap removed or made one short) are all caught. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Bill of materials types: phantom, reference, purchased and inseparable (#3874) (#5107) * Bill of materials types: phantom, reference, purchased and inseparable (#3874) A component's bill of materials type is a part property (`bom_type` in its metadata, set with component.metadata.set and read by component.metadata, as part_number is), so older releases keep it and no format change is needed. It isn't a design property: the Design Properties dialog doesn't list it and document.metadata.set refuses it. A word this build doesn't know reads as normal and is kept. assembly.bom counts by type: a phantom has no row and what it holds counts where it is placed, in the same rows as the same parts there; a reference counts nowhere, and nothing inside it does; purchased and inseparable components are one row, and in parts only an inseparable component's purchased parts are listed too. Rows of one part number but different types are different rows. Rows and children carry bom_type. With no types set the rows are as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Glossary: bill of materials and bill of materials type (#3874) The bill of materials types are a new idea, so the glossary gets their term, with the five types, and "normal" gets its fourth meaning in the table of words with more than one meaning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bill of materials types: a plain line for each (#3874) The guide's table and the glossary entry say what each type is for in plain words: something you make, something just to group, something to use as a reference, something you buy, something made as one piece. The names stay the five standard ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bill of materials: kits fold together only when their contents' types match (#3874) With group_children, two kits with one part number share a row when what they hold reads the same. That reading (kit_key) compared each child by part number and count, but not by its bill of materials type, though rows themselves are told apart by type. So a kit holding a made bolt and a kit holding a bought one, both KIT, folded into one KIT x2 row listing the first kit's bolt twice: the other type was lost, and which one depended on creation order. 0xlillith found it in review of #5107. kit_key now includes each child's type. Equal part number, type and contents still fold. Test: two KIT kits each holding a BOLT fold into one row of two; once one bolt is purchased they are two rows, each listing its own bolt. Planted fault (the type left out of the key): caught. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing files: a drawing saved as a container or as plain JSON, never read as a design (#3890) (#7098) * File format families: drawings share the design format's versioning (#3890) The format code works for a family of files: its own tag, its own numbers and its own folder of change files, with one reading of a file's format, one refusal of a file a newer build saved and one upgrade for every family. Designs (solidcraft/N) are one family and keep everything they had: FORMAT and FORMATS are as they were, and pending_names, format_string, version_of and read mean what they did. Drawings (solidcraft-drawing/N) are the other, with format 1 as their first record and each later change one file in crates/doc/src/drawing/format/pending/, as design changes are. Nothing saved changes. A design tag never reads as a drawing's or the other way round, and a file that names no format is a design from before format strings, never a drawing. Every refusal names its family; a design's read word for word as before. A design listing a pending change this build doesn't have is now refused as a newer build's before its content is checked, as a newer number already was, so a newer build's fields never read as damage. The list reader is take_names(v, key, noun), the shape #4811 uses for its own list. For the maintainer, the release step changes: scripts/fold-format-pending.sh now folds every family that crates/doc/build.rs lists (FAMILIES), each into its own next number: designs into crates/doc/src/format/v<N>/ as before, drawings into crates/doc/src/drawing/format/v<N>/ from 2. With nothing pending in a family it says so and moves nothing. Its test folds a drawing change into v2 beside the design changes. docs/releasing.md, CONTRIBUTING.md and AGENTS.md say so. build.rs lists each family's changes from its own folder, each folder's versions contiguous from its own base. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing record: format 1 of the drawing family (#3890) A drawing is now a record of its own (crates/doc/src/drawing.rs): its name, the designs it draws (sources: an id and the design file's path, relative to the drawing's folder as a linked design's is), its properties under the design metadata's rules, its units (each design's own, or one length unit of the expression unit table), its content language (a BCP 47 tag) and its sheets. A sheet holds its paper, a copy of its drawing border's profile, the resolution its pictures are rendered at (300 dpi unless it says, 25 to 2400), the component it documents and whether it is left out of the sheet count or of printing. This is format 1 of the drawing family; each later kind of record adds one file in crates/doc/src/drawing/format/pending/. The rules: one id counter for every object, raised above the largest id when a file is read and never rewound; every reference is an id that must resolve, or the file is damaged and refused, never patched; 1 to 1,000 sheets, at most 64 sources, names of 1 to 256 bytes, paths of 1 to 4,096 bytes without NUL. The same drawing always saves as the same bytes. Drawing::from_json reads at most 16 MiB of text, then runs the family's format check, the number check, the upgrade, the typed record (unknown fields refused everywhere) and Drawing::validate. The number check is now one function every family calls, Family::check_numbers, told which field holds other tools' data; a design's refusals read as before. Nothing saves or opens a drawing yet: the container is the next step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing ids: random and collision-free from format 1 (#3890) A drawing's objects now take random ids, the form jyviko chose on #3890 from #1017: no counter, so objects added on two branches never share an id and never edit one line. The allocator is one module for drawings now and designs later (#626 Phase 3), crates/doc/src/ids.rs: an IdSource gives nonzero u64s by splitmix64 from a seed, each checked against the ids in use and drawn again when it is one, or when it is 0. A run never draws a number twice, so at most one draw per id in use comes before a free one, and a seeded run over a file the same seed made still gets a new id. IdSource::seeded gives the same ids for tests and replays; IdSource::from_entropy seeds a session from the standard library's hash keys, the time and a count, with no new dependency. In a file an id is 13 characters of Crockford base32, lowercase (0000000000001 to fzzzzzzzzzzzz): ids::encode writes it and ids::decode reads only that spelling, case-sensitive, so an id has one form to search for. Anything else, 0 included, makes the drawing damaged. The drawing loses next_id (a file that holds one is refused), and Drawing::new and allocate_id take the IdSource. Messages name ids as the file writes them. The component a sheet documents stays its design's own number until designs move over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing record: a drawing too large to reopen isn't saved (#3890) from_json reads at most MAX_DRAWING_BYTES (16 MiB), but to_json had no limit. Other tools' properties are kept as they came, and indenting them can grow a drawing read in at about 4 MB past 16 MiB (two million zeros in one array), so the next read refused what the save had just written. 0xlillith found it in review of #3891. to_json now returns a Result and refuses JSON longer than a read takes ("drawing too large to save (at most 16 MiB)"), so every drawing saved opens again. Nothing writes drawing files yet, so the error reaches the future file writer before it publishes anything. Test: that case is refused, and a smaller compact drawing saves and reopens unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing files: a drawing saved as a container or as plain JSON, never read as a design (#3890) K3 of the drawing storage plan (W11.2): the file a drawing is saved in. Nothing writes one from a command yet; opening and saving come with K5. - A drawing container is a design container holding `drawing.json` (Deflate, as `document.json`) in place of `document.json`, and `container.json` says so: {"content": "drawing", "format": "solidcraft-container/1"}. A design's `container.json` is as it was, with no `content`. The plain JSON form is the drawing's own text. Both give the same bytes every save, so a drawing can live in version control as text. - `io::file_kind` tells a design from a drawing by `container.json` or by the JSON's format. The design reader refuses a drawing ("this file is a SolidCraft drawing, not a design: open it as a drawing") and the drawing reader a design; plain JSON only asks when it isn't a design, so reading a design costs nothing more. A container's content this build doesn't know, or a drawing container that also holds a design's `document.json`, is refused. - `read_drawing` (bytes, either form), `read_drawing_file` (one seekable handle, the design reader's checks) and `write_drawing_container`; `DRAWING_EXT` (`scdwg`) and `is_drawing_path` beside the design's. - The container writer's set-up and finish, the file opening and the container sniffing move into one function each, which designs and drawings both use. The container's own checks (budgets, entries, the ZIP directory) are the design container's, word for word. Tests: a drawing round-trips through both forms byte for byte, and the container holds exactly `container.json` and `drawing.json`; `file_kind` on both forms of both kinds; each reader refuses the other's file; unknown content, a stray `document.json`, a missing drawing, an unknown container version and a damaged drawing refused; both forms from disk, and a directory refused; mutation fuzz of both forms (an error or a drawing, never a panic). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Paper scene: SVG of a sheet, its lines, arcs, cubics and fills (#4890 A3a) (#7032) The first export of a paper scene, as SVG first (jyviko and 0xlillith on #4890), from the research in the plan's §16. A3b adds ellipses, hyperbolas and Béziers above degree 3, which this refuses as "not supported yet". - `geom::paper::Prepared`: writers read a scene through `Scene::prepare`, which checks it first, so none reads one unchecked. Clipping goes there too, once (A8). - `geom::paper::arc_pieces`: an arc's ends in pieces of at most a quarter turn, the last its own end. A half turn drawn from its ends is ill-conditioned: rounded to the nanometre (six decimals of a millimetre) its centre moves up to 1e-2 mm, where a quarter turn's stays under 1e-6 mm. - `io::paper::paper_svg`: one user unit a millimetre, the size in mm on the root; y flipped by writing the height less y, so the file's numbers are page coordinates and an anticlockwise arc has sweep flag 0; lines `L`, arcs `A` (never large-arc), cubics `C`, parabolas `Q`; fills black and even-odd; one path per item; groups as `data-object` and `data-kind` (object ids can't be XML ids). No background, so cutters see only the marks. - Numbers are whole nanometres (six decimals of a millimetre): no exponent, no "-0", the same bytes every time. They are the writer's own state too, so a joint is written once and a closed path ends exactly on its first point. Numbers are written straight onto the document, with no text made per number. A width under half a nanometre is written as one, and a piece too short for the grid is merged into the next, both counted in the report; a path's last piece has no next, so it is kept however short (unless it has no length). Output over 64 MiB is refused. Tests: the A3 drawing border's whole document; quarter, 270°, clockwise and whole-turn arcs, and a circle of two halves giving the same four quarters; the quarter-turn boundary both sides; cubics and parabolas, and the conics refused; the number format; a closed path whose start and end round apart; a sliver merged; an open path's short last line and arc kept, and one of no length dropped; read back through our own SVG reader (lines exactly, the arc as an arc); the scene's JSON mutated into the writer (never a panic). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing W17a: view definitions as data (#4816) (#4817) * claim: #4816 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing views as data: named views, scales and the map onto paper (#4816, W17a P1) What a drawing view is, before anything draws it (geom::drawing_view): - NamedView: the six principal views and the isometric views from the four upper corners, each an exact basis (a Plane whose normal points back at the viewer), never taken from a camera, whose pitch clamp would tilt a top view, with no -0 to write out; `iso` reads as the front-right one. - basis_looking: any other view from its direction, an up and an origin (paper x = direction x up, paper y = x x direction), refusing a zero direction, an up along it, or an origin that isn't finite or within 1e12 mm. W07's {direction, up, origin} view reads through it. - Scale: paper : model as two positive numbers, read and written as "1:2", from 1:100000 to 1000:1; Scale::fit picks the largest ISO 5455 preset (50:1 to 1:10000) at which a view fits a space (W07's "fits the page" default), compared as size x paper <= room x model so an exact fit at a reduction isn't lost to rounding. - ViewFrame: a view's basis, scale and place on paper, the point of its plane at `centre` landing at `position_mm`. Its paper map moves the view's curves and their accuracy onto paper (paper error = scale x model error); `boundary` maps a box of its plane onto paper from its four corners. In geom, where Seg2 lives: - Similarity2 and Seg2::similar (new): scale, turn and shift a segment exactly, an arc staying an arc. Similarity2::new refuses a scale that isn't above 0 and works the turn out once, not per point; - Seg2::map_controls: a segment's control points mapped by any function. It was doc's sheet::map_controls; its four callers in doc use the method now, unchanged. CurveAccuracy::scaled gives a curve's accuracy at another scale. Tests: each named view's basis and viewer side, and the same bases from a direction and up; a 50 mm cube in every principal view at 1:1 and 1:2, and in isometric (edges 50 x sqrt(2/3), outline 50 x sqrt(2)); the view's centre on its position (W07's 100 x 20 front view on A3 at 2:1); scales read, written and refused; the fitted scale, an exact fit at 1:5 and ISO's smallest; no -0 in any named view's axes; an origin refused by its own message; hostile bases, centres and positions; every segment kind moved by a similarity, and the similarities refused; scaled accuracy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drawing views: the bottom view pinned, square near parallel, bounded scales and boxes (#4816) From DevonHope's review of #4817: - The tests pin all six principal views exactly. The direction-and-up test uses literal ups, so a bottom view turned half round now fails it. - basis_looking squares x to the direction before normalising. Near the 1e-9 threshold the normal was up to 8.8e-9 rad off; now it is about 1e-16. - Scale::fit fits nothing for a size or room that is negative or not finite. A size of 0 still sets no limit that way. - Each side of a scale is 1e-6 to 1e6, so a scale writes at most 49 characters. - ViewFrame::boundary returns a Result. It refuses a box that is inside out or not finite on paper, instead of dropping a corner. - paper_map, boundary and seg_to_paper say which coordinates they take. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing W18 S2: title blocks as data (#3875) (#3877) * Title blocks: what a cell shows and where its text goes (#3875) geom::cell holds a title-block cell's content: parts of text and fields (component, drawing, sheet, revision or prompt, by key, with an optional unit and decimals kept as written for the resolver to check), at most 64 parts and 4096 bytes of text, with no control character but a line break. Beside it are a resolved value (live, overridden and drifted, or missing, with "-" for each part that couldn't be read and why), an override with the value it replaced, and the projection a drawing's views use (first or third angle), which the title block's symbol will read. A fit warns and never cuts: a count of characters, not bytes, with its own count for text in Chinese, Japanese, Korean or Yi characters. The cell text rule places a label and a value: the label's baseline 1 mm plus its cap height below the cell's top, the value box under it inset by 1 mm, one line centred in the box, and wrapped or several lines down from its top at the font's pitch, with a warning when the last falls below the box. Wrapping by width, width warnings and glyphs a font lacks need the text measured, so they wait for the paper scene's text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Title blocks: definitions and their place on a sheet (#3875) geom::title_block holds a title block's definition: its size (1 to 1000 mm a side), its outline and cell line widths, and up to 64 cells. Each cell has a snake-case name of its own, a rectangle measured from the block's lower-right corner, an optional one-line label with its cap height, and a value: text from cell content, with its cap height, alignment and fit, or the projection symbol, which reads the drawing's projection and is never fixed in the block. validate() checks a definition on its own, as when one is read: cells inside the block and not overlapping (touching is fine), label and value cap heights that fit, and the content's own limits. layout() puts the block on the drawing border's lower-right corner of a sheet, refusing one that doesn't fit, and gives each cell's rectangle, its label's place and its value box by the cell text rule. On A3 landscape a 180 x 54 mm block runs from (230, 10) to (410, 64); it fits A5 landscape and every larger sheet, but not A5 upright. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cell text: a value box lies within the paper's coordinates (#3875) place_lines took any finite, ordered value box, but centring one far out (min x 1e308, max x 1.1e308) overflows: the anchor came back infinite, with no warning. 0xlillith found it in review of #3877. The paper scene already bounds every coordinate it holds by MAX_COORDINATE_MM (twenty times the longest sheet side). That limit is now public, and place_lines keeps its box inside it, so its anchor is always finite: one rule for what a paper coordinate may be. Tests: the hostile-box test refuses both overflow cases from the review and a box just past the limit on each side, and places lines in a box the size of the whole paper limit. The fuzz test's numbers now include values past the limit (1.1e5, ±1e308). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Title blocks: padding from the lines' inner edges, one label rule, nothing unread (#3875) A cell's text is padded from the inner edge of its lines: each side of a cell moves in by half the line drawn on it (the outline's on the block's edges, a cell line's inside), so a wide outline never covers the text. Before, the padding ran from the line's centre. The label's one-line rule lives with the cell text rules (`cell::check_label`), where parts lists will find it. The cell types nothing read yet (`Override`, `Resolved`, `State`, `Problem`, `Reason` and `MISSING`) wait for the resolver that will read them. The tests check the exact thresholds inside the lines (a label that leaves no room, a value that just fits), pick each of the lines, alignment and wrapping on their own so every combination comes up, and lay fuzzed blocks out on every standard sheet, either way, with and without the filing margin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Title blocks: the drawing border's line, descenders, room where the block sits (#3875) From DevonHope's review of #3877: - Each cell's lines are clamped to the inner edge of the drawing border's line. That line runs along the block's right and bottom edges, and along any other edge it reaches, as when the block fills the border. Aabb2::intersection, the 2D twin of Aabb3's, does the clamping. - layout checks each cell's room again where the block sits (place_cells, which validate runs at the origin), so rounding at the real corner can't turn a cell inside out. - The cell text rule counts descenders, at 3/10 of the cap height (ISO 3098's lettering B). They may reach into the padding under a value box, never past it to a line. place_lines warns Height when they would, and validate asks it whether one line fits. - The module doc no longer links to a private function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * Drawing W06a: exact orthographic hidden lines for the analytic subset (#2455) (#2457) * Drawing W06a: exact orthographic hidden lines for the analytic subset (#2455) crates/geom/src/hlr (L0): prepare once per body, then hidden_lines for a scene of placed bodies. Edges and face silhouettes are projected from their exact curves, split where projections cross, and each piece is classified by rays toward the viewer against the trimmed faces: visible, hidden or explicitly uncertain, never guessed. Planes, cylinders, cones, spheres and tori are supported whether stored as analytic records, revolutions or spline patches; freeform faces are reported and make what they could hide uncertain. Screen geometry is Seg2 with the W01a accuracy vocabulary: exact lines and conic arcs where the projection is exact, bounded chords otherwise. docs/drawing-hlr.md holds the contract and its limits. * geom/hlr: handle the brep records main gained in #3093 Main's exact model storage (#3093) added Curve::SurfaceIntersection, Surface::PlaneThrough and Surface::PlacedRevolution, and the HLR matches on the records no longer compiled. A plane through three points is a plane: it is trimmed and intersected as one, and a box stored that way draws the same bits as one with axis-stored planes. The procedural intersection curve and the placed revolution (whose affine placement may scale it unevenly) are refused by prepare as not supported yet, until the kernel adapter handles them. The records' positions move with the body's centre like the others. * geom/hlr: pad plane faces' boxes, so a plane record off by rounding still hides Plane faces took the box of their boundary chords unpadded, flat along the normal. When the plane record and its edges disagree by rounding (a unit cube's top plane one ulp above its edges, a 100 mm cube's 1e-11), the ray toward the viewer reached the plane just outside that box, the slab test culled the hit, and the back edges came out Visible with no doubt. Every face's box is now padded by 1e-6 of its size, as curved faces' boxes already were. Reported by DevonHope on #2457. * geom/hlr: limits from review: trim samples, scene extent, crossing pairs, surface groups From DevonHope's review of #2457: - Options::trim_samples must be 1 to MAX_TRIM_SAMPLES (4096). - Placed bodies must stay within MAX_COORDINATE (1e12 mm, as section and brep bound stored numbers). Placements 1e200 apart made the scene's size infinite and every tolerance taken from it with it, so everything came out Visible. - The crossing search counts its candidate pairs before it starts and meets each pair only in the first grid cell the two segments share, instead of keeping a set of the pairs met, which could outgrow wasm's 4 GB before the 500 million pair budget tripped. The same pairs in the same order. - prepare groups faces on one quadric or torus by comparing each face with the faces of its kind whose size agrees to the tolerance (sorted), not with every face; at most 50 million comparisons. prepare_cancellable polls a cancel callback between edges, faces and comparisons. * geom/hlr: boundary chords on a grid, and no fine pass over coincident curved chords The cost items of DevonHope's review of #2457: - Every ray hit asked how far it lies from its face's boundary by scanning all of the face's chords (4096 per circle; a plate's face with 256 holes has about a million). The chords are now bucketed on a uniform grid over the boundary's box, cells at least as long as the longest chord, and the test looks only at the cells around the point. It finds exactly the chords the scan finds. - Chords of two curved curves that lie on each other end to end (aligned holes seen along their axes) were cut into 16 by 16 pieces and intersected, to find crossings that are only noise within the curves' tolerance. They are now skipped; visibility changes where a coincident run starts or ends, and chords that only partly overlap still find those. - Fixtures: a plate with holes, coaxial cylinders seen along their axis, and 300 placements. * geom/hlr: pad boxes by the body's coordinates too; coincident chords only to rounding Review of the previous commits found two holes: - The padding grew with the face's size, but rounding grows with where the body was modelled: a 0.001 mm cube 1e7 mm out, its top plane one step (1.9e-9 mm) above its edges, still had its hit culled. The pad now adds 1e-12 of the body's offset. - Two chords were taken for coincident when their ends lay within each other's sag, which in a coarse scene can hide real crossings: an arc can bulge across a line 1 mm off its chord twice. Only chords that coincide to rounding are skipped now; identical projections, the costly case, still are. * geom/hlr: coincident curves meeting at shared sample points do not cross Timing the review's scenes showed that skipping coincident chords was not enough: chords of two coincident curves also touch end to end at every sample point they share, and each touch was a crossing to refine and split at. 64 coaxial cylinders seen along their axis gave 8.3 million crossings (19 s), 64 cylinders one behind another along the line of sight 4.2 million (214 s; before these commits the crossing budget refused it). A touch at a shared sample point is now dropped when the two curves also coincide, to rounding, on both sides of it. Where a coincident run starts or ends one side differs, and the touch is kept. * geom/hlr: the own-face margin allows for rounding where a body was modelled The far 0.001 mm cube of the previous commit showed a second effect of records rounded at large coordinates: its top plane, one ulp (1.9e-9 mm) above its edges, hid the top edges themselves, because a ray leaving an edge met its own face 3.2e-9 mm along, past the own-face margin of 1e-6 of the face (1.4e-9). The margin now also covers 1e-14 of the body's offset (about 45 ulps of its coordinates), which only matters for parts far smaller than their distance from the origin. * geom/hlr: no parity test for a hit on the boundary, where it is uncertain anyway * docs/drawing-hlr: the fixtures of the review's second round --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> --------- Co-authored-by: CamWight <91156642+CamWight@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Benny1496 <163034806+Benny1496@users.noreply.github.com> Co-authored-by: Mateusz Sych <151755914+SychPL@users.noreply.github.com> Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> |
||
|
|
b2525ed34e |
Tests: the six timing assertions do not depend on the wall clock (#6917) (#6925)
* claim: #6917 * Tests: the six timing assertions do not depend on the wall clock (#6917) Six tests asserted on the wall clock. Their result depended on how loaded the machine was and not only on the code under test: they passed on an idle machine and failed when the test binaries ran beside each other, which is how the suite runs (`cargo xtask test` starts every test binary at once). CONTRIBUTING.md:196 says it: "Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing." The change is confined to the six test files the issue names: - geom: the tangent chain of a helix is a ratio (750 pieces against 3 000) instead of a limit of 200 ms. - io: the placement budget is spent inside the reader, so the clock is gone. - ui-egui: the classification of many faces is a ratio (900 against 3 600) instead of a limit of 10 s. - engine: the clearance measurement drops its 30 s limit; the boolean that gives up reports itself. - engine: the fillet beside a modelled thread is a ratio (a 1 mm thread against an 8 mm one) instead of a limit of 30 s. - ui-egui: the window frame budget goes from 250 ms to 20 s, the limit #6072 gave its own probe. The helper is the one #649 wrote, copied into the crates that need it, with the noise of a run as a parameter. |
||
|
|
699f526429 |
Show imported mesh edges in shaded and wireframe views (#5662) (#5740)
* claim: #5662 * Show imported mesh outlines in viewport (#5662) * test: update mesh regression origin visibility setup |
||
|
|
75261cd797 |
Viewport: order-independent transparency for see-through surfaces (#4734) (#4903)
* Viewport: levels of detail for large meshes (#4724) A large instanced mesh (20 k triangles or more) gets coarser copies, made once on a worker (`mesh_lod`): a quarter, a sixteenth and a sixty-fourth of its triangles, by quadric edge collapse (`geom::simplify`, after Garland and Heckbert 1997; no fold-overs, no sheets joined, open boundaries kept), each with its measured deviation from the full mesh. Each frame, as placements are culled, each is drawn from the coarsest copy whose deviation stays under 0.75 px where it stands (`placement_scale`, `level_for`). Until the copies are made the mesh is drawn in full; the scene is built again when they are. Display only: picking, measuring and exports keep the reference mesh. Outside the kernel. 25 placements of a 2 M-triangle STL, 3600 x 2190 px, 4x MSAA, M2 Max (`instanced_scene_gpu_benchmark`): 20.2 -> 1.2 ms a frame with all in view, 21.9 -> 2.9 ms close up. The copies take 10 s to make, on a worker. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Viewport: see-through and faded parts drawn as instances (#4888) See-through (an appearance's opacity) and faded (context around an active component) bodies in one colour are drawn as instances like opaque ones, their alpha in each placement's colour: one copy of the triangles, levels of detail and culling, no per-placement copies built on the window thread. Their placements are ordered back to front each frame (not their triangles one by one) for devices that sort see-through surfaces. The GPU benchmark gains `SOLIDCRAFT_BENCH_OPACITY` and the CPU share of each frame, and its pictures each placement's own triangles too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Viewport: order-independent transparency for see-through surfaces (#4734) The model's see-through surfaces (glass, faded context; instanced or not) were sorted back to front triangle by triangle on the window thread whenever the view turned. They are now drawn with weighted blended order- independent transparency (McGuire and Bavoil 2013): when there are any, the frame is drawn into the renderer's own textures (as supersampling does), the see-through surfaces gathered into a weighted colour and a revealage target against the frame's depth, laid over the frame in one full-screen pass, then the overlays drawn over them. Nothing is sorted. A device that cannot blend into float targets keeps the sorted way. 25 placements of a 2 M-triangle STL at opacity 0.5, M2 Max, 3600 x 2190 px: about 10 s a frame sorted, 21.6 ms (17.2 ms instanced, 2.8 ms with levels of detail). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
93a2922c5e |
Display plans and the model cache for large STEP assemblies (#2814) (#4021)
* STEP import: mesh band faces that wrap around a closed surface correctly Fixes two related bugs that caused SolidCraft to fail to mesh a face wrapping all the way around a closed, non-periodic surface (a revolved ridge band, or a tube-shaped B-spline surface): a missing face, a folded/self-overlapping triangulation, or a hole in the rendered body. 1. Ring edges that run slightly off their surface. STEP files write a surface and the curves bounding it to the file's own declared uncertainty, so a ring edge can legitimately sit a few micrometres off the surface it bounds. The code finding where a ring edge crosses the planned split line compared against the edge's raw (off-surface) point using a hardcoded tolerance far stricter than the file's own precision, rejecting a valid cut. Now matches against the point on the surface directly below the ring crossing, respecting the file's declared tolerance. 2. Closed B-spline surfaces, their slits and their slivers. truck only treats the rotation of a revolved surface as periodic; a face on a B-spline surface whose control net closes on itself (a tube) wasn't recognized as wrapping around at all. This produced a folded face, a hole (a boundary loop landing a full period away from where it should cut), and one near-zero-width sliver face costing the entire body its tessellation plan. Adds periodicity detection for closed B-spline surfaces, period- aware hole-loop alignment, dropping of zero-width slit boundaries, and isolating a too-thin-to-triangulate face so it no longer takes the rest of the body down with it. The GPU shader's periodic wrap mirrors the CPU math so the two don't drift apart. Regression tests: band_faces_split_when_ring_edges_run_off_their_surface, band_face_with_a_zero_width_slit_is_still_meshed, a_sliver_face_too_thin_to_triangulate_does_not_cost_the_body_its_plan. Ablation-tested: disabling either mechanism in (2) reproduces its target test's original failure message while leaving the other test unaffected. * STEP import: mesh the band's split faces, not just split them jyviko asked whether a band wall missing from the interactive app's render was a shading issue or a genuinely missing face. It was missing: the ring- edge split fix made the band's topology correct, but plan_face still produced zero triangles for the two new sub-faces. Two causes, both in plan_face (crates/kernel/src/tess/mod.rs): 1. The tolerance for how far an imported boundary may lie off its surface was tied to the requested mesh's chord tolerance instead of to the body's own size. A finer display tolerance could then reject a boundary a coarser one had just accepted, so the face would drop out exactly when zooming in on it. 2. Near a sharp corner of this flat-panelled band, several boundary points that are legitimately a few micrometres apart (within the STEP file's own declared precision) all projected onto the same surface point. The boundary loop then repeated a point in (u, v), which a triangulator can't follow, so the face's plan failed outright. One face without a plan costs the whole body its plan, and the fallback mesher can't handle a closed B-spline surface either, leaving both halves of the split band with no triangles at all: present in the topology, invisible in every renderer, and absent from the body's volume. Fix: scale the off-surface tolerance to the body's own extent (new Rules::extent), and collapse a run of boundary nodes that land on the same surface point to one (drop_collapsed) before triangulating. Regression test additions to band_faces_split_when_ring_edges_run_off_their_surface: the body's volume now matches the analytic pentagon prism (was a third of it - only the top cap - before this fix); display_plan leaves no face without triangles from coarse to fine tolerance; and the actual tolerance and facet angle the viewport draws with gives the split band its full lateral area on both halves. Confirmed on review_band_big.step, the fixture behind the review screenshots: volume's relative error against the analytic pentagon prism drops from 1.4e-4 to 4.1e-6, and the band wall now occludes the floor grid behind it in a snapshot the same way the top and bottom caps always did. Full kernel suite: 739 tests, 0 failed. * STEP import: hold an imported boundary to the file's own tolerance, not the part's size jyviko: the off-surface acceptance added in the last commit (rules.extent * OFF_SURFACE) scaled with the part, so a 2 m part silently accepted a boundary 2 mm off its surface while a 20 mm copy of the same part accepted only 0.02 mm. A STEP file's own declared uncertainty does not grow with the part, so the bound shouldn't either. Body gains boundary_tol: Option<f64>, Some(file's declared tolerance) for a body read from STEP (crate::step_in::Units::tol, already used by the ring-split fix in split.rs), None for anything this kernel built. It's set before the import's own unmeshed-face check runs, and carried over by the two face-split operations that re-cut an imported surface (splitface.rs, outline.rs), since the file's uncertainty still describes those boundaries afterward. plan_face's accept now reads: the file's own tolerance when the body declares one, or the old chord-tolerance/extent bound otherwise. The old bound's tol * 10.0 term had to go too, not just the extent term: at a 2 m part's typical display tolerance that term alone is ~20 mm, nearly ten times the extent bound jyviko objected to, so keeping it would have left the exact 2 m/2 mm example still silently accepted. Two new tests in step_in/tests.rs: - band_faces_split_when_a_scaled_copy_runs_off_its_surface: the same band fixture at a realistic 2 m scale with the same absolute slop and declared uncertainty as the small one, meshes the same way at every chord tolerance from coarse to fine. - off_surface_budget_follows_the_file_not_the_part_size: the actual regression. The same off-surface error is accepted or refused the same way whether the part is 20 mm or 2 m; a 2 mm error against a 1 µm declared uncertainty is refused at both sizes now (accepted at 2 m under the old bound, refused at 20 mm - that asymmetry was the bug). Checked at both a coarse and a fine display tolerance. Known gap, not fixed here: a STEP-imported body saved into a .solidcraft design and reopened loses boundary_tol (native.rs's decoder has no field to read it from) and falls back to the old extent-based bound on reload. Today's design format has no concept of this value at all; persisting it is a real format change, out of scope for this fix. Full kernel suite: 776 tests, 0 failed. clippy -D warnings clean. * Display plans for STEP threads and pointed ends (#3847) Three faces cost a threaded or pointed STEP part its whole display plan, so it was drawn from the reference mesh: cracked, faceted, and never refined up close. - A loop through a pole (a cone's apex on the boundary) seemed to wind around the surface: the jump at the apex, unwrapped to the nearest copy, was arbitrary. The loop is turned to end where it leaves the pole, so the step that closes it is an ordinary one (`seam::close_through_pole`). - Projection onto a spline with hundreds of spans (a helical flank) restarted from a 16 x 16 grid and settled on another turn; it now starts from two points per span, capped. - Sag refinement no longer inserts a point exactly on a boundary side, which split the side and failed the face. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * tess: a refused face's edges are divided finer and it is planned again (#2814) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * tess: sliver faces plan empty; a face no method plans is filled flat from its nodes (#2814) A face whose boundary runs out and back along itself (two edges between the same vertices, a rounding apart, as STEP files write slivers) encloses nothing and is planned with no triangles. A face still refused after its edges are divided finer (a cusp, a sliver whose sides sag across each other, a small cap at a pole) is filled flat from its shared boundary nodes, so the rest of the body keeps its display plan instead of falling back to the reference mesh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * display mesh tests: a rim the chord tolerance leaves a line is divided again (#2814) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * portable B-rep: up to 8192 control points along a spline direction (#2814) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * solidcraft-cli: --debug logs what the engine reports to stderr Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * STEP import: a spline written over a tiny parameter range is read over one (#2814) Some writers parameterize splines over a few millionths (a humanoid robot's edges span 5.4e-6). The kernel tells parameters apart only to 1e-6, so over such a range it evaluated the curve's end wrongly, and trimming an edge to its vertices dropped a control point: the bodies could not be stored exactly and the whole model cache was left out of the design. The knots are now stretched to span one; the curve is the same and edges are trimmed by their vertices, so nothing read from the file refers to the old parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * model cache: a body keeps its file's declared accuracy (#2814) A body restored from the cache dropped the accuracy the file it was read from declares, so the planner held its boundaries to the kernel's own bound and a reopened design drew differently from the import. It is stored with the body's runtime values, and only when there is one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * tess: a boundary within a hundredth of its body lies on its surface, at every quality (#2814) #1721 held a body read from a file to the accuracy the file declares. A writer declares what it aims for, not a bound its edges keep: a 251-body robot assembly declaring 0.01 µm has boundaries 10 to 30 µm off their surfaces, and 95 of its bodies lost their display plans. The bound is a hundredth of the body again, what Normal quality always allowed, and now the same at every quality, so zooming in still never drops a face (#1721's point). A file declaring more raises it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * STEP import: keep faces whose edge vertices sit slightly off their curves Some exporters write an edge's vertex beside the end of its curve, further off than the accuracy the file declares (a surface export declares 0.01 mm and puts a vertex 0.19 mm off two spline ends). The reader refused such edges and left the face, and with single-face bodies the body, out. A vertex may now sit up to the file's tolerance or 1/1000 of the model's size (the diagonal around its VERTEX_POINTs), whichever is larger, never more than the existing 1 mm ceiling. Spline and open-conic edges have the end control point moved onto the vertex, so every such edge meets its vertices exactly. The file's declared accuracy still governs closed-curve, seam and circle checks. * tess, io: review clean-ups (#2814) - A sliver face is one whose loops are all slit: drop_slits finds it in the retry after a failed plan, so the pre-check on every face (folded) is gone. - The flat fill uses cdt::triangulate (a lenient fill that leaves out sides crossing one already kept) and its flood-fill inside test, and the plane from geom::Plane. - Refinement only builds its indexes when a face was refused, takes the faces to plan again from edge_faces, and re-samples edges on the pool; Rules::finer for the finer rules. - One PackageWriter::fits for the model cache, the preview and the thumbnail. - The band split's ring tolerance lives in split.rs; comments say what the off-surface bound is now. - The CLI logger prints Level::as_str(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM --------- Co-authored-by: David Lackovic <lackovicdavid1234@gmail.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Ray <raykholo@users.noreply.github.com> |
||
|
|
7e05c8b23c |
Hovering a long helical edge no longer freezes (#3745) (#3818)
Hovering an edge lights its circular rim, which starts from the edge's tangent chain. That looked at every edge of the mesh for each edge in the chain (and searched the chain itself each time), so a long helical edge in many pieces, a thread's crest, took seconds: 7.6 s inside the frame on the first hover of the stem's thread in a real watch-movement STEP file. The edges are now found by the grid cells of their ends, two join tolerances across, so each step looks only at the edges that end near it: the same chain in the same order, in 29 ms for that thread. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
e37a8fa6bb |
Fillet: skip smooth seams in tangent chain and restore picked edges when unticked (#3370) (#3437)
* claim: #3370 * fillet: stop tangent chains at smooth seams and sync chain toggle (#3370) |
||
|
|
fe3b14e798 |
refactor: Aabb2 and point-built Aabb3 for every hand-rolled box (#2428)
* Iterate instead of indexing in nine loops Replace index loops that look up neighbours by position with enumerate, zip(cycle().skip(1)) for wrap-around pairs and zip(skip(1)) for consecutive pairs. No behaviour change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: ring_edges for the edges of a closed ring Every walk over a polygon's edges, the last back to the first, spelled x.iter().zip(x.iter().cycle().skip(1)); eighteen of them now call geom::ring_edges. Three redundant take(len) calls go with them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Ring, a closed ring that knows it is closed A Ring holds items in order around a closed polygon, loop or fan; edges() walks each with the next and the last with the first, corners() each with both neighbours. Ring<Vec2> has signed_area and even-odd contains. Loop2::polyline returns a Ring, as do doc's face_loops and boundary_loops, kernel's coplanar wire samples, CapLoop's segments, tess pieces' rings, the gizmo's plane quads and the navigation cube's facets. Their walks take edges() or corners() instead of wrapping an index; sketch's text-hole test and doc's poly_area become contains and signed_area. Loop2::joints pairs each segment with the next. Replaces the ring_edges free function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Chain, Ring and Path; bounds for a chain of points Ring becomes Chain<T, Closed> beside Path = Chain<T, Open>: edges() closes a ring and stops at a path's end, Path::close makes the ring, and bounds() (the empty box for no points) serves both. Eight hand-rolled min/max folds over a loop's polyline call bounds(); doc's flat_polyline returns a Ring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: area and inside on Ring for any planar point Ring's signed_area and contains take any Planar point (Vec2 or [f64; 2]). The cam profile in joints_cam is a Ring: its own even-odd test goes, and its edge walks, gap and area take edges(). face_params' loop area, the text path's winding, tess pieces' ring area and the copies in three tests call signed_area or contains; the formulas are the same term for term. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * doc: sheet's closed outlines go through Ring dedup takes and returns a Ring (dropping a last point back on the first is what makes it a ring's), near_outline walks a Ring's edges, and the copy of it beside fold checks calls it. point_in goes: its callers ask the Ring. Its formula anchored each crossing on the other end of the edge, so the two can differ only for a point on an edge to the last digit; one caller sets such points aside first, the others test a region's interior point, a cut's midpoint or another outline's corners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * doc: cam tests build their outlines with Ring::from Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: area_vector on Ring<Vec3>; three Newell sums use it Ring<Vec3>::area_vector is half the sum of a × b round the ring. heal's loop_area, lip's outer-loop turning and the revolve's loop orientation summed it by hand with a wrapped index; they take it from the Ring (the half is exact, and two of them only read its sign). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Aabb3 from points; boxes built and asked through it Aabb3 collects from points (skipping non-finite ones, as add does) and gains inflated, contains and meets. Fifteen hand-rolled min/max folds over points or boxes in kernel, doc and engine become collect or union, and their "within tol of the box" tests become inflated(tol).contains. blend's refused-edge boxes and naming's face index hold an Aabb3. A NaN point is now outside every box, where the old negated comparisons let it in; an infinite point no longer stretches a box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Aabb2, the plane's box; 2D folds and box tuples use it Aabb2 mirrors Aabb3 (collect, add, union, inflated, contains, meets) and adds relative, a point's place in the box. sketch's link and link_loops each had that formula; link_loops' boundaries, prism's segment and loop boxes, profiles' shape boxes and grid, and ends' segment box hold an Aabb2 instead of a (low, high) pair. Folds in the cam, sketch bounds, sheet's perforation grid and the sketch clipboard collect one. Boxes are built from the same terms (a round's box keeps c - r - tol). As with Aabb3, a NaN point is outside every box and an infinite point no longer stretches one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * kernel: prism's segment boxes are plain Aabb2s The SegBox alias named a (low, high) pair; Aabb2 says it itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Chain::bounds is an Aabb2; the boxes #2382 left as pairs follow Chain::bounds returns an Aabb2. Its callers keep it: sheet's hole and cut boxes (loop_box), letter boxes, island outlines and line-near-cut test use meets, contains and inflated; profile's outline overlap and max_clearance, profiles' box tests and the cam test read its corners. flat_size and the flat pattern's JSON size are the union of the loops' boxes (0 × 0 for no loop, where the pair fold gave -∞). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: Aabb3::intersection; hand-grown and hand-met boxes use the box Aabb3::intersection is the box both hold. ops' four overlap boxes of a boolean's operands take it, and seven boxes grown by hand (ops' body boxes, the distance set's bounds, coplanar's face box, eval_more's padded mesh box, compare's group and region boxes) call inflated with the same amount on every side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com> |
||
|
|
d0afc31632 |
Save the evaluated model with every design: exact, stored once, scaled caps (#3053) (#3093)
* Model cache written by default: exact bodies, stored once, scaled caps (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bench-open: say why bodies were not kept * Exact planes and revolutions; budgets for large bodies (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Native B-rep: rebuild shells as the kernel made them (closed and seam edges) (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: the model cache and the exact native B-rep (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * bench-open: time a save without the cache too * Model cache entries are Deflate-compressed (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Model cache: one that does not fit the design file is left out, never a failed save (#3053) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0a328a3d0a |
perf: placed parts draw their definition's display plan, moved (#3044) (#3127)
World placement already shares a part's reference mesh between its occurrences (#2924), but the display plan the GPU viewport draws was made again for every occurrence from its placed B-rep: planning every face of every instance of the part. A placement now carries the definition's plan cells too, and a placed body's plan (and its CPU mesh) is the definition's, moved by the placement's matrix (`TessPlan::transformed`): the part is planned once however often it is placed. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
62b49f9f89 |
Boolean: a pin in a bore of its size is clear at every placement (#2714) (#2715)
* Boolean: a pin in a bore of its own size is clear of it, whatever its facets
A pin whose wall is in other faces than the bore's (turned from a profile: three
faces; the bore two) is faceted at other angles. Where one body's faces end on
the shared cylinder (the pin's wall through the end face of the tube, the pin's
end inside the bore) the facets cross by the facetings' error, so the sampled
shortcut doubted its own "apart" answer, every retry failed on the coincident
walls, and the result came back unverified after seconds: inspect.clearance
left such pairs unchecked ("could not be intersected there").
The shortcut now leaves out crossings that lie on a cylinder both bodies have
faces on (axes and radii within the kernel's same-surface tolerance, where both
faces run). A pin a hair off its hole's axis, or one pushed into a narrower
bore, is still not taken for apart.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Boolean: skip only the shared cylinder's own crossings, not a flat along it
The shortcut left out every crossing near a cylinder both bodies have faces
on, from any face: a pin through a D-bore whose flat is 0.022 mm inside it
came back as contact. Mesh::crosses_except now hands the predicate the two
triangles' faces, and only crossings between the faces on the shared
cylinder, or between one of them and an end face across its axis that keeps
to its body's side of the cylinder (within the facetings' error), are left
out. The D-bore's end face reaches into the cylinder to the flat, so the
pin's wall through it counts again; a flat, key or rib along the axis always
counts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Boolean: a pin in a bore of its size is clear at every placement
The sampled shortcut's check that no facet has material under it on the
wrong side of the other body still caught the facetings of a shared
cylinder. A facet of a pin's end inside the bore (or of the bore part's
end face round the pin) with all three corners on the rim has its middle
just inside the cylinder. Where the other body's chords reach further in
at that angle, the point under it lies inside the other mesh, though the
bodies only touch. Which facets do so depends on the face splits and the
placement, so the same pair read as clear at one placement and as "not
verified" at another.
Such points are no evidence now when they lie on the shared cylinder,
under a facet of one of its end faces: the end faces and band the
crossing check already leaves out. A pin 0.02 mm larger is no shared
cylinder, and its overlap is still found.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
|
||
|
|
355154fa9b |
refactor: geom::Grid for control nets; one surface fit through a grid of samples (#2331)
Thanks @jijinbei. |
||
|
|
7ad5130cd2 |
refactor: geom::UnitVec3 for unit axes and normals (#2603)
* geom: UnitVec3; a face's analytic axes and normals are unit by type Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * geom: UnitVec3 adds to and subtracts from a Vec3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * kernel, doc, engine: cylinder axes and measured normals held as UnitVec3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * kernel, engine: functions that take a unit axis or direction say so by type Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * doc: a face's normal goes to Plane::from_normal as is Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * kernel, doc: main's new cylinder helpers take the face's axis as a UnitVec3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7c9f8a5895 |
Drawing: validate section geometry contracts (#2053) (#2085)
Thanks @0xlillith. |
||
|
|
83c31d77c4 |
surface.boundary: surfaces through boundary curves and curve networks (#763) (#2129)
* feat(geom): surfaces through curve networks, and spline spaces to build them Adds spline_space (clamped B-spline spaces, Hermite interpolation, exact embedding by Bézier degree elevation and knot insertion, a monotone cubic for reparameterising) and network (loft, Coons and Gordon surfaces through curves in one or two directions, with G1/G2 cross-boundary conditions against a neighbouring face). Part of #763. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(kernel): boundary surfaces from sketch curves, 3D curves and edges kernel::boundary_network turns sketch chains, 3D sketch polylines and edge chains into network curves, gives G1 frames of the faces along open edges (EdgeFaces), builds the B-spline face (sides split at chain vertices so a join shares the body's edges) and joins it into a surface body by stitching. G2 is refused for now (not supported yet). Part of #763. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(kernel): second derivatives of B-spline surfaces in the vendored truck-geometry BSplineSurface::uuder and vvder multiplied the second differences by p instead of p·(p − 1), as BSplineCurve::der2 does, so every surface of degree p ≥ 3 (NURBS too, through its homogeneous surface) reported 1/(p − 1) of its second derivative: a cubic surface's curvature read as half. This is why a G2 boundary surface on a cylinder measured half the curvature it carries (#763). New tests hold both to finite differences for degrees 2 to 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * feat(kernel): G2 boundary surfaces against a body's face The network always carried the face's curvature across a G2 edge; the refusal came from measuring it with truck's halved surface second derivatives (fixed in the previous commit). EdgeFaces now reads the face's second fundamental form from the exact second derivatives instead of differencing the normal. A new test holds the curvature across a cylinder's edge to the cylinder's within 0.1%, also with a sheared far curve; G3 is refused (a network holds up to G2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * test(kernel): a boundary patch with straight corners measures its own area A stadium's arcs meet its lines at 180°, so the Coons patch is singular at its corners. From such a corner the reference mesher's boundary walk ran onto the B-spline's extrapolation past its domain (a planar spline passes through every point of its plane there), and the face measured 289 mm² for its 158.5 (#763). The vendored mesher's bounded projection (nearest_projection, #2043) keeps the walk on the domain; this holds it there, by the faces, the measuring mesh and the display mesh. The display path (#1653) was never affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * feat(geom,kernel): three-sided boundary surfaces (one side collapsed to a point) A network curve may be a PointCurve: the apex where the two sides beside it meet. The curves through it cross it at the point itself, its row of the net collapses there, and the kernel's face leaves that side out of its loop (three edges, one vertex at the apex). One point per patch, as a side; refused elsewhere, when it moves, or with a continuity condition. Tests: a triangle and a quarter disc filled flat with their analytic areas (40 mm², 25π), by geom and by the kernel's faces and meshes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * feat(doc): boundary surfaces through curve networks and loops of three or four FeatureKind::SurfaceNetwork saves a network as `u` and `v` curves, each a sketch chain (planar or 3D), a chain of a body's edges with an optional G1/G2 continuity, or a point (the apex of a three-sided patch), and an `operation` that can join the surface into the body of its edges. A SurfaceBoundary of three or four sketch curves, refused until now, builds as the flat Coons patch of its loop (three collapse the side opposite the first curve). The format change is one pending file; feature-walk goldens record the new variant's sketches, bodies and points. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * feat(engine): surface.boundary through loops of three or four and curve networks The command keeps its loop form (two to four curves of one sketch; three and four now build the flat patch of the loop, crown refused there) and gains a network form: `u` and `v`, each curve a sketch curve id, a {sketch?, curves} chain (planar or 3D), a {body, edges, continuity?} edge chain (g1/g2 with the face beside it) or a {point} for a three-sided patch, with `operation` new or join. Edits may move between the two forms (timeline.redefine accepts the command now that it has a dialog). Tests: analytic areas of loop patches and of a hyperbolic paraboloid bounded by four 3D lines (every mesh vertex on z = xy/10), a loft across two sketches, G1 and G2 measured across a cylinder's edge with inspect.continuity (angle < 0.1°; curvature jump 1e-16 with G2 against 0.027 with G1), 26 hostile networks refused without panicking, and u/v added to hostile_params_never_panic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * feat(ui): Boundary Surface dialog Curves and Cross curves take sketch curves and edges (picks that meet end to end in one sketch or body are one chain); Continuity and Operation (New Body or Join). Two to four sketch curves of one sketch with nothing across are the loop patch; anything else is a network. Edit Feature brings either back (per-side orders, crown and stations the dialog does not show are kept). Strings in en, ja and zh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * docs: ROADMAP M8 and the bridge patch's notes for boundary networks (#763) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * test(oracle): a boundary surface through a loop, thickened (#763) Case 83: four sketch curves (a square's three sides and a half circle for its top) make one boundary surface, thickened 2 mm; analytic values from tools/oracle-analytic/cases_surfaces.py (V = 2A, area 2A + 2P, centroid by moments). Recipes gain boundary_surface, thicken and remove. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM * refactor: boundary surfaces, review cleanups Project onto the face once per derivative evaluation (the frame is passed to natural() and reused by scale()), keep each track's finite-difference step instead of rescanning its table, export the kernel's edge-pick and chain caps and the default station count for the engine, doc and dialog to share, map TooLarge spline errors to KernelError::TooLarge, reuse the engine's operation parser, and skip the dialog's sketch lookup when the picks are no loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LGeCa5jWWdroMWvXxN3LLM --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: bherbruck <brennen.herbruck@gmail.com> |
||
|
|
186555dbe9 |
Clearance report: overlaps, near pairs and nearest bodies (#1900)
Thanks @NOWN-DEV. |
||
|
|
8907da5e86 |
Viewport picking: a BVH per body mesh and a GPU pick buffer for visible edges (#1776)
Thanks @oursland. |
||
|
|
bbd4f29ebb |
Class-A surfacing 3/3: blend curves, curved-edge fillets, the G2 corner and continuity analysis (#1642)
Thanks @midasdf. |
||
|
|
61678654c2 |
Extrude: dragging the taper stops where the profile would close (#1602) (#1614)
Thanks @DuBearte. |
||
|
|
26bf1af93b |
Sketch on an origin plane faces the side you clicked (#1587)
Fixes #1586. |
||
|
|
fac3f96bdd |
Patch: G1/G2 to the faces beside the loop, general and N-sided loops, points (#759) (#1521)
Thanks @jyviko. |
||
|
|
e8e2ccc8a9 |
Class-A surfacing 1/3: the continuity core, a G3 free-form blend and patch crown (#1249)
The continuity core (kernel bridge: Hermite control rows reproducing a support surface's position, tangent, curvature and change of curvature, G0 to G3), surface.blend between two edge chains (#764), and a crown for fitted patches. Existing patches and fillets are unchanged by default (crown 0 is the plain patch bit for bit). SurfaceBlend and Patch.crown are noted under format 22. |
||
|
|
d0a4443939 |
inspect.compare: where two versions of a part differ, without a boolean of the two (#1423)
New command inspect.compare {a, b, tolerance?}: measures of both and their differences, two-way surface deviation from a bounding-volume tree, regions beyond the tolerance clustered by side (only_in_a / only_in_b) with integrated volumes and faces, a verdict, optional face colouring as one undo step, and warnings for open meshes. Geometry in solidcraft-geom (compare.rs), command in the engine. Closes #1363.
|
||
|
|
587cf865b8 |
Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws. Part 4 of 4 of #303. Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> |
||
|
|
17db109c5d |
URDF export for robot simulators (#719) (#942)
A design with components and joints exports as a URDF robot description (file.export to .urdf, the MCP export tool, solidcraft-cli run --out, File > Export URDF...). It writes one binary STL per component in metres, links with mass, centre of mass and inertia from material density, and maps rigid, revolute, slider, cylindrical, pin-slot, planar and ball joints to URDF joints or short chains. Loops, broken joints and loose parts are reported as warnings. Options: mesh_prefix, effort, velocity. Docs in docs/urdf.md. |
||
|
|
fdaf57daa3 |
Count the contact grid's own work, and test find_close_pair (#836)
find_close_pair's budget now counts the grid's own work too (each cell a triangle is filed under or visits), not only the pair tests, so a pathological pair of meshes can't spend unbounded time building and walking the grid. Adds tests for find_close_pair and the budget. |
||
|
|
1f7f37d1ef |
Cut an extrude's profiles that stand apart in one boolean (#848)
When an extrude cuts with profiles whose outlines stay more than 1 µm apart (the letters of a text), their tools are apart too, so they are joined and cut from the body in one boolean instead of one per profile. A ten-letter text cut drops from about 13 s to 1.5 s with the same result. Sheet metal targets and touching profiles keep cutting tool by tool. Tapered extrudes are unaffected. |
||
|
|
b6e3a6c366 |
fix: contact and shell-grouping checks use a shared broad phase (#838)
Contact and shell-grouping checks find close triangle pairs through one shared grid instead of testing every pair. Refs #232. |
||
|
|
ff7a347013 |
Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)
Display meshes are built from tessellation plans, with GPU evaluation in the viewport. |
||
|
|
bfbf5f9432 |
Display tessellation plans (1/4): planner with shared edge nodes (#802)
Adds TessPlan and the kernel planner (shared edge nodes, structured and Coons grids, constrained Delaunay for trimmed faces) behind Body::display_plan. Nothing drawn changes yet. Refs #303 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4ff8f3e14d |
feat: free-form cut-outs stay exact in sheet-metal flat patterns (#692)
Sheet-metal cut-outs and base flange outlines drawn with ellipses, splines or text stay exact curves in the flat pattern and the unfolded body; the folded body follows them with tangent arcs within 0.001 mm (#659). |
||
|
|
9f88537927 |
Define a kernel-neutral native B-rep format and guarded adapter (#461)
A SolidCraft-owned portable B-rep schema (geometry schema 2 with analytic curves and surfaces, explicit frames and ranges, fitted intersection curves) and a versioned body envelope (SCBREP03) with its own metadata records, a bounded binary codec, a validator with resource budgets, and an adapter that refuses what it can't convert exactly. Pinned by a box-and-fillet golden fixture. Not yet written by application saves. Refs #303. |
||
|
|
3159fc0dd0 |
fix(kernel): a small overlap is not taken for bodies apart or inside (#484)
When sample points say two bodies are apart or one is inside the other, the facetings are now checked for surfaces passing through each other; if they do, the boolean is computed instead of shortcut. If it still can't be intersected, the sampled answer is kept as a flagged, unverified result, and Interference lists such pairs as "could not be checked" with the reason. Fixes #442 and #443. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
201e9d5f9f |
Patch fills non-planar edge loops with one smooth face (#250) (#472)
* Add a one-surface fill of closed 3D loops to geom
A closed loop of curves that does not lie in a plane has no exact surface;
the usual answer is one smooth face fitted over it. fill_loop does that:
- the loop's best-fit plane (Newell normal), with a frame chosen from the
loop itself so the result does not depend on where the loop starts, its
direction, or where the part sits
- the loop seen along that normal must be simple and must not fold or run
along the normal; otherwise it is refused ("not supported yet"), never
filled with wrong geometry
- one non-rational bicubic B-spline height field over the loop's box,
fitted by least squares to the loop's samples (The NURBS Book 9.4.1,
9.4.3, eq. 9.62/9.65) with a thin-plate smoothness term
lambda*A*int(h_uu^2 + 2 h_uv^2 + h_vv^2), A one grid cell's area, so the
fit is the same at any scale; only planes are reproduced exactly
- span counts per direction (long narrow loops fit), refined until the
loop is within tolerance, else refused with the deviation reached
bspline: bisected span search, basis functions on a stack array, one
clamping helper shared by curves and surfaces, one binomial helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Patch fills non-planar loops of edges with one smooth face (#250)
surface.patch with edges refused any loop that is not planar ("not
supported yet: patching a loop that is not planar"). It now fills it:
- planar loops still get one exact planar face, but planarity is certified
conservatively: lines by their ends, B-spline/NURBS edges by their control
points, intersection curves by their samples lying on a support plane
within tolerance; a loop that can't be certified is filled instead
- other loops get one face from geom's fill_loop, bounded by the body's own
chained edges, whole (no split): its history is one generated face whose
edges are the body's, and it stitches back into a closed solid
- intersection-curve edges are sampled by a seeded, guarded double
projection shared by the planarity test and the fill (plane_fit)
- the face tolerance is 10x the planarity tolerance (documented: later
operations take the face to contain its edges within it)
- edge picking runs inside the guard; the geom->truck conversion is shared
with the T-spline sheets (spline_conv); the Newell plane fit is shared with
STEP import
Tests: 3- to 8-sided, concave (dart, arrow, plus), flat-corner, two-edge and
imported rims fill as one face and stitch closed (volumes against an
independent integral); finite mesh normals, same result for any pick order
or placement; non-height-field loops and too-fine detail are refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
* Test the Patch dialog filling a non-planar loop of picked edges
The Patch dialog's only test used a sketch profile. Picking the four
curved rim edges of a box cut by a sideways cylinder now makes one
surface.patch command whose result is a single smooth face over the rim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
296683229d |
Rename SolveCraft to SolidCraft everywhere, with compatibility shims (#389)
* WIP deep rename (not for push) * WIP2 * WIP3 * WIP4 wxs fix * rustfmt * Rename the remaining identifiers in merged crash-report code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b2fc6d1fac |
Keep a zoomed-in profile's hover fill: count the slabs each edge crosses (#336)
fill_triangles bounded the slab triangulator by edges x slabs, as if every edge crossed every slab. A ring sampled finely at a close zoom has hundreds of edges and slabs but only four edges in each slab, so the estimate ran a hundred times over the hover highlight's budget and the fill was dropped, leaving only the outline. Count the slabs each edge actually spans instead. Region2::triangulate tested every edge against every slab, so its work was the overestimate. It now sweeps upward with the edges crossing the current slab, giving the same triangles in time proportional to them. Fixes #164 Co-authored-by: Vitali M. <6042958+vitalibr@users.noreply.github.com> |
||
|
|
a04ef4ca59 |
T-spline core library and sheet bodies (#250, step 1) (#337)
* Add B-spline basis, knot insertion and NURBS surfaces to geom Freeform faces need local refinement and gap-free joins between patches with different knot vectors, which tensor B-spline grids cannot give; T-splines can, and the first user is an N-sided Boundary Fill of non-planar loops. This lays the univariate groundwork they rest on, in the L0 geometry crate so it stays independent of the B-rep kernel: - Cox-de Boor basis of one function from its local knot vector, on half-open spans with a left-limit side for the domain end, with derivatives (NURBS Book eq. 2.5, 2.9) - knot insertion into a single basis function (Sederberg et al. 2004 section 3) and Bezier extraction of one function by repeated insertion - Boehm knot insertion for curves (NURBS Book A5.1), de Casteljau evaluation and sub-segments - a rational tensor NURBS surface with evaluation, knot insertion and clamping, and a rational Bezier patch type All input is checked (finite, monotone knots, positive weights, size caps) and returns SplineError; nothing indexes unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 * Add T-spline surfaces: T-mesh, evaluation, AS check, refinement, extraction Tensor B-spline faces refine only by whole rows and columns and cannot join patches with different knot vectors without gaps. T-splines allow T-junctions in the control net, which fixes both; the first user is an N-sided Boundary Fill. This adds the maths only (no command, no change in behaviour): - T-mesh in index space with knot values per line, validated (outer rectangle, rectangular faces, no empty lines, size caps); local knot vectors read off rays from each vertex, rays leaving the mesh repeat the boundary (Sederberg et al. 2003 section 3) - rational T-spline evaluation and first derivatives for odd bi-degree, with a complete (p-1)/2-line frame and the domain inside it - T-junction extensions and the analysis-suitability check (Scott et al. 2012 section 2) - local refinement that keeps the surface exactly: requested knot lines are cut in, then every original blending function is carried as a record and split (violation 1) or the mesh gains edges (violations 2 and 3) until each record is a blending function of the refined mesh (Sederberg et al. 2004 section 3); analysis-suitable input stays analysis-suitable by carrying conflicting T-junctions across faces - NURBS <-> T-spline conversion for meshes without T-junctions, and Bezier extraction over the rectangles no blending-function knot line crosses Measured: regular T-spline vs NURBS < 1e-12, round trip exact, partition of unity with T-junctions < 1e-14, refinement deviation <= 1.4e-14 over 40 random refinements, Bezier patches vs surface < 1e-14. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 * Convert T-splines to sewn NURBS sheet bodies in the kernel A T-spline is only useful to the modeller once it is a B-rep face set. tspline_sheet builds an open shell: a mesh without T-junctions becomes one clamped NURBS face; otherwise each Bezier element is a face, and sides are split at the T-points where neighbours' corners land, so all faces share vertices and edges and the sheet is one oriented shell (V - E + F = 1). Every truck call runs under guard. No command yet; Boundary Fill will be the first caller. Tests: one face for a regular surface, sewn Bezier faces with T-junctions, tessellated points on the surface to ~1e-9 mm, and hostile control nets (coincident, 1e300, needle-thin) never panic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8c81bc60d0 |
feat(kernel): creation builders report exact face history
Extrude, revolve (partial and full turn), tapered extrude, loft (incl. to a point), sweep, path sweep, coil, primitives, lip, emboss and thicken attach the exact history of the bodies they build: each face's profile segment, cap or operand face, and each edge's operand edge, recorded by topology identity inside the building call. Nothing is sampled; a body whose faces do not follow the expected construction carries no history. Face sources are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J53sR6gEaVt56soFDpTck8 |
||
|
|
400f200efa | Speed up cold document geometry rebuilds | ||
|
|
af536a7907 | Rename displayed product to SolidCraft and add header bug reporting | ||
|
|
ab05b24dee | Prepare 0.2.0 usability release and refresh desktop demo | ||
|
|
acf4669f73 |
fix: select complete circular rims and curved surfaces in 3D
Group kernel pieces across circular rims and hidden surface seams for hovering, selection, dialogs, and measurement. Preserve physical references and cover full-item toggling and picking with regression tests. Fixes #88 |