mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
64bbfe9ed3402f2fb00a95a116e4d6531c54bb21
370 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f595f38e1c |
Define a named parameter inline from any value input (#4071) (#7341)
* claim: #4071 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Define a named parameter inline from any value input (#4071) Any value input, typed or sent as a command's expression parameter, takes `name = expression` (or `#name = expression`): the design gets the user parameter `name` with that expression, and the input stores `name`, in the command's one undo step. - doc: the lexer skips a definition's `name =`, so until the design stores it the text evaluates as its expression and commands read and check it like any value. `Document::define_inline` then turns every stored definition (feature inputs and placements, dimension parameters, sketch pattern and text values, joint and coupling values, sheet metal and plastic rules, canvas planes) into a user parameter, the unit following how the input reads a bare number. A taken, reserved or empty name, two different values for one name, and a definition in a parameter's own expression or a configuration cell are refused; all or nothing. - engine: `Session::execute` runs it after any command whose parameters hold definition-shaped text; the reply has `defined` (the first name, as `sketch.dimension` already gives) and `definitions`. A refusal fails the command, which leaves the design as it was. - `parameters.change` with `new = expression` renames the parameter it changes (a dimension's or feature input's own, references following) and sets it; when the value is just `new`, it sets `new`. `changes` refuses definitions. `expr.evaluate` reports `defines`. - UI: lengths keep the name when the design unit is applied; sketch text sends a definition as typed and previews only its value; the box corner and offset Distance 2 use the value; fields whose command keeps no expression (sketch scale, move, chamfer) refuse a definition with the way to name it. The dimension editor says which parameter it named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
02ac56d12f |
Feature windows choose Join, New Body or Cut from what the material touches (#6901) (#6903)
* claim: #6901 * Feature windows choose Join, New Body or Cut from what the material touches (#6901) Extrude, Revolve, Loft, Sweep, Coil and the Box, Cylinder, Sphere and Torus choose their operation by running the Join on a copy of the design off the window thread and counting the pieces: Join when the material makes one body with what it touches, New Body when it would be a separate piece, Cut when it lies inside a body (Revolve and the four primitives; Extrude keeps its own Cut). The choice changes only when a check is in, a pick in the Operation row (the shown option included) stops it for good, and OK waits for a running check. Hidden bodies take no part: with some hidden, the shown ones become the feature's targets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Operation row: a pick in Extrude stops its own choice at once; ja/zh strings, gallery entry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Operation check: contact test first, mesh volumes for Cut, OK takes the evaluated Join Material touching no body is a New Body without running the Join (the contact test costs 0-240 ms where the Join costs seconds on a large body). Inside is told from display-mesh volumes instead of exact ones (8 s on a 400-hole plate). The check evaluates the Join through Session::ahead, and OK of the same commands takes it (Session::prime) rather than computing it again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Preference: Choose the operation automatically (on by default) Preferences > Design gets "Choose the operation automatically" (auto_operation, on by default). Off, feature windows don't choose Join, New Body or Cut from what their material touches: each keeps the operation it opens with (Extrude its own Join or Cut), no check runs and OK doesn't wait for one, as before Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Arabic, Hebrew, Vietnamese and Turkish strings for the operation choice and its preference Arabic, Hebrew and Vietnamese are complete catalogs now, and Turkish has a full catalog, so this change's three strings get them too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Operation check: a closed window's check stops; the icon audit doesn't choose A window's operation check kept running after the window closed, a Join on the design for nothing, and the test harness waited for it after every step: the icon audit, which opens every window, took ~30 min instead of ~1. The check now runs under a Progress, and closing the window (or one that doesn't choose) abandons it: its evaluation stops at the next feature and nothing waits for it. The icon audit turns auto_operation off, as it only looks at icons (75 s again). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Box placement test: the window keeps its New Body The test #6248 added places a box that overlaps Base and expects a second body; with #6901 the window would join or cut it. The test is about where the box goes, so it turns auto_operation off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Join pieces: read the target lists leniently after the rebase 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> |
||
|
|
261d00fbb2 |
History and browser: a sketch's icon in the colour of its state (#7245) (#7250)
* claim: #7245 * feat(ui): a sketch's icon in History and the browser takes the colour of its solve state (#7245) |
||
|
|
05f62b09e0 |
Browser menus: V runs the open menu's Show/Hide on the row it was opened for (#8377)
Most browser menus show V beside Show/Hide, but the V key always ran toggle_visibility, which acts on the selection. A right-click on a body or sketch row also selects it, so the two agreed there; a right-click on the Bodies, Sketches or Origin folder, a group, the component or an origin plane does not, so V did nothing, or flipped a body selected earlier, and left the menu open. With a menu open, V now runs that menu's row whose shortcut is V, through run_item as a click does, and closes the menu (context_menu::press_shortcut). A disabled row takes the key and does nothing. With no menu open, or a menu without such a row, V still acts on the selection (#4128, #4409: V stays selection-based). Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5d1c33fc82 |
Docs: the macOS app is notarized, and the update check names tangency-labs/solidcraft (#8402)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d26de76097 |
Updates: leave the update to the launcher that manages this copy (#8403)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
65784cae2f |
Save As offers the open file's own name, not the name stored in the design (#8329) (#8354)
* claim: #8329 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ * Save As offers the open file's own name (#8329) Save As, Save As JSON and Save a Copy proposed the design's name, the one stored inside the file. Copies of a file under other names keep the stored name, so "Sample Plate-bug.solidcraft" was offered "Sample Plate.sc", the same as its sibling, and one replaced the other. Session::save_as_name() offers the stem of the open file unless the design was renamed since it was opened or last saved, and the design's name otherwise (a new design, or one renamed in the Browser). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
dd2e44bb12 |
Raise the largest file SolidCraft opens with a setting or launch flag (#4086) (#7565)
* claim: #4086 * Raise the largest file SolidCraft opens with a setting or launch flag (#4086) * Translate the file size limit strings * Size limits: design files are .sc now, .solidcraft still read |
||
|
|
dca332201f |
Status bar: the sketch's state, what is being edited and the design's units (#7243) (#7248)
* claim: #7243 * feat(ui): status bar with the sketch's state, what is being edited and the design's units (#7243) |
||
|
|
b72165fd30 |
Sketch: show temporary centre and midpoint alignment guides (#7903)
* Sketch: show center and midpoint alignment guides Add transient per-viewport horizontal and vertical guides with a sketch preference. Related tests, native UI, WASM and supplementary Linux checks pass. Full CI remains blocked by unchanged Windows engine dead-code Clippy errors. * Sketch: complete alignment-guide translations * Tests: compile chosen-program mutex only on Unix |
||
|
|
c8c9534022 |
Create Sketch: the grid previews the origin plane under the pointer (#6975) (#7404)
* claim: #6975 * Create Sketch: the grid previews the plane under the pointer (#6975) While Create Sketch waits for its plane, the grid now lies on the origin or construction plane under the pointer, so the sketch's plane shows before the click. A plane's browser row under the pointer lights that plane in the view, and previews it the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Create Sketch: the planes at the top of the History preview too (#6975) main now draws the Origin rows at the top of the vertical History as well (history_rows). Hovering a plane there lights it like its row in the browser: the plane is handed over the same way the hovered bodies are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
aae2c5e417 |
Assemblies: edit a linked component in place, with the assembly around it (#7672) (#7819)
* Links: update linked designs that changed while the assembly's tab was away A design coming back to its tab compares the modification time and size of its linked design files with what they were when it went to the background, and runs Update Links on the worker thread for those that changed: saved in another tab, another window or by a checkout. Preference update_changed_links (Design, Linked designs) turns it off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: the assembly guide says linked designs update when their tab returns Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Links: Vietnamese strings, now that vi is a complete catalog Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Links: remember what each design last read, so an update that fails is tried again A design's linked files are now recorded as the design last read them: when Update Links reads a link without an error, or when the link is first seen. Coming back to the tab reads again what differs, one link per run, so a file that cannot be read holds up no other and is tried again the next time, and a part changed while the assembly was shown is taken up on its next return. Closing several tabs together reads nothing on the way, and the web build, which keeps no file times, looks at nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Links: no read during a save-changes prompt, records for adopted designs and missing files A tab shown to answer "save changes?" reads no link before its save, and Cancel there or on a batch lets the tab left in front read what it skipped. A design opened in place of the blank one records its links at once, a link whose file is missing is recorded as missing so it is read when the file comes back, and a read queued for a design that is no longer shown by the time it starts is dropped. More changed links than can wait at once are read on the next return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Assemblies: edit a linked component in place, with the assembly around it Activating a linked component opens the design it comes from in a tab of its own, with the rest of the assembly drawn faded around it where the component sits, so the part's own history, browser and commands are at hand. Finish Editing in the browser saves the part, goes back to the assembly and closes the part's tab when the edit opened it; coming back to the assembly updates the changed link (or Finish Editing runs Update Links when that is turned off). Update Links in the part also reads the assembly's links again, off the window thread, and redraws the assembly around the part; a result for an assembly changed meanwhile is dropped with a message. The part is an ordinary tab, so saving, autosave, recovery, closing and quitting treat it as any other design. The model scene's ghost triangles are now drawn (only the preview's were). Preference edit_linked_in_place turns it off. Builds on the update of changed links when an assembly's tab returns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: a double-click opens it, Activate only activates A double-click on a linked component edits it in place, in the browser as on a face in the view (around the placement clicked), and the component's menu gets Edit in Place. Activate and the browser's radio button make it the active component as they always did, so a linked component can still be activated without opening its design. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Double-click on a component's face: edit a linked one in place, activate any other A double-click on a face in the view activates the component the body belongs to, at that placement, as a double-click on its row does; a linked one is edited in place. Bodies of the design itself or of the active component are left alone, and so is a double-click with a modifier. One preference, double_click_components, covers both gestures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from review An edit whose part never opens is forgotten instead of starting on a later open of the same file, and the web build starts the edit after its synchronous open. Finish Editing goes on only for a save into the linked file and in the part's own tab, waits for a "changed on disk" answer, and says so when the part was saved elsewhere. Showing the assembly no longer drops a refresh, which now records the links it read so coming back reads nothing twice. The faded assembly is worked out once per picture, a bad mesh index skips its whole triangle, and the last two status texts come from the catalogs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: Finish Editing on the MODEL tab and the mouse's back button Finish Editing is a command now, component.finish_edit, with its schemas: the app window answers it, anywhere else nothing changes, as with view.fit. While a linked design is edited in place the MODEL tab ends with a FINISH EDITING panel, pinned at the right as FINISH SKETCH is, with a new left-arrow icon; the browser keeps only the line naming the assembly. The mouse's back button, or a keyboard's Back key, finishes too unless a dialog, tool or sketch is open. The assembly guide describes editing in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from the second review Saving the assembly waits while it reads its links for the part Finish Editing waits for dialogs, tools, sketches and prompts open in the part, also when run as a command The assembly reads the edited part explicitly when it does not check its links on its own The FINISH EDITING panel cannot be customized A tab opened by an edit stays that edit's to close when the part is entered again The mouse's back button finishes editing; the keyboard's only while no text field has focus Refreshing the assembly keeps its selection and active component Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from the third review component.finish_edit run as a command opens nothing and answers with an error when it cannot finish; engine.batch and engine.script refuse it A double-click or Edit in Place while a dialog, tool or sketch is open in the assembly keeps it open What changes in the assembly while it reads its links without changing its document stays: picks, activation, components dragged but not captured Showing the assembly while it reads its links starts no second read A new or opened design in the part's tab ends the edit Finish Editing keeps the part's tab open while its agent chat runs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: Update Links run as a command in the part refreshes the assembly too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: the ribbon button reads Finish Edit, as Finish Sketch does A bare "Finish" took the catalogs' surface-finish translation (German "Oberfläche"). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from the fourth review Finish Edit waits while the part's agent is in the middle of its turn, instead of keeping the tab of any chat open A part saved while its assembly refreshes is read again when the edit finishes: the refresh records the files as it found them A double-click on a linked component while something is open in the assembly says so instead of activating it Commands are recognised in any spelling the engine takes, so engine.batch and engine.script cannot slip Finish Edit through Update Links run as a command refreshes the assembly from the control channel only, not for the app's own reads Moves of components the refresh removed are not carried over, and a refusal is one message The command description and the guide say that the command does not save the part Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: the command is Finish Edit, as Finish Sketch is Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: a command to start it, and fixes from the full review Added component.edit_in_place {component, path?}: the double-click and Edit in Place in the menu run it, and as a command it asks nothing, so a network location not allowed yet or a design open elsewhere makes it fail instead Finish Edit asked by the part's own agent finishes once that agent's turn ends; the button still waits for the turn The parameters and design properties panels, with text typed and not applied yet, hold up entering and finishing an edit A refresh is not taken up under an undo, a queued command or a feature still being computed in the assembly Refusals say what holds them up (a refresh, a file being read or written) instead of nothing The browser strip names the assembly that is refreshing, not any assembly The schema description says the command does not save the part Korean strings for every new message Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from the second full review The MCP batch tool refuses component.edit_in_place and component.finish_edit as steps instead of sending them one by one Edit in Place asked while the assembly's agent is in its turn opens the part once the turn ends; a double-click then is refused rather than stopping the agent Both commands answer queued: true when they wait for the agent's turn, and stopping the agent forgets what it queued An undo, a queued command or a delete check running in the design holds up entering and finishing an edit A refresh redraws every edit of the assembly, the parts in other tabs too In an assembly never saved, a double-click activates the linked component as before The control and MCP docs list the new batch and script refusals, and the commands list every way they fail Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: fixes from the third full review The MCP batch tool refuses the two commands however a step names them, an execute step too A queued Edit in Place is checked fully when asked for, a second one is refused, and both queued requests wait for what runs in the design instead of being dropped Disconnecting the agent forgets what it queued, as stopping it does Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Edit in place: help text for the Finish Edit button main now requires help for every toolbar command (#7134), and Finish Edit is one. Written in every language the branch's catalog carries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
b68d42e5ec |
Theme: paste a theme into the Import tile (#8226) (#8227)
* claim: #8226 * Theme: paste a theme into the Import tile (#8226) Ctrl/Cmd+V on Preferences > Interface imports a copied theme file, the tile's menu offers Paste theme, and a theme file dropped on the tile imports instead of opening as a design. Pasted text that is not a theme keeps its usual meaning. The tile reads "file or paste". * Theme paste: the new strings in Arabic, Hebrew and Vietnamese too (#8226) |
||
|
|
4a34a258e1 |
Logo: an option to draw it in the theme's accent (#8133)
* Logo: an option to draw it in the theme's accent Preferences > Interface > Logo: Theme's colours (the default, as before) or Accent, three shades of the theme's accent. ui.prefs logo_accent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Logo: US spelling for the option's label, as upstream's English catalog uses Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Translations for the new options in the five complete catalogs (ar, he, ja, vi, zh) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3a951bc8c3 |
Sketch: dimension new shapes as drawn (squashed onto current main) (#5987)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4fa051c9fc |
View angles: an optional readout under the orientation gizmo and a command to set them (#6032) (#6249)
* claim: #6032 * View angles: an optional readout under the orientation gizmo and a command to set them (#6032) View > View angles (also Preferences > Graphics, or view.angle_readout) shows the view's pitch, yaw and roll in degrees and Ortho/Persp under the axis view or cube, updated live; off by default. Clicking the readout opens a small card to type exact angles, which runs view.apply. The angles are the camera's own (Camera::view_angles / with_view_angles), about the up axis: yaw is the heading of the line of sight (Front 0, Left 90, Right -90, Back 180), pitch the eye's elevation (Top 90, Bottom -90), and a positive roll turns the model clockwise. view.apply documents this convention and now refuses a pitch past +/-pi/2. 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> |
||
|
|
1e3cb3e36e |
Motion interference: a check cut short is not clear, and budget_s gives it time (#7855) (#7934)
* claim: #7855 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ * Motion interference: a check cut short by its work budget is not clear (#7855) motion.interference stops when its work budget runs out (truncated) but answered clear: collisions.is_empty(), so a check that reached only the first steps read as a pass: a gear pair checked for 14 of 61 samples was "clear". clear is now true only when every sample was checked and none collide. The budget is an argument of interference_within, so a test can cut a check short: B slides up to touching A, a full check is clear, and the least work that checks a first step stops after 1 of 16 samples with clear: false (it was clear: true before). The guide and the mechanisms skill say the same. The issue's second request, a budget_s on motion.interference, is not here. Refs #7855. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ * Motion interference: budget_s gives a check the time it needs (#7855) The work budget is fixed, so a gear pair drawn in detail was cut short after a few samples, or refused before its first one. motion.interference now takes budget_s (seconds, above 0, at most 86400, as inspect.interference allows): the check then runs until that time instead of the work budget, looked at before each sample, and echoes budget_s in its answer. Without budget_s nothing changes. The refusal before a first step now names the budget that ran out and suggests budget_s. Tests: given a time, the smallest work budget (which checks no step) no longer stops the check, through motion.interference too; budget_s 0, -1, 1e9 and "soon" are refused. Fixes #7855. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019FAWPuFoh9qxn2uS2j53QZ --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
8f8df03358 |
Navigation: Maya and HFSS mouse presets (#6474, #2742) (#8184)
Maya moves the camera with Alt and any button (left orbits, middle pans, right zooms), middle drag also pans. HFSS pans with Alt+left drag and zooms with Shift+Alt+left; its Ctrl+left orbit is the drag from empty space every preset already has, so a Ctrl+click on geometry still adds to the selection. The help texts are in the catalog for every locale that has the other presets' help. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
303fddb20e |
Browser: Bodies folders only where they help: Always, Several bodies only (the default) or Never (#4217) (#4219)
* claim: #4217 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Browser: Bodies folders only where they help: Always, Several bodies only (the default) or Never (#4217) A new preference on the Interface page, in the Layout section under Browser, sets when a component's bodies sit in a Bodies folder of their own. Several bodies only lists a component's lone body, when it has no groups, straight under its placement. Never lists every component's bodies and groups there. Always keeps the folder as before. The design's own Bodies folder and a folder hidden with its eye always stay. One rule, browser::bodies_folded, decides it for the rows and for the menu. A folded folder draws no row and stays open; its items move up one level, and their own eyes, hover and drops do its jobs. While a folder is folded, its placement's menu takes the folder's Create Components from Bodies and Group Selected; New Group and Show All were already there. ui.prefs takes browser_bodies_folders (always, several, never) and refuses other values; saved preferences read an unknown value as the default. The s37 scenario sets Always so that it still checks the Bodies folder's chevron. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Browser: Bodies folders in Arabic, Hebrew and Vietnamese too (#4217) Arabic, Hebrew, Japanese, Vietnamese and Chinese are complete catalogues, so every_complete_catalog_carries_every_english_message fails a new message any of them lacks. The preference's four messages get their Arabic, Hebrew and Vietnamese text, in the words those catalogues already use for a body and a folder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
03b8f4138b |
New designs save as .sc; .solidcraft files still open (#7325)
* claim: new designs save as .sc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * New designs save as .sc; .solidcraft files still open and keep their name on Save DESIGN_EXT is now "sc"; DESIGN_EXTS lists sc, solidcraft and solvecraft and every open path (dialogs, drag-drop, recent files, CLI, MCP, links, the web picker) accepts all three. Save of an opened .solidcraft file writes that file; Save As, autosave, recovery and crash reports use .sc. Windows, macOS and Linux register .sc alongside the older extensions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Update UI tests for the .sc default extension Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c0a0da225a |
Ribbon tooltips: help for the Solid > Modify tools, and new toolbar commands must have it (#2758) (#7134)
* Ribbon tooltips: help for the Solid > Modify tools; new toolbar commands need it (#2758) Writes the two-sentence help for the 16 Modify tools that had none (en, fr, ja, zh), and adds a test that fails when a toolbar command has no help; the 190 older ones are on a backlog that only shrinks. AGENTS.md, CONTRIBUTING.md and the Adding a command guide say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ribbon tooltips: the help test reads the ribbon as the toolbar lays it out (#2758) Review: commands placed by a workspace section without .at(...) were not checked; 79 of them join the backlog. Physical Material's help no longer promises the centre of mass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ribbon tooltips: French help uses the glossary terms (#2758) Matériau, Apparence and Scinder, as rlartot asked in review; Delete Face says the hole stays open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ribbon tooltips: the help rule fits the commands skill's size limit (#2758) AGENTS.md became a router to skills on main, so the rule lives in skills/solidcraft-commands/SKILL.md, kept under its 3600-character limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Ribbon tooltips: Arabic, Hebrew and Vietnamese help; the backlog follows main (#2758) Arabic, Hebrew and Vietnamese are complete catalogs now, so the 16 texts come in those too. Twelve toolbar commands that landed before the rule join the backlog; the Render commands, which left the ribbon, leave it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
83ac98ac76 |
Origin plane names: Top / Front / Right (#6491) (#6496)
* claim: #6491 * feat(ui): origin plane names Top / Front / Right (#6491) |
||
|
|
42ec27cfe4 |
Browser with History under it: drag the split between the two cards (#7371) (#7803)
* claim: #7371 * Browser with History under it: drag the split between the two cards (#7371) Under the browser, History took exactly half the column. The seam between the two expanded cards is now a grip with the vertical resize cursor; dragging it moves the split, as the column's right edge moves its width. The split is kept as the browser's share of the column in a new preference, browser_share, whose default is the equal split, so nothing changes until someone drags. Either card keeps at least its header and two rows. Folding either card works as before and gives the other the rest; while one is folded or folding there is no grip. A non-finite share falls back to one half. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FN5eEMrqWWXqi65FcK8GXq * Prefs: keep the browser share with the History-under-browser settings Moves the entry so it no longer sits next to the render and dialog preferences that main added after browser_width. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: sascha2101 <221609842+sascha2101@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
7a6c884418 |
Change the app id to io.tangencylabs.solidcraft, with aliases for existing installs (#7681)
The app id moves from ai.storyteller.solidcraft to io.tangencylabs.solidcraft: macOS bundle id (the CLI signs as io.tangencylabs.solidcraft.cli), Linux desktop/metainfo/MIME file names, icon name, Wayland app_id / X11 class, Flatpak, Snap common-id, nfpm/FreeBSD/Nix/AppImage staging, the web app's icon paths and the docs. Existing installs keep working: the Flatpak repository marks both old ids (ai.storyteller.solidcraft and ai.storyteller.solvecraft) end-of-life with a rebase straight to the new id, re-marking the solvecraft ref that pointed at the 0.8 id; the manifest grants read-only access to both old ~/.var/app folders and the first launch copies settings and autosaves from the newest one; AppStream <replaces>/<provides> and X-Flatpak-RenamedFrom list both; the AppImage removes menu entries it wrote under either old id. The data and settings folders (solidcraft / SolidCraft) are unchanged. The MSI UpgradeCode and ProgIDs are unchanged. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d2f0627421 |
Follow a theme file and reload it when it changes (#7573) (#7574)
* claim: #7573 * Follow a theme file and reload it when it changes (#7573) * Translate the follow-a-theme-file strings |
||
|
|
98abb249c4 |
Cylinder: place by distances from reference edges, like Hole (#6380) (#6381)
* claim: #6380 * Cylinder: place by distances from reference edges, like Hole (#6380) A Cylinder standing on a flat face can take up to two straight edges of that face as references, each with the distance of its base centre from the edge's line, measured in the face's plane, as a Single hole does (#37). `solid.cylinder` takes them as `references: [{edge, distance}]` with a numeric `base`; the edges are found again by their persistent names, else their points, so the cylinder follows them when the part changes, and each distance is a parameter (an input of Inspect Feature). A cylinder without references stays at its base, as before. In the Cylinder window, started on a picked face, Reference 1 and 2 take the edges, each with its Distance measured from the pick, so it works with the mouse alone; the canvas shows each distance with a box to type it in, and Edit Feature picks the edges again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cylinder: Edit Feature shows each reference edge input once (#6380) A face selected when Edit Feature opened the window had already added the reference inputs; the edit added them a second time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cylinder references: the measured-once cost test's cylinder has none (#6380) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cylinder: pick the face it stands on in its window, then the edges (#6380) The Cylinder window took its place only from a face selected before it opened, so started first it stood at the origin and offered no reference edges. It now has a Face input before Reference 1 and 2, as the Hole window does: start Cylinder, click the face, click the edges, OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Glossary: reference edge, and "Reference" among the words with more than one meaning (#6380) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cylinder edge references: the Cone dialog (#6250) gets the references field too The Cone primitive landed on main after this branch began; its dialog initialisers lacked the `refs` field the Primitive kind gained here. The dense form check for Cylinder fills only its fields: its face and reference edges place it, which the reference tests cover. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cylinder: declare references in its contract; reference-placed cylinders are not placement-independent 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> |
||
|
|
7b03439dda |
Navigation: Free orbit can turn around the vertical axis (#6964) (#6965)
* Navigation: Free orbit can turn around the vertical axis (#6964) A new option under Free orbit, Turn around the vertical axis, makes mouse drags a turntable without the pole limit: sideways turns about the world's up axis (reversed upside down so the model follows the cursor), up/down tilts over the top. Off by default; free orbit without it is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: Japanese and Chinese for the vertical-axis orbit option (#6964) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * i18n: Arabic, Hebrew, Vietnamese, German and Korean for the vertical-axis orbit option (#6964) Main now treats ar, he and vi as complete catalogs, so the merge queue's every_complete_catalog_carries_every_english_message failed on the two new ids. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhpMr6L9PR8SJXjM6MbQrD --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: danieldimitrov86 <danieldimitrovdev@gmail.com> |
||
|
|
c8c5c882de |
Sketch: name = value in a drawing tool's value box defines the parameter (#7567) (#7660)
* claim: #7567 * Sketch: a drawing tool's value box takes `name = value` (#7567) While drawing, a value box (circle diameter, line length, rectangle width and height, slot, arc, polygon) now accepts `name = expression` and `#name = expression`, as the Dimension tool already does: the shape is drawn at the expression's size and its dimension defines and uses the user parameter `name`, in one undo step. - sketch.draw.dimensioned checks a definition's right-hand side, read as the new parameter will hold it (a bare number in the design's unit), and passes the text on to sketch.dimension; the reply lists `defined`. - The value boxes size the preview and validate by the right-hand side, refuse a definition in a box that is not a dimension (polygon sides, rounded rectangle corner radius, conic rho), and the status line names the parameters defined. - Guides: parameters and sketching. Fixes #7567 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5a477b7774 |
Dialogs: open in the left column in place of the browser and History (#6707) (#6712)
* claim: #6707 * feat(dialogs): open in the left column in place of the browser and History (#6707) * fix(i18n): Vietnamese messages for the dialog placement option (#6707) |
||
|
|
236b5c9ebf | Add the built-in 1-bit and 1-bit Dark themes (#7550) (#7557) | ||
|
|
04e4e72ea1 |
History: delete a folder with its features, and suppress a whole folder (#7219) (#7220)
* claim: #7219 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * History: delete a folder with its features, and suppress a whole folder (#7219) `timeline.delete` and `timeline.suppress` take `groups: [timeline folder ids]`: every feature in those folders and their nested folders, in one undo step. A delete takes the folders themselves too; `timeline.ungroup` still keeps the features. The History on the left offers Suppress/Unsuppress Selected Folders and Delete Folders and Features… (asking first, like Delete Selected Features…, when something outside needs them); a folder whose features are all suppressed is drawn faint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * History folder actions: Arabic, Hebrew and Vietnamese strings (complete catalogs) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
2151bc0497 |
Drive Joints: say why a rigid joint can't be driven and offer Edit Joint (#6981) (#7051)
* claim: #6981 * Drive Joints: say why a rigid joint can't be driven and offer Edit Joint (#6981) Drive Joints opened on joint 0 when no joint moved, and its preview ran joint.drive on that rigid joint, so the dialog showed a red "a rigid joint does not move" with a Create Issue button. The dialog now says a rigid joint holds its parts still and how to let them move (Revolute to rotate, Slider to slide, Cylindrical for both), with an Edit Joint button that turns it into Edit Joint on that joint. Its commands refuse a rigid joint, so nothing is previewed and OK stays off. A rigid joint picked in the browser opens Drive Joints on it to say the same; the joint box lists only joints that move. joint.drive's own refusal names the types that move and joint.edit. * Drive Joints: the rigid joint message in Arabic, Hebrew and Vietnamese (#6981) Main now delivers ar, he and vi complete, so every new message needs them. --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> |
||
|
|
ae530bff44 |
Typed contracts for the cylinder, sphere, torus, cone, pipe, bounding solid and scale (#2829) (#7145)
* Typed contracts for the cylinder, sphere, torus, pipe, bounding solid and scale Contracts for solid.cylinder, solid.sphere, solid.torus, solid.pipe, solid.bounding_solid and solid.scale, with `FeatureResult` output, and the shared types `AxisDir` (calling `cmd::axis_dir_form`, split from `axis_dir_param`), `NumberExpr` and `ScaleFactors`. The descriptions say what the evaluation does: the limits it enforces, the real default scope of `targets`, and where `participants` overrides a key or has no effect. Scale makes no body, so it declares `name` and `participants` itself. Box's descriptions get the same corrections. The reply test runs every form in Warn and checks for no `param_warnings`; the guide lists the new types and adds lessons from the conversion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Typed contract for the cone `solid.cone`, added on main beside the primitives this PR converts, gets its contract and `FeatureResult` output: the cylinder's keys plus `top_radius` / `top_diameter`, with the evaluation's limits in the descriptions (a radius above 0, a top radius of 0 or more, a height that is not 0 and may be negative, sizes within 1e7 mm). A `null` top radius stays the handler's to refuse, since the handler takes it beside a `top_diameter`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5fc23436ca |
History: show the origin and bodies at the top (#6490) (#6495)
* claim: #6490 * feat(history): show the origin and bodies at the top (#6490) |
||
|
|
6e12f5954f |
Navigation: snap free orbit direction and roll with Shift (#6943) (#6949)
* claim: #6943 free orbit Shift snapping * fix(ui): snap free orbit direction and roll with Shift * test(ui): align free orbit coverage and complete snap help translations |
||
|
|
2d629069c6 |
Dialogs: OK and Cancel at the top (#6708) (#6713)
* claim: #6708 * feat(dialogs): OK and Cancel at the top (#6708) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f1cc49bcdb |
History: list a sketch under the feature built from it (#6489) (#6494)
* claim: #6489 * feat(history): list a sketch under the feature built from it (#6489) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
afd6a5c584 |
Send to Slicer: send the selected bodies to a slicer (#2595) (#3600)
* Add Open in Slicer for the selected bodies (#2595) One command saves the selected bodies as a 3MF in millimetres (an STL if the 3MF can not be written) and opens it in the last slicer used. It is on the File menu, the Mesh tab and the body right-click menu. The first time, or when that slicer is gone, it asks which one with the same list Save as mesh uses. The Save as mesh dialog is unchanged. * Send slicer files to a private per-user folder (#2595) Files sent to a slicer used to go to a shared folder in the temp directory. They now go to the user cache folder (mode 0700 on Unix), under a name that stays the same for the same design and bodies, so sending again replaces the file and the slicer can reload it. The file is written to a temporary name and renamed into place. If the name can not be replaced, as on Windows while the slicer holds the file open, the copy gets a number and the status line says so. Files older than seven days are deleted at startup. The first send tells the user that sending again replaces the open file, and so does the tooltip of Open in Slicer. Send to slicer in Save as mesh uses the same path. * Open files in a running slicer (#2595) PrusaSlicer, SuperSlicer, Bambu Studio and Cura are started with --single-instance, so a slicer that is already open loads the file instead of opening a second window. OrcaSlicer has no such flag and is left alone, and macOS bundles are single-instance already. PrusaSlicer 3.0 ships a launcher entry that already carries --single-instance-on-url; it keeps that one and never gets a second flag. It is a preference, on by default, and scripts can pass single_instance. Preferences also gets a button to forget the slicer Open in Slicer remembers. * List slicers the desktop entries say open 3MF or STL (#2595) On Linux the scan of desktop entries now also lists a program that claims 3MF or STL in MimeType, even when it is not in our table of known slicers. The table still supplies names, ids, icons and flags for the ones it knows. Entries marked NoDisplay or Hidden are skipped, and so are terminal programs and SolidCraft itself. The existing size and count limits stay. The Windows scan now looks inside a Prusa3D folder, where PrusaSlicer installs itself. * Only start slicers the user can see or chose (#2595) file.save_mesh took a slicer parameter that could be any executable path, so a script or an MCP client could run any program. The slicer now has to be one of the detected slicers (by id or name) or a program the user already added under Custom, which the app hands to the engine from its settings. The same rule holds for file.open_in_slicer. The agent chat already asked before any file command; its question now also names the slicer. * Review fixes for Open in Slicer (#2595) The file name hash keeps all 64 bits instead of 32. When Open in Slicer has to fall back to an STL, the status line now says so. The engine test that sets the chosen programs clears them again. The unused "could not make the folder" message is gone from the catalogs, and the docs say plainly that finding slicers by file type is Linux only. * Keep unsaved designs apart and hide Open in Slicer in the browser (#2595) Two unsaved designs with the same name and body names got the same file in the slicer folder. Every document now has a uid that is new for each New, Open and Recover and stays the same for the life of the document, and the file name uses it (with the process id) until the design has a path. At the first Save As the name changes to the path-based one, so the slicer gets one new file; the old one goes with the seven-day sweep. The browser can not start a program, so Open in Slicer is now disabled there: the engine command reports why, the File menu item and the body and component right-click items are greyed, and starting it anyway says so on the status line instead of opening a dialog. The Save as mesh dialog keeps its browser guard for the open flow too. * Put the Session doc comment back on Session (#2595) The new NEXT_UID static had landed between the doc comment and the struct it describes. * List every install of a slicer, and test the first Save As (#2595) Detection kept one entry per known slicer, so a system PrusaSlicer 2.9 and a Flatpak 3.0 beta showed up as one. Every distinct install is listed now. The same program found twice, through a desktop entry and through PATH, is still one. When a slicer has several installs, each gets an id made from where it was found (prusa#comprusa3dprusaslicer) that stays the same on every run, and a name worked out from its command and location without running anything: PrusaSlicer (system), PrusaSlicer (Flatpak, beta), PrusaSlicer (AppImage). A lone install keeps its plain id and name. The plain id or name still picks the first install, so a remembered "prusa" keeps working. Single-instance flags and the other per-slicer behaviour go by the id without the install part. Also a test that the first Save As changes the file name once and later sends keep the name that comes from the saved path. * Start PrusaSlicer 3.0 first and send the file once it is ready (#2595) PrusaSlicer 3.0 starts through slic3r-app-launcher, which ignores a file given at a cold start whatever the flag, and loads it over D-Bus when an instance is already running. A small table in slicers.rs names such slicers by their command; today it holds that launcher, so PrusaSlicer 2.9 and OrcaSlicer are untouched. Ready means a name starting with com.prusa3d.prusaslicer.BroadcastListener. is on the session bus, read from busctl --user list with no new dependency. If busctl is missing or fails, nobody waits and the file is sent as before. When the slicer is not ready, Open in Slicer starts it without the file and looks every 200 ms for up to 10 seconds. The wait runs on another thread, so the window stays live, and the status line says Starting PrusaSlicer. Once the name appears the file is sent with the same arguments as before. If the limit passes, the status line says the slicer did not respond, that the file is already saved, and to send again. A second click during the wait only repeats the Starting line, so there is one launch and one send. Scripts calling file.open_in_slicer wait inside the command by default, or pass wait: return or no. * Always show the slicer picker for Open in Slicer (#2595) File > Open in Slicer, the Mesh tab entry and the right-click entry now always open the picker, with the slicer used last time pre-selected, so the usual case is one click or Enter. Scripts and file.open_in_slicer with a slicer still send without a dialog. The preference button that forgot the remembered slicer is gone: it could no longer make anything ask again, and it did not clear the pre-selection either. The guide says how it works now. * Leave Bambu Studio out of the single-instance list (#2595) Open files in a running slicer now adds --single-instance for PrusaSlicer, SuperSlicer and Cura only. Bambu Studio is started as it is, so it may open a second window. A test pins that, for the detected slicer and for one the user picked as a custom program. The guide and the preference tooltip no longer name Bambu Studio as getting the flag. * Sort the English catalog and leave features.md to the generator (#2595) cargo xtask lists wanted one message moved in en.json after the rebase. docs/features.md is written by the Generated files workflow on main, so the row I had added by hand is gone from this branch; the workflow adds Open in Slicer when it lands. * Drop the unused folder message from the Vietnamese catalog (#2595) Vietnamese arrived on main after I removed ui.save_mesh_temp_failed from the other catalogs, so the check that every translated id exists in English failed on it. Nothing uses the message any more. * Say so when a started slicer can not be found again (#2595) After Open in Slicer had started a slicer on its own, a failure to look it up again to send the file just returned, so the status line stayed on Starting and then nothing. It now says the slicer was started, that it can not be found to send the file, and where the file is saved. A test covers it. * Say why a failed busctl ends the wait (#2595) If busctl fails partway through a wait, the wait stops and the file is sent, the same as when busctl can not be run at all. That is on purpose: a file sent too early is dropped quietly and costs one more send. A comment in wait_for_ready says so. * Keep the readiness wait inside its limit (#2595) wait_until asked the probe before checking the deadline, including right after a sleep, so a yes could be accepted after the limit, and busctl ran with no timeout, so one look could outlast the whole wait. The deadline is checked first now, the probe is told how much time is left, and busctl is given at most two seconds (less near the end of a wait) and is killed after that. A hung busctl counts as not telling, which sends the file as before. This covers the first look on the UI thread too. Tests cover a slow probe, the shrinking time left, and a fake busctl that hangs, answers and fails. * Send the file that was already written once the slicer is ready (#2595) When the wait for a started slicer ended, the UI ran file.open_in_slicer a second time. That exported the design again on the UI thread, from whatever the session looked like by then, and could write a numbered copy while the timeout message still named the first file. The wait now keeps the slicer it found and the answer of the first call, and opens the file that was written. A test changes the design during the wait and shows the same file is sent, is not written again, no command runs, and no second file appears. * Send with the single-instance flag after starting a slicer (#2595) After Open in Slicer has started a launcher that needs a start first, the file has to reach the instance that is now running, so the send that follows a start carries --single-instance whatever the preference says. A normal send, to a slicer that was already up, still follows the preference. A comment in both places says why. The test uses a plain launcher command with the preference off: the normal send has no flag and the send after a start has it. * Find the remembered slicer again after an uninstall (#2595) With two installs the remembered key is prusa#prusaslicer. Remove the other install and the lone one is plain prusa, which matched neither the exact key nor the plain-key fallback, so the picker fell back to the first slicer in the list, maybe a different one. The key is now also matched by the part before the #, so the same slicer is pre-selected. A test covers the lone install, a plain key with two installs, an exact key, and that another slicer is never picked. * Say a second click did not send anything while a slicer starts (#2595) While a started slicer is being waited for, another click showed Starting <the first slicer> even when it had picked another slicer or other bodies. It now says it is still waiting for that slicer and that nothing else was sent. The test checks the wording and that it names the slicer being waited for. * Label macOS installs by their application (#2595) An open -a command resolved its program to /usr/bin/open, so every application on a Mac was labelled system. Labels for open commands now come from the .app path: /Applications is system and a folder under home is shown as ~/Applications. A test with two PrusaSlicer.app bundles and a made-up home checks both labels and that the ids differ. * Pick the same place for a program found twice every time (#2595) When two desktop files, or a desktop file and PATH, pointed at one program, the first one read was kept and its file name went into the id, so the ids could change with the order the folders were read in even though nothing was installed or removed. The place that sorts first now speaks for the program. A test with two aliases and a second install lists them with the folders in both orders and gets the same ids and commands. * Treat flatpak run spelled two ways as one install (#2595) flatpak run ... and /path/to/flatpak run ... with the same arguments had different install keys, so one invocation could be listed twice. The launcher is now compared by its file name and the arguments are kept, so both give the same key, while a different branch is still its own install. The test uses a real launcher file so the full-path entry is not skipped as stale. * Say plainly that a late yes is accepted on purpose (#2595) The comment on wait_until said a slow probe could not accept a yes that came too late. That was not what the code does: no probe is started after the limit, but one already running may finish and its yes is taken, because the file should still go. The comment now says so, with the bound: the limit plus that last probe, which in production is given the time left and so ends by the limit. A test has a probe that runs past the limit and says yes, and checks it is accepted and the wait stays within limit plus the probe time, and that a probe that says no is not followed by another. * Give reading busctl the same deadline as running it (#2595) After busctl exited, the output was read with a join that could wait forever if a process it left behind kept the pipe open. The reader now hands the text back over a channel and the wait uses the time that is left of the same limit, and gives up with nothing otherwise. The polling sleep no longer overshoots the time left either. A test uses a fake busctl that backgrounds a sleep with its output inherited and exits at once: the call returns within its limit. * Tell two different flatpak launchers apart (#2595) Comparing only the file name of the launcher made /opt/tool-a/flatpak run ... and /usr/bin/flatpak run ... with the same arguments one install even when they are different programs. The launcher is now found the way any program is, through PATH and with links followed, and its name stands in only where it can not be found. The test has flatpak on PATH and the same file by its path as one install, a different executable of the same name as a second, a different branch as a third, and the fallback when there is no PATH. * Keep waiting when busctl is slow, send only when it is missing or fails (#2595) Since the busctl timeout, a slow busctl came back as "can not tell", and that was read as send now. One slow look, likely just when a cold PrusaSlicer 3.0 is starting under load, ended the wait and sent the file early, which gets dropped. The look now says why it has nothing: slow means not up yet, so the wait goes on to its limit and then reports that the slicer did not respond; a missing or failing busctl still means it can not be told and the file is sent. The comments name only those two cases. Tests use fake busctl programs: one that hangs keeps the wait going to the limit and times out, a missing one sends at once, one that says not up and then fails sends, and one that names the slicer is ready. * Do not open an empty path when the answer had no file (#2595) If the answer kept for a started slicer had no path, the send ran open with an empty string. The tick now says there is no file to send and starts nothing. The test gives a wait with no path, checks the error line, and that the fake slicer was never run. * Drop the unused folder message from the Danish catalog (#2595) Danish arrived on main with ui.save_mesh_temp_failed, which this branch removed from English because nothing uses it any more, so the check that every translated id exists in English failed. The entry is gone from the Danish catalog too. * Always start the 3.0 launcher with the single-instance flag (#2595) A hand run showed that starting the PrusaSlicer 3.0 launcher with no file while a 3.0 is already running forwards to it and exits when the flag is there, but opens a second full window when it is not. A slow first look at the bus can start the launcher twice, and with the preference off the flag was stripped. A slicer that has to be started first now keeps the flag on that start whatever the preference says, the same reasoning as the send after a start; when nothing runs the flag does no harm. A normal send with the preference off still has none. The test covers the launcher entry, a bare launcher command, a normal send and a slicer that is not on the table. * Swap --single-instance-on-url where it does not hand the file over (#2595) A hand run on the PrusaSlicer 2.9.6 Flatpak showed that its entry's --single-instance-on-url does not forward a file to the running instance: every send opened a new one, three after a cold start and two sends. With plain --single-instance the second process finds the first, forwards, and exits. So when a command carries --single-instance-on-url and the slicer is not on the wait table, that is, not the 3.0 launcher, it is replaced by --single-instance, with the file markers handled as before, when the preference is on, and stripped when it is off. On the 3.0 launcher the flag does forward and stays. The comment on the rule says why. A test uses the real 2.9.6 and 3.0 Flatpak command lines. * Cut long slicer names to fit the picker (#2595) With installs listed on their own, a name like PrusaSlicer (Flatpak, master) is wider than its row. The list painted it with no limit, so it ran out of the popup, and the closed selector wrapped it onto a second line. The list now lays the name out on one line that ends in an ellipsis when it does not fit, the selector truncates, and the full name is in the tooltip when it was cut (for a program the user added, ahead of the delete hint). A test checks that a long name is cut to one line within the width and a short one is left alone. * Keep the 3.0 launcher's flag on a send whatever the preference says (#2595) A hand run with the single-instance preference off showed the 3.0 launcher needs its flag on a send too, not only on a start: a cold start with a file and no flag loaded nothing, and a send to a running 3.0 left a launcher process that never forwarded. A slicer on the wait table now gets the flag on every launch, as it already did when started alone. The doc comment says why. The preference text, in English, Japanese and Chinese, and the guide now say that PrusaSlicer 3.0 is the exception to turning this off. The tests that took the flag away with the preference off now expect it, for the launcher entry, a bare launcher command and the 3.0 Flatpak line; the test of the send in the UI checks the first send carries it. * Keep the install-flag swap to PrusaSlicer, found by what it is (#2595) The swap of --single-instance-on-url for --single-instance ran for any program whose command had the flag, so another program with that flag would have been rewritten. It is now a PrusaSlicer rule: it applies to a slicer recognised as PrusaSlicer by its identity, its id or, for a program the user picked, the name it goes by, never by a version number. The single-instance table uses the same identity check. A test keeps the flag of a program that is not PrusaSlicer, strips it when the preference is off, and swaps it for a picked program named PrusaSlicer. * Do not promise a hard bound in the wait comment (#2595) The comment on wait_until said the production probe ends by the limit. Its clock starts after the process is spawned, and killing and reaping the process and scheduling are not bounded by it, so it is a polling and read budget with process overhead on top. The comment says that. Comment only. * Tell PrusaSlicer 2 from 3 by the installed version (#2595) PrusaSlicer 2 and 3 need different handling, and the rules had to guess from the command line. Each install of PrusaSlicer now carries its major version, read when it is detected from the release at the top of its appstream metadata and never by running the program: a Flatpak's own files (the branch the command names, else the current one), or the metainfo file beside a desktop entry. PrusaSlicer older than 3 is sent the file with --single-instance in place of --single-instance-on-url, which does not hand a file over there, and is not waited for. PrusaSlicer 3 and newer keeps the launcher's flag, is started on its own, waited for, and sent the file. When the version can not be read, the command decides as before: the slic3r-app-launcher launcher counts as 3, and any other entry is left as it is. A program that is not PrusaSlicer is untouched whatever its flags. Tests use metadata fixtures, no Flatpak: 2.9.6, 3.0.0-alpha10, a branch that is not installed, a system package, an unreadable version with and without the launcher, and a non-PrusaSlicer program with the same flag. * Make the busctl wait tests steady under load (#2595) One of them failed once in a full run at its first assertion and passed alone. A script that has only just been written can fail to start (text file busy) while another test forks, and that reads as a busctl that is missing. The fake programs are now started once, patiently, with a flag they answer at once, before a test relies on them; a hanging one uses exec so it is killed cleanly. The time bounds that could trip on a busy machine are several seconds now, and the checks are on outcomes: not ready and keeps waiting, send now, and the number of looks. Thirty runs alone, thirty of the whole slicers filter and thirty of the busctl tests under eight busy loops all pass. * Say what the busctl reader thread leaves behind (#2595) The thread that reads busctl output is left to end when the pipe closes. A busctl that answered but left a process holding its stdout reads as slow, and every poll of such a bus can leave one blocked reader behind until that process lets go. A comment next to the thread says so. Comment only. * Make Save as mesh a plain export (#2595) Save as mesh asked for the bodies and the format, and could also send the file to a slicer with a checkbox and a slicer list. Sending now has its own command, so the checkbox, the list and the Send label are gone from this dialog: bodies, format, a file dialog, Save. The two settings that only the checkbox used, save_mesh_send and save_mesh_slicer, are no longer kept; a settings file that still has them loads, and they are ignored. The strings only they used are gone from the catalogs. The slicer list stays in the dialog of Open in Slicer. file.save_mesh keeps its slicer parameter for scripts. The guide describes the two as separate. * Rename Open in Slicer to Send to Slicer (#2595) The command sends a file to a slicer rather than opening one, and a later Send to App can share the name. File menu, Mesh tab, right-click menus, the dialog button, status lines, the guide and the English, Japanese and Chinese strings now say Send to Slicer, and the command id file.open_in_slicer is file.send_to_slicer. It is new in this change, so there is no alias. The remembered slicer is stored as send_slicer, and the question the agent chat asks says send to. Module and type names stay as they are. * Put the send targets in one table and one send pipeline (#2595) The known slicers, the list of slicers with a single-instance flag and the rule for slicers that must be started and waited for were three separate things. They are one table of targets now: id, label, kind, names and program files, accepted formats and the default, how the file is handed over, the single-instance flag, the version from which PrusaSlicer's on-url flag hands a file over, and the wait-for-ready rule. Programs that are not in the table are slicers with the same defaults. The kind is slicer, the only one so far; its comment says it is where a later Send to App would plug in. The send itself is one function: write the bodies, find the target, start it and wait if its rule says so, hand the file over, report. Detection and resolution take a kind. Nothing changes for what already worked. file.save_mesh keeps its slicer parameter, now through the same function, with its export arguments and result unchanged: a cold PrusaSlicer 3 is started and waited for before the file goes, and it accepts the program path of a detected slicer as file.slicers lists it (file.slicers gives it as program). A path is accepted only when it is the program of exactly one detected target, and that target's own command and arguments run; a launcher several entries share, such as /usr/bin/flatpak, is refused as ambiguous, and any path nobody listed or chose is refused. The parameter and the code say it may be deprecated later in favour of file.send_to_slicer or a Send to App workflow, which is a possibility and not a plan. Tests go through file.save_mesh itself for the two reported failures, a listed AppImage path and a cold PrusaSlicer 3 that is waited for, with the slicers and the busctl made up; and they check that the result and the file are what an export without a slicer gives, and that a path that is not listed, or is shared by two entries, writes nothing. The pipeline has its own tests: a cold launcher waited for, one already up, wait return, and one that never comes up. A test of save_mesh that shared a file name with another is given its own. * Keep an AppImage's id through an upgrade (#2595) When a slicer has several installs, each gets an id made from where it was found. For an AppImage that was its file name, which carries the version, so upgrading it changed the id and the remembered choice missed. The id is now made of the slicer's id, the fact that it is an AppImage and a short hash of its folder, so a new version that replaces the old one in the same folder keeps the id, and the same AppImage in another folder is its own install. Two versions kept side by side in one folder are two installs: while both are there, their file names are added to their ids to tell them apart. A remembered id is found again only when it names exactly one install. An id that was one AppImage and is now two versions, or a plain id that is now several installs, is treated as not remembered, so the picker starts from the first entry as on a first run and does not quietly pick another install; a single install left of the same slicer is still found, as before. Installs that are not AppImages are told apart as before. An id from before this change no longer matches. Tests cover an upgrade, two versions side by side, a second folder, and the picker's side of each case. * Add Open with EasyPrint to Send to Slicer EasyPrint is Printables' slicer in the browser. It's a plain text row in the Send to Slicer dialog, backed by a new file.open_in_easyprint command. The command exports the bodies as a 3MF (or STL), hands them to the host to upload to slicer.printables.com/import, and the app opens the link that comes back. The desktop app uploads with ureq and the browser build with fetch, so this is the one send target that works on the web. The model leaves the machine, so the command refuses without consent: true, the dialog asks once and remembers, and an AI agent's request asks first. A model over 100 MB is refused before anything is sent, and a failed upload shows as an error with the reason. The chat's privacy note said SolidCraft sends nothing anywhere, which isn't true any more, so it now says that about the chat. New strings are in every catalog. * Place Send to Slicer on the Insert Files panel |
||
|
|
c391ef17cb |
Themes: Clay (light) and Clay Dark, warm paper and charcoal (#7342)
* Themes: Clay (light) and Clay Dark, warm paper and charcoal with a terracotta accent Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Clay: on-accent text a touch lighter for 4.5:1 on the accent Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
436ee95095 |
Preference: return to the previous view when a sketch ends (#7038) (#7043)
* claim: #7038 * Preference: where the view goes when a sketch ends (#7038) |
||
|
|
47285ccb88 |
Thread: right- and left-handed direction (#4414) (#4415)
* claim: #4414 * feat: persist right and left thread handedness (#4414) * fix(thread): verify handedness with referenced holes and complete locales * test: verify handedness with merged tapped thread catalogues * test: retain tapped thread depth when changing handedness (#4414) * test: restrict Unix symlink coverage to Unix targets * test: inspect hole thread depth inside edit command parameters (#4414) * test(doc): initialize cylinder radius in handedness fixture |
||
|
|
e063a7778e |
Axes and points from circular edges and round faces (#3128) (#4610)
* Axes and points from circular edges and round faces
In 3D, a round edge or face stands for its circle wherever a command
takes an axis or a point.
Revolve, Surface Revolve, both Circular Pattern dialogs, Plane at Angle
and Move -> Rotate take a circular edge (through its centre, along its
normal) or a cylinder, cone or torus face (its axis). The features keep
the pick as a model reference, so they follow the model when a parameter
moves it. Straight edges work as before; flat faces are refused.
Every 3D point input takes a circle's centre: construction points (which
follow the circle), To Object, patch points, hole placement, and Move's
pivot and points. Hovering shows the axis as a dashed construction line
or the centre as a star, and the dialog's chip names what the pick
stands for ("centre of edge 10", "axis of face 3").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Format the round-pick hover check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Selection chips: name a round pick as a circle centre or axis
The chip has room for little more than the body and a word or two:
"Shaft · circle centre", "Shaft · circle axis", "Shaft · face 2 axis",
and "Shaft · point" for a point of the model.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Move: a circle's centre in a placed component is where the component shows it
Move's pivot and points read a picked circle from the model in its
component's own frame, while the pick is in the world. They now read it
from the world state (and, editing a Move, from the state before it,
placed the same way), as the viewport and the other round picks do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Plane at Angle on a planar face about a model edge: test that it follows both
Upstream #1956 made a planar face a base for Plane at Angle; this branch makes a model edge an
axis. The rebase keeps both in one PlaneRef::AtAngle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
3bcf77ee32 |
Expressions: a number and its unit are one value (10 mm / 2 mm is 5) (#6331)
* Expressions: a number and its unit are one value (10 mm / 2 mm is 5) A unit written after a number was multiplied in at the precedence of `*` and `/`, so `10 mm / 2 mm` was (10 mm / 2) mm = 5 mm² and `90 deg / 1 rad` a value in angle², reported as a unit error. The number now takes the unit after it before `*` and `/` apply; a fraction of numbers before a unit stays one value (`1/2 in` is half an inch), and an exponent stays a plain number (`2^3 mm`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Expressions: after a division, a fraction before a unit only when closed up `width / 10 / 2 mm` read the `10 / 2 mm` as the fraction 5 mm and gave width / (5 mm) = 8 instead of (width / 10) / 2 mm = 2. After a `/` the fraction rule now needs the fraction written without spaces (`1 in / 1/2 in` is still 2); elsewhere `1/2 in` and `10 / 2 mm` are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
91638cb1f3 |
Add practical command guides and a contribution template (#5237) (#6772)
* claim: #5237 command-level guide follow-up * docs: add practical command guides and resource contributions (#5237) * docs: index practical guides and reject incomplete command links |
||
|
|
66b872a749 |
Web app: open a design from a same-origin path (#6670)
?open= now also accepts an absolute path on the page's own origin, fetched with the page's scheme, so a self-hosted setup that serves SolidCraft and other tools under one origin can link a design on a LAN without a public HTTPS address. Absolute URLs keep the HTTPS-only rule (a URL on the page's own origin is also accepted), redirects must end on HTTPS or the same origin, and protocol-relative, dot-segment, backslash and control-character values are refused. |
||
|
|
c7e53899af |
Updated AppImage takes the release file name (#3538) (#3658)
* claim: #3538 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> * Updated AppImage takes the release's file name (#3538) A versioned AppImage name (solidcraft-<version>-linux-<arch>.AppImage) is replaced by the new release's name, so zsync-based tools keep finding the update data. Other names are kept. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com> |
||
|
|
d3720041b6 |
CI tooling: opt-in bound for local test concurrency (#1188) (#6831)
* claim: #1188 * #1188: split per-binary test threads and matrix workers under SOLIDCRAFT_TEST_JOBS --------- Co-authored-by: Dahak Builds <dahakbuilds@users.noreply.github.com> |
||
|
|
72a76edcf7 |
Shortcuts: Ctrl+Shift+S opens Save As in the workspace (#6558) (#6567)
* claim: #6558 * Shortcuts: Ctrl+Shift+S opens Save As in the workspace (#6558) The Home page already opened Save As with Ctrl+Shift+S, but the design workspace matched Ctrl+S, so the chord saved over the current file instead of offering another one. The workspace now routes the chord to Save As, and the File menu shows that shortcut next to Save As, taken from the command registry. |
||
|
|
c264c7a42a | Startup falls back from a Direct3D 12 that can't build the UI pipeline (#6648) (#6682) |