Compare commits

..

97 Commits

Author SHA1 Message Date
CamWight c8cbe43dd9 Drawings: drawing.status says which dimensions the sheet shows as they last were (#12980)
A deferred drawing that holds shows each dimension's last reading until drawing.update. drawing.status now says so
for each one, as it says so for each view: a dimension whose view is out of date, or one read from another model or
record than it has now, has `out_of_date: true`. `held` is true when views or dimensions wait. Before, a change that
left the views current (a sketch, which shows the same) held their dimensions with no sign in the status.

The list comes from one rule, `out_of_date_dimensions`: the rule `drawing.scene` and `drawing.export` wrote inline for
their own `out_of_date`, moved into one function that they and the status share. `standing` gives it the views out of
date and the stale dimensions of every sheet. The status
reply's schema says what the new field means.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 07:49:04 -05:00
CamWight 7d4befc1b9 Drawings: dimension a sphere's size as SØ or SR, and a cone's included angle (#4896) (#12910)
* Drawings: a sphere's size reads SØ when the sphere is whole and SR on the arc a dome shows

A dimension can now name a face on a sphere (drawing.dimension.add {sphere: {face}}). It reads SØ, the
diameter, when the body's faces on that sphere are the whole sphere, and SR, the radius, when they are
part of one (a ball end, a dome, a cap), as ISO 129-1 writes them.

Both are decided on exact geometry, with no mesh. Whole or part comes from the record's topology: the
faces are the whole sphere when every edge of their loops is used only by them. SR is read on the arc of
the sphere's contour in the view that lies on the faces: the contour is cut where it crosses each rim
circle's plane, and one point classification at each arc's middle says whether it is on them. Arcs on
them next to each other join, and the longest run carries the number. A view that sees only a cap's rim
refuses it.

The cylinder's rule that a name's faces all lie on one surface is now one function for both kinds
(Lookup::on_surface), and the number's label comes from the measure where the kind alone doesn't say.
The paper draws a sphere as a circle's or an arc's radial dimension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawings: a cone's included angle reads between its two silhouettes

A dimension can now name a face on a cone (drawing.dimension.add {cone: {face}}), such as a
chamfer's, a countersink's or a drill point's. It reads the cone's included angle, twice its
half-angle, between the cone's two silhouettes in the view. It is foreshortened when the view's
plane doesn't hold the axis, and a view along the axis, or within the half-angle of it, is refused.

Both silhouettes must lie on the body's faces on the cone, so half a cone is refused. This is
decided on exact geometry, with no mesh. Each silhouette is a ray from the apex, cut where it
crosses each rim circle's plane, and one point classification at each piece's middle says whether
it is on the faces. The pieces on them join, as a sphere's contour arcs do: one function now joins
runs for both.

In crates/geom, drawing_measure gains Cone and its two silhouettes, worked out analytically from
the apex, the axis and the half-angle. It holds no kernel geometry. Faces lie on one cone when they
share its apex, its axis's way and its half-angle, within the cylinder rule's margins; the kernel
reports a cone's surface but has no rule of its own for that. The paper draws the angle as an
angle dimension between the two silhouettes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:26:00 -05:00
CamWight ce252381bb Drawings: a reopened drawing on hold exports its views and dimensions from a background copy, as the window does (#12860)
A drawing whose updates are deferred, closed while held, then opened again keeps its views' and its
dimensions' last lines until something reads its sheet. An export to a file runs on a copy of the
session, and that copy didn't carry those kept lines: exported before the sheet was read, the file came
out with no views and no dimensions.

The copy now carries the lines kept for the open drawing, and takes them back the way the session does.
The test exports from the copy first and from the session after, and both write the same sheet.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 01:33:41 -05:00
CamWight cee33def28 Drawings: a deferred drawing keeps its dimensions across a close, and lists them out of date (#12802) (#12803)
* claim: #12802

* Drawings: a deferred drawing keeps its dimensions across a close, and lists them out of date (#12802)

A deferred drawing closed during a model change came back with its views' last lines but no
dimensions: the close kept only the views', and a held drawing reads nothing again. Now the close
keeps each dimension's entry too, beside the views' lines, with its world state let go (an empty
one), so a closed drawing still holds no model. Opened again, each comes back read from a state
that has gone: a drawing that may draw reads it again once its view is up to date, as before, and a
held one shows it as it was.

A held view's dimensions are out of date with it, so every command that reads a sheet now lists
them in `out_of_date`, after the views, with the drawing open or reopened. They are listed only:
the page never reads them again against the view's old lines. A dimension or view gone while the
drawing was closed is forgotten by the rule that was already there when the sheet is put together.

Tests: a deferred drawing closed during a change keeps its dimension and lists it, reads it again
once updated, and lists it held with the drawing open; a dimension gone while closed is not shown,
listed or kept; a deleted drawing keeps no dimension either. #10431's held export test now
expects the held view's dimension listed with its views (3, was 2), and takes the shared edge helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawings: a closed drawing's kept dimensions hold no world state, like its views

A dimension kept at a drawing's close is now a `KeptDimension`: its record, its view's drawing, its
lettering and its lines, with no world state, as `KeptView` already is for a view. So the type keeps
"a closed drawing holds no model", where before a swap for an empty state in `forget_closed` did.
`take_kept` gives the views and the dimensions the one gone state, so both are read again from the
same place. Nothing changes in what a drawing shows or lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 00:21:02 -05:00
CamWight e50e7e1bf6 One way a sheet sets its text: wait for fonts or ask, then set, pending or refused (#4890) (#12846)
The title block's text and a dimension's text each chose for themselves between waiting for the
search for fonts to fall back to (an export, drawing.inspect) and asking it (the window's sheet),
then each mapped the answer to set, pending or refused. That rule is now in one place, in sketch
beside the fonts: PaperFonts holds the drawing's family and language with the font folders listed
once, and sets lines or text the waiting or the asking way; Outcome is the one mapping of its
answer. The title block's lay and the dimensions' paper_group take a PaperFonts, made once for
each block and each read of dimensions. Nothing else changes: the same text in the same fonts,
pending and refused in the same cases. A parts list's words on paper will use it too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 23:00:08 -05:00
CamWight d506dd6552 A parts list's rows as a table reads them (#3874 P7b) (#12839)
PartsList::rows gives each row of a bill of materials with its values formatted, each followed by the
rows listed under it. The item comes from the rule a balloon reads, so the two can't disagree. A mass
follows the list's unit and decimals and a length the drawing's units; a value a row hasn't got is left
out, for the table to mark. A nested row's quantity is how many are in one of its parent, and `count`
has them all.

A row's key names each member, so one part listed in two places keeps two keys, and the limit is now
2048 bytes to fit the 65 members of the deepest list. The plan's "item with its padding" has nothing to
do: no padding exists. The text measure goes with its first caller, the sheet's tables (P8b).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 21:41:12 -05:00
CamWight f1153f462d Drawing export: a sheet exported to a file runs in the background, like the view DXF (#4890) (#10431)
* Drawing export: a sheet exported to a file runs in the background, like the view DXF (#4890 EX2)

A sheet exported to a file through the control channel was drawn and written on the window's
thread, so a large PNG at a high dpi held the window up until it was done. `drawing.export` with a
`path` now goes through `file_load` as `Kind::ExportSheet`, beside the view DXF's `ExportDxf`: on a
copy of the design, off the window thread, with an "Exporting sheet" card. Without a path the
reply holds the document, so it runs at once as before. Cancel on the card says which export it
stopped: it said "DXF export cancelled" for every export while the view DXF was the only one.

`Kind::exports()` holds what the two export kinds share (the copy, never adopting the worker's
session, Cancel waiting for the worker, a drop cancelling it, a finished export staying finished),
in place of seven `ExportDxf` tests and a check on the command's name.

`paper_png`'s band loop reports its rows to the current `solidcraft_kernel::watch` and stops
between bands once it is cancelled. The command writes the file only after the PNG is complete, so a
cancelled export writes nothing. With no watch, every other caller, the bytes are unchanged.
`drawing.export`'s checks and write, its commands and its schema are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing export: a held sheet exported in the background has its views, as on the page (#4890 EX2)

A sheet exported to a file runs on a copy of the session. `Session::scratch` leaves the copy's
drawing-scene cache empty, so a deferred drawing that was held (the model changed since its last
`drawing.update`) exported from the copy with no views or dimensions. The same export without a
path, on the window's session, wrote the lines the sheet page shows.

`Session::scratch_as_shown` is `scratch()` plus `Cache::shown_copy`: the world state the cache last
updated against, each view's and dimension's last drawing, and the serial they were numbered to.
That is what `as_shown` reads. The lines kept for closed drawings, the last answer and a request in
flight stay out, since only the window's page uses them. `file_load` takes that copy for its two
export kinds; inserts, updates and opens keep `scratch()`, which is cheaper and never draws a sheet.

Tests: the engine compares the export (svg and out-of-date list) from the session and from the copy
for a drawing that isn't deferred (also after a view's edit), a deferred one with a view edited
since its update (drawn at once), and a held one, its dimension included, on the sheet last shown
and on a second sheet. The window
test writes a held sheet through the background export and compares the file with the synchronous
export.

Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com>
2026-11-06 20:38:51 -05:00
CamWight 594945bd69 Drawing text: a font search that stops part-way no longer leaves text waiting for the rest of the session (#4890) (#12395)
* Drawing text: a font search that stops part-way no longer leaves text waiting for the rest of the session (#4890)

The search thread clears its running mark only when it ends normally. If a search stopped
part-way (a panic), the mark stayed set: nothing was searched again, and text needing a fallback
font waited for the rest of the session. The thread now holds a `Running` mark; dropped before it
finishes, it clears the mark, and what it held or was asked meanwhile is answered as a miss, so that
text is set as no font has it. A character asked for next starts a search of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing text: a font search that stops part-way answers what it held under the same lock that clears its mark (#4890)

Drop for Running cleared the running mark and answered the held queries as misses under two
locks. Between them, a request for one of those queries found no answer and no search running,
and started a new search for it: the query that may have stopped the search was searched
again. Both now happen under one lock. store keeps its shape and shares store_in with the
Drop, so the cap on kept answers stays one rule.

Found and fixed by @theironchef in review.

Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: theironchef <6278036+theironchef@users.noreply.github.com>
2026-11-06 19:36:33 -05:00
CamWight 2a2544956a Drawings: sheet text in a script the drawing's font lacks no longer holds up the window while a font is found (#12778)
* Paper scene: what a font adds to a table, a run's reach, and text in the sheet reply's schema (#4890)

One rule for everything that puts text on a sheet: `Scene::font_records_beyond`
counts only what a scene's font table doesn't hold yet, `TextRun::reach` says
how far a run's glyphs go along its baseline, and `drawing.scene`'s schema has
text runs and fonts, never a font's bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: a font's records are found by its id, not by its place in the table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: their numbers on the sheet, in the drawing's font (#4896 W19h)

A dimension on a sheet now carries its text: each value is set in the
drawing's font and language, measured, and placed along the dimension's
text direction, reading from the bottom or the right. Arrows flip by the
measured text width. A text the font can't set leaves the lines and says
why. The scene adds each dimension's fonts to its table once, counting their
records against the scene's limit, and measures text by its outlines for an
object's bounds. A dimension is read again when the drawing's units,
language or font change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a title block's labels and values set on paper (#4890 B3, #3875)

Each label is set as one line at its place, and each text cell's value in its
value box by the cell text rule, broken between words to the box's width when
its fit wraps, in the drawing's font and language. What doesn't fit is shown
whole and noted; a text no font can set is left off the sheet and noted with
why, so the sheet and its export never fail over one cell. drawing.inspect
lists each cell's notes; the sheet scene sets what was measured.

What a cell shows is worked out in one place for drawing.inspect and the scene.
The block's fonts count toward the scene's records with the border, so no view
takes them, and the scene is drawn again when the design or a cell's text
changes.

Ported from the personal PC's 1951bc696 onto today's drawings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sheet scene: a title block's text set once while its sheet fills in, and its faces in the key (#4890 F5)

The sheet scene read its title block's fields and set their text again on
every extract that missed its last answer, and views are drawn one a tick,
so a sheet filling in weighed the documented component and laid out the
block once for each view drawn. The text is now kept with the last scene
and set again only for another sheet, drawing, design, model or lettering.

A scene is reused only for the same faces too: one family name can mean
another face once a font is installed or removed, and that face is another
scene. A family with no face is set in the built-in font, so it stays the
same scene until it's found.

Ported from the personal PC's d82fd03e7 onto today's title block text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sheet text never waits on the window's thread: a title block's and a dimension's text wait for no font search, and inspect names the fonts fallen back to (#4890 F7)

A text in a script the drawing's font lacks needs a search of the installed
fonts. The window's sheet (`drawing_scene::extract`, `step` and the view's
job, on the UI's tick) now asks the search's thread instead of waiting for it:

- sketch: `paper_lines_now` beside `paper_lines_in`, as `paper_text_now` is
  beside `paper_text_in` (FontPending until the search answers).
- title block: `title_block_text::lay(.., wait)`. A text whose font is still
  being looked for is off the sheet and marks the `Laid` as `pending`, never as
  refused. While the last scene's block is pending, the fast path and the
  text's reuse both miss, so `extract` sets the block again until the search
  has answered.
- dimensions: the same rule for a dimension's number (`paper_group(.., wait)`,
  a `wait` flag on `draw_dimensions`). The step, its dimension step and the
  view's job (#11126) all ask; a dimension whose text is pending is kept as
  pending, and `extract` reads those dimensions alone again
  (`set_waiting_dimensions`), keeping each one only once its text is set.
- `SheetView::waiting_for_fonts` says when text is still waiting. `step` does
  not: pending text has no lines to draw again, so a `true` from it would spin
  on no work.
- `build` (export, `drawing.scene`) and `drawing.inspect` wait, so their
  answers are whole. `drawing.inspect` adds `font_fallbacks`: which font set
  each script of the title blocks' text that the drawing's font lacks
  ("Kana set in BIZ UDPGothic").

The scene tests' `settled` asks as the window does (extract, then steps,
until neither has more to do), with a time bound.

Ported from the personal PC's cae4a2969 onto F5; the dimensions' half is new
(one rule for every text the window's sheet sets). On `drawings` f50edc1ed:
`draw_dimensions` takes #12516's (frame, line group) pair as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:37:51 -05:00
CamWight 67a325808f Drawings: a balloon, dimension or note attaches only to a part its view draws (#4896) (#12394)
Adding one on a part the view's saved settings leave out (its hide, or a source that doesn't
choose it) is refused, naming the view and the setting, and the parent view when it set it. One
added before a later view edit stays, and its inspect reply warns until the edit is undone.

The rule stays the view's own (`chosen`): `left_out` asks it about one occurrence path and says
why; `check_drawn`, `not_drawn` and `drawn_warning` beside the view commands use it for the three
annotations. `DimensionKind::paths` and `Anchor::path` list what each points at, so every kind of
dimension is checked. No model is evaluated.

On drawings a7c0310ab, #12516 (line groups) gave a resolved setting two more setters, the
view's sheet and the drawing. The words for where a setting was set are one function, set_on,
with an arm for each: a parent names its view, a sheet and a drawing say so, and a view's own
setting and the default add nothing. Hide and the chosen occurrences come only from a view's
chain today, so no refusal says "its sheet" yet; the match keeps the compiler checking each
new setter.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 15:34:10 -05:00
CamWight a7c0310aba Drawings: drawing.create and drawing.set refuse a misspelt setting instead of ignoring it (#3890) (#12625)
A script that calls drawing.create or drawing.set with a misspelt setting now gets an error
naming the setting. Before, the call reported success and the setting was lost. The two
commands now read their parameters through input schemas, like every other drawing command.

- schema/drawing.rs gains DrawingCreateParams and DrawingSetParams. Each field's reader keeps
  the refusal words the hand readers used.
- The hand readers for units, projection, updates and revision_numbering move into the schema.
- The drawing's line_group reads through cmd::line_group_of, so it keeps its sheets' words.
  drawing.view.edit is unchanged.
- The output types are their own slice.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 08:42:27 -05:00
CamWight e859b77cf4 Drawings: drawing.status catches up before it reads, so a reopened drawing's views say current (#4892) (#12573)
Straight after a drawing opened again, drawing.status said out_of_date for views whose kept lines
still show what the model shows, and drawing.update counted them as waiting. The first extract then
found them up to date and drew nothing. status read the cache as it was: the kept lines came back
and the keep pass ran only inside extract and the drawing step.

One function, catch_up, now brings the cache to the drawing open and the world state: the kept
lines come back, then each view that shows the same takes the state. extract, step, begin and
drawing.status (and so drawing.update) all start there, so each says the same of a view.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 06:09:23 -05:00
CamWight f50edc1ed0 Drawings: a drawing's views are worked out beside the window, so it keeps responding (#4892) (#11126)
* Drawings: views drawn beside the window (#4892 B7)

`drawing_scene::begin` asks for the first view out of date, `Job::run` works
its lines out on a copy of the session (pending moves cleared: the design's
placements, as dimensions use), on any thread, and `finish` takes them only
for the latest request while the revision, the model's world state and the
view as it resolves are as they were; `cancel` forgets the request. Request
numbers come from one counter for every session, so a drawing closed and
opened again never takes an old job's lines.

A job carries what `step` draws in one swap: the view's lines, what they
show, and the view's dimensions through its new frame. `step` and `begin`
start from one helper, so both keep the views that show the same and both
draw nothing while a deferred drawing holds. `step` is the same three run at
once. The window's hook is `drawing_jobs::tick` (the shape of `save_job`): no
caller yet, no UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawings: a built sheet leaves uncaptured moves out too (#4892 B7)

`build` (every export) draws the parts where the design places them, as the
provider's jobs do: one rule for lines, dimensions and files, and once
nothing waits the provider's scene is still the built one. Exports change
only while an occurrence move isn't captured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawings: the window reads a sheet's dimensions too, once no view is waiting (#4892 B7)

Sheets now read their dimensions after their views (`drawing_scene::step`). Drawing views beside
the window went through `begin`, which asks only for views, so with every view drawn the
dimensions out of date were never read. With no view waiting, `tick` now steps on the window as
before; reading dimensions is quick. The tick test adds a dimension and ends with none out of date.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:10:50 -05:00
CamWight ecf75d9088 Drawings: a drawing, a sheet or a view saves the line group its lines are drawn in (#4890 D2d) (#12516)
A drawing saves the ISO 128 line group its views and dimensions are drawn
in; a sheet or a view may save its own. A view's group is resolved like its
other inherited settings: its chain, then its sheet, then the drawing, and
inspect says which gave it. The group joins the display settings a projected
view follows. drawing.create and drawing.set take it; the sheet commands and
drawing.view.edit take it or null to follow.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:29:01 -05:00
CamWight 855ce28fda Drawings: a deferred drawing keeps its lines until it is updated (#4892 U2) (#12449)
A drawing's views follow the model automatically, or, when deferred, only on
drawing.update, which lets them draw from the model as it is now, for this
session only. The next model change holds again, and a close drops the update,
so a reopened deferred drawing waits. While it waits, its views show their last
lines, out of date.

drawing.set and drawing.inspect take and say `updates`, saved only when
deferred (a drawing-format pending note). drawing.scene, its hit and line
commands and drawing.export read what the page shows and list what is out of
date; drawing.status says held.

The ribbon's Update Drawing button stays planned as drawing.update.planned with
its art awaiting review, as Export does; the command itself takes the existing
update icon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 20:53:02 -05:00
CamWight b4242e9844 Drawings: a view draws again only when what it shows changes (#4892 U1) (#12423)
A view's entry keeps what it was drawn from: each body of the world state by
its occurrences, name, reference mark and geometry key. A view drawn from a
world state that showed the same takes the new one and draws nothing, so a
sketch, or an edit taken back, projects no view.

A drawing closed keeps its views' lines in the sheet scene's provider, and
opened again starts from them: with nothing changed it draws no view, and with
the model changed its last lines show, out of date, until drawn. Dimensions are
read again. Only in memory, never saved; a deleted drawing's lines go with it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:53:53 -05:00
CamWight 41dcdd1273 Drawings: which line of a view is under a point, and drawing.scene.line (#4816) (#12396)
* Drawings: which line of a view is under a point, and drawing.scene.line (#4816)

A view's group now keeps what drew each of its strokes (the body, and the edge or silhouette of it),
one entry per item with none for its pictures, from the same walk that paints the strokes
(paper_lines). line_at finds the view under a point (hit, over the views alone), then the nearest
of its strokes within a band, the one painted later of two as near, and names it as
drawing.view.snapshot names its lines (one From<Source> for both). drawing.scene.line draws the
sheet's views out of date first, as the page does, and evaluates nothing. The two tests that built
the two-part design inline now share one fixture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawings: a pressed line names what drew it on each placement (#4816, #4892 Re)

A test: a round bar placed twice. A press on either one's outline names the silhouette's face
through the occurrence pressed, and a press on its top names the top rim's edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:48:19 -05:00
CamWight 1b069e1a33 Drawings: a view's dimensions change in the same step as its lines (#4896) (#12287)
The sheet page draws through the provider, which brings one thing up to date per tick. After a model
change it drew a view's new lines in one step and that view's dimensions in a later one, so for a
step the old dimensions sat on the new lines. `step` now reads the view's dimensions through the
frame it has just drawn and keeps them in the same swap; an edited dimension is still read alone and
projects nothing.

Keeping drawn dimensions is one method, `Cache::keep_dimensions`, which the view step and
`step_dimensions` both call. The same reads happen as before, a step earlier.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 14:06:54 -05:00
CamWight 2c052b4b5f Drawing export: a sheet PNG draws its text (#4890 B2b) (#12355)
A sheet's text is drawn from the same glyph outlines the SVG writer fills,
filled nonzero so a glyph's overlapping contours fill whole, where the PNG
writer refused any text before. Text counts toward the point cap and is
drawn band by band like any fill, in every colour.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:56:22 -05:00
CamWight 90fc4f39e0 Paper text: lines measured before they are set, wrapped between words to a width (#4890 B3b) (#12349)
paper_lines measures a text for paper in the drawing's font and language:
its lines, each with its width, and the pitch between them, broken at each
line break and, given a width, between words (a word is never cut, so a
one-word line may be wider). A word the font lacks is measured in the font
it falls back to. PaperLines::set only places what was measured, and
paper_text is paper_lines then set, so one rule measures and sets text.

Ported from the personal PC's abefb3713 onto today's fallback fonts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:49:59 -05:00
CamWight 157c6469d6 Drawings: what is under a point of a sheet, and drawing.scene.hit (#4816) (#12346)
To pick a view or a dimension on a sheet, something has to say which one is under the pointer.
drawing_scene::hit takes the objects a sheet's scene lists, a point and a band: of the boxes that
hold the point within the band, the smallest, then the topmost, and whether the point is on the
box's edge. A view's box is its boundary, so one that can't be drawn can still be picked; the
drawing border is never hit. It is one pass over the objects, cheap enough for every hover.

drawing.scene.hit answers it for scripts and agents, from the sheet as drawing.scene draws it:
read-only, with input and output schemas, its label in a pending locale file and an EXEMPT line.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 13:37:20 -05:00
CamWight 69709bdbf1 Paper scene: every line takes its width from its kind and line group (#4890 D2a, D2d) (#12232)
* Paper scene: kinds of line and their widths by ISO 128-20 (#4890 D2a)

A line's kind (visible, hidden, centre, thin) and a line group give its width in one place:
the standard's series of widths, and a group's wide and narrow lines two steps apart. Nothing
reads it yet; the next commit draws every line on paper from it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: every line on paper takes its width from its kind and line group (#4890 D2d)

A view's lines and a dimension's lines are drawn in a line group: a visible edge is its wide
line, a hidden edge, a reference part's edge and a dimension's lines its narrow one, and a
hidden line is dashed 12 line widths on and 3 off. The five width constants that waited for
this go. Every caller draws in the default 0.5 mm group, so today's sheets are unchanged; a
drawing, a sheet and a view choose their own group next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 04:39:10 -05:00
CamWight 8067a0af23 Kernel cases: list the two drawing repair commands as tested in the engine (#12216)
#12170 added drawing.balloon.repair and drawing.dimension.repair without a
line in tests/kernel-cases/EXEMPT, so the kernel rig's coverage check now
fails on every drawings pull request that runs it ("NO CASE"). Like the
other drawing commands, both are tested in their area's test files
(drawing_balloon_tests.rs, drawing_dimension_tests.rs), so each gets the
same line, in sorted order.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:13:29 -05:00
CamWight 5792393fb2 What a saved view shows: source and hide, following its parent (#4816) (#11873)
A saved view now keeps the occurrences it draws and the ones it leaves out,
as the snapshot takes them. drawing.view.base, edit and inspect take them; each
follows the parent until a view sets its own, and a copy made alone keeps what
it showed. Boundaries, fitted scales and lines all read them through one rule.

A path whose occurrence was deleted never refuses the file: a hide path goes
quiet, and a view with nothing left is a placeholder that says why.

Replayed onto drawings fd4b7a7bc: the fit and the lines take reference_shown_percent
(the rename of reference_margin_mm in #7316) through shows(); the path limits now live in doc.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 03:07:08 -05:00
CamWight 188192cb8b Drawings: a lost reference lists what it may now be, and a repair points it there (#4892 B5) (#12170)
A dimension or balloon whose edge, corner or face is lost now lists up to 16 leads, offered and never taken: the
blend face named after a filleted edge, that face's edges, then the faces the same feature made. Lookup still never
writes the record, so undo brings a lost reference back.

drawing.dimension.repair {dimension, slot, pick} and drawing.balloon.repair {balloon, pick} point a lost or
ambiguous reference at a pick of the same kind, on the reference's own occurrence path: same id and paper place, one
undo step, through one shared helper. Only a reference that no longer finds the one thing it names is repaired, so a
dimension's other, still-found reference is kept, and silhouettes repaired onto one cylinder are refused as add
refuses them. A pick takes the forms add takes, so a lead of the slot's kind from inspect passes back as it stands.
The saved format is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:48:01 -05:00
CamWight 371d25dadd Drawings on the web: Arabic, Korean and Chinese text is set in fonts the web app fetches (#4890) (#11790)
The web covered Japanese only (F6): any other script on a drawing was
refused. Now each script the web covers has its own font, fetched once
from the web app's origin the first time a drawing's text needs it:

- kana and Japanese Han: BIZ UDPGothic, as before;
- Hangul and Korean Han: Noto Sans KR;
- simplified or traditional Chinese Han: Noto Sans SC or TC;
- Arabic: Noto Sans Arabic.

Han takes the font of whose Han the text wants (Han::of, the desktop's
rule), so a drawing in English with Han asks for the simplified Chinese
font, as a desktop does; F6 sent all Han to the Japanese font. A text in
two scripts waits for both fonts; one whose fetch failed is refused by
name, and the rest sets. Thai is still refused by name (Hebrew is in the
built-in font).

The four fonts and their licences are unmodified, in assets/fonts/<family>/
with ATTRIBUTION rows, and trunk copies each to fonts/<family>/. Four
files rather than one CJK collection: a session fetches only its own
language's font (4.6 to 8.3 MB), where the collection is one large file.

The folder the web app fetches fonts from is named once (FONTS), and a test
checks that index.html copies each font from its asset into that folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:20:50 -05:00
CamWight e821b3ea3c Drawing export: the last two tests that read text read svg (#12164)
#12152 put an SVG's text back under `svg` in drawing.export's reply and
missed two readers: the delete test that selects a drawing's objects and the
pictures test that draws an image under the views.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 21:08:15 -05:00
CamWight e6e6df0d86 Drawing views: a default base view fits around the title block (#3875) (#10987)
fit works inside the sheet's free space by the same rule as leaves (drawing_view::Room): it takes the
largest preset the border allows, then steps down until the view's box is off the block. A sheet with
no block fits as before; the snapshot, with no sheet, passes none. The default position stays the
sheet's middle.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 20:11:48 -05:00
CamWight bf32aa1fdc Drawing export: an SVG's text is under svg in the reply again (#12152)
#10681 renamed the reply's `svg` key to `text`, and that broke the commands'
existing readers: three tests on drawings read `svg` and failed. The SVG goes
back under `svg`; a PNG's base64 stays under `data`. The output schema and the
params doc follow, and #10681's own export tests read `svg` again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 19:59:03 -05:00
CamWight 30942265a8 A new dimension goes on the side away from what it measures (#4896) (#11932)
drawing.dimension.add, with no offset_mm, puts a linear, length or chain
dimension 10 mm on paper to the side of its line away from the material
beside it: it looks half a paper millimetre either way across the line, in
the view's plane, at the depth of what it measures, and asks each shown
body's B-rep exactly (Body::classify_point; inside or on its boundary is
material). With material on both sides or neither, it goes away from the
middle of the view's model box; on that middle, the fixed rule is above or
to the left (+10). Radial, angle, cylinder and silhouette dimensions keep
+10, and an offset given keeps its sign.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 19:31:48 -05:00
CamWight 13cd786c50 Kernel cases: the note, revision tag, revise and table drawing commands are exempt (#12137)
The rig's "Every command has a case" check failed on drawings for the ten
commands #11898 added. Each is tested in crates/engine/src/cmd/drawing*_tests.rs,
as the other drawing commands are, so they join them in EXEMPT.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:41:35 -05:00
CamWight f7a24278bb Drawing export: a sheet can be exported on Blueprint or Prussian Blue paper (#10679) (#10681)
* Drawing export: a sheet as PNG at a chosen dpi and colour, and one reply shape (#4890 EX1)

drawing.export {format: png, dpi?, colour?} writes the sheet's scene
through the sheet PNG writer: 25 to 2400 dpi (300 when not given), grey,
black and white, RGB or transparent (grey when not given). dpi and
colour are refused on an SVG. A path must end in the format's extension,
which ExportFormat::extension gives for every format.

One reply shape for every format: text (SVG) or data (base64 PNG) when
no path is given, in place of svg; report.pixels gives a PNG's size. A
PNG held for the reply stops at 64 MiB, as an SVG does, and the refusal
says to give a path; a file may be as large as the sheet needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing export: a sheet on blue paper, in SVG and PNG (#10679)

One rule for the two papers and their ink, used by both writers. White stays the default, byte for byte. A blue paper's PNG is rgb; the other colours are refused by name. A picture keeps its own colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 18:01:47 -05:00
CamWight da576824b2 Drawing views: a view's look is its visual_style, the viewport's name for it (#5307) (#11579)
A drawing view's setting for how it is drawn (visible lines, with hidden lines, shaded)
was display_style while the viewport calls the same idea its visual style. The field,
its command parameters, its schemas and its replies are now visual_style, on a view's
settings and on its own overrides. display_style is refused as an unknown field, as
drawings haven't shipped and no saved design holds it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:57:32 -05:00
CamWight f2b0b39393 A parts list's ballooned column, for its editor only (#3874) (#11964)
A table's column can be left off paper (printed: false, saved only then): the
layout places the printed columns only, while filters and overrides still read
every column. No printed column may read the ballooned row field, and a table
prints at least one column.

A new parts list gets a seventh column, ballooned, not printed.
Drawing::balloons_by_list works out once which list each balloon reads, and
balloons_by_row counts one list's balloons row by row, every row listed, 0
included; drawing.inspect gives parts_lists with each
list's counts. Worked out on each read and never saved.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:42:48 -05:00
CamWight d0da5e5277 Mesh lines: a refusal says what happened, and each caller says what to do (#5866) (#11826)
The view lines' errors were the view DXF's, remedies included, so a drawing
view could say "DXF export cancelled" or "increase tolerance". The module now
answers a Refused (cancelled, over the work or memory limit, across the eye
plane, invalid) that says only what happened. The view DXF adds what to do in
a DXF's terms (hide bodies or sketches, increase tolerance, zoom out), and a
drawing view in a drawing's (raise chord_mm, or choose a smaller scale).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:38:31 -05:00
CamWight b3af8346cf Drawings: a part's drawing starts on A3 and an assembly's on A2, each with its tolerances note (#3875) (#12000)
* Drawing templates: the built-ins carry SolidCraft's title block again, so a new drawing starts with it (#3875)

Every built-in template, ISO A4 to A0, is one sheet with the ISO border and
the built-in title block, so drawing.create with no template (iso-a3)
gives a drawing with the block. #7208 made them the border only; the
decision since is that a drawing starts with SolidCraft's block, and a
user replaces or changes the default template.

- A drawing saved as a template is the user's own: a copy of the built-in
  with a company's name in the block and its tolerances typed in starts
  drawings that keep both (tested through drawing.template.save and
  drawing.create).
- The built-in block by name stays, for a sheet without one.
- The general tolerances stay empty, and the five names (iso-a4 to
  iso-a0) and the one drawing_template setting stay: the part and
  assembly versions would be the same.
- The tests that gave a new drawing the block first no longer do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates by kind: a part starts on A3, an assembly on A2, each with its tolerances note (#3875)

Ten built-ins, iso-a4-part and iso-a4-assembly up to iso-a0-part and iso-a0-assembly, each with its kind's general
tolerances note typed over the title block cell. template::default_for(assembly) is the one rule for the default, and
template::is_assembly says a design that places components is an assembly. drawing.create with no template uses it,
and drawing.template.list marks the design's default and says whether each built-in is an assembly's. The older names
iso-a4 to iso-a0 still read as that size's part template.

Preferences gets one setting per kind in place of the one; a saved name of the other kind reads as the kind's default,
and the old drawing_template key is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 17:38:27 -05:00
CamWight 6be23a3e19 Hidden lines on paper: a view in visible_hidden draws them dashed (#5866 V7b) (#11839)
A saved view or a snapshot's scene in visible_hidden now draws its hidden lines,
0.25 mm wide in ISO 128's dashed line, under the visible ones; a reference part in
visible_hidden draws its hidden lines the same way. Paint order from the bottom:
hidden lines, reference parts' visible lines, the model's. The sheet no longer fails
for that style, so it exports; only shaded styles with lines still refuse.
The paper scene's output schema takes a stroke's dash pattern, so a reply
with hidden lines still matches it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:58:17 -05:00
Mateusz Sych e81a201ff8 Drawing W06a: parallel hidden lines and shared work limits (#2455) (#3013)
* geom/hlr: per-curve work on worker threads, shared work limits

Silhouette tracing, screen sampling, shadow cuts, classification, coverage and
the crossing search (by grid rows) with crossing refinement run on worker
threads through one ordered map: results go back in item order, so drawings
are the same bit for bit on any number of threads. Options::threads picks the
workers (0 = one per core, small views stay on the calling thread); the
browser build stays serial. Placements sharing a body and a turn share one
silhouette tracing.

Cancellation is polled before every item and inside long ones from every
worker; the first error stops the others; a panicking worker becomes
HlrError::Internal instead of escaping. Ray, curve, crossing, shadow and
coverage budgets are totals over all threads (shared counters; crossing pairs
are counted before the search).

Limits: placements and the default max_tracks are hlr::MAX_ITEMS, which is
section::MAX_ITEMS (also the brep record limit); trim_samples must be 1..=4096.

* docs/drawing-hlr: link the parallel-work PR

* geom/hlr: clearer limits and parallel-work docs, faster thread tests

docs/drawing-hlr.md now separates input limits (with the error each returns:
body records fail with Invalid, not Budget) from the work budgets, names every
budget's error, and states how errors and panics behave on worker threads.
Code comments say the same in plainer terms. The budget and cancellation
tests drop the oblique torus, and the freeform pass of the identity test
compares one parallel run, which keeps the HLR suite under 4 s.

* geom/hlr: poll cancellation inside long items, find spline spans by bisection

Review found two items a worker could run for seconds without seeing a
cancellation: tracing a body with many curved surfaces, and sampling one long
spline (up to 4096 control points). Silhouette tracing now asks before each
surface, screen sampling at every refinement pass, the cuts at left-out faces
every 4096 chords and coverage every 65536 checks.

The long spline was slow because every point and every part hull scanned all
of its knot spans. de Boor evaluation now finds its span by bisection over the
ordered knots, and a part's hull starts at its first piece and stops after
its last: the same spans, so the same bits (checked on the fixture scenes),
and a 4096-point spline edge prepares in 0.9 s instead of 2.2 s and samples
in 0.23 s instead of 1.47 s.

* geom/hlr: poll cancellation while forming and trimming silhouette sets

Review found that forming a body's silhouette sets scanned each face's whole
surface group before the first poll, quadratic in the faces on one surface
(about 4e8 steps for 20 000 faces). The group scan now stops at its first
supported face (groups list faces in order, so that is the same face), the
loop polls every 256 faces, and trimming a traced curve asks before each
face. A cancellation seen while trimming drops the partial curves, even if
the callback answers false later. Output unchanged on the fixture scenes.

* geom/hlr: poll cancellation per boxes met when cutting at left-out faces

Review found that one chord crossing the screen boxes of very many left-out
faces bisects every side without a poll (polls came only every 4096 chords).
It now also polls every 1024 boxes met. The docs state the cancellation
contract precisely: polls are where listed, and the work between two polls is
bounded by the input limits rather than by a time.

* docs/drawing-hlr: say where coverage polls cancellation

* geom/hlr: body records have brep's own, larger limit now

Main raised brep::MAX_ITEMS to 1 000 000 (#3093, which stores whole evaluated
models), so it no longer equals section::MAX_ITEMS. hlr::MAX_ITEMS stays
section's 100 000 for placements and the default curve budget; the docs and
the limits test now say that a body's records are bounded separately, by
brep's limit.

* geom/hlr: a panicking worker stops the others; the ray budget is HLR's own

A worker that panics now sets the shared stop flag while it unwinds, so the
other workers end the phase early instead of finishing work the call throws
away. The docs no longer compare the ray budget with the occlusion work
budget of file.export_view_dxf: the two count different work.

* geom/hlr: an ignored timing test, prepare and drawing timed apart

hlr_timing prepares four bodies, then draws 100 differently turned placements
on one thread, two workers and one worker per core, printing the total, the
phase timings and the ray count. SOLIDCRAFT_BENCH_HLR sets the placement count.

* geom/hlr: timing scenes for sharing and crossings; placements keep their lines when permuted

hlr_timing now starts its prepare clock after the fixtures are built, adds the
same grid at one turn (shared definitions against one per placement) and
sixteen thin bars crossing 8x8 from above, rotates the thread order over three
rounds and checks the drawings match across thread counts outside the timed
part. A new test permutes three placements of one prepared box and removes one:
each line keeps its own placement index, and the boxes' lines stay exact.

* docs/drawing-hlr: the review's limits and fixtures in the parallel tables

* geom/hlr: follow the B-rep record into solidcore-record

The record, its version and its item limit now come from solidcore-record
(#11496): the new tests build their bodies at `solidcore_record::brep::VERSION`
and make a spline's curve with `C3::bounded`, as the other fixtures do, and
the limits table gives the record's own limit, 4 000 000 items, checked by
`brep::check::validate`.

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
2026-11-04 16:56:07 -05:00
CamWight 96bb41c480 Drawings: notes on a drawing, the record and commands (#4896) (#11898)
* Issue a revision: drawing.revise (#3874 R1)

drawing.revise appends a copy of a row to the issued history: an existing row as
it reads, or a new revision with the texts given, in one undo step. A row is
issued once. The copy keeps what it read through an edit, a hide or a delete, and
nothing is filled in: no date, checker or approval.

Issuing used to make a drawing unreadable, as a copy has its row's id and the
drawing's check refused any id seen twice. Each id now counts once, and issued
ids are unique among themselves.

model_version becomes {path, change}, on issued copies only. One function on the
session answers it, and answers nothing until version control exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing revisions: numbered 1, 2, 3 (#3874)

A drawing's new revisions are numbered 1, 2, 3, never lettered
(@CamWight, 3 Nov). drawing.set's revision_numbering is {start: 0 or 1},
the first number. A revision typed by hand is kept as typed; the next one
follows the highest number, and drawing.inspect says where numbering
starts. A record in letters is refused (drawings haven't shipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Revision tags on a sheet: add, edit and delete (#3874 R3)

A tag gains the sheet it sits on and its place there. drawing.revision.tag.add,
.edit and .delete keep them, inspect and every revision reply list them with what
each shows, and a sheet's delete counts and takes the tags on it. A deleted row
still leaves its tags showing the revision they had. Tags draw on paper once scene
text lands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Saved tables on a sheet: drawing.table.add, .edit and .delete (#3874)

A drawing saves its parts lists and revision tables, each on a sheet with its
definition and place. Rows are never saved. A parts list starts at the top right
listing what its sheet documents, else the design; a revision table at the bottom
left; headings on top, QTY, rows of 7 mm.

Placing a list saves the numbers its view shows, once per view, so what is printed
doesn't move. Numbers are the design's: undoing the placing puts back only the
drawing, as every drawing step does, and the numbers stay. That one command changes
both, and the proof that nothing needs evaluating now allows it.

The tables are a ById list, as the balloons and images are, and keep its rules: at
most 1000, listed by id. Revision tags take their own id type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A balloon reads its row in a parts list (#3874 L1)

A balloon can name the parts list it reads; else it reads its sheet's only list,
else the drawing's only one, else none, with the choices listed. Its row's item is
worked out on each read and never saved: drawing.balloon.inspect gives item and
row_status (ok, no_list, reference, no_row or not_found). A lost row never removes
the balloon. A list's delete reports the balloons that named it and clears their
links.

Bom::item_of cuts a path from the top of the design to start below the assembly
listed, and row_of is no longer test-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Notes on a drawing: the record and commands (W19e, #4896)

A note sits on a sheet, a view, or an edge or corner of a view's model, saved by
name as a dimension's is, with the title block's text and fields, its place and
its text height. drawing.note.add, .edit and .inspect, one undo step each, no
model evaluated. Its one leader is worked out, never saved: a lost reference keeps
the note and its text, with no leader. Anchor is one type so other symbols placed
on a sheet can use it.

A view's notes go with its copy and delete; a sheet's with the sheet. The plan left
delete for later, but selection.delete now handles a drawing's objects, so notes
join it. Balloons and notes share Lookup::middle. Drawing them waits for text in
the paper scene (slice 12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 16:53:53 -05:00
bherbruck ca6a095795 Merge pull request #12087 from tangency-labs/sync/main-into-drawings-1104
Sync main into drawings (Nov 4)
2026-11-04 15:51:54 -05:00
bherbruck 0273441236 Sync: the three dimension functions that read the display mesh, on drawings only
Picker::placed, face_corners and BodyEdges::new decide with the display mesh.
They are listed on this branch only and must be cleared before drawings lands
on main, where the list takes no new lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:24:44 -05:00
bherbruck 58a00f41be Sync: the repository checks know the drawings' commands and modules
The kernel rig's coverage check exempts the drawing and bill of materials
commands, each naming where it is tested. The mesh decisions check follows the
view line modules to mesh_lines/, and skips the two that only draw and count a
drawing's view lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:23:15 -05:00
bherbruck b841eb939f Merge main into drawings (Nov 4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 11:39:05 -05:00
CamWight 49eedf122c Drawings: a reference to a suppressed or failed feature says so (#4892) (#11823)
* Labelled measurements on a view's snapshot (#5866)

measure: linear, aligned, angle from model points, radius from an edge, with
units and decimals for the labels. No new maths: drawing_measure's rules, and
a dimension's radial rule through one Lookup::whole_arc. Labels go through one
function, reading_label, which dimensions' text now uses too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pieces on one circle are that circle, and one piece is that edge (#4892)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A dimension between two flat faces (#4892)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A suppressed or failed feature's references say so, not lost (#4892)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 06:40:17 -05:00
CamWight 669aaa7744 Drawings: a dimension finds its circle again after the edge is split (#4892) (#11869)
* Labelled measurements on a view's snapshot (#5866)

measure: linear, aligned, angle from model points, radius from an edge, with
units and decimals for the labels. No new maths: drawing_measure's rules, and
a dimension's radial rule through one Lookup::whole_arc. Labels go through one
function, reading_label, which dimensions' text now uses too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Pieces on one circle are that circle, and one piece is that edge (#4892)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 06:40:10 -05:00
CamWight a3375c58e3 Render: a triangle tests only the samples of each row it can touch (#5624, W17b) (#11042)
The raster walked a triangle's whole box, testing every sample, and the pixel budget counted that box.
A plain plate's few long triangles have boxes that are mostly misses, so a view at 2:1 counted 18 tests
a sample. Now each row of the box tests only the span the triangle can touch there, and the budget
counts those spans (an empty row costs one unit, as the walk does).

row_span works the span out from each edge's weight along the row, allowing for the test's own
tolerance and the rounding of its arithmetic, so it is never short; sample_weights is the raster's test
pulled out unchanged. Pictures are the same to the byte. A product image shares the raster and the
count, so its 128 M admits more; the tests built on its old count hold more triangles.

With the misses gone nearly every counted test paints, and a painted surface costs about 240 ns a
test on one thread, so a picture's limit falls from 400 M to 250 M to stay near a minute at worst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 06:12:20 -05:00
CamWight a301359652 Dimensions on the sheet: lines and arrowheads in paper millimetres (#4896) (#11867)
Each dimension of a view on the sheet is one group, ObjectKind::Dimension,
after the views: extension and dimension lines and filled arrowheads
(Terminator::place, #9466), laid out by dim_layout_sized in paper millimetres, so it is the same size at any
view scale. All seven kinds draw; a chain is one line of frames at one offset
with each extension line once, an angle sits where its lines meet on the view,
and a cylinder's line runs across its silhouettes at the end on its offset's
side.

drawing.scene and drawing.export list each dimension: its status, whether it
is drawn, and why not. A lost one, or one on a view that isn't drawn, has no
lines and leaves the sheet and the file written. The provider keeps each
dimension's group by its record, world state, view drawing and units: a moved
view moves them unread, and an edit reads that dimension alone, in one short
step after the views. Text joins with B1.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 04:53:24 -05:00
CamWight fd4b7a7bc0 Seg2: one rule for an arc under a shape-keeping map (#4816) (#7316)
`Seg2::mapped(f, arc)` is a segment under a map of the plane that keeps shapes. Points go
through `f` itself (an arc's centre, every control point). `ArcMap { angle, mirrored, scale }`
says how the map moves an arc: it turns it by `angle`, after a mirror across the x axis when
`mirrored` (start `angle - start`, sweep reversed), and scales its radius by `scale`.
`ArcMap::turn(angle, mirrored)` keeps lengths; `ArcMap::rigid(f)` measures a map of points that
keeps lengths, from where it puts the origin and the unit axes, with a scale of exactly 1.

`Seg2::similar` and the three arc maps written out by hand use it:
- a sheet-metal region onto a panel's flat (`region_to_flat`, which measured its map by hand and
  now asks `ArcMap::rigid`);
- the mirrored flat (`flip_x_loop`);
- a sketch region mirrored in its x axis (`mirror_y`).

The points go through the same functions as before, so every result is the same, bit for bit.
This comes from the architecture review of the drawing work (2026-10-25): four places wrote the
arc rule out, each its own way, and one measured its map by hand.

`map_controls` is private to geom now: on its own it hands an arc back unmapped, which is what the
three hand-written arms were there to avoid.

Tests: every kind of segment under a mirrored map with a turn, a scale and a shift, point by point,
its length scaled and an arc's sweep reversed; the three old formulas, bit for bit (the sketch
mirror keeps a +0.0 start's -0.0); a rigid map's turn and mirror measured whatever its shift, and
the identity; the sheet-metal and mirror tests unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 14:59:59 -05:00
CamWight 664c9812d2 Paper scene: a sheet's pieces on paper are capped, refused by name past 4,194,304 (#4890 A1) (#11709)
A run's glyphs count once in a scene's records, for a writer that embeds its font, but the
walk onto a writer's grid copies every placed glyph's whole outline, so a sheet dense with
text could reach any size. The walk now counts the pieces it places and refuses a scene past
MAX_GRID_PIECES (1 << 22), saying to move some text to another sheet. place() is public so
a writer that draws text as outlines (PDF) counts with the same rule.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 13:20:44 -05:00
CamWight 08cd8d6442 Drawings: title blocks on sheets, and a drawing can start from or be restyled by a template (#3875) (#7208)
* Drawing sheets: a copy of a title block on the drawing border, saved with the sheet (#3875)

`Sheet.title_block` holds a copy of its definition, as `border` does. Each sheet's check lays it
out on the drawing border, so a sheet without a border, a block wider or taller than the border,
or a change of paper that leaves no room is refused. `drawing.sheet.add` and `drawing.sheet.set`
take `title_block` (a definition, or null for none), a new sheet takes the block of the sheet it
follows, and `drawing.inspect` lists it. A sheet without a block saves no `title_block`, so an
older drawing reads as having none (pending format file `sheet-title-block.rs`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: the built-in title block, on every new drawing (#3875)

`title_block::default_block()` is the 180 x 54 mm block of twelve cells approved on #3875, with
the legal owner read from the drawing's `organization` and the general tolerances as text to
change. `drawing.create` starts its sheet with it. The geometry tests read the built-in block
instead of their own copy of it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a title block's lines on paper, drawn after the views (#3875)

`TitleBlockLayout::paper` draws the outline, then every cell edge inside it, each once:
neighbours' shared edges, and edges that meet along one line, are one stroke. The sheet scene
paints the block after the views, and counts its records with the border's, so no view takes
its room. The view tests' drawing leaves the block off, so their scene is still the border and
the views.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: the title block's projection symbol, in the drawing's projection (#3875)

ISO 5456-2 (Annex A): a truncated cone and its end view, h across the cone's
small end from table A.1's sizes (3.5 to 20 mm), the largest that fits the
cell, lines h/10 wide. The end view is on the right in first angle and on
the left in third. The block only says a cell holds the symbol; the angle
comes from drawing.projection, and the sheet scene redraws when it changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a title block's fields, and text typed over its cells (#3875)

A title block cell reads its fields from the component the sheet documents (its
name, part properties, material and mass from the bill of materials), the
drawing, the sheet (number and count among the counted sheets) and the newest
revision row; a field with no value shows "-". A key its source doesn't have is
refused when a block is given or read. Sheets save text typed over their cells by
cell name; drawing.inspect lists each text cell with its value beside an override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates as data: a drawing with no model, and ISO built-ins (#3875)

A template is a drawing with no sources, views, dimensions, balloons, revision
rows or documented components, so it is saved and read as a drawing is. A new
drawing is a copy with ids of its own. The built-ins, ISO A4 to A0 for parts and
for assemblies, are made in code when asked for and differ only in their general
tolerances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates: drawing.create from a template, and the preferred ones (#3875)

drawing.create takes template? (default ISO A3, for a part or, for a design with
components, an assembly) and projection? over the template's. The new
drawing.template.list lists the built-ins and marks the one a new drawing starts
from. Preferences add a template for part drawings and one for assembly drawings;
the app fills in the preferred template and projection before drawing.create
runs, so a template named keeps its own projection, instead of changing the
projection after the drawing is made. A saved template this version doesn't know
reads as the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates: apply one to the drawing open, moving nothing (#3875)

drawing.template.apply {template, dry_run?} gives each sheet the paper, drawing
border and title block of the template's sheet in its place (its last for the
sheets past its own). Text typed over a cell stays while the new block has a
text cell of that name; the cells whose text is dropped are listed. Nothing
moves: the views a new paper leaves outside the drawing space are listed. A dry
run answers the same and changes nothing; applying is one undo step, none when
nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: views a title block covers are listed, never moved (#3875)

A view lying over its sheet's title block is listed with the ones the drawing
space leaves out, after a change of paper or block (drawing.sheet.set) and when
a template restyles a sheet. Where a block sits is one rule, the one a sheet is
checked by (Sheet::title_block_outline), and the overlap of two boxes is one
rule in Aabb2::overlaps, which the title block's own cell check now uses too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates: save one as a file, and start or restyle drawings from it (#3875)

drawing.template.save {path} writes the drawing open as a template: its format,
and nothing it draws or documents. drawing.create and drawing.template.apply take
a .json template file as well as a built-in's name. A file is read as a drawing
is, then held to the template rules; files are read and written through the same
file check and atomic write as the sheet export. A drawing made from a file keeps
its own copy, so a later save changes no drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: the built-in title block is 180 x 49 mm (#3875)

The top row is 15 mm, Sheet is 30 mm wide, lined up with Mass, and Date of
issue is 32 mm. The top row now holds one line of the legal owner, two of the
general tolerances and the 3.5 mm projection symbol.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: one operation sets a sheet's title block, and drawing.sheet.set names the typed text it drops (#3875)

A sheet's block was set in two places: drawing.sheet.set kept the typed
text of the text cells the new block still has, and drawing.template.apply
did the same and listed what it dropped. Sheet::set_title_block is now the
one rule both use. drawing.sheet.set's reply gains `lost`, the cells whose
typed text a new block drops, as drawing.template.apply's reply has it, and
its help says that a view over the title block is listed too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates: the built-ins and a new drawing start with the border only; the built-in title block is added by name (#3875)

A template now holds only the ISO border by default. A title block,
tables, references and text are added to a drawing, and a template saved
from it keeps them.

- The ten built-ins (iso-a4-part ... iso-a0-assembly) become five,
  iso-a4 to iso-a0: one sheet with the ISO border. Without the block, the
  part and assembly versions were the same.
- drawing.create with no template starts from iso-a3. The two
  preferences for parts and assemblies become one, drawing_template,
  "Template for new drawings".
- drawing.sheet.add and drawing.sheet.set take title_block: "built-in".
  The built-in block's general tolerances are a part's or, for a design
  with components, an assembly's, through Sheet::set_title_block.
- An old name such as iso-a3-part is refused with the list of names;
  drawings haven't shipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing title blocks: a drawing with no number of its own is numbered by the component its sheet documents (#3875)

The identification number cell read the drawing's part number property,
and showed "-" for a drawing that has none, which is every new one. Now
such a drawing shows the name of the component the sheet documents, so a
new drawing of a part is numbered by the part until it is given a number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing templates: a template saved from a drawing keeps typed text only over plain text cells (#3875)

A template saved from a drawing kept all the text typed over its title
block's cells, including text over a cell that shows a field, such as
the material or the title, which belongs to the design the drawing
documents. Now it keeps the text typed over cells of plain text, such as
the general tolerances, and leaves the rest behind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing title blocks and tables: a field without a value shows nothing (#3875)

A title block cell or a table row whose field has no value (a property
nobody gave, a row field its row doesn't have, an uncounted sheet's
number) now shows nothing instead of "-".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing title blocks: the built-in block's general tolerances start empty (#3875)

The general tolerances cell of the built-in title block now starts empty for
parts and assemblies alike, rather than ISO 2768-m in the block and a part's
or an assembly's text added by drawing.sheet.set. A shop types its own, or
its template carries them, and an empty cell shows nothing.

- template::PART_TOLERANCES, ASSEMBLY_TOLERANCES, for_assembly and
  built_in_block go: "built-in" is title_block::default_block.
- drawing.template.list's help no longer speaks of part and assembly
  built-ins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:01:13 -05:00
CamWight c5dab0b9a8 Paper scene: ellipses, hyperbolas and higher Béziers as cubics on the grid (#4890 A3b) (#9846)
A conic of weight other than 1 and a Bézier above degree 3 become cubic Béziers within
1e-4 mm of the curve (`paper::cubic_pieces`), so the walk takes them and every writer
draws them with the cubics it already writes. Each piece's cubic has the curve's ends
and end tangents; its distance is bounded from the Bernstein form, not sampled: for a
Bézier, by the control points of its difference with the cubic raised to its degree;
for a conic, by Q·D − N over the denominator's least value. A piece over the error is
halved exactly and tried again, up to 4096 a curve and 1,000,000 a scene; past that the
curve is refused, never drawn coarser. The walk counts the cubics it made. Circular arcs
are left to the shared `paper::cubics` helper.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:27:36 -05:00
CamWight cca77b176f Drawings: a shaded view is drawn on the sheet as a picture under its lines (#5624) (#10926)
* A drawing's list of one kind of record: ById, with balloons first (#5426)

Each kind on a sheet writes out the same add, find by halves, cap, order and check loop. ById holds
those, so a kind's own file holds only its own rules, and the file's bytes don't change: it saves as
the plain list. Views and dimensions stay on their own lists for now, since other work is on them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A drawing keeps an image's file once, however many places it sits (#5426 W38e-1)

images holds the files and sheet_images each place one sits, so a logo on every sheet costs what one
does. drawing.image.add reads the file (bytes and pixels checked from its header before anything
decodes it), reuses a file with the same bytes, and saves the bytes without the path. A file goes
with the last thing that uses it, which Drawing::images_in_use alone says, and reading a drawing
refuses a file nothing uses or a record its file disagrees with. A sheet's list and delete take the
placements on it. Deleting one placement waits for selection.delete to take a drawing's objects. Its
pixels on paper are W38e-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sheet images: an image file's pixels on any paper grid (#5425, W38e-2)

A PNG or JPEG decoded once, then stretched over its paper rectangle onto the pixel grid a writer
asks for: a box filter in linear light with alpha premultiplied, so a shrunk logo keeps its edges
and gains no dark fringe. The size is read from the header and refused over 64 megapixels before
anything is decoded. An EXIF orientation is applied; colour profiles are not read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Sheet images: a placement in the sheet's scene and its SVG (#5425, W38e-2)

Each image placed on a sheet is a group of the new kind `sheet_image`, between the drawing border
and the views, so a view's lines draw over a photo. `drawing_pictures` holds both that group and
the provider a writer asks for its pixels, so the two read the same placement.

The provider holds what one sheet uses, decodes a file when it is first asked, once however many
placements show it, and refuses a sheet whose files hold more than 64 million pixels, from their
headers, before anything decodes. `drawing.export` passes it to the SVG writer in place of
`NoPictures`, and a file that won't decode refuses its sheet by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: pin the product image's output before it is drawn in tiles (#5624, W17b S1)

The decoded rows of five product renders (orthographic on a transparent background, perspective on a
solid one, a PBR surface, two glass layers sorted back to front, triangles crossing every edge of the
picture), taken on this tree. The next change reshapes ImageTask into a prepare stage, tiles and a PNG
sink; they must not move. The PNG bytes are not pinned: their compression depends on the deflate
backend the build unifies (flate2's zlib-rs when the UI crate is built alongside, miniz_oxide
without), not on this code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: pictures drawn tile by tile into any pixel window (#5624, W17b S1)

The CPU product renderer, reshaped so drawings can use it: one rasteriser (`Raster`) prepares a
scene once (appearances, projected triangles, glass sorted back to front), then draws each tile of
the picture on a canvas of its own. A sample's position comes from its whole-picture index, so tiles
put side by side are the picture drawn whole, to the byte.

- `ImageTask` (render.image) is that rasteriser over one whole-picture tile from the product camera,
  then its PNG: the same output, pinned by the previous commit's rows.
- `PictureTask` draws a `PictureRequest`: a `Projection` (the product camera, or an `OrthoFrame`,
  W17a's view frame as plain data: a `Plane` basis, a scale and a paper shift, on a
  `geom::pixel_grid::PixelGrid`), any list of `PixelWindow` tiles, `Supersample::One` (pixel centres)
  or `Two` (i + 1/4 and i + 3/4), a `BodyRole` per body (`DepthOnly` hides what lies behind it and
  draws nothing itself) and an optional background (none: transparent). Each tile comes back as
  straight-alpha sRGB RGBA rows, blended in linear light.
- The orthographic map is the frame's paper map, onto the grid, so lines drawn from the same frame
  register with the pixels (to 1e-9 mm in the test).
- Limits: 65,536 px a side, no total cap; each tile under the resolver's `RenderLimits`, as the
  product image is; at most 65,536 tiles; the prepared lists as a product image's; pixel tests up to
  8 per sample of the picture (128 M for a product image, as before), counted at admission by the
  one projection the raster uses, as main counts a product image's.

Not here: the drawing light, stored per-tile bins (S2, measured), the paper scene's picture item
(S3), 4x sampling, and `raster::render`, the snapshot rasteriser, which stays as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: time the tile renderer (#5624, W17b S2)

An ignored timing test, as `simplify_timing` is: a 65,548-triangle front view drawn as the
product image, as one 300 dpi picture, and in 64-row strips at 300 and 1200 dpi, each the median
of three runs. It is the measure S2's speed-ups are judged by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: a tile passes over a triangle outside it before painting it (#5624, W17b S2)

Each tile placed and painted every triangle of the picture, then dropped those outside it: the
appearance lookup, the PBR normals and the colour decode ran for every triangle in every tile. A
triangle's work is now three steps, `corners`, `look` and `place`. Preparation runs them in
today's order, so its checks and errors are the same. A tile places a triangle first and paints
only those inside it. What is drawn doesn't change: the pin and the tile tests stay as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: one light per picture (#5624, W17b S2)

`raster::shade` normalised its key light and highlight directions again for every sample,
although both depend only on the view. `raster::Light` works them out once per view and shades
with them. The tile renderer and the snapshot rasteriser (`raster::render`) both use it, so the
product light is written in one place. The arithmetic is the same, so every pixel is too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: sRGB codes from tables (#5624, W17b S2)

Each shaded sample was stored as sRGB through a power (three per sample), and each output pixel
was averaged from its samples through twelve more and written back through three: the powers
were most of a sample's arithmetic. Decoding is now a 256-entry table of `geom::srgb`'s own
values (#7162), which also ends this file's own copy of the formula. Encoding counts the code
starts (the linear values halfway, in sRGB, between two codes) at or below the value. Within a
billionth of a start, where the formula's rounding could fall either way, `srgb::encode8`
decides, so every code is the one it gives. A test checks both against `geom::srgb` at every
start, the numbers around it, the band's edges and a million values in and outside 0 to 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: a tile's passes run in loops (#5624, W17b S2)

The raster went back through its step loop for every sample: the cancel check, the stage match,
the tile's window and its canvas bounds, once per sample cleared, tested or averaged. Clearing,
the opaque and glass passes and the downsample now each loop on their own up to the step's budget,
counting a unit as before; preparation and the glass sort still take one unit a call. A step
still checks for cancellation before each stage's run and still does at most 16384 units, so the
host's slices and the progress counts are the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: tests for a picture's light, glass order and test budget (#5624, W17b S2)

Three planted faults went through the image tests; each now has one
that fails:
- an orthographic picture lit from another direction than its eye: a
  face square-on to the view is the colour the product image gives it;
- glass placed by a triangle's first corner, not its middle: a tilted
  square whose first corners lie in front of the near one, its middles
  behind;
- a picture held to the product image's pixel-test budget: a face
  across most of an A0 sheet at 1200 dpi is prepared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a shaded view's picture on paper (#5624, W17 V9b)

A saved view in a shaded style draws on its sheet and in drawing.export:
draw puts one picture per entry of the view's parts under its lines, and
ViewPictures renders each on the writer's grid through the frame that
placed the lines. SheetPictures answers for a sheet's images and views.
The styles with lines stay refused until W06's exact lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Render: a picture's count of its tiles and its fixed limit, as #6483 has them (#5624)

A triangle counts only where it meets the tiles asked for, and a picture has a fixed 400 M pixel tests,
as #6483 at bf93e890e. V9b's stack holds an older copy of that slice; this drops away when drawings
takes main. PictureTask says what admission counted, for the view's running sum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a shaded view's pictures add up to one limit (#5624, W17 V9b)

A writer asks for a picture a band at a time, and each request is limited on its own, so ViewPictures
keeps what each view's requests have counted and refuses the next one past the limit, by name, before it
paints. That makes 400 M a bound on a whole view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:21:51 -05:00
CamWight 1bd0040d25 Drawing export: SVG and PNG cut a picture to a detail view's boundary (#4890) (#10969)
* Paper scene: clips of lines and arcs, cut once before every writer (#4890 A8a)

Scene::prepare_clipped(clips) cuts each group of an object to that
object's Clip, a rectangle or a circle (a detail view's), and keeps what
lies inside; prepare() is prepare_clipped with none. Prepared holds a
Cow: a scene without clips is borrowed as it is, and Group and Scene are
unchanged, so today's scenes are the same bytes.

- Lines and arcs are cut where they cross the edge; a part is kept when
  its middle is inside, the edge included. Parts no longer than EPS go.
- Kept parts that follow one another stay one open stroke. A stroke
  wholly inside is kept whole, closed if it was; a closed one cut open
  rejoins across its seam.
- A dashed stroke keeps its pattern: each part's phase is where it
  starts along the stroke. Across a closed one's seam the pattern runs
  on only when its length is whole patterns, so the parts rejoin only
  then.
- Checked: one clip an object, a rectangle from its lower-left to its
  upper-right corner or a circle with a radius above 0, within 1e5 mm;
  a clipped object's items strokes of lines and arcs. What the cuts
  made is validated again.

SVG and PNG draw the cut scene through Prepared as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: clips of every curve, and fills kept or dropped whole (#4890 A8b)

A clipped object's strokes may now hold any curve, not only lines and
arcs, and its fills are checked against the clip.

- A line or an arc is still cut where its equation says. Any other
  curve is halved while its control points lie either side of the edge
  (widened by EPS), until they span at most CUT_MM (1e-9 mm), and is cut
  there. A curve that needs more than MAX_HALVINGS halvings (one that
  runs this close along the edge) is refused as not supported yet.
  Each part is the same kind of curve as the whole.
- Seg2::part gives the piece between two parameters, starting and
  ending exactly where split_at puts those points; a conic's tail is
  re-normalised, so its second parameter is carried into it.
  Seg2::MIN_SPLIT names split_at's distance from the ends.
- A line or a circle within EPS of touching a circle only touches it:
  one cut there, never a sliver of chord.
- A fill wholly inside its clip is kept and one wholly outside is
  dropped. One that crosses the edge, or lies round the whole clip, is
  refused as not supported yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures in a clipped object, cut by the SVG and sheet PNG writers (#4890 A8c)

A picture wholly outside its object's clip is dropped when the scene is prepared. One crossing the clip's edge reaches its writer with the clip, on the grid item, since pixels can't be cut as geometry: SVG cuts it with a clipPath, the sheet PNG by whether each pixel's middle is inside. Both draw only the pixels that meet the clip's box. Text in a clipped object stays refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 07:58:27 -05:00
CamWight 49a3ba1c03 Paper scene: dashed lines, in SVG and PNG (#4890 A5) (#7430)
A stroke can carry a pattern, `Dashes { lengths_mm, phase_mm }`: dash and
gap lengths in turn, in paper millimetres, and how far into the pattern
the stroke starts. Without one a stroke is solid, and a solid stroke
reads and writes as before.

- Checks: 2 to 12 lengths, an even count, each above 0 and at most
  1000 mm; the phase from 0 up to the pattern's length; at most a
  million dashes along the stroke, counted from its pieces' own lengths.
- The walk puts the pattern on the writer's grid: a dash or gap under
  half a step is one step, counted, as widths are.
- SVG: stroke-dasharray, and stroke-dashoffset when the phase isn't 0.
- PNG: tiny-skia's dashing on the flattened line; its points count
  against the sheet's, a line in a gap draws nothing, and one tiny-skia
  won't cut is refused rather than drawn solid.
- The paper fuzz cases carry a dashed arc.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 06:52:45 -05:00
CamWight d78895de4d Where a drawing stands: drawing.status in one read (#4894 WE2) (#11352)
drawing.status says each view's status (current, out of date, placeholder or
failed, with why) from what the sheet page last drew, and each dimension's in the
words inspect uses, reading each body once for the call. Nothing is drawn.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 04:41:33 -05:00
CamWight 112e0a3d9c Paper text in SVG: a run's glyph outlines on the writer's grid, filled nonzero (#4890 B2a) (#9708)
The walk puts a text run on the grid as its glyphs' outlines, placed by the run
(`TextRun::outlines`), each loop walked as a fill's: `GridItem::Text`, with the run's
index among its group's items, so a writer that sets real text (PDF) finds it in paint
order. SVG writes one path a run, filled black with its default nonzero rule, so text
looks the same without the font and never reflows. The sheet PNG refuses text by name
until B2b, and its refusal test says so.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 02:09:25 -05:00
CamWight eb0b427a36 Drawings WE3-WE5: commands report where each object sits on a sheet, and select and delete views, dimensions and balloons (#4894) (#11047)
* Where a sheet's objects sit: bounds beside the scene (#4894 WE3)

The sheet scene now says where its border and each view sit on paper: the box
round what each draws, and a view's boundary, so one that can't be drawn can
still be picked. It is worked out once per assembly, beside the scene rather
than in it, so no writer sees it and the page needn't inspect each view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Selected drawing objects: a view, dimension or balloon in the selection (#4894 WE4)

select.set takes {type: drawing, drawing, object} for the drawing open. It adds no
undo step and changes nothing saved, and a selected object leaves the selection
when an undo takes it or its drawing closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Delete on a sheet: views, dimensions and balloons in one step (#4894 WE5)

selection.delete now works with a drawing open: a view goes with its dimensions
and balloons as one undo step, and one with views projected from it is refused,
naming them, unless they are selected too or contents: delete takes them. A sheet's
delete and this one share the record's one removal rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: a view's box holds what it draws, and what's chosen goes in any order (#4894 WE3, WE5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:59:41 -05:00
CamWight ba69e5488c Drawings WE1: Inspect Drawing lists each sheet's views, with their names, dimensions and balloons (#4894) (#11350)
* Where a sheet's objects sit: bounds beside the scene (#4894 WE3)

The sheet scene now says where its border and each view sit on paper: the box
round what each draws, and a view's boundary, so one that can't be drawn can
still be picked. It is worked out once per assembly, beside the scene rather
than in it, so no writer sees it and the page needn't inspect each view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Selected drawing objects: a view, dimension or balloon in the selection (#4894 WE4)

select.set takes {type: drawing, drawing, object} for the drawing open. It adds no
undo step and changes nothing saved, and a selected object leaves the selection
when an undo takes it or its drawing closes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Delete on a sheet: views, dimensions and balloons in one step (#4894 WE5)

selection.delete now works with a drawing open: a view goes with its dimensions
and balloons as one undo step, and one with views projected from it is refused,
naming them, unless they are selected too or contents: delete takes them. A sheet's
delete and this one share the record's one removal rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: a view's box holds what it draws, and what's chosen goes in any order (#4894 WE3, WE5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* What's on each sheet: drawing.inspect lists its views (#4894 WE1)

Each sheet in drawing.inspect now lists its views: each base view by id, followed
by those projected from it, with what it is called and its dimensions and balloons.
It is one pass over the record with no model read, so the Browser can read it once
per revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 00:59:36 -05:00
CamWight 9308095744 Paper text in the browser: Japanese through the font the web app fetches (#4890 F6) (#10516)
The browser has no font folders, so `font_dirs` is empty there. The first
kana or Han a text's font lacks asks for the font the web app already
fetches on demand (BIZ UDPGothic); no new file. The text is pending until
it comes, then sets whatever it has a glyph for. If the fetch fails, or
the font lacks a character, that character is refused by name, and the
refusal says the web covers Japanese for now.

The web app starts the fetch through its existing loader, once, when a
text waits for the font, and hands the font over when it arrives.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:59:34 -05:00
CamWight 5bf3940c04 Sheet tables on paper: a table's lines (#3874, #4890 W10.7, D3a) (#7745)
TableLayout::paper gives a placed table's lines as one paper group of
the kind it is (parts_list or revision_table), so both sheet tables reach
paper through one function:

- the outline as one closed stroke, outline_line_mm wide;
- a stroke cell_line_mm wide across the whole table at each row boundary
  inside the outline, the heading's too, from the top whichever side the
  heading is on;
- a stroke down the whole table at each column boundary inside it.

Each line is drawn once, centred on its edge, on the layout's own
corners. The cells' text follows once the paper scene has text (D3b,
after #4813); hatch clearance waits for hatching.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:53:06 -05:00
CamWight 2f483a00c0 Drawing text F4: a character missing from the chosen font is drawn with another installed font (#4890) (#10515)
* Drawings: the font a drawing's text is set in, by family (#4890 F1)

A drawing names the font family its text is set in (`font`, saved only when
set; a pending drawing-format file). `drawing.create` and `drawing.set` take
it, and `drawing.inspect` reports it, with `font_note` when this computer sets
the text in the built-in font instead ("Futura isn't on this computer: set in
Liberation Sans"). Drawings name fonts by family, so they travel: a font file
is refused.

Paper text finds the family among the installed fonts as sketch text does
(`paper_text_in` for given folders), admits each face once a process, and
sets a family that is missing or can't be set on paper in the built-in font.
Bold takes the family's bold face; the serif flag comes from the face's OS/2
family class. The fixed list of drawing fonts is gone.

New drawings made in the app take the interface font when it is a family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper text: any face with outlines is set, and says how a PDF may carry it (#4890 F2)

A face is admitted for paper whatever its tables lack:
- a variable face is set at its default instance, which is what a PDF draws;
- its cap height is the one sketch text scales it by (OS/2, else the top of
  its `H`, else 70 % of the em);
- its PostScript name is what a PDF name can hold of its own, else of its
  family and style, else `Font-` and the start of its id;
- without a `post` table it has no italic angle, and a fixed pitch when every
  glyph advances alike;
- its stem width is measured across its `l`, else taken from its weight.
A bitmap or colour-only face, with no outlines to fill, is refused by name.

`Font.embed` says how a PDF may carry the face, from its embedding permissions:
a subset, the whole face (no subsetting), or not at all (restricted, bitmap
embedding only, or CFF2 outlines), when the PDF draws the runs' outlines with
their text beside them. Which characters a PDF name may hold is now one rule,
`is_postscript_char`, that the record's check and sketch share.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper text: a font's family, for readers that pick fonts by family (#4890 F2)

`Font.family` is the face's own family name: its typographic family (name
ID 16), else its family (ID 1), read from the face itself (in a collection,
not the first face), without control characters and at most 255 characters;
empty when it has neither. A DXF text style picks a font by it; `Font.id`
stays the identity, so two faces of one family are still two fonts
(@0xlillith on #4890). The name lookup PostScript names use is one function
now, shared by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fonts: one list of fallback fonts per script, in sketch (#4890 F3)

The interface's lists of fonts to try for a script move down to
`sketch::fonts::fallback`, so drawings can use the same ones. Each row is
a family and the files it's known to be in. The interface reads the files
in the order it always did, so what it finds is unchanged.

- `CRAFT_FONTS_DIR` joins `font_dirs`, as the interface already has it:
  `SOLIDCRAFT_FONT_DIRS`, else the pack, then the system's folders.
- `font_files` sorts the files within each folder, so a search doesn't
  depend on how the system lists a folder.
- The Windows folder search takes its names from the Japanese list
  instead of its own copy of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper text: a character its font lacks falls back to a font that has it (#4890 F4)

A text on paper was refused for any character its font lacks. Now each
such character falls back to an installed font that has it: the script's
list in `sketch::fonts::fallback` first, Han by the drawing's language,
then the first face in sorted order with outlines and the character.

- Runs split by direction, then by font. Every font takes the same em
  size, on the line's baseline and pitch. A space, a digit or punctuation
  stays with the run it follows when that font has it; a mark stays with
  its letter. A font fallen back to is shaped, with the drawing's
  language. Its nearest style is used: bold or italic isn't refused.
- A line its own font has all of is set as it was.
- A character no font has is still refused by name.
- The last-resort search reads each file's character map alone, stays
  within `MAX_FONT_FILES`, and keeps its answers, misses too, for the
  process.
- `paper_text_now` is for the window's thread: the search runs on a thread
  of its own (`font-search`) and it refuses with `FontPending` until the
  answers are in. `paper_text` searches where it's called.
- `paper_fallback_notes` names what fell back, for a drawing's status,
  from the answers so far: it looks for no font.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:27:16 -05:00
Nathan Dane d5fd14135f Add the native Drawing workspace UI scaffold (#5349)
* docs: plan drawing workspace UI implementation

* assets: add drawing icon drafts for owner review

* feat(ui): add drawing workspace scaffold with shared ribbon and docked settings

Expose planned Drawing tools without introducing drawing documents, paper rendering or CAD behavior. Reuse existing source BOM, properties and flat-pattern services; retain placeholders for unapproved icon drafts.

Validation: full UI, kernel, icon, clippy, wasm and layout checks passed. Full cargo xtask ci reports three geometry failures that also reproduce on unchanged drawings base 3b982ca: counterbored chamfered-disc ring cut, robustness seed 36 and seed 71. No geometry source or golden fixtures changed.

* Preserve default icon palette in Drawing scaffold tests

* Preserve Drawing providers and complete scaffold translations
2026-11-01 21:45:05 -05:00
CamWight 85be50df0d Reference parts: a view shows them by percent, not by a margin (#5866 V5c) (#10713)
reference_margin_mm becomes reference_shown_percent, 0 to 100. At 0 the
boundary is the model's box, as before, and no reference part is measured.
At 100 it holds the model and all the view's reference parts. Between, each
side moves on its own, so a part on one side grows only that side.

The model's middle now places a view, not the middle of what it shows, so a
view stays lined up with its projected views as its box grows. With nothing
grown the two are the same point and the picture is the same.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:36:53 -05:00
CamWight 71edae80df Drawing views made by command: drawing.view.base and drawing.view.projected (#4816) (#9254)
* Drawing views made by command: drawing.view.base and drawing.view.projected (#4816)

drawing.view.base {sheet, view?, scale?, position_mm?, the view settings?} adds a base view of the
drawing's design to a sheet. Without a scale it takes the largest ISO 5455 preset at which it lies
inside the sheet's drawing space, centred where it goes, its margin included, as the snapshot fits
it; that scale is saved as the view's own (1:1 with nothing to measure).

drawing.view.projected {parent, side, place?} adds a view projected from a saved view, on its
parent's sheet, following its parent's scale and display settings. Without a place it is lined up
with its parent, its boundary 20 mm past the parent's (DEFAULT_GAP_MM, until a drawing style
gives it).

Each is one undo step through edit_drawing, refused with nothing changed when the record's rules
refuse it; the reply gives the view, its sheet, its scale, its boundary and, when it leaves the
sheet's drawing space, itself as outside (it isn't moved back in). Unknown fields are refused.

One rule, one place: whether a view leaves its drawing space is drawing_view::leaves, and the sheet
of an id is drawing_view::sheet, used by outside() and both commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views made by command: a build fix, and each view one undo step, tested (#4816)

From the review of the view create commands:
- drawing.view.base measures the shown bodies (`measure` takes them, not the session), so it builds.
- The nothing-to-measure case is a design with one sketch and no bodies, as the other view tests
  make it: drawing.create refuses an empty design.
- The schema test gives drawing.view.base its own key list (it takes no snapshot-only keys).
- Every view a test makes is checked to be one undo step, not only the first: a second step made
  under the first (a nested edit) still let one undo take the view away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:12:38 -05:00
CamWight 93baf88e2d Paper scene: pictures in the sheet PNG and SVG (#5624, W17b S5) (#7591)
* Paper scene: one walk onto a writer's number grid, for every writer (#4890, C4)

The SVG writer kept its own number grid: each coordinate whole nanometres, every joint written once,
a closed path ending exactly on its first point, slivers merged into the next piece and a path's
last piece kept. PDF, PNG and the Drawing workspace's canvas need the same rules, so they move into
`geom::paper::grid`, and the SVG writer only formats what the walk gives it.

- `Grid`: `per_mm` steps a millimetre. `Grid::NANOMETRE` (six decimals of a millimetre) is SVG's.
  `Grid::new` takes 1 to 1e9; a power of two keeps every number exact as a binary float, which a
  writer of 32-bit numbers such as PDF can use.
- `Prepared::on_grid(grid) -> OnGrid`: each group's items as paths of `Piece`s (lines, arcs of at
  most a quarter turn, cubics, parabolas), y up as in the scene, a closed path's closing line left
  to the writer's close, and a report of widths snapped and pieces merged. Slivers are measured in
  the grid's own steps, so a coarser grid merges more.
- The SVG writer flips y on the whole numbers it's given and writes the same bytes as before.
  `SvgReport` is the grid's report.
- Ellipses, hyperbolas and Béziers above degree 3 are still refused (A3b), now by the walk.

DXF stays on its own number rule for now: it writes exact entities (ELLIPSE, SPLINE) and is being
reworked under #5318 to #5321; whether its polylines take a grid is decided there.

Tests: every SVG test unchanged, bytes included; the grid's rounding and its bounds; the A3 border
on the grid (y up, closing line left out, fill loops closed); slivers in the grid's own steps, with
the merge boundary both ways (10 and 11 steps, a 6-8 and a 7-8 step); an arc's quarters with their
radius and turn in steps; a width under half a step made one step; an ellipse refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: a sheet PNG at a chosen dpi and colour (#4890 A4)

A new writer, io::paper::paper_png, that reads the shared walk on the
nanometre grid, the one the SVG writer uses, so a PNG draws the same joints,
closures and merges. Neither the walk nor the SVG writer changes.

- Curves are flattened within 1/8 px by geom::paper::flatten, which turns
  each walked piece back into its Seg2 (an arc's centre from its ends, radius
  and direction) and takes Seg2::chords' steps. chords is the bound
  Seg2::polyline already used (the chord's bulge for arcs, Wang's bound for
  the rest), moved out of polyline unchanged so both read one rule.
  polyline itself doesn't fit: it floors an arc's tolerance at a millionth of
  its radius and caps a piece at 4096 steps, coarser than asked without a
  word (0.1 mm, 9 px, on a 100 m arc at 2400 dpi). Over 2^20 points a path,
  or 2^24 a sheet, is refused instead. tiny-skia only gets lines: its own
  flattening stops at 64 lines a curve, more than a pixel off on a large arc
  at a high dpi.
- 25 to 2400 dpi (300 by default). 8-bit greyscale (the default), 1-bit
  black and white (no anti-aliasing, every line at least a pixel wide), 24-bit
  colour, or 32-bit colour with transparent paper.
- The file records its dpi (pHYs) and sRGB, so it prints at the sheet's size.
- Each line is stroked once, then the sheet is drawn in bands of 16 MiB of
  pixels streamed into the file, so memory doesn't grow with the sheet. Each
  band also draws a row above and below that isn't written: tiny-skia starts
  an edge a little differently where a clip cuts it (one subsample, 16
  levels, in a few pixels), so no written row sits on a cut and the bytes are
  the same whatever the band.
- A line under a pixel wide (in greyscale and colour) is drawn as tiny-skia's
  hairline, its coverage scaled by its width: its stroked outline would fall
  between the rows the rasteriser samples and vanish.

Tests: the pixel size and pHYs of A4 at 300 and 25 dpi; a line's and a fill's
pixels at 254 dpi (butt ends, the fill's hole); each colour's PNG type, ink
and paper; a hairline between two rows' centres that is light grey in
greyscale and stays one pixel in black and white, and a bar a fifth of a pixel
wide that black and white inks by its centre; a closed outline joined at its
start; a 40 mm circle round to the pixel at 600 dpi; the same bytes drawn 1, 3
or 7 rows at a time or all at once, and again; the refusals; the flattener's
bounds and counts, and its point cap at the exact boundary; the scene-JSON
fuzz through the PNG writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Geom: the pixel grid of a paper rectangle (#5624, W17b S1)

One rule for where a picture's pixels lie on paper, shared by the picture renderer and the writers
that place it: anchored at the rectangle's lower-left corner, rounded outward (W = ceil(w / p - 1e-9)
with p = 25.4 / dpi), so the overhang is at the top and the right; rows run top-down. At most
65,536 px a side; nothing caps the total, as pictures are drawn in tiles and strips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures, and a sheet's pixel grid and windows (#5624, W17b S3; #4890 A9)

A picture on a sheet is `Item::Picture`: the rectangle it sits in, the name of its pixels
(`source`, opaque as a group's object id is) and its opacity. The pixels stay out of the scene: a
writer asks a `Pictures` provider for them on the grid it draws on. A PNG at 1200 dpi then gets a
shaded view drawn at 1200 dpi, not one stretched, and a scene stays small, plain data.

The pixel grid gains the two rules a sheet needs, beside the rectangle's own grid:
- `PixelGrid::sheet`: the whole sheet's grid, anchored at its top-left corner, as an image of
  the sheet is drawn.
- `PixelGrid::window`: the columns and rows a rectangle meets; a side on a pixel edge takes no
  sliver beyond it. A picture drawn into its window of the sheet grid is placed by copying whole
  pixels.

`Picture::grid` is a picture's own grid at a dpi, for SVG and PDF to embed. `Scene::validate`
counts a picture as one record. It refuses a rectangle outside the coordinate bound or without
area, and an opacity that isn't above 0 and at most 1. `Aabb2` is read and written as
`{min, max}`.

On the walk (C4), which S3 follows here: `GridItem::Picture` passes a picture through with its rectangle in
millimetres, since the pixel grid places its pixels, and the SVG and PNG writers refuse it ("not supported yet:
pictures (A9)") until their own slices. `GridItem`, `GridGroup` and `OnGrid` are `PartialEq` only, as a
picture holds numbers that aren't `Eq`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures in a sheet PNG (#5624, W17b S5)

The sheet PNG writer draws the scene's pictures (S3's Item::Picture) where
the scene orders them, between its lines and fills, from a Pictures provider.

- The PNG is now the sheet's pixel grid (PixelGrid::sheet, S1): anchored at
  the sheet's top-left corner and rounded outward, so nothing is clipped.
  210 mm at 300 dpi is 2481 px (it was 2480, rounded to the nearest). A side
  over 65,536 px is refused and the refusal names the limit ("a sheet PNG
  side of ... px at ... dpi is over the limit of 65536 px"); an A0 sheet
  fits up to about 1400 dpi.
- A picture fills exactly its window of that grid, so it registers with the
  lines drawn over the same rectangle. Each band asks the provider for just
  its own rows, as straight-alpha sRGB RGBA, and the bytes are checked: too
  few or too many, or the provider's error, refuse the PNG with the picture's
  source named. paper::NoPictures refuses every picture.
- Pictures are asked for at most MAX_PICTURE_DPI (1200): above it, on the
  grid k = ceil(dpi / 1200) times coarser, whose pixels each fill exactly
  k x k of the sheet's (both grids share the top-left anchor), so a 2400 dpi
  export doesn't ask a renderer for four times the pixels.
- Source-over in sRGB, the picture's alpha times its opacity; transparent
  paper keeps the alpha. In black and white each picture pixel is dithered
  with an 8 x 8 ordered (Bayer) matrix at its place on the sheet, so bands
  don't show; lines stay solid. PngReport says whether anything was
  dithered.
- In the writer a picture is one more shape a band draws, beside A4's
  outlines, fills and hairlines; the walk's picture arm passes pictures
  through, and the SVG writer still refuses them (A9).

Tests: a picture's exact window and its register with a fill over the same
rectangle, also on a sheet 100.8 px high (the grid anchored at its top);
pictures keep their place in the paint order; opacity scales the alpha (on
white and transparent paper, and the picture's own half alpha); above 1200
dpi a coarser picture's pixels are each copied 2 x 2; black and white
dithers half of mid grey and keeps a line over it solid, whatever the band;
a provider's short rows, its error and no provider are refused, and a
picture off the sheet asks for nothing; the bands test draws three pictures
1, 3 or 7 rows at a time, the same bytes and each picture row asked for
once; the sheet sizes on the outward grid and the 65,536 px refusal; the PNG
fuzz with a picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures in a sheet SVG (#5624, W17b S5)

A picture is an <image> holding a PNG as a data URI, on the picture's own
pixel grid (Picture::grid) at the picture resolution the caller gives,
25 to 1200 dpi: paper_svg(scene, pictures, picture_dpi), the same shape as
paper_png. Only its window over the sheet is embedded and asked for, so it
never costs more pixels than the sheet holds; one off the sheet is left out.
Its x, y, width and height are the grid's own pixel edges put on the
nanometre grid every line is on, so a line on its edge has its number.
Opacity below 1 is written, in millionths as lengths are.

The PNG writer gives the embed its encoder settings (sRGB, the dpi
recorded) and its provider check, now one function each, and streams the
picture a band of rows at a time, refused once it passes the room left
under the file's 64 MiB.

Tests: the 1182 x 591 px grid at 300 dpi and its edges, a line on the
edge, paint order and opacity, the crop and the picture off the sheet, the
bands, and each refusal; the SVG fuzz case gains a picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:01:08 -05:00
CamWight 4060069fcd Drawing views: shaded snapshots and their pictures' parts (#5624, W17b S4) (#9798)
* Paper scene: one walk onto a writer's number grid, for every writer (#4890, C4)

The SVG writer kept its own number grid: each coordinate whole nanometres, every joint written once,
a closed path ending exactly on its first point, slivers merged into the next piece and a path's
last piece kept. PDF, PNG and the Drawing workspace's canvas need the same rules, so they move into
`geom::paper::grid`, and the SVG writer only formats what the walk gives it.

- `Grid`: `per_mm` steps a millimetre. `Grid::NANOMETRE` (six decimals of a millimetre) is SVG's.
  `Grid::new` takes 1 to 1e9; a power of two keeps every number exact as a binary float, which a
  writer of 32-bit numbers such as PDF can use.
- `Prepared::on_grid(grid) -> OnGrid`: each group's items as paths of `Piece`s (lines, arcs of at
  most a quarter turn, cubics, parabolas), y up as in the scene, a closed path's closing line left
  to the writer's close, and a report of widths snapped and pieces merged. Slivers are measured in
  the grid's own steps, so a coarser grid merges more.
- The SVG writer flips y on the whole numbers it's given and writes the same bytes as before.
  `SvgReport` is the grid's report.
- Ellipses, hyperbolas and Béziers above degree 3 are still refused (A3b), now by the walk.

DXF stays on its own number rule for now: it writes exact entities (ELLIPSE, SPLINE) and is being
reworked under #5318 to #5321; whether its polylines take a grid is decided there.

Tests: every SVG test unchanged, bytes included; the grid's rounding and its bounds; the A3 border
on the grid (y up, closing line left out, fill loops closed); slivers in the grid's own steps, with
the merge boundary both ways (10 and 11 steps, a 6-8 and a 7-8 step); an arc's quarters with their
radius and turn in steps; a width under half a step made one step; an ellipse refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: a sheet PNG at a chosen dpi and colour (#4890 A4)

A new writer, io::paper::paper_png, that reads the shared walk on the
nanometre grid, the one the SVG writer uses, so a PNG draws the same joints,
closures and merges. Neither the walk nor the SVG writer changes.

- Curves are flattened within 1/8 px by geom::paper::flatten, which turns
  each walked piece back into its Seg2 (an arc's centre from its ends, radius
  and direction) and takes Seg2::chords' steps. chords is the bound
  Seg2::polyline already used (the chord's bulge for arcs, Wang's bound for
  the rest), moved out of polyline unchanged so both read one rule.
  polyline itself doesn't fit: it floors an arc's tolerance at a millionth of
  its radius and caps a piece at 4096 steps, coarser than asked without a
  word (0.1 mm, 9 px, on a 100 m arc at 2400 dpi). Over 2^20 points a path,
  or 2^24 a sheet, is refused instead. tiny-skia only gets lines: its own
  flattening stops at 64 lines a curve, more than a pixel off on a large arc
  at a high dpi.
- 25 to 2400 dpi (300 by default). 8-bit greyscale (the default), 1-bit
  black and white (no anti-aliasing, every line at least a pixel wide), 24-bit
  colour, or 32-bit colour with transparent paper.
- The file records its dpi (pHYs) and sRGB, so it prints at the sheet's size.
- Each line is stroked once, then the sheet is drawn in bands of 16 MiB of
  pixels streamed into the file, so memory doesn't grow with the sheet. Each
  band also draws a row above and below that isn't written: tiny-skia starts
  an edge a little differently where a clip cuts it (one subsample, 16
  levels, in a few pixels), so no written row sits on a cut and the bytes are
  the same whatever the band.
- A line under a pixel wide (in greyscale and colour) is drawn as tiny-skia's
  hairline, its coverage scaled by its width: its stroked outline would fall
  between the rows the rasteriser samples and vanish.

Tests: the pixel size and pHYs of A4 at 300 and 25 dpi; a line's and a fill's
pixels at 254 dpi (butt ends, the fill's hole); each colour's PNG type, ink
and paper; a hairline between two rows' centres that is light grey in
greyscale and stays one pixel in black and white, and a bar a fifth of a pixel
wide that black and white inks by its centre; a closed outline joined at its
start; a 40 mm circle round to the pixel at 600 dpi; the same bytes drawn 1, 3
or 7 rows at a time or all at once, and again; the refusals; the flattener's
bounds and counts, and its point cap at the exact boundary; the scene-JSON
fuzz through the PNG writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Geom: the pixel grid of a paper rectangle (#5624, W17b S1)

One rule for where a picture's pixels lie on paper, shared by the picture renderer and the writers
that place it: anchored at the rectangle's lower-left corner, rounded outward (W = ceil(w / p - 1e-9)
with p = 25.4 / dpi), so the overhang is at the top and the right; rows run top-down. At most
65,536 px a side; nothing caps the total, as pictures are drawn in tiles and strips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures, and a sheet's pixel grid and windows (#5624, W17b S3; #4890 A9)

A picture on a sheet is `Item::Picture`: the rectangle it sits in, the name of its pixels
(`source`, opaque as a group's object id is) and its opacity. The pixels stay out of the scene: a
writer asks a `Pictures` provider for them on the grid it draws on. A PNG at 1200 dpi then gets a
shaded view drawn at 1200 dpi, not one stretched, and a scene stays small, plain data.

The pixel grid gains the two rules a sheet needs, beside the rectangle's own grid:
- `PixelGrid::sheet`: the whole sheet's grid, anchored at its top-left corner, as an image of
  the sheet is drawn.
- `PixelGrid::window`: the columns and rows a rectangle meets; a side on a pixel edge takes no
  sliver beyond it. A picture drawn into its window of the sheet grid is placed by copying whole
  pixels.

`Picture::grid` is a picture's own grid at a dpi, for SVG and PDF to embed. `Scene::validate`
counts a picture as one record. It refuses a rectangle outside the coordinate bound or without
area, and an opacity that isn't above 0 and at most 1. `Aabb2` is read and written as
`{min, max}`.

On the walk (C4), which S3 follows here: `GridItem::Picture` passes a picture through with its rectangle in
millimetres, since the pixel grid places its pixels, and the SVG and PNG writers refuse it ("not supported yet:
pictures (A9)") until their own slices. `GridItem`, `GridGroup` and `OnGrid` are `PartialEq` only, as a
picture holds numbers that aren't `Eq`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures in a sheet PNG (#5624, W17b S5)

The sheet PNG writer draws the scene's pictures (S3's Item::Picture) where
the scene orders them, between its lines and fills, from a Pictures provider.

- The PNG is now the sheet's pixel grid (PixelGrid::sheet, S1): anchored at
  the sheet's top-left corner and rounded outward, so nothing is clipped.
  210 mm at 300 dpi is 2481 px (it was 2480, rounded to the nearest). A side
  over 65,536 px is refused and the refusal names the limit ("a sheet PNG
  side of ... px at ... dpi is over the limit of 65536 px"); an A0 sheet
  fits up to about 1400 dpi.
- A picture fills exactly its window of that grid, so it registers with the
  lines drawn over the same rectangle. Each band asks the provider for just
  its own rows, as straight-alpha sRGB RGBA, and the bytes are checked: too
  few or too many, or the provider's error, refuse the PNG with the picture's
  source named. paper::NoPictures refuses every picture.
- Pictures are asked for at most MAX_PICTURE_DPI (1200): above it, on the
  grid k = ceil(dpi / 1200) times coarser, whose pixels each fill exactly
  k x k of the sheet's (both grids share the top-left anchor), so a 2400 dpi
  export doesn't ask a renderer for four times the pixels.
- Source-over in sRGB, the picture's alpha times its opacity; transparent
  paper keeps the alpha. In black and white each picture pixel is dithered
  with an 8 x 8 ordered (Bayer) matrix at its place on the sheet, so bands
  don't show; lines stay solid. PngReport says whether anything was
  dithered.
- In the writer a picture is one more shape a band draws, beside A4's
  outlines, fills and hairlines; the walk's picture arm passes pictures
  through, and the SVG writer still refuses them (A9).

Tests: a picture's exact window and its register with a fill over the same
rectangle, also on a sheet 100.8 px high (the grid anchored at its top);
pictures keep their place in the paint order; opacity scales the alpha (on
white and transparent paper, and the picture's own half alpha); above 1200
dpi a coarser picture's pixels are each copied 2 x 2; black and white
dithers half of mid grey and keeps a line over it solid, whatever the band;
a provider's short rows, its error and no provider are refused, and a
picture off the sheet asks for nothing; the bands test draws three pictures
1, 3 or 7 rows at a time, the same bytes and each picture row asked for
once; the sheet sizes on the outward grid and the 65,536 px refusal; the PNG
fuzz with a picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper scene: pictures in a sheet SVG (#5624, W17b S5)

A picture is an <image> holding a PNG as a data URI, on the picture's own
pixel grid (Picture::grid) at the picture resolution the caller gives,
25 to 1200 dpi: paper_svg(scene, pictures, picture_dpi), the same shape as
paper_png. Only its window over the sheet is embedded and asked for, so it
never costs more pixels than the sheet holds; one off the sheet is left out.
Its x, y, width and height are the grid's own pixel edges put on the
nanometre grid every line is on, so a line on its edge has its number.
Opacity below 1 is written, in millionths as lengths are.

The PNG writer gives the embed its encoder settings (sRGB, the dpi
recorded) and its provider check, now one function each, and streams the
picture a band of rows at a time, refused once it passes the room left
under the file's 64 MiB.

Tests: the 1182 x 591 px grid at 300 dpi and its edges, a line on the
edge, paint order and opacity, the crop and the picture off the sheet, the
bands, and each refusal; the SVG fuzz case gains a picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: shaded snapshots and their pictures' parts (#5624, W17b S4)

drawing.view.snapshot takes `shaded` as a display or reference style and,
when a picture is drawn, says which: picture {pixels, dpi, rect_mm}, the
boundary's pixel grid at the sheet's dpi or 1200, the lower, since lines
may export finer. It renders nothing. A shaded part's own lines are left
out of the reply, as its picture draws it; it still hides what lies behind
it. shaded_visible and shaded_visible_hidden stay refused until the view's
exact lines (W06), and shaded with `scene` (and on a sheet) until the
paper scene draws pictures.

- engine::drawing_picture: the pictures a view draws, bottom first, and how
  each body takes part in each (painted, depth only, left out). The model
  always hides reference parts behind it; reference parts hide the model
  only when reference_hides_model is on; shaded reference parts that don't
  get a picture of their own under the model's, so they never cover it.
- product_scene::extract_where: the scene for the bodies a caller picks;
  extract keeps the browser's eye, a drawing draws every body.

Tests: every style and hide case, reference parts through a subassembly
and a part, the picture's size at 300, 1200 and 2400 dpi, a view with
nothing shaded, the hidden body in the filtered scene, and which lines a
shaded model or shaded reference parts leave.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:01:04 -05:00
CamWight a4c9bc380f Paper scene: text runs and their fonts, as records with their rules (#4890 B1a) (#9512)
`Item::Text` is a run of placed glyphs in one font, with its logical text and the clusters that
tie the two together, and `Scene.fonts` the table of the fonts the runs use (as posted on #4890,
with each glyph's `hmtx` advance). `Scene::validate` holds them to the posted rules. Writers never
parse a font.
- A run names its font by the font's id (a hash of its face's bytes and index), so groups laid
  out apart name a font alike whichever machine's font it is; `Scene::add_fonts` joins their
  tables at assembly, kept in order of id, so the same input gives the same bytes.
- A font's bytes never go through serde: a scene read from JSON is refused until they come from
  the font of the id it names.
- `TextRun::outlines` places the glyph outlines on the sheet, for the writers to fill nonzero.
The SVG writer refuses text by name until B2. `Scene::new` builds a scene without text, so the
next field doesn't touch every caller again.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:15:32 -05:00
CamWight f219d9b795 Paper text: a text laid out by sketch's rule, as the scene's runs and their drawing font (#4890 B1b) (#9513)
* Paper scene: text runs and their fonts, as records with their rules (#4890 B1a)

`Item::Text` is a run of placed glyphs in one font, with its logical text and the clusters that
tie the two together, and `Scene.fonts` the table of the fonts the runs use (as posted on #4890,
with each glyph's `hmtx` advance). `Scene::validate` holds them to the posted rules. Writers never
parse a font.
- A run names its font by the font's id (a hash of its face's bytes and index), so groups laid
  out apart name a font alike whichever machine's font it is; `Scene::add_fonts` joins their
  tables at assembly, kept in order of id, so the same input gives the same bytes.
- A font's bytes never go through serde: a scene read from JSON is refused until they come from
  the font of the id it names.
- `TextRun::outlines` places the glyph outlines on the sheet, for the writers to fill nonzero.
The SVG writer refuses text by name until B2. `Scene::new` builds a scene without text, so the
next field doesn't touch every caller again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper text: a text laid out by sketch's rule, as the scene's runs and their drawing font (#4890 B1b)

`paper_text` lays a text out with `TextMetrics`, the rule sketch text draws and measures with, and
returns the paper scene's runs of placed glyphs (B1a) with the font they are set in, holding the
glyphs they place: each one's `hmtx` advance and its outline in font units.
- Drawings are set in bundled fonts only (`DRAWING_FONTS`: Liberation Sans for now). A font is
  admitted once: a static face with a PostScript name, a cap height and a `post` table, its stem
  width measured across its `l`, and its id hashed then. A run names its font by that id.
- A character the font has no glyph for is refused by name; bold is refused; an italic leans as
  sketch text's made-up italic does; text on paper follows no path.
- Lines break a pitch apart and align as sketch text's do. A line with right-to-left characters
  is a run per direction, shaped, with clusters from the shaper; any other is one run, a
  character to a glyph.
`TextMetrics` keeps one layout: it now reports each glyph's cluster and the line's runs, and the
glyph outline walk gives loops of `Seg2`, which sketch text turns into the geometry it drew before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Paper text: the fonts a text uses, each once (#4890 F0)

`paper_text` returns its runs with the list of fonts they use, each once in order of id (so the
same text gives the same list) and each holding exactly the glyphs its runs place, where it
returned one font. Still one drawing font today; the list is the shape a text set in several
fonts (a Latin, Arabic or CJK line) needs, and `Scene::add_fonts` already takes it as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:15:28 -05:00
CamWight 21888e8000 Drawing views: reference parts in a view's lines and thin on paper (#5866, V5) (#10520)
* Reference parts in a drawing view's lines (V5a, #5866)

A body placed through an occurrence that counts as reference is a reference
part: one rule, Document::reference_occurrences. Its lines say so in replies.
With reference_hides_model off (the default) the model's lines skip reference
parts at the one occlusion test, and reference lines test against everything.
The two roles merge apart, so a reference line never takes a model line's
place on one stretch. Extents come from the model parts when there are any. A
sketch curve is the model's: a reference part hides it only as it hides the model.

One thing the plan didn't name: reference lines stop at the view's boundary
(extents and reference margin) when model parts are there too, which is what
the margin is for; a part much bigger than the model would otherwise run off
the sheet.

Hidden reference lines in a scene are refused until dashed lines (V7b), as the
model's are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reference parts on paper: thin, under the model's lines (V5b, #5866)

Saved views and the snapshot's scene alike, 0.25 mm until the drawing styles
slice sets widths by kind of line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Reference parts: tests the planted faults asked for (#5866 V5)

A reference line against a model line on one stretch, and against another reference part; one part
placed through a reference assembly and on its own; a dimension on a reference part's edge; undoing
and redoing a change of type, set on the occurrence or its component; and a saved view's setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A test reads any other paper item as no stroke (#5866 V5)

Naming Fill alone would stop compiling when #7590 adds a kind of item, with
no conflict for git to show.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:51:20 -05:00
CamWight 9b73ec8d9d Display units: every length unit of the expression table shows its own numbers (#4896) (#7809)
A drawing's fixed unit may be any length unit of the expression unit
table (expr::UNITS: um, inch, yd, mil and the rest besides a design's
five), but the display functions knew only the five and showed the
others' measurements as millimetres. unit_scale now reads the same
table, so the two can't drift apart again; the five keep exactly the
scales they had (1 over 10, 1000, 25.4 and 304.8). The inch rule (a
fourth digit when it means something) follows an inch's size, so
`inch` reads as `in` does.

A design's own units are only the five, so nothing it shows changes.
The test that pinned `inch` to millimetres becomes one that checks
every length unit of the table shows its own numbers; angle names
(`deg`, `rad`) and other text keep the millimetre fallback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 05:38:22 -04:00
CamWight 0d4739fffb Drawing dimensions: between two cylinders' silhouettes (#4896, W20b) (#9251)
* Drawing dimensions: a hole's or an arc's centre as a point to measure from (#4896, W20c)

A linear dimension's point can be the centre of a circle or an arc, saved as its one edge's name
with `centre`. It is found again as construction geometry finds a round edge's centre
(`construct::as_circle`), in 3D, so any view can measure it; a split edge is ambiguous, and an edge
that is no longer round can't be measured. `drawing.dimension.add` takes `{centre: edge}` wherever
a corner goes, and refuses a straight edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: a chain of distances along one line (#4896, W20c)

A chain dimension runs through 3 to 64 points in order (corners or hole centres) and reads the
distance from each to the next along one line as the view shows it: horizontal, vertical, along a
direction, or aligned from the first point to the last. The line runs the way the first point to
the last goes, and a point out of order is refused, never moved, so the members add up to the
overall from the first point to the last (`drawing_measure::chain`). It reads projected only (true
lengths of points off one line don't add up) and shows its members' numbers, never a text instead.
`drawing.dimension.add` takes `{chain: {points, direction?}}` and counts the points before looking
any up; `drawing.dimension.edit` changes its direction; inspect lists its members, its value being
the overall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: a whole cylinder's diameter between its silhouettes (#4896, W20b)

A cylinder dimension saves a face by name (ModelFace, as ModelEdge names an edge) and reads Ø
between the cylinder's two silhouettes, which any view but one along its axis shows 2r apart
(`drawing_measure::silhouette_side`). The face, or the pieces it was cut into, must lie on one
cylinder (the kernel's own condition for faces on a common cylinder); every face of the body on
it counts, since the kernel builds a whole cylinder as two half faces; and both silhouettes must
lie on those faces, so a fillet's quarter round is refused. `Document::face_reference` picks a
face by a point on it or by its index; `drawing.dimension.add` takes `{cylinder: {face}}`, edit
refuses its settings, and inspect can list face candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: between two cylinders' silhouettes (#4896, W20b)

A dimension between a silhouette of one cylinder and a silhouette of another, as the view shows
them. Each is named by a face of its cylinder and by its side: the one nearer the other cylinder
on the view, or the one farther (SilhouetteSide). So it keeps to them whichever way round the
view shows the cylinders.

- geom: drawing_measure::between_silhouettes takes the two cylinders (Cylinder, now geom's: the
  type doc kept privately), their sides and a direction as a linear dimension takes it (aligned:
  square to the silhouettes). The silhouettes must be parallel on the view and the axes apart
  there. Foreshortened when the axes aren't parallel in the model, or the silhouettes lie at
  different depths.
- doc: DimensionKind::Silhouettes { a, b: Silhouette { face, side }, direction }, read as the
  view shows it. Each silhouette must lie on its cylinder's faces (Lookup::on_cylinder, now shared
  with the cylinder kind). Document::silhouettes_kind makes two silhouettes of one cylinder of
  one body that cylinder's diameter (which takes no direction), else the distance between them.
- engine: drawing.dimension.add takes {silhouettes: {a, b: {face, side}, direction?}};
  drawing.dimension.edit changes its direction only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 04:49:53 -04:00
CamWight 693cf911f1 Recovery: unsaved drawing work survives a crash (#3890, K9′) (#9289)
* Recovery: a change to a drawing alone is autosaved and recovered (#3890, K9')

A design's drawings are part of its document (K3b), so the autosave already sees a drawing's
change as unsaved work and writes it with the design, and recovery reads it back. This test
holds that: with the model saved, a change to a drawing's properties alone is autosaved, and the
recovered design has the drawing as changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Recovery: a drawing that breaks a rule is left out of the autosave and the crash copy, named (#3890)

Reading a design refuses it whole for one drawing that breaks a rule of the record, so an autosave
or a crash report's design holding one never opened again: all the unsaved work was lost. Both
copies now keep the rest of the design (drawing::openable) and name each drawing left out and the
rule: the autosave as its error, the crash report in its notes. A design's save error names the
drawing it couldn't save too (Drawing::label).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Recovery: a copy that must open keeps a design's drawing rules too (#3890)

drawing::openable now leaves out what a read refuses at the design's level as well: the later of
two drawings with one id, and the drawings past MAX_DRAWINGS, each named with its rule. So an
autosave never keeps a stale copy over a drawing problem. The id rule is take_id, which
check_in_design uses too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Recovery: a drawing too large to save is left out too, and the status line names it (#3890)

The autosave and the crash copy already leave out a drawing that breaks a rule of the record.
A drawing too large to save failed the whole autosave instead; `openable` now leaves it out the
same way, before its id, so the rest of the design is kept. Nothing is skipped silently: the
autosave's error names each drawing left out, and the status line shows it.

Tests: a drawing left out takes no id from a later one; an autosave leaves out one too large to
save; an autosave keeps what is hidden; the status line names a drawing the autosave left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:56:45 -04:00
CamWight b21c377296 A lost dimension keeps its last value after the design is reopened (#4896 W19d) (#9764)
Dimension::last_good saves the value the session last read, written only when
there is one, and a save takes it from the session's reads as it takes what is
hidden: no read touches the document, makes an undo step or asks to be saved.
drawing.dimension.inspect answers this session's read first, else the file's.
resolve_dimensions never sees it, so a lost dimension stays lost. A value past
the drawing format's limit is left out of the save, not written into a file that
then can't open.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:59:15 -04:00
CamWight 878bc1b421 A point on a curved edge finds it, between the edge's chords (#4896) (#9765)
An edge given as a point looked along the edge's mesh lines, within a point's tolerance, so a point on a
circle or an arc that fell between two chords found no edge: a radial dimension on most points of a rim
said "no edge at". A point off every chord now takes the edge whose chords are within what its body was
meshed to, and only when no edge passes through the point itself, so a point on a straight edge beside
a curve is still that edge.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 18:59:11 -04:00
CamWight 562bb379a5 Dimension layout: sizes for paper, and text above the line (#4896, W19c) (#9193)
* Dimension layout: sizes for paper, and text above the line (#4896, W19c)

A dimension's sizes become a parameter, so a drawing can lay its
dimensions out in paper millimetres with the same code as a sketch:
DimSizes { arrow, gap, overshoot, offset, above } and
dim_layout_sized. dim_layout and dim_layout_reaching keep their
signatures and use DimSizes::screen(px), today's 9, 3, 4 and 22 px;
the other default text places stay the same fractions of the offset
(18, 48 and 20 to 22), and default_text takes its 22 from the same
place. A size that isn't a finite number above 0 (or is beyond MAX_COORD,
the largest length a sketch takes) falls back to its 1 px value, as a
bad px does today.

`above` puts a linear dimension's text above an unbroken dimension
line (ISO 129-1), running along the line and reading from the bottom
or the right; DimLayout::text_dir gives its direction (level for text
in the line). The arrows go inside or out by the same test, with the
text's length along the line; a leader to text outside runs on under
it.

On main, #7056 moved the four default places into
default_text_offset(frame, px). This leaves those lines as they are,
so when main next comes into drawings the layout's arms call it with
this px (the offset's 22nd), and lib.rs's export line takes both lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format the dimension layout's re-exports (#4896, W19c)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 13:59:49 -04:00
bherbruck 1a47ac13de Merge pull request #9467 from tangency-labs/sync/main-into-drawings-1030
Sync main into drawings (Oct 30)
2026-10-30 10:29:35 -04:00
bherbruck 1c957c105d Translate the drawings' new messages into the complete catalogs (ar, he, vi)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:15:28 -04:00
CamWight ec527a32ab geom: arrowheads and leaders on paper (#4890 D1a) (#9466)
`paper::annotate` places the marks that point at things on a sheet as plain
strokes and fills in sheet millimetres: open, closed and filled arrowheads,
the oblique stroke and the dot, and leaders (points, a landing line in the
reading direction, a terminator at the first point). Every size is the
caller's; ISO 128-2's dot and landing ratios and ISO 129-1's 15° to 90°
arrowhead angle are constants beside them. A closed or filled arrowhead stops
its line at the base. Dimension layout stays W19c's (#4896) and the datum
triangle the datum symbol's (#7257).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:59:02 -04:00
bherbruck d01de87552 Merge main into drawings (Oct 30)
Brings the 1,270 commits on main since the drawings branch last met it,
including the first drawings landing (#8249), into drawings.

Merged with the drawings snapshot that #8249 landed (ebafe0748) as the
merge base, so only the drawings work after that landing is replayed on
top of main. Main's structure wins; drawings' additions are kept and
adapted to main's APIs (metadata Owner/KEYS table, checked! without a
contract argument, Snapshot visibility, crash journal outcome, the
part-properties normalisation, model_body_components, io helpers).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 09:54:08 -04:00
CamWight fefca6bfa0 Drawing export: a sheet of the open drawing as SVG, from its scene (#4890 A7) (#9137)
* Drawing sheets on paper: the drawing border, grid and marks (#4890 A6, #3875)

SheetLayout::paper gives what a sheet's drawing border draws as paper
groups of kind drawing_border, in order, each left out when empty:

- the drawing border, one closed stroke drawing_border_line_mm wide
  (0.7 mm in the built-in profile, ISO 5457);
- the grid: the grid reference border, closed, and a mark at each edge
  between two fields from the drawing border out to it, on all four
  sides, GRID_LINE_MM (0.35 mm, ISO 5457's grid line width as the
  drawings study's section 11.3 gives it) wide; none without fields or
  with the grid reference border on the drawing border;
- the centring marks, as the layout places them, in the drawing
  border's width, as the profile says;
- the trimming marks, each rectangle filled.

The grid's labels are text and come once the paper scene has text
(#4813). Only the function, its constant and its tests are added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets on paper: one sheet's scene, its border then its views by id (#4890 W10, #3890 W15)

`drawing_scene::build` draws one sheet of a drawing as one paper scene, the
one the sheet page and every export will draw, and `drawing.scene {sheet}`
answers it (in_drawing, noundo, doc_only, typed contract and output schema).
In paint order: the sheet's drawing border groups as `SheetLayout::paper`
gives them (object: the sheet), then each view on the sheet by id (object:
the view), its visible lines as `paper_group` makes them.

- A view's lines are worked out centred on the view and then moved to its
  centre, exactly (each point plus the centre), so the same lines on the same
  centre are the same bytes. They match the snapshot's within rounding.
- The shown set is worked out once per sheet. No model is evaluated.
- A view with nothing to measure is a placeholder, and one that can't be
  drawn yet (shaded, or hidden lines) fails with the snapshot's reason. Each
  draws nothing, and the sheet's other views are still drawn.
- A view is placed only while the sheet stays within the paper scene's
  100,000 records. One that would pass the limit fails and says so, and later
  views that fit are still drawn. Each view's lines are placed as they are
  drawn, so at most one view's lines are held beside the scene.
- A view above 100:1 takes a coarser chord rather than asking the mesh for
  chords below its floor. A saved view has no chord setting to give.

One rule, one place:
- the snapshot's two style refusals are `drawing_view::not_drawable`;
- a group's record count is `paper::Group::records`, which `Scene::validate`
  now counts with;
- the stats reply is `From<Stats>`;
- both paper scene schemas come from `paper_scene_schema`.

Tests: the border then the views by id, byte for byte against the layout,
with hand-worked outlines and lengths; the command's reply and its schema; the
SVG's group order; each view against the snapshot, and one of many lines (a
block with eight holes, from above); an empty sheet; a failed and a
placeholder view; no evaluation; refusals; the record limit at exactly 100,000
and one past it; a 200:1 view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets on paper: the provider the sheet page draws through (#4890 W10, #3890 W15)

The sheet page asks for a sheet every frame, so it draws through a provider
that keeps each view's lines between frames, instead of through `build`.

- `drawing_scene::extract(session, sheet)` returns the last scene at once,
  with each view as it was last drawn and the ones out of date. Nothing is
  drawn there. When the drawing, the world state and the views drawn are all
  as they were, it returns the same answer without looking at the sheet.
- `drawing_scene::step(session)` draws the first out-of-date view of the
  sheet last extracted and says whether more wait: a short piece of work for
  the UI's tick, one view's projection.
- `generation` comes from one counter for every session. It goes up only
  when a sheet's scene is assembled again, so a page never sees one number for
  two scenes, even across closing and opening its drawing.

The cache is the session's view state, beside the record: never saved, never
an undo step. `Session::execute` drops it when a command closes the drawing or
opens another. One entry per view, keyed by the world state it was drawn from
(held in the entry and compared with `Arc::ptr_eq`) and the view as it resolves.
A drawing-only edit keeps the world state, so a move, and its undo, project
nothing: the same centred lines are moved to the new centre. The shown set is
worked out once per world state. Views no longer in the drawing are forgotten
when a scene is assembled again.

Tests:
- a new cache gives the border with both views out of date;
- each step draws one view, in id order, until the scene is `build`'s, byte for
  byte;
- the generation stays put while nothing changes;
- a move and its undo project nothing, and the views move by exactly the move;
- after a model change (a checkpoint restored) the last scene shows, and a view
  listed as up to date always draws as `build` does;
- closing drops the cache, and the scene after reopening has a new generation;
- seeded mixes of moves, setting edits, undos, redos, extracts and steps
  always end at `build`'s scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing export: a sheet of the open drawing as SVG, from its scene (#4890 A7)

drawing.export {sheet, format?, path?} writes one sheet of the open
drawing from drawing_scene::build's scene, the scene the sheet page
draws, so a file shows what the page does: its drawing border, then
each view's visible lines, through the SVG writer. With a path (a .svg
file, checked by vfs::check before and again just before writing, as
the view DXF is) the file is written atomically; without one the reply
holds the SVG. The reply gives its size, what the writer snapped or
merged, and each view's status as drawing.scene reports it.

A sheet with a view that failed isn't written: the refusal names each
such view with why. A placeholder view (nothing to measure) draws
nothing and is exported all the same. PNG and DXF are "not supported
yet": they come with the sheet PNG writer (#4890 A4) and the shared DXF
writer (#5319). No undo step; no model is evaluated.

One rule, one place: the scene's sheet look-up is
drawing_scene::sheet_id, and a view's reply entry is
From<ViewState> for SheetSceneView, both used by drawing.scene and
drawing.export. The scene tests' drawing fixture is open to the crate's
tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:20:10 -04:00
CamWight 6a2bdbf144 Bill of materials: virtual components and a typed quantity (#3874, P6) (#9226)
`bom_quantity` is a part property of any component: what one placement of it means, an expression
evaluated with the design's parameters (pieces without a unit, else a length). A row's `quantity`
is what its placements add up to, with `quantity_unit`; pieces beside a length, or a quantity that
no longer evaluates, give none and say why. A quantity is a parameter user: a rename follows it, and
a parameter it uses can't be deleted. The CSV gains a `quantity` column at the end.

`virtual` marks a component with no geometry (grease, paint, a label): listed with its quantity,
weighing nothing. It can't be the active component, and one check after each undoable outermost
command refuses any change that puts a body, a sketch or a placed component into one, whichever
command made it. A file a damaged virtual component already fills opens, and other changes go on.

Mass, volume and area quantities wait for the expression units of #4811.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 01:15:51 -04:00
CamWight 413c145c56 Bill of materials: stable item numbers, saved with the design (#3874) (#7779)
* Bill of materials types on one occurrence (#3874)

An occurrence can count in the bill of materials as another type than its
component, for that occurrence only: `bom_type` on the occurrence, set with
occurrence.bom_type (one undo step; it changes no geometry, so nothing is
evaluated). An occurrence without it counts as its component, so older
designs need no step; the format change is one pending file
(occurrence-bom-type.rs).

assembly.bom counts each placement by its own type: a purchased placement of
a kit part is one row without the kit's contents, a phantom placement lists
what it holds where it sits, and one component placed as two types gives a
row for each. Paste New keeps the types; Paste Instance counts as the
component. The compact list and the locales (en, de, ja, ko, zh) carry it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials rows: material, and mass when asked (#3874)

Every row of assembly.bom (and every part folded into a row) names the
physical materials of its definitions' own bodies, each once, with "; "
between several. An unassigned body's is Default. A subassembly or an
empty component has none of its own.

With mass: true each row also carries mass_g, what one of its parts
weighs as the row stands for it, so the rows of any list add up to the
assembly once:
- at the top level, all it holds;
- in parts only, its own bodies, because what it holds is listed beside
  it; but a folded part's row is the kit (all it holds), a purchased part
  is all it holds, and an inseparable one all but its purchased parts;
- a reference placement adds nothing, nor does anything inside it.
It is null when nothing in the part has a body, a body can't be
measured or measures a negative volume (inside out, such as an open mesh
facing the origin), or the definitions sharing the row's part number
weigh differently.

Measuring runs the kernel, so it is asked for: the Bill of Materials
window re-runs assembly.bom after every change and doesn't ask (0.2 ms
without mass, 135 ms with it on a bolted bracket). Placements are rigid,
so each definition is weighed once however often it is placed, and each
body is measured once with measure_mass (the same volume as measure,
without the topology counts' tessellation).

A body's material and its density now follow one rule:
Document::body_material (new, beside Document::density) is the material
its mass is worked out with, named as the library or the design spells
it: the assigned one when it is known and has a usable density, else
DEFAULT_MATERIAL (new, beside DEFAULT_DENSITY). Document::density is
that material's density, with the same values as before. So a damaged
file's unknown or broken material reads as Default, as it weighs, and a
hand-edited "steel" reads as Steel. The bill of materials uses both; the
other places that read a body's material or spell "Default"
(component.properties, inspect and others) move to them in a separate
cleanup PR for #4783, which brings the same two functions to main.

Document::body_owners returns each body's owner in order, so the rows
find their bodies in one pass. It no longer repeats the owner rule:
model_body_component, legacy_body_component and body_owners go through
the rule written once, given how to find a feature's component (the
timeline for one body; body_owners still builds its map once). The CSV
columns are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Physical properties: a reference placement adds nothing (#3874)

component.properties and occurrence.properties now leave out a placement
that counts as a reference in the bill of materials (its occurrence's
bom_type, else its component's: a fixture shown for context), and
everything inside it: no body entry, mass, volume, area, centre of mass
or bounding box, and it isn't in placement_count. That is the bill of
materials' rule, so a reference part is in neither the parts list nor
the assembly's mass, and every bill of materials list still adds up to
the assembly's Properties. The placement asked for counts whatever its
type: a reference fixture's own Properties still measure it and what it
holds, as a component's own bill of materials lists what it holds.

Only the totals change. The placements walk that face picking, sketches
on placed faces and the occurrence path lookup use stays complete, so a
reference fixture's faces can still be picked.

The type an occurrence counts as is now written once:
Document::occurrence_bom_types (new, beside BomType) gives each
occurrence's type in their order, looking each component up once. The
bill of materials, occurrence.bom_type's reply and the physical
properties use it, in place of the two copies in the bill of materials.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: one module of typed rows in doc (#3874)

The bill of materials now lives in solidcraft_doc::bom as the typed rows
of a bill of materials view. assembly.bom's JSON and CSV and the Bill of
Materials window present those rows; parts lists, item numbers and
balloons are to read them too.

- list(tree, request) walks the placements below the assembly once. One
  rule (classify) decides for each placement whether it is counted in a
  row, how what it holds is walked and what its row stands for when
  weighed, from the type it counts as, the view and the component's
  shape. "An inseparable part's purchased parts are listed beside it" is
  written there once, and the mass walk reads it.
- A row carries its identity (RowKey: the rows it is listed under and
  its lowest member, a component and the type it counts as), its item,
  count, each, the placements it counts by occurrence path, what it
  stands for (Holds), its part properties, materials, mass and children.
- Mass is a Mass: not asked, grams, or unknown with the reason (nothing
  to weigh, unmeasurable, the definitions differ). Each definition is
  weighed once for each kind of row and kept in the Tree. The assembly's
  total is weighed from the design the same way, not added up from rows,
  so it is known even when a row's definitions weigh differently.
- Bom::row_of(tree, path) finds the row a placement is counted in (a nut
  inside a purchased bolt is in the bolt's), or says why there is none:
  no such placement, a reference, or no row of its own.
- rows_deep() lists each row followed by its children (1, 1.1, 1.2, 2);
  Request::default() is assembly.bom {}; View and Group carry the
  commands' words.

What changes, beside that the output is otherwise the same:
- One placement budget (100,000) and one depth limit (64 levels) cover
  the whole list. A folded part's contents used to be walked under a
  budget of their own, so a design past the limit was listed with
  group_children; it is refused now, as it already was without it.
- Kits are compared by what they hold (part, type and count), not as
  text: a part without a part number whose id is 12 no longer reads as a
  part numbered "#12", nor a part numbered "P×1\nQ" as a P and a Q. Kits
  holding the same parts through different definitions (two bolt
  definitions, each with its own nut definition of one part number) are
  one row, and its children count every bolt's placements, where they
  showed the first bolt's definitions only.
- A folded part's row always stands for all it holds. It used to stand
  for its own bodies when nothing inside it was listed, which differs
  only for a phantom with bodies of its own inside it.
- An inseparable part's row in parts only names the material of what it
  stands for (its steel blocks read Steel) where it was blank, and a
  subassembly whose parts share one material names it at the top level.
- A damaged file's component placed inside itself is refused at the first
  level too, since naming a row's material walks inside it, as weighing
  it already did.
- A row's body count saturates, so a compact assembly with more placed
  bodies than a count holds (a block placed 2^64 times) is weighed
  instead of overflowing.

Materials follow Cameron's rule for a row that stands for an assembly
(C3, 2026-10-23): a part's row lists its own bodies' materials, as before;
a row that weighs what is placed inside it (a subassembly at the top level,
a purchased or inseparable part with parts inside it, a folded part) names
a material only when everything it stands for shares it, else none. A frame
of steel and aluminium parts reads blank, and a bought bearing of steel
parts reads steel. One walk (adding) decides what is placed inside a row's
part for both its mass and its materials, and each definition's materials
are read once.

Components are looked up by id once per list; they were searched for
each row and each kit. The window reads the rows directly instead of
running assembly.bom after every change, so it no longer adds an entry
to the crash journal each time it refreshes. The engine keeps the
command, its parameters and the files.

Tests check rows. The module's own: an empty assembly, one part, a row
named by its lowest member through part-number edits, one part number
of two types, a phantom in a phantom, a reference at the root and its
own list, the 64-level limit, the budget hit exactly with and without
folding, kits told apart by what they hold and the types they hold it as, a folded part's contents not
folded again, row_of for every type, every placement in one row or
none, a damaged file's missing component and cycle, the mass agreement
rule (moved from the engine with row_mass) and what adds to a row. In
the engine: the material rule for every kind of row, the JSON against the
rows for every option, why a mass is
unknown, the total against component.properties in every view, what a
row stands for and its placements, and the overflow. The assembly.bom
tests from #1888 on pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: structured, all levels (#3874)

P4: `assembly.bom {levels: "all"}` lists the assembly's direct contents, each subassembly a row with
its own rows listed under it, to any depth (items 1, 1.1, 1.1.1), each child counted `each` per one
of its parent and `count` in all.

- `doc::bom::View::AllLevels` goes through the one rule (`classify`) the other views use: a
  normal subassembly is a row holding what it holds, listed under it as all levels again; a
  phantom has no row and what it holds joins the level it sits in, merged with the same parts
  there; a purchased part is a row with nothing under it unless `show_purchased_contents`
  (Request::purchased_contents) asks for its contents; an inseparable part is a row with nothing
  under it; a reference counts nowhere.
- The rows under a row are the folded-part rows already built (kits), so subassemblies with one
  part number and different contents are different rows, and no second nested builder is written.
  All levels gathers every level as it gathers the first; a folded part's contents are still
  gathered by part number in the other views.
- The JSON and CSV already nest to any depth; `show_purchased_contents` is echoed with the other
  options. Each row's mass still adds up to the assembly at the top.

Tests, worked by hand: four bolts and a two-bolt subassembly placed twice (8 bolts in parts only;
in all levels the subassembly twice with 2 bolts each, 4 in all, beside the top's 4); three levels
deep (2 x 3 x 2: counts 2, 6, 12, items 1, 1.1, 1.1.1, the CSV in that order); each type (a
phantom's bolt merged into its subassembly's, purchased with and without its contents,
inseparable, reference); two subassemblies of one part number with different contents as two
rows; `levels: "deep"`, a flag that isn't one, and an assembly over 100,000 placements refused.
D's test that a folded part's contents are listed as parts only lists them now names the two views
it holds for, and checks all levels' nested rows (the subassembly inside the folded part a row with
its own rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: parts with one part number are one row only when they hold the same to any depth (#3874)

The kit compare looked one level down: two parts with one part number whose contents differed two
levels below merged into one row, and the children that only one of them held came out at each 0.

Each placement now has a kit, a number interned from what it holds (each child's part, type and
own kit), built in one pass from the last entry back: an entry's children come after it in the
walk, so their kits are known first. Rows are told apart by that number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: a phantom's own bodies are a row of their own (#3874)

A phantom had no row, and its own bodies were weighed in the assembly's total but in no row, so
the rows didn't add up. Its own bodies, when it has any, are now a row of the phantom's type that
stands for them alone, as a subassembly's own bodies are in parts only; what it holds still counts
where it is placed. Inside an inseparable part in parts only, nothing changes: the part's row
weighs them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: the assembly's total mass in assembly.bom (#3874)

`list` weighed the assembly as a whole (from the design, not added up from rows) and the command
dropped it. `assembly.bom` now gives it as `total_mass_g`, and so does a JSON file it writes, only
when `mass: true` (null when it isn't known).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: all levels as JSON lists its rows one after another (#3874)

All levels nested each level's rows in the row above it: two JSON containers a level, so the
64 levels a list may hold came to 129, past the 128 that serde_json (and so the MCP server and
the control channel) reads. In all levels `assembly.bom` now gives the rows one after another,
each followed by the rows under it (items 1, 1.1, 1.1.1), as its CSV does; a row listed under
another still reads its item as text and gives `each`. A folded part's contents, one level, stay
inside its row as `children`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: a row whose definitions weigh something and nothing differs (#3874)

A row gathering a definition with bodies and one with none (one part number on a steel block and
on an empty assembly) said there was nothing to weigh. Their masses differ, so it says so; only a
row where none of them has a body has nothing to weigh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: definitions agree on a mass within the measurement accuracy (#3874)

A row's definitions had to weigh the same to 1e-9, but the kernel measures a volume from
tessellations, and one part modelled another way is tessellated differently: a pin made as a
cylinder and as a revolved rectangle measured 1.05e-5 apart, so it read as two masses. The bill of
materials now has its own `bom::MEASURE_ACCURACY` (1e-4: ten times that gap, with its doc on what
it follows: the kernel's `measure` gives one solid's volume to about that, relative, however the
solid was modelled), and a row's definitions agree within it. A pin 0.1% wider (0.2% heavier) still
differs. The kernel is unchanged: its work is paused while it is being replaced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials guide: all levels, purchased contents and this round's changes (#3874)

The guide had no `levels: "all"` or `show_purchased_contents`. It now has both, with how all
levels tells subassemblies apart and lists its rows as JSON, a phantom's own bodies as a row, the
assembly's `total_mass_g`, and definitions that agree on a mass within the measurement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: materials are bodies', as the code says (#3874)

The rule stays as built (a row that stands for an assembly names a material only when everything
in it shares one), but its comment and the guide said a material set on a component is set on
all its bodies. `material.assign` takes bodies only: a component has no material of its own yet.
The module notes now say so, and where component materials will join the rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: a body's volume measured once with the evaluated model (#3874)

`assembly.bom` measured each definition's bodies anew for every list, and `component.properties`
measured each placed body again. A body's measure depends on nothing but the body, so the evaluated
model keeps its volume (`ModelBody::volume`, shared by clones, as its outline box is), and both
commands weigh that one measurement: the same grams to the bit. A body offset scales it by the size
of its determinant (`mat_volume_scale`) instead of copying the B-rep to measure it. The physical
properties still measure the placed body for its area, centre, box and counts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: row keys, what rows stand for, members and row_of stay in doc (#3874)

Nothing outside `doc` reads them yet: `RowKey`, `Unlisted`, `Holds`, a row's `key`, `stands_for`
and `members`, and `Bom::row_of` are crate-private until item numbers and balloons need them, so
they can still change without breaking another crate. The engine test that read them is covered
by doc's own (`row_of_finds_the_row_a_placement_is_counted_in`, `every_placement_is_in_one_row_or_none`).
`Unknown` stays public: `Mass::Unknown` carries it, and a public enum can't carry a private type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Assemblies: one fallible placement walk for totals that cover the whole assembly (#3874)

The physical properties walked the placements twice in their own code, with the 64 levels and
100,000 placements written as literals, and the bill of materials gave the same refusal in words
of its own. `Document::placements_below` is that walk once, from any placement, refusing what is
past `MAX_PLACEMENT_DEPTH` or `MAX_PLACEMENTS` (never walking it in part), and both use its
refusal: "the assembly is deeper than 64 levels or has more than 100000 placements". The physical
properties now allow 100,000 placements below the start, as the bill of materials does (they
stopped at 99,999).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: the same rows for less work (#3874)

- Part numbers are borrowed from their definitions (`Part<'a>::Number(&'a str)`) instead of copied
  for every placement grouped and every kit.
- A row's members, the placements' paths, are built only when the list is asked for them
  (`list_with`, for item numbers, balloons and `row_of`): the walk then keeps paths and moves each
  into its one row instead of copying it. A list made for its rows alone builds no paths at all.
- `row_of` reads an index of member paths, built once with the list, instead of scanning every
  row's members for each placement asked about.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* occurrence.bom_type: input and output schemas (#3874, #4035)

`OccurrenceBomTypeParams` takes the occurrence (an id or a name), its path when given and the
type (one of the five, `same_as_component` or null), each read by the handler's own reader: the
path through `cmd::path_ids`, split out of `occurrence_path`, the type through
`cmd::occurrence_bom_type`, split out of the handler. The handler builds its reply from
`OccurrenceBomTypeResult` ({occurrence, bom_type, effective, changed}).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials tests: all levels presented at every depth, and its caps (#3874)

- `assembly_bom_presents_the_typed_rows` now covers all levels (with mass, purchased contents and
  by definition) and the total mass, on a three-level design with a purchased gearbox holding a
  gear, comparing every field of every row down to 1.1.1.1.
- All levels lists 100,000 placements and 64 levels, and refuses 100,001 and 65.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: static materials, typed in for a component (#3874)

A value the bill of materials works out can now be typed in by hand for a component (a static
value, Cameron's choice of word). Material comes first:
`component.metadata.set {component, material}` takes any text up to MAX_TEXT_BYTES, and
`component.metadata` reads it back as `material`. It is shown wherever the component is listed,
at every level and in every view, over the materials worked out from its own bodies or an
assembly's blank, and the row carries `static: ["material"]`. Clearing it (empty text or null)
brings the worked-out material back.

It is kept the way `bom_type` is: a part property in the component's metadata (`material`), so
there is no format change and older releases keep it. `PART_ONLY` names both keys in one place:
Design Properties doesn't list them and `document.metadata.set` refuses them. Another tool's
non-text value is kept and reads as no static material.

The bill of materials says one thing about the part everywhere: a static material also stands for
the part in the one material an assembly around it shares (two parts typed in as S355 make their
frame read S355; S355 beside plain steel reads blank). A phantom's row is its own bodies, so its
static material names those only, and what it holds keeps its own. A row gathering several
definitions takes the first one's that has a static material, as its description does. It only
names: mass still comes from the bodies.

The CSV gets a `material` column at the end, so the other columns keep their places: what the row
shows, the static material or the worked-out ones joined with "; ".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Bill of materials: stable item numbers, saved with the design (#3874, P5a)

A numbered parts list keeps each row's item number under the row's key in
`assembly.item_numbers`, so a row keeps its number when others are added,
removed or reordered. New rows take the next number after the highest at
their level; Renumber restarts a view in list or item order and leaves
locked rows alone; duplicates are marked; numbers change no geometry and
are part properties in the undo history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:29:25 -04:00
CamWight b708170028 Drawing W07: a view's lines, on paper, for the occurrences it shows (#5866) (#8970)
* View DXF: its lines come from one module both view commands share (#5866, W07 V1)

The view DXF (file.export_view_dxf) finds its lines in four files private to the command: the
export meshes, the projection, the hidden-line scene and the progress stages. Drawing views
(W07's drawing.view) need the same lines, so the four move to engine/src/mesh_lines, with
three changes that let a caller other than the DXF use them:
- prepare takes the bodies to draw from its caller (the DXF passes the shown ones), and
  names the caller's command in its errors;
- the lines are seen from a View (an origin, a right/up/back basis and, for perspective, the
  eye's distance) instead of a viewport Camera; the DXF builds one from its camera, with the
  same arithmetic as before;
- a sketch's polyline (Part) belongs to the module, so the sketch code no longer defines what
  the scene draws.

The DXF writing stays in cmd/view_dxf.rs, with connect (the dashed-line joining), and the stage
names and cancellation text stay the DXF's. Nothing else changes: the module is the DXF's own
code, moved, and the DXF writes the same bytes as before for every case checked (see the PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* View DXF: the sketch runs go once the lines are made, and a box's outline pinned (#5866, review)

From the review of #6482:
- The sketch runs (up to 4,000,000 points) are dropped once the lines are made, as they were
  before the move, instead of living through the DXF writing.
- `mesh_lines::prepare`'s doc says which errors name the command: those about the placements.
  A body with no exportable surface, the memory budget and cancellation come back as the meshing
  stage's own.
- A new test pins the lines themselves by hand: a plain 40 x 30 x 10 box seen from the front is
  its 40 x 10 outline. Every line lies along a side, they add up to the 100 mm perimeter, and
  nothing is hidden.
- The shared-geometry test hides the definition's first placement: the other two are still
  drawn from the one shared mesh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: the bodies a view shows, worked out once per request (#5866, W07 V3)

A view's extents and, next, its lines read one list: each body the design places, with the
occurrences that place it and its definition's body name, in the world state's order. Choosing
occurrences and hiding parts (V4) and the reference split (V5) become filters on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a view's lines from the mesh projection, each named by what drew it (#5866, W07 V3)

drawing.view.snapshot now returns its lines: every shown body's edges and silhouettes from the
view DXF's mesh projection, seen orthographically on the view's basis, in pieces of one visibility
on the sheet. Each line names its occurrence path, its body and its source (an edge, as model.edges
numbers it, with whether it is a tangent edge, or a silhouette across a face). They are marked
approximate and sampled (Cameron's choice, 2026-10-27): the exact lines (W06) take over per body
after the kernel swap (V8).

- display_style visible or visible_hidden; tangent_edges hide or show; chord_mm, the paper chord
  the meshes are asked for (0.001 to 1, 0.01 by default). The model chord is chord_mm over the
  scale: the view DXF's 1 mm cap doesn't apply, so a view below 1:100 keeps its chord on paper;
  its 0.0001 mm floor does, and a chord under it is refused with the chord_mm that would do.
- max_error_mm is a check, never a setting (the accuracy rule on #5866): refused while any piece
  is sampled, with the count of pieces short of it.
- The three view DXF changes moved here from V1: each segment keeps its owner (its placement and
  its edge or silhouette face); a stretch two sources share belongs to the nearest visible one,
  else the nearest hidden one (level ones in owner order), ends where that changes, and counts as
  covered; the triangles keep their faces for that. The view DXF asks for neither, so it merges and
  writes as before. (Nearest, not first: a hole seen end on has its bottom rim under its top one,
  and by owner order the bottom rim took the stretches it shows through, cutting the top rim up.)
- A mesh side between two faces every edge between which is drawn is no silhouette: it lies on one
  of those edges, already drawn from its own curve. (Seen end on, a hole's wall faces neither way,
  and the side it shares with the top face drew stray chords just inside the rim.) The triangles
  keep their faces for it in both commands (4 bytes each, in the memory budget); without them the
  view DXF drew a hole's rim twice, and a view DXF test now finds every line drawn once.
- A triangle seen edge on, its normal at most 1e-3 (cosine) toward the view, faces away whatever
  sign the mesh's own error gives it, so no silhouette lies between two of them. (Seen end on, a
  bend's surface drew its rows as chords beside its arcs: a 60° bend's outer arc 1.76 times over.
  A view DXF test now finds each of its arcs drawn once.)
- Mesh positions within the tolerance weld: a position takes the key of the first one within it,
  in its own rounding cell or a neighbouring one. (The faces round a hole's rim are meshed apart,
  their copies of a rim vertex up to about 1e-7 mm apart, and copies either side of a cell
  boundary never met: each copy's side was a boundary, drawn as a silhouette.) The view DXF takes
  these three rules too; they change its lines only where a side lies along a drawn edge, a
  triangle is seen edge on or a weld was missed. The sample designs' view DXFs lose lines, and
  each line they gain lies within 0.0011 mm of an old one.
- One source's stretches join into runs within twice the tolerance the lines were merged at: the
  merge drops spans up to that long, so a stretch can stop that short of a corner on either side.
- The tangent test is a stadium of cubic segments: the kernel marks a join smooth only between
  faces swept from cubic segments, and one between two pieces of one surface is a seam, never
  drawn; a fillet's round keeps its edges, as the viewport draws them.

compile fix by the personal PC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a circle facing the view is drawn as its exact arc (#5866, W07 V3)

An edge that is a circle, or part of one, in a plane parallel to the view
is drawn as its exact arc from Body::planar_edges instead of the mesh's
chords. Where the mesh projection says its visibility changes, the arc is
cut there: a piece that runs along all of the edge is analytic, a cut
piece keeps the mesh's sampled accuracy; its ends may stop within the
stretches' join tolerance of the edge's own, as the merge leaves them.
planar_edges is asked once per shared mesh and placement turn, for the
curved edges only; a part placed again the same way takes the arcs
shifted on the view's plane.

compile fix by the personal PC

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a view's visible lines as a paper scene group (#5866, W07 V7a)

The paper scene gains a kind for views (ObjectKind::View, layer VIEW).
drawing_view::paper_group turns a view's lines into its group: each
visible piece one stroke, 0.5 mm wide until D2 sets widths by kind of
line, closed when it ends where it starts; a line with no length is left
out and an arc too short to sweep is its chord. W14 calls it for saved
views.

drawing.view.snapshot takes scene: true and replies with the lines as a
paper scene of its sheet, checked by Scene::validate before it is
returned. Hidden lines need dashed strokes (A5, #7430), so scene with
visible_hidden is refused until V7b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a view draws the occurrences its source names and hide leaves out (#5866, W07 V4)

drawing.view.snapshot takes source: {occurrences: [[ids]]}, the occurrence
subtrees it draws ([] the design's own bodies; every body when not given),
and hide: [[ids]], subtrees left out. A body is drawn when its path is under
a source path and under no hide path, so a hidden path wins over a source
path above it. The rule is one filter on the shown set (ShownSet::keep), so
the extents, the lines and the paper scene follow it; each body costs one
look-up per head of its path, at most 64.

Each path is checked by Session::occurrence_placement as it is (each id in
its parent, no id twice, 1 to 64 ids), with at most 1000 paths in each list
and 4096 ids in all. [] in hide and a choice that leaves nothing are
refused. M2's occurrence tree will make the check one step per id later,
inside occurrence_placement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a bend seen end on draws its arcs once at any angle (#5866, W07 V4b)

A triangle whose surface is seen edge on at each of its corners (the mesh's own normals, at most
1e-3 toward the view) now faces away, whatever lean its facet has. A thin triangle's facet leans by
the mesh's chord error over its own width, which can be far past the 1e-3 a facet may lean and
still count as edge on. Seen end on, a 90° bend's surface is meshed with its edges' samples at its
ends and its rows at even angles inside, 1.667 mm in; the triangles that join the two lean 1.1e-3
to 5.8e-3, so each one facing the view among its neighbours facing away drew silhouettes along its
sides: chords beside the arcs, 1.14 times the inner arc and 1.69 times the outer. The view DXF
takes the rule too.

The export meshes keep their normals for it: 24 bytes a vertex, a position's size again, counted
in the export's 512 MB mesh budget, so the largest view that budget takes is about 30%
smaller. A mesh without normals keeps its facets' facing.

Tests: the bend test takes a 90° bend beside the 60° one; each bend circle's lines add up to its
arc, less only what sampling takes off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:25:05 -04:00
CamWight 8679063629 Drawing revisions: rows added, edited and deleted by command, and listed by drawing.inspect (#3874) (#9150)
* cell: rules both kinds of table share (#3874)

Sheet tables (parts lists first) use the title blocks' cell content, so
the rules both read live in cell.rs:

- Source::Row: the row of a sheet table a cell is on. A title block has
  no rows, so its validation refuses a cell that reads one.
- Warning::Width: a word wider than its value box, shown whole, never
  cut: the width warning that measured text brings.
- is_snake_case moves here from title_block.rs: table columns are named
  by the same rule as cells. check_text is shared the same way.

Tests: a title block cell reading a row is refused, beside other parts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 640cc3e0f5)

* Sheet tables: the parts-list table model and layout (#3874, P7a)

A Table is a sheet table's definition: its columns, the side its heading
is on (top, or at the bottom with the first row just above it, ISO
7573), its row height (7 mm by default), text height, line widths, a
filter and overrides. A column's cells are the title blocks' cell
content read against each row: row fields and text. Rows are input, in
their listed order, with their values worked out and formatted, so a
filter (parts, assemblies, purchased, text per column ignoring case)
hides rows and never changes a count or a quantity.

Table::layout places it with a text measurer, from the right end of the
heading's outer edge:
- a column without a set width is as wide as its widest line, padding and
  line insets added; a set width is kept;
- every cell in a set-width column breaks between words to fit, and its
  row grows to hold its lines, 10/7 cap heights apart (ISO 3098-0); a
  word wider than its column is shown whole with Warning::Width;
- a row field the row doesn't have shows "-"; an override shows its text
  and keeps the value it replaces, an empty one is a blank cell, and one
  for a row that has gone is kept;
- cells sit inside their lines as a title block's do.

Limits, refused and never truncated: 10,000 rows and overrides, 64
columns, 1 MiB of text in all (each cell's text counted before it is
built), 256-byte keys, a measured width that isn't finite and at least
0, a table off the paper's coordinates.

Nothing is saved or drawn yet: the real measurer, DXF and CSV output and
the command come with P7b.

Tests: hand-worked cell corners and text anchors with a fixed-width
measurer, the heading at the top and at the bottom; wrapping and the
row it grows; a word too wide; overrides, blanks, "-"; each filter
leaving every shown value as given; JSON round trip, defaults and
unknown fields; every limit and one past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 0c5180267d)

* Revision record and table (#3874, W31, P14a)

A drawing's revisions as data (geom::revision): one RevisionRecord for
the whole drawing, or per sheet with each sheet numbered apart.

- A row holds what people type: revision, description, designer,
  checker and date. Adding a row fills in only the next revision;
  nothing fills in a date, a checker or an approval.
- Numbers run from 1, or 0; letters run A to Y without I, O, Q, S, X
  and Z, then AA, AB. The next revision follows the highest in its scope
  among the rows and the issued history, so an issued revision is never
  given out again. A typed revision is kept as typed; in letters one
  with a skipped letter is accepted with a SkippedLetter warning.
- Rows are added, hidden and deleted; issue() appends a copy of a row to
  the issued history, which nothing removes or changes (private, read
  through issued()). Row and tag ids come from the caller (the
  design's random ids), never from a counter, so two people adding rows
  never take the same id; an id in use is refused.
- Triangular tags bind to a row id and show its revision or their own
  text; deleting a row reports its tags, which keep the revision they
  showed, marked lost.
- The revision table is the sheet table (table()): Revision,
  Description, Designer, Checker, Date, heading at the top; hidden rows
  are left out. The table's rows and a title block's revision fields
  (newest(), hidden rows included) read RevisionRow::field, one path.

This is the drawing's revision only: the model's revision and the
export time are kept apart. Release, approval and frozen snapshots
(G16/G22) are not in this slice. Nothing is placed or saved on a sheet
yet (W12, W15).

Tests: numbering both ways with the skips and past Y and YY, typed
revisions and the warning, the next after issued and deleted rows, per
sheet; add fills in nothing else; history kept through hide and
delete; tags reported and lost; the title block's fields and the table
rows from one function, laid out by the sheet table; JSON round trip,
unknown fields and damaged records refused; the caps; an id in use refused.

- An issued copy keeps the model version it was issued against
  (model_version, given to issue(); none until version control gives
  versions, #2869), so the release step has its field here. It is read
  by the label rule, like the revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit fa7849ada7)

* Drawing revisions: rows added, edited, deleted and listed by command (#3874, W31)

A drawing saves one revision record for all its sheets (Drawing::revisions, behind an Arc as the
sheets are, so an undo step shares it; validated with the drawing; its ids among the drawing's ids
in use), in the order its rows were added, the newest last. Pending drawing format: revisions.

- drawing.revision.add {set?}: a row with the next revision, after the highest the drawing has
  given, and any texts given: one undo step.
- drawing.revision.edit {row, set: {revision?, description?, designer?, checker?, date?,
  hidden?}}: the texts as typed (a revision is one line) and what changed; a revision in letters
  with a letter the lettering skips is kept and warned about.
- drawing.revision.delete {row}: the row, and the tags it leaves showing the revision they had.
- drawing.revision.list: every row, the oldest first, and the revision a new row gets.

An id's text form (13 characters of lowercase Crockford base32) moves down to solidcraft_geom::ids,
with an option helper, so the revision record (geom) writes its row and tag ids as every drawing id
is written; solidcraft_doc::ids gives them on, beside IdSource.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing revisions: listed by drawing.inspect, a table row keyed by its id's text, and a record per sheet tested (#3874, W31)

The rows show in drawing.inspect beside the sheets, as `revisions: {rows, next}`, from the same
function the revision commands answer with; drawing.revision.list goes, so a drawing's rows have one
place to be read. A revision table's row is keyed by its id as a file writes it, the one spelling
every drawing id has (a table's cell overrides name a row by its key). A record kept per sheet whose
rows keep its own rules is refused by the drawing, so that rule has a test of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 00:22:51 -04:00
CamWight f87b66d759 Drawing balloons: the record, its rules and its commands (#3874) (#9126)
* Drawing balloons: the record, its rules and the sheet hooks (#3874)

A balloon points at a part placed in its view's model by an edge of it, saved
by name as a dimension's edge is (ModelEdge), with the occurrence path as the
part it numbers, and sits at a place on paper from its view's centre. The
dimension module's reference checks and strict lookup are shared, not copied.
Balloons go with their sheet, list in on_sheet and take ids from the drawing.
One pending drawing format file; a drawing without balloons saves as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing balloons: add, edit and inspect, with their schemas (#3874)

drawing.balloon.add puts a balloon on a saved view, pointing at a placed part
by an edge picked as a dimension's is and saved by name; edit moves it on
paper; inspect says where it stands with the model now, with the arrow's
point on its edge's middle. Add and edit are one undo step each, and none of
them evaluates the model. The status words come from one function shared with
the dimension commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing balloons: a copied view takes its balloons, and views and sheets list them (#3874)

A view's balloons are part of what it shows, so copying a view copies them,
with ids of their own, as its dimensions are copied. drawing.view.inspect
lists a view's balloons, and deleting a sheet names the balloons on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 23:49:00 -04:00
CamWight a09c24e9bf Drawing dimensions: a circle's or an arc's radius and diameter (#4896, W20a) (#8959)
* Drawings saved inside the design's file, in both forms (#3890)

K3b of the drawing storage plan, after #7098: a drawing lives in the file of the design it draws
(bherbruck's proposal, Cameron's choice), so the separate drawing file goes.

- `Document.drawings`: the design's drawings, each its own record of the drawing family
  (`solidcraft-drawing/N`), each behind its own `Arc`. Every reader and writer of a design now
  carries them: save, Save As, autosave and recovery, crash copies, the CLI.
- A design container keeps each drawing in its own entry, `drawings/<id>.json` (Deflate), beside
  `document.json`, which holds nothing of them: its bytes, and the model cache keyed to them, are
  the same with drawings or without. A plain JSON design keeps them inline under `drawings`.
- Each drawing is read by its own family's rules, taken out of the design's JSON before the design's
  own checks. One a build can't read (a newer number, a change it doesn't know, damage) refuses the
  whole design, never dropped. So does an entry under `drawings/` that isn't a drawing's, one whose
  record has another id, drawings inline in a container's `document.json`, two drawings with one
  id, or more than 1,000 (counted before any is read). A design is never saved with two drawings of
  one id.
- Drawings are listed by name, then id: no stored order, nothing to merge when two people add
  drawings.
- A drawing has an `id`, unique among its design's drawings (`Drawing::new` takes the ids already
  taken). Its source is the design it is in: at most one, an id and no path.
- One pending design change, `drawings`: the last release refuses a design saved with it, so it
  never opens one and saves it without its drawings. One pending drawing change, `in-design`, for
  the record's new shape.
- Gone: `read_drawing`, `read_drawing_file`, `write_drawing_container`, `file_kind`, `FileKind`,
  `DRAWING_EXT` and `is_drawing_path`. `read_document`, `read_design` and the container writer are
  as they were before #7098, with the drawings added.

Tests: a design's drawings round-trip through both forms byte for byte, with `document.json` and
`container.json` the same with drawings or without; their order; every refusal above; a drawing's
properties kept as other tools wrote them while a sheet's number out of range is refused; the
drawing's id among its objects', a new drawing's id none of the design's; a design with two
drawings of one id not saved; mutation fuzz of a design with drawings in both forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing commands on the design's own drawings, each drawing its own undo (#3890)

K4' of the drawing storage plan, on K3b (a drawing lives in its design's file): make, list, open,
close and delete a design's drawings, and read and change the open one's settings and properties.
It replaces #3891's drawing sessions, which held a drawing as a file of its own.

- `drawing.create {name?, language?, units?}` adds a drawing of the design (one ISO A3 landscape
  sheet with the ISO border, the design its one source) and opens it; refused on an empty design.
  `drawing.list`, `drawing.open {drawing}`, `drawing.close`, `drawing.delete {drawing}`.
  `drawing.inspect`, `drawing.set`, `drawing.properties`, `.properties.set`, `.properties.remove`
  work on the open drawing. Labels in en, ja and zh.
- The open drawing is the session's (`Session::open_drawing`, kept in checkpoints), and the part
  gate keeps the parts apart: a command says the part it works on, the model unless it says
  otherwise (`.in_drawing()`, `.any_part()`). While a drawing is open the model's commands wait
  ("a drawing is open: close it to change the model"); the drawing's run only while one is.
  Files, recovery, undo and redo, checkpoints, selection, diagnostics, the command list and
  choosing a drawing work either way. `CommandSpec::allowed` is the one check running a command,
  running it inside another and listing it make.
- `Session::drawing` and `Session::edit_drawing` are the open drawing's one way in: an edit is kept
  when it changed something and still keeps the record's rules, in a new design whose other
  drawings are shared.
- A change that only touches drawings evaluates nothing: `Document::differs_only_in_drawings`
  joins the render-only path, as render settings and part properties do. A model edit shares
  every drawing, so it pays nothing for the check.
- Undo per part: a step records the part it changed (`Snapshot::part`: a drawing, or the model
  and its list of drawings). Undo and redo go through the open part's steps only. A drawing's
  step puts back only that drawing; the model's step puts back the design as it was, keeping each
  drawing it had as it is now. Making and deleting a drawing are the model's steps. A new step
  ends what could be redone in its own part only. `Snapshot::of` is the one place a step is
  taken.
- An agent's message that changed several parts keeps their steps apart, rather than merging them
  into one step that would undo only one part.
- `cmd::metadata`'s rules for reading, setting and removing properties are written once
  (`reading`, `setting`, `removing`) and used by the design's and the drawing's commands.
- `drawing::sort` is the one place the drawings' order (by name, then id) is kept;
  `Document::put_drawing` adds or replaces one in that order.

Tests: making, listing, opening, closing and deleting; what making one refuses, at the cap too; the
gate both ways, inside another command and in the command list; drawing edits as undo steps that
evaluate nothing over 100 rounds; undo and redo per part with model and drawing steps interleaved;
redo ended per part; the order by name, then id, after renames; delete and its undo; refused
changes; checkpoints; save and open in both forms; properties; hostile values for every command
that runs with a drawing open, from the registry; a fuzz of the steps in any order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Unknown-command tests: the closest id first (#3890)

drawing.open now joins doc.open in the suggestion for `file.open`, as the rule says: both end in
the same word. The engine's and the CLI's tests check that doc.open comes first and let other
close ids follow, as the `solid.extrud` line beside them already does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Model bodies: their extents seen on a plane, for a drawing view's size (#5866)

ModelBody::mesh_bounds_in(plane) is the box of a body's mesh in a plane's own coordinates
(Plane::to_local), beside mesh_bounds and in the same way: a body meshed already is measured from
its own mesh, and a placement of a part from its definition's shared mesh, with the placement
folded into the plane's two axes once (two dot products a point), so no moved copy is made. W07's drawing.view measures a view's extents with it.

Tests: a 100 x 50 x 20 block seen on the three principal pairs of axes and from an isometric
corner (150/sqrt(2) by 190/sqrt(6), worked by hand), a plane away from the origin, and every
placement in the shared-mesh test against its moved mesh, still without a moved copy, and the
same again from its own mesh once it is meshed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: projected views in first or third angle, lined up with their parent (#4816, W17a)

W17a's second slice: the principal views derived from a base view.

- `ProjectedSide` (left, right, above, below): the side of its parent a
  projected view sits on.
- `projected_basis(parent, side, projection)`: in third angle a view shows
  the side of the model it sits on (one to the right looks from the right), in
  first angle the side away from it (ISO 5456-2). It's the parent's basis
  turned a quarter turn about one of its paper axes, through its origin, with
  no -0. From the front view it gives exactly the named views: right, left, top
  and bottom in third angle; left, right, bottom and top in first.
- `ViewFrame::aligned(side, projection, centre, position_mm)`: a projected
  view at its parent's scale, lined up with it, so it moves only along the free
  axis.
- The projection is S2's `cell::Projection`, the value the title block's
  symbol reads, so a drawing has one first-or-third setting.

Tests: the front view's projections are the named views in both angles, with
no -0; from every named view and a leaning one, a projected view looks from
its side and keeps the parent's other axis and origin, four turns or a turn
and its opposite bring the parent back; a box's corners line up across
aligned views in both angles; hostile parents, centres and positions refused.

Corner (isometric) projected views wait for one convention check on first
angle. The derivation graph (projected from, aligned to, cycles refused) moves
to the view record in the drawing file, where the view ids live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: measurement on view frames (#4896, W19a)

`geom::drawing_measure` is what a dimension on a view reads, in model mm or radians, from model
points and edges and the view's basis: one rule for every dimension. The view's scale and place
on the sheet never enter.

- `linear`: horizontal and vertical along the view's paper x and y, aligned as the view shows
  the segment, or along a direction picked on the view; a true reading is the segment's own
  length (aligned only).
- `angle`: from the view's horizontal or vertical, a model axis as the view shows it, or
  another edge, from 0 to π as each line is oriented; a true reading is between model lines.
- `foreshortened` says a measured line isn't parallel to the view's plane.
- Refused with a reason: points that aren't finite or are past 1e12 mm, an invalid basis, a
  zero-length segment, a line seen end-on, a zero direction, a zero reading, parallel lines,
  and a true reading where there is none. `drawing_view`'s `PARALLEL` and `require` are shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* drawing.view.snapshot: a view's scale, place and boundary on its sheet (#5866, W07 V2)

The first slice of W07: one orthographic view of the design, worked out and returned as data,
before any line is drawn, so scripts can lay views out by their boundaries.

- `drawing.view.snapshot` takes the view's settings, the sheet it goes on (A3 landscape at
  300 dpi when not given) and where its centre goes (the sheet's centre when not given). With no
  scale it takes the largest ISO 5455 scale at which the view, centred there, lies inside the
  sheet's drawing border. It returns the resolved view, the scale (and whether it was fitted),
  the extents on the view's plane, the boundary on the sheet and what it was drawn from.
  Read-only and not undoable; it opens nothing and saves nothing.
- Every body the design places is in the view, wherever its occurrences place it; the browser's
  eye counts for nothing (`engine::drawing_view::extents`, from P2's `mesh_bounds_in`). A design
  whose assembly walk stops at its limits (deeper than 64 levels, or past 100,000 placements) is
  refused rather than drawn with parts left out: `Document::placements_complete`, beside the walk,
  says whether every placement it holds had its occurrences walked.
- `doc::drawing_view::ViewSettings` is the view's one parameter set: the view (named, or a
  direction and an up), the scale, the two visual styles, the reference settings and tangent
  edges. A saved view (W12) keeps the same fields; no scale is written as none, not null, so
  what it writes reads back as the snapshot's parameters.
- `render::VisualStyle` names the five styles: visible, visible_hidden, shaded, shaded_visible
  and shaded_visible_hidden. The shaded ones are refused as not supported yet until the drawing
  renderer (#5624).
- The command has a typed contract (input and output schemas). Its handler reads the contract's
  own type through `schema::read`, in the contract check's own words, so the check's levels give
  one message for one mistake and an unknown field is refused by name. The reply includes the
  sheet, its defaults filled in.

Tests: the 100 x 50 x 20 block's boundary in the front, top, right and isometric views at 1:2,
worked by hand; the fitted scale on A3 and A4 and near the border; scale, position and margin;
the browser's eye ignored; the design untouched; each hostile setting refused; the walk's
depth limit one level either side; the contract's keys the same as the settings' own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views in the drawing record: base and projected views, and the projection (#4816)

E4 of the view-boundary plan, with #4816's derivation graph (plan on #4816).
A drawing now saves its views, as one pending drawing-format change
(`drawing/format/pending/sheet-views.rs`):

- `Drawing.views`: each view an object of the drawing, on one of its sheets.
  A base view draws a source from a direction (`ViewDirection`, as
  `drawing.view.snapshot` takes it); a projected view sits on one side of its
  parent. Its place is a point of the sheet, or, lined up with its parent, only
  how far along the free axis, as the other comes from the parent. A projected
  view's source and basis come from its chain, so neither is saved.
- `ViewOverrides` beside V2's settings: what a view sets itself, each setting
  on its own (scale, display style, reference style, reference parts hide the
  model, tangent edges); none is written when left out. Each view has its own
  reference margin.
- `Drawing.projection`: first or third angle (S2's `cell::Projection`). Third
  angle is the type's own default (Cameron, 2026-10-26), for a drawing that
  doesn't say and for a new one until a template or a preference chooses.
- The rules, checked on reading and after every edit: a view is on one of the
  drawing's sheets; a base view draws one of its sources, has a scale and a
  direction that makes a view; every parent is a view, no chain of parents
  loops, and a chain is at most 32 views; a lined-up view is on its parent's
  sheet; a place is on paper; at most 4,096 views. A view's id is the
  drawing's, never another object's.
- `Drawing::resolve_view`: one rule for every reader. It turns the base view's
  basis down the chain (`projected_basis`, in the drawing's projection) and
  takes each setting from the nearest view that sets it, then the default,
  saying who gave it.
- geom's `is_paper_coordinate`: the one rule for a view's place on paper, now
  used by `ViewFrame::new` and the record.

The record tests' seed drawing has views, so its round trip and mutation tests
cover them; their unknown-field examples no longer use `views`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Engine tests: one temporary folder helper and the schema check for every test module (#4816)

The view tests use the drawing tests' temporary folder and the schema tests' `conforms`, which were
private to their modules. `Folder` moves to the engine's test module and `conforms` is open to the
crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: inspect and edit a saved view (#4816, W17c)

E5 of the view-boundary plan (plan on #4816): what the view's window reads and
writes, for the views of a drawing (E4). Both are drawing commands with
input and output schemas, and neither evaluates a model.

- `drawing.view.inspect {view}`: its kind (base or projected), sheet, source,
  parent and side, its direction as resolved (named only when it is exactly a
  named view), its place, and each setting as {value, from, parent?, set}:
  where the value comes from (the view, a parent by id, the default) and what
  the view saves itself. A projected view says whether its display settings
  follow its parent (`linked`: true, false or "partly"). `choices` gives the
  display styles and the preset scales, so a window keeps no list of its own.
- `drawing.view.edit {view, set}`: one undo step. Each setting takes a value,
  or null to follow the parent (a base view keeps a scale); the margin, a base
  view's direction and the place change too. `linked: false` saves the display
  settings at the values they have, `linked: true` clears them so they follow.
  The reply names the fields changed and the views projected from it whose
  settings changed with it. The record's rules hold after every edit; an edit
  that changes nothing is no step.

The contract test keeps the edit's fields and the record's (ViewOverrides) one
list. The view's boundary comes with the next slice, which works out where each
saved view sits on its sheet from its design's extents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: where a saved view sits on its sheet (#4816)

`Drawing::view_centre` gives a view's centre on its sheet: its point, or, lined up with its
parent, its own place along the free axis and the parent's centre across it. The free axis is
one rule on `ProjectedSide` (`along`, `on_free_axis`), which `ViewFrame::aligned` uses too.

`engine::drawing_view::on_sheet` puts a saved view on its sheet: its design's extents on its
plane, grown by its reference margin, at its scale, round its centre. The snapshot and saved
views share `measure` (refusing an assembly past the walk's limit or one with no bodies) and
`boundary`. With nothing to measure (the design has no bodies or places more than a view walks) a
view gets a 20 x 20 mm placeholder round its centre. A view's source is the design the drawing
is in, so the session's own model is what it measures.

`drawing.view.inspect` adds `boundary_mm` and `extents` (status `placeholder` and why, when there
is nothing to measure). It still evaluates no model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Projection of new drawings: a preference, and drawing.set takes the projection (#4816)

A drawing's views follow its projection, first or third angle (third when nothing chooses).
`drawing.inspect` now shows it and `drawing.set {projection}` changes it as one undo step.
Preferences > Design gets a Drawings section with "Projection of new drawings"
(First angle / Third angle). A new drawing takes it as a new design takes "Units of new
designs": the app sets it after `drawing.create`, with no undo step of its own. A saved
value this version doesn't know reads as third angle; asked for over the control channel,
it is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: one edit, one lookup, a border that lays out, and schemas (#3875)

The sheet commands change the drawing through `Session::edit_drawing`, so the same settings or
order again leave no undo step, and answer from one reply type. A sheet is looked up by id or
name on `Drawing` (`sheet_index`), where later commands that name sheets find it too.

A sheet's border must lay out on it: the record's check runs the border's own layout, the one
rule drawing uses, so a 15 x 15 mm sheet with the ISO border is refused when it is made, not
when it is drawn. `excluded` must be an object of `count` and `print`: a mistyped key changed
nothing unseen. A new sheet documents nothing and leaves nothing out unless told, and the add's
parameter documentation says it takes `documents` and `excluded` too.

The drawing's properties sit behind an `Arc`, as its sheets do, so an undo step shares them.

Each sheet command has an input and an output schema (#4035). A flattened part of a contract
reports its keys as the parent's own (`schema::flattened`, which `FeatureCommon` now uses).
The tests add the missing cases: 1000 sheets and the 1001st, `after` by id and unknown, the
resolution's bounds, a name two sheets share, the same order and settings again, an undone
delete in place, and the add's reset of what it documents and leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing ids: one id source per session, its seed kept for replays (#3890)

The drawing commands drew ids from a fresh random source each time, so a crash replay of a drawing
could never find what an earlier command made by its id. A session now has one id source
(`Session::ids`), kept across a file change, from which `drawing.create` draws its drawing's,
sheet's and source's ids: seeded alike and run alike, a session gives the same ids, and a failed
command draws none, so a replay of what succeeded draws the same. The crash journal keeps the seed
at each base and `replay.json` carries it, which `solidcraft-cli crashes replay` seeds the replay
with (`Session::seed_ids`).

Tests: two sessions seeded alike make a drawing with the same ids, another seed other ids.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: add, change, reorder and delete a drawing's sheets (#3875)

W18 S4: the sheets of a drawing, on the drawing session K4 made (#3890). Each command is one undo
step of the drawing and names a sheet by its id or, when only one has it, its name.

- `drawing.sheet.add {after?, name?, size?, orientation?, dpi?}`: a new sheet after the one named
  (the last when not), taking that sheet's paper, border and resolution, with a name of its own
  ("Sheet 2") and nothing documented yet.
- `drawing.sheet.set {sheet, name?, size?, orientation?, dpi?, documents?, excluded?}`: one
  setting at a time, the rest kept; `documents: null` clears what it documents, and `excluded`
  changes only the flags it gives.
- `drawing.sheet.reorder {sheets}`: every sheet's id once, in the new order.
- `drawing.sheet.delete {sheet}`: a drawing keeps at least one sheet.
- Every change goes through the drawing's own rules (`Drawing::validate`), so what a saved file
  would refuse is refused here. Changing a sheet's paper changes only the paper: regrowing its
  border and moving what is on it is S5's. Labels in en, ja and zh.

Tests: a new sheet copying the paper it follows, after a named sheet too, and undone; each setting
changed with the others kept; nine refusals (resolution, sizes, orientation, name, an unknown
source, a flag, an unknown sheet, no sheet named) with the drawing unchanged; reordering, and four
orders that aren't every id once; a name two sheets share refused in favour of the id; the last
sheet kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a new sheet's id from the session's id source (#3875)

`drawing.sheet.add` draws its id from `Session::ids` (#3890's one id source per session), so a
crash replay of "add a sheet, then change it by id" finds the sheet: seeded alike and run alike, a
session adds sheets with the same ids, and a failed add draws none.

Tests: two sessions seeded alike add the same sheet ids, another seed others, a failed add none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: move or copy a view to a sheet (#4816)

`drawing.view.move_to_sheet {view, sheet, position_mm?, break_alignment?}` puts a saved view on
another sheet as one undo step, keeping its id, direction, source and settings. The views lined
up with it go with it and keep their places beside it; a view placed apart stays. Without a
position it keeps its place. A view lined up with its parent moves only with `break_alignment`,
and is then placed apart: at the position, else the sheet's centre. Its own sheet changes
nothing.

`drawing.view.copy_to_sheet {view, sheet, position_mm?, with_linked?}` makes new views, each
following none of the views it was copied from (Cameron's rule). A view with views lined up
with it needs `with_linked`: true copies them too, lined up with the copy; false copies it
alone. A projected view copied alone becomes a base view, looking the way it did (by name when
it is exactly a named view) with the settings it had as its own. The copy goes to the position,
else the sheet's centre.

A lined-up view's centre comes from the places alone: its place along the free axis, and its
parent's centre across it. So the views that travel with a view are shifted along their free
axis, and nothing needs the design's extents. `descendants` takes which views it follows, so the
edit's followers and the views lined up with a view are one walk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: move and copy take a view's centre from the record (#4816)

Moving or copying a view to a sheet worked out a view's centre with its own copy of the
lined-up rule. It now uses `Drawing::view_centre`, and a lined-up view's place moves along
its free axis by `ProjectedSide::along`, so the rule is written once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a change of paper lists the views it leaves outside, and moves none (#3875)

W18 S5, its first part. `Sheet::drawing_space` is where a sheet's drawing goes: inside its
drawing border, or the whole paper with none. Nothing on a sheet is saved against the border,
so a change of paper regrows the border and its zones by itself.

`engine::drawing_view::outside` lists the views whose boundary (`on_sheet`) leaves their sheet's
drawing space, sides included. `drawing.sheet.set` lists them after a change of paper (size or
orientation; nothing else moves the space, so nothing else is measured), and moving or copying a
view to a sheet lists those it puts outside there. None is moved: where a view goes stays the
author's choice. A view with nothing to measure is listed by its placeholder, with why.

Tests: the drawing space with the ISO border, a filing margin and no border, and margins that
leave none; A3 to A4 either way up listing the right and top views with their boundaries and
moving nothing; a border through a view's edge leaving it inside, 0.1 mm less paper putting it
outside; a change of name checking nothing; undo; a move to a smaller sheet listed and a copy
that fits not; a placeholder listed as `past_walk_limit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing ids and the sheet border: the tests the slices left out (#3890, #4816, #3875)

What the slices' planted faults found untested, now each caught by a test:
- a file change keeps the session's id source: a drawing made after `file.new` draws the same ids
  in two sessions seeded alike;
- a copied view's ids come from the session's id source, seeded alike giving the same copies;
- a crash report's replay carries the session's id seed, from a caught panic (the seed when the
  command started) and from the journal of an unclean exit (the seed at its base);
- a view short of the sheet's border by less than a rounding is still inside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: listed by id, whatever order they were added in (#4816)

The record's rule says a drawing saves its later records by id, never in
the order they were added in. Views were kept, and saved, in the order they
were added, so the same views added in another order saved as other bytes.

- `Drawing::add_views` adds views where their ids put them. It is the one way
  views are added: `drawing.view.copy` uses it, and so do the test fixtures.
- `Drawing::validate` checks the order, so every edit keeps it.
- Reading puts a file's views in id order before it checks the record. The
  order means nothing, so a file in another order (saved before views went by
  id, or edited by hand) isn't damaged: it opens, and saves by id.
- What a command lists in the drawing's order (the views a change of paper
  leaves outside, an edit's followers) now comes by id.

No format change: no release has saved a view (the views change is still
pending).

Tests:
- views added out of id order are listed and saved by id;
- the same views added the other way round make the same drawing and the same
  bytes, which read back as saved;
- a file that lists them the other way round reads by id, as the same
  drawing, and saves as those bytes;
- an edit that leaves them in another order breaks the record's rule;
- the existing tests now index the file by id order, and the engine's lists
  are by id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: the dimension record, saved by name on a view (#4896, W19b)

A drawing saves its dimensions: each on one of its views, measuring named
corners or edges of the view's model (a linear distance between two corners,
a straight edge's length, or the angle between two edges), with the direction
and reading W19a measures with, how far its line sits from what it measures,
where its text sits on paper, its decimals and any text shown instead of the
number. A reference names its body, the feature that made the body and its
occurrence path, never a picked point. Nothing worked out is saved.

- Dimensions are listed by id, read in that order whatever order a file has,
  as views are: one helper pair (add_by_id, in_id_order) now keeps both lists.
- One cap for the names a saved model reference keeps
  (MAX_REFERENCE_NAME_BYTES, used where 512 was written three times), and the
  corner's edge cap shared with construct's vertex references.
- LinearDirection and Reading save as snake_case words; a direction along a
  line saves as its two coordinates.
- A direction along a line is at most 1e9 each way (format::MAX_REAL, the
  limit the file's number check holds every real number to): a whole number
  past it passes that check, reads as a real, and would save as one the next
  read refuses (found by the mutation test).
- One pending drawing-format file (dimensions.rs).

Tests: what each dimension saves and reads back as the same bytes, a file in
another order reads by id, an older drawing has none; every rule on both sides
of its cap, unknown fields at every level, the id rule, and the count cap; the
plate fixture carries one dimension of each kind, so the existing round-trip
and mutation tests cover them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Edges and corners by name: the matching step and the shared ends, each in one place (#4896, W19b)

A dimension finds an edge by name the way a fillet does, but never takes
the piece nearest an old pick, and finds a corner where its named edges
meet, refusing to choose between two. Both steps are split out of the code
that already does them, so there is one rule each:

- BodyEdges: a body's edge and face names, worked out once for every name
  looked up on it, and `matching(name)`: the edge with that name, else the
  one edge between the faces it names, else the pieces of it. locate_edge
  now calls it and then takes the piece nearest the pick, as before.
- construct::shared_ends: the ends every named edge has, each once.
  named_vertex keeps the one nearest its point, as before.

No behaviour change: every caller of locate_edge, find_edge and the vertex
references gets the same results. The existing naming, joint and
construction tests cover both paths; three new ones pin what they left
open: a vertex only where every named edge ends, the piece of a split
edge nearest the pick, and a lone match through the stem of a face taken
as a piece, with the warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A body's frame through an occurrence path, in one place (#4896, W19b)

Document::body_frame_at(body, path): the placement the path leads to,
then the body's offset, without expanding every assembly instance. It is
what EmbossFace::body_frame composed for its saved path; that now calls
it, and drawing dimensions use it for the occurrence path they save. No
behaviour change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions found again in the model, strictly (#4896, W19b)

Document::resolve_dimensions(model, drawing, ids) works out each dimension
as the model has it now:

- An edge by its name, or the one edge between the faces it names
  (BodyEdges::matching), never the piece nearest an old pick. A corner is
  the one end all its named edges, or their pieces, share; two shared ends
  are ambiguous, never "the nearer one".
- The body by its name and the feature that made it, placed through the
  saved occurrence path (Document::body_frame_at). A same-named body made
  by another feature isn't it.
- Status: ok; overridden (the text given is shown, the value still worked
  out); ambiguous (the candidates, at most 16, none chosen); suppressed (its
  feature is suppressed or rolled back, read from the evaluation's own
  skipped mark); lost (why); unmeasurable (found, but the view can't
  measure it so, in W19a's words, such as a line seen end-on).
- The value comes from W19a on the view's basis. An angle is the smaller one
  between the two lines, or pi minus it on its other side, whichever way the
  kernel runs the edges; a split edge's pieces on one line are still that
  line. A split edge's length is ambiguous.
- The text: the override, else length_number in the drawing's unit (or the
  design's) with the dimension's decimals, or degrees for an angle.
- The cost: each body named is looked up once per call, and its edge names
  worked out once, however many dimensions name it. A body none names isn't
  touched.

Not saved: nothing here changes the drawing. The last good value of a lost
dimension needs the session, so it comes with the commands (W19d).

Tests (engine, an 80 x 40 x 30 box, values by hand): front and right view
values, foreshortening and text, the drawing's own unit, several at once
and an unknown id; a change and its undo; a chamfer's 45 and 135 degrees,
and end-on in the front view; lost after a fillet and back on undo,
suppressed and back, a path to no occurrence, a new box with the same body
name; a notched edge's length ambiguous with both pieces while its end
corners still read 80 and its angle to the left edge 90 degrees, neither
exact; an angle on a piece of an edge whose slot is then suppressed still
reads 90 degrees, found through the stem of the face it was cut from, so
not exact; one edge named twice gives two corner candidates;
an override shown while the value follows the model.

Not covered by a test here: a part placed twice through real occurrences
(only a path to no occurrence is); the once-per-body cost (not measured).
A drawing unit outside display_units' five (um, yd, km, mil, nm, inch, µm)
shows millimetre numbers, the formatter's existing fallback; named as its
own follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing sheets: a sheet with anything on it goes only with it, when asked (#3875)

drawing.sheet.delete refuses a sheet with anything on it, and its
error names what is there: its views and the dimensions in them, the
first few ids of each. With `contents: "delete"` the sheet goes with
everything on it as one undo step, which an undo brings back whole.

What is on a sheet is said once, in the record (Drawing::on_sheet),
and Drawing::remove_sheet takes the sheet out with exactly that; each
later kind of record on a sheet (a sheet's own sketches) adds itself
there, so delete covers it with no change here. drawing.sheet.set's
list of a sheet's views uses the same function.

Nothing on another sheet goes with it: a view there projected from one
of the sheet's views would lose its parent, so the delete is refused
naming those views, to be moved or deleted first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a view found by id by halves, in one place (#4816)

The views are listed by id, so Drawing::view and Drawing::view_index
find one by halves instead of walking the list. Every lookup by id
uses them: a view's chain of parents (so resolving a view no longer
walks the list once for each parent), the views' rules (which no
longer build a map of every view to check each chain), a dimension's
view, and the engine's view commands and outside check.

The rules check the id order before anything looks a view up, so a
file with its views out of order is still refused as before.

Measured on a drawing of 4096 views (1024 chains of a base view and
three views projected one from another), median of 30 runs: 4096
lookups by id took 6.85 ms by a linear find and take 105 us by
halves; the rules' check took 1.77 ms and takes 1.45 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a damaged chain names the view whose parent is missing (#4816)

A view whose chain of parents breaks further up was refused with "its
parent X isn't one of the drawing's views", naming a view that isn't
its parent. Further up the chain, the refusal now names the view whose
parent is missing: "its chain of parents reaches view Y, whose parent
X isn't one of the drawing's views". A missing parent of the view
itself reads as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: add one to a view, and read it back (#4896, W19d)

drawing.dimension.add puts a dimension on a saved view of the open
drawing; drawing.dimension.inspect reads one back with what it
measures now. Adding is one undo step of the drawing; neither
evaluates the model.

What a dimension measures is picked and saved by name, never by the
point picked. Document::edge_reference and corner_reference turn a
pick into the record's names: an edge is a point on it in the design's
frame or {body, index} (its index among the body's edges as the model
lists them, as an ambiguous dimension's candidates are given), and a
corner is a point or the edges that meet there, each with the
occurrence path its body is placed through. A pick that doesn't name
one thing is refused, saying what it found: no edge, a point on
several edges, a corner of more than one body, edges that don't meet.
Each body's edges are placed in the design's frame once per pick, and
only the body picked has its edge names worked out.

The kind is given as the record saves it ({linear: {a, b, direction?,
reading?}}, {length: …}, {angle: …}), read by the record's own types,
so a command and a file read the same words. A new dimension's line is
10 mm from what it measures (until a dimension style gives it), shows
2 decimals, and its text is centred on its line; the text's default
place at its centred point (Q58) comes with the layout on paper.

The inspect reply has the record, the status (ok, overridden,
ambiguous, suppressed, lost or unmeasurable, with why), the value
(model mm, or degrees for an angle), whether it is foreshortened, the
text, whether every name was found by exact history, and an ambiguous
dimension's candidates.

One rule, one place: the corner rule's point tolerance is
construct::point_tolerance, shared by the picks; a dimension by id is
Drawing::dimension, which the resolver uses too.

Not here yet: edit, the view's list of its dimensions, the layout on
paper and the last good value (the next slices); delete waits with
view delete for the drawing UI's choice of one delete command or one
for each kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: change a dimension's settings (#4896, W19d)

drawing.dimension.edit {dimension, set} changes what a dimension
shows and where, as drawing.view.edit changes a view: the offset of
its line, its text's place, its decimals, the text shown instead of
its number (null shows the number again), and its kind's direction,
reading or other side. The reply names the fields that changed. The
same values again change nothing and add no undo step (the drawing is
put back only when it differs). What a dimension measures isn't a
setting: to measure something else, add a new one.

A setting its kind hasn't got is refused (an angle has no direction,
only an angle has an other side); every changed record passes the
check a file read runs, so an edit can't save what opening the file
would refuse. A dimension's place in the list is
Drawing::dimension_index, which Drawing::dimension now uses too, as a
view's is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: inspect lists a view's dimensions (#4896, W19d)

drawing.view.inspect answers `dimensions`: the ids of the view's
dimensions, in id order, so a caller can find them and read each with
drawing.dimension.inspect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing views: a copied view takes copies of its dimensions (#4896, W19d)

drawing.view.copy_to_sheet copies each copied view's dimensions with
it, each with an id of its own on the copy, in the same undo step:
they are part of what the view shows (the W19d plan's question 1, its
recommended answer). A dimension's text placed `at` a point is from its
view's centre, so it keeps its place on the copy. A moved view keeps
its own dimensions as they are: they name the view by id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: the last good value, in memory (#4896, W19d)

A dimension that can't be measured now (its edge filleted away, its
corner gone) gives `last_good`: the value it last read this session,
as `value` gives it (model mm, or degrees for an angle). Each value a
dimension reads is kept by drawing and dimension in the session, and a
scratch copy keeps the session's. It is never saved (the W19b plan's
question 3), so a design's drawings don't change with every model
edit, and after reopening a lost dimension shows no number.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: hostile values anywhere in the commands' params (#4896, W19d)

A hostile value in each place of drawing.dimension.add, .edit and
.inspect's parameters, at every depth (the kind, its picks and paths, a
corner's edges, the edit's settings), with 1 000-item lists, the
largest index and 600-byte text besides the engine's shared hostile
values: no panic, and the drawing keeps its rules. The engine's own
hostile test sends only its list of key names, none of which these
commands read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: a new dimension's defaults, tested (#4896, W19d)

The offset (10 mm), the text's place (centred) and the decimals (2) a
dimension is added with when not told.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A plane carried by a transform, in one place

`assembly::map_plane` is public and gives `None` for a transform that flattens the plane; its two
copies (the occurrence mapping, which keeps the plane as it was, and the face anchor's, which
refuses) now call it and keep their own fallback. W20a's radial dimensions use it to carry a
view's plane into a body's frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drawing dimensions: a circle's or an arc's radius and diameter (#4896, W20a)

A radial dimension saves its edge by name and whether it shows the diameter. It reads the edge's
exact circle in the view's plane through the edge (`Body::planar_edges`, as the view draws it), so
a view that doesn't see the circle face on can't measure it; its text is R or Ø before the number.
`drawing.dimension.add` takes `{radial: {edge, diameter?}}`, a diameter by default for a whole
circle or half a turn or more (a hole's rim is two halves), and refuses a dimension of any kind its
view can't measure; `drawing.dimension.edit` switches `diameter`, keeping the id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 22:56:06 -04:00
Mateusz Sych ebafe07487 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>
2026-10-27 00:20:17 -04:00
CamWight 3575c99750 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>
2026-10-27 00:18:05 -04:00
CamWight de7bf4fc4d 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>
2026-10-27 00:18:01 -04:00
CamWight f18616e19c 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>
2026-10-26 16:54:42 -04:00
CamWight 657c733166 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>
2026-10-26 16:18:29 -04:00
CamWight 4c4f25115b 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>
2026-10-25 11:05:56 -04:00
CamWight 672ef13eb8 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>
2026-10-24 19:59:28 -04:00
CamWight 4c63a88fc3 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>
2026-10-23 11:48:17 -04:00
CamWight 5e23f58f62 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>
2026-10-23 10:23:18 -04:00
Benny1496 01a542d6e8 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
2026-10-23 07:35:28 -04:00
CamWight f0cc73eb83 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>
2026-10-23 03:01:08 -04:00
1238 changed files with 72953 additions and 184322 deletions
-6
View File
@@ -33,12 +33,6 @@ slow-timeout = { period = "60s" }
[profile.ci.junit]
path = "junit.xml"
# Bound the whole test process, including Vulkan host initialization and teardown.
# Cold runs can warm multiple pipelines before the first image starts rendering.
[[profile.ci.overrides]]
filter = "package(=solidcraft-ui-egui) & test(=render_workspace::tests::progressive_image_controls_route_to_the_registered_host)"
slow-timeout = { period = "60s", terminate-after = 20 }
# The action matrix sample runs its designs in parallel workers (ACTION_MATRIX_JOBS, set to match in
# ci.yml). Its workers reserve that many of nextest's threads, so the cores are not oversubscribed
# (a pi-case hole that takes 8 s alone went past the matrix's 90 s action limit when they were),
+2 -13
View File
@@ -725,16 +725,10 @@ jobs:
run: |
tag="v$VERSION"
sha256sum --check SHA256SUMS.txt
# Generated notes start at the newest published release, pre-releases included. Left
# to GitHub they start at the Latest stable one, and the list outgrows the 125,000
# characters a release body may have.
start=()
prev="$(gh release list --exclude-drafts --limit 30 --json tagName,publishedAt --jq 'sort_by(.publishedAt) | last | .tagName // empty')"
if [ -n "$prev" ]; then start=(--notes-start-tag "$prev"); fi
# Stage all packages before publishing. Published tags are never replaced.
gh release create "$tag" --draft --prerelease --latest=false \
--title "SolidCraft v$VERSION (nightly)" --target "$GITHUB_SHA" \
--notes-file ../notes.md --generate-notes "${start[@]}" -- *
--notes-file ../notes.md --generate-notes -- *
gh release edit "$tag" --draft=false --prerelease --latest=false
echo "### Published nightly: $tag (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
- name: Create or update the draft release
@@ -766,13 +760,8 @@ jobs:
--target "$GITHUB_SHA" --prerelease="$PRERELEASE"
gh release upload "$tag" --clobber -- *
else
# As for nightlies: start the generated notes at the newest published release.
start=()
prev="$(gh release list --exclude-drafts --limit 30 --json tagName,publishedAt --jq 'sort_by(.publishedAt) | last | .tagName // empty')"
if [ -n "$prev" ]; then start=(--notes-start-tag "$prev"); fi
gh release create "$tag" --draft --title "SolidCraft v$VERSION" \
--target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${start[@]}" \
"${pre[@]}" -- *
--target "$GITHUB_SHA" --notes-file ../notes.md --generate-notes "${pre[@]}" -- *
fi
{
echo "Draft **SolidCraft v$VERSION** at \`${GITHUB_SHA::9}\`:"
+5 -7
View File
@@ -3,13 +3,13 @@
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
## Start here
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Local `plan/` notes are optional.
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
## Hard rules (what enforces each)
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code, errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
- New kernel cases go to solidcore: `cargo xtask lists`.
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
@@ -21,14 +21,12 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line: `pr-checks.yml`.
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR: `skills/solidcraft-contributing/SKILL.md`.
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `skills/solidcraft-contributing/SKILL.md`.
- Code, comments, commit messages and docs are in English, always: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
- Independent work runs in parallel, dependent work stays sequential: `skills/solidcraft-contributing/SKILL.md` ("Working as an agent").
- Show observable changes with real screenshots or a recording, captured yourself.
- Run `cargo xtask ci` before every commit. `cargo xtask skills` keeps this file and `skills/` honest.
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
## Working on X: read the skill first
| Working on | Read |
+6 -60
View File
@@ -82,6 +82,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.ttf | IBM Corp. (IBM Plex Mono) | https://github.com/IBM/plex (via google/fonts), unmodified; licence text in assets/fonts/ibm-plex-mono/OFL.txt; the interface monospace font | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/IBMPlexSans-Variable.ttf | IBM Corp. (IBM Plex Sans, variable wdth and wght) | https://github.com/IBM/plex (via google/fonts), unmodified; licence text in assets/fonts/ibm-plex-sans/OFL.txt; the default interface font | SIL OFL 1.1 |
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| assets/fonts/craft-ui-sc/CraftUI-SC.otf | Adobe / Google Inc. (Noto Sans SC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/SC Regular, glyph-subset for assets/locales/zh.json and renamed Craft UI SC (OFL reserved-name); licence text in assets/fonts/craft-ui-sc/OFL.txt; web UI Simplified Chinese | SIL OFL 1.1 |
| assets/fonts/craft-ui-tc/CraftUI-TC.otf | Adobe / Google Inc. (Noto Sans TC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/TC Regular, glyph-subset for assets/locales/zh-TW.json and renamed Craft UI TC (OFL reserved-name); licence text in assets/fonts/craft-ui-tc/OFL.txt; web UI Traditional Chinese | SIL OFL 1.1 |
@@ -146,6 +147,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/fonts/craft-ui-tc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-TC.otf | SIL OFL 1.1 |
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/icons/app/linux/exports/hicolor/128x128/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/16x16/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/24x24/apps/io.tangencylabs.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
@@ -213,7 +215,6 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/styles/ui/themes/one-dark.json | SolidCraft contributors | original SolidCraft theme file with colours adapted from the One Dark palette (Atom One Dark by GitHub Atom, https://github.com/atom/atom, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/tokyo-night.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Tokyo Night by enkia, https://github.com/enkia/tokyo-night-vscode-theme, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solarized-light.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Solarized by Ethan Schoonover, https://github.com/altercation/solarized, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/solarized-dark.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Solarized by Ethan Schoonover, https://github.com/altercation/solarized, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/nord.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Nord by Sven Greb and the Nord contributors, https://github.com/nordtheme/nord, MIT) | MIT OR Apache-2.0 |
| assets/styles/ui/themes/high-contrast.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
@@ -316,6 +317,7 @@ Editable drawing code, native projects and shipped exports are collected under `
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
@@ -785,63 +787,6 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
| assets/locales/pending/theme-blur-ui.json | SolidCraft contributors | Original blur controls, notices and translations | MIT OR Apache-2.0 |
| crates/ui-egui/tests/fixtures/theme-blur.json | SolidCraft contributors | Original theme for blur option scenarios and manager review | MIT OR Apache-2.0 |
| assets/locales/ka.json | David Machakhelidze (devius) | Original Georgian translation of SolidCraft's English catalog, written with Claude Code from the meaning of each English string; terminology and doubtful strings decided by a native Georgian speaker; no commercial CAD text used | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark.svg | SolidCraft contributors | static SolidCraft mark: the five faces and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg (the final frame of assets/styles/web/loading-logo.svg), cropped to the mark's bounds; original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark-mono-black.svg | SolidCraft contributors | one-colour (black) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark-mono-white.svg | SolidCraft contributors | one-colour (white) version of assets/branding/logo/solidcraft-mark.svg, same paths; original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark-512.png | SolidCraft contributors | transparent 512 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark-1024.png | SolidCraft contributors | transparent 1024 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-mark-2048.png | SolidCraft contributors | transparent 2048 px render of assets/branding/logo/solidcraft-mark.svg (cairosvg); original artwork | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-wordmark.svg | SolidCraft contributors | the name SolidCraft set like the loading logo's wordmark (bold, +0.02 em tracking, #15499e) in IBM Plex Sans Bold from assets/fonts/ibm-plex-sans (SIL OFL 1.1, IBM Corp.), outlined to paths; no font file is embedded | MIT OR Apache-2.0 |
| assets/branding/logo/solidcraft-lockup-horizontal.svg | SolidCraft contributors | assets/branding/logo/solidcraft-mark.svg beside assets/branding/logo/solidcraft-wordmark.svg, in the proportions of assets/branding/web/source/og.png; original artwork | MIT OR Apache-2.0 |
| assets/styles/ui/themes/onyx.json | SolidCraft contributors | original Onyx stone-grey palette, option overlays and semantic icon-role mapping; existing SolidCraft artwork is unchanged | MIT OR Apache-2.0 |
| docs/book/src/media/ui-tour/window.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (`solidcraft --sample`, default dark theme, software renderer, 1280x800) with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/ui-tour/navigate.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (`--sample`, default dark theme, software renderer) orbiting, panning and zooming, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| assets/locales/pending/sketch-tool-hints.json | carlymx | Original sketch tool hints and engine error translations | MIT OR Apache-2.0 |
| assets/locales/pending/primitive-pick-field-tips.json | SolidCraft contributors | Original primitive pick-field tooltip translations | MIT OR Apache-2.0 |
| assets/locales/pending/ribbon-panel-hover.json | SolidCraft contributors | Original ribbon panel menu preference labels and translations | MIT OR Apache-2.0 |
| docs/screenshots/box-insert-designer.gif | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (2.5× playback, 800 px wide) | MIT OR Apache-2.0 |
| docs/screenshots/box-insert-designer.mp4 | Shawn Marx | original recording made with BoxInsertDesigner's own Motion export (its three.js viewer) of its example game `examples/ccg-example` (https://github.com/ShawnMarx/BoxInsertDesigner): a card game box insert whose trays were built by SolidCraft through `solidcraft-cli`; the published card, board and box art in it is pixelated beyond recognition, and no logos or game names are shown (1280×720) | MIT OR Apache-2.0 |
| assets/locales/pending/agent-http-consent.json | SolidCraft contributors | Original endpoint-specific HTTP consent messages and translations | MIT OR Apache-2.0 |
| assets/locales/pending/ground-shadow-placement.json | SolidCraft contributors | Original ground-shadow placement preference messages and translations | MIT OR Apache-2.0 |
| docs/book/src/media/assemblies/components.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) making components from two original hinge-leaf bodies, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/assemblies/joint.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) creating a revolute joint between two hinge leaves, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/assemblies/drag.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) dragging a jointed hinge leaf, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/assemblies/drive.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) driving a revolute joint between two hinge leaves, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/extrude.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) extruding a sketch's profile, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/shell.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) shelling a block, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/fillet.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) filleting four edges, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/edit.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) editing a feature from the timeline, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/hole.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) placing a hole, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/features/pattern.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) patterning a hole around an axis, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/parameters/dialog.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer) showing the Parameters dialog, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/files/menu.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (`--sample`, default dark theme, software renderer) showing the File menu, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/getting-started/start.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer, 1280x800) showing the start page with its built-in sample thumbnails, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/getting-started/sample.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) opening the built-in Sample Plate from the start page, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sheet-metal/base.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) making a sheet metal base flange, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sheet-metal/flange.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) adding sheet metal edge flanges, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sheet-metal/flat.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) showing a sheet metal flat pattern, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/create.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) starting a sketch on the Top plane, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/draw.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) drawing a rectangle and a circle with typed values, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/dimension.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) dimensioning a sketch until it is fully constrained, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/parts.png | SolidCraft contributors | screenshot of SolidCraft 0.29.0 itself (default dark theme, software renderer, 1280x800) showing a fully constrained sketch, with original numbered callouts (digits set in DejaVu Sans Bold), captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/offset.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) offsetting a sketch outline, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| docs/book/src/media/sketching/trim.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.29.0 English desktop UI (default dark theme, software renderer) trimming a notch in a sketch, with the original drawn pointer of examples/demo/overlay.py and labels set in DejaVu Sans Bold, captured with examples/demo/doc_shots.py | MIT OR Apache-2.0 |
| assets/locales/ca.json | carlymx | Catalan translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/pending/sketch-mirror-auto-line.json | SolidCraft contributors | Original Mirror preference label and translations | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-01-extrude5-edge-frame.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-04-preview-outer-l11.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-05-preview-inner-l8.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-06-result-outer-120.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-07-preview-360.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-08-cancelled.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-13-colliding-id.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-14-surface-l1.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/demo-15-surface-result.png | SamD98 | screenshot of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| docs/screenshots/7452/revolve-selected-edge.mp4 | SamD98 | screen recording of the actual SolidCraft desktop UI showing the revolve axis pick on the Cat Post Topper repro design (docs/screenshots/7452/revolve-edge-frame.solidcraft), for #7452 | MIT OR Apache-2.0 |
| assets/locales/bg.json | Yasen-55 | Original translation | MIT OR Apache-2.0 |
| crates/sketch/assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in crates/sketch/assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| crates/sketch/assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/locales/pending/drag-to-zoom.json | SolidCraft contributors | Original drag-to-zoom labels and translations | MIT OR Apache-2.0 |
## Code adapted from sibling projects
@@ -893,7 +838,7 @@ into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
## FreeCAD and Rhino import library
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
0.6.7 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
0.6.3 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
`cadmpeg-container`, `cadmpeg-codec-freecad`, `cadmpeg-codec-rhino` and `cadmpeg-codec-step`
in native and browser builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
default features, so cadmpeg's codecs for commercial CAD formats and its ShapeManager/ACIS
@@ -911,10 +856,11 @@ copied into SolidCraft.
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
## Vendored renderer compatibility fix
`vendor/egui-wgpu` is egui-wgpu 0.37.0 from https://github.com/emilk/egui
`vendor/egui-wgpu` is egui-wgpu 0.36.2 from https://github.com/emilk/egui
(MIT OR Apache-2.0; both original licence texts are included). Rust sources and
its original `egui.wgsl` shader are retained; no images or other artwork are copied.
SolidCraft's narrow patch selects a safe 1x MSAA path on the chosen OpenGL adapter
+1 -13
View File
@@ -88,8 +88,6 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
- `crates/geom`: vectors, planes, meshes, measures.
- `crates/sketch`: our own constraint solver, plus profile finding.
- `crates/dynamics`: multibody dynamics: rigid bodies, joint constraints, gravity and springs,
integrated in time (RFC #9719). It depends only on `geom`.
- `crates/kernel`: the B-rep kernel boundary. It wraps [truck](https://github.com/ricosjp/truck),
and it's the only crate allowed to touch truck. Fillets, chamfers, shell/offset, Delete Face,
Split Face, STEP/IGES read and write, and face provenance are our own code here. Patched
@@ -279,7 +277,7 @@ mean something else, it needs a format change. **Add one file, and edit nothing
change's file, or any `v<N>/` folder: those are fixed once released.
That's all. The build counts the files: while any are pending it writes the next format number,
which no release reads, so a release warns about a design that has your change and keeps what it can't read of it.
which no release reads, so a release refuses a design that has your change instead of losing it.
A file also lists the pending changes it has (`format_pending`), so a later development build
runs only the steps that file is missing. Two pull requests that each add a change merge without
a conflict. `crates/doc/src/format/pending/README.md` has an example.
@@ -474,16 +472,6 @@ the same area), first resolve the command registry and source conflicts, then ru
The generator tests cover stable ordering and additions that leave existing lines
unchanged. The initial switch to sorted rows is a one-time larger diff.
## Pictures in the user guide
The user guide's screenshots and short clips (`docs/book/src/media/`) are captured from the real
app by `examples/demo/doc_shots.py`, one function per figure. Each run starts a fresh app with its
own settings, so your preferences never show. When a change makes a figure out of date, run
`python3 examples/demo/doc_shots.py <page or shot>` (it needs Xvfb, ffmpeg, Pillow and a release
build) and commit the new file. A new figure is a new function there, an ATTRIBUTION row in
`attribution/pending/`, and a `<video>` (clips) or image link on the page. Keep a still under
about 250 KB and a clip under about 1 MB.
## Licence
By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest
Generated
+180 -194
View File
@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "accesskit"
version = "0.25.2"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be"
checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320"
dependencies = [
"enumn",
"serde",
@@ -15,12 +15,12 @@ dependencies = [
[[package]]
name = "accesskit_atspi_common"
version = "0.21.1"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d"
checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954"
dependencies = [
"accesskit",
"accesskit_consumer",
"accesskit_consumer 0.36.0",
"atspi-common",
"phf",
"serde",
@@ -29,37 +29,41 @@ dependencies = [
[[package]]
name = "accesskit_consumer"
version = "0.39.2"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3"
checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
dependencies = [
"accesskit",
"hashbrown 0.17.1",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_ios"
version = "0.2.2"
name = "accesskit_consumer"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232"
checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.17.1",
"objc2 0.5.3",
"objc2-foundation 0.2.2",
"objc2-ui-kit 0.2.2",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_consumer"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245"
dependencies = [
"accesskit",
"hashbrown 0.16.1",
]
[[package]]
name = "accesskit_macos"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84"
version = "0.26.3"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.17.1",
"accesskit_consumer 0.38.0",
"hashbrown 0.16.1",
"objc2 0.5.3",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
@@ -67,9 +71,9 @@ dependencies = [
[[package]]
name = "accesskit_unix"
version = "0.24.1"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6"
checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -85,13 +89,13 @@ dependencies = [
[[package]]
name = "accesskit_windows"
version = "0.35.2"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc"
checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.17.1",
"accesskit_consumer 0.35.0",
"hashbrown 0.16.1",
"static_assertions",
"windows",
"windows-core",
@@ -99,12 +103,11 @@ dependencies = [
[[package]]
name = "accesskit_winit"
version = "0.34.2"
version = "0.32.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc"
checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313"
dependencies = [
"accesskit",
"accesskit_ios",
"accesskit_macos",
"accesskit_unix",
"accesskit_windows",
@@ -200,7 +203,6 @@ dependencies = [
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -562,9 +564,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadmpeg-codec-freecad"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe0ed1935ace2c203b87107a064818dec2bb2ec2f250c4ad121767f3da75d1b"
checksum = "3c4a8fa61748cbb3ce80838b00a8cebfe553460583941f5aba6e2b97c16b4d37"
dependencies = [
"cadmpeg-container",
"cadmpeg-core",
@@ -578,9 +580,9 @@ dependencies = [
[[package]]
name = "cadmpeg-codec-rhino"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd2b76bd1d9301c949fbd731fd22cff9d37d10d1f481c754b557c2187bd0171"
checksum = "d7af41440d4c1d69e88e612de02cc3c9bad79d03429531e640ac924fef53c6bf"
dependencies = [
"cadmpeg-container",
"cadmpeg-core",
@@ -597,9 +599,9 @@ dependencies = [
[[package]]
name = "cadmpeg-codec-step"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cbce33ed681a28b62d8b01ae64cca2b01f11f31b9a3e5e2455b9ffde915712"
checksum = "35e1b6d17cf26b31f59f2edab8ef5c29d2a33216c581fd913f012ad4869bd559"
dependencies = [
"base64",
"cadmpeg-container",
@@ -614,9 +616,9 @@ dependencies = [
[[package]]
name = "cadmpeg-container"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03f0d1c22b875294c5fbf4a4738aec8f8c0a4ad035eb28ece404f1dec21f371a"
checksum = "5ab7bcef48ac7480b4917820272d85cd2c9533e17904e15b0e4d22f19a6045bd"
dependencies = [
"cadmpeg-core",
"crc32fast",
@@ -626,9 +628,9 @@ dependencies = [
[[package]]
name = "cadmpeg-core"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "795bdc6e2a10ecc0627138874c17e2948a3925cd22c6c6b0c2e4f6621d92f89c"
checksum = "e86bb7a40521832504bb1f5953a1c706b2c52aad73bf6d84e1b35e5cb0beae3f"
dependencies = [
"flate2",
"memchr",
@@ -641,9 +643,9 @@ dependencies = [
[[package]]
name = "cadmpeg-ir"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5592036516aaf2574b159a86a3d1ae872dc245261de86e4c0ce6773fed00a249"
checksum = "b570a1a8c579e407a9fd7bb10fa1900f8827f1d578b8fcb4ea2a056b5f323c72"
dependencies = [
"base64",
"cadmpeg-core",
@@ -656,9 +658,9 @@ dependencies = [
[[package]]
name = "cadmpeg-registry"
version = "0.6.7"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc163233d5f664337267192848505854de6aeed4843181adf87f58baddcee9a0"
checksum = "b2be23469a9bca0b684f1811118fffb581ebd32ded08e6cc8b0716a41f8c1377"
dependencies = [
"cadmpeg-codec-freecad",
"cadmpeg-codec-rhino",
@@ -685,18 +687,42 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "calloop"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b32313e937c8721c9d17ee9bf60699a6402b4b7c2cb6ac1bee0ffcaa4b4e5e"
dependencies = [
"bitflags 2.13.2",
"polling",
"rustix 1.1.5",
"tracing",
]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop",
"calloop 0.13.0",
"rustix 0.38.44",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "calloop-wayland-source"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa"
dependencies = [
"calloop 0.14.5",
"rustix 1.1.5",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.1.2"
@@ -1097,9 +1123,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecolor"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31bcbef0d4263a63eb32c4834e5d711613035ce1333468e107d928923e21e6e2"
checksum = "0656a7530b9e2841ee7a5f983965bdd9840c32f4012f759cfd44b912cced6b34"
dependencies = [
"bytemuck",
"emath",
@@ -1108,9 +1134,9 @@ dependencies = [
[[package]]
name = "eframe"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5642501a65ca657d05f00c643a5c14ff2346cb3818032bfe23d58e1f8e5f84"
checksum = "55ad41fac6e3149abb4866e93df37dbd33baa95db525c3368c944584e95a5dc2"
dependencies = [
"ahash",
"bytemuck",
@@ -1133,7 +1159,6 @@ dependencies = [
"profiling",
"raw-window-handle",
"ron",
"send_wrapper",
"serde",
"static_assertions",
"wasm-bindgen",
@@ -1146,9 +1171,9 @@ dependencies = [
[[package]]
name = "egui"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec9144f49b7a28b5f395d3147e325b4fe3e77ba686eac11d1bdf0a7da3f375f"
checksum = "dc938cc27cd911415e1e4d151bc56ff9df063d8db081374e87aeb805ea74b2ba"
dependencies = [
"accesskit",
"ahash",
@@ -1168,7 +1193,7 @@ dependencies = [
[[package]]
name = "egui-wgpu"
version = "0.37.0"
version = "0.36.2"
dependencies = [
"ahash",
"bytemuck",
@@ -1186,34 +1211,32 @@ dependencies = [
[[package]]
name = "egui-winit"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e449f4c4a2a60c7ccb4ba7fa4078417bfe1f9ddc03b5e05ce75748e33a70de"
checksum = "98466000559d66ba4db786a30448b01483946add4188e7f10f522a27b4fb89cc"
dependencies = [
"accesskit_winit",
"arboard",
"block2 0.6.2",
"bytemuck",
"egui",
"log",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"objc2-ui-kit 0.3.2",
"profiling",
"raw-window-handle",
"serde",
"smithay-clipboard",
"web-time",
"webbrowser",
"windows-sys 0.61.2",
"winit",
]
[[package]]
name = "egui_glow"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9275ca5cc35a6b7c94e0e8cedf22542f66cb0cf45ed8ab2657c78ff49fec7268"
checksum = "606ed4167daf341e5753d703285c4c1d562324b2c07aa8f405cd7d066fe58f3b"
dependencies = [
"bytemuck",
"egui",
@@ -1231,9 +1254,9 @@ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
[[package]]
name = "emath"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e73ce90f54750be8672c02be0743fe47530e6f251e066f0ee2869b59eb297af5"
checksum = "52c8d4141cf0f60fb2f7aefaf352d3ffa5171ef0ab09731c64391801e95e48f1"
dependencies = [
"bytemuck",
"serde",
@@ -1307,9 +1330,9 @@ dependencies = [
[[package]]
name = "epaint"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97bac86ae3dde93fc3951e790951caea739ff39a6c89d909cdaf1d9278dbb1bd"
checksum = "a0ec0308dc23130fc1a7d4623a8534c2ed069c6dc84e258ee4f3de99615aa1a3"
dependencies = [
"ahash",
"bytemuck",
@@ -1327,16 +1350,15 @@ dependencies = [
"skrifa",
"smallvec",
"unicode-general-category",
"unicode-properties",
"unicode-segmentation",
"vello_cpu",
]
[[package]]
name = "epaint_default_fonts"
version = "0.37.0"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfc1cc505827f4c23eedc297975a0fd038268d41293f389dd8ff3b574aefab19"
checksum = "773fa9c96dd0dbef887e39d0ed6177f141cce4d68e6041df77570aa3702dfa13"
[[package]]
name = "equivalent"
@@ -1422,12 +1444,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "flate2"
version = "1.1.10"
@@ -1439,12 +1455,6 @@ dependencies = [
"zlib-rs",
]
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foldhash"
version = "0.2.0"
@@ -1641,12 +1651,12 @@ dependencies = [
[[package]]
name = "glifo"
version = "0.3.0"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be"
checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a"
dependencies = [
"bytemuck",
"foldhash 0.2.0",
"foldhash",
"hashbrown 0.17.1",
"log",
"peniko",
@@ -1816,15 +1826,6 @@ dependencies = [
"smallvec",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash 0.1.5",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -1833,7 +1834,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
"foldhash",
]
[[package]]
@@ -1842,7 +1843,7 @@ version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
dependencies = [
"foldhash 0.2.0",
"foldhash",
]
[[package]]
@@ -2499,15 +2500,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -2964,16 +2956,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_pipe"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -3022,22 +3004,11 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455"
dependencies = [
"fixedbitset",
"hashbrown 0.15.5",
"indexmap",
]
[[package]]
name = "phf"
version = "0.14.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared",
@@ -3046,9 +3017,9 @@ dependencies = [
[[package]]
name = "phf_generator"
version = "0.14.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared",
@@ -3056,9 +3027,9 @@ dependencies = [
[[package]]
name = "phf_macros"
version = "0.14.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator",
"phf_shared",
@@ -3069,9 +3040,9 @@ dependencies = [
[[package]]
name = "phf_shared"
version = "0.14.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
@@ -3791,12 +3762,6 @@ version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "send_wrapper"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
[[package]]
name = "serde"
version = "1.0.229"
@@ -4021,8 +3986,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.13.2",
"calloop",
"calloop-wayland-source",
"calloop 0.13.0",
"calloop-wayland-source 0.3.0",
"cursor-icon",
"libc",
"log",
@@ -4039,6 +4004,44 @@ dependencies = [
"xkeysym",
]
[[package]]
name = "smithay-client-toolkit"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
"bitflags 2.13.2",
"calloop 0.14.5",
"calloop-wayland-source 0.4.1",
"cursor-icon",
"libc",
"log",
"memmap2",
"rustix 1.1.5",
"thiserror 2.0.21",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-experimental",
"wayland-protocols-misc",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]]
name = "smithay-clipboard"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
dependencies = [
"libc",
"smithay-client-toolkit 0.20.0",
"wayland-backend",
]
[[package]]
name = "smol_str"
version = "0.2.2"
@@ -4150,17 +4153,6 @@ dependencies = [
"web-time",
]
[[package]]
name = "solidcraft-dynamics"
version = "0.40.0-rc.2"
dependencies = [
"serde",
"serde_json",
"solidcore-record",
"solidcraft-geom",
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft-engine"
version = "0.40.0-rc.2"
@@ -4227,6 +4219,7 @@ dependencies = [
"gethostname",
"gltf",
"log",
"png",
"quick-xml",
"rustix 1.1.5",
"serde",
@@ -4237,8 +4230,10 @@ dependencies = [
"solidcraft-geom",
"solidcraft-kernel",
"solidcraft-profile",
"solidcraft-sketch",
"solidcraft-testkit",
"thiserror 2.0.21",
"tiny-skia",
"zip",
]
@@ -4283,11 +4278,9 @@ dependencies = [
name = "solidcraft-render"
version = "0.40.0-rc.2"
dependencies = [
"flate2",
"image",
"moxcms",
"png",
"robust",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -4304,6 +4297,7 @@ dependencies = [
"harfrust",
"serde",
"serde_json",
"sha2 0.10.9",
"solidcore-record",
"solidcraft-geom",
"thiserror 2.0.21",
@@ -4662,6 +4656,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -4687,17 +4682,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "tree_magic_mini"
version = "3.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6"
dependencies = [
"memchr",
"nom",
"petgraph",
]
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -4772,12 +4756,6 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993ad287eb20e8ba755312801cfe1413e0f25d604075863acaddca10b04c1833"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
@@ -4861,9 +4839,9 @@ dependencies = [
[[package]]
name = "vello_common"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd"
checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2"
dependencies = [
"bytemuck",
"fearless_simd",
@@ -4876,9 +4854,9 @@ dependencies = [
[[package]]
name = "vello_cpu"
version = "0.2.0"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9"
checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea"
dependencies = [
"bytemuck",
"glifo",
@@ -5053,6 +5031,32 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-experimental"
version = "20250721.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-misc"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8"
dependencies = [
"bitflags 2.13.2",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.12"
@@ -5630,7 +5634,7 @@ dependencies = [
"bitflags 2.13.2",
"block2 0.5.1",
"bytemuck",
"calloop",
"calloop 0.13.0",
"cfg_aliases",
"concurrent-queue",
"core-foundation 0.9.4",
@@ -5651,7 +5655,7 @@ dependencies = [
"raw-window-handle",
"redox_syscall 0.4.1",
"rustix 0.38.44",
"smithay-client-toolkit",
"smithay-client-toolkit 0.19.2",
"smol_str",
"tracing",
"unicode-segmentation",
@@ -5694,24 +5698,6 @@ version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wl-clipboard-rs"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "959c7ac3b8afde864f7a7448ac16f29b2b83350b7eccbf77fb781c3999aa6848"
dependencies = [
"libc",
"log",
"os_pipe",
"rustix 1.1.5",
"thiserror 2.0.21",
"tree_magic_mini",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-wlr",
]
[[package]]
name = "writeable"
version = "0.6.4"
+10 -9
View File
@@ -22,7 +22,6 @@ solidcraft-kernel = { path = "crates/kernel" }
solidcraft-smlib = { path = "crates/smlib" }
solidcraft-step = { path = "crates/step" }
solidcraft-render = { path = "crates/render" }
solidcraft-dynamics = { path = "crates/dynamics" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
solidcraft-io = { path = "crates/io" }
@@ -82,9 +81,9 @@ gethostname = "1"
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
# features, so cadmpeg's codecs for commercial formats (f3d, sldprt, inventor, catia, creo, nx)
# and its ShapeManager/ACIS `sat` codec are not compiled in. `step` is the writer we restate through.
cadmpeg-core = "=0.6.7"
cadmpeg-ir = "=0.6.7"
cadmpeg-registry = { version = "=0.6.7", default-features = false, features = ["fcstd", "rhino", "step"] }
cadmpeg-core = "=0.6.3"
cadmpeg-ir = "=0.6.3"
cadmpeg-registry = { version = "=0.6.3", default-features = false, features = ["fcstd", "rhino", "step"] }
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
ttf-parser = { version = "0.25", default-features = false, features = ["std", "variable-fonts"] }
@@ -103,12 +102,11 @@ tiny-skia = { version = "0.12", default-features = false, features = ["std", "si
toml = "1"
# ui
# default_fonts: the built-in Hack is the UI's symbol fallback (#2955); 0.37 no longer turns it on.
egui = { version = "0.37", features = ["default_fonts"] }
eframe = { version = "0.37", default-features = false, features = ["persistence"] }
egui = "0.36"
eframe = { version = "0.36", default-features = false, features = ["persistence"] }
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
web-time = "1"
egui-wgpu = "0.37"
egui-wgpu = "0.36"
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
naga = { version = "30", default-features = false }
@@ -163,9 +161,12 @@ debug = "line-tables-only"
egui-wgpu = { path = "vendor/egui-wgpu" }
# Deliver trackpad pinch and rotation gestures on Wayland (zwp_pointer_gestures_v1), which
# winit 0.30 never binds and eframe 0.37 still requires winit 0.30.
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
winit = { path = "vendor/winit" }
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
accesskit_macos = { path = "vendor/accesskit_macos" }
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
# builds, since every mutant rebuilds the crates above the one it changes.
[profile.mutants]
+6 -6
View File
@@ -173,6 +173,12 @@ and the [foundation ledger](docs/drawing-foundations.md) track the bounded imple
packages. The first geometry increment defines validated section records in a plane-local
frame; the ledger separates that contract from section producers and the Drawing workspace.
Drawing UI scaffold: the native workspace now supplies the shared four-tab ribbon,
source Browser, compact dockable sheet strip and resizable Sheet Settings/options
panel. Planned tools remain unavailable; only existing source services execute.
Drawing files and paper rendering are still deferred. See the
[UI implementation roadmap](docs/drawing-workspace-ui.md).
Desktop view DXF export runs in the file-operation worker with five-stage progress and
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
@@ -225,12 +231,6 @@ completed drawings publish atomically. The browser still exports synchronously.
## Render foundation
- Wood surface images (#1345): native CPU and GPU opaque rendering share physical
repeat frames and filtered mip chains. Imported maps retain exact bytes and
declared provenance in the design, reopen offline, and expose grain-angle
controls. Scalar face appearances suppress body maps. Planar projection and
CPU encoding/optical limits remain explicit; see [portable surfaces](docs/render-portable-surfaces-and-stages.md).
- Original starter appearances (#1722): 34 opaque recipes through existing commands, with six explicitly unavailable glass recipes and preserved legacy looks; see [the contract](docs/render-starter-appearances.md).
- Bounded linear film accumulation (#1696) supplies caller-admitted mean/variance storage and complete-pass views; actual progressive consumers remain separate. See [the film contract](docs/render-film.md).
+130 -289
View File
@@ -1,17 +1,18 @@
Third-party software in SolidCraft
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 540 crates below.
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 537 crates below.
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
accesskit 0.25.2 (MIT OR Apache-2.0)
accesskit_atspi_common 0.21.1 (MIT OR Apache-2.0)
accesskit_consumer 0.39.2 (MIT OR Apache-2.0)
accesskit_ios 0.2.2 (MIT OR Apache-2.0)
accesskit_macos 0.27.2 (MIT OR Apache-2.0)
accesskit_unix 0.24.1 (MIT OR Apache-2.0)
accesskit_windows 0.35.2 (MIT OR Apache-2.0)
accesskit_winit 0.34.2 (Apache-2.0)
accesskit 0.24.1 (MIT OR Apache-2.0)
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
accesskit_consumer 0.35.0 (MIT OR Apache-2.0)
accesskit_consumer 0.36.0 (MIT OR Apache-2.0)
accesskit_consumer 0.38.0 (MIT OR Apache-2.0)
accesskit_macos 0.26.3 (MIT OR Apache-2.0)
accesskit_unix 0.21.1 (MIT OR Apache-2.0)
accesskit_windows 0.32.1 (MIT OR Apache-2.0)
accesskit_winit 0.32.2 (Apache-2.0)
adler2 2.0.1 (0BSD OR MIT OR Apache-2.0)
aes 0.8.4 (MIT OR Apache-2.0)
ahash 0.8.12 (MIT OR Apache-2.0)
@@ -56,15 +57,17 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
byteorder 1.5.0 (Unlicense OR MIT)
byteorder-lite 0.1.0 (Unlicense OR MIT)
bytes 1.12.1 (MIT)
cadmpeg-codec-freecad 0.6.7 (Apache-2.0)
cadmpeg-codec-rhino 0.6.7 (Apache-2.0)
cadmpeg-codec-step 0.6.7 (Apache-2.0)
cadmpeg-container 0.6.7 (Apache-2.0)
cadmpeg-core 0.6.7 (Apache-2.0)
cadmpeg-ir 0.6.7 (Apache-2.0)
cadmpeg-registry 0.6.7 (Apache-2.0)
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
cadmpeg-codec-step 0.6.3 (Apache-2.0)
cadmpeg-container 0.6.3 (Apache-2.0)
cadmpeg-core 0.6.3 (Apache-2.0)
cadmpeg-ir 0.6.3 (Apache-2.0)
cadmpeg-registry 0.6.3 (Apache-2.0)
calloop 0.13.0 (MIT)
calloop 0.14.5 (MIT)
calloop-wayland-source 0.3.0 (MIT)
calloop-wayland-source 0.4.1 (MIT)
cbc 0.1.2 (MIT OR Apache-2.0)
cfg-if 1.0.5 (MIT OR Apache-2.0)
cgl 0.3.2 (MIT / Apache-2.0)
@@ -106,21 +109,21 @@ document-features 0.2.12 (MIT OR Apache-2.0)
downcast-rs 1.2.1 (MIT/Apache-2.0)
dpi 0.1.2 (Apache-2.0 AND MIT)
dyn-clone 1.0.20 (MIT OR Apache-2.0)
ecolor 0.37.0 (MIT OR Apache-2.0)
eframe 0.37.0 (MIT OR Apache-2.0)
egui 0.37.0 (MIT OR Apache-2.0)
egui-wgpu 0.37.0 (MIT OR Apache-2.0)
egui-winit 0.37.0 (MIT OR Apache-2.0)
egui_glow 0.37.0 (MIT OR Apache-2.0)
ecolor 0.36.2 (MIT OR Apache-2.0)
eframe 0.36.2 (MIT OR Apache-2.0)
egui 0.36.2 (MIT OR Apache-2.0)
egui-wgpu 0.36.2 (MIT OR Apache-2.0)
egui-winit 0.36.2 (MIT OR Apache-2.0)
egui_glow 0.36.2 (MIT OR Apache-2.0)
either 1.19.0 (MIT OR Apache-2.0)
emath 0.37.0 (MIT OR Apache-2.0)
emath 0.36.2 (MIT OR Apache-2.0)
encoding_rs 0.8.42 ((Apache-2.0 OR MIT) AND BSD-3-Clause)
endi 1.1.1 (MIT)
enumflags2 0.7.12 (MIT OR Apache-2.0)
enumflags2_derive 0.7.12 (MIT OR Apache-2.0)
enumn 0.1.14 (MIT OR Apache-2.0)
epaint 0.37.0 (MIT OR Apache-2.0)
epaint_default_fonts 0.37.0 ((MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0)
epaint 0.36.2 (MIT OR Apache-2.0)
epaint_default_fonts 0.36.2 ((MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0)
equivalent 1.0.2 (Apache-2.0 OR MIT)
errno 0.3.14 (MIT OR Apache-2.0)
error-code 3.4.0 (BSL-1.0)
@@ -131,9 +134,7 @@ fastrand 2.5.0 (Apache-2.0 OR MIT)
fax 0.2.7 (MIT)
fdeflate 0.3.7 (MIT OR Apache-2.0)
fearless_simd 0.4.1 (Apache-2.0 OR MIT)
fixedbitset 0.5.7 (MIT OR Apache-2.0)
flate2 1.1.10 (MIT OR Apache-2.0)
foldhash 0.1.5 (Zlib)
foldhash 0.2.0 (Zlib)
font-types 0.12.6 (MIT OR Apache-2.0)
foreign-types 0.5.0 (MIT/Apache-2.0)
@@ -153,7 +154,7 @@ getrandom 0.2.17 (MIT OR Apache-2.0)
getrandom 0.3.4 (MIT OR Apache-2.0)
getrandom 0.4.3 (MIT OR Apache-2.0)
gif 0.14.2 (MIT OR Apache-2.0)
glifo 0.3.0 (Apache-2.0 OR MIT)
glifo 0.2.0 (Apache-2.0 OR MIT)
glow 0.17.0 (MIT OR Apache-2.0 OR Zlib)
gltf 1.4.1 (MIT OR Apache-2.0)
gltf-derive 1.4.1 (MIT OR Apache-2.0)
@@ -167,7 +168,6 @@ gpu-allocator 0.28.0 (MIT OR Apache-2.0)
guillotiere 0.7.0 (MIT/Apache-2.0)
half 2.7.1 (MIT OR Apache-2.0)
harfrust 0.12.0 (MIT)
hashbrown 0.15.5 (MIT OR Apache-2.0)
hashbrown 0.16.1 (MIT OR Apache-2.0)
hashbrown 0.17.1 (MIT OR Apache-2.0)
hermit-abi 0.5.3 (MIT OR Apache-2.0)
@@ -234,7 +234,6 @@ ndk-sys 0.6.0+11769913 (MIT OR Apache-2.0)
nix 0.29.0 (MIT)
nix 0.31.3 (MIT)
nohash-hasher 0.2.0 (Apache-2.0 OR MIT)
nom 8.0.0 (MIT)
num 0.4.3 (MIT OR Apache-2.0)
num-bigint 0.4.8 (MIT OR Apache-2.0)
num-complex 0.4.6 (MIT OR Apache-2.0)
@@ -276,17 +275,15 @@ orbclient 0.3.55 (MIT)
ordered-float 2.10.1 (MIT)
ordered-float 5.5.0 (MIT)
ordered-stream 0.2.0 (MIT OR Apache-2.0)
os_pipe 1.2.3 (MIT)
parking 2.2.1 (Apache-2.0 OR MIT)
parking_lot 0.12.5 (MIT OR Apache-2.0)
parking_lot_core 0.9.12 (MIT OR Apache-2.0)
peniko 0.6.1 (Apache-2.0 OR MIT)
percent-encoding 2.3.2 (MIT OR Apache-2.0)
petgraph 0.8.3 (MIT OR Apache-2.0)
phf 0.14.0 (MIT)
phf_generator 0.14.0 (MIT)
phf_macros 0.14.0 (MIT)
phf_shared 0.14.0 (MIT)
phf 0.13.1 (MIT)
phf_generator 0.13.1 (MIT)
phf_macros 0.13.1 (MIT)
phf_shared 0.13.1 (MIT)
pin-project 1.1.13 (Apache-2.0 OR MIT)
pin-project-internal 1.1.13 (Apache-2.0 OR MIT)
pin-project-lite 0.2.17 (Apache-2.0 OR MIT)
@@ -355,7 +352,6 @@ security-framework 3.7.0 (MIT OR Apache-2.0)
security-framework-sys 2.17.0 (MIT OR Apache-2.0)
self_cell 1.3.0 (Apache-2.0 OR GPL-2.0-only)
semver 1.0.28 (MIT OR Apache-2.0)
send_wrapper 0.6.0 (MIT/Apache-2.0)
serde 1.0.229 (MIT OR Apache-2.0)
serde-value 0.7.0 (MIT)
serde_core 1.0.229 (MIT OR Apache-2.0)
@@ -379,6 +375,8 @@ slab 0.4.12 (MIT)
slotmap 1.1.1 (Zlib)
smallvec 1.16.2 (MIT OR Apache-2.0)
smithay-client-toolkit 0.19.2 (MIT)
smithay-client-toolkit 0.20.0 (MIT)
smithay-clipboard 0.7.3 (MIT)
smol_str 0.2.2 (MIT OR Apache-2.0)
solidcore 1.0.1 (Apache-2.0)
solidcore-record 0.1.0 (Apache-2.0)
@@ -411,7 +409,6 @@ toml_writer 1.1.3+spec-1.1.0 (MIT OR Apache-2.0)
tracing 0.1.44 (MIT)
tracing-attributes 0.1.31 (MIT)
tracing-core 0.1.36 (MIT)
tree_magic_mini 3.2.2 (MIT)
ttf-parser 0.25.1 (MIT OR Apache-2.0)
twox-hash 2.1.5 (MIT)
type-map 0.5.1 (MIT/Apache-2.0)
@@ -422,7 +419,6 @@ uds_windows 1.2.1 (MIT)
unicode-bidi 0.3.18 (MIT OR Apache-2.0)
unicode-general-category 1.1.0 (Apache-2.0)
unicode-ident 1.0.26 ((MIT OR Apache-2.0) AND Unicode-3.0)
unicode-properties 0.1.5 ((MIT OR Apache-2.0) AND Unicode-3.0)
unicode-segmentation 1.13.3 (MIT OR Apache-2.0)
unicode-width 0.2.2 (MIT OR Apache-2.0)
untrusted 0.9.0 (ISC)
@@ -432,8 +428,8 @@ url 2.5.8 (MIT OR Apache-2.0)
utf8-zero 0.8.1 (MIT OR Apache-2.0)
utf8_iter 1.0.4 (Apache-2.0 OR MIT)
uuid 1.27.0 (Apache-2.0 OR MIT)
vello_common 0.2.0 (Apache-2.0 OR MIT)
vello_cpu 0.2.0 (Apache-2.0 OR MIT)
vello_common 0.1.0 (Apache-2.0 OR MIT)
vello_cpu 0.1.0 (Apache-2.0 OR MIT)
wasi 0.11.1+wasi-snapshot-preview1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
wasip2 1.0.4+wasi-0.2.12 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
wasm-bindgen 0.2.129 (MIT OR Apache-2.0)
@@ -446,6 +442,8 @@ wayland-client 0.31.15 (MIT)
wayland-csd-frame 0.3.0 (MIT)
wayland-cursor 0.31.14 (MIT)
wayland-protocols 0.32.13 (MIT)
wayland-protocols-experimental 20250721.0.1 (MIT)
wayland-protocols-misc 0.3.12 (MIT)
wayland-protocols-plasma 0.3.12 (MIT)
wayland-protocols-wlr 0.3.12 (MIT)
wayland-scanner 0.31.11 (MIT)
@@ -502,7 +500,6 @@ winit 0.30.13 (Apache-2.0)
winnow 1.0.4 (MIT)
winreg 0.56.0 (MIT)
wit-bindgen 0.57.1 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT)
wl-clipboard-rs 0.9.5 (MIT OR Apache-2.0)
writeable 0.6.4 (Unicode-3.0)
x11-dl 2.21.0 (MIT)
x11rb 0.13.2 (MIT OR Apache-2.0)
@@ -573,14 +570,13 @@ num_enum 0.7.6, num_enum_derive 0.7.6, once_cell 1.21.4, ordered-stream 0.2.0, p
pin-project 1.1.13, pin-project-internal 1.1.13, pin-project-lite 0.2.17, piper 0.2.5, polling
3.11.0, portable-atomic 1.15.0, portable-atomic-util 0.2.8, proc-macro-crate 3.5.0, proc-macro2
1.0.107, quote 1.0.47, ref-cast 1.0.27, ref-cast-impl 1.0.27, rustc-hash 1.1.0, rustc-hash 2.1.3,
rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, send_wrapper 0.6.0, serde 1.0.229,
serde_core 1.0.229, serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json 1.0.151,
serde_path_to_error 0.1.20, serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 2.0.119, syn
3.0.6, thiserror 1.0.69, thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path
0.13.0, typeid 1.0.3, unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1,
wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, xdg-home 1.3.0, zbus-lockstep 0.5.2,
zbus-lockstep-macros 0.5.2, zbus_names 3.0.0, zmij 1.0.23, zvariant_utils 2.1.0, zvariant_utils
4.2.0
rustix 0.38.44, rustix 1.1.5, rustversion 1.0.23, semver 1.0.28, serde 1.0.229, serde_core 1.0.229,
serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json 1.0.151, serde_path_to_error 0.1.20,
serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 2.0.119, syn 3.0.6, thiserror 1.0.69,
thiserror 2.0.21, thiserror-impl 1.0.69, thiserror-impl 2.0.21, typed-path 0.13.0, typeid 1.0.3,
unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12,
wit-bindgen 0.57.1, x11-dl 2.21.0, xdg-home 1.3.0, zbus-lockstep 0.5.2, zbus-lockstep-macros 0.5.2,
zbus_names 3.0.0, zmij 1.0.23, zvariant_utils 2.1.0, zvariant_utils 4.2.0
================================================================================
Permission is hereby granted, free of charge, to any
@@ -1090,26 +1086,25 @@ ahash 0.8.12, arrayvec 0.7.8, async-channel 2.5.0, async-executor 1.14.0, async-
0.9.4, core-foundation-sys 0.8.7, core-graphics 0.23.2, core-graphics-types 0.1.3, core_detect
1.0.0, crossbeam-channel 0.5.17, crossbeam-deque 0.8.8, crossbeam-epoch 0.9.21, crossbeam-utils
0.8.23, dbus-secret-service 4.1.0, displaydoc 0.2.7, either 1.19.0, equivalent 1.0.2, errno 0.3.14,
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, fixedbitset
0.5.7, flate2 1.1.10, form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow
0.17.0, gltf 1.4.1, gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.15.5, hashbrown 0.16.1,
hashbrown 0.17.1, hermit-abi 0.5.3, httparse 1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap
2.14.2, itertools 0.15.0, js-sys 0.3.106, keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static
1.5.1, linux-raw-sys 0.12.1, linux-raw-sys 0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num
0.4.3, num-bigint 0.4.8, num-complex 0.4.6, num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2,
num-traits 0.2.19, once_cell 1.21.4, openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1,
parking_lot 0.12.5, parking_lot_core 0.9.12, percent-encoding 2.3.2, petgraph 0.8.3, piper 0.2.5,
plain 0.2.3, png 0.18.1, polling 3.11.0, pollster 0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core
1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron 0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix
0.38.44, rustix 1.1.5, rustls 0.23.46, rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard
1.2.0, secret-service 4.0.0, security-framework 2.11.1, security-framework 3.7.0,
security-framework-sys 2.17.0, send_wrapper 0.6.0, signal-hook-registry 1.4.8, simd_cesu8 1.2.0,
smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait 1.2.1, tempfile 3.27.0, ttf-parser 0.25.1,
unicode-bidi 0.3.18, unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2, url
2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wasm-bindgen
0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129, wasm-bindgen-macro-support
0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12, wit-bindgen 0.57.1,
wl-clipboard-rs 0.9.5
euclid 0.22.14, event-listener 5.4.2, event-listener-strategy 0.5.4, fastrand 2.5.0, flate2 1.1.10,
form_urlencoded 1.2.2, futures-lite 2.6.1, gethostname 1.1.0, gif 0.14.2, glow 0.17.0, gltf 1.4.1,
gltf-derive 1.4.1, gltf-json 1.4.1, hashbrown 0.16.1, hashbrown 0.17.1, hermit-abi 0.5.3, httparse
1.10.1, idna 1.1.0, idna_adapter 1.2.2, indexmap 2.14.2, itertools 0.15.0, js-sys 0.3.106,
keyboard-types 0.8.3, khronos-egl 6.0.0, lazy_static 1.5.1, linux-raw-sys 0.12.1, linux-raw-sys
0.4.15, lock_api 0.4.14, log 0.4.34, muda 0.21.2, num 0.4.3, num-bigint 0.4.8, num-complex 0.4.6,
num-integer 0.1.47, num-iter 0.1.46, num-rational 0.4.2, num-traits 0.2.19, once_cell 1.21.4,
openssl-probe 0.2.1, ordered-stream 0.2.0, parking 2.2.1, parking_lot 0.12.5, parking_lot_core
0.9.12, percent-encoding 2.3.2, piper 0.2.5, plain 0.2.3, png 0.18.1, polling 3.11.0, pollster
0.4.0, pollster 1.0.1, rayon 1.12.0, rayon-core 1.13.0, renderdoc-sys 1.1.0, robust 1.2.0, ron
0.12.2, roxmltree 0.21.1, rustc-hash 1.1.0, rustix 0.38.44, rustix 1.1.5, rustls 0.23.46,
rustls-native-certs 0.8.5, scoped-tls 1.0.1, scopeguard 1.2.0, secret-service 4.0.0,
security-framework 2.11.1, security-framework 3.7.0, security-framework-sys 2.17.0,
signal-hook-registry 1.4.8, simd_cesu8 1.2.0, smallvec 1.16.2, smol_str 0.2.2, stable_deref_trait
1.2.1, tempfile 3.27.0, ttf-parser 0.25.1, unicode-bidi 0.3.18, unicode-segmentation 1.13.3,
unicode-width 0.2.2, url 2.5.8, uuid 1.27.0, wasi 0.11.1+wasi-snapshot-preview1, wasip2
1.0.4+wasi-0.2.12, wasm-bindgen 0.2.129, wasm-bindgen-futures 0.4.79, wasm-bindgen-macro 0.2.129,
wasm-bindgen-macro-support 0.2.129, wasm-bindgen-shared 0.2.129, web-sys 0.3.106, weezl 0.1.12,
wit-bindgen 0.57.1
================================================================================
Apache License
@@ -1315,7 +1310,7 @@ See the License for the specific language governing permissions and
limitations under the License.
================================================================================
allocator-api2 0.2.21, async-trait 0.1.92, dyn-clone 1.0.20, enumn 0.1.14, glifo 0.3.0, itoa 1.0.18,
allocator-api2 0.2.21, async-trait 0.1.92, dyn-clone 1.0.20, enumn 0.1.14, glifo 0.2.0, itoa 1.0.18,
libc 0.2.190, litrs 1.0.0, num_enum 0.7.6, num_enum_derive 0.7.6, proc-macro2 1.0.107, quote 1.0.47,
ref-cast 1.0.27, ref-cast-impl 1.0.27, rustc-hash 2.1.3, rustversion 1.0.23, semver 1.0.28, serde
1.0.229, serde_core 1.0.229, serde_derive 1.0.229, serde_derive_internals 0.30.0, serde_json
@@ -1535,7 +1530,7 @@ android-activity 0.6.1, arboard 3.6.1, color 0.3.3, fdeflate 0.3.7, fearless_sim
image 0.25.10, image-webp 0.2.4, linebender_resource_handle 0.1.1, miniz_oxide 0.8.9, miniz_oxide
0.9.1, naga 30.0.1, naga-types 30.0.1, pin-project 1.1.13, pin-project-internal 1.1.13,
pin-project-lite 0.2.17, portable-atomic 1.15.0, portable-atomic-util 0.2.8, range-alloc 0.1.5,
raw-window-handle 0.6.2, simdutf8 0.1.5, vello_common 0.2.0, vello_cpu 0.2.0, wgpu 30.0.1, wgpu-core
raw-window-handle 0.6.2, simdutf8 0.1.5, vello_common 0.1.0, vello_cpu 0.1.0, wgpu 30.0.1, wgpu-core
30.0.1, wgpu-core-deps-apple 30.0.1, wgpu-core-deps-emscripten 30.0.1, wgpu-core-deps-wasm 30.0.1,
wgpu-core-deps-windows-linux-android 30.0.1, wgpu-hal 30.0.1, wgpu-naga-bridge 30.0.1, wgpu-types
30.0.1
@@ -2593,7 +2588,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
bit-set 0.10.0, bit-vec 0.9.1, downcast-rs 1.2.1, egui-wgpu 0.37.0, presser 0.3.1
bit-set 0.10.0, bit-vec 0.9.1, downcast-rs 1.2.1, egui-wgpu 0.36.2, presser 0.3.1
================================================================================
Apache License
@@ -3139,7 +3134,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
calloop 0.13.0, smithay-client-toolkit 0.19.2
calloop 0.13.0, calloop 0.14.5, smithay-client-toolkit 0.19.2, smithay-client-toolkit 0.20.0
================================================================================
Copyright (c) 2018 Victor Berger
@@ -3163,7 +3158,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
calloop-wayland-source 0.3.0
calloop-wayland-source 0.3.0, calloop-wayland-source 0.4.1
================================================================================
Copyright (c) 2023 Kirill Chibisov
@@ -5236,7 +5231,7 @@ have been licensed under extremely permissive terms.
------------------------------------------------------------------------------
================================================================================
egui-wgpu 0.37.0
egui-wgpu 0.36.2
================================================================================
Copyright (c) 2018-2021 Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
@@ -5266,7 +5261,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
either 1.19.0, itertools 0.15.0, petgraph 0.8.3
either 1.19.0, itertools 0.15.0
================================================================================
Copyright (c) 2015
@@ -5714,36 +5709,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
fixedbitset 0.5.7
================================================================================
Copyright (c) 2015-2017
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
flate2 1.1.10
================================================================================
@@ -5775,7 +5740,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
foldhash 0.1.5, foldhash 0.2.0
foldhash 0.2.0
================================================================================
Copyright (c) 2024 Orson Peters
@@ -6546,7 +6511,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
glifo 0.3.0
glifo 0.2.0
================================================================================
Copyright 2025 the Vello Authors
@@ -7173,7 +7138,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
hashbrown 0.15.5, hashbrown 0.16.1, hashbrown 0.17.1
hashbrown 0.16.1, hashbrown 0.17.1
================================================================================
Copyright (c) 2016 Amanieu d'Antras
@@ -9494,31 +9459,6 @@ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
nom 8.0.0
================================================================================
Copyright (c) 2014-2019 Geoffroy Couprie
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================================================
num_enum 0.7.6, num_enum_derive 0.7.6
================================================================================
@@ -9607,30 +9547,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
os_pipe 1.2.3
================================================================================
The MIT License (MIT)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
parking 2.2.1
================================================================================
@@ -9646,7 +9562,7 @@ option. All files in the project carrying such notice may not be
copied, modified, or distributed except according to those terms.
================================================================================
phf 0.14.0, phf_generator 0.14.0, phf_macros 0.14.0, phf_shared 0.14.0
phf 0.13.1, phf_generator 0.13.1, phf_macros 0.13.1, phf_shared 0.13.1
================================================================================
The MIT License (MIT)
@@ -12461,6 +12377,30 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
smithay-clipboard 0.7.3
================================================================================
Copyright (c) 2018 Lucas Timmins & Victor Berger
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
solidcore 1.0.1
================================================================================
@@ -13033,32 +12973,6 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
tree_magic_mini 3.2.2
================================================================================
MIT License
Copyright (c) 2017 Aaron Hancock
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
ttf-parser 0.25.1
================================================================================
@@ -13881,7 +13795,7 @@ carrying such notice may not be copied, modified, or distributed except
according to those terms.
================================================================================
unicode-bidi 0.3.18, unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2
unicode-bidi 0.3.18, unicode-segmentation 1.13.3, unicode-width 0.2.2
================================================================================
Copyright (c) 2015 The Rust Project Developers
@@ -13955,51 +13869,7 @@ dealings in these Data Files or Software without prior written
authorization of the copyright holder.
================================================================================
unicode-properties 0.1.5
================================================================================
UNICODE LICENSE V3
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2026 Unicode, Inc.
NOTICE TO USER: Carefully read the following legal agreement. BY
DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of data files and any associated documentation (the "Data Files") or
software and any associated documentation (the "Software") to deal in the
Data Files or Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, and/or sell
copies of the Data Files or Software, and to permit persons to whom the
Data Files or Software are furnished to do so, provided that either (a)
this copyright and permission notice appear with all copies of the Data
Files or Software, or (b) this copyright and permission notice appear in
associated Documentation.
THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder shall
not be used in advertising or otherwise to promote the sale, use or other
dealings in these Data Files or Software without prior written
authorization of the copyright holder.
================================================================================
unicode-properties 0.1.5, unicode-segmentation 1.13.3, unicode-width 0.2.2
unicode-segmentation 1.13.3, unicode-width 0.2.2
================================================================================
Licensed under the Apache License, Version 2.0
@@ -14145,7 +14015,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
vello_common 0.2.0, vello_cpu 0.2.0
vello_common 0.1.0, vello_cpu 0.1.0
================================================================================
Copyright 2020 the Vello Authors
@@ -14176,8 +14046,8 @@ DEALINGS IN THE SOFTWARE.
================================================================================
wayland-backend 0.3.17, wayland-client 0.31.15, wayland-cursor 0.31.14, wayland-protocols 0.32.13,
wayland-protocols-plasma 0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.11, wayland-sys
0.31.11
wayland-protocols-experimental 20250721.0.1, wayland-protocols-misc 0.3.12, wayland-protocols-plasma
0.3.12, wayland-protocols-wlr 0.3.12, wayland-scanner 0.31.11, wayland-sys 0.31.11
================================================================================
Copyright (c) 2015 Elinor Berger
@@ -15072,36 +14942,6 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
wl-clipboard-rs 0.9.5
================================================================================
Copyright (c) 2019 Ivan Molodetskikh
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
================================================================================
x11rb 0.13.2, x11rb-protocol 0.13.2
================================================================================
@@ -16127,34 +15967,35 @@ Crates whose package carries no licence file (their licence is named in Cargo.to
texts are as in SolidCraft's own LICENSE-MIT and LICENSE-APACHE)
================================================================================
accesskit 0.25.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_atspi_common 0.21.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_consumer 0.39.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_ios 0.2.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_macos 0.27.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_unix 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_windows 0.35.2: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_winit 0.34.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_atspi_common 0.18.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_consumer 0.35.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_consumer 0.36.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_consumer 0.38.0: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_macos 0.26.3: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_unix 0.21.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_windows 0.32.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
cadmpeg-codec-freecad 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-rhino 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-step 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-container 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-core 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-ir 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-registry 0.6.7: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
dispatch2 0.3.1: Zlib OR Apache-2.0 OR MIT (https://github.com/madsmtm/objc2)
ecolor 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui)
eframe 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/eframe)
egui 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui)
egui-winit 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui-winit)
egui_glow 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui_glow)
emath 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/emath)
epaint 0.37.0: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/epaint)
epaint_default_fonts 0.37.0: (MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0 (https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts)
ecolor 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui)
eframe 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/eframe)
egui 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui)
egui-winit 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui-winit)
egui_glow 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/egui_glow)
emath 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/emath)
epaint 0.36.2: MIT OR Apache-2.0 (https://github.com/emilk/egui/tree/main/crates/epaint)
epaint_default_fonts 0.36.2: (MIT OR Apache-2.0) AND OFL-1.1 AND Ubuntu-font-1.0 (https://github.com/emilk/egui/tree/main/crates/epaint_default_fonts)
jni 0.22.4: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-rs)
jni-macros 0.22.4: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-rs)
jni-sys-macros 0.4.1: MIT OR Apache-2.0 (https://github.com/jni-rs/jni-sys)
@@ -16198,7 +16039,7 @@ spirv 0.4.0+sdk-1.4.341.0: Apache-2.0 (https://github.com/gfx-rs/rspirv)
Bundled fonts (SIL Open Font License, Version 1.1)
================================================================================
- LiberationSans-Regular.ttf (crates/sketch/assets/fonts/liberation-sans/LiberationSans-Regular.ttf)
- LiberationSans-Regular.ttf (assets/fonts/liberation-sans/LiberationSans-Regular.ttf)
Copyright 2012 Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google)
Source: https://github.com/liberationfonts
+2 -2
View File
@@ -11,7 +11,7 @@ use std::process::{Command, ExitCode};
const TARGET: &str = "solidcraft-cli";
fn main() -> ExitCode {
solidcraft_engine::note!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
eprintln!("solvecraft-cli is deprecated and will be removed in a later release: use solidcraft-cli");
let sibling = std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(format!("{TARGET}{}", std::env::consts::EXE_SUFFIX))))
@@ -23,7 +23,7 @@ fn main() -> ExitCode {
None => ExitCode::FAILURE,
},
Err(e) => {
solidcraft_engine::note!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
eprintln!("solvecraft-cli: could not run {}: {e}", program.to_string_lossy());
ExitCode::FAILURE
}
}
+4
View File
@@ -63,6 +63,10 @@ fn replay(report: &Value) -> Result<(), String> {
Some(d) => super::load(d)?,
None => solidcraft_engine::Session::default(),
};
// The ids the session drew from there, so the commands give the same objects the same ids.
if let Some(seed) = script["ids"].as_u64() {
s.seed_ids(seed);
}
std::panic::set_hook(Box::new(|info| {
let loc = info.location().map_or_else(|| "(unknown)".to_string(), |l| format!("{}:{}", l.file(), l.line()));
let msg = info.payload_as_str().unwrap_or("(not a text message)").to_string();
+6 -9
View File
@@ -33,8 +33,8 @@ pub fn one(args: &[String]) -> Result<(), String> {
std::panic::set_hook(Box::new(|_| {
PANICS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}));
solidcraft_engine::note!("design {seed}");
let run = fuzz::run_with(seed, steps, &mut |st| solidcraft_engine::note!("step {}", serde_json::to_string(st).unwrap_or_default()));
eprintln!("design {seed}");
let run = fuzz::run_with(seed, steps, &mut |st| eprintln!("step {}", serde_json::to_string(st).unwrap_or_default()));
let mut design = json!({
"outcome": run.outcome,
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
@@ -45,13 +45,13 @@ pub fn one(args: &[String]) -> Result<(), String> {
if run.outcome.is_bug() {
design["script"] = run.script();
if minimise {
solidcraft_engine::note!("minimise");
eprintln!("minimise");
let m = fuzz::minimise(&run);
design["minimal"] = json!({"outcome": m.outcome, "steps": m.steps.iter().map(|s| s.kind).collect::<Vec<_>>(), "script": m.script()});
}
}
let (a, b) = solidcraft_engine::fuzz::boolean_shapes(seed);
solidcraft_engine::note!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
eprintln!("boolean {seed} {} {}", serde_json::to_string(&a).unwrap_or_default(), serde_json::to_string(&b).unwrap_or_default());
let b = boolean_case(seed);
let bp = PANICS.swap(0, std::sync::atomic::Ordering::SeqCst);
println!("{}", json!({"seed": seed, "design": design, "boolean": b, "boolean_panics": bp}));
@@ -162,14 +162,11 @@ fn record(v: &Value, out_dir: Option<&str>) {
let d = &v["design"];
let bug = matches!(d["outcome"]["kind"].as_str(), Some("failed" | "panic" | "invalid" | "volume"));
if bug || v.get("timeout").is_some() || v.get("crash").is_some() {
solidcraft_engine::note!(
"seed {seed}: {}",
if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") }
);
eprintln!("seed {seed}: {}", if bug { format!("{} {}", d["outcome"]["kind"], d["outcome"]["detail"]) } else { format!("{v}") });
}
let bk = v["boolean"]["outcome"].as_str().unwrap_or("ok");
if bk != "ok" {
solidcraft_engine::note!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
eprintln!("seed {seed}: boolean {bk} {}", v["boolean"]["detail"]);
}
if let Some(dir) = out_dir {
use std::io::Write;
+35 -148
View File
@@ -1,7 +1,7 @@
//! `kernel-cases --export=DIR`: one kernel rig case as a corpus entry for solidcore's rig. The
//! case's commands run as the rig runs them while the kernel's operations are traced
//! (`smlib_trace`); the entry holds every operation in execution order, including discarded
//! tool construction and handled refusals, each operand that came in as a record (the
//! (`smlib_trace`); the entry holds the operation (or the chain of operations) that made the
//! bodies the case ends with, each operand that came in as a record given as that record (the
//! B-rep record, solidcore-record's JSON), the fix set, the case's expected values and the rig's
//! own result. A case whose result no kernel operation alone accounts for is written too, with
//! the reasons, so nothing is dropped silently.
@@ -98,6 +98,32 @@ impl<'a> Trace<'a> {
}
}
/// The operations `ends` need, in the order they ran: each body's maker, and its operands' makers
/// in turn, back to bodies made from records. Operands made by nothing traced are listed in
/// `unknown`.
fn chain(t: &Trace<'_>, ends: &[u64], unknown: &mut BTreeSet<u64>) -> Vec<usize> {
let mut need = BTreeSet::new();
let mut todo: Vec<u64> = ends.to_vec();
while let Some(id) = todo.pop() {
if t.imports.contains_key(&id) {
continue;
}
match t.producer.get(&id) {
Some(&i) => {
if need.insert(i)
&& let Some(c) = t.calls.get(i)
{
todo.extend(c.1.iter().copied());
}
}
None => {
unknown.insert(id);
}
}
}
need.into_iter().collect()
}
/// The entry of case `dir`, run with result `out`, into `root/<area>/<id>.json`, and the fix set
/// into `root/fixes.json`.
pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<(), String> {
@@ -194,18 +220,14 @@ pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<()
}
let mut unknown = BTreeSet::new();
let mut available: BTreeSet<u64> = t.imports.keys().copied().collect();
for (_, inputs, _, outputs) in &t.calls {
unknown.extend(inputs.iter().filter(|id| !available.contains(id)).copied());
if let Ok(made) = outputs {
available.extend(made.iter().copied());
}
}
unknown.extend(ends.iter().filter(|id| !available.contains(id)).copied());
let mut order = chain(&t, &ends, &mut unknown);
if !unknown.is_empty() {
why_not.push(format!("operands made by nothing traced: {unknown:?}"));
}
// Operand records: every body a call takes, or the case ends with, that came in as a record.
if let Some(i) = failed_call.filter(|i| !order.contains(i)) {
order.push(i);
}
// Operand records: every body the chain takes, or the case ends with, that came in as a record.
let mut bodies = BTreeMap::new();
let given = |id: u64, bodies: &mut BTreeMap<String, Value>| {
if let Some((record, trusted, splines)) = t.imports.get(&id) {
@@ -213,21 +235,14 @@ pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<()
}
};
let mut calls: Vec<Value> = Vec::new();
for (i, (op, inputs, params, outputs)) in t.calls.iter().enumerate() {
for (op, inputs, params, outputs) in order.iter().filter_map(|&i| t.calls.get(i)) {
for &id in inputs.iter() {
given(id, &mut bodies);
}
let mut c = json!({"op": op, "inputs": inputs.iter().map(|i| format!("b{i}")).collect::<Vec<_>>(), "params": params});
match outputs {
Ok(made) => c["outputs"] = json!(made.iter().map(|i| format!("b{i}")).collect::<Vec<_>>()),
Err(e) => {
c["error"] = json!(e);
// The application handled this refusal. Replay must continue to any later
// calls and grade the final bodies, even when this was the last attempt.
if failed_call != Some(i) {
c["continued"] = json!(true);
}
}
Err(e) => c["error"] = json!(e),
}
calls.push(c);
}
@@ -275,134 +290,6 @@ pub fn write(root: &Path, dir: &str, out: &Value, capture: Capture) -> Result<()
mod tests {
use super::*;
fn empty_record() -> Brep {
Brep { version: 4, tolerance: 1e-6, regions: Vec::new(), shells: Vec::new(), faces: Vec::new(), edges: Vec::new(), vertices: Vec::new() }
}
fn call(op: &'static str, inputs: &[u64], outputs: Result<Vec<u64>, String>) -> Event {
Event::Call { op, inputs: inputs.to_vec(), params: json!({}), outputs }
}
fn exported_case(name: &str, events: Vec<Event>, ran: Result<Vec<(String, Brep)>, String>) -> Value {
let root = std::env::temp_dir().join(format!("solidcraft-export-{}-{name}", std::process::id()));
let dir = root.join("tests/kernel-cases/traces").join(name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("expected.json"), "{}").unwrap();
let output = root.join("export");
write(&output, dir.to_str().unwrap(), &json!({"status": "pass"}), Capture { events, ran: Some(ran) }).unwrap();
let entry = output.join("traces").join(format!("{name}.json"));
serde_json::from_str(&std::fs::read_to_string(entry).unwrap()).unwrap()
}
#[test]
fn discarded_tool_construction_is_exported_in_execution_order() {
let record = empty_record();
let e = exported_case(
"discarded-tool",
vec![call("extrude", &[], Ok(vec![1])), call("extrude", &[], Ok(vec![2])), Event::Export { id: 1, record: Box::new(record.clone()) }],
Ok(vec![("Part".into(), record)]),
);
assert_eq!(e["calls"].as_array().unwrap().len(), 2);
assert_eq!(e["calls"][0]["outputs"], json!(["b1"]));
assert_eq!(e["calls"][1]["outputs"], json!(["b2"]));
assert_eq!(e["traced_ops"], json!(["extrude", "extrude"]));
}
#[test]
fn handled_refusals_are_exported_without_becoming_terminal() {
let record = empty_record();
let e = exported_case(
"handled-refusal",
vec![
call("extrude", &[], Ok(vec![1])),
call("fillet", &[1], Err("too large".into())),
call("fillet", &[1], Ok(vec![2])),
Event::Export { id: 2, record: Box::new(record.clone()) },
],
Ok(vec![("Part".into(), record)]),
);
assert_eq!(e["calls"].as_array().unwrap().len(), 3);
assert_eq!(e["calls"][1]["error"], "too large");
assert_eq!(e["calls"][1]["continued"], true);
}
#[test]
fn a_handled_last_attempt_keeps_the_retained_body_as_the_result() {
let record = empty_record();
let e = exported_case(
"handled-last-attempt",
vec![
call("extrude", &[], Ok(vec![1])),
call("fillet", &[1], Err("too large".into())),
Event::Export { id: 1, record: Box::new(record.clone()) },
],
Ok(vec![("Part".into(), record)]),
);
assert_eq!(e["calls"][1]["continued"], true);
assert_eq!(e["result"], json!([{"body": "Part", "made": "b1"}]));
assert_eq!(e["operation_expressible"], true);
}
#[test]
fn the_final_refusal_still_ends_replay() {
let e = exported_case(
"terminal-refusal",
vec![
call("extrude", &[], Ok(vec![1])),
call("fillet", &[1], Err("too large".into())),
call("fillet", &[1], Err("no room".into())),
call("copy", &[1], Ok(vec![2])),
],
Err("no room".into()),
);
assert_eq!(e["calls"].as_array().unwrap().len(), 4);
assert_eq!(e["calls"][1]["continued"], true);
assert!(e["calls"][2].get("continued").is_none());
assert_eq!(e["outcome"]["kernel_error"], "no room");
}
#[test]
fn discarded_operations_still_require_their_operand_records() {
let record = empty_record();
let e = exported_case(
"missing-tool-operand",
vec![call("extrude", &[], Ok(vec![1])), call("copy", &[99], Ok(vec![2])), Event::Export { id: 1, record: Box::new(record.clone()) }],
Ok(vec![("Part".into(), record)]),
);
assert_eq!(e["operation_expressible"], false);
assert!(e["why_not"].as_array().unwrap().iter().any(|why| why.as_str().unwrap().contains("99")));
}
#[test]
fn discarded_operations_keep_their_imported_operands() {
let record = empty_record();
let e = exported_case(
"imported-tool-operand",
vec![
call("extrude", &[], Ok(vec![1])),
Event::Import { id: 9, record: Box::new(record.clone()), trusted: false, keeping_splines: true },
call("copy", &[9], Ok(vec![2])),
Event::Export { id: 1, record: Box::new(record.clone()) },
],
Ok(vec![("Part".into(), record.clone())]),
);
assert_eq!(e["bodies"]["b9"], json!({"record": record, "trusted": false, "keeping_splines": true}));
assert_eq!(e["calls"][1]["inputs"], json!(["b9"]));
assert_eq!(e["operation_expressible"], true);
}
#[test]
fn a_later_output_cannot_supply_an_earlier_discarded_operation() {
let record = empty_record();
let e = exported_case(
"future-tool-operand",
vec![call("copy", &[9], Ok(vec![2])), call("extrude", &[], Ok(vec![9])), Event::Export { id: 9, record: Box::new(record.clone()) }],
Ok(vec![("Part".into(), record)]),
);
assert_eq!(e["operation_expressible"], false);
assert!(e["why_not"].as_array().unwrap().iter().any(|why| why.as_str().unwrap().contains("9")));
}
#[test]
fn the_area_is_the_path_under_kernel_cases() {
assert_eq!(area_and_id("tests/kernel-cases/booleans/1894-x").unwrap(), ("booleans".into(), "1894-x".into()));
+27 -79
View File
@@ -18,7 +18,7 @@
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! [--param-check off|warn|strict] [--compact]
//! [--param-check off|warn|strict]
//! MCP server on stdio (docs/mcp.md)
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
@@ -62,9 +62,9 @@ struct Command {
}
/// `run`'s lines in the usage.
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n [--param-check off|warn|strict] [--quality standard|fine|finest]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--quality: how closely an STL, OBJ or 3MF --out follows curved faces, as file.export's quality: a chord of\n 2e-4, 5e-5 or 1e-5 of each body's size, never under 1 µm, default standard)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
/// `eval`'s lines in the usage.
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] [--validation each_step|each_feature|off] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile, --profile-out and --validation as for run)\n";
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
/// `script`'s lines in the usage.
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
/// `bench-open`'s lines in the usage.
@@ -91,7 +91,7 @@ const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [-
const USAGE_FUZZ: &str =
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
/// `mcp`'s lines in the usage.
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu] [--compact]\n (--compact, SOLIDCRAFT_MCP_COMPACT=1 or the app's Preferences › AI (with --connect): compact tool output for\n agents: inspect_design summarises sketches and takes `sketches` by name and a `bodies` filter, and tool JSON\n is not pretty-printed; off by default)\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.sc|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
/// `crashes`'s lines in the usage.
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
/// `licences`'s lines in the usage.
@@ -154,7 +154,7 @@ fn main() -> ExitCode {
}
};
if let Err(e) = max_file_size(&mut args) {
solidcraft_engine::note!("error: {e}");
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
let r = match args.first().map(String::as_str) {
@@ -235,7 +235,7 @@ fn main() -> ExitCode {
match r {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
solidcraft_engine::note!("error: {e}");
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
@@ -318,19 +318,6 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
/// the session keeps.
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
let s = read_session(path, check, validation)?;
// A newer SolidCraft's file opens all the same; stderr says so, stdout stays the caller's.
for warning in s.doc.newer_file.iter().flat_map(|file| file.warnings()) {
eprintln!("{path}: warning: {warning}");
}
Ok(s)
}
fn read_session(
path: &str,
check: solidcraft_engine::schema::ParamCheck,
validation: solidcraft_engine::kernel::Validation,
) -> Result<Session, String> {
let mut s = Session::default();
s.param_check = check;
s.set_validation(validation);
@@ -364,7 +351,7 @@ fn run_script_warning(s: &mut Session, path: &str, script: &Value) -> Result<Vec
/// Step `step`'s `param_warnings` on stderr, one per line, named after the script at `path`.
fn print_param_warnings(path: &str, step: usize, r: &Value) {
for w in r.get("param_warnings").and_then(Value::as_array).into_iter().flatten() {
solidcraft_engine::note!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
eprintln!("{path}: step {}: {}", step + 1, w.as_str().unwrap_or_default());
}
}
@@ -451,12 +438,11 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
known_args(
"run",
args,
&["--in", "--out", "--save", "--json-out", "--param-check", "--profile-out", "--validation", "--quality"],
&["--in", "--out", "--save", "--json-out", "--param-check", "--profile-out", "--validation"],
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
true,
)?;
let validation = validation_flag("run", args)?;
let quality = export_quality("run", args)?;
let base = flag(args, "--in");
let path = positional(args);
if path.is_none() && base.is_none() {
@@ -498,17 +484,11 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
// A feature that fails leaves the bodies as they were before it (a parameter change can
// break a feature without failing the command that made it): writing them would hand out
// the wrong part with success, so fail the run unless asked to write it anyway.
// A feature only a newer SolidCraft can build isn't a broken design: saving the design keeps it
// and hands out no part. Exporting bodies without it would, so that still stops.
if !args.iter().any(|a| a == "--allow-errors") {
timeline_ok(&s, flags(args, "--out").is_empty())?;
timeline_ok(&s)?;
}
for out in flags(args, "--out") {
let mut params = json!({"path": out});
if let Some(q) = quality {
params["quality"] = json!(q);
}
s.execute("file.export", &params).map_err(|e| e.to_string())?;
s.execute("file.export", &json!({"path": out})).map_err(|e| e.to_string())?;
}
if let Some(p) = flag(args, "--save") {
s.execute("file.save_as", &json!({"path": p})).map_err(|e| e.to_string())?;
@@ -549,20 +529,6 @@ fn open_validating(
}
}
/// `--quality standard|fine|finest`: how closely the meshes `--out` writes (STL, OBJ, 3MF)
/// follow curved faces, as `file.export`'s `quality` (#12270). Checked before anything runs;
/// given twice it is refused, since only one quality can apply.
fn export_quality<'a>(cmd: &str, args: &'a [String]) -> Result<Option<&'a str>, String> {
if flags(args, "--quality").len() > 1 {
return Err(format!("{cmd}: --quality is given more than once"));
}
match flag(args, "--quality") {
Some(q) if solidcraft_engine::io::MeshQuality::from_name(q).is_some() => Ok(Some(q)),
Some(q) => Err(format!("{cmd}: --quality must be standard, fine or finest, not `{q}`")),
None => Ok(None),
}
}
/// `--validation MODE` (`diagnostics.validation`): the default, each_feature, when not given.
fn validation_flag(cmd: &str, args: &[String]) -> Result<solidcraft_engine::kernel::Validation, String> {
match flag(args, "--validation") {
@@ -595,12 +561,8 @@ fn known_args(cmd: &str, args: &[String], valued: &[&str], switches: &[&str], po
}
/// Every failing timeline feature, one per line, as an error; Ok when none fails.
fn timeline_ok(s: &Session, skip_newer_features: bool) -> Result<(), String> {
let newer = |id: u64| {
skip_newer_features && s.doc.feature(id).is_some_and(|f| matches!(f.kind, solidcraft_engine::doc::FeatureKind::NewerFileFeature { .. }))
};
let failed: Vec<String> =
s.model.results.iter().filter(|r| !newer(r.id)).filter_map(|r| r.error.as_ref().map(|e| format!(" {}: {e}", r.name))).collect();
fn timeline_ok(s: &Session) -> Result<(), String> {
let failed: Vec<String> = s.model.results.iter().filter_map(|r| r.error.as_ref().map(|e| format!(" {}: {e}", r.name))).collect();
if failed.is_empty() {
return Ok(());
}
@@ -611,15 +573,13 @@ fn timeline_ok(s: &Session, skip_newer_features: bool) -> Result<(), String> {
))
}
/// Runs `solidcraft-cli eval` (and its `inspect` alias), opening the input design or script with the requested `--validation` mode and reporting both `inspect.measure` and `document.inspect`.
fn cmd_eval(args: &[String]) -> Result<(), String> {
let json_out = json_out(args)?;
known_args("eval", args, &["--in", "--json-out", "--profile-out", "--validation"], &["--profile"], true)?;
let validation = validation_flag("eval", args)?;
let mut s = open_validating(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off, validation)?;
known_args("eval", args, &["--in", "--json-out", "--profile-out"], &["--profile"], true)?;
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
let mut out = json!({
let out = json!({
"body_count": m["body_count"],
"bodies": m["bodies"],
"total": m["total"],
@@ -627,10 +587,6 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
"params": d["params"],
"sketches": d["sketches"],
});
if let Some(file) = &s.doc.newer_file {
out["newer_file"] = file.report();
out["warnings"] = json!(file.warnings());
}
print_json(&out, json_out)
}
@@ -642,10 +598,10 @@ fn cmd_script(args: &[String]) -> Result<(), String> {
let out = flag(args, "--out");
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
for what in r["omitted"].as_array().into_iter().flatten().filter_map(Value::as_str) {
solidcraft_engine::note!("note: the script leaves out {what}");
eprintln!("note: the script leaves out {what}");
}
match out {
Some(out) => solidcraft_engine::note!("{out}: {} steps", r["steps"]),
Some(out) => eprintln!("{out}: {} steps", r["steps"]),
None => println!("{}", pretty(&r["script"])),
}
Ok(())
@@ -676,7 +632,7 @@ fn cmd_gpu(args: &[String]) -> Result<(), String> {
);
match s.lands_on {
Some(solidcraft_gpu::select::Landing::Cpu) => {
solidcraft_engine::note!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
eprintln!("warning: {}", solidcraft_engine::capture::CPU_WARNING);
Ok(())
}
Some(_) => Ok(()),
@@ -753,7 +709,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
if let Some(w) = solidcraft_engine::capture::warning(png.renderer) {
solidcraft_engine::note!("warning: {w}");
eprintln!("warning: {w}");
reply["warning"] = json!(w);
}
println!("{}", pretty(&reply));
@@ -836,15 +792,11 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
}
install_renderer();
let options = solidcraft_mcp::Options::from_args_and_env(args).map_err(|e| format!("mcp: {e}"))?;
if options.compact {
eprintln!("solidcraft mcp: compact output (inspect_design summarises sketches; tool JSON is not pretty-printed)");
}
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
if let Some(s) = &scope {
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
solidcraft_engine::note!(
eprintln!(
"solidcraft mcp: files limited to {} and the folders of designs opened or saved here (--allow-path DIR adds one, --allow-any-path lifts the limit)",
roots.join(", ")
);
@@ -852,7 +804,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
.and_then(|p| std::path::Path::new(p).parent())
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
solidcraft_engine::note!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
}
}
// How calls that break a command's parameter contract are treated, headless or bridged.
@@ -866,7 +818,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
// Serve without the app too: the tools connect once it runs, and say it isn't until then.
let mut remote = solidcraft_mcp::Remote::new(addr, flag(args, "--token")).with_param_check(param_check);
if let Err(e) = remote.try_connect() {
solidcraft_engine::note!(
eprintln!(
"solidcraft mcp: no SolidCraft app at {addr} yet ({e}); start it with `solidcraft --control {port}` \
(or SOLIDCRAFT_CONTROL_PORT={port}), tool calls connect once it runs"
);
@@ -883,9 +835,9 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
(None, None) => {
let auto = !args.iter().any(|a| a == "--headless");
if auto && let Some(port) = discover_app() {
solidcraft_engine::note!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
let remote = solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token")).with_param_check(param_check);
return serve_mcp(Box::new(remote), scope, options);
return serve_mcp(Box::new(remote), scope);
}
let mut headless = solidcraft_mcp::Headless::default();
headless.session.param_check = param_check;
@@ -900,17 +852,13 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
}
}
};
serve_mcp(backend, scope, options)
serve_mcp(backend, scope)
}
fn serve_mcp(
backend: Box<dyn solidcraft_mcp::Backend>,
scope: Option<solidcraft_engine::io::vfs::PathScope>,
options: solidcraft_mcp::Options,
) -> Result<(), String> {
fn serve_mcp(backend: Box<dyn solidcraft_mcp::Backend>, scope: Option<solidcraft_engine::io::vfs::PathScope>) -> Result<(), String> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
solidcraft_mcp::Server::new(backend).with_path_scope(scope).with_options(options).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
}
/// The port of the running desktop app: the one its discovery file names, if something listens
-23
View File
@@ -188,26 +188,3 @@ fn max_file_size_raises_the_limit_and_refuses_bad_sizes() {
assert!(!output.status.success() && !stderr.contains("too large"), "{stderr}");
let _ = std::fs::remove_dir_all(&dir);
}
/// #10111: a line for a stderr whose reader has gone (the pipe closed before it, as an agent may
/// leave the MCP server's, and the app that one starts) is dropped, and the command ends as it
/// would with stderr open: here a script that isn't there fails with 1, where `eprintln!`
/// panicked with "failed printing to stderr" (101).
#[test]
fn a_closed_stderr_does_not_stop_the_cli() {
for _ in 0..5 {
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["run", "no-such-script.json"])
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.unwrap();
drop(child.stderr.take());
assert_eq!(child.wait().unwrap().code(), Some(1));
}
// The old name's alias writes its deprecation notice first: an agent still configured with
// `solvecraft-cli mcp` must get the server, not that panic.
let mut child = Command::new(env!("CARGO_BIN_EXE_solvecraft-cli")).arg("--help").stdout(Stdio::null()).stderr(Stdio::piped()).spawn().unwrap();
drop(child.stderr.take());
assert_eq!(child.wait().unwrap().code(), Some(0));
}
-48
View File
@@ -1,48 +0,0 @@
//! `mcp --compact` and `SOLIDCRAFT_MCP_COMPACT` turn on compact tool output; without them the
//! server's tools are as they always were.
use std::io::Write;
use std::process::{Command, Stdio};
use serde_json::{Value, json};
/// `inspect_design`'s input schema as `solidcraft-cli mcp` with `args` and `env` lists it, or
/// the server's stderr when it does not start.
fn inspect_schema(args: &[&str], env: Option<&str>) -> Result<Value, String> {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
// --headless: never bridge to (and read the preference of) an app the developer has running.
cmd.args(["mcp", "--headless", "--allow-any-path"]).args(args).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
match env {
Some(v) => cmd.env("SOLIDCRAFT_MCP_COMPACT", v),
None => cmd.env_remove("SOLIDCRAFT_MCP_COMPACT"),
};
let mut child = cmd.spawn().unwrap();
{
let mut stdin = child.stdin.take().unwrap();
// The server may refuse to start and close stdin first.
let _ = writeln!(stdin, "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/list"}));
}
let out = child.wait_with_output().unwrap();
if !out.status.success() {
return Err(String::from_utf8_lossy(&out.stderr).into_owned());
}
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
let tools = reply["result"]["tools"].as_array().cloned().unwrap_or_default();
Ok(tools.into_iter().find(|t| t["name"] == "inspect_design").map(|t| t["inputSchema"].clone()).unwrap_or(Value::Null))
}
#[test]
fn compact_output_is_off_unless_asked() {
let plain = inspect_schema(&[], None).unwrap();
assert_eq!(plain["properties"]["sketches"]["type"], "boolean", "{plain}");
assert!(plain["properties"].get("bodies").is_none(), "{plain}");
assert_eq!(inspect_schema(&[], Some("0")).unwrap(), plain);
for (args, env) in [(&["--compact"][..], None), (&[][..], Some("1")), (&[][..], Some("true"))] {
let compact = inspect_schema(args, env).unwrap();
assert!(compact["properties"]["bodies"].is_object(), "{args:?} {env:?}: {compact}");
}
let e = inspect_schema(&[], Some("maybe")).unwrap_err();
assert!(e.contains("SOLIDCRAFT_MCP_COMPACT"), "{e}");
}
+13 -34
View File
@@ -60,25 +60,15 @@ fn mcp_connect_serves_before_the_app_runs_and_reaches_it_once_it_does() {
let listener = TcpListener::bind(&addr).unwrap();
let app = std::thread::spawn(move || {
let (stream, _) = listener.accept().unwrap();
let mut lines = BufReader::new(stream.try_clone().unwrap());
loop {
let mut line = String::new();
lines.read_line(&mut line).unwrap();
let req: Value = serde_json::from_str(&line).unwrap();
// The server first asks for the agents' output options (the compact preference: off).
let options = req["method"] == "mcp.options";
let reply = if req["token"] != json!(token) {
json!({"id": req["id"], "ok": false, "error": format!("unauthorized: token {}", req["token"])})
} else if options {
json!({"id": req["id"], "ok": true, "result": {"compact": false}})
} else {
json!({"id": req["id"], "ok": true, "result": {"bodies": [], "method": req["method"]}})
};
writeln!(&stream, "{reply}").unwrap();
if !options {
break;
}
}
let mut line = String::new();
BufReader::new(stream.try_clone().unwrap()).read_line(&mut line).unwrap();
let req: Value = serde_json::from_str(&line).unwrap();
let reply = if req["token"] == json!(token) {
json!({"id": req["id"], "ok": true, "result": {"bodies": [], "method": req["method"]}})
} else {
json!({"id": req["id"], "ok": false, "error": format!("unauthorized: token {}", req["token"])})
};
writeln!(&stream, "{reply}").unwrap();
});
let r = ask(4, "tools/call", measure);
assert_ne!(r["result"]["isError"], true, "{r}");
@@ -121,21 +111,10 @@ fn mcp_without_arguments_finds_the_running_app() {
break (stream, line);
}
};
let mut lines = BufReader::new(stream.try_clone().unwrap());
let mut line = line;
// The server first asks for the agents' output options (the compact preference: off).
loop {
let req: Value = serde_json::from_str(&line).unwrap();
let ok = req["token"] == json!(token);
let options = req["method"] == "mcp.options";
let result = if options { json!({"compact": false}) } else { json!({"bodies": [], "method": req["method"]}) };
writeln!(&stream, "{}", json!({"id": req["id"], "ok": ok, "result": result, "error": "unauthorized"})).unwrap();
if !options {
break;
}
line.clear();
lines.read_line(&mut line).unwrap();
}
let req: Value = serde_json::from_str(&line).unwrap();
let ok = req["token"] == json!(token);
let reply = json!({"id": req["id"], "ok": ok, "result": {"bodies": [], "method": req["method"]}, "error": "unauthorized"});
writeln!(&stream, "{reply}").unwrap();
});
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
.args(["mcp", "--allow-any-path"])
+22 -92
View File
@@ -67,9 +67,6 @@ fn run_rejects_invalid_options_before_script_execution_or_output_writes() {
(&["--out", "--quiet"][..], "--out needs a value"),
(&["--ouut", "part.step"][..], "doesn't take --ouut"),
(&["--unknown"][..], "doesn't take --unknown"),
(&["--quality"][..], "--quality needs a value"),
(&["--quality", "best"][..], "--quality must be standard, fine or finest, not `best`"),
(&["--quality", "standard", "--quality", "best"][..], "--quality is given more than once"),
];
for existing in [false, true] {
let saved = dir.0.join(format!("{existing}.solidcraft"));
@@ -162,7 +159,7 @@ fn cli_loads_both_design_encodings_by_contents() {
}
/// #1369: a pending concept that did not land in the numbered release must not be silently
/// dropped on open/resave, in either encoding: it opens with a warning, and saving keeps it.
/// dropped on open/resave, in either encoding or before replacing an existing destination.
#[test]
fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
use solidcraft_engine::{Session, doc, io};
@@ -176,29 +173,34 @@ fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
let source = dir.0.join(format!("experimental-{version}.{extension}"));
std::fs::write(&source, &bytes).unwrap();
let mut session = Session::default();
// It opens, and the reply says what it is.
let reply = session.execute("doc.open", &json!({"path": source})).unwrap();
let notice = reply["newer_file"]["notice"].as_str().unwrap().to_string();
assert!(notice.contains("experimental-card") && notice.starts_with("Saved by a newer SolidCraft"), "{notice}");
assert_eq!(reply["warnings"][0], notice.as_str());
assert_eq!(session.doc.name, "Experimental");
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
let original = session.doc.clone();
let original_path = session.path.clone();
let undo = session.undo.len();
let dirty = session.is_dirty();
let err = session.execute("doc.open", &json!({"path": source})).unwrap_err().to_string();
assert!(err.contains("experimental-card") && err.contains("update SolidCraft"), "{err}");
assert_eq!(session.doc, original);
assert_eq!(session.path, original_path);
assert_eq!(session.undo.len(), undo);
assert_eq!(session.is_dirty(), dirty);
for existing in [false, true] {
let destination = dir.0.join(format!("output-{version}-{extension}-{existing}.{extension}"));
let backup = io::backup_path(&destination);
if existing {
std::fs::write(&destination, b"existing design").unwrap();
std::fs::write(&backup, b"existing backup").unwrap();
}
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
assert_eq!(output.status.code(), Some(1));
let error = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(0), "{error}");
assert!(error.contains("warning: Saved by a newer SolidCraft") && error.contains("experimental-card"), "{error}");
// Saved, it still says it has the change, and opens the same way.
let saved = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
assert!(saved.format_pending.iter().any(|name| name == "experimental-card"));
assert!(saved.newer_file.is_some());
assert!(error.contains("experimental-card") && error.contains("update SolidCraft"), "{error}");
if existing {
assert_eq!(std::fs::read(&backup).unwrap(), b"existing design");
assert_eq!(std::fs::read(&destination).unwrap(), b"existing design");
assert_eq!(std::fs::read(&backup).unwrap(), b"existing backup");
} else {
assert!(!destination.exists() && !backup.exists());
}
assert_eq!(std::fs::read(&source).unwrap(), bytes);
}
@@ -206,43 +208,6 @@ fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
}
}
/// `eval` and `run` on a design one format newer, with a feature kind this version doesn't have:
/// the rest builds, the reply names it, and the saved file still holds it, under the file's number.
#[test]
fn the_cli_opens_a_newer_design_and_saving_keeps_what_it_cant_read() {
use solidcraft_engine::{Session, doc, io};
let dir = Scratch::new();
let mut session = Session::default();
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
let mut file: Value = serde_json::from_slice(&io::write_design(&session.doc)).unwrap();
let newer = doc::format::format_string(doc::format::FORMAT + 1);
file["format"] = json!(newer);
let future = json!({"id": 900, "name": "Lattice1", "type": "gyroid_lattice", "cell": 2.5});
file["features"].as_array_mut().unwrap().push(future.clone());
let source = dir.0.join("newer.json");
std::fs::write(&source, file.to_string()).unwrap();
let output = cli().args(["eval"]).arg(&source).output().unwrap();
let error = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(0), "{error}");
assert!(error.contains("warning: Saved by a newer SolidCraft") && error.contains("Lattice1"), "{error}");
let reply: Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(reply["body_count"], 1);
assert!(reply["newer_file"]["not_understood"][0].as_str().unwrap().contains("gyroid_lattice"), "{reply}");
assert!(reply["warnings"][0].as_str().unwrap().starts_with("Saved by a newer SolidCraft"), "{reply}");
for extension in ["json", "sc"] {
let destination = dir.0.join(format!("saved.{extension}"));
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
assert_eq!(output.status.code(), Some(0), "{}", String::from_utf8_lossy(&output.stderr));
let saved = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
assert_eq!(saved.format, newer, "never a lower number");
let again: Value = serde_json::from_slice(&io::write_design(&saved)).unwrap();
assert_eq!(again["features"].as_array().unwrap().last().unwrap(), &future);
}
}
#[test]
fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
use solidcraft_engine::{doc, io};
@@ -291,38 +256,6 @@ fn results_still_save_and_export_the_script_once() {
}
}
/// The triangles of a binary STL: the count after its 80-byte header.
fn stl_triangles(path: &Path) -> u32 {
let bytes = std::fs::read(path).unwrap();
u32::from_le_bytes(bytes[80..84].try_into().unwrap())
}
/// #12270: `run --out part.stl --quality fine` writes the mesh at that quality, as
/// `file.export`'s `quality` does, without a script to say so.
#[test]
fn run_exports_meshes_at_the_quality_asked_for() {
let dir = Scratch::new();
let script = dir.0.join("cylinder.json");
std::fs::write(&script, json!({"commands": [{"command": "solid.cylinder", "params": {"diameter": 40, "height": 10}}]}).to_string()).unwrap();
let export = |name: &str, quality: Option<&str>| {
let stl = dir.0.join(name);
let mut command = cli();
command.arg("run").arg(&script).arg("--quiet").arg("--out").arg(&stl);
if let Some(q) = quality {
command.args(["--quality", q]);
}
let output = command.output().unwrap();
assert!(output.status.success(), "{quality:?}: {}", String::from_utf8_lossy(&output.stderr));
stl_triangles(&stl)
};
let default = export("default.stl", None);
let standard = export("standard.stl", Some("standard"));
let fine = export("fine.stl", Some("fine"));
let finest = export("finest.stl", Some("finest"));
assert_eq!(default, standard);
assert!(standard < fine && fine < finest, "standard {standard}, fine {fine}, finest {finest}");
}
#[test]
fn results_with_base_design_and_quiet_still_report_and_save() {
let dir = Scratch::new();
@@ -527,16 +460,13 @@ fn snapshot_runs_its_script_on_the_in_design() {
}
}
/// An unknown command's error suggests the id that was meant.
/// An unknown command's error suggests the id that was meant first; other close ids (such as
/// `drawing.open`) may follow it.
#[test]
fn unknown_command_suggests_the_closest_id() {
let output = cli().args(["exec", "file.open"]).output().unwrap();
assert_eq!(output.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open` or `panel.open`?"),
"{}",
String::from_utf8_lossy(&output.stderr)
);
assert!(String::from_utf8_lossy(&output.stderr).contains("did you mean `doc.open`"), "{}", String::from_utf8_lossy(&output.stderr));
}
/// #1833: `--timings` reports each command's time instead of the measurement, `--profile` adds
+1 -1
View File
@@ -25,7 +25,6 @@ solidcraft-engine = { workspace = true }
solidcraft-ui-egui = { workspace = true }
egui = { workspace = true }
log = { workspace = true }
serde_json = { workspace = true }
url = "2"
percent-encoding = "2"
@@ -34,6 +33,7 @@ eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
serde_json = { workspace = true }
web-sys = { version = "0.3", features = [
"Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "Location", "Navigator", "StorageManager",
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemGetDirectoryOptions", "FileSystemWritableFileStream",
+10 -1
View File
@@ -9,7 +9,7 @@
<meta property="og:site_name" content="SolidCraft">
<meta property="og:title" content="SolidCraft: try it in your browser">
<meta property="og:description" content="Open-source parametric CAD built in Rust. Desktop, browser, and MCP for AI agents.">
<meta property="og:url" content="https://app.solidcraft.io/">
<meta property="og:url" content="https://solidcraft.ai/">
<meta property="og:image" content="https://docs.solidcraft.io/og.png">
<meta property="og:image:width" content="1280">
<meta property="og:image:height" content="640">
@@ -26,6 +26,15 @@
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
<!-- A drawing's Arabic, Korean and Chinese text: each fetched once, when a drawing needs it. -->
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/NotoSansArabic-Regular.ttf" data-target-path="fonts/noto-sans-arabic" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-arabic/OFL.txt" data-target-path="fonts/noto-sans-arabic" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/NotoSansKR-Regular.otf" data-target-path="fonts/noto-sans-kr" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-kr/OFL.txt" data-target-path="fonts/noto-sans-kr" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/NotoSansSC-Regular.otf" data-target-path="fonts/noto-sans-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-sc/OFL.txt" data-target-path="fonts/noto-sans-sc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/NotoSansTC-Regular.otf" data-target-path="fonts/noto-sans-tc" />
<link data-trunk rel="copy-file" href="../../assets/fonts/noto-sans-tc/OFL.txt" data-target-path="fonts/noto-sans-tc" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
+1 -4
View File
@@ -553,7 +553,7 @@ mod web {
/// Open a design remembered from an earlier session (a recent one, after a reload): ask once
/// for access to the file (call in the click), read it into the engine's files and queue it
/// to open. A file that is gone is forgotten and dropped from the recent designs.
pub fn restore(files: &Rc<Files>, path: &str, on_opened: Option<Box<dyn FnOnce()>>) {
pub fn restore(files: &Rc<Files>, path: &str) {
let Some((id, name)) = parse(path) else { return };
let (files, path, id, name) = (files.clone(), path.to_string(), id.to_string(), name.to_string());
spawn_local(async move {
@@ -568,9 +568,6 @@ mod web {
files.opened.borrow_mut().push(path);
files.wake();
touch(&id).await;
if let Some(done) = on_opened {
done();
}
}
Err(e) => {
let gone = e.contains("NotFoundError") || e.contains("could not be found") || e.contains("no longer remembers");
+5 -175
View File
@@ -5,8 +5,7 @@
//! and go out as downloads (exports, and "Download design"). Where the File System Access API
//! exists a design also lives on the computer's disk, saved and opened through the browser's file
//! dialogs (`disk.rs`). An autosave of unsaved work is kept in OPFS and offered again after a
//! reload or a crash. With "keep unsaved work in this browser" on, each open design's unsaved
//! changes are kept there instead, one copy per design (`keep.rs`, #10220).
//! reload or a crash.
use std::cell::{Cell, RefCell};
use std::rc::Rc;
@@ -14,7 +13,7 @@ use std::rc::Rc;
use js_sys::{Array, Function, Object, Reflect, Uint8Array};
use solidcraft_engine::io::vfs;
use crate::{disk, keep};
use crate::disk;
use wasm_bindgen::{JsCast as _, JsValue, closure::Closure};
use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::{
@@ -80,16 +79,6 @@ pub struct Files {
pub recovery: RefCell<Option<(String, Option<String>)>>,
/// The app's context, to wake it when a browser call finishes.
pub ctx: RefCell<Option<egui::Context>>,
/// This page's id: the copies it keeps are named after it (`keep::entry_id`).
pub keep_page: RefCell<String>,
/// Kept copies stored by this page, and the design revision each holds once its write ended.
pub kept: RefCell<std::collections::HashMap<String, Option<u64>>>,
/// Kept copies being written or removed now (one browser call at a time per copy).
pub keeping: RefCell<std::collections::HashSet<String>>,
/// When (ms) the last write of a copy failed, to wait before trying again (`keep::may_write`).
pub kept_failed: RefCell<std::collections::HashMap<String, f64>>,
/// Copies earlier pages left, newer than their design's last save: offered at start.
pub keep_offered: RefCell<Vec<keep::Entry>>,
}
impl Files {
@@ -299,9 +288,6 @@ pub fn start(files: &Rc<Files>) {
let _ = JsFuture::from(p).await;
});
}
let page = keep::page_id(js_sys::Date::now(), js_sys::Math::random());
hold_page_lock(&page);
*files.keep_page.borrow_mut() = page;
let files = files.clone();
spawn_local(async move {
match disk::known_ids().await {
@@ -314,7 +300,6 @@ pub fn start(files: &Rc<Files>) {
files.say("This page can't use the browser's file storage (it needs https or localhost): designs are kept in local storage, a few MB at most.", false);
}
let mut entries = Vec::new();
let kept = kept_copies(&all);
let has_recovery = all.iter().any(|(n, _, _)| n == RECOVERY);
let legacy = (!has_recovery).then(|| LEGACY_RECOVERY.into_iter().find(|l| all.iter().any(|(n, _, _)| n == l))).flatten();
for (name, bytes, modified) in all {
@@ -330,7 +315,7 @@ pub fn start(files: &Rc<Files>) {
*files.recovery.borrow_mut() = Some((n, meta["path"].as_str().map(str::to_string)));
continue;
}
if name != RECOVERY && keep::parse_name(&name).is_none() {
if name != RECOVERY {
entries.push(Entry { name: name.clone(), size: bytes.len() as u64, modified });
}
vfs::insert(&format!("{OPFS}{name}"), bytes);
@@ -341,7 +326,6 @@ pub fn start(files: &Rc<Files>) {
if !vfs::exists(&format!("{OPFS}{RECOVERY}")) {
*files.recovery.borrow_mut() = None;
}
sort_kept(&files, kept, &live_pages().await);
}
Err(e) => files.say(format!("browser storage: {e}"), true),
}
@@ -359,7 +343,7 @@ pub fn flush(files: &Rc<Files>) {
{
let mut e = files.entries.borrow_mut();
e.retain(|x| x.name != name);
if name != RECOVERY && name != RECOVERY_META && keep::parse_name(&name).is_none() {
if name != RECOVERY && name != RECOVERY_META {
e.push(Entry { name: name.clone(), size: bytes.len() as u64, modified: js_sys::Date::now() });
e.sort_by_key(|a| a.name.to_lowercase());
}
@@ -489,15 +473,7 @@ pub fn recovery_path() -> String {
/// A recovery file lives in OPFS, but its relative links belong to the original design's
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
prepare_copy(recovery_path(), original_path)
}
/// The same for a kept copy (#10220).
pub fn prepare_kept(id: &str, original_path: Option<&str>) -> Result<String, String> {
prepare_copy(format!("{OPFS}{}", keep::design_file(id)), original_path)
}
fn prepare_copy(path: String, original_path: Option<&str>) -> Result<String, String> {
let path = recovery_path();
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
solidcraft_engine::resolve_design_links(&mut doc, original_path);
@@ -670,149 +646,3 @@ pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput, theme: bool) {
});
}
}
/// Hold this page's browser lock while it runs, so other tabs know its kept copies are in use
/// (`keep.rs`). Browsers without the Web Locks API hold none; their copies are then offered by
/// any later page.
fn hold_page_lock(page: &str) {
let Some(locks) = web_locks() else { return };
let Ok(request) = Reflect::get(&locks, &"request".into()).and_then(|f| f.dyn_into::<Function>()) else { return };
// A promise that never settles: the lock is let go only when the page goes.
let hold = Closure::<dyn FnMut(JsValue) -> js_sys::Promise>::new(|_lock: JsValue| js_sys::Promise::new(&mut |_, _| {}));
let name = format!("{}{page}", keep::LOCK_PREFIX);
if let Err(e) = request.call2(&locks, &name.into(), hold.as_ref().unchecked_ref()) {
log::info!("keep: no page lock: {}", err(e));
}
// The callback lives as long as the page.
hold.forget();
}
fn web_locks() -> Option<JsValue> {
let nav = web_sys::window()?.navigator();
Reflect::get(&nav, &"locks".into()).ok().filter(|l| l.is_object())
}
/// The pages still open (in this browser profile) that hold their lock.
async fn live_pages() -> Vec<String> {
let query = async {
let locks = web_locks().ok_or("no Web Locks")?;
let f: Function = Reflect::get(&locks, &"query".into()).map_err(err)?.dyn_into().map_err(|_| "no query".to_string())?;
let p: js_sys::Promise = f.call0(&locks).map_err(err)?.dyn_into().map_err(|_| "no promise".to_string())?;
let state = JsFuture::from(p).await.map_err(err)?;
let held: Array = Reflect::get(&state, &"held".into()).map_err(err)?.dyn_into().map_err(|_| "no list".to_string())?;
Ok::<_, String>(
held.iter()
.take(10_000)
.filter_map(|l| Reflect::get(&l, &"name".into()).ok()?.as_string())
.filter_map(|n| n.strip_prefix(keep::LOCK_PREFIX).map(str::to_string))
.collect(),
)
};
query.await.unwrap_or_else(|e| {
log::info!("keep: open pages unknown ({e})");
Vec::new()
})
}
/// The kept copies among the stored files: (copy, when its design's file was last written), and
/// the ids of designs stored without their metadata.
struct Found {
copies: Vec<(keep::Entry, Option<f64>)>,
orphans: Vec<String>,
}
fn kept_copies(all: &[(String, Vec<u8>, f64)]) -> Found {
let modified = |path: &str| path.strip_prefix(OPFS).and_then(|n| all.iter().find(|(name, _, _)| name == n)).map(|(_, _, m)| *m);
let mut copies = Vec::new();
let mut designs = Vec::new();
for (name, bytes, _) in all {
match keep::parse_name(name) {
Some((id, keep::Part::Meta)) => {
if let Some(e) = keep::Entry::parse(id, bytes) {
let saved = e.path.as_deref().and_then(modified);
copies.push((e, saved));
}
}
Some((id, keep::Part::Design)) => designs.push(id.to_string()),
None => {}
}
}
// A copy is whole only with its design.
copies.retain(|(e, _)| designs.contains(&e.id));
let orphans = designs.into_iter().filter(|id| !copies.iter().any(|(e, _)| &e.id == id)).collect();
Found { copies, orphans }
}
/// Offer the copies earlier pages left that hold unsaved work, and remove those their design's
/// later save made stale (and parts without the rest), leaving open pages' copies alone.
fn sort_kept(files: &Rc<Files>, found: Found, live: &[String]) {
let mut offered = Vec::new();
let mut gone = Vec::new();
for (e, saved) in found.copies {
match keep::fate(&e, live, saved) {
keep::Fate::Offer => offered.push(e),
keep::Fate::Drop => gone.push(e.id),
keep::Fate::Leave => {}
}
}
gone.extend(found.orphans.into_iter().filter(|id| !keep::page_of(id).is_some_and(|p| live.iter().any(|l| l == p))));
keep::order_offered(&mut offered);
*files.keep_offered.borrow_mut() = offered;
for id in gone {
remove_kept(files, &id);
}
}
/// The path (in the engine's files) a kept copy opens from.
pub fn kept_path(id: &str) -> String {
format!("{OPFS}{}", keep::design_file(id))
}
/// Write the copy of a design with unsaved changes (`bytes`, described by `entry`) that holds
/// its revision `revision`. Its metadata goes last, so a copy is never offered half written.
pub fn write_kept(files: &Rc<Files>, entry: &keep::Entry, bytes: Vec<u8>, revision: u64) {
let id = entry.id.clone();
if !files.keeping.borrow_mut().insert(id.clone()) {
return;
}
files.kept.borrow_mut().entry(id.clone()).or_insert(None);
let meta = entry.to_json();
let files = files.clone();
spawn_local(async move {
let r = async {
write_file(&keep::design_file(&id), &bytes).await?;
write_file(&keep::meta_file(&id), meta.as_bytes()).await
};
match r.await {
Ok(()) => {
files.kept.borrow_mut().insert(id.clone(), Some(revision));
files.kept_failed.borrow_mut().remove(&id);
}
Err(e) => {
files.kept_failed.borrow_mut().insert(id.clone(), js_sys::Date::now());
files.say(format!("keeping unsaved work in this browser: {e}"), true)
}
}
files.keeping.borrow_mut().remove(&id);
files.wake();
});
}
/// Remove kept copy `id` (its design was saved, closed, discarded or restored).
pub fn remove_kept(files: &Rc<Files>, id: &str) {
let id = id.to_string();
if !files.keeping.borrow_mut().insert(id.clone()) {
return;
}
let _ = vfs::remove(&kept_path(&id));
files.kept_failed.borrow_mut().remove(&id);
let files = files.clone();
spawn_local(async move {
// The metadata first: what's left without it is never offered.
let _ = remove_file(&keep::meta_file(&id)).await;
let _ = remove_file(&keep::design_file(&id)).await;
files.kept.borrow_mut().remove(&id);
files.keeping.borrow_mut().remove(&id);
files.wake();
});
}
+2 -2
View File
@@ -23,9 +23,9 @@ fn prompt() {
let _ = js_sys::Reflect::set(&window, &OFFER.into(), &JsValue::NULL);
}
let shown = js_sys::Reflect::get(&offer, &"prompt".into())
.and_then(|f| f.dyn_into::<js_sys::Function>())
.and_then(|f| f.dyn_into::<js_sys::Function>().map_err(JsValue::from))
.and_then(|f| f.call0(&offer))
.and_then(|p| p.dyn_into::<js_sys::Promise>());
.and_then(|p| p.dyn_into::<js_sys::Promise>().map_err(JsValue::from));
match shown {
// Accepted or dismissed, the browser handles it; a refusal is only logged.
Ok(p) => wasm_bindgen_futures::spawn_local(async move {
-360
View File
@@ -1,360 +0,0 @@
//! Keep unsaved work in this browser (#10220): the decisions, in plain Rust (tested natively).
//!
//! With Preferences › General › "Keep unsaved work in this browser instead of asking when the tab
//! closes" on, each open design with unsaved changes has a copy in the browser's private storage:
//! `keep-<id>.sc` (the design) and `keep-<id>.json` (its name, its file and when it last
//! changed). The copy is written shortly after each change ([`due`]) and again when the page is
//! hidden, so it is already current when the tab closes; the page then doesn't ask through the
//! browser's "leave site?" box ([`asks_before_unload`]). A design that is saved, closed or
//! discarded has its copy removed. The next visit offers the copies newer than their design's
//! last save ([`fate`]) in SolidCraft's own window, answered with [`plan`].
//!
//! An id is `<page>-<n>`: the page that wrote it (each page load has its own) and the design's
//! number in that page. A page holds a browser lock named after it while it runs, so another tab
//! never offers, or removes, the copies of a page that is still open.
#![cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
use serde_json::{Value, json};
/// The start of every kept file's name.
pub const PREFIX: &str = "keep-";
/// The browser lock a page holds while it runs: this, then its page id.
pub const LOCK_PREFIX: &str = "solidcraft.keep.";
/// A copy is written once the design has been still this long (ms)…
pub const QUIET_MS: f64 = 800.0;
/// …or at the latest this long after the first change it doesn't hold yet (ms).
pub const MAX_WAIT_MS: f64 = 4000.0;
/// The most copies offered at once (the rest wait for the next visit).
pub const MAX_OFFERED: usize = 32;
const MAX_ID: usize = 64;
const MAX_NAME: usize = 256;
const MAX_PATH: usize = 4096;
/// Copies' metadata is small: a damaged or hostile one is not read.
pub const MAX_META_BYTES: usize = 64 * 1024;
/// Ids are short and safe in a file name: ASCII letters, digits and dashes, with a page part.
pub fn valid_id(id: &str) -> bool {
!id.is_empty() && id.len() <= MAX_ID && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') && page_of(id).is_some()
}
/// The page that wrote copy `id`.
pub fn page_of(id: &str) -> Option<&str> {
id.rsplit_once('-').map(|(page, _)| page).filter(|p| !p.is_empty())
}
/// This page's id, from the time it started (ms since 1970) and a random number in 0..1:
/// letters and digits only.
pub fn page_id(now_ms: f64, random: f64) -> String {
let now = if now_ms.is_finite() && now_ms > 0.0 { now_ms.min(1e15) as u64 } else { 0 };
let random = if random.is_finite() { (random.clamp(0.0, 1.0) * f64::from(0xff_ffff)) as u64 } else { 0 };
format!("{now:x}{random:06x}")
}
/// The copy of design `uid` (`Session::uid`) open in page `page`.
pub fn entry_id(page: &str, uid: u64) -> String {
format!("{page}-{uid}")
}
/// The stored file names of copy `id`: its design and its metadata.
pub fn design_file(id: &str) -> String {
format!("{PREFIX}{id}.{}", solidcraft_engine::io::DESIGN_EXT)
}
pub fn meta_file(id: &str) -> String {
format!("{PREFIX}{id}.json")
}
/// Which part of a copy a stored file is.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Part {
Design,
Meta,
}
/// A stored file's copy id and part; `None` for every other file.
pub fn parse_name(name: &str) -> Option<(&str, Part)> {
let rest = name.strip_prefix(PREFIX)?;
let (id, part) = if let Some(id) = rest.strip_suffix(".json") {
(id, Part::Meta)
} else {
(rest.strip_suffix(solidcraft_engine::io::DESIGN_EXT)?.strip_suffix('.')?, Part::Design)
};
valid_id(id).then_some((id, part))
}
/// A kept copy, as its metadata describes it.
#[derive(Clone, Debug, PartialEq)]
pub struct Entry {
pub id: String,
/// The design's name.
pub name: String,
/// Its file (`None`: never saved).
pub path: Option<String>,
/// When the design last changed (ms since 1970).
pub changed_at: f64,
}
impl Entry {
pub fn to_json(&self) -> String {
json!({"name": self.name, "path": self.path, "changed_at": self.changed_at}).to_string()
}
/// Read copy `id`'s metadata. Anything malformed, oversized or not finite is no copy.
pub fn parse(id: &str, bytes: &[u8]) -> Option<Entry> {
if !valid_id(id) || bytes.len() > MAX_META_BYTES {
return None;
}
let v: Value = serde_json::from_slice(bytes).ok()?;
let name = v.get("name")?.as_str()?.chars().take(MAX_NAME).collect::<String>();
let path = match v.get("path") {
None | Some(Value::Null) => None,
Some(p) => Some(p.as_str().filter(|p| !p.is_empty() && p.len() <= MAX_PATH)?.to_string()),
};
let changed_at = v.get("changed_at")?.as_f64().filter(|t| t.is_finite() && *t >= 0.0)?;
Some(Entry { id: id.to_string(), name, path, changed_at })
}
}
/// Whether a copy that last changed at `changed_at` holds work its design's file doesn't:
/// the file was last written at `saved_at` (ms since 1970; `None` or unknown: never saved, or
/// the time can't be read, so the copy is kept rather than lost).
pub fn newer_than_save(changed_at: f64, saved_at: Option<f64>) -> bool {
match saved_at {
Some(saved) if saved.is_finite() && saved > 0.0 => changed_at > saved,
_ => true,
}
}
/// What the next visit does with a copy it finds.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Fate {
/// Offer it: "Restore unsaved design…?".
Offer,
/// Its design was saved after it: remove it without asking.
Drop,
/// Its page is still open (another tab): leave it alone.
Leave,
}
/// The copy `entry`, whose design's file was last written at `saved_at`, while the pages
/// `live` are open.
pub fn fate(entry: &Entry, live: &[String], saved_at: Option<f64>) -> Fate {
if page_of(&entry.id).is_some_and(|p| live.iter().any(|l| l == p)) {
Fate::Leave
} else if newer_than_save(entry.changed_at, saved_at) {
Fate::Offer
} else {
Fate::Drop
}
}
/// Copies to offer first: the most recently changed, at most [`MAX_OFFERED`].
pub fn order_offered(entries: &mut Vec<Entry>) {
entries.sort_by(|a, b| b.changed_at.total_cmp(&a.changed_at).then_with(|| a.id.cmp(&b.id)));
entries.truncate(MAX_OFFERED);
}
/// Whether a design's copy is due to be written at `now`: the design last changed at
/// `last_change`, and the copy has lacked a change since `first_unwritten` (all ms).
pub fn due(now: f64, first_unwritten: f64, last_change: f64) -> bool {
now - last_change >= QUIET_MS || now - first_unwritten >= MAX_WAIT_MS
}
/// How long after a failed write of a design's copy it isn't tried again (ms).
pub const RETRY_MS: f64 = 30_000.0;
/// Whether a copy whose write last failed at `failed_at` (ms; `None`: it hasn't) may be written
/// at `now`: not before [`RETRY_MS`] has passed, so a full or denied store isn't hit every tick.
pub fn may_write(now: f64, failed_at: Option<f64>) -> bool {
failed_at.is_none_or(|t| !(now >= t) || now - t >= RETRY_MS)
}
/// Whether `wanted` is the design now open, given the open design's path.
pub fn is_open(open: Option<&str>, wanted: &str) -> bool {
open == Some(wanted)
}
/// Whether leaving the page asks through the browser's "leave site?" box: there is unsaved work,
/// and either the user still wants to be asked or it isn't all kept in the browser yet (with the
/// keep preference off it never is, so there the box always stays).
pub fn asks_before_unload(persists: bool, keep: bool, also_ask: bool, any_unsaved: bool, all_kept: bool) -> bool {
persists && any_unsaved && (also_ask || !(keep && all_kept))
}
/// An answer in the "Restore unsaved design?" window.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Answer {
/// Open the copy, with its unsaved changes, as the design it was (saving goes to its file).
Restore,
/// Open the design's file as last saved; the copy goes.
OpenSaved,
/// The copy goes.
Discard,
}
/// What an answer does.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Plan {
/// Open the copy (and, once it is open, give it back its name and file).
pub open_copy: bool,
/// Open this saved file.
pub open_saved: Option<String>,
/// Remove the copy.
pub drop_copy: bool,
}
/// What `answer` does to copy `entry`; `saved_exists`: its file can still be opened. `None`: the
/// answer isn't offered for it (Open saved version of a design never saved, or gone).
pub fn plan(answer: Answer, entry: &Entry, saved_exists: bool) -> Option<Plan> {
match answer {
Answer::Restore => Some(Plan { open_copy: true, open_saved: None, drop_copy: true }),
Answer::OpenSaved => {
let path = entry.path.clone().filter(|_| saved_exists)?;
Some(Plan { open_copy: false, open_saved: Some(path), drop_copy: true })
}
Answer::Discard => Some(Plan { open_copy: false, open_saved: None, drop_copy: true }),
}
}
/// The answers the window offers for `entry`, in order.
pub fn answers(entry: &Entry, saved_exists: bool) -> Vec<Answer> {
[Answer::Restore, Answer::OpenSaved, Answer::Discard].into_iter().filter(|a| plan(*a, entry, saved_exists).is_some()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: &str, changed_at: f64, path: Option<&str>) -> Entry {
Entry { id: id.into(), name: "Bracket".into(), path: path.map(str::to_string), changed_at }
}
#[test]
fn a_failed_write_waits_before_the_next_try() {
assert!(may_write(1000.0, None));
assert!(!may_write(1000.0 + RETRY_MS - 1.0, Some(1000.0)));
assert!(may_write(1000.0 + RETRY_MS, Some(1000.0)));
// A clock that went back doesn't block for good.
assert!(may_write(10.0, Some(1000.0)));
}
#[test]
fn a_design_counts_as_opened_only_at_its_own_path() {
assert!(is_open(Some("a.sc"), "a.sc"));
assert!(!is_open(Some("b.sc"), "a.sc"));
assert!(!is_open(None, "a.sc"));
}
#[test]
fn copies_are_named_by_page_and_design_and_nothing_else_reads_as_one() {
let page = page_id(1_700_000_000_000.0, 0.5);
assert!(page.chars().all(|c| c.is_ascii_hexdigit()));
let id = entry_id(&page, 7);
assert!(valid_id(&id));
assert_eq!(page_of(&id), Some(page.as_str()));
assert_eq!(parse_name(&design_file(&id)), Some((id.as_str(), Part::Design)));
assert_eq!(parse_name(&meta_file(&id)), Some((id.as_str(), Part::Meta)));
for other in ["part.sc", "recovery.sc", "recovery.json", "keep-.sc", "keep-x.sc", "keep-a/b-1.sc", "keep-a-1.step", "keep-a-1sc"] {
assert_eq!(parse_name(other), None, "{other}");
}
// Hostile clocks still give a usable id.
for (now, random) in [(f64::NAN, f64::NAN), (f64::INFINITY, -1.0), (-5.0, 9.0)] {
assert!(valid_id(&entry_id(&page_id(now, random), 1)));
}
}
#[test]
fn metadata_round_trips_and_hostile_metadata_is_no_copy() {
let e = entry("a1-3", 1234.0, Some("/opfs/Bracket.sc"));
assert_eq!(Entry::parse("a1-3", e.to_json().as_bytes()), Some(e.clone()));
let never_saved = entry("a1-4", 99.0, None);
assert_eq!(Entry::parse("a1-4", never_saved.to_json().as_bytes()), Some(never_saved));
for bad in [
"",
"[]",
r#"{"name":"x","changed_at":"soon"}"#,
r#"{"name":"x","changed_at":-1}"#,
r#"{"name":"x","changed_at":1e999}"#,
r#"{"name":"x","path":7,"changed_at":1}"#,
r#"{"name":"x","path":"","changed_at":1}"#,
r#"{"changed_at":1}"#,
] {
assert_eq!(Entry::parse("a1-3", bad.as_bytes()), None, "{bad}");
}
assert_eq!(Entry::parse("../x", e.to_json().as_bytes()), None);
assert_eq!(Entry::parse("a1-3", &vec![b' '; MAX_META_BYTES + 1]), None);
let long = json!({"name": "n".repeat(10_000), "changed_at": 1}).to_string();
assert_eq!(Entry::parse("a1-3", long.as_bytes()).map(|e| e.name.len()), Some(MAX_NAME));
}
#[test]
fn a_copy_newer_than_the_last_save_is_offered_an_older_one_dropped_an_open_pages_left() {
let e = entry("p1-1", 2000.0, Some("/opfs/Bracket.sc"));
// Changed after the file was last written: offered.
assert_eq!(fate(&e, &[], Some(1000.0)), Fate::Offer);
// Saved since (in this tab or another): nothing to offer.
assert_eq!(fate(&e, &[], Some(2000.0)), Fate::Drop);
assert_eq!(fate(&e, &[], Some(3000.0)), Fate::Drop);
// Never saved, or the save time can't be read: kept rather than lost.
for saved in [None, Some(0.0), Some(f64::NAN), Some(f64::INFINITY), Some(-4.0)] {
assert_eq!(fate(&e, &[], saved), Fate::Offer, "{saved:?}");
}
// Its page is still open in another tab: neither offered nor removed.
assert_eq!(fate(&e, &["p1".into()], Some(1000.0)), Fate::Leave);
assert_eq!(fate(&e, &["p1".into()], Some(3000.0)), Fate::Leave);
assert_eq!(fate(&e, &["p2".into()], Some(1000.0)), Fate::Offer);
}
#[test]
fn the_newest_copies_are_offered_first_and_at_most_a_windowful() {
let mut list: Vec<Entry> = (0..40).map(|i| entry(&format!("p-{i}"), f64::from(i), None)).collect();
order_offered(&mut list);
assert_eq!(list.len(), MAX_OFFERED);
assert_eq!(list.first().map(|e| e.id.as_str()), Some("p-39"));
}
#[test]
fn a_copy_is_written_once_the_design_is_still_or_after_the_longest_wait() {
// Still changing, not waited long: not yet.
assert!(!due(1000.0, 900.0, 900.0));
// Still for QUIET_MS: due.
assert!(due(900.0 + QUIET_MS, 900.0, 900.0));
// Changing all the time: due after MAX_WAIT_MS anyway.
assert!(!due(5000.0, 2000.0, 4900.0));
assert!(due(2000.0 + MAX_WAIT_MS, 2000.0, 2000.0 + MAX_WAIT_MS - 1.0));
}
#[test]
fn the_browser_asks_before_unload_unless_told_not_to_and_every_copy_is_current() {
// Keeping off: exactly as before, unsaved work asks, whatever the second option says.
assert!(asks_before_unload(true, false, true, true, true));
assert!(asks_before_unload(true, false, true, true, false));
assert!(asks_before_unload(true, false, false, true, true));
// Keeping on, asking on (the default): both, the box and the copy.
assert!(asks_before_unload(true, true, true, true, true));
// Keeping on, asking off, every unsaved design kept: no browser box.
assert!(!asks_before_unload(true, true, false, true, true));
// Keeping on, asking off, a copy still being written (or failed): the browser still asks.
assert!(asks_before_unload(true, true, false, true, false));
// Nothing unsaved, or an embedded page that keeps nothing: never.
assert!(!asks_before_unload(true, true, true, false, false));
assert!(!asks_before_unload(true, false, true, false, false));
assert!(!asks_before_unload(false, false, true, true, false));
}
#[test]
fn restore_open_saved_and_discard_each_do_one_thing_and_always_remove_the_copy() {
let saved = entry("p-1", 5.0, Some("/opfs/Bracket.sc"));
assert_eq!(plan(Answer::Restore, &saved, true), Some(Plan { open_copy: true, open_saved: None, drop_copy: true }));
assert_eq!(
plan(Answer::OpenSaved, &saved, true),
Some(Plan { open_copy: false, open_saved: Some("/opfs/Bracket.sc".into()), drop_copy: true })
);
assert_eq!(plan(Answer::Discard, &saved, true), Some(Plan { open_copy: false, open_saved: None, drop_copy: true }));
assert_eq!(answers(&saved, true), [Answer::Restore, Answer::OpenSaved, Answer::Discard]);
// A design never saved, or whose file is gone, has no saved version to open.
let never = entry("p-2", 5.0, None);
assert_eq!(plan(Answer::OpenSaved, &never, true), None);
assert_eq!(plan(Answer::OpenSaved, &saved, false), None);
assert_eq!(answers(&never, false), [Answer::Restore, Answer::Discard]);
}
}
+1 -1
View File
@@ -48,7 +48,7 @@ impl Download {
}
if solidcraft_ui_egui::documents::any_dirty(app) {
let mut answer = None;
let closed = ui_kit::Window::new(egui::Id::unique("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
.required()
.show_body(ctx, |ui| {
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
+88 -320
View File
@@ -3,8 +3,7 @@
//! Runs the same [`solidcraft_ui_egui::SolveApp`] as the desktop app through eframe's web runner
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from this
//! directory (output in `dist/web`). URL flags: `?webgl` forces WebGL2 (remembered until
//! `?webgpu`), `?sample` opens the sample part, `?embed=editor` runs it as an editing embed in a
//! host page (no application bar or start page; nothing kept in the browser; #8517).
//! `?webgpu`), `?sample` opens the sample part.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#[cfg(target_arch = "wasm32")]
@@ -19,13 +18,16 @@ mod easyprint;
mod files;
#[cfg(target_arch = "wasm32")]
mod install;
#[cfg(any(target_arch = "wasm32", test))]
mod keep;
#[cfg(target_arch = "wasm32")]
mod linked;
#[cfg(any(target_arch = "wasm32", test))]
mod space_mouse;
/// The folder the page serves its fonts from: index.html copies each there, and a drawing's text
/// fetches its fonts from it (#4890).
#[cfg(any(target_arch = "wasm32", test))]
const FONTS: &str = "fonts";
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
/// Ctrl+PageDown already reach the browser.)
@@ -45,8 +47,6 @@ fn query_flag(query: &str, name: &str) -> bool {
struct Launch {
open: Option<String>,
embed: bool,
/// `?embed=editor` (#8517): an editing embed in a host page that keeps the files itself.
editor: bool,
}
#[cfg(any(target_arch = "wasm32", test))]
@@ -57,19 +57,16 @@ impl Launch {
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
match key.as_ref() {
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
"embed" if value == "editor" => launch.editor = true,
"embed" => embed = true,
_ => {}
}
}
launch.embed = embed && launch.open.is_some() && !launch.editor;
launch.embed = embed && launch.open.is_some();
launch
}
/// The browser's own storage (designs, autosave, recovery, preferences) is used: not when
/// embedded, where the host keeps what is kept.
fn persists(&self) -> bool {
!self.embed && !self.editor
!self.embed
}
fn full_app_url(&self, mut url: url::Url) -> String {
@@ -186,7 +183,6 @@ mod web {
use crate::api::Api;
use crate::disk::{self, SaveTarget};
use crate::files::{self, Files, Intent};
use crate::keep;
use crate::space_mouse::SpaceMouse;
const CANVAS_ID: &str = "solidcraft_canvas";
@@ -246,9 +242,7 @@ mod web {
}
/// The browser's "leave site? changes you made may not be saved" prompt while a design has
/// unsaved changes (the page can't show its own dialog when it is being closed). With "keep
/// unsaved work in this browser" on and "still ask" off, there is nothing to ask once every
/// unsaved design's copy is current (`keep::asks_before_unload`, #10220).
/// unsaved changes (the page can't show its own dialog when it is being closed).
fn ask_before_unload(window: &web_sys::Window) {
let on_unload = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::BeforeUnloadEvent)>::new(|e: web_sys::BeforeUnloadEvent| {
if UNSAVED.with(std::cell::Cell::get) {
@@ -261,50 +255,6 @@ mod web {
on_unload.forget();
}
/// With "keep unsaved work in this browser" on, the page brings every unsaved design's copy up
/// to date when it is hidden or goes (`pagehide`, `visibilitychange`): the browser may not
/// wait for that write, so the copies are written shortly after each change as well
/// (`Shell::keep_tick`, #10220).
fn keep_when_hidden(window: &web_sys::Window, runner: &eframe::WebRunner) {
let keep_now = {
let runner = runner.clone();
move || {
if let Some(mut shell) = runner.app_mut::<Shell>() {
shell.keep_tick(None, true);
}
}
};
let on_hide = {
let keep_now = keep_now.clone();
wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| keep_now())
};
let on_visibility = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::Event)>::new(move |_: web_sys::Event| {
let hidden = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| js_sys::Reflect::get(&d, &"visibilityState".into()).ok())
.and_then(|v| v.as_string())
.is_some_and(|v| v == "hidden");
if hidden {
keep_now();
}
});
let _ = window.add_event_listener_with_callback("pagehide", on_hide.as_ref().unchecked_ref());
if let Some(document) = window.document() {
let _ = document.add_event_listener_with_callback("visibilitychange", on_visibility.as_ref().unchecked_ref());
}
// The listeners live as long as the page.
on_hide.forget();
on_visibility.forget();
}
/// One open design followed for its kept copy: its revision, when that last changed, and since
/// when its copy has lacked a change (`None`: current, or being written).
struct Track {
revision: u64,
last_change: f64,
first_unwritten: Option<f64>,
}
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
/// the canvas, before eframe sees them; the browser then acts on them as usual.
@@ -342,6 +292,8 @@ mod web {
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
/// The fetches of drawing fonts that have finished, for the next frame to hand on.
type DrawingFontsDone = Rc<std::cell::RefCell<Vec<(solidcraft_engine::sketch::fallback::WebFont, Result<Vec<u8>, String>)>>>;
struct Shell {
app: SolveApp,
@@ -372,48 +324,27 @@ mod web {
font_fetch_kind: Option<&'static str>,
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
font_language: String,
/// The fonts a drawing's text asked for, other than Japanese, each fetched once a session.
drawing_fonts_asked: std::collections::HashSet<solidcraft_engine::sketch::fallback::WebFont>,
drawing_fonts_done: DrawingFontsDone,
space_mouse: SpaceMouse,
wheel_gestures: solidcraft_ui_egui::WebWheelGestures,
/// The settings this tab wrote or loaded last, and those another tab wrote since (#8644).
prefs_writer: crate::PrefsWriter,
prefs_warning: Option<String>,
other_tab_prefs: PrefsInbox,
/// The open designs followed for their kept copy, by `Session::uid` (#10220).
keep_track: std::collections::HashMap<u64, Track>,
/// Every unsaved design's kept copy is current (and the preference is on).
all_kept: bool,
}
impl Shell {
/// The settings as they are kept between visits: with the user's own language, not a
/// session-only English fallback.
fn persisted_prefs(&mut self, previous: impl FnOnce() -> Result<Option<String>, String>) -> Option<String> {
fn persisted_prefs(&mut self) -> String {
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
let live = std::mem::replace(&mut self.app.preferences.language, saved);
let (prefs, warning) = match self.app.try_prefs() {
Ok(prefs) => (Some(prefs), None),
Err(reason) => match previous().and_then(|previous| self.app.prefs_with_stored_panel_layout(previous.as_deref())) {
Ok(prefs) => (Some(prefs), Some(format!("Panel layout changes were not saved: {reason}"))),
Err(error) => (None, Some(format!("Settings were not written: {error}; panel layout: {reason}"))),
},
};
if self.prefs_warning != warning {
if let Some(warning) = &warning {
log::warn!("{warning}");
}
self.prefs_warning = warning;
}
let prefs = self.app.prefs();
self.app.preferences.language = live;
prefs
}
fn stored_prefs() -> Result<Option<String>, String> {
let storage = web_sys::window()
.and_then(|window| window.local_storage().ok().flatten())
.ok_or_else(|| "browser settings storage is unavailable".to_owned())?;
storage.get_item(&crate::prefs_key()).map_err(|_| "browser settings could not be read".to_owned())
}
/// A settings change goes to the browser's storage in the frame it is made, and from
/// there to the other open tabs (#8644). Input is what changes settings, so a frame
/// without any is skipped; eframe's own save still catches anything else.
@@ -421,7 +352,7 @@ mod web {
if !self.launch.persists() || ctx.input(|i| i.events.is_empty()) {
return;
}
let Some(prefs) = self.persisted_prefs(Self::stored_prefs) else { return };
let prefs = self.persisted_prefs();
let Some(prefs) = self.prefs_writer.take(prefs) else { return };
let stored = web_sys::window().and_then(|w| w.local_storage().ok().flatten()).map(|s| s.set_item(&crate::prefs_key(), &prefs));
if !matches!(stored, Some(Ok(()))) {
@@ -435,9 +366,8 @@ mod web {
fn load_other_tabs_prefs(&mut self) {
let Some(prefs) = self.other_tab_prefs.take() else { return };
self.app.load_prefs(&prefs);
if let Some(held) = self.persisted_prefs(Self::stored_prefs) {
self.prefs_writer.loaded(held);
}
let held = self.persisted_prefs();
self.prefs_writer.loaded(held);
}
/// Files from the computer, writes to store, messages and the autosave.
@@ -516,14 +446,8 @@ mod web {
self.sync_recents();
}
let now = ctx.input(|i| i.time);
// With "keep unsaved work in this browser" on, each design's own copy takes over from
// the single autosave (#10220).
if self.app.preferences.keep_unsaved_in_browser && self.autosaved.is_some() {
files::autosave(&self.files, None);
self.autosaved = None;
}
// Unsaved work is kept (at most every AUTOSAVE_S); saved work needs no autosave.
if self.files.ready.get() && self.files.recovery.borrow().is_none() && !self.app.preferences.keep_unsaved_in_browser {
if self.files.ready.get() && self.files.recovery.borrow().is_none() {
let s = &self.app.session;
let changed = s.is_dirty() && self.autosaved != Some(s.revision);
let wait = AUTOSAVE_S - (now - self.autosaved_at);
@@ -542,175 +466,6 @@ mod web {
if !self.files.ready.get() || !self.files.incoming.borrow().is_empty() {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
self.all_kept = self.keep_tick(Some(ctx), false);
}
/// Keep each open design's unsaved changes in the browser, when the preference asks for
/// it: a design's copy is written once it has been still a moment (`keep::due`), or at
/// once with `now` (the page is being hidden); a copy whose design is saved or closed, or
/// with the preference off, goes. Answers whether every unsaved design's copy is current.
fn keep_tick(&mut self, ctx: Option<&egui::Context>, now: bool) -> bool {
if !self.launch.persists() || !self.files.ready.get() {
return false;
}
let Shell { app, files, keep_track, .. } = self;
let on = app.preferences.keep_unsaved_in_browser;
let time = js_sys::Date::now();
let page = files.keep_page.borrow().clone();
let mut wanted = std::collections::HashSet::new();
let mut all_kept = true;
let mut wake: Option<f64> = None;
let mut error = None;
let mut soon = |ms: f64| wake = Some(wake.map_or(ms, |w: f64| w.min(ms)));
if on {
for s in solidcraft_ui_egui::documents::open_sessions(app).into_iter().filter(|s| s.is_dirty()) {
let id = keep::entry_id(&page, s.uid);
wanted.insert(s.uid);
let t = keep_track.entry(s.uid).or_insert(Track { revision: s.revision, last_change: time, first_unwritten: None });
if t.revision != s.revision {
t.revision = s.revision;
t.last_change = time;
}
if files.kept.borrow().get(&id).copied().flatten() == Some(s.revision) {
t.first_unwritten = None;
continue;
}
all_kept = false;
if files.keeping.borrow().contains(&id) {
soon(100.0);
continue;
}
let first = *t.first_unwritten.get_or_insert(time);
let failed = files.kept_failed.borrow().get(&id).copied();
if !keep::may_write(time, failed) {
// The last write failed: try again after a while.
soon(keep::RETRY_MS);
continue;
}
if now || keep::due(time, first, t.last_change) {
match solidcraft_engine::io::write_design_container(&s.design()) {
Ok(bytes) => {
let entry = keep::Entry { id, name: s.doc.name.clone(), path: s.path.clone(), changed_at: t.last_change };
files::write_kept(files, &entry, bytes, s.revision);
t.first_unwritten = None;
}
Err(e) => {
files.kept_failed.borrow_mut().insert(id, time);
error = Some(format!("keeping unsaved work in this browser: {e}"));
}
}
soon(100.0);
} else {
soon((keep::QUIET_MS - (time - t.last_change)).min(keep::MAX_WAIT_MS - (time - first)).max(16.0));
}
}
}
// This page's copies no design needs any more: saved, closed, or the preference off.
let stale: Vec<String> = files
.kept
.borrow()
.keys()
.filter(|id| keep::page_of(id) == Some(page.as_str()))
.filter(|id| !wanted.iter().any(|uid| keep::entry_id(&page, *uid) == **id))
.cloned()
.collect();
for id in stale {
files::remove_kept(files, &id);
soon(100.0);
}
keep_track.retain(|uid, _| wanted.contains(uid));
if let Some(e) = error {
app.set_status(e, true);
}
if let (Some(ctx), Some(ms)) = (ctx, wake) {
ctx.request_repaint_after(std::time::Duration::from_secs_f64(ms / 1000.0));
}
on && all_kept
}
/// Offer the copies an earlier visit kept (#10220): "Restore unsaved design: <name>,
/// changed <time>?", one window for all of them, each with Restore, Open saved version
/// (when it has a file) and Discard.
fn keep_prompt(&mut self, ctx: &egui::Context) {
let offered = self.files.keep_offered.borrow().clone();
if offered.is_empty() || !self.files.ready.get() {
return;
}
let now = (js_sys::Date::now() / 1000.0) as u64;
let rows: Vec<_> = offered
.iter()
.map(|e| solidcraft_ui_egui::recovery_ui::BrowserCopy {
name: &e.name,
when: solidcraft_ui_egui::recovery_ui::ago_between(now, (e.changed_at / 1000.0) as u64),
open_saved: keep::answers(e, self.saved_exists(e)).contains(&keep::Answer::OpenSaved),
})
.collect();
let act = solidcraft_ui_egui::recovery_ui::browser_copies_window(ctx, &self.app.preferences.language, &rows);
let Some((index, answer)) = act else { return };
let Some(entry) = offered.get(index) else { return };
let answer = match answer {
solidcraft_ui_egui::recovery_ui::CopyAnswer::Restore => keep::Answer::Restore,
solidcraft_ui_egui::recovery_ui::CopyAnswer::OpenSaved => keep::Answer::OpenSaved,
solidcraft_ui_egui::recovery_ui::CopyAnswer::Discard => keep::Answer::Discard,
};
self.keep_answer(entry, answer);
}
/// Whether a kept copy's design file can still be opened: in the browser's storage, or a
/// file on the computer this browser remembers.
fn saved_exists(&self, e: &keep::Entry) -> bool {
e.path.as_deref().is_some_and(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| self.files.disk_known.borrow().contains(id)))
}
/// Act on an answer in the "Restore unsaved design?" window (`keep::plan`).
fn keep_answer(&mut self, entry: &keep::Entry, answer: keep::Answer) {
let Some(plan) = keep::plan(answer, entry, self.saved_exists(entry)) else { return };
if plan.open_copy {
let copy = match files::prepare_kept(&entry.id, entry.path.as_deref()) {
Ok(copy) => copy,
Err(error) => {
self.app.set_status(error, true);
return;
}
};
self.app.open_path(&copy);
if self.app.session.path.as_deref() != Some(copy.as_str()) {
// Not opened (the status says why): it stays offered.
return;
}
// It is its own design again (saving goes to its file), with its changes unsaved.
self.app.session.path = entry.path.clone();
self.app.session.doc_mut().name = entry.name.clone();
self.app.session.mark_unsaved();
self.app.home.open = false;
}
if let Some(path) = &plan.open_saved {
if vfs::exists(path) {
self.app.open_path(path);
if !keep::is_open(self.app.session.path.as_deref(), path) {
// Not opened (the status says why): the copy stays offered.
return;
}
} else {
// A file on the computer: the browser asks for access again, in this click.
// The copy goes once the file has opened, not before: if access is refused,
// the picker is dismissed or the file is gone, it stays offered.
let (files, id) = (self.files.clone(), entry.id.clone());
disk::restore(
&self.files,
path,
Some(Box::new(move || {
files.keep_offered.borrow_mut().retain(|e| e.id != id);
files::remove_kept(&files, &id);
})),
);
return;
}
}
if plan.drop_copy {
self.files.keep_offered.borrow_mut().retain(|e| e.id != entry.id);
files::remove_kept(&self.files, &entry.id);
}
}
/// The start page's recent designs are the stored ones: the remembered order first (those
@@ -747,7 +502,7 @@ mod web {
return;
}
let mut act: Option<(&'static str, String)> = None;
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_files"), "folder", "Files in this browser")
let closed = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_files"), "folder", "Files in this browser")
// Room for long names; the kit still caps the window at the browser's width.
.cards(4.0)
.show_body(ctx, |ui| {
@@ -849,9 +604,7 @@ mod web {
self.files.panel.set(false);
}
"insert" => {
if !solidcraft_ui_egui::mesh_units::ask(&mut self.app, &path, true) {
self.app.insert_path(&path);
}
self.app.insert_path(&path);
self.files.panel.set(false);
}
"download" => match vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES) {
@@ -896,16 +649,10 @@ mod web {
*r = new.clone();
}
}
if let Some(at) = self.app.home.opened_at.remove(&old) {
self.app.home.opened_at.insert(new.clone(), at);
}
// A forgotten design follows its new name.
if self.app.home.forgotten.remove(&old) {
self.app.home.forgotten.insert(new.clone());
self.app.home.forgotten.insert(new);
}
// An open tab of that file keeps its path and, when the name came
// from the file, the title that matches the file list (#11375).
solidcraft_ui_egui::documents::follow_file_rename(&mut self.app, &old, &new);
}
Err(e) => self.app.set_status(e, true),
}
@@ -919,7 +666,7 @@ mod web {
fn recovery_prompt(&mut self, ctx: &egui::Context) {
let Some((name, path)) = self.files.recovery.borrow().clone() else { return };
let mut answer = None;
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::unique("sc_browser_recovery"), "restore", "Recover unsaved work?")
let _ = solidcraft_ui_egui::ui_kit::Window::new(egui::Id::new("sc_browser_recovery"), "restore", "Recover unsaved work?")
.required()
.show_body(ctx, |ui| {
ui.label(format!("“{name}” had unsaved changes when this page last closed."));
@@ -955,13 +702,6 @@ mod web {
files::autosave(&self.files, None);
}
/// Whether leaving the page asks through the browser's own box (`keep::asks_before_unload`).
fn asks_before_unload(&self) -> bool {
let any = solidcraft_ui_egui::documents::any_dirty(&self.app);
let prefs = &self.app.preferences;
keep::asks_before_unload(self.launch.persists(), prefs.keep_unsaved_in_browser, prefs.keep_unsaved_also_asks, any, self.all_kept)
}
/// Whether the shell already has the on-demand face the live language needs.
fn script_font_ready(&self) -> bool {
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
@@ -990,6 +730,10 @@ mod web {
self.chinese_kind = Some("zh-TW");
}
_ => {
solidcraft_engine::sketch::fallback::provide_web_font(
solidcraft_engine::sketch::fallback::WebFont::Japanese,
bytes.clone(),
);
self.cjk_bytes = Some(bytes);
}
}
@@ -1003,6 +747,9 @@ mod web {
_ => JAPANESE_FONT_PATH,
};
log::warn!("failed to fetch script font from {path}: {err}");
if !matches!(kind, Some("zh" | "zh-TW")) {
solidcraft_engine::sketch::fallback::web_font_failed(solidcraft_engine::sketch::fallback::WebFont::Japanese);
}
false
}
};
@@ -1038,10 +785,12 @@ mod web {
self.font_language = effective.to_owned();
}
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
/// fetch the Japanese font, so the text does not stay tofu (#720).
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name), or a
/// drawing's text waiting for one: fetch the Japanese font, so the text does not stay tofu (#720).
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
let japanese = solidcraft_engine::sketch::fallback::web_fonts_wanted().contains(&solidcraft_engine::sketch::fallback::WebFont::Japanese);
let wanted = solidcraft_ui_egui::fonts::painted_cjk(ctx) || japanese;
if self.font.watching_text() && wanted && self.font.text_needs_font() {
self.fetch_font(ctx, "ja");
}
}
@@ -1062,6 +811,33 @@ mod web {
ctx.request_repaint();
});
}
/// The other fonts a drawing's text waits for (Arabic, Chinese, Korean): each fetched once,
/// and handed to the text when it comes. Japanese comes with the interface's CJK font.
fn fetch_drawing_fonts(&mut self, ctx: &egui::Context) {
use solidcraft_engine::sketch::fallback::{self, WebFont};
for (font, res) in self.drawing_fonts_done.borrow_mut().drain(..) {
match res {
Ok(bytes) => fallback::provide_web_font(font, bytes),
Err(err) => {
log::warn!("failed to fetch the drawing font {}/{}: {err}", crate::FONTS, font.file());
fallback::web_font_failed(font);
}
}
ctx.request_repaint();
}
for font in fallback::web_fonts_wanted() {
if font == WebFont::Japanese || !self.drawing_fonts_asked.insert(font) {
continue;
}
let (done, ctx) = (self.drawing_fonts_done.clone(), ctx.clone());
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(&format!("./{}/{}", crate::FONTS, font.file())).await;
done.borrow_mut().push((font, res));
ctx.request_repaint();
});
}
}
}
impl eframe::App for Shell {
@@ -1069,11 +845,12 @@ mod web {
// First: a language another tab chose fetches its font this frame.
self.load_other_tabs_prefs();
self.tick_font(ctx);
self.fetch_drawing_fonts(ctx);
self.app.logic(ctx);
self.api.poll();
self.space_mouse.logic(&mut self.app, ctx);
self.files_tick(ctx);
UNSAVED.with(|u| u.set(self.asks_before_unload()));
UNSAVED.with(|u| u.set(solidcraft_ui_egui::documents::any_dirty(&self.app)));
DESIGN_SHOWN.with(|d| d.set(solidcraft_ui_egui::toolbar::design_shown(&self.app)));
}
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
@@ -1093,7 +870,7 @@ mod web {
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
self.linked.tick(&mut self.app, ui.ctx());
UNSAVED.with(|u| u.set(self.asks_before_unload()));
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
if self.launch.embed {
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
@@ -1108,15 +885,10 @@ mod web {
if self.launch.persists() {
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
self.keep_prompt(&ctx);
}
self.watch_text_for_font(&ctx);
self.store_prefs(&ctx);
}
#[cfg(target_arch = "wasm32")]
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(&mut *self)
}
fn persist_egui_memory(&self) -> bool {
// eframe keeps this under one fixed key: only stable uses it, so a pre-release never rewrites it.
self.launch.persists() && crate::channel::current().is_empty()
@@ -1125,7 +897,7 @@ mod web {
if !self.launch.persists() {
return;
}
let Some(prefs) = self.persisted_prefs(|| Ok(storage.get_string(&crate::prefs_key()))) else { return };
let prefs = self.persisted_prefs();
storage.set_string(&crate::prefs_key(), prefs);
}
}
@@ -1191,8 +963,7 @@ mod web {
{
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
}
let runner = eframe::WebRunner::new();
let result = runner
let result = eframe::WebRunner::new()
.start(
canvas,
options,
@@ -1204,7 +975,7 @@ mod web {
}
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
let (restorable, restorer) = (files.clone(), files.clone());
let services = if !launch.persists() { Services::default() } else { Services {
let services = if launch.embed { Services::default() } else { Services {
// Open shows the browser's open dialog for a design where it has one
// (the file keeps its handle, so Save writes back to it), else the
// browser's files window. A command that reads another kind of file
@@ -1236,7 +1007,7 @@ mod web {
can_restore: Some(Box::new(move |path: &str| {
disk::parse(path).is_some_and(|(id, _)| restorable.disk_known.borrow().contains(id))
})),
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path, None))),
restore_file: Some(Box::new(move |path: &str| disk::restore(&restorer, path))),
download_design: Some(Box::new(files::download)),
easyprint_upload: Some(std::sync::Arc::new(crate::easyprint::upload)),
browser_files: Some(Box::new(move || shown.panel.set(true))),
@@ -1248,7 +1019,6 @@ mod web {
let (sender, receiver) = std::sync::mpsc::channel();
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
app.embedded = launch.embed;
app.embed_editor = launch.editor;
// Listening before reading: settings another tab writes from now on are
// newer than those read here.
let other_tab_prefs = PrefsInbox::default();
@@ -1299,23 +1069,16 @@ mod web {
chinese_kind: None,
font_fetch_kind: None,
font_language,
drawing_fonts_asked: Default::default(),
drawing_fonts_done: Default::default(),
space_mouse: SpaceMouse::default(),
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
prefs_writer: crate::PrefsWriter::default(),
prefs_warning: None,
other_tab_prefs,
keep_track: Default::default(),
all_kept: false,
}))
}),
)
.await;
if result.is_ok()
&& crate::Launch::parse(&query()).persists()
&& let Some(window) = web_sys::window()
{
keep_when_hidden(&window, &runner);
}
if let Some(el) = document.get_element_by_id(LOADING_ID) {
match result {
Ok(()) => dismiss_loading(el),
@@ -1368,17 +1131,6 @@ mod tests {
assert!(full.fragment().is_none());
}
#[test]
fn embed_editor_needs_no_design_link_and_keeps_nothing_in_the_browser() {
let editor = Launch::parse("?embed=editor");
assert!(editor.editor && !editor.embed && !editor.persists());
let linked = Launch::parse("?embed=editor&open=https://example.org/a.sc");
assert!(linked.editor && !linked.embed, "an editing embed may open a linked design, still editable");
let viewer = Launch::parse("?embed=viewer&open=https://example.org/a.sc");
assert!(viewer.embed && !viewer.editor, "any other value is the viewer embed, as before");
assert!(!Launch::parse("?other=editor").editor);
}
#[test]
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
@@ -1484,4 +1236,20 @@ mod tests {
assert_eq!(settings_written(key, None, None), None, "clearing all storage leaves this tab's settings");
assert_eq!(settings_written(key, Some("rc:solidcraft.prefs"), Some("{}".into())), None, "another channel's settings are not loaded");
}
/// Each font a drawing's text can ask for on the web is one index.html copies, from an asset
/// in the repository, into the folder the app fetches it from.
#[test]
fn each_drawing_font_is_copied_where_it_is_fetched() {
let page = include_str!("../index.html");
let here = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for font in solidcraft_engine::sketch::fallback::WebFont::ALL {
let served = format!("{FONTS}/{}", font.file());
let (folder, file) = served.rsplit_once('/').unwrap_or_default();
let copy = page.lines().find(|l| l.contains("rel=\"copy-file\"") && l.contains(&format!("/{file}\""))).unwrap_or_default();
assert!(copy.contains(&format!("data-target-path=\"{folder}\"")), "index.html doesn't copy {served}: {copy}");
let from = copy.split("href=\"").nth(1).and_then(|rest| rest.split('"').next()).unwrap_or_default();
assert!(here.join(from).is_file(), "{served}: no {from}");
}
}
}
+9 -13
View File
@@ -68,9 +68,7 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
Ok(l) => {
if let Some(port) = listen_on_listener(l, token.clone(), tx, ctx, false) {
if matches!(listen_on, control_auth::Listen::Default) {
solidcraft_engine::note!(
"solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)"
);
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
}
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
match control_auth::write_current_port(port) {
@@ -79,14 +77,14 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
*p = Some((port, token));
}
}
Err(e) => solidcraft_engine::note!("solidcraft: could not write the control discovery file ({e})"),
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
}
}
}
Err(e) => solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
}
}
None => solidcraft_engine::note!("solidcraft: control server not started: no random token available"),
None => eprintln!("solidcraft: control server not started: no random token available"),
}
rx
}
@@ -97,13 +95,13 @@ pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<Co
/// requests go to `tx`, where the chat's gate reads them; one may wait long for the user's Allow.
pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u16> {
let Some(token) = new_token() else {
solidcraft_engine::note!("solidcraft: agent chat endpoint not started: no random token available");
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
return None;
};
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
Ok(l) => l,
Err(e) => {
solidcraft_engine::note!("solidcraft: control server failed to bind 127.0.0.1: {e}");
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
return None;
}
};
@@ -117,21 +115,19 @@ fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRe
match control_auth::token_path(port) {
Some(path) => match control_auth::write_token_file(&path, &token) {
Ok(()) => {
solidcraft_engine::note!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
if let Ok(mut files) = TOKEN_FILES.lock() {
files.push((path, token.clone()));
}
}
Err(e) => solidcraft_engine::note!(
Err(e) => eprintln!(
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
clients need SOLIDCRAFT_CONTROL_TOKEN",
path.display()
),
},
None => {
solidcraft_engine::note!(
"solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN"
)
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
}
}
let token = std::sync::Arc::new(token);
-100
View File
@@ -1,100 +0,0 @@
//! Is the file chooser that opens the desktop's own? (#10159)
//!
//! `rfd` opens the file chooser through the XDG desktop portal; without the portal it falls
//! back to zenity, a window that does not belong to the app and that the app never mentions.
//! The desktop's own chooser is the portal one, so the app has to know when it is not the one
//! that opens, and say so in the session log.
//!
//! The session bus is asked once, on a thread of its own: a bus that answers late must not
//! hold the window. What it answered is kept, and read when a file is opened.
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
/// The portal's bus name: the desktop's own file chooser lives behind it.
const PORTAL: &str = "org.freedesktop.portal.Desktop";
/// The bus has not answered yet.
const UNKNOWN: u8 = 0;
/// There is no session bus to reach the portal on.
const NO_SESSION_BUS: u8 = 1;
/// The bus is there but the portal does not answer on it.
const NO_PORTAL_SERVICE: u8 = 2;
/// The bus is there and the portal answers on it.
const PORTAL_THERE: u8 = 3;
/// What the session bus said about the portal.
static BUS: AtomicU8 = AtomicU8::new(UNKNOWN);
/// A file chooser is opening: whether it is the desktop's own is to be said.
static OPENING: AtomicBool = AtomicBool::new(false);
/// Ask the session bus, on a thread of its own, whether the portal is there.
pub fn start(ctx: &egui::Context) {
let thread_ctx = ctx.clone();
let probe = move || {
let answer = match session_bus() {
None => NO_SESSION_BUS,
Some(connection) => match portal_on(&connection) {
true => PORTAL_THERE,
false => NO_PORTAL_SERVICE,
},
};
BUS.store(answer, Ordering::Relaxed);
// The answer is in: a file chooser waiting for it can be told now.
thread_ctx.request_repaint();
};
if let Err(e) = std::thread::Builder::new().name("desktop-chooser".into()).spawn(probe) {
log::warn!("file chooser: no thread to ask the session bus: {e}");
// Nothing will answer: the desktop's own chooser cannot be told apart from the start.
BUS.store(PORTAL_THERE, Ordering::Relaxed);
}
}
/// The session bus, when there is one to talk to.
fn session_bus() -> Option<zbus::blocking::Connection> {
zbus::blocking::Connection::session().ok()
}
/// Whether the portal answers on the bus: it is either running on it or waiting to be started.
fn portal_on(connection: &zbus::blocking::Connection) -> bool {
let Ok(bus) = zbus::blocking::fdo::DBusProxy::new(connection) else { return false };
let named = |names: Vec<zbus::names::OwnedBusName>| names.iter().any(|name| name.as_str() == PORTAL);
// A portal that is running is on the bus; one that is not running yet is still there when
// the bus can start it, which is what the portal's service file is for.
match (bus.list_names().map(named), bus.list_activatable_names().map(named)) {
(Ok(running), Ok(startable)) => running || startable,
(Ok(running), Err(_)) => running,
(Err(_), Ok(startable)) => startable,
// The bus does not answer: nothing answers on it.
(Err(_), Err(_)) => false,
}
}
/// A file chooser is opening: ask for a frame, so that the reason it is not the desktop's own
/// is said (#10159).
pub fn note_opening(ctx: &egui::Context) {
OPENING.store(true, Ordering::Relaxed);
ctx.request_repaint();
}
/// The reason the file chooser that is opening is not the desktop's own, if it is not.
///
/// The decision is the one the test states: without a session bus there is no bus to reach the
/// portal on; with a bus but no portal on it, `rfd` falls back to zenity; and under XWayland the
/// window is an X11 one while the session is a Wayland one, which the portal refuses to parent
/// its chooser to.
pub fn take_reason() -> Option<&'static str> {
if !OPENING.load(Ordering::Relaxed) {
return None;
}
let (dbus_session, portal_service) = match BUS.load(Ordering::Relaxed) {
NO_SESSION_BUS => (false, false),
NO_PORTAL_SERVICE => (true, false),
PORTAL_THERE => (true, true),
// The bus has not answered yet: wait for it, and say nothing yet.
_ => return None,
};
OPENING.store(false, Ordering::Relaxed);
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
let x11_display = super::window_system_now() == Some("x11");
super::desktop_chooser_fallback(dbus_session, portal_service, wayland_display, x11_display)
}
+1 -1
View File
@@ -112,7 +112,7 @@ pub fn restart(software: bool, lost_at: u64, reason: &str) {
pub fn end() -> ! {
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
std::thread::sleep(END_GRACE);
solidcraft_engine::note!("SolidCraft graphics: still closing after the device was lost; aborting");
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
std::process::abort();
});
if let Err(e) = watchdog {
+26 -246
View File
@@ -23,8 +23,6 @@ mod app_icon;
mod come_forward;
mod control_server;
#[cfg(target_os = "linux")]
mod desktop_chooser;
#[cfg(target_os = "linux")]
mod desktop_theme;
#[cfg(any(target_os = "windows", target_os = "macos"))]
mod desktop_transparency;
@@ -79,7 +77,7 @@ fn mark(step: &str) {
static START: std::sync::OnceLock<(std::time::Instant, bool)> = std::sync::OnceLock::new();
let (start, on) = START.get_or_init(|| (std::time::Instant::now(), solidcraft_engine::geom::env::var_os("PROFILE_STARTUP").is_some()));
if *on {
solidcraft_engine::note!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
eprintln!("startup {:>6.0} ms {step}", start.elapsed().as_secs_f64() * 1000.0);
}
}
@@ -244,12 +242,6 @@ impl eframe::App for App {
menu.tick(&mut self.solve, ctx);
}
self.solve.logic(ctx);
// The file chooser that opened is not the desktop's own: the user has to be told (#10159).
#[cfg(target_os = "linux")]
if let Some(reason) = desktop_chooser::take_reason() {
let text = self.solve.tr(reason).to_string();
self.solve.set_status(text, false);
}
self.dialog_folder.design(self.solve.session.path.as_deref());
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
if !self.solve.file_picker_busy() {
@@ -282,20 +274,13 @@ impl eframe::App for App {
let now = std::time::Instant::now();
if self.prefs_writer.due(ui.input(|i| i.events.iter().any(changes_settings)), now)
&& let Some(storage) = frame.storage_mut()
&& let Ok(prefs) = self.prefs_for_storage(storage)
&& let Some(wait) = self.prefs_writer.store(storage, prefs, now)
&& let Some(wait) = self.prefs_writer.store(storage, self.solve.prefs(), now)
{
ui.ctx().request_repaint_after(wait);
}
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
let prefs = match self.prefs_for_storage(storage) {
Ok(prefs) => prefs,
Err(reason) => {
log::warn!("Settings were not written: {reason}");
return;
}
};
let prefs = self.solve.prefs();
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
self.prefs_writer.written = Some(prefs);
}
@@ -310,8 +295,6 @@ impl eframe::App for App {
log::warn!("a picture was still being drawn as the app closed");
}
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
// A GPU path tracer setup still running did not stop the app (#9236).
solidcraft_ui_egui::render_workspace::close(&mut self.solve);
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
// Before a restart (`Drop for App`) starts the next instance, which would otherwise get
// nothing from the 3D mouse driver (`solidcraft_3dx_client::stop`).
@@ -368,14 +351,6 @@ struct LostWatch {
}
impl App {
fn prefs_for_storage(&self, storage: &dyn eframe::Storage) -> Result<String, String> {
self.solve.try_prefs().or_else(|reason| {
log::warn!("Panel layout changes were not saved: {reason}");
let previous = solidcraft_ui_egui::prefs::stored(|key| storage.get_string(key));
self.solve.prefs_with_stored_panel_layout(previous.as_deref())
})
}
/// The graphics device was lost (#4172): the window can't draw again, so close, keeping
/// unsaved changes for recovery as a forced quit does (the window can't ask about them), and
/// start again once closed ([`device_lost::restart`]).
@@ -383,7 +358,7 @@ impl App {
let Some(gpu) = self.gpu.as_mut().filter(|g| g.lost_at.is_none() && g.lost.is_lost()) else { return };
let reason = gpu.lost.reason().unwrap_or_default();
log::error!("graphics: the device was lost ({reason}); closing to start again");
solidcraft_engine::note!("SolidCraft graphics: the device was lost ({reason}); starting again");
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
gpu.lost_at = Some(device_lost::now_secs());
gpu.reason = reason;
@@ -496,19 +471,12 @@ fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom:
fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
// The file chooser that opens may not be the desktop's own: the user is told when it does
// not (#10159).
#[cfg(target_os = "linux")]
let chooser_ctx = cc.egui_ctx.clone();
let open_parent = parent.clone();
let program_parent = parent.clone();
let file_parent = parent.clone();
let (open_folder, file_folder, save_folder) = (folder.clone(), folder.clone(), folder.clone());
Services {
pick_open: Some(Box::new(move |first: &[&str]| {
// The chooser that opens may not be the desktop's own (#10159).
#[cfg(target_os = "linux")]
desktop_chooser::note_opening(&chooser_ctx);
// The file type the command reads comes first, so the picker opens on it (#1801).
let mut dialog = open_folder.start(open_parent.clone());
if !first.is_empty() {
@@ -817,28 +785,6 @@ fn graphics() -> Option<serde_json::Value> {
graphics_path().and_then(|p| std::fs::read_to_string(p).ok()).and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
}
fn restore_preferences(app: &mut SolveApp, stored_ui: Option<&str>, saved_graphics: Option<&serde_json::Value>, safe_mode: bool) {
if let Some(p) = stored_ui.filter(|_| !safe_mode) {
app.load_prefs(p);
}
// Even safe mode edits the real graphics.json. Keep its choices in Preferences so an
// anti-aliasing change cannot erase them; safe mode's renderer still uses its own defaults.
restore_graphics_preferences(app, saved_graphics);
}
/// Restore startup choices after loading the separately saved UI preferences.
fn restore_graphics_preferences(app: &mut SolveApp, saved: Option<&serde_json::Value>) {
app.preferences.graphics_adapter = saved.and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str)).unwrap_or_default().to_string();
// graphics.json chooses the window before app.ron is loaded. Reflect that saved choice,
// not the current session's window system (which an environment override may change).
app.preferences.window_system = saved
.and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str))
.map(str::trim)
.filter(|choice| matches!(*choice, "wayland" | "x11"))
.unwrap_or("auto")
.to_string();
}
/// The graphics backend preference (`graphics.json`'s `backend`: `auto`, `dx12`, `vulkan`, `gl`).
fn backend_preference() -> Option<String> {
graphics().and_then(|v| v.get("backend").and_then(serde_json::Value::as_str).map(str::to_string))
@@ -1008,7 +954,7 @@ fn main() -> eframe::Result {
if let Some(value) = max_file_size {
let applied = startup::max_file_size(value.as_deref()).and_then(|bytes| solidcraft_engine::io::limits::set_override(Some(bytes)));
if let Err(error) = applied {
solidcraft_engine::note!("SolidCraft: {error}");
eprintln!("SolidCraft: {error}");
return Err(eframe::Error::AppCreation(std::io::Error::other(error).into()));
}
}
@@ -1040,7 +986,7 @@ fn main() -> eframe::Result {
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
if let Some(note) = &fallback_note {
log::warn!("{note}");
solidcraft_engine::note!("SolidCraft: {note}");
eprintln!("SolidCraft: {note}");
notes.push(note.clone());
}
let saved_samples = if safe_mode { None } else { msaa() };
@@ -1057,8 +1003,6 @@ fn main() -> eframe::Result {
solidcraft_engine::crash::install(dir, build)
})
.unwrap_or_default();
// The recovery folder also holds the reopen guard marker (#5064); read once, reused below.
let recovery_dir = solidcraft_engine::recovery::default_dir();
// A crash in native code (a graphics driver) says where it happened: the exception or signal
// and the module on Windows and macOS, and on every system the startup step the app was in.
#[cfg(target_os = "windows")]
@@ -1141,10 +1085,6 @@ fn main() -> eframe::Result {
solidcraft_ui_egui::fonts::load_system_fonts_in_background(&cc.egui_ctx);
mark("fonts");
let dialog_folder = DialogFolder::default();
// The file chooser that opens may not be the desktop's own: the session bus is asked
// now, so the answer is there when a file is opened (#10159).
#[cfg(target_os = "linux")]
desktop_chooser::start(&cc.egui_ctx);
let mut app = SolveApp::new(Session::default(), services(cc, &dialog_folder));
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
app.custom_titlebar = custom_titlebar;
@@ -1153,20 +1093,21 @@ fn main() -> eframe::Result {
// Windows: the taskbar button shows the mark even when slow first frames keep the
// taskbar waiting for the window's icon.
if !native_frame::set_class_icon(cc) {
solidcraft_engine::note!("SolidCraft window: no class icon; the taskbar may show a generic one");
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
}
// The OS hit test offers the resize border, at the window's DPI, from the first
// message (#1381).
app.native_resize_border = custom_titlebar && native_frame::install(cc);
if custom_titlebar && !app.native_resize_border {
solidcraft_engine::note!("SolidCraft window: no native resize border; egui edge zones instead");
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
}
}
app.integrated_titlebar = cfg!(target_os = "macos");
app.cpu_meshing = safe_mode;
app.graphics_fallback = fallback_note.clone();
let stored_ui = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k)));
restore_preferences(&mut app, stored_ui.as_deref(), graphics().as_ref(), safe_mode);
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
if let Some(path) = &theme_file
&& let Err(e) = solidcraft_ui_egui::prefs::follow_theme_file_for_run(&mut app, path)
{
@@ -1179,6 +1120,10 @@ fn main() -> eframe::Result {
lost_at: None,
reason: String::new(),
});
if !safe_mode {
app.preferences.graphics_adapter =
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
}
if let Some(rs) = &cc.wgpu_render_state {
app.services.graphics_adapters = rs
.available_adapters
@@ -1190,10 +1135,9 @@ fn main() -> eframe::Result {
.collect();
let info = rs.adapter.get_info();
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
solidcraft_engine::note!(
eprintln!(
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
app.preferences.graphics_adapter,
info.name
app.preferences.graphics_adapter, info.name
);
}
let samples = scene_msaa_request(saved_samples);
@@ -1201,7 +1145,7 @@ fn main() -> eframe::Result {
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
});
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
solidcraft_engine::note!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
app.msaa_supported = supported;
@@ -1252,25 +1196,8 @@ fn main() -> eframe::Result {
};
// No design given: the designs open at the last quit, when the preference asks (#5542).
// Not when driven over the control channel, whose clients expect a fresh design.
// A design that stopped the last session while it was being reopened is skipped this
// time, so one bad file cannot brick every launch (#5064).
app.reopen_guard_dir = recovery_dir.clone();
let files = if files.is_empty() && !sample && control_port.is_none() {
let (reopen, skipped) = solidcraft_ui_egui::documents::reopen_plan(&app, recovery_dir.as_deref());
if !skipped.is_empty() {
let names: Vec<String> = skipped
.iter()
.map(|p| std::path::Path::new(p).file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| p.clone()))
.collect();
notes.push(format!(
"The last session stopped while reopening {}. It was not reopened this time; open it yourself if you need it.",
names.join(", ")
));
}
reopen
} else {
files
};
let files =
if files.is_empty() && !sample && control_port.is_none() { solidcraft_ui_egui::documents::designs_to_reopen(&app) } else { files };
for f in &files {
app.open_path_async(f);
}
@@ -1286,9 +1213,9 @@ fn main() -> eframe::Result {
app.home.show(rev);
}
// Autosave, and offer what crashed sessions left behind.
if let Some(dir) = recovery_dir.as_ref() {
if let Some(dir) = solidcraft_engine::recovery::default_dir() {
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
solidcraft_ui_egui::recovery_ui::start(&mut app, dir, std::time::Duration::from_secs_f64(every.min(1e9)));
solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
// Safe mode keeps autosaving but offers nothing: the entries wait for a normal start.
if safe_mode {
app.recoverable.clear();
@@ -1362,7 +1289,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
solidcraft_engine::note!("SolidCraft graphics: starting {backends:?}");
eprintln!("SolidCraft graphics: starting {backends:?}");
setup.instance_descriptor.backends = backends;
}
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
@@ -1388,7 +1315,7 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
/// event loop or a failed device; each retry advances through the APIs, then software once.
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
log::error!("the window could not open: {error}");
solidcraft_engine::note!("SolidCraft: the window could not open: {error}");
eprintln!("SolidCraft: the window could not open: {error}");
// This failure was handled, so neither the marker nor the journal should call it a crash.
if let Some(dir) = settings {
startup::started(dir);
@@ -1401,7 +1328,7 @@ fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backen
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
log::warn!("{note}");
solidcraft_engine::note!("SolidCraft: {note}");
eprintln!("SolidCraft: {note}");
let mut command = std::process::Command::new(exe);
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
if next == "cpu" {
@@ -1476,47 +1403,6 @@ fn prefer_x11(display: bool, wayland_display: bool, desktop: Option<&str>, wayla
display && !(niri && wayland_display)
}
#[cfg(all(unix, not(target_os = "macos")))]
const CHOOSER_NO_SESSION_BUS: &str = "file.open.portal_fallback.no_session_bus";
#[cfg(all(unix, not(target_os = "macos")))]
const CHOOSER_NO_PORTAL_SERVICE: &str = "file.open.portal_fallback.no_portal_service";
#[cfg(all(unix, not(target_os = "macos")))]
const CHOOSER_XWAYLAND: &str = "file.open.portal_fallback.xwayland";
/// Whether the chooser that opens is NOT the desktop's own, and why.
///
/// `rfd` opens the file chooser through the XDG desktop portal; when the portal is not there it
/// falls back to zenity, which opens a window that does not belong to SolidCraft and that the
/// application never mentions. The application has to know that: this decision is what the warning
/// is built on.
///
/// * `dbus_session`: the session bus is there (`DBUS_SESSION_BUS_ADDRESS`).
/// * `portal_service`: the file chooser portal answers on it.
/// * `wayland_display`: a Wayland session runs (`WAYLAND_DISPLAY`).
/// * `x11_display`: the window is an X11 one (`DISPLAY`, or XWayland).
///
/// `None` means the desktop's own chooser is the one that opens and there is nothing to say;
/// `Some` carries the reason to report.
#[cfg(all(unix, not(target_os = "macos")))]
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
fn desktop_chooser_fallback(dbus_session: bool, portal_service: bool, wayland_display: bool, x11_display: bool) -> Option<&'static str> {
// Without a session bus there is no bus to reach the portal on, so the chooser is not the
// desktop's own whatever else is there.
if !dbus_session {
return Some(CHOOSER_NO_SESSION_BUS);
}
// The bus is there but the portal does not answer on it: `rfd` falls back to zenity.
if !portal_service {
return Some(CHOOSER_NO_PORTAL_SERVICE);
}
// Under XWayland the window is an X11 one while the session is a Wayland one, and the portal
// refuses to parent its chooser to an X11 window: `rfd` falls back to zenity.
if wayland_display && x11_display {
return Some(CHOOSER_XWAYLAND);
}
None
}
#[cfg(all(test, unix, not(target_os = "macos")))]
mod backend_tests {
use super::prefer_x11;
@@ -1552,30 +1438,6 @@ mod backend_tests {
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
}
/// The file chooser must say when it is NOT the desktop's own: `rfd` falls back to zenity
/// when the XDG portal is not there, and the user gets a foreign window with no explanation.
#[test]
fn the_file_picker_says_when_it_is_not_the_desktops_own_chooser() {
use super::desktop_chooser_fallback;
// The normal case: the portal is there, the desktop's own chooser opens, nothing to say.
assert_eq!(desktop_chooser_fallback(true, true, true, false), None);
// No session bus: the portal can not be reached at all, so `rfd` falls back to zenity.
assert!(
desktop_chooser_fallback(false, true, true, false).is_some(),
"without a D-Bus session bus the file chooser is not the desktop's own: the user has to be told"
);
// The bus is there but the portal service is not: same fallback, same warning.
assert!(
desktop_chooser_fallback(true, false, true, false).is_some(),
"without the desktop portal service the file chooser is not the desktop's own: the user has to be told"
);
// XWayland: the parent window is an X11 one and the portal refuses it, so `rfd` falls back.
assert!(
desktop_chooser_fallback(true, true, true, true).is_some(),
"under XWayland the portal rejects the parent window: the user has to be told"
);
}
#[test]
fn the_desktops_title_bar_only_when_asked_for() {
use super::titlebar_choice;
@@ -1807,7 +1669,7 @@ mod viewport_tests {
viewport.fullscreen = Some(fullscreen);
viewport.native_pixels_per_point = Some(retina);
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
ctx.run_ui(raw, |ui| super::content_ui(app, ui)).textures_delta.clear();
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
};
// Enter, leave, and enter again: no stale inset when returning to a normal window.
for fullscreen in [false, true, false, true] {
@@ -1853,88 +1715,6 @@ mod viewport_tests {
#[cfg(test)]
mod msaa_tests {
/// Safe mode uses default UI settings but edits the real graphics.json for the next start.
#[test]
fn safe_mode_preserves_graphics_choices_when_changing_antialiasing() {
use serde_json::{Value, json};
use solidcraft_ui_egui::scenario::Harness;
for choice in ["wayland", "x11"] {
let mut h = Harness::new();
let path = h.dir.join("graphics.json");
let saved = json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 8});
std::fs::write(&path, saved.to_string()).unwrap();
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
let defaults = h.app.preferences.clone();
let stored_ui = json!({"preferences": {"window_system": "auto", "graphics_adapter": "Stale GPU", "msaa": 2,
"display_quality": "fine"}})
.to_string();
super::restore_preferences(&mut h.app, Some(&stored_ui), Some(&saved), true);
assert_eq!(h.app.preferences.window_system, choice);
assert_eq!(h.app.preferences.graphics_adapter, "Selected GPU");
assert_eq!(h.app.preferences.msaa, defaults.msaa, "safe mode ignores saved anti-aliasing");
assert_eq!(h.app.preferences.display_quality, defaults.display_quality, "safe mode ignores saved UI settings");
assert_eq!(serde_json::from_str::<Value>(&std::fs::read_to_string(&path).unwrap()).unwrap(), saved, "startup does not write settings");
assert_eq!(h.call("ui.prefs", json!({"open": "Graphics"}))["ok"], true);
assert_eq!(h.call("ui.prefs", json!({"msaa": 1}))["ok"], true);
assert_eq!(h.call("ui.prefs", json!({"button": "ok"}))["ok"], true);
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(after, json!({"window_system": choice, "adapter": "Selected GPU", "backend": "vulkan", "msaa": 1}));
let mut normal = Harness::new();
super::restore_preferences(&mut normal.app, Some(&stored_ui), Some(&after), false);
assert_eq!(normal.app.preferences.window_system, choice);
assert_eq!(normal.app.preferences.graphics_adapter, "Selected GPU");
assert_eq!(normal.app.preferences.display_quality, "fine", "normal startup still restores UI settings");
}
}
#[test]
fn missing_or_invalid_startup_window_system_uses_automatic() {
use serde_json::json;
for saved in [None, Some(json!(null)), Some(json!({})), Some(json!({"window_system": 7})), Some(json!({"window_system": "unknown"}))] {
let mut app = super::SolveApp::new(super::Session::default(), super::Services::default());
app.preferences.window_system = "wayland".into();
super::restore_graphics_preferences(&mut app, saved.as_ref());
assert_eq!(app.preferences.window_system, "auto", "{saved:?}");
}
}
/// A manual graphics.json choice wins over stale app.ron preferences and survives an
/// unrelated graphics edit (#10116), including on a fresh launch with no saved UI state.
#[test]
fn startup_window_system_survives_changing_antialiasing() {
use serde_json::{Value, json};
use solidcraft_ui_egui::scenario::Harness;
for choice in ["wayland", "x11", "auto", " wayland ", " x11 "] {
for stale in [None, Some("auto"), Some("wayland"), Some("x11")] {
let mut h = Harness::new();
if let Some(stale) = stale {
h.app.load_prefs(&json!({"preferences": {"window_system": stale}}).to_string());
}
let path = h.dir.join("graphics.json");
std::fs::write(&path, json!({"window_system": choice, "backend": "vulkan", "adapter": "Selected GPU", "msaa": 4}).to_string())
.unwrap();
h.app.services.graphics_path = Some(path.to_string_lossy().into_owned());
let saved: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
super::restore_graphics_preferences(&mut h.app, Some(&saved));
let result = h.call("ui.prefs", json!({"msaa": 2}));
assert_eq!(result["ok"], true, "{result}");
let after: Value = serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(after["window_system"], choice.trim(), "saved choice {choice:?}, stale UI choice {stale:?}");
assert_eq!(result["result"]["window_system"], choice.trim());
assert_eq!(after["msaa"], 2);
assert_eq!(after["backend"], "vulkan");
assert_eq!(after["adapter"], "Selected GPU");
}
}
}
#[test]
fn first_launch_adopts_the_previous_release_settings() {
let d = std::env::temp_dir().join(format!("solidcraft-adopt-settings-{}", std::process::id()));
+1 -1
View File
@@ -434,7 +434,7 @@ mod tests {
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
app.home.open = false;
let ctx = Context::default();
ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {}).textures_delta.clear();
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
let queue = |bridge: &NavBridge| {
let mut shared = bridge.shared.lock().unwrap();
-1
View File
@@ -1914,7 +1914,6 @@ mod tests {
fn wheel() -> egui::Event {
egui::Event::MouseWheel {
source: egui::MouseWheelSource::Unknown,
unit: egui::MouseWheelUnit::Line,
delta: egui::vec2(0.0, -1.0),
modifiers: Default::default(),
+17 -23
View File
@@ -40,10 +40,10 @@ fn connect_to(path: &Path) -> Option<UnixStream> {
/// The axes of one record, or `None` for a record that is not motion.
///
/// The daemon reports x to the right, y up and z away from the user; our raw HID frame is x
/// right, y towards the user and z down. Swap and negate y/z and ry/rz to match that frame.
/// With nothing set in `spnavcfg` the two readers give the same motion; what the user swaps
/// or inverts there then carries over.
/// The daemon's frame is right-handed with x to the right, y away from the user and z up; ours is
/// the device's raw one, x right, y towards the user and z down, so y and z (and the rotations
/// about them) change sign. With nothing set in `spnavcfg` the two give the same motion; what
/// the user swaps or inverts there then carries over.
fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
let mut data = [0_i32; 8];
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
@@ -51,8 +51,8 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
}
let [kind, x, y, z, rx, ry, rz, _period] = data;
(kind == 0).then(|| Axes {
translation: [unit(x), unit(-i64::from(z)), unit(-i64::from(y))],
rotation: [unit(rx), unit(-i64::from(rz)), unit(-i64::from(ry))],
translation: [unit(x), unit(-i64::from(y)), unit(-i64::from(z))],
rotation: [unit(rx), unit(-i64::from(ry)), unit(-i64::from(rz))],
})
}
@@ -149,28 +149,22 @@ mod tests {
#[test]
fn motion_records_become_axes_in_the_devices_raw_frame() {
// Each daemon axis reaches only its matching raw HID axis, in both directions.
for (daemon_axis, raw_axis, sign) in [(0, 0, 1), (1, 2, -1), (2, 1, -1), (3, 3, 1), (4, 5, -1), (5, 4, -1)] {
for value in [-500, 500] {
let mut data = [0; 8];
data[daemon_axis + 1] = value;
let mut expected = [0; 6];
expected[raw_axis] = sign * (value * 350 / 500) as i16;
assert_eq!(decode(&record(data)), Some(Axes::from(expected)), "daemon axis {daemon_axis}, value {value}");
}
}
// Full deflection of the daemon's x, y and z is the device's full scale, and y and z flip.
assert_eq!(decode(&record([0, 500, 500, 500, 0, 0, 0, 8])), Some(Axes { translation: [350, -350, -350], rotation: [0; 3] }));
// Rotations follow translations: right-handed in both frames, so ry and rz flip as y and z do.
assert_eq!(decode(&record([0, 0, 0, 0, 250, 250, 250, 8])), Some(Axes { translation: [0; 3], rotation: [175, -175, -175] }));
// Rest is a motion record of zeros.
assert_eq!(decode(&record([0, 0, 0, 0, 0, 0, 0, 1200])), Some(Axes::default()));
}
#[test]
fn what_spnavcfg_swaps_reaches_the_view() {
// A push away is daemon z (raw y); swapping Y/Z makes it a lift (raw z).
// The same swap changes a sideways tilt (daemon rz, raw ry) into a twist (raw rz).
let before = decode(&record([0, 0, 0, 500, 0, 0, 250, 8]));
let after = decode(&record([0, 0, 500, 0, 0, 250, 0, 8]));
assert_eq!(before, Some(Axes { translation: [0, -350, 0], rotation: [0, -175, 0] }));
assert_eq!(after, Some(Axes { translation: [0, 0, -350], rotation: [0, 0, -175] }));
// The daemon sends a push away as y. With Y and Z swapped in spnavcfg it sends the same push
// as z, and the view gets a lift (raw z) instead of the zoom (raw y).
let before = decode(&record([0, 0, 500, 0, 0, 0, 0, 8])).map(|axes| axes.translation);
let after = decode(&record([0, 0, 0, 500, 0, 0, 0, 8])).map(|axes| axes.translation);
assert_eq!(before, Some([0, -350, 0]));
assert_eq!(after, Some([0, 0, -350]));
}
#[test]
@@ -185,7 +179,7 @@ mod tests {
// Nothing overflows on the extremes of `i32`.
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
assert_eq!(extreme.map(|axes| axes.translation[0]), Some(i16::MIN));
assert_eq!(extreme.map(|axes| axes.translation), Some([i16::MIN, i16::MIN, i16::MAX]));
assert_eq!(extreme.map(|axes| axes.translation[1]), Some(i16::MAX));
assert_eq!(extreme.map(|axes| axes.rotation[0]), Some(i16::MAX));
}
+6 -11
View File
@@ -311,18 +311,13 @@ pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
#[cfg(target_os = "macos")]
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
/// Where macOS keeps its `nvram` tool.
#[cfg(target_os = "macos")]
const NVRAM_TOOL: &str = "/usr/sbin/nvram";
/// `true` if the OCLP NVRAM key is present on this system.
///
/// Runs `/usr/sbin/nvram <key>` and treats exit 0 as present. The full path keeps a `PATH` that
/// lacks `/usr/sbin`, or lists a look-alike first, from changing the answer. Any error (missing
/// binary, permission denied, key absent) returns `false`: this is a best-effort probe.
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
/// permission denied, key absent) returns `false` — this is a best-effort probe.
#[cfg(target_os = "macos")]
pub fn oclp_nvram_present() -> bool {
std::process::Command::new(NVRAM_TOOL).arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
}
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
@@ -339,8 +334,8 @@ fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
///
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
/// already queried the OS version by then, so the program starts itself again with the variable
/// set and waits for that child.
/// already queried the OS version by then. Re-launching is the only safe approach that
/// works without `unsafe` (a true `execve` replacement would require it).
///
/// Returns `Ok(false)` immediately when:
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
@@ -357,7 +352,7 @@ pub fn relaunch_with_version_compat() -> Result<bool, String> {
return Ok(false);
}
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
solidcraft_engine::note!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
let status = std::process::Command::new(&exe)
.args(std::env::args_os().skip(1))
+3 -6
View File
@@ -17,13 +17,12 @@ assets/
ui/design/tools/ # authoring bases, checks and included review app
ui/source/ # Rust viewport glyphs, markers and navigation
branding/web/source/og.png # shared social preview card
branding/logo/ # print/web logo files; guide in branding/README.md
styles/ui/palettes.rs # light/dark defaults
styles/web/loading.css # web loading/canvas styles
appearances/library.rs # built-in visual presets
materials/library.rs # physical defaults and colours
shaders/viewport.wgsl # original viewport shader
fonts/biz-udp-gothic/ # unmodified font and OFL licence
fonts/<family>/ # unmodified fonts and OFL licences
locales/{en,de,ja,ko}.json # shared offline catalogs
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
@@ -80,16 +79,14 @@ The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `io.tangencylabs.solidcraft` |
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/`, uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
| Web | HTML/Trunk and `manifest.webmanifest` stay in `apps/solidcraft-web/`; copies the Japanese font/licence to `fonts/` and the Noto Sans Arabic, KR, SC and TC fonts with their licences to `fonts/<family>/` (fetched when a drawing's text needs them), uses the collected CSS, and copies the Linux 32/256/512 px images to `icons/` for the favicon and installed app |
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
## Code and provenance
Resources remain compiled by their owning layer through existing APIs:
`ui-egui::icons::paint_icon` and `Tokens` at L5, model presets in L2 `doc`, and sketch
fonts at L1. The sketch crate owns its built-in Liberation Sans font and OFL
licence under `crates/sketch/assets/fonts/liberation-sans/`, so Cargo source
packages and vendored builds include both files. State, projection, interaction, commands and GPU bindings stay in
fonts at L1. State, projection, interaction, commands and GPU bindings stay in
their current modules. Embedded data uses `CARGO_MANIFEST_DIR`; application code
needs no runtime resource registry or repository lookup.
-73
View File
@@ -1,73 +0,0 @@
# SolidCraft brand kit
Logo files for stickers, shirts, the web and social posts. Everything in `logo/` is drawn
from the app icon (`assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg`),
which is also the last frame of the loading animation (`assets/styles/web/loading-logo.svg`).
The SVGs are plain paths: no scripts, fonts, animation or external links, so any printer can use them.
## Files
| File | Use it for |
|---|---|
| `logo/solidcraft-mark.svg` | The mark in full colour. First choice for stickers, print and the web. |
| `logo/solidcraft-mark-mono-black.svg` | One-colour print on light material: screen-printed shirts, stamps, engraving, vinyl. |
| `logo/solidcraft-mark-mono-white.svg` | One-colour print on dark material, such as a white print on a dark shirt. |
| `logo/solidcraft-mark-512.png`, `-1024.png`, `-2048.png` | Transparent square PNGs for places that don't take SVG: avatars, social posts, slides, print shops that ask for a bitmap (send the 2048). |
| `logo/solidcraft-wordmark.svg` | The name on its own, outlined to paths. |
| `logo/solidcraft-lockup-horizontal.svg` | Mark and name side by side: headers, banners, shirt fronts, wide stickers. |
| `web/source/og.png` | The existing link preview card for the docs site. |
The gaps between the faces are part of the logo. In the colour and one-colour files they are
transparent, so the material shows through. For a die-cut sticker, cut around the outer shape
and let the backing colour (white is best) fill the gaps.
## Colours
The mark's five faces each use a two-stop gradient from the icon source:
| Face | From | To |
|---|---|---|
| Top | `#51adff` | `#2b83f1` |
| Left | `#258df5` | `#1461da` |
| Right | `#1b66cf` | `#124596` |
| Lower step (top) | `#459fff` | `#2379e7` |
| Upper step (inner side) | `#1d63c6` | `#15499e` |
Other colours, all already used in the project:
| Colour | Hex | Where it comes from |
|---|---|---|
| Wordmark blue | `#15499e` | loading logo `--word` |
| Interface accent | `#2678da` | light theme `accent` token (`assets/styles/ui/palettes.rs`) |
| Light background | `#e8ebf0` | web loading screen (`assets/styles/web/loading.css`) |
| Dark background | `#1f232b` | dark theme `file_background` token |
| Black / white | `#000000` / `#ffffff` | one-colour versions only |
If a printer can't do gradients, use the one-colour files, or fill every face with `#2678da`.
## Clear space and size
- Keep empty space around the mark of at least one fifth of its height on every side. Treat
the lockup the same way, measured from the mark.
- Smallest mark: 16 px tall on screen, 12 mm tall in print. Below that the gaps fill in.
- Smallest lockup: 120 px wide on screen, 40 mm wide in print.
## Don'ts
- Don't recolour it outside the colours above.
- Don't stretch, squash, rotate, skew or crop it. Scale it evenly.
- Don't add shadows, outlines, glows, bevels or other effects.
- Don't fill or close the gaps between the faces, or move the faces apart.
- Don't rearrange the lockup, retype the name in another font, or put the mark on a busy
background where it loses contrast.
## Name and logo
The SolidCraft code and these files are licensed MIT OR Apache-2.0 (see the [README](../../README.md#licence)),
so you may copy and change them. The name SolidCraft and this logo identify the SolidCraft
project. Use them to refer to the project, for example in an article, a talk, or a sticker for a
meetup. If you publish a fork or a modified build, give it its own name and logo so people
don't mistake it for SolidCraft.
The wordmark is set in IBM Plex Sans Bold (SIL Open Font License 1.1, the interface font in
`assets/fonts/ibm-plex-sans/`) and outlined, so you don't need the font to print it.
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 2748.71 904.436" width="2749" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g><path fill="#15499e" transform="translate(1092.78 632.20) scale(0.36281)" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#000000"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 612 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><g fill="#ffffff"><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z"/></g></svg>

Before

Width:  |  Height:  |  Size: 612 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="116.406 53.364 791.188 904.436" width="791" height="904" role="img"><title>SolidCraft</title><defs><linearGradient id="sc1" x1="619.14" y1="179.734" x2="694.687" y2="396.761" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1d63c6"/><stop offset="1" stop-color="#15499e"/></linearGradient><linearGradient id="sc2" x1="619.14" y1="413.244" x2="790.839" y2="605.547" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#459fff"/><stop offset="1" stop-color="#2379e7"/></linearGradient><linearGradient id="sc3" x1="632.876" y1="399.508" x2="783.971" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#1b66cf"/><stop offset="1" stop-color="#124596"/></linearGradient><linearGradient id="sc4" x1="207.063" y1="303.357" x2="426.837" y2="968.175" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#258df5"/><stop offset="1" stop-color="#1461da"/></linearGradient><linearGradient id="sc5" x1="385.63" y1="53.364" x2="522.989" y2="510.769" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#51adff"/><stop offset="1" stop-color="#2b83f1"/></linearGradient></defs><g><path d="M547.883 282.66 L709.797 188.211 L709.797 375.985 Z" fill="url(#sc1)"/><path d="M727.797 593.626 L727.797 407.136 L889.571 500.381 Z" fill="url(#sc2)"/><path d="M530 521.17 L691.797 427.912 L691.797 655.928 L907.594 531.545 L907.594 740.159 L530 957.8 Z" fill="url(#sc3)"/><path d="M116.406 510.769 L116.406 303.529 L494 521.17 L494 957.8 L314.203 854.167 L314.203 624.777 Z" fill="url(#sc4)"/><path d="M134.374 272.334 L512 53.364 L691.828 157.015 L476.117 282.846 L673.752 396.761 L512 489.993 Z" fill="url(#sc5)"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="38.0 -749.0 4847.0 761.0" width="4847" height="761" role="img"><title>SolidCraft</title><path fill="#15499e" d="M303 12Q212 12 147.5 -20Q83 -52 38 -101L138 -202Q173 -162 217.5 -142Q262 -122 313 -122Q369 -122 396 -145Q423 -168 423 -206Q423 -225 416.5 -240.5Q410 -256 392.5 -266.5Q375 -277 342 -281L273 -290Q200 -300 152.5 -326.5Q105 -353 82 -396.5Q59 -440 59 -498Q59 -561 90.5 -608.5Q122 -656 180.5 -683Q239 -710 321 -710Q400 -710 460 -684.5Q520 -659 563 -612L462 -510Q437 -539 401.5 -557.5Q366 -576 312 -576Q261 -576 236 -558Q211 -540 211 -508Q211 -484 219.5 -469Q228 -454 246.5 -446Q265 -438 294 -433L363 -422Q435 -411 482 -385Q529 -359 552 -317Q575 -275 575 -215Q575 -148 543.5 -97Q512 -46 451 -17Q390 12 303 12ZM926 12Q849 12 794 -21.5Q739 -55 709.5 -117Q680 -179 680 -263Q680 -348 709.5 -409Q739 -470 794 -503.5Q849 -537 926 -537Q1003 -537 1058 -503.5Q1113 -470 1142.5 -409Q1172 -348 1172 -263Q1172 -179 1142.5 -117Q1113 -55 1058 -21.5Q1003 12 926 12ZM926 -105Q970 -105 994 -132.5Q1018 -160 1018 -209V-316Q1018 -366 994 -393Q970 -420 926 -420Q883 -420 858.5 -393Q834 -366 834 -316V-209Q834 -160 858.5 -132.5Q883 -105 926 -105ZM1511 0H1444Q1368 0 1332.5 -37Q1297 -74 1297 -144V-740H1445V-115H1511ZM1694 -581Q1649 -581 1628.5 -602Q1608 -623 1608 -654V-676Q1608 -708 1628.5 -728.5Q1649 -749 1694 -749Q1739 -749 1759.5 -728.5Q1780 -708 1780 -676V-654Q1780 -623 1759.5 -602Q1739 -581 1694 -581ZM1620 0V-525H1768V0ZM2248 0V-88H2243Q2232 -45 2192.5 -16.5Q2153 12 2102 12Q2036 12 1991 -19.5Q1946 -51 1923 -112.5Q1900 -174 1900 -263Q1900 -352 1923 -413Q1946 -474 1991 -505.5Q2036 -537 2102 -537Q2153 -537 2192.5 -509Q2232 -481 2243 -437H2248V-740H2396V0ZM2151 -108Q2179 -108 2201 -117Q2223 -126 2235.5 -143.5Q2248 -161 2248 -185V-340Q2248 -364 2235.5 -381.5Q2223 -399 2201 -408Q2179 -417 2151 -417Q2109 -417 2081.5 -387.5Q2054 -358 2054 -308V-217Q2054 -168 2081.5 -138Q2109 -108 2151 -108ZM2844 12Q2750 12 2680.5 -28Q2611 -68 2573 -147Q2535 -226 2535 -344Q2535 -462 2573 -543.5Q2611 -625 2680.5 -667.5Q2750 -710 2844 -710Q2940 -710 3003.5 -670Q3067 -630 3105 -546L2974 -478Q2961 -522 2930.5 -548.5Q2900 -575 2844 -575Q2777 -575 2736.5 -531.5Q2696 -488 2696 -405V-293Q2696 -211 2736.5 -167Q2777 -123 2844 -123Q2900 -123 2933.5 -153.5Q2967 -184 2983 -227L3107 -155Q3068 -76 3004 -32Q2940 12 2844 12ZM3373 0H3225V-525H3373V-411H3378Q3384 -440 3400 -466Q3416 -492 3443.5 -508.5Q3471 -525 3512 -525H3538V-387H3501Q3458 -387 3429.5 -379.5Q3401 -372 3387 -355Q3373 -338 3373 -307ZM4105 0H4023Q3996 0 3972.5 -13.5Q3949 -27 3935 -53.5Q3921 -80 3921 -117V-130L3953 -92H3917Q3905 -41 3864 -14.5Q3823 12 3763 12Q3684 12 3642 -30.5Q3600 -73 3600 -141Q3600 -197 3627.5 -233Q3655 -269 3705.5 -287Q3756 -305 3825 -305H3908V-338Q3908 -376 3888 -398.5Q3868 -421 3821 -421Q3777 -421 3751 -402Q3725 -383 3708 -359L3620 -437Q3652 -484 3701 -510.5Q3750 -537 3832 -537Q3943 -537 3999.5 -487.5Q4056 -438 4056 -345V-115H4105ZM3908 -221H3836Q3793 -221 3771 -206Q3749 -191 3749 -162V-147Q3749 -119 3767 -105Q3785 -91 3818 -91Q3843 -91 3863 -98Q3883 -105 3895.5 -120Q3908 -135 3908 -159ZM4250 0V-410H4176V-525H4250V-587Q4250 -663 4290.5 -701.5Q4331 -740 4407 -740H4503V-625H4398V-525H4503V-410H4398V0ZM4877 0H4789Q4713 0 4673 -39Q4633 -78 4633 -153V-410H4559V-525H4596Q4626 -525 4637 -539.5Q4648 -554 4648 -581V-667H4781V-525H4885V-410H4781V-115H4877Z"/></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

+93
View File
@@ -0,0 +1,93 @@
Copyright 2022 The Noto Project Authors (https://github.com/notofonts/arabic)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+92
View File
@@ -0,0 +1,92 @@
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+2 -1
View File
@@ -60,6 +60,7 @@ circle_ttt false
close false
coil false
colored_components false
colored_components_off false
combine false
compare false
component false
@@ -160,7 +161,6 @@ min_radius false
mirror false
motion_link false
move false
nav_settings false
new false
normal false
offset false
@@ -178,6 +178,7 @@ pattern_rect false
perforate false
perspective false
pipe false
placeholder true
plane false
plane_along_path false
plane_angle false
@@ -0,0 +1,21 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Disable component colors</title>
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Align drawing annotations</title>
<path d="M12 6L12 58" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 12L51.5 12A2.5 2.5 0 0 1 54 14.5L54 17.5A2.5 2.5 0 0 1 51.5 20L22.5 20A2.5 2.5 0 0 1 20 17.5L20 14.5A2.5 2.5 0 0 1 22.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 28L40.5 28A2.5 2.5 0 0 1 43 30.5L43 33.5A2.5 2.5 0 0 1 40.5 36L22.5 36A2.5 2.5 0 0 1 20 33.5L20 30.5A2.5 2.5 0 0 1 22.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 44L47.5 44A2.5 2.5 0 0 1 50 46.5L50 49.5A2.5 2.5 0 0 1 47.5 52L22.5 52A2.5 2.5 0 0 1 20 49.5L20 46.5A2.5 2.5 0 0 1 22.5 44Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 962 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Align drawing views</title>
<path d="M10.5 24L23.5 24A2.5 2.5 0 0 1 26 26.5L26 43.5A2.5 2.5 0 0 1 23.5 46L10.5 46A2.5 2.5 0 0 1 8 43.5L8 26.5A2.5 2.5 0 0 1 10.5 24Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M39.5 12L52.5 12A2.5 2.5 0 0 1 55 14.5L55 43.5A2.5 2.5 0 0 1 52.5 46L39.5 46A2.5 2.5 0 0 1 37 43.5L37 14.5A2.5 2.5 0 0 1 39.5 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M6 52L58 52" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17 29L17 40M46 18L46 39" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 846 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing balloon</title>
<path d="M9.5 37L29.5 37A2.5 2.5 0 0 1 32 39.5L32 52.5A2.5 2.5 0 0 1 29.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 39.5A2.5 2.5 0 0 1 9.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 45L35 30L40 25" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 50L19 37L28 45Z" fill="#236ead"/>
<circle cx="45" cy="17" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 651 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Base drawing view</title>
<path d="M9.5 10L54.5 10A2.5 2.5 0 0 1 57 12.5L57 51.5A2.5 2.5 0 0 1 54.5 54L9.5 54A2.5 2.5 0 0 1 7 51.5L7 12.5A2.5 2.5 0 0 1 9.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M17.5 19L42.5 19A2.5 2.5 0 0 1 45 21.5L45 39.5A2.5 2.5 0 0 1 42.5 42L17.5 42A2.5 2.5 0 0 1 15 39.5L15 21.5A2.5 2.5 0 0 1 17.5 19Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="30" cy="30.5" r="6" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M47 46L53 46M50 43L50 49" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing centre mark</title>
<circle cx="32" cy="32" r="18" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 6L32 21M32 27L32 37M32 43L32 58M6 32L21 32M27 32L37 32M43 32L58 32" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 420 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Check drawing</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21 19L42 19M21 29L35 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 44L37 53L57 30" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 44L37 53L57 30" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 730 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing datum</title>
<path d="M6 53L58 53" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M32 52L24 38L40 38Z" fill="#236ead"/>
<path d="M32 38L32 27" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M21.5 7L42.5 7A2.5 2.5 0 0 1 45 9.5L45 24.5A2.5 2.5 0 0 1 42.5 27L21.5 27A2.5 2.5 0 0 1 19 24.5L19 9.5A2.5 2.5 0 0 1 21.5 7Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 631 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Detail drawing view</title>
<path d="M9.5 11L30.5 11A2.5 2.5 0 0 1 33 13.5L33 45.5A2.5 2.5 0 0 1 30.5 48L9.5 48A2.5 2.5 0 0 1 7 45.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M13 18L23 18L23 33L29 33" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="23" cy="30" r="9" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M31 35L39 41" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="47" cy="46" r="12" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M40 39L46 39L46 48L53 48" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1000 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing dimension</title>
<path d="M10 10L10 55M54 10L54 55" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 14L54 14L54 36L10 36Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 49L44 49" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M11 49L23 43L23 55Z" fill="#236ead"/>
<path d="M53 49L41 55L41 43Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Edit drawing table</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M25 26L54 26L54 37L25 37Z" fill="#b9ddf8"/>
<path d="M32 32L47 32M42 27L47 32L42 37" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Hole callout</title>
<circle cx="18" cy="43" r="13" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="18" cy="43" r="6" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M27 35L39 22L57 22" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23 40L27 28L34 35Z" fill="#236ead"/>
<circle cx="44" cy="10" r="4.5" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M39 16L49 4" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 794 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Insert drawing image</title>
<path d="M9.5 11L54.5 11A2.5 2.5 0 0 1 57 13.5L57 50.5A2.5 2.5 0 0 1 54.5 53L9.5 53A2.5 2.5 0 0 1 7 50.5L7 13.5A2.5 2.5 0 0 1 9.5 11Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="43" cy="23" r="5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 45L24 27L36 41L42 34L54 47Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing leader</title>
<path d="M8.5 34L33.5 34A2.5 2.5 0 0 1 36 36.5L36 51.5A2.5 2.5 0 0 1 33.5 54L8.5 54A2.5 2.5 0 0 1 6 51.5L6 36.5A2.5 2.5 0 0 1 8.5 34Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18 45L39 18L56 18" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 49L19 35L26 41Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 516 B

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing parts list</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.5" cy="31.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="15.5" cy="46.5" r="3.5" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M30 31L48 31M30 46L48 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 958 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Print drawing</title>
<path d="M18.5 7L45.5 7A2.5 2.5 0 0 1 48 9.5L48 29.5A2.5 2.5 0 0 1 45.5 32L18.5 32A2.5 2.5 0 0 1 16 29.5L16 9.5A2.5 2.5 0 0 1 18.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M8.5 24L55.5 24A2.5 2.5 0 0 1 58 26.5L58 49.5A2.5 2.5 0 0 1 55.5 52L8.5 52A2.5 2.5 0 0 1 6 49.5L6 26.5A2.5 2.5 0 0 1 8.5 24Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="48" cy="32" r="2.5" fill="#236ead" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M18.5 37L45.5 37A2.5 2.5 0 0 1 48 39.5L48 55.5A2.5 2.5 0 0 1 45.5 58L18.5 58A2.5 2.5 0 0 1 16 55.5L16 39.5A2.5 2.5 0 0 1 18.5 37Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M23 44L41 44M23 51L36 51" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Projected drawing view</title>
<path d="M9.5 33L31.5 33A2.5 2.5 0 0 1 34 35.5L34 52.5A2.5 2.5 0 0 1 31.5 55L9.5 55A2.5 2.5 0 0 1 7 52.5L7 35.5A2.5 2.5 0 0 1 9.5 33Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<circle cx="20.5" cy="44" r="5" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9.5 8L31.5 8A2.5 2.5 0 0 1 34 10.5L34 19.5A2.5 2.5 0 0 1 31.5 22L9.5 22A2.5 2.5 0 0 1 7 19.5L7 10.5A2.5 2.5 0 0 1 9.5 8Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M9 25L9 30M32 25L32 30M38 44L54 44" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="3 5"/>
<path d="M44 28L44 13" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44 7L48 14L40 14Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Browse drawing resources</title>
<path d="M15 7L51 7A3 3 0 0 1 54 10L54 48A3 3 0 0 1 51 51L15 51A3 3 0 0 1 12 48L12 10A3 3 0 0 1 15 7Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L46 12A3 3 0 0 1 49 15L49 53A3 3 0 0 1 46 56L10 56A3 3 0 0 1 7 53L7 15A3 3 0 0 1 10 12Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 20L42 20L42 47L14 47Z" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M14 38L42 38M28 38L28 47" fill="none" stroke="#6f879c" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22 27L34 27L34 36L22 36Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 933 B

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Add drawing revision</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M20 19L43 19M20 29L37 29" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M43 33L58 57L28 57Z" fill="#ffffff" stroke="#ffffff" stroke-width="4.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M43 33L58 57L28 57Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 745 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Drawing revision table</title>
<path d="M10.5 10L53.5 10A2.5 2.5 0 0 1 56 12.5L56 51.5A2.5 2.5 0 0 1 53.5 54L10.5 54A2.5 2.5 0 0 1 8 51.5L8 12.5A2.5 2.5 0 0 1 10.5 10Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 12L54 12L54 23L10 23Z" fill="#b8c3cf"/>
<path d="M8 24L56 24M8 39L56 39M23 24L23 54" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 27L10 35L20 35ZM15 42L10 50L20 50Z" fill="#236ead"/>
<path d="M31 31L49 31M31 46L49 46" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Section drawing view</title>
<path d="M8 15L56 15L56 50L37 50L37 27L27 27L27 50L8 50Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M10 38L26 22M10 25L20 15M10 50L24 36M39 35L54 20M39 48L54 33M49 50L56 43" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 450 B

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Space drawing annotations</title>
<path d="M28.5 9L53.5 9A2.5 2.5 0 0 1 56 11.5L56 14.5A2.5 2.5 0 0 1 53.5 17L28.5 17A2.5 2.5 0 0 1 26 14.5L26 11.5A2.5 2.5 0 0 1 28.5 9Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 28L53.5 28A2.5 2.5 0 0 1 56 30.5L56 33.5A2.5 2.5 0 0 1 53.5 36L28.5 36A2.5 2.5 0 0 1 26 33.5L26 30.5A2.5 2.5 0 0 1 28.5 28Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M28.5 47L53.5 47A2.5 2.5 0 0 1 56 49.5L56 52.5A2.5 2.5 0 0 1 53.5 55L28.5 55A2.5 2.5 0 0 1 26 52.5L26 49.5A2.5 2.5 0 0 1 28.5 47Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 19L15 25M15 38L15 44" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M15 14L10 21L20 21ZM15 30L10 23L20 23ZM15 33L10 40L20 40ZM15 49L10 42L20 42Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<title>Update drawing</title>
<path d="M14.5 7L49.5 7A2.5 2.5 0 0 1 52 9.5L52 54.5A2.5 2.5 0 0 1 49.5 57L14.5 57A2.5 2.5 0 0 1 12 54.5L12 9.5A2.5 2.5 0 0 1 14.5 7Z" fill="#e2e8ef" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M22.5 16L41.5 16A2.5 2.5 0 0 1 44 18.5L44 28.5A2.5 2.5 0 0 1 41.5 31L22.5 31A2.5 2.5 0 0 1 20 28.5L20 18.5A2.5 2.5 0 0 1 22.5 16Z" fill="#b8c3cf" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#ffffff" stroke-width="6.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M38 35A11 11 0 1 0 45 45" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
<path d="M44 40L32 37L40 28Z" fill="#236ead"/>
</svg>

After

Width:  |  Height:  |  Size: 900 B

+12 -1
View File
@@ -79,6 +79,17 @@ pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: C
painter.circle_filled(c, 1.3, col);
}
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
/// while it is hovered or dragged.
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
let ic = Tokens::get().icons;
let r = 6.0;
painter.circle_filled(c, r + 3.5, ic.marker_halo);
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
}
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
Some(match name {
"Coincident" => "c_coincident",
@@ -89,7 +100,7 @@ pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
"Smooth" => "c_smooth",
"Equal" => "c_equal",
"Concentric" => "c_concentric",
"MidPoint" | "PointAlong" => "c_midpoint",
"MidPoint" => "c_midpoint",
"Symmetry" => "c_symmetry",
"Fix" => "c_fix",
_ => return None,
+2 -1
View File
@@ -55,7 +55,8 @@ pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, fon
painter.ctx().add_plugin(LabelAtlas);
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
let atlas = painter.ctx().fonts(|f| f.font_image_size());
let primitives = painter.ctx().tessellate(vec![egui::epaint::ClippedShape::new(Rect::EVERYTHING, text)], painter.ctx().pixels_per_point());
let primitives =
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
for primitive in primitives {
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
mesh.texture_id = LABEL_PIXELS;
+23 -614
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Analiza dostupnosti",
"cmd.inspect_center_of_mass": "Centar mase",
"cmd.inspect_clearance": "Provjera zazora",
"cmd.inspect_colored_components": "Omogući boje komponenti",
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
"cmd.inspect_compare": "Uporedi tijela",
"cmd.inspect_compare_picture": "Uporedi sliku",
"cmd.inspect_continuity": "Analiza kontinuiteta",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Učitava putanje SVG crteža u skicu, skalirane, zakrenute i postavljene gdje zadate. Koristite ga da logotip ili vektorski obris pretvorite u krive koje možete ekstrudirati.",
"help.sketch.inspect": "Otvara izvještaj o otvorenoj skici: koliko je stepeni slobode preostalo i je li potpuno ograničena. Koristite ga da saznate šta se još pomjera prije nego što završite skicu.",
"help.sketch.intersect": "Dodaje krive ili tačke gdje tijela, lica, ivice ili druge skice sijeku ravan skice. Ostaju povezane i prate model kada se promijeni.",
"help.sketch.intersection_curve": "Pravi 3D krive tamo gdje se dva tijela, lica ili krive drugih skica sastaju, povezane s njima. Koristite ih kao putanje ili vodilice koje prate model.",
"help.sketch.intersection_curve": "Pravi 3D krive tamo gdje se dva tijela ili lica sastaju, povezane s njima. Koristite ih kao putanje ili vodilice koje prate model.",
"help.sketch.isoparametric_curve": "Pravi 3D krivu u smjeru U ili V lica kroz tačku koju izaberete, povezanu s licem. Koristite je kao putanju ili liniju razdvajanja koja prati površ.",
"help.sketch.line": "Crta povezane linije tačku po tačku, kao otvoreni lanac ili zatvoren obris. To je osnovni alat za većinu profila.",
"help.sketch.line.midpoint": "Crta liniju od njene središnje tačke do jednog kraja, a drugi kraj se preslikava. Koristite je za linije centrirane na tački, npr. simetričnu ivicu.",
File diff suppressed because it is too large Load Diff
+178 -354
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Tilgængelighedsanalyse",
"cmd.inspect_center_of_mass": "Massecentrum",
"cmd.inspect_clearance": "Frigangskontrol",
"cmd.inspect_colored_components": "Aktivér komponentfarver",
"cmd.inspect_colored_components_disable": "Deaktivér komponentfarver",
"cmd.inspect_compare": "Sammenlign emner",
"cmd.inspect_compare_picture": "Sammenlign billede",
"cmd.inspect_continuity": "Kontinuitetsanalyse",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Læser stierne i en SVG-tegning ind i en skitse, skaleret, drejet og placeret, som du angiver. Brug den til at gøre et logo eller en vektorkontur til kurver, du kan ekstrudere.",
"help.sketch.inspect": "Åbner en rapport om den åbne skitse: hvor mange frihedsgrader der er tilbage, og om den er fuldt bundet. Brug den til at finde ud af, hvad der stadig kan bevæge sig, før du afslutter skitsen.",
"help.sketch.intersect": "Tilføjer de kurver eller punkter, hvor emner, flader, kanter eller andre skitser krydser skitseplanet. De forbliver linkede og følger modellen, når den ændres.",
"help.sketch.intersection_curve": "Opretter 3D-kurver, hvor to emner, flader eller kurver fra andre skitser mødes, linket til dem. Brug dem som baner eller styrekurver, der følger modellen.",
"help.sketch.intersection_curve": "Opretter 3D-kurver, hvor to emner eller flader mødes, linket til dem. Brug dem som baner eller styrekurver, der følger modellen.",
"help.sketch.isoparametric_curve": "Opretter en 3D-kurve langs en flades U- eller V-retning gennem et punkt, du vælger, linket til fladen. Brug den som bane eller delelinje, der følger overfladen.",
"help.sketch.line": "Tegner forbundne linjer punkt for punkt som en åben kæde eller en lukket kontur. Det er det grundlæggende værktøj til de fleste profiler.",
"help.sketch.line.midpoint": "Tegner en linje fra dens midtpunkt ud til den ene ende, og den anden ende spejles. Brug den til linjer centreret om et punkt, f.eks. en symmetrisk kant.",
@@ -3807,8 +3809,6 @@
"sample.plate.name": "Eksempelplade",
"sample.vents.about": "Slidser i et rektangulært mønster: Rækker, kolonner og deling er parametre",
"sample.vents.name": "Ventilationsplade",
"scale.per_axis": "Per akse",
"scale.uniform": "Ensartet",
"search.command_keys": "Enter kør Shift+Enter kør og hold åben Esc luk",
"search.commands": "Kommandoer",
"search.design": "I dette design",
+30 -360
View File
File diff suppressed because it is too large Load Diff
+2 -14
View File
@@ -22,7 +22,8 @@
"cmd.component_cycle_color": "Cycle Component Colour",
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
"cmd.inspect_center_of_mass": "Centre of Mass",
"cmd.inspect_colored_components": "Component Colours",
"cmd.inspect_colored_components": "Enable Component Colours",
"cmd.inspect_colored_components_disable": "Disable Component Colours",
"cmd.parameters_favorite": "Favourite Parameter",
"cmd.render_aging_describe": "Layered Surface Aging",
"cmd.render_effects_describe": "Lens and Creative Effects",
@@ -86,7 +87,6 @@
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
"prefs.megabytes": "MB",
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
"prefs.select_hidden_edges_help": "Select edges through faces when Shaded with hidden edges is active, including for Fillet. Off keeps v0.30.1 selection behaviour. Hidden bodies stay unselectable.",
"prefs.sketch.center_arc": "Centre-point arc follows the cursor",
"prefs.sketch.conflict_colour": "Conflict colour",
"prefs.sketch.tracking_guides": "Centre and midpoint guides",
@@ -134,7 +134,6 @@
"render.aging_metallic": "Layer metallic fraction",
"render.aging_origin": "Field origin · world mm",
"render.aging_patina": "Copper-green patina",
"render.aging_raytrace_help": "Ageing is applied by CPU ray tracing and GPU path tracing. Raster keeps schematic rendering.",
"render.aging_refresh_targets": "Inspect current bodies",
"render.aging_roughness": "Layer roughness",
"render.aging_rust": "Oxidized iron / rust",
@@ -160,7 +159,6 @@
"render.background_workflow": "Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
"render.base_color": "Base colour · sRGB",
"render.bloom_help": "Creative output-only glow from positive linear highlights, spread with three normalised box passes. Radius is in output pixels. Added in linear display light before sRGB; not a lens, volumetric effect or convergence improvement. The raw EXR is unchanged.",
"render.check_media_remaining": "Density grids, displacement, full spectral integration and propagated polarisation remain unavailable.",
"render.chromatic_offset": "Chromatic edge offset",
"render.cpu_area_editor_help": "Full side vectors in millimeters; U cross V points towards the emitting side. This differs from GPU half-edge vectors.",
"render.cpu_light_help": "Up to eight directional lights. An empty custom list turns them off; selecting a preset restores its lights. Colours are linear RGB and intensity is relative, not watts.",
@@ -180,7 +178,6 @@
"render.flare_strength": "Highlight flare strength",
"render.flare_threshold": "Scene-linear flare threshold",
"render.frame_centre_mark": "Centre mark",
"render.glass_model_help": "Glass roughness scatters transmitted rays. Dispersion and film colour use three wavelength bands. Rough sheets require straight transmitted shadows off.",
"render.hdr_primaries_declared": "The source declares its colour primaries.",
"render.hdr_primaries_srgb": "Colour primaries: sRGB/D65",
"render.hdr_primaries_undeclared": "The source does not declare primaries; review the renderer colour requirements.",
@@ -238,12 +235,6 @@
"render.studio_area_help": "Up to four one-sided rectangles. Orthogonal half-edges U and V define size; U × V faces the lit side. Coordinates are world-space millimetres.",
"render.studio_center": "Centre X, Y, Z (mm)",
"render.studio_hdr_help": "Radiance .hdr, 2:1 panorama, up to 2048 × 1024. EXR is unavailable. The colour assumption applies only when primaries are unspecified.",
"render.surface_import.complete": "Choose an image and enter its author, source and licence.",
"render.surface_import.license": "Licence / ownership",
"sketch.hint.arc.center_point": "Arc: click centre and start, go round either way and click the end, or type a signed sweep",
"sketch.hint.circle.center": "Circle: click the centre, then a point on the circle",
"sketch.hint.constraint.midpoint": "MidPoint: pick a point and a line, or two lines to centre the second on the first",
"sketch.hint.rectangle.center": "Centre rectangle: click the centre, then a corner",
"sketch_text.align_center": "Centre",
"ui.3d_mouse_kind_cap": "Return to Centre",
"ui.3d_mouse_looks_like_cap": "The reading stays put while you hold it, the way a Return to Centre device does.",
@@ -271,9 +262,6 @@
"ui.layout_compact": "Compact",
"ui.layout_floating": "Floating",
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
"ui.mesh_import_origin_tip": "Centre the mesh on X and Y with its base on Z = 0. When off, the mesh keeps the coordinates stored in the file.",
"ui.organize": "Organise",
"ui.panel_layout.workspace.modelling": "Modelling",
"ui.point_cloud_color_by": "Colour By",
"ui.point_cloud_file_colors": "File Colours",
"ui.point_cloud_no_file_colors": "The file has no colours",
+62 -616
View File
File diff suppressed because it is too large Load Diff
+16 -1331
View File
File diff suppressed because it is too large Load Diff
+2 -824
View File
File diff suppressed because it is too large Load Diff
+3 -29
View File
@@ -566,7 +566,6 @@
"Select edges": "Sélectionner des arêtes",
"Select points of the sketch being edited": "Sélectionner des points de l'esquisse en cours de modification",
"Select the centre the pattern turns around first": "Sélectionner d'abord le centre autour duquel la répétition pivote",
"Select the curves to divide first": "Sélectionner d'abord les courbes à diviser",
"Select the curves to offset first": "Sélectionner d'abord les courbes à décaler",
"Select the mirror line": "Sélectionner la ligne de symétrie",
"Select the sketch curves or points to mirror first": "Sélectionner d'abord les courbes ou points d'esquisse à symétriser",
@@ -957,6 +956,8 @@
"cmd.inspect_accessibility": "Analyse d'accessibilité",
"cmd.inspect_center_of_mass": "Centre de gravité",
"cmd.inspect_clearance": "Jeu",
"cmd.inspect_colored_components": "Activer les couleurs des composants",
"cmd.inspect_colored_components_disable": "Désactiver les couleurs des composants",
"cmd.inspect_compare": "Comparer les corps",
"cmd.inspect_compare_picture": "Comparer les images",
"cmd.inspect_continuity": "Analyse de continuité",
@@ -1049,7 +1050,6 @@
"cmd.pointcloud_edit": "Modifier le nuage de points",
"cmd.pointcloud_insert": "Insérer un nuage de points",
"cmd.pointcloud_list": "Nuages de points",
"cmd.pointcloud_reload": "Recharger le nuage de points",
"cmd.render.compare.divider": "Déplacer le séparateur de comparaison des rendus",
"cmd.render.result.discard": "Écarter le résultat de rendu",
"cmd.render.result.select": "Sélectionner le résultat de rendu",
@@ -1195,7 +1195,6 @@
"cmd.sketch_options": "Options de la palette d'esquisse",
"cmd.sketch_paste": "Coller les entités d'esquisse",
"cmd.sketch_point3d": "Point 3D",
"cmd.sketch_point_divide": "Diviser la courbe",
"cmd.sketch_polygon_circumscribed": "Polygone circonscrit",
"cmd.sketch_polygon_edge": "Polygone par côté",
"cmd.sketch_polygon_inscribed": "Polygone inscrit",
@@ -1366,14 +1365,11 @@
"dialog.computing": "Calcul… {seconds} s",
"dialog.could_not_be_checked": "non vérifiable",
"dialog.custom_axis": "↖ Axe personnalisé",
"dialog.cylinder_face_tip": "La face ou le sommet du modèle sur lequel le cylindre est posé : cliquer ici, puis dessus dans la vue. Laissé vide, X, Y et Z le placent.",
"dialog.dimension_insert_hint": "Double-cliquez sur une autre cote pilotante pour insérer son nom",
"dialog.dimension_name": "Cote : {name}",
"dialog.edit_title": "MODIFIER {title}",
"dialog.no_shown_body": "Aucun corps affiché à modifier : affichez-en un ou choisissez Nouveau corps",
"dialog.pattern_sketch_not_object": "Choisissez des corps ou des fonctions : une esquisse n'est ni l'un ni l'autre. Répétez ses courbes avec la Répétition ou la Symétrie de l'esquisse, ou extrudez-la d'abord.",
"dialog.primitive_point_tip": "Un point qui remplit X, Y et Z : cliquer ici, puis sur un sommet, une arête ou une face du modèle, sur un point d'une esquisse ou sur l'origine.",
"dialog.reference_edge_tip": "Une arête droite de la face choisie, depuis laquelle la distance ci-dessous est mesurée : cliquer ici, puis sur l'arête.",
"dialog.select": "↖ Sélectionner",
"dialog.selected_count": "↖ {count} sélectionné(s)",
"dialog.selection_constraint": "Contrainte {id}",
@@ -1428,7 +1424,6 @@
"file_load.reading_percent": "Lecture du fichier... {percent} %",
"file_load.rebuilding": "Reconstruction de l’entité {done} sur {total}",
"file_load.recovering": "Récupération de la conception",
"file_load.reloading_cloud": "Rechargement du nuage de points",
"file_load.stopping": "Attente de l’arrêt de l’opération précédente...",
"file_load.updating_links": "Mise à jour des liens",
"file_load.waiting": "En attente d’une opération sur fichier",
@@ -1628,7 +1623,7 @@
"help.sketch.insert_svg": "Lit les tracés d'un dessin SVG dans une esquisse, mis à l'échelle, tournés et placés comme vous le définissez. À utiliser pour transformer un logo ou un contour vectoriel en courbes que vous pouvez extruder.",
"help.sketch.inspect": "Ouvre un rapport sur l'esquisse ouverte : combien de degrés de liberté il reste et si elle est entièrement contrainte. À utiliser pour savoir ce qui bouge encore avant de terminer l'esquisse.",
"help.sketch.intersect": "Ajoute les courbes ou points où des corps, faces, arêtes ou d'autres esquisses traversent le plan d'esquisse. Ils restent liés et suivent le modèle lorsqu'il change.",
"help.sketch.intersection_curve": "Crée des courbes 3D là où deux corps, faces ou courbes d'autres esquisses se rencontrent, liées à eux. À utiliser comme trajectoires ou rails guides qui suivent le modèle.",
"help.sketch.intersection_curve": "Crée des courbes 3D là où deux corps ou faces se rencontrent, liées à eux. À utiliser comme trajectoires ou rails guides qui suivent le modèle.",
"help.sketch.isoparametric_curve": "Crée une courbe 3D le long de la direction U ou V d'une face, par un point que vous choisissez, liée à la face. À utiliser comme trajectoire ou ligne de partage qui suit la surface.",
"help.sketch.line": "Dessine des lignes connectées point par point, en chaîne ouverte ou en contour fermé. C'est l'outil de base de la plupart des profils.",
"help.sketch.line.midpoint": "Dessine une ligne de son milieu vers une extrémité, l'autre extrémité étant son symétrique. À utiliser pour des lignes centrées sur un point, comme une arête symétrique.",
@@ -1640,7 +1635,6 @@
"help.sketch.pattern.circular": "Copie la géométrie d'esquisse autour d'un centre, sur un cercle complet ou un angle donné. À utiliser pour des cercles de perçages, des rayons et des dents.",
"help.sketch.pattern.rectangular": "Copie la géométrie d'esquisse en lignes et colonnes à des espacements donnés. À utiliser pour des grilles de trous, de rainures ou d'aérations.",
"help.sketch.point": "Place un point d'esquisse. À utiliser comme référence pour des cotes, des contraintes, des centres de perçage ou des fonctions ultérieures.",
"help.sketch.point.divide": "Place des points qui divisent les courbes choisies en longueurs égales. Les points restent sur chaque courbe et gardent leur espacement quand elle change.",
"help.sketch.point3d": "Place un point dans l'espace, avec Esquisse 3D activée. À utiliser comme référence pour des courbes et trajectoires 3D.",
"help.sketch.polygon.circumscribed": "Dessine un polygone régulier autour d'un cercle, dimensionné du centre au milieu d'un côté. À utiliser lorsque la largeur sur plats est connue, comme pour un écrou hexagonal.",
"help.sketch.polygon.edge": "Dessine un polygone régulier à partir des deux extrémités d'un côté. À utiliser lorsque la longueur et la position d'un côté sont connues.",
@@ -2206,8 +2200,6 @@
"prefs.edges": "Arêtes et ombrage",
"prefs.extend": "Étendre",
"prefs.extra_navigation": "Ctrl+glisser gauche dans le vide fait orbiter ; maintenir Espace pour un panoramique.",
"prefs.extrude_arrow_face_stops": "La flèche d'extrusion s'arrête aux faces",
"prefs.extrude_arrow_face_stops_note": "En faisant glisser la flèche de distance d'Extrusion, elle s'arrête aux faces planes parallèles au profil, comme l'autre côté d'un bloc qu'on découpe, et souligne la face. Désactivé : la flèche avance seulement par pas ronds.",
"prefs.extrude_view_isometric": "Passer la vue en isométrique à l'ouverture de l'extrusion",
"prefs.extrude_view_return": "Revenir à la vue précédente à la fermeture de l'extrusion",
"prefs.follows_os": "Suit le système",
@@ -2287,8 +2279,6 @@
"prefs.mouse": "Souris et pavé tactile",
"prefs.mouse_heading": "Souris",
"prefs.mouse_mapping": "Affectation des boutons de la souris",
"prefs.move_drag_highlights": "Mettre en évidence ce qui est sous le pointeur pendant que les flèches de Déplacer font glisser un composant",
"prefs.move_drag_highlights_tip": "Désactivé : pendant que les flèches de Déplacer font glisser un composant, rien n’est mis en évidence sous le pointeur, ce qui garde les grands assemblages fluides. Avec une coupe ou une analyse affichée, la mise en évidence reste, et une pièce glissée le long de ses liaisons n’en montre jamais.",
"prefs.navigation": "Navigation",
"prefs.navigation_note": "Comment la souris, le pavé tactile et la souris 3D déplacent la vue.",
"prefs.navigator_fit": "Cadrer le modèle après un changement de vue du repère de navigation",
@@ -2364,8 +2354,6 @@
"prefs.shortcuts": "Raccourcis",
"prefs.single_surface_analysis": "Utiliser une seule analyse de surface",
"prefs.single_surface_analysis_help": "Désactivé, l'ombrage de surface et Continuité peuvent rester actifs ensemble. Activé, seule la dernière analyse lancée ou modifiée est conservée.",
"prefs.sketch.escape_twice_finishes": "Un double Échap termine l'esquisse",
"prefs.sketch.escape_twice_finishes_note": "Deux pressions rapides sur Échap terminent l'esquisse. Désactivé : Échap annule seulement l'outil ou la sélection, et Terminer l'esquisse ferme l'esquisse.",
"prefs.sketch_align_points": "S'aligner sur d'autres points d'esquisse pendant le dessin",
"prefs.sketch_align_points_note": "Pendant le dessin, un point s'aligne à la fois horizontalement sur un point d'esquisse et verticalement sur un autre, avec un guide en pointillés, et le clic conserve les deux alignements. Désactivé : uniquement sur le point précédent, un axe à la fois.",
"prefs.sketch_conflict_colour": "Géométrie en conflit dans la couleur de conflit",
@@ -2789,8 +2777,6 @@
"render.cpu_lighting": "Préréglage d'éclairage",
"render.cpu_lights": "Lumières CPU",
"render.cpu_linear_color": "Couleur linéaire (R, V, B)",
"render.cpu_maps_saved_help": "Le rendu CPU accepte les cartes opaques enregistrées : choisissez une surface, puis enregistrez ses cartes dans la vue. Avec l’éclairage indirect, un autre corps peut utiliser Verre ; le corps avec les cartes ne le peut pas. Désactivez les réflexions sélectionnées, l’anisotropie et les effets procéduraux. Les fichiers temporaires de cartes utilisent le GPU.",
"render.cpu_material_certification": "La vérification CPU valide la géométrie de verre prise en charge avant le rendu. Le verre fermé utilise la distance géométrique.",
"render.cpu_neutral": "Neutre",
"render.cpu_opaque_required": "Le lancer de rayons CPU nécessite un arrière-plan opaque. Décochez Arrière-plan transparent pour continuer.",
"render.cpu_options_pending": "Ce panneau CPU ne peut pas encore modifier l'émission, les effets de surface, le verre ni l'anisotropie. Effacez ces options d'API avant de l'utiliser.",
@@ -3015,11 +3001,9 @@
"render.frame_wide": "16:9 · Écran large",
"render.gpu_depth": "Profondeur maximale des chemins",
"render.gpu_options_help": "Les échantillons, la profondeur, la graine et l'exposition sont enregistrés avec ce préréglage. Seule la méthode d'image active est enregistrée.",
"render.gpu_path_crashed": "SolidCraft s’est fermé pendant sa dernière préparation sur cet adaptateur graphique, très probablement dans le pilote graphique ; il n’est donc plus préparé de lui-même. Utilisez le lancer de rayons sur le CPU, ou réessayez (une mise à jour du pilote réessaie aussi).",
"render.gpu_path_failed": "Traceur de chemins GPU indisponible",
"render.gpu_path_first_setup": "La première initialisation sur ce GPU peut prendre plusieurs minutes ; SolidCraft reste utilisable pendant ce temps. Les démarrages suivants réutilisent les shaders compilés lorsque le pilote les conserve.",
"render.gpu_path_preparing": "Traceur de chemins GPU : compilation des shaders…",
"render.gpu_path_retry": "Réessayer",
"render.gpu_seed": "Graine aléatoire",
"render.gpu_unavailable": "Le tracé de chemins GPU n'est pas prêt sur cet appareil.",
"render.ground_apply": "Ajuster et enregistrer le sol",
@@ -3827,8 +3811,6 @@
"sample.plate.name": "Plaque d'exemple",
"sample.vents.about": "Rainures en répétition rectangulaire : lignes, colonnes et pas sont des paramètres",
"sample.vents.name": "Plaque d'aération",
"scale.per_axis": "Par axe",
"scale.uniform": "Uniforme",
"search.command_keys": "Entrée exécuter Maj+Entrée exécuter et garder ouvert Échap fermer",
"search.commands": "Commandes",
"search.design": "Dans cette conception",
@@ -3916,7 +3898,6 @@
"sketch.dimension_applied": "Cote appliquée",
"sketch.dimension_applied_defined": "Cote appliquée ; paramètre `{name}` défini",
"sketch.dimension_applied_renamed": "Cote appliquée ; paramètre nommé `{name}`",
"sketch.divide_hint": "Les points divisent chaque courbe en longueurs égales et les gardent quand elle change.",
"sketch.dof_left": "{count} degrés de liberté restants",
"sketch.dof_left_one": "{count} degré de liberté restant",
"sketch.draw_curves": "Dessiner en {linetype}",
@@ -4459,7 +4440,6 @@
"ui.click_selects_whole_bodies": "Un clic sélectionne des corps entiers",
"ui.click_then_press_key": "Cliquer, puis appuyer sur la nouvelle touche",
"ui.close": "Fermer",
"ui.close_tab": "Fermer l’onglet",
"ui.coil": "Hélice",
"ui.coincident": "Coïncidence",
"ui.collapse": "Réduire",
@@ -4706,7 +4686,6 @@
"ui.heading_create_sketch": "CRÉER UNE ESQUISSE",
"ui.heading_cylinder": "CYLINDRE",
"ui.heading_design": "DESIGN",
"ui.heading_divide_curve": "DIVISER LA COURBE",
"ui.heading_draft": "DÉPOUILLE",
"ui.heading_edit": "ÉDITION",
"ui.heading_equation_curve": "COURBE PAR ÉQUATION",
@@ -4864,8 +4843,6 @@
"ui.mesh_format_3mf": "3MF",
"ui.mesh_format_3mf_tagged": "3MF avec étiquettes de faces",
"ui.mesh_format_stl": "STL",
"ui.mesh_import_origin": "Déplacer à l'origine",
"ui.mesh_import_origin_tip": "Centre le maillage en X et Y et pose sa base sur Z = 0. Désactivé, le maillage garde les coordonnées enregistrées dans le fichier.",
"ui.minor_radius": "Petit rayon",
"ui.mirror": "Symétrie",
"ui.mirror_line": "Ligne de symétrie",
@@ -4946,7 +4923,6 @@
"ui.point_cloud_add_filter": "Ajouter un filtre…",
"ui.point_cloud_auto_range": "Plage automatique",
"ui.point_cloud_color_by": "Colorer par",
"ui.point_cloud_dots": "Points isolés",
"ui.point_cloud_file_colors": "Couleurs du fichier",
"ui.point_cloud_filter_by": "Filtrer par",
"ui.point_cloud_height_high": "Plus haut",
@@ -4955,7 +4931,6 @@
"ui.point_cloud_no_file_colors": "Le fichier n'a pas de couleurs",
"ui.point_cloud_pause": "Pause",
"ui.point_cloud_play": "Lire",
"ui.point_cloud_reload": "Recharger le fichier",
"ui.point_cloud_single_color": "Couleur unique",
"ui.point_cloud_size": "Taille des points",
"ui.point_cloud_units": "Unités du fichier",
@@ -5048,7 +5023,6 @@
"ui.second_rail": "Second rail",
"ui.section_analysis": "Analyse de coupe",
"ui.section_size": "Taille de la section",
"ui.segments": "Segments",
"ui.select": "Sélectionner",
"ui.selectOther": "Sélectionner un autre…",
"ui.selectTangentChain": "Sélectionner la chaîne tangente",
+3 -3
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Análise de accesibilidade",
"cmd.inspect_center_of_mass": "Centro de masas",
"cmd.inspect_clearance": "Folga",
"cmd.inspect_colored_components": "Activar as cores dos compoñentes",
"cmd.inspect_colored_components_disable": "Desactivar as cores dos compoñentes",
"cmd.inspect_compare": "Comparar corpos",
"cmd.inspect_compare_picture": "Comparar a imaxe",
"cmd.inspect_continuity": "Análise de continuidade",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Le os trazados dun debuxo SVG nun bosquexo, escalados, xirados e colocados onde indiques. Úsao para converter un logotipo ou un contorno vectorial en curvas que podes extruír.",
"help.sketch.inspect": "Abre un informe do bosquexo aberto: cantos graos de liberdade quedan e se está totalmente restrinxido. Úsao para saber o que aínda se move antes de rematar o bosquexo.",
"help.sketch.intersect": "Engade as curvas ou os puntos onde corpos, caras, arestas ou outros bosquexos cruzan o plano do bosquexo. Quedan ligados e seguen o modelo cando cambia.",
"help.sketch.intersection_curve": "Crea curvas 3D onde se atopan dous corpos, caras ou curvas doutros bosquexos, ligadas a eles. Úsaas como traxectorias ou guías que seguen o modelo.",
"help.sketch.intersection_curve": "Crea curvas 3D onde se atopan dous corpos ou caras, ligadas a eles. Úsaas como traxectorias ou guías que seguen o modelo.",
"help.sketch.isoparametric_curve": "Crea unha curva 3D na dirección U ou V dunha cara por un punto que escollas, ligada á cara. Úsaa como traxectoria ou liña de división que segue a superficie.",
"help.sketch.line": "Debuxa liñas conectadas punto a punto, como cadea aberta ou como contorno pechado. É a ferramenta básica para a maioría dos perfís.",
"help.sketch.line.midpoint": "Debuxa unha liña desde o seu punto medio ata un extremo, co outro extremo reflectido. Úsao para liñas centradas nun punto, como unha aresta simétrica.",
@@ -3807,8 +3809,6 @@
"sample.plate.name": "Placa de exemplo",
"sample.vents.about": "Ranuras nun patrón rectangular: filas, columnas e paso son parámetros",
"sample.vents.name": "Placa de ventilación",
"scale.per_axis": "Por eixo",
"scale.uniform": "Uniforme",
"search.command_keys": "Intro executar Maiús+Intro executar e manter aberto Esc pechar",
"search.commands": "Ordes",
"search.design": "Neste deseño",
+16 -607
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Analiza dostupnosti",
"cmd.inspect_center_of_mass": "Središte mase",
"cmd.inspect_clearance": "Provjera zazora",
"cmd.inspect_colored_components": "Omogući boje komponenti",
"cmd.inspect_colored_components_disable": "Onemogući boje komponenti",
"cmd.inspect_compare": "Usporedi tijela",
"cmd.inspect_compare_picture": "Usporedi sliku",
"cmd.inspect_continuity": "Analiza kontinuiteta",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Čita putanje SVG crteža u skicu, skalirane, zakrenute i postavljene kako zadate. Upotrijebite ga za pretvaranje logotipa ili vektorskog obrisa u krivulje koje možete ekstrudirati.",
"help.sketch.inspect": "Otvara izvješće o otvorenoj skici: koliko je stupnjeva slobode preostalo i je li potpuno ograničena. Upotrijebite ga kako biste saznali što se još pomiče prije dovršetka skice.",
"help.sketch.intersect": "Dodaje krivulje ili točke u kojima tijela, lica, bridovi ili druge skice presijecaju ravninu skice. Ostaju povezane i prate model kad se promijeni.",
"help.sketch.intersection_curve": "Izrađuje 3D krivulje na mjestu gdje se dva tijela, lica ili krivulje drugih skica susreću, povezane s njima. Upotrijebite ih kao putanje ili vodilice koje prate model.",
"help.sketch.intersection_curve": "Izrađuje 3D krivulje na mjestu gdje se dva tijela ili lica susreću, povezane s njima. Upotrijebite ih kao putanje ili vodilice koje prate model.",
"help.sketch.isoparametric_curve": "Izrađuje 3D krivulju u smjeru U ili V lica kroz točku koju odaberete, povezanu s licem. Upotrijebite je kao putanju ili liniju razdvajanja koja prati plohu.",
"help.sketch.line": "Crta povezane linije točku po točku, kao otvoreni lanac ili zatvoreni obris. To je osnovni alat za većinu profila.",
"help.sketch.line.midpoint": "Crta liniju od njezine središnje točke do jednog kraja, a drugi kraj zrcali. Upotrijebite ga za linije centrirane na točku, primjerice simetričan brid.",
+3 -3
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
"cmd.inspect_center_of_mass": "Tömegközéppont",
"cmd.inspect_clearance": "Hézag",
"cmd.inspect_colored_components": "Komponensszínek bekapcsolása",
"cmd.inspect_colored_components_disable": "Komponensszínek kikapcsolása",
"cmd.inspect_compare": "Testek összehasonlítása",
"cmd.inspect_compare_picture": "Kép összehasonlítása",
"cmd.inspect_continuity": "Folytonosságelemzés",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Beolvassa egy SVG-rajz útvonalait egy vázlatba, az Ön által megadott léptékkel, elforgatással és helyen. Használja egy logó vagy vektoros körvonal kihúzható görbékké alakítására.",
"help.sketch.inspect": "Jelentést nyit a megnyitott vázlatról: hány szabadságfok maradt, és teljesen kényszerezett-e. Használja annak kiderítésére, mi mozdul még, mielőtt befejezi a vázlatot.",
"help.sketch.intersect": "Hozzáadja azokat a görbéket vagy pontokat, ahol testek, lapok, élek vagy más vázlatok metszik a vázlatsíkot. Kapcsoltak maradnak, és követik a modellt, ha az megváltozik.",
"help.sketch.intersection_curve": "3D görbéket hoz létre ott, ahol két test, lap vagy más vázlatok görbéi találkozik, hozzájuk kapcsolva. Használja őket útvonalként vagy vezérgörbeként, amely követi a modellt.",
"help.sketch.intersection_curve": "3D görbéket hoz létre ott, ahol két test vagy lap találkozik, hozzájuk kapcsolva. Használja őket útvonalként vagy vezérgörbeként, amely követi a modellt.",
"help.sketch.isoparametric_curve": "3D görbét hoz létre egy lap U vagy V iránya mentén egy kiválasztott ponton át, a laphoz kapcsolva. Használja útvonalként vagy osztóvonalként, amely követi a felületet.",
"help.sketch.line": "Összefüggő vonalakat rajzol pontról pontra, nyitott láncként vagy zárt körvonalként. Ez a legtöbb profil alapeszköze.",
"help.sketch.line.midpoint": "Vonalat rajzol a felezőpontjától az egyik végéig, a másik véget tükrözve. Használja egy pontra középen álló vonalakhoz, például szimmetrikus élhez.",
@@ -3807,8 +3809,6 @@
"sample.plate.name": "Mintalemez",
"sample.vents.about": "Hornyok téglalap mintában: a sorok, oszlopok és osztás paraméterek",
"sample.vents.name": "Szellőzőlemez",
"scale.per_axis": "Tengelyenként",
"scale.uniform": "Egyenletes",
"search.command_keys": "Enter futtatás Shift+Enter futtatás és nyitva tartás Esc bezárás",
"search.commands": "Parancsok",
"search.design": "Ebben a tervben",
+3 -3
View File
@@ -957,6 +957,8 @@
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
"cmd.inspect_center_of_mass": "Pusat Massa",
"cmd.inspect_clearance": "Kelonggaran",
"cmd.inspect_colored_components": "Aktifkan Warna Komponen",
"cmd.inspect_colored_components_disable": "Nonaktifkan Warna Komponen",
"cmd.inspect_compare": "Bandingkan Bodi",
"cmd.inspect_compare_picture": "Bandingkan dengan Gambar",
"cmd.inspect_continuity": "Analisis Kontinuitas",
@@ -1621,7 +1623,7 @@
"help.sketch.insert_svg": "Membaca path gambar SVG ke dalam sketsa, diskalakan, diputar, dan ditempatkan sesuai pengaturan Anda. Gunakan untuk mengubah logo atau kontur vektor menjadi kurva yang dapat diekstrusi.",
"help.sketch.inspect": "Membuka laporan tentang sketsa yang terbuka: berapa derajat kebebasan yang tersisa dan apakah sketsa terbatasi penuh. Gunakan untuk mengetahui apa yang masih bergerak sebelum Anda menyelesaikan sketsa.",
"help.sketch.intersect": "Menambahkan kurva atau titik tempat bodi, muka, tepi, atau sketsa lain memotong bidang sketsa. Semuanya tetap tertaut dan mengikuti model saat model berubah.",
"help.sketch.intersection_curve": "Membuat kurva 3D di tempat dua bodi, muka, atau kurva sketsa lain bertemu, tertaut padanya. Gunakan sebagai jalur atau rel pemandu yang mengikuti model.",
"help.sketch.intersection_curve": "Membuat kurva 3D di tempat dua bodi atau muka bertemu, tertaut padanya. Gunakan sebagai jalur atau rel pemandu yang mengikuti model.",
"help.sketch.isoparametric_curve": "Membuat kurva 3D sepanjang arah U atau V sebuah muka melalui titik yang Anda pilih, tertaut ke muka itu. Gunakan sebagai jalur atau garis pisah yang mengikuti permukaan.",
"help.sketch.line": "Menggambar garis-garis tersambung titik demi titik, sebagai rantai terbuka atau kontur tertutup. Ini alat dasar untuk sebagian besar profil.",
"help.sketch.line.midpoint": "Menggambar garis dari titik tengahnya ke satu ujung, dengan ujung lainnya dicerminkan. Gunakan untuk garis yang dipusatkan pada sebuah titik, seperti tepi simetris.",
@@ -3807,8 +3809,6 @@
"sample.plate.name": "Pelat Contoh",
"sample.vents.about": "Slot dalam pola persegi panjang: baris, kolom, dan pitch adalah parameter",
"sample.vents.name": "Pelat Ventilasi",
"scale.per_axis": "Per sumbu",
"scale.uniform": "Seragam",
"search.command_keys": "Enter jalankan Shift+Enter jalankan dan tetap buka Esc tutup",
"search.commands": "Perintah",
"search.design": "Di desain ini",
+3 -14
View File
@@ -567,7 +567,6 @@
"Select edges": "Seleziona spigoli",
"Select points of the sketch being edited": "Seleziona punti dello schizzo in modifica",
"Select the centre the pattern turns around first": "Seleziona prima il centro attorno a cui ruota la serie",
"Select the curves to divide first": "Selezionare prima le curve da dividere",
"Select the curves to offset first": "Seleziona prima le curve per l'offset",
"Select the mirror line": "Seleziona la linea di simmetria",
"Select the sketch curves or points to mirror first": "Seleziona prima le curve o i punti dello schizzo da specchiare",
@@ -958,6 +957,8 @@
"cmd.inspect_accessibility": "Analisi di accessibilità",
"cmd.inspect_center_of_mass": "Centro di massa",
"cmd.inspect_clearance": "Distanza libera",
"cmd.inspect_colored_components": "Attiva colori dei componenti",
"cmd.inspect_colored_components_disable": "Disattiva colori dei componenti",
"cmd.inspect_compare": "Confronta corpi",
"cmd.inspect_compare_picture": "Confronta immagine",
"cmd.inspect_continuity": "Analisi di continuità",
@@ -1195,7 +1196,6 @@
"cmd.sketch_options": "Opzioni della tavolozza schizzo",
"cmd.sketch_paste": "Incolla entità dello schizzo",
"cmd.sketch_point3d": "Punto 3D",
"cmd.sketch_point_divide": "Dividi curva",
"cmd.sketch_polygon_circumscribed": "Poligono circoscritto",
"cmd.sketch_polygon_edge": "Poligono per lato",
"cmd.sketch_polygon_inscribed": "Poligono inscritto",
@@ -1366,14 +1366,11 @@
"dialog.computing": "Calcolo in corso… {seconds} s",
"dialog.could_not_be_checked": "impossibile verificare",
"dialog.custom_axis": "↖ Asse personalizzato",
"dialog.cylinder_face_tip": "La faccia o il vertice del modello su cui poggia il cilindro: fai clic qui, poi su di esso nella vista. Se resta vuoto, lo posizionano X, Y e Z.",
"dialog.dimension_insert_hint": "Fai doppio clic su un’altra quota di controllo per inserirne il nome",
"dialog.dimension_name": "Quota: {name}",
"dialog.edit_title": "MODIFICA {title}",
"dialog.no_shown_body": "Nessun corpo visibile da modificare: mostrane uno o scegli Nuovo corpo",
"dialog.pattern_sketch_not_object": "Seleziona corpi o lavorazioni: uno schizzo non è nessuno dei due. Ripeti le sue curve con Ripetizione o Specchia dello schizzo stesso, oppure estrudilo prima.",
"dialog.primitive_point_tip": "Un punto che compila X, Y e Z: fai clic qui, poi su un vertice, uno spigolo o una faccia del modello, su un punto di uno schizzo oppure sull'origine.",
"dialog.reference_edge_tip": "Uno spigolo rettilineo della faccia scelta da cui si misura la distanza sotto: fai clic qui, poi sullo spigolo.",
"dialog.select": "↖ Seleziona",
"dialog.selected_count": "↖ Selezionati: {count}",
"dialog.selection_constraint": "Vincolo {id}",
@@ -1627,7 +1624,7 @@
"help.sketch.insert_svg": "Legge i tracciati di un disegno SVG in uno schizzo, scalati, ruotati e posizionati come imposti. Usalo per trasformare un logo o un contorno vettoriale in curve che puoi estrudere.",
"help.sketch.inspect": "Apre un rapporto sullo schizzo aperto: quanti gradi di libertà restano e se è completamente vincolato. Usalo per scoprire cosa si muove ancora prima di terminare lo schizzo.",
"help.sketch.intersect": "Aggiunge le curve o i punti in cui corpi, facce, spigoli o altri schizzi attraversano il piano dello schizzo. Restano collegati e seguono il modello quando cambia.",
"help.sketch.intersection_curve": "Crea curve 3D dove si incontrano due corpi, facce o curve di altri schizzi, collegate a essi. Usale come percorsi o guide che seguono il modello.",
"help.sketch.intersection_curve": "Crea curve 3D dove si incontrano due corpi o facce, collegate a essi. Usale come percorsi o guide che seguono il modello.",
"help.sketch.isoparametric_curve": "Crea una curva 3D lungo la direzione U o V di una faccia passante per un punto che scegli, collegata alla faccia. Usala come percorso o linea di divisione che segue la superficie.",
"help.sketch.line": "Disegna linee collegate punto per punto, come catena aperta o contorno chiuso. È lo strumento di base per la maggior parte dei profili.",
"help.sketch.line.midpoint": "Disegna una linea dal suo punto medio fino a un'estremità, con l'altra estremità speculare. Usalo per linee centrate su un punto, come un bordo simmetrico.",
@@ -1639,7 +1636,6 @@
"help.sketch.pattern.circular": "Copia la geometria dello schizzo attorno a un punto centrale, su un cerchio completo o su un angolo stabilito. Usalo per cerchi di bulloni, raggi e denti.",
"help.sketch.pattern.rectangular": "Copia la geometria dello schizzo in righe e colonne con spaziature stabilite. Usalo per griglie di fori, asole o prese d'aria.",
"help.sketch.point": "Colloca un punto dello schizzo. Usalo come riferimento per quote, vincoli, centri di fori o lavorazioni successive.",
"help.sketch.point.divide": "Posiziona punti che dividono le curve scelte in lunghezze uguali. I punti restano su ogni curva e ne mantengono la spaziatura quando cambia.",
"help.sketch.point3d": "Colloca un punto nello spazio, con Schizzo 3D attivo. Usalo come riferimento per curve e percorsi 3D.",
"help.sketch.polygon.circumscribed": "Disegna un poligono regolare attorno a un cerchio, dimensionato dal centro al punto medio di un lato. Usalo quando è nota l'apertura chiave, come per un dado esagonale.",
"help.sketch.polygon.edge": "Disegna un poligono regolare a partire dai due estremi di un lato. Usalo quando sono note la lunghezza e la posizione di un lato.",
@@ -3816,8 +3812,6 @@
"sample.plate.name": "Piastra di esempio",
"sample.vents.about": "Asole in ripetizione rettangolare: righe, colonne e passo sono parametri",
"sample.vents.name": "Piastra di ventilazione",
"scale.per_axis": "Per asse",
"scale.uniform": "Uniforme",
"search.command_keys": "Invio esegui Maiusc+Invio esegui e tieni aperto Esc chiudi",
"search.commands": "Comandi",
"search.design": "In questo progetto",
@@ -3905,7 +3899,6 @@
"sketch.dimension_applied": "Quota applicata",
"sketch.dimension_applied_defined": "Quota applicata; parametro `{name}` definito",
"sketch.dimension_applied_renamed": "Quota applicata; parametro denominato `{name}`",
"sketch.divide_hint": "I punti dividono ogni curva in lunghezze uguali e le mantengono quando cambia.",
"sketch.dof_left": "{count} gradi di libertà rimanenti",
"sketch.dof_left_one": "{count} grado di libertà rimanente",
"sketch.draw_curves": "Disegna come {linetype}",
@@ -4695,7 +4688,6 @@
"ui.heading_create_sketch": "CREA SCHIZZO",
"ui.heading_cylinder": "CILINDRO",
"ui.heading_design": "PROGETTAZIONE",
"ui.heading_divide_curve": "DIVIDI CURVA",
"ui.heading_draft": "SFORMO",
"ui.heading_edit": "MODIFICA",
"ui.heading_equation_curve": "CURVA DA EQUAZIONE",
@@ -4854,8 +4846,6 @@
"ui.mesh_format_3mf": "3MF",
"ui.mesh_format_3mf_tagged": "3MF con etichette delle facce",
"ui.mesh_format_stl": "STL",
"ui.mesh_import_origin": "Sposta all'origine",
"ui.mesh_import_origin_tip": "Centra la mesh in X e Y con la base su Z = 0. Se disattivato, la mesh mantiene le coordinate salvate nel file.",
"ui.minor_radius": "Raggio minore",
"ui.mirror": "Specchia",
"ui.mirror_line": "Asse di simmetria",
@@ -5036,7 +5026,6 @@
"ui.second_rail": "Seconda guida",
"ui.section_analysis": "Analisi in sezione",
"ui.section_size": "Dimensione della sezione",
"ui.segments": "Segmenti",
"ui.select": "Seleziona",
"ui.selectOther": "Seleziona altro…",
"ui.selectTangentChain": "Seleziona catena tangente",
+53 -607
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -963,7 +963,8 @@
"cmd.inspect_analysis": "ანალიზის ნახვა",
"cmd.inspect_center_of_mass": "მასის ცენტრი",
"cmd.inspect_clearance": "ღრეჩო",
"cmd.inspect_colored_components": "კომპონენტების ფერები",
"cmd.inspect_colored_components": "კომპონენტების ფერების ჩართვა",
"cmd.inspect_colored_components_disable": "კომპონენტების ფერების გამორთვა",
"cmd.inspect_compare": "სხეულების შედარება",
"cmd.inspect_compare_picture": "სურათთან შედარება",
"cmd.inspect_continuity": "უწყვეტობის ანალიზი",

Some files were not shown because too many files have changed in this diff Show More