Compare commits

...
179 Commits
Author SHA1 Message Date
bherbruck c29b0d2f13 Release 0.10.1 2026-10-14 14:41:19 +00:00
bherbruckandClaude Opus 5.5 480fd8d517 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

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

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:41:16 +00:00
bherbruckandClaude Opus 5.5 4c09fa5355 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
bherbruck 626fe6fdee Release 0.10.0 (#1010) 2026-10-14 05:38:13 -04:00
selune 01f2c78cd1 Validate sourced physical material property cards and elastic stability (#934)
Adds the read-only material.properties.validate command: schema_version 1 engineering material cards in SI units are checked for numerical consistency and elastic stability, with derived isotropic moduli, missing conditions and sources needing review. Nothing is assigned or saved.

Thanks @0xlillith.
2026-10-14 05:28:18 -04:00
Karl Von Richter 9ca7d307a4 Offset of an irregular closed spline no longer fails its error certificate; offset fit nodes stay private (#1009)
Fixes #971. At the node limit the offset certificate splits the fitted curve where it meets the exact offset midpoint, so curves with different parameter speeds certify within the same 0.002 mm tolerance. Associated offset fit nodes are hidden from display, picking, box selection and snapping until Break Association.

Thanks @Karl-VonRichter.
2026-10-14 05:27:39 -04:00
bherbruck 2f91338d3d Large STEP imports: no more multi-minute hang, shared STEP text, cancel and progress (#925) 2026-10-14 05:01:12 -04:00
bherbruckandClaude Opus 5.5 dfbf7dd6a8 fix: a lenient parameter boundary found across a seam is not used; record seed 81 (#1003)
#970's lenient retry of a face's parameter boundary searches the whole surface as a last resort; on robustness seed 20 it found an edge's points across a cylinder's seam, half a turn apart, and the Cut failed. A lenient boundary whose points jump half the parameter range or more is refused, and the face is left undivided as before (#970's elbows step at most a fifth of the range). Seed 81's volume moved by 0.1 mm3 with #952's hole direction; recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:56:39 -04:00
Mateusz SychandSychPL cf9d7772f1 To / From Object finds a target in another, moved component (#993)
Fixes #928. Extrude To / From Object and Revolve To Object now resolve their target in the feature's own component frame, so a face in another component whose occurrence is moved is found where it is shown instead of the extrude stopping at a flat plane through the picked point. The point is named after the nearest face, a named point keeps its named face when another component touches it there, and such features re-evaluate when a placement changes, so they follow the target when it moves later.

Co-authored-by: SychPL <SychPL@users.noreply.github.com>
2026-10-14 04:41:59 -04:00
midasdfandmidasdf 6e84c03946 Curvature-continuous (G2) fillets for straight planar edges (#862)
Fillet gets a Curvature (G2) radius type (type: "curvature", alias "g2"): a quintic blend whose size is the equivalent circular setback, for convex and concave straight edges between planar faces with a perpendicular planar end face at each end. Unsupported cases are refused rather than falling back to a circular fillet, and a blend that would cross a loop in its end face is refused. Persisted as a fillet style under format 19; labels in en/de/ja/ko/zh.

Co-authored-by: midasdf <midasdf@users.noreply.github.com>
2026-10-14 04:26:53 -04:00
froglemonade2andfroglemonade2 3669d4b3d0 Cut a hole tangent inside a stepped boss (zero-thickness curved wall) (#986)
Touching sews now split 4-use straight edges on extruded walls (along their rulings), not only on planes. Faces round such an edge are ordered by the way they leave it and, where they leave it tangent, by their curvature. Sheets bound the material wedges as before, except where that would make a face touch itself (a hole touching the outline from inside); edges ending there pair the empty wedges instead, so each loop gets its own vertex. Coincident faces are still refused, plain sew still refuses vertex contact, and shell_groups is unchanged.

A Ø10 hole tangent inside a stepped Ø40/Ø20 boss now cuts exactly instead of failing with "wire is not simple". Robustness seeds 42 and 51 are recorded: their touching hole patterns in stepped bodies now cut exactly instead of by a tool nudge.

Co-authored-by: froglemonade2 <froglemonade2@users.noreply.github.com>
2026-10-14 03:55:06 -04:00
NOWN-DEVandNOWN-DEV 345c0882cd Renders hide edges just behind a face; lines on steep faces draw whole (#983)
The CPU renderer's line depth test allowed a fixed share of the scene's depth range (2e-4), which on large designs or close views let edges a millimetre or more behind thin sheet show through it. Each pixel now keeps the screen depth slope of the surface it shows, and a line passes where it is no further behind than that slope spans across the line's width, plus f32 rounding. Section caps use their plane's slope.

Edges lying on steep faces now draw their full width; edges behind a face stay hidden. Only crates/render/src/raster.rs changes; the GPU viewport is unchanged.

Co-authored-by: NOWN-DEV <NOWN-DEV@users.noreply.github.com>
2026-10-14 03:28:31 -04:00
bherbruckandClaude Opus 5.5 18347b3d0d Boolean nudge: no sliver where a tool only touches a round side (#827, seen in #897) (#941)
* Boolean nudge tried both ways: no sliver where a cut's end touches a round side (#897, #827)

Robustness seed 14 cuts a cylinder whose end lies exactly on the apex of the
plate's arc side. Since the display tessellation plans (#805) the generator sees
the arc's true extent, so the cut's base lands on -10 instead of -10.000000000000002
and the end is tangent to the arc. The direct boolean fails there and the
last-resort tool nudge pushed the end into the arc, leaving a planar sliver a
tenth of a millimetre wide; shell then picked that sliver and refused it. The
13 October merges were not the cause (the bisect lands on #805).

The nudge is now tried both ways and the simpler result (fewer faces) is kept,
so the opposite nudge parts the touching faces instead. The same change joins
the hinge barrel in #827, whose top lies tangent to the lid's underside where
the mounting leaf ends. Each result still passes the same checks as before.

Goldens: seeds 14, 26 and 85 now build to the end, 31 and 78 come out with
fewer faces. Seeds 6, 43 and 73 still stop at shell: their joins now succeed and
shell reaches corners it can't offset (a cusp where a box face touches an arc,
coplanar faces facing opposite ways), which is unchanged.

Fixes #897
Fixes #827

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

* Nudge the other way only when the first nudge fails or leaves a sliver

Trying both nudges and keeping the one with fewer faces could prefer a result
that keeps two pieces joined by a hair over one that separates them (the
centred coplanar revolve cut). The opposite nudge now runs only when the first
one failed or left a face narrower than 1/200 of the part, and it is taken
when it works with fewer of those.

Goldens re-recorded on current main: seeds 14, 26 and 85 build to the end,
31 and 78 with fewer faces. The other changed seeds are main's own since its
last full record, identical with and without this change.

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

* Nudge the other way only for slivers the nudge made

On the sensor enclosure's lid the first nudge left thin faces the operands
already had (thin walls, small rounds), so the opposite nudge ran a second
boolean for nothing and the press/pull took 79 s against main's 55 s. The
opposite nudge now runs only when the first one failed or left more slivers
than the operands brought; the press/pull test takes 36 s against 42 s on main
(same machine, one after the other).

Goldens re-recorded on top of #940.

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

* Goldens on current main: seeds 31, 43 and 78 with the two-way nudge

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

* Keep the hinge and tangent-cut tests beside the #871 fillet test

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

* Goldens: seeds 33 and 91 as main's fillet changes build them

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

* Goldens: keep main's #950 record

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:55:37 -04:00
bherbruckandClaude Opus 5.5 ac84491638 test: web-offline-test reads the worker's caches without opening them (#982)
The state probe opened every cache it listed; when a worker deleted one in between, the open made it again, empty, and the next wait timed out. The probe now reads each cache with caches.match over the builds' files and marks, which never makes a cache (3 failures in 20 runs before, 0 in 30 after). The worker only ever starts a complete build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:54:45 -04:00
NOWN-DEV 6a67cc8f1e An edge flange folds beside a ledge as tall as its setback (#978)
Trimming the base at an edge flange's setback snaps corners within tolerance onto the trim line and merges away the zero-width run a ledge as tall as the setback left along it, so the ledge goes with the strip as a lower one does. Splitting outline edges at other cells' corners now skips points within the model's tolerance of an end, so a ledge a little taller keeps its sliver and the bend runs under it, also off the axes. Both used to fail with "sew: a degenerate edge". Nothing new is stored.
2026-10-14 02:51:29 -04:00
bherbruckandClaude Opus 5.5 6cb3025b72 Sheet metal: a hole drilled into a rim is a bore its panel keeps (#560) (#955)
A hole drilled along a panel, into its rim, has no place in the flat pattern
as a cut-out, so every such hole was refused. It is now kept as a bore of that
panel, in flat coordinates, and cut only from that panel's material when the
folded part and its flat pattern are built, so the sheet still unfolds and
folds back with it. A hole that would run into a bend, or cross a panel at an
angle, is still refused. The flat pattern lists the bores.

The action matrix worker takes ACTION_MATRIX_ONLY (just these actions) and
ACTION_MATRIX_UNFOLD (unfold and refold every sheet after each action) for
rechecking one group by hand.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:31:19 -04:00
bherbruckandClaude Opus 5.5 b34d45233d Holes on round faces from the app, and Flip for holes at sketch points (#980)
* Holes on round faces from the app, one hole per pick, and Flip for holes at sketch points (#967)

The Hole dialog takes any face: a hole on a round face drills against the face's normal where it
was picked, and its arrow shows that. A round face split at its seam is picked whole, but makes
one hole where it was clicked. Holes at sketch points get Flip (saved as flip in the hole's points,
under format 18), in the command and in the dialog.

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

* Holes at sketch points show their arrow: into the part, or away with Flip (#967)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:10:09 -04:00
Karl Von Richter 47a7f29485 Guide sketch patterns with live previews and editable offsets (#966)
Sketch Mirror, rectangular and circular Pattern, and Offset now show a green live preview that follows the selection and settings; OK commits, Cancel leaves the sketch unchanged. The shared dialogs show the current selection step and source counts, sources can be toggled with Ctrl/Cmd-click, cleared and reselected.

Offsets keep editable driving distance dimensions (One Side, Two Sides, Symmetric) that update the derived curves without moving the source, with undo/redo and save/reopen. Format 19: older files keep their fixed offsets.

New dialog labels in en, zh, ja, de and ko.

Fixes #696. Fixes #517.
2026-10-14 01:58:01 -04:00
Giovanni 9c51f9d433 Retry a face's parameter boundary leniently (#970)
Cutting one lofted round tube from another failed in truck's divide_face ("parameter boundary") for some section counts: the intersection points lie on the loft's seam, the previous point's parameter can sit a hair outside the range, and the Newton search from there fails.

The boundary is built as before; only when that returns None is it built again leniently, starting each search from the hint clamped into the parameter range and, as a last resort, searching the whole surface. Nothing that succeeds today changes. The boolean-retry guards are untouched.

Fixes 5 of the 6 failing hollow-elbow cases in #968 (new test crates/engine/src/tests/loft_cut.rs, volumes within 0.3% of exact). 90 degrees / 14 sections still fails in a later step and stays open in #968.
2026-10-14 01:50:00 -04:00
NOWN-DEV 4687e3b073 Convert to Sheet Metal keeps a plate's round holes round (#977)
The plate check compared the face outline's area (from the tessellation, so each hole was a polygon of chords) with the exact volume, and refused plates with a few small holes. Where the outline runs along a cylinder about the plate's normal it is now an arc of that circle; coaxial pieces of one radius are one wall, so a hole split into two, three or four faces is one circle. The check passes with its tolerance unchanged, and the flat pattern and its DXF carry one CIRCLE per hole (ARC for rounded corners and slot ends).

A round blank now converts too, so unfold_cut_refold_and_convert checks the refusal with a block thicker at one end instead of a rod.
2026-10-14 01:47:40 -04:00
bherbruckandClaude Opus 5.5 1ff16cc72b A hole on a face drills into the material, also in older designs (#950) (#952)
* Hole matrix: placement, types, extents, edits and 0.9.0 designs, with today's failures listed

Each case checks the drill goes into the material, the bore sits on its axis, a blind hole stops
where it should, the removed volume against its analytic value, the face count and that every
body is still one closed valid solid. Failing cases are listed in tests/hole_matrix/known_failures.txt
as a ratchet.

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

* A hole on a face drills into the material, also in older designs (#950)

Without a direction, solid.hole took the direction from any face whose infinite plane passed
within ten tolerances of the point, the last one found winning: the far side of a thin plate or a
face of another body at the same height could set it, and the wrong direction was saved. It now
uses the face nearest the point, against its outward normal (exact for planes, cylinders and
spheres, so holes on curved faces work without a direction). When a design is evaluated, a face
hole whose saved direction leaves the face it starts on is mirrored into it, so designs saved
with the wrong direction drill into the part.

Fixes #950.

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

* Hole at a point on no face (a planar face's centroid outside it) keeps the plane it lies in

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

* Record the hole direction fix in the goldens and the action matrix baseline (#950)

Seeds 7, 21, 81 and 85 drill their holes straight into their faces now. In the action matrix,
holes on four faces that used to go the wrong way now build, and two whose correct direction
meets a cut the kernel refuses are marked failing.

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

* Goldens: seeds 7, 21 and 85 drill their holes straight in (#950)

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:45:11 -04:00
bherbruckandClaude Opus 5.5 813bee8914 Fillet: refuse a result with openings in its surface (#871) (#949)
* Fillet: refuse a result with openings in its surface (#871)

A fillet on #871's part (radius 1 on a window's edges and on the sides of a
block 1.26 mm away) came back closed as topology with every face meshing, but
the rounds overlapped and the end faces' boundary loops crossed, so the model
showed holes. The constant-radius fillet now checks its result before taking it:
the body's mesh, welded within a millionth of its size, must have no edge
without as many triangles running the other way along it, except for cracks
within the mesh tolerance where fitted curves meet. A result that fails is
refused, so the corner build gets its try and the radius search treats it as
too large.

Body::open_mesh_edges is the check. Kernel tests cover the #871 part (refused
at radius 1, closed with a round on every edge at 0.5), and an engine test runs
the fillet from the report and leaves the part as it was. All 96 robustness
seeds build the same with and without it.

Fixes #871

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

* Check fillets for crossed face boundaries, not free mesh edges

Free edges in the mesh also come from cracks where fitted curves meet (corner
patches, rounds ending on curved edges), so the mesh test refused good fillets.
The check is now on the cause of #871: a planar face whose boundary loops cross
each other or themselves has no inside. Body::crossed_face_loops samples each
planar face's loops within 1/1000 of the size and counts proper crossings
(touching within a millionth doesn't count).

The pyramid apex blend and the loft fillets build again. Ten action-matrix fillets
stay refused, recorded in the baseline: r = 3 on the 2 mm rim of oracle 09's
shelled box (the round runs past the inner wall; the result was 240 mm3 lighter
than any round can make it), and the six seam-edge fillets on oracle 60's bolt
holes, whose boundary jumps across the hole.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:36:30 -04:00
Mateusz SychandKarl-VonRichter 482abb4562 test(mcp): compare the scratch folder by its canonical path (#965)
PathScope stores its roots canonicalised (\\?\ on Windows, /private/var on macOS), so the default-folders test now canonicalises scratch_dir() before looking it up, like the recovery folder on the line above.

The same fix came in from @Karl-VonRichter in #969, found on macOS.

Co-authored-by: Karl-VonRichter <20079846+Karl-VonRichter@users.noreply.github.com>
2026-10-14 01:26:51 -04:00
7bed28752e Sheet metal: fold a second tab on a line, and a tab on an L's inside edge (#638, #639) (#963)
- A fold's zone clipped across the root of a tab folded before on the same
  line kept a sliver of no width there, which made the zone look like it
  crossed a slanted edge. The one real piece is the zone now (#638).
- When the tabs a fold line spans can't take the side beyond them along,
  because it joins the side that stays through other material (a tab on the
  inside edge of an L, the fixed point on the far leg), the tabs' own ends fold
  instead, the same part as with the fixed point on the near leg (#639).

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:16:04 -04:00
bherbruckandClaude Opus 5.5 ea23401710 Sketch on every side wall of one sketch extruded up and down (#957)
* Name a side drawn on several sketch curves by all of them

An extrude named each side wall after the one sketch curve its profile
segment lies on. A wall along two coincident lines (squares drawn edge to
edge) or along collinear lines merged into one profile segment had no
single curve, so it was named from geometry and could not carry a sketch
attachment. One sketch extruded up and down and joined (the logo) refused
a sketch on every one of its side walls.

Such a side is now named by every curve it lies on, in sketch order: the
first is its name, the others aliases. The set is fixed by the sketch, so
the name is exact. The refusal message no longer blames merged faces.

Adds the reported logo design and a synthetic up-and-down join to the
action-matrix designs, and a regression test that sketches and extrudes on
every flat face, fresh and after save and reopen.

Fixes #954

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

* Action matrix: record the up-and-down join designs

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:00:04 -04:00
Nathan Dane 9f76cca8b4 Collect application resources for #433 (#833) 2026-10-14 00:49:32 -04:00
d5ff5ea1a5 Sheet metal: a thousand cut-outs fit the flat budget, ten times faster (#732) (#964)
- A new cut was clipped against the panel with every one of its cut-outs, so
  each hole of a perforation cost the square of all the holes before it and the
  second cut of 500 ran out of the flat budget. A tool is now clipped against
  only the cut-outs whose boxes meet its own (the same result).
- The planar booleans' point-in-loop test now skips loops whose box is clear of
  the point, before the costly on-the-loop test; with a thousand holes that test
  was most of the time.

The 1000-hole panel now takes about 15 s instead of failing.

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:40:08 -04:00
bherbruck ce902be66c Fillets and chamfers ending on a boss's pieced wall or beside it, on a face that runs along its seam twice, and loops the loop blend refuses (#926)
Fillets and chamfers ending on a boss's pieced wall or beside it, on a face that runs along its seam twice, and loops the loop blend refuses

- A straight edge whose end meets one curved surface in several faces (a cylinder a join left
  in pieces) ends on that surface: the section is cut where it crosses each seam, which is
  shortened there; where no seam is crossed (the wall missing inside the other body) the
  section lies on the bared surface and the rest of the side edge becomes a seam.
- Two end faces that aren't both planes meeting in lines: each face takes the section's points
  on it, cut where it crosses the edge between them. Not where an end face runs on smoothly
  from a side (an earlier round): the corner build takes those.
- Face boundaries close over several connector edges.
- A face on a periodic surface running along its seam twice (as a STEP file gives it) is
  rebuilt the same way (#691).
- A curved chain ending square on a face that runs on past its side extends that face's seam.
- A smooth chain stops before an edge between faces that meet smoothly.
- A loop the loop blend refuses for its shape is blended edge by edge; refusals about its size
  stand.
- Action matrix: ACTION_MATRIX_ACTIONS and ACTION_MATRIX_VALIDITY for quick local runs; the
  fillets and chamfers that pass now recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:28:09 -04:00
63095fedb3 Sheet metal: a fold's fixed point on the sheet's face counts as on it (#685) (#958)
On a large thin sheet the probes around the fixed point stepped clean through
the sheet (twice the mesh tolerance of a 465 mm part is more than its 0.8 mm
thickness), so a point exactly on the face touched no material and the fold
was refused. The probes now also step a quarter of the thickness.

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:14:49 -04:00
bherbruckandClaude Opus 5.5 00fdcf3e72 fix: grid lines for split ruling edges only in display and export meshes; record the moved seeds (#961)
#921's grid lines through the vertices that split a ruling edge also went into the meshes Boolean operations intersect, which changed seven robustness seeds (seed 2's sphere cut failed). Deflection::rulings now asks for them only in the kernel's own tessellation (display, export, measurement). The seeds #936 (rejected instead of unsupported), #946 (26 and 85 build, others fail differently) and #921's display side (about 1e-6) moved are recorded with their reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:14:18 -04:00
bherbruckandClaude Opus 5.5 774a8b1d9a test: record the bracket example's action matrix after its fillet moved (#962)
#820 fixed examples/bracket.json's fillet pick, which lay far from every edge and rounded the outer corner instead of the inner one. This re-records examples/bracket only: its edges carry the fillet's new names, and three chamfers beside and across the inner fillet are refused (the blend is larger than the neighbouring faces).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:57:59 -04:00
bherbruckandClaude Opus 5.5 97ff7578c2 Rounded Rectangle sketch tool (#891) (#948)
* Rounded Rectangle sketch tool (#891)

A new SKETCH > CREATE command, sketch.rectangle.rounded, in the Rectangle
family: two opposite corners and a corner radius make a 2-point rectangle
whose four corners are filleted alike, as one undo step. The radius is one
parameter (one radius dimension, the other arcs equal to it), so changing it
changes all four corners. While drawing, Width, Height and Radius boxes take
typed values; typed width and height dimension the overall size between
opposite sides. Without a typed radius the corners get a fifth of the shorter
side.

Fixes #891

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

* Rounded Rectangle in the Simplified Chinese catalog

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:52:10 -04:00
HEOJUNFO 19bf854dc9 Loft: pair section vertices by best fit (#881) (#956)
Each section now starts at the rotation whose vertices line up best with the lead section's, vertex for vertex, instead of at the one vertex closest in direction to the lead's first. Fixes the twisted loft of matching non-convex sections; regression test loft_pairs_the_vertices_of_matching_non_convex_sections.
2026-10-13 23:50:45 -04:00
HEOJUNFO f204042bf2 Preferences › Sketch: auto-projection holds across designs (#680) (#959)
Session::replace_design keeps auto_project when file.new, doc.open and doc.recover swap in another design, and the UI keeps it across tabs, adopt and the last-tab close. Regression tests in engine (sketch_project) and ui-egui (documents).
2026-10-13 23:49:51 -04:00
Jan Szumiec 4bea4c7785 Make the web app work offline and update itself (#239) 2026-10-13 23:40:19 -04:00
selune 09b5da3e9f docs: propose extensible material, appearance and component libraries (#792) 2026-10-13 23:22:58 -04:00
Lasse Lianandlasselian a2c7396160 Fillets and chamfers where edges meet at corners on a curved wall or a knife-edge tip (#851) (#935)
Rolling-ball blends now handle knife edges, where the two faces have opposite normals; an edge that runs out along an end face tangent to one side; and rounds that end in an earlier round on curved faces, picked together or one after another. The three edges at a vertex of smooth faces, rounded to one radius, end in a piece of sphere. When the given order of up to four edges is refused, or leaves a face boundary off its surface, other orders are tried.

Closes #851.

Co-authored-by: lasselian <22126420+lasselian@users.noreply.github.com>
2026-10-13 23:21:47 -04:00
NolyandNoly1990 45f92277cb i18n: complete Simplified Chinese catalog, Chinese font fallback and a coverage xtask (#930)
Adds a complete Simplified Chinese (zh) catalog covering every English message, a font fallback for Chinese text, and `cargo xtask i18n --check`, which checks that the complete catalogs (ja, zh) carry every English message and that every command label is translatable.

Co-authored-by: Noly1990 <20500931+Noly1990@users.noreply.github.com>
2026-10-13 23:00:39 -04:00
Irakli Kourtisandjyviko 587cf865b8 Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws.

Part 4 of 4 of #303.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:57:08 -04:00
Irakli Kourtisandjyviko c1c813e572 Thicken any surface, across sharp edges (#765) (#932)
Thicken now moves faces through the same mover as Offset Surface. Spheres, cones, tori and B-spline faces thicken as well as planes and cylinders, and a thickness that would fold a curved face over is refused. Where thickened faces meet at a sharp edge, a wall closes the gap and corners are closed with flat patches. Thickening into an inside corner is still refused with a clear message.

Closes #765.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:55:04 -04:00
bherbruck 7498e3b367 The model's body mesh comes from the reference mesher again (#940)
Fixes 20 action-matrix regressions from #805 and the seed 14 shell refusal (#897).
2026-10-13 22:37:11 -04:00
Igor Churmeev f037c92370 Add the solidcraft-checking skill (#880) 2026-10-13 22:37:07 -04:00
Irakli Kourtisandjyviko 2b70ed7b72 Offset Surface: faces copied out at a distance, exact or fitted (#758) (#872)
Offsets picked faces along their normals into a new surface body: planes and cylinders move exactly, other faces get a fitted B-spline within tolerance, and offsets that would fold over are refused. Adds the SurfaceOffset feature (noted under format 18), the surface.offset engine command and an Offset Surface dialog.

Closes #758.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:34:16 -04:00
bherbruckandClaude Opus 5.5 15b5b51bf5 Section Analysis in the browser, with an eye to turn it off (#822) (#947)
A section stayed on with nothing in the browser to switch it off; only the
viewport's context menu had Remove Section. While there is one, the browser
now has an Analysis folder listing it. Its eye (and the folder's) hides the
section and shows it again on the same plane; a double-click or Edit Section
Analysis opens the dialog, and Delete removes it. Browser rows with an eye
publish an `eye:<label>` handle for automation.

Fixes #822

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:18:06 -04:00
bherbruckandClaude Opus 5.5 abddd67d22 fix: a refused control connection reads what was sent before closing (#945)
Closing a socket with unread input resets the connection, and the reset could reach the client before the refusal did (requests_run_only_after_the_token failed 17 times in 100). The server now ends its side and drains, briefly and up to 64 KiB, what the client had sent before closing: 0 in 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:07:23 -04:00
bherbruckandClaude Opus 5.5 78d9aa0153 Booleans: small tools in large bodies are traced at their own scale (#559) (#946)
A through-all hole's tool is twice the largest body long, and the boolean's tolerances
followed that length: a 6 mm hole through a 920 mm part was traced at nearly 1 mm, wider than
the hole, and the cut came out empty or failed. When every other strategy has failed, the
boolean is now asked again (unshifted, then once shifted) at tolerances scaled to where the
bodies meet: the overlap of their boxes and the thinner operand's thickness. Its result is
checked as the others are and held to the retry guard. It runs last, so whatever an earlier
strategy answered is kept as it was, and only for single-shell operands (bodies in several
pieces are retried a piece at a time, each with this pass of its own).

The sampled shortcut also gave up on a slender tool lying across its box (a long hole drilled at
an angle fills little of it): it now samples past the first 2000 points until enough land
inside, so a tool that misses a body leaves that body as it is. Bodies that had enough samples
in the first 2000 are sampled exactly as before.

Baseline: the 90 actions that newly pass, rechecked one by one with the final build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:54:09 -04:00
Mateusz Sych 17db109c5d URDF export for robot simulators (#719) (#942)
A design with components and joints exports as a URDF robot description (file.export to .urdf, the MCP export tool, solidcraft-cli run --out, File > Export URDF...). It writes one binary STL per component in metres, links with mass, centre of mass and inertia from material density, and maps rigid, revolute, slider, cylindrical, pin-slot, planar and ball joints to URDF joints or short chains. Loops, broken joints and loose parts are reported as warnings. Options: mesh_prefix, effort, velocity. Docs in docs/urdf.md.
2026-10-13 21:52:30 -04:00
Irakli Kourtis d2e409f40c Surface walls named after their sketch curves (#757) (#850)
Each wall of a surface built from sketch curves (Surface Extrude, Revolve, Sweep and Loft) is now named after the curve it comes from, exactly rather than from geometry, so references to those faces survive edits.
2026-10-13 21:47:24 -04:00
bherbruckandClaude Opus 5.5 8746376ee9 Web: fetch the CJK font when Japanese text is drawn, whatever the UI language (#720) (#937)
The browser build fetched its Japanese fallback font only for a Japanese UI,
so a Japanese design name, file name or typed text showed as tofu with the UI
in English. After drawing each frame the web shell now looks through the
painted text for kana, CJK ideographs and full-width forms while no CJK font
is loaded, and fetches the same font once on the first hit. A failed fetch
with an English UI leaves the language alone and is not retried in a loop.
Sessions that never show such text download nothing extra.

Fixes #720

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:49 -04:00
bherbruckandClaude Opus 5.5 5808bcc0f0 Toolbar drop-downs show all their rows after a shorter list (#874) (#918)
* Toolbar drop-downs show all their rows after a shorter list (#874)

A panel's drop-down kept its size by an id made from the panel name alone,
so PLASTIC's MODIFY reused the one-row size of MESH's MODIFY and showed two
rows with a scroll bar. The id now includes the tab, and the rows' scroll area
may grow past the size the area remembered (up to the room below it), which
also covers the same panel collapsed and expanded.

Fixes #874

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

* Keep the Sketch Create menu's own sizing

Its rows sit in egui menus (also inside the overflow menu), where a minimum
height moved the rows between frames. Only the panel drop-downs need it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:45 -04:00
bherbruckandClaude Opus 5.5 d23593d3eb Clamp the window surface to the device's largest texture (#869) (#905)
Moving a window between monitors of different scale can briefly ask for a
surface larger than the device's largest texture (11512 x 6808 against 8192 in
the report), and Surface::configure failed validation, which wgpu turns into a
panic. The surface is now clamped to the device limit with its aspect ratio
kept, and egui draws the whole window at the matching smaller scale, so the
frame is only softer until the size settles. The device also asks for the
adapter's own texture limit (16384 on most desktop GPUs) instead of 8192, and
the resize path and an unsupported surface log instead of unwrapping.

Fixes #869

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:40 -04:00
Irakli Kourtis 897b7d93b5 Surface Loft: sheets through open or closed sections (#847)
Surface Loft lofts open or closed curve chains, one section per sketch in pick order, into surface bodies. Adds the SurfaceLoft feature (format 18 note), the surface.loft command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 21:07:16 -04:00
bherbruckandClaude Opus 5.5 7c85970bfc Fillet and chamfer: refuse a smooth seam as having no corner to round (#936)
* Fillet and chamfer: refuse a smooth seam as having no corner to round

Picking the seams where R10 rounds run into the faces beside them, and asking for an R1
fillet there, failed with "not supported yet: blending a smooth edge (... both faces next
to the edge must be planar)". That reads like a missing feature, but the faces meet
tangentially at those edges and there is nothing to round.

An edge whose two faces are tangent along it is now refused as an invalid pick that names
the edge and asks for the sharp edges.

Fixes #896.

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

* Robustness goldens: seed 72's smooth-seam fillet is now a rejected pick, not unsupported

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:02:06 -04:00
bherbruckandClaude Opus 5.5 8fb5742706 fix: imprinting a partial contact boxes faces by their control points (#929)
#825 chose the faces to split by boxing each face, dividing its curved edges down to the contact tolerance; on the sensor enclosure's lid that division never finished (press/pull and extrude on 18 faces grew past 24 GB and timed out in the action matrix, on CI as here). The boxes now come from the edges' control points, with no division; a face whose extent can't be found counts as near. A test holds the lid's press/pull to a time budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:52:35 -04:00
Irakli Kourtis aba57f1ca2 Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 20:48:30 -04:00
Giovanni 57d738eac4 Join walls swept along splines in 2D (#908)
Walls swept along a spline have flat B-spline tops and bottoms, so booleans between them went to the general intersection, which fails on coincident spline faces. Such faces are now put on true planes, and two prisms of the same height and direction are decided in 2D with their spline outlines fitted by arcs within a micron. The result is kept only when its volume is plausible; otherwise the general boolean takes over. The free-form flag is cleared by a drop guard so an unwinding boolean can't leave it set.

Fixes #844.
2026-10-13 20:48:27 -04:00
selune 3f1c023563 Keep fine meshes whole at collapsed taper boundaries (#906)
The exact surface search could return an arbitrary parameter at a collapsed NURBS boundary, which corrupted the trimmed triangulation domain and lost volume on fine meshes of a fillet tapering to zero. Search results in the boundary projection are now validated, preferring a bounded parameter at a pole and snapping to a finite endpoint only when its transverse tangent has collapsed and the residual improves. Adds four regressions for decreasing chord tolerances, pole parameters, unbounded planar support and provisional preview.

Fixes #796.
2026-10-13 20:45:02 -04:00
Mqrius 7da7bbb335 Planar source matching looks only at planes with the same normal (#917)
Body::with_planar_sources compared every planar face of a result with every unsourced planar face of its inputs, so a boolean on an import with a few thousand facets ran out of its comparison budget before it started. The lists are now sorted by normal x and each face only compares with entries inside a narrow normal window found by binary search; the exact same_support test and the work budget are unchanged. Adds an engine test joining a pin onto a 4000-sided imported prism.
2026-10-13 20:44:58 -04:00
Irakli Kourtis 2eb292dd4a Surface Revolve: sheets turned from open curves and profiles (#824)
Surface Revolve turns open curves and closed profile boundaries about a sketch axis into surface bodies, with cone apexes and sphere poles where a chain ends on the axis, and a closed shell made solid. Adds the SurfaceRevolve feature (format 18 note), the surface.revolve command, a dialog with Edit Feature support and analytic kernel and engine tests.

Part of #757.
2026-10-13 20:30:41 -04:00
bherbruckandClaude Opus 5.5 b41ad23e3f Lip: run along the rim's opening, not the foot of a boss standing on the face (#933)
A lip picked on a face that isn't a shelled body's rim, such as the Sample Plate's top
face with its two holes and the boss standing on it, ran its band round the largest inner
edge. That was the boss's foot, so the band stood against the boss's wall and the join
failed with a boolean error about curved faces.

The band now goes round the face's opening (an inner edge with no material just over the
face inside it). A rim face that has a smaller hole in it still works. A face whose inner
edges are only where other features stand on it, or whose openings are alike in size (a
plate's holes), is refused with a plain message that names the rim face it needs.

Fixes #892.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:26:24 -04:00
bherbruck 2dcfea8df7 A generated many-part assembly: test and open-phase benchmark (#927)
Generates N parts of four primitive bodies, each moved into a placed component. The test (12 parts) checks the open, the world placement and the saved round trip; the ignored bench_generated_assembly prints parse, evaluation inside and outside the features, mesh warm-up and Measure.
2026-10-13 20:11:34 -04:00
bherbruckandClaude Opus 5.5 7135407268 Mesh a cylinder wall whose seam edges a cut split without fans across it (#921)
A boolean that cuts a pocket into the top of a cylinder (Shell does this) splits the
outer wall's straight seam edges at the pocket's floor. The wall's parameter grid has no
division along its rulings, so the triangulation fanned out from the two split vertices
to the far rims and its triangles cut through the part: the shelled Bolted Flange sample
drew and exported its rim cut off (mesh volume 29201 mm3 for a 40796 mm3 body).

Give the grid a line through each vertex that splits a boundary segment running along a
ruling, in a direction that needs no division of its own and within the grid budget.

Fixes #819.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:55 -04:00
Mateusz Sych 5f7ce80a93 MCP batch runs as one request in the app (#743) (#885)
The app's control protocol gets engine.batch: the steps run in one request (so one frame), each as engine.execute runs it, stopping at the first failure and rolling back to the checkpoint by default. The MCP batch tool sends it to a running app (with the control token), is never resent after a lost reply, and falls back to step-by-step when the app refuses (agent cursor shown, or a step loads a file). A batch may take up to an hour to answer.
2026-10-13 20:03:09 -04:00
Giovanni fdaf57daa3 Count the contact grid's own work, and test find_close_pair (#836)
find_close_pair's budget now counts the grid's own work too (each cell a triangle is filed under or visits), not only the pair tests, so a pathological pair of meshes can't spend unbounded time building and walking the grid. Adds tests for find_close_pair and the budget.
2026-10-13 20:03:05 -04:00
bherbruck 7eac1de9ee Open large designs faster: list each face's mesh vertices once when flagging seams (#914)
face_vertices lists each vertex once per face (first-met order) and normal_near measures each distance once; seam flags, merged topology and cone signatures are unchanged because the dropped repeats were identical entries.
2026-10-13 19:54:50 -04:00
Que fe07f79387 fix: a hole at sketch points drills into the part its sketch touches (#910)
Hole centres from a sketch now drill into the material on whichever side of the sketch plane it lies: material the spot starts in wins, then the nearest body ahead, else against the sketch normal as before. A sketch on a part's underside or below it now drills up into the part. Covers counterbores, through-all, edge and corner centres, patterns and reopening.
2026-10-13 19:53:21 -04:00
Irakli Kourtis b52d4390a0 face.delete heal: false leaves an open surface body (#861)
Delete Face takes heal (default true, as before). With heal: false the picked faces are removed and the gap stays open, leaving the remaining faces as a surface body (kernel remove_faces); deleting every face is refused. Saved only when false, noted under format 18.
2026-10-13 19:53:17 -04:00
bherbruck c616f50bd0 Open large designs faster: move bodies into components without copying the model (#915)
A body transfer works out the moved bodies first and puts them in place in the evaluator's own copy of the model, instead of copying every body of the model for each transfer.
2026-10-13 19:49:19 -04:00
bherbruck 6c61442bb2 Open large designs faster: meshes, face names and Measure on all cores (#916)
Display meshes after opening, the face names read when a feature finds a face by name, and inspect.measure over all bodies are now worked out per body on plain scoped threads (solidcraft_kernel::for_each_on_threads / map_on_threads), in body order and with the same results; the browser build stays serial.
2026-10-13 19:49:04 -04:00
bherbruck c333889fc0 Open large designs faster: index the previous bodies by name after each feature (#912)
After each feature the bodies before it are indexed by name once (first match first, as the linear searches were), feature components come from a map built once per evaluation, and Body::same compares shared face sources, paint and split lines by pointer first.
2026-10-13 19:45:18 -04:00
bherbruck 648f0ff768 Open large designs faster: count the cache budget without cloning wires (#911)
Body::retained_bytes counts face and edge uses on the borrowed boundaries (the same numbers, checked by a test), and each model body keeps the footprint measured when a feature made or changed it, so the evaluation cache's budget no longer walks every body's topology after every feature.
2026-10-13 19:44:46 -04:00
bherbruck eaab205302 Open large designs faster: name a moved body's faces only when asked (#913)
NameCell::same now refers to its source body and takes the source's names on first request, instead of naming every face of every moved or placed body up front. Names are unchanged.
2026-10-13 19:42:31 -04:00
Mqrius eea49922bd fix: face projection walks through pinch points (#899)
Where two boundaries of a projected face family touch at a single vertex, an even number of boundary edges meet there. The walk now stays on the face it came along (or takes the first edge in geometric order), so every loop closes; an odd count is still refused as a real branch. Adds a test with two squares meeting at a corner.
2026-10-13 19:27:47 -04:00
phanoob dd7790a43f Sketch text: bold and italic, text along a curve, alignment and spacing (#852)
A sketch text gets an optional style: bold and italic faces (a slanted fallback for italic), left/center/right alignment, letter and line spacing, and a path curve of the same sketch for the baseline to follow (reverse takes the other side). Edit Text gets B/I toggles, alignment, spacing fields and a Path chip, all live. The style is saved only when not plain and is noted under format 18.
2026-10-13 19:27:35 -04:00
bherbruckandClaude Opus 5.5 fe3717f2e4 fix: control port needs a per-launch token; MCP tool annotations tell the whole truth (#205) (#901)
solidcraft --control PORT writes a random per-launch token to a user-only file (<data>/control/PORT.token; 0700/0600 on Unix, icacls on Windows; SOLIDCRAFT_CONTROL_TOKEN overrides) and refuses any connection whose first request lacks it. mcp --connect, the demo and packaging smoke clients send it. MCP execute/batch/open/save/export/screenshot are annotated destructive and open-world, new_design destructive. Docs: control-protocol.md, mcp.md, AGENTS.md, pending release note in releasing.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:51:19 -04:00
bherbruckandClaude Opus 5.5 d9237d004e ci: the nightly and Run workflow get their own concurrency group (#877)
Only one run per concurrency group can wait. On a busy main each push replaced the waiting run, so the nightly and Run workflow runs on main were cancelled before they started. The group now includes the event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:49:38 -04:00
JIJINBEI d8d54ded0d Renaming a body rewrites only the references to it (#875)
Renaming a body used to replace every string in later features equal to its old name, including sketch text, plane and feature names and expressions. Now only values under keys that name bodies (body, bodies, target, targets, tools) at any depth are rewritten, with a test that a sketch text spelling the body's name stays while a move follows the rename.
2026-10-13 18:47:10 -04:00
Igor Churmeev 95d394dcab Modeling skill: rim fillets after shell, slots along Y, boolean retry caveats (#879)
Corrects the modeling skill: an edge of the face a shell removes is rounded after the shell, a slot along Y is dimensioned horizontally, long shallow arcs measure to about 0.1%, and a boolean retry warning can hide wrong geometry when the volume does not match.
2026-10-13 18:46:47 -04:00
lunar-me 7ee351dc91 docs: perf.md grammar (one of the last features) (#895)
Fixes a plural in docs/perf.md.
2026-10-13 18:46:43 -04:00
lunar-me 9297f81501 docs: sheet metal guide grammar (corner relief size) (#894)
Says the rectangular relief is the size of the rule's corner relief.
2026-10-13 18:46:40 -04:00
lunar-me 2afc10fbd5 docs: sheet metal guide grammar (the base left beside a bend) (#893)
Adds a missing article in the sheet metal guide.
2026-10-13 18:46:37 -04:00
NOWN-DEV 5c98c1e24a A hole square to a folded sheet metal panel is flat machining (#656)
A hole drilled square to a flange's panel, not only the base's, is now a flat cut-out of the sheet: it unfolds with its panel to where the bend allowance puts it, shows in the flat pattern and DXF as a circle, and survives unfold and refold. A pocket tool spans the cut-outs already inside it (a counterbore's hole), so the order they are made in no longer matters. Fixes #560.
2026-10-13 18:46:11 -04:00
bherbruck 0040ce6bf2 Robustness goldens: record the seeds the 13 October kernel merges changed (#898)
Main has failed the full robustness goldens since #838, #839, #841, #855 and #856 landed together. Records the eleven seeds that now build differently (2, 6, 14, 22, 36, 43, 50, 63, 71, 73, 94), with a note. Shell now refusing seed 14, which used to build, is tracked in #897.
2026-10-13 18:46:00 -04:00
phanoob 1f7f37d1ef Cut an extrude's profiles that stand apart in one boolean (#848)
When an extrude cuts with profiles whose outlines stay more than 1 µm apart (the letters of a text), their tools are apart too, so they are joined and cut from the body in one boolean instead of one per profile. A ten-letter text cut drops from about 13 s to 1.5 s with the same result. Sheet metal targets and touching profiles keep cutting tool by tool. Tapered extrudes are unaffected.
2026-10-13 18:29:49 -04:00
Kevin Ravensberg 96f6424aec fix: confine MCP agents' files to allowed folders; recovery discard deletes only recovery entries (#205) (#578)
Agent tool calls (MCP, and bridged control requests carrying allow_paths) reach files only in the open design's folder (strictly inside home or the temporary folder, not in a hidden folder, and never its hidden files), the app's recovery folder, the server's private scratch folder and each --allow-path; --allow-any-path lifts the limit. Paths are resolved first, so .. and symlinks that lead out (including a save through a link, #845) are refused. On Windows, UNC, device, NT, verbatim non-drive, drive-relative, alternate-stream, reserved-device and 8.3 short-name paths are refused before any file system call, and the resolved path must be on a local drive. doc.recovery_discard and doc.recover {discard} delete only real recovery entries. Docs: mcp.md, control-protocol.md, README.
2026-10-13 18:24:08 -04:00
bherbruck 3d3f9b626f Robustness goldens: record seed 0 after the tessellation plans (#870)
#805 and the goldens (#826) landed side by side, so seed 0 was recorded before the new meshing reached main.
2026-10-13 17:30:16 -04:00
bherbruck c3ebe13991 Cargo.lock: solidcraft-gpu at the workspace version (#857)
Refreshes the solidcraft-gpu lock entry so --locked builds pass on main.
2026-10-13 16:48:43 -04:00
Irakli Kourtis b69cd2d0b7 feat: add and remove fillet spline points; handles on every edge (#855)
Double-click a fillet's radius curve to add a control point, or a handle to remove one; a spline fillet shows handles on all its edges.
2026-10-13 16:45:42 -04:00
Irakli Kourtis 753b0a2eab Unstitch and Reverse Normal (#856)
Unstitch takes a body's faces out as surface bodies; Reverse Normal turns a surface body's faces over. Both are saved in format 18. Closes #766.
2026-10-13 16:45:31 -04:00
bherbruck b702f0efde Fix live hinge animation and Drive Joint slider previews (#835)
Joint previews keep showing finished poses while the slider moves, and the latest value is still computed.
2026-10-13 16:34:09 -04:00
William Dunn 152418b624 A tool window for the circular sketch pattern (#801)
Circular sketch pattern gets a tool window with entities, center, quantity and total angle, replacing the click flow.
2026-10-13 16:34:00 -04:00
selune e99b3bd4e8 render: bounded cooperative HDR input foundation (#837)
A cancellable, bounded Radiance HDR decoder for render lighting input; reference images still accept PNG and JPEG only.
2026-10-13 16:33:51 -04:00
NOWN-DEV 8319c35a32 fix: a base trimmed for an edge flange keeps the arcs of its outline (#839)
A base trimmed for an edge flange keeps the arcs of its outline instead of falling back to a polygon.
2026-10-13 16:33:35 -04:00
froglemonade2 c3e0ea498a fix: joins, cuts and sketch profiles where outlines touch at a point (#841)
Fixes #588.
2026-10-13 16:33:24 -04:00
Mqrius b6e3a6c366 fix: contact and shell-grouping checks use a shared broad phase (#838)
Contact and shell-grouping checks find close triangle pairs through one shared grid instead of testing every pair. Refs #232.
2026-10-13 16:33:14 -04:00
Lasse Lian 4d8c80a86d fix: saving follows symbolic links and keeps the file's permissions (#845)
A save through a symbolic link replaces the file it points to and keeps the link; the saved file and its backup keep the original permissions.
2026-10-13 16:32:55 -04:00
phanoob ec5d7f9789 Sketch text: live Edit Text from the Text tool, a font list, handles, and whole-text picking (#821)
Live Edit Text from the Text tool, a font list, handles, and picking the whole text.
2026-10-13 16:29:37 -04:00
HiroKenEz 69050c81d9 Fillet and chamfer: refuse an edge pick far from every edge (#820)
Fixes #783.
2026-10-13 16:29:33 -04:00
William Dunn 53238fba5f Sketch Offset on two sides, or symmetric about the source (#807)
Sketch Offset can place the copy on both sides of the source, or symmetric about it.
2026-10-13 16:29:24 -04:00
Irakli Kourtis 5d661fa2af Export 3MF with B-rep face tags (#834)
3MF export carries each triangle's B-rep face tag.
2026-10-13 16:29:09 -04:00
Irakli Kourtis ff7a347013 Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)
Display meshes are built from tessellation plans, with GPU evaluation in the viewport.
2026-10-13 16:27:16 -04:00
bherbruckandClaude Opus 5.5 534393ed5a test: seeded goldens for the robustness seeds (#826)
tests/robustness/goldens.json records what each of seeds 0-95 builds: its steps, how it ended (ok, or the kind and the start of the error it was refused with) and, for the design it left, the bodies' total volume and area (within a relative 1e-6) and their face, edge and shell counts. tests::robustness_goldens fails on any change; ROBUSTNESS_GOLDENS_BLESS="why" records one, with the reason and the seeds it moved. Pull requests that touch crates/kernel run every 8th seed; pushes to main and the nightly run every seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:23:27 -04:00
bherbruckandClaude Opus 5.5 200a83a629 Release 0.9.0 (#832)
Bumps the workspace to 0.9.0 and writes the release notes. Minor bump: designs are now format 17, and there are many new features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:47:06 -04:00
William Dunn d302aa1a00 Construction Geometries, a Sketch Palette row that hides construction geometry (#804)
A Sketch Palette toggle (sketch.options show_construction) that hides construction geometry from drawing and picking; saved with the sketch's view options under format 17.

Co-authored-by: William Dunn <william.dunn@quasarke.com>
2026-10-13 15:15:13 -04:00
NOWN-DEV 9184994721 fix: coincident faces are pushed apart only where that changes nothing (#817)
A coincident face is pushed apart only when the space it moves through holds nothing that changes the result; otherwise the boolean falls through to the exact strategies.

Co-authored-by: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com>
2026-10-13 15:10:31 -04:00
bherbruckandClaude Opus 5.5 d2676da789 perf: imprint partial face contacts only near the contact (#825)
The partial-contact imprint (#704, #751) split every face in the contact plane against every tool face, one call per face, and measured all the tool's cylinders for each pair: the sensor enclosure's Case shell went from 6.6 s to 76 s and timed out in the CI matrix. Only faces near the contact are split now, each surface once, in one call: 7.0 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:06:55 -04:00
bherbruckandClaude Opus 5.5 6a6a7e2865 test: give an action-matrix design 300 s to load (#812)
Loading a design (from a worker's start or restart to its first output) gets 300 s; each action keeps 90 s. oracle/41-tube-radial-holes (about 23 s here) timed out loading on a loaded CI runner, and reloads after a hung action were charged to the next action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:59:36 -04:00
William DunnandClaude Opus 5 3ea8562c5e A tool window for the rectangular sketch pattern (#799)
sketch.pattern.rectangular gets a tool window with both quantities and spacings and an optional direction, instead of a click flow with a fixed count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 14:54:48 -04:00
Irakli KourtisandClaude Opus 5.5 e540832626 Surface Extrude: sheets from open curves and profile walls (#815)
surface.extrude sweeps open curve chains and profile boundaries along the sketch normal into a surface body (walls only, no caps). Saved under format 17.

Refs #757

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:54:38 -04:00
Irakli KourtisandClaude Opus 5.5 8dd583930e Display tessellation plans (2/4): size rules and the facet angle (#803)
Per-face target side lengths, the caller's facet angle, refined grid lines, and fixes for underflowing coordinates and straight edges ending exactly on their vertex. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:45:54 -04:00
HiroKenEzandClaude Opus 5.5 9bb18842e9 Convert Mesh says how big a mesh is, the limit, and what converts (#797)
A mesh over the conversion budget is refused with its triangle (or vertex) count, the limit, and that only constant-section parts convert.

Refs #346

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:43:29 -04:00
Mateusz Sych 9cabc3532d STL export welds shared vertices like the 3MF (#816)
STL (binary and ASCII) now welds each body's export mesh with the same tolerance as 3MF, so filleted and drilled bodies are closed. The weld grid's neighbour lookup no longer overflows for bodies far from the origin.

Fixes #784
2026-10-13 14:39:20 -04:00
William Dunn bdcfe3ff1d One tool window for sketch Fillet and the three sketch Chamfers (#789)
Sketch Fillet and the three sketch Chamfers share one tool window, with its own icons, labels and a scenario.
2026-10-13 14:32:13 -04:00
semmelrindeandClaude Opus 5.5 dbfb689f6c Single-hole snapping takes only shown points and checks the face once per pick (#798)
Follow-up to #718: a face place snaps only to sketch points that are shown, and the face under the cursor is looked up once per pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:54 -04:00
Irakli KourtisandClaude Opus 5.5 17b4b9a32c Reference case for a fillet with a B-spline radius (#810)
Adds oracle case 81 (a box edge rounded at a clamped cubic B-spline radius) with analytic expected values, and passes spline fillets through recipes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:49 -04:00
Irakli KourtisandClaude Opus 5.5 c8d668108a Surface toolset: icons and planned commands (#808)
Icons for the surface toolset, and its planned commands registered and disabled with the issue that tracks each one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:38 -04:00
NOWN-DEVandClaude Opus 5.5 dd084ec9e1 Sheet metal: tear as a fourth corner relief shape (#809)
A sheet metal rule's corner relief shape can now be tear; saved under format 17 with a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:28 -04:00
Irakli KourtisandClaude Opus 5.5 bfbf5f9432 Display tessellation plans (1/4): planner with shared edge nodes (#802)
Adds TessPlan and the kernel planner (shared edge nodes, structured and Coons grids, constrained Delaunay for trimmed faces) behind Body::display_plan. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:27:44 -04:00
William Dunn facff1050e Put Move/Copy Sketch Entities on the toolbar, behind a window with distances, an angle and a copy flag (#735)
Sketch Move gets a toolbar placement in Sketch > Modify and a Move/Copy tool window with X and Y distances, an angle, an optional centre point and a Create Copy flag, replacing the context-menu route that ran the command with no parameters.
2026-10-13 13:48:13 -04:00
William Dunn de1479029d A sketch dialog and the Sketch Palette on screen at the same time (#791)
A command dialog opened while sketching no longer hides the Sketch Palette: the dialog docks at the left of the viewport and the palette keeps the right. A view too narrow for both keeps the dialog alone.
2026-10-13 13:41:46 -04:00
William Dunn 16766bdbc7 Edit Sketch in the viewport menu over a sketch curve (#795)
Right-clicking a curve or point of a sketch that isn't being edited now opens that sketch's own menu, with Edit Sketch, instead of the body menu. Solid geometry in the selection keeps the body menu.
2026-10-13 13:37:54 -04:00
William Dunn d4fd760b0d A Mirror window for sketch entities, holding both picks until OK (#734)
Sketch Mirror now opens a tool window with Entities and Mirror line inputs, so both picks stay visible and editable until OK, replacing the old click-then-right-click flow.
2026-10-13 13:29:26 -04:00
Mithril_MEX d437c21368 feat(i18n): complete Japanese localization and eliminate UI hardcoded strings (#753)
Completes the Japanese catalog (all commands, panels, dialogs and preferences) and routes the remaining hard-coded UI labels through the translation lookup. English stays the default.
2026-10-13 13:24:24 -04:00
Alexander Nutz a8b3b21fd0 Feature history on the left side, with contiguous folders (#449)
The feature history can sit in a panel on the left, with folders that hold one unbroken run of features, drag and drop as one undo step, and a resizable divider. Timeline folders save under format 17.
2026-10-13 13:01:31 -04:00
Mateusz Sych be55a49b04 docs: agent skills for modeling and editing through MCP (#728)
Two agent skills for modeling and editing through the MCP server, with a command reference and model-only screenshot examples.
2026-10-13 12:49:02 -04:00
selune 293d660d6c fix(kernel): preserve intentional Split Face lines in contact joins (#782)
Intentional Split Face lines survive a join with partial planar contact, so the far face stays in two pieces. Follow-up to #704 / #674.
2026-10-13 12:46:34 -04:00
William Dunn beca7eb126 A tool window for sketch Offset, with a typed Distance, Flip and Chain (#731)
Sketch Offset gets its own tool window with a typed Distance, Flip and Chain, as a variant of the sketch modify window.
2026-10-13 12:46:24 -04:00
bherbruckandClaude Opus 5.5 1a0100cdd9 test: record the action-matrix outcomes that changed on main (#781)
26 baseline actions that fail on main: 17 are the retry guard's refusals of geometry that used to delete a body or fill past an edge (#644), 9 vary between runs (#707). Load timeouts left as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:05 -04:00
Irakli Kourtis 5267c96ab2 feat: drag a fillet's radius spline by its handles on the edge (#752)
A spline fillet's radius can now be dragged by handles on the edge, and the spline radius saves under format 17.
2026-10-13 12:35:42 -04:00
semmelrinde 159f9913c1 fix(sketch): a side put onto a projected line keeps its closed profile (#635) (#780)
A side constrained onto a projected line keeps the sketch's closed profile. Fixes #635.
2026-10-13 12:35:33 -04:00
semmelrinde dfa9e094cd fix: a size worked out from a bare diameter keeps its unit when the design's units change (#716) (#779)
A size typed as a bare diameter now keeps the design's length unit, so it no longer changes when the design's units change. Fixes #716.
2026-10-13 12:35:25 -04:00
Irakli Kourtis 8cdefd4cd5 test: reference cases for variable, chord-length and tapered fillets (#776)
Four analytic reference cases for variable, to-zero, chord-length and taper-to-fit fillets, with the formulas in tools/oracle-analytic.
2026-10-13 12:35:16 -04:00
Mqrius 7b0a1b296c perf(kernel): a refused edge is not rolled again after blends far from it (#777)
A blend no longer re-rolls an edge it already refused unless a later blend came near it, so long curved chains stop paying for the same refusal over and over.
2026-10-13 12:35:09 -04:00
Irakli Kourtis 25a3dc501b docs(roadmap): list the proposed surface toolset under M8 (#773)
List the proposed surface toolset under M8 in the roadmap and link the scope discussion.
2026-10-13 12:35:02 -04:00
bherbruckandClaude Opus 5.5 a3dfbaa0b0 Booleans build the same body every time: face order no longer follows heap addresses (#748)
truck's face and edge IDs are their addresses, so walking maps keyed by them followed heap layout, and the ribbed lid's side-wall hole passed or failed by chance. The planar boolean's rebuild and glue now take truck's connected components in the shell's order, and the planar boolean's own maps are ordered. Healing's union-find keeps the lesser root and takes a run's ends in order of use. The face classification after an intersection orders each component's faces by the shell first. The lid now builds the same body on every run, with a test that builds it six times in one process. Its 0.5 mm side-wall holes, and one sensor enclosure hole of the same kind, now fail every time and are marked failing in the baseline.

Fixes #707.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:31:23 -04:00
NOWN-DEV 58af8accfa fix: a projected edge stays the edge that was picked when its body changes (#730)
A sketch's link to a projected edge now keeps the edge's persistent name and finds it by that name when the body changes, instead of taking whichever edge ends up nearest its point.
2026-10-13 12:06:13 -04:00
William Dunn 4a989f6e2d Three readable line type icons in the Sketch Palette, with construction orange moved to a caller contract (#747)
The Sketch Palette's line types are three drawn icons (solid, one gap, gap with a dot) instead of text glyphs, and the construction orange is now chosen by the caller (`fixed_accent` on the toolbar) rather than hardcoded in the icons.
2026-10-13 11:53:59 -04:00
William Dunn aae70053ee Let the conic tool take a typed Rho, and preview the curve it is about to draw (#736)
The conic tool takes a typed Rho in its canvas value box and previews the curve it's about to draw, with the tangent legs as a faint guide.
2026-10-13 11:47:28 -04:00
William Dunn 9b5e39844a A Sketch Scale window, replacing a toolbar button that only raised an engine error (#733)
Sketch Scale now opens a tool window with the entities, an optional base point and a typed factor, instead of running the command with no parameters and raising an engine error.
2026-10-13 11:46:00 -04:00
Irakli Kourtis 1d30e4e1d2 feat: fillet radius tapers along an edge to the room there (#749)
Adds `fit: taper` to Fillet: where a constant radius doesn't fit along a straight edge, it follows the room there as a smooth spline, down to zero where a face closes to a point. Taper to Fit is in the Fillet dialog, and the feature's warning says how far it tapered.
2026-10-13 11:45:33 -04:00
Diogo Soares Rodrigues 1a5e961362 feat(ui): sketch tool shortcuts open a sketch on the selected plane (#737)
Outside a sketch, a sketch tool's shortcut opens Create Sketch on the selected plane or planar face (or asks for one), then starts that tool in the new sketch.
2026-10-13 11:45:15 -04:00
William Dunn ff609bc320 Make SolveApp::start honour a command's enabled flag, and disable solid.move inside a sketch (#740)
`SolveApp::start` now honours a command's enabled flag, so shortcuts, the command palette and the control channel can't start a disabled command, and Move is disabled while a sketch is open.
2026-10-13 11:43:45 -04:00
Mateusz Sych 62a7f903f2 fix: MCP instructions name body_topology, not a list_edges tool (#729)
The MCP server instructions now point to `body_topology` for finding edge points (there is no `list_edges` tool), with a test that every tool the instructions name exists.
2026-10-13 11:38:54 -04:00
bherbruckandClaude Opus 5.5 11c03fb0ab fix: imprint each surface once when fusing partial face contacts (#751)
Robustness seed 16 failed in most runs: the imprint split a block's side along both halves of a cylinder's round side, and the second split re-traced the first circle, sometimes leaving a sliver the glue could not match. The imprint now splits along each surface once (10/10 runs pass, 3/12 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:35:27 -04:00
William Dunn 6f4a9d96fb Give the Move dialog the canvas value box every other dialog already has (#739)
The Move dialog gets the same canvas value box as the other dialogs: X for translating moves, the angle for Rotate, placed clear of the triad.
2026-10-13 11:28:50 -04:00
William Dunn 4112189551 A Feature Options section in the Sketch Palette showing the running tool's modes (#750)
Adds a Feature Options section to the Sketch Palette with the running tool's modes as icon buttons, so you can switch between e.g. circle or rectangle variants mid-tool.
2026-10-13 11:28:37 -04:00
SahenandClaude Opus 5.5 2bb24d1d2a perf: prism pairs before the mesh, bodies measured once, and a STEP import that copies less (#714)
A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:55:56 -04:00
NOWN-DEVandClaude Opus 5.5 a1cb8d4d36 feat: Convert to Sheet Metal takes a solid or a shelled body by its bend edges (#725)
Convert to Sheet Metal now takes a solid or a shelled body by its bend edges (`bends`): the faces reached from the fixed face across those edges become the sheet, keeping the part's envelope, with the flat pattern by the rule's K-factor and radius. Other edges between sheet faces are cut open. The result works with edge flanges, cuts, Unfold/Refold, the flat pattern and DXF. A conversion without `bends` is the plate conversion as before. The optional `bends` field is noted under format 17. Fixes #647.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:52:28 -04:00
MqriusandClaude Opus 5.5 a513304917 fix(engine): pick the face of a named body where bodies meet flush (face_body) (#710)
Extrude and Revolve from a body face take `face_body` to say whose face is meant where faces of several bodies meet flush at the picked point. Without it, an ambiguous pick now names the bodies that meet there instead of saying there is no face. Fixes #703.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:51:57 -04:00
semmelrindeandClaude Opus 5.5 d7f5b2435e fix(ui): the wheel over a dialog's list scrolls only the list (#678) (#713)
The mouse wheel over a dialog's list, a drop-down or a menu that hangs over the model now scrolls only that list; the view zooms only when the pointer is really over the model. Fixes #678.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:44:29 -04:00
semmelrindeandClaude Opus 5.5 ce5762d873 fix(ui): Box, Cylinder, Sphere, Torus and Coil dialogs show the unit of their sizes (#630) (#709)
The Box, Cylinder, Sphere, Torus and Coil dialogs now start their sizes in the design's units with the unit written out ("20 mm", "20 in"), like the other dialogs. OK makes the same sizes as before. Fixes #630.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:44:10 -04:00
semmelrindeandClaude Opus 5.5 831fa34752 fix(ui): a Single hole picked at a sketch circle's centre goes to the centre (#657) (#718)
A Single hole picked on or near a sketch point that lies on the face (a circle's centre) now goes to that point, as a Multiple pick does. Picks away from sketch points, or near one that isn't on the face under the cursor, stay where they were clicked. Fixes #657.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:43:47 -04:00
selune 11084a5963 fix(sketch): move and copy editable text as a whole source (#715)
Dragging a text outline now moves the whole editable text, and Move/Copy shifts or rotates it as one piece (a copy stays independently editable). Content, font, height and outline identities are kept; a drag is one undo step and Esc restores the start.
2026-10-13 10:29:02 -04:00
semmelrindeandClaude Opus 5.5 2c4d197bf4 fix(ui): Extrude and Revolve leave Profiles empty when the last sketch is already used (#684) (#705)
With nothing selected, Extrude and Revolve no longer pre-pick the last sketch's profile when a feature already builds from it, so OK no longer cuts into a part nobody picked. Fixes #684.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:28:17 -04:00
NOWN-DEV 4ff8f3e14d feat: free-form cut-outs stay exact in sheet-metal flat patterns (#692)
Sheet-metal cut-outs and base flange outlines drawn with ellipses, splines or text stay exact curves in the flat pattern and the unfolded body; the folded body follows them with tangent arcs within 0.001 mm (#659).
2026-10-13 10:18:15 -04:00
Irakli Kourtis eac4d66adc feat: variable fillet radius may run down to zero at one end (#675)
A variable fillet's radius may run down to zero at one end, where the round closes to the corner.
2026-10-13 10:17:55 -04:00
Lasse Lian cb071bb93b fix: axis or tangent plane on a drill tip no longer fails with "no face at NaN" (#567) (#655)
The nearest point on a triangle with a zero-length edge (a cone's tip) is no longer NaN, so an axis or tangent plane on a drill tip works again (#567).
2026-10-13 10:17:49 -04:00
selune 0e500cacd3 Render: add backend capability discovery and strict request resolution (#650)
Adds render.backends, a metadata-only command that lists the render backends, their executors and profiles, with strict request resolution in the render crate (docs/render-backends.md).
2026-10-13 10:17:41 -04:00
bherbruckandClaude Opus 5.5 1e4387f3ea fix: cuts no longer split walls they don't touch (#670)
A cut built slab by slab (the exact prism boolean) sewed the result from horizontal
slabs and only joins merged the coplanar pieces back. So a floor vent cut through the
Raspberry Pi case split the long wall into strips at the ports' tops and bottoms
(87 faces instead of 79 at 32 mm), and at 24 mm the end opening split the end wall.

- Prism booleans merge coplanar neighbours for every operation (faces only: edges,
  curves and surfaces stay exact), keeping each face's construction source where all
  its pieces share it, Split Face lines and regularization protection. Painted or
  protected operands keep their authored pieces as before.
- The prism history now tells apart a wall's several edges with its caps (a wall
  with openings meets each opening's floor), so cuts beside existing openings keep
  exact history and faces keep their names.
- Tests: the Pi case's faces pinned after every feature at 24, at 24 changed to 32,
  and built at 32 (each outer side one face); random openings and bosses on a
  shelled case never split another side; every corpus design rebuilds a parameter
  edit like a fresh build and reverts exactly.
- Action matrix baseline: demo/pi-case re-blessed (fewer, merged faces; no
  regressions), demo/pi-case-0.8 recorded, and 88 new passes on
  regressions/lid_ribs_flush_with_window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:14:52 -04:00
bherbruckandClaude Opus 5.5 9ba72e118b Sketch: accept redundant but consistent constraints (#687) (#699)
A clover of same-size rectangles drawn around a shared corner was refused as over-constrained.

Constraints that remove no degree of freedom but still hold (zero residual) are now kept and shown dimmed as redundant. Conflicting ones are still refused. A redundant driving dimension is refused with "already determined by ...". Typed sizes become driven (reference) dimensions. Drawing no longer adds Horizontal/Vertical to a side that is already implied through two coincident corners. The solver reports redundant constraints using a Jacobian row-dependence test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:12:55 -04:00
bherbruckandClaude Opus 5.5 add4a18961 fix: fuse bodies that touch back to back over part of a face (#679, #674) (#704)
Two L's extruded up and down from one 2 x 2 grid now join into one solid (the planar rebuild gives each fan of faces around a pinched vertex its own vertex copy), and a pad whose end lies half past the edge of the face it stands on fuses exactly: contact faces are imprinted with each other's outline and the common faces glued away, still held to the retry guard. Face splitting handles a line crossing an edge twice; glue matches differently parametrised curves and keeps several pieces apart. Robustness seed 16 now fuses with the exact volume.

Fixes #679, fixes #674.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:02:44 -04:00
bherbruckandClaude Opus 5.5 17e42e7b94 Action matrix: mark the ribbed lid's side-wall hole as failing while it varies between runs (#708)
The ratchet has no flaky marker, so regressions/lid_ribs_flush_with_window Lid/F11:+y face.hole is marked as failing until the boolean is made deterministic (#707).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:56:43 -04:00
NOWN-DEV 812669e861 fix: an edge flange runs along what cut-outs leave of its edge (#702)
An edge flange picked on an edge that cut-outs have partly removed is built along what is left of that edge (the stretch nearest the pick), so each side of a notch can get its own flange.
2026-10-13 09:53:53 -04:00
semmelrinde c166c64b8e test: measure a scaling pair again when the load of other test binaries spikes (#649)
The two scaling tests measure a pair again (up to three times, keeping each size's fastest run) when other test binaries slow one down.
2026-10-13 09:38:28 -04:00
bherbruckandClaude Opus 5.5 0b1e61504d File format 17 for qualified piece names and sheet metal rim names (#700)
0.8.0 shipped format 16. Lineage names of a piece qualified by the faces around it, and sheet face names of a rim (rim.<edge>) or a bend end (rim.bend0/1), now come under format 17. Format 16 designs need no transformation, and the version stamp makes 0.8.0 ask for an update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:34:20 -04:00
bherbruckandClaude Opus 5.5 7249623855 Home and Fit frame what is shown now; middle double-click goes Home (#701)
Home, Fit, the orbit pivot and the scene's depth range counted sketches the view doesn't draw (ones used by an extrude or revolve, ones in hidden components), so after a body moved away from its sketch Home framed the empty spot it came from. They now use the world-space bounds of what is shown now (view::visible_bounds / view::sketch_drawn, viewport::view_bounds). The pick-box cache holds its world state instead of its address. A middle-button double-click goes Home in every preset (Preferences › Navigation: Home, Fit or Off).

Fixes #689

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:33:55 -04:00
Karl Von Richter 7b34d57b25 feat(ui): offer a cube navigator alongside signed axes (#664)
Adds a View navigator preference (Navigation page) with an optional cube navigator alongside the signed axes, which stay the default (#165).
2026-10-13 09:28:52 -04:00
lucas-aguilar89 aa24d9f071 Pasted features act on the pasted bodies (#613) (#682)
Pasted features now act on the bodies pasted with them instead of the originals (#613).
2026-10-13 09:28:27 -04:00
semmelrinde 319d360c85 fix(ui): a refused OK's error clears once the dialog's inputs change (#641) (#666)
A dialog's error from a refused OK now clears once its inputs, picks or the design change (#641).
2026-10-13 09:28:15 -04:00
selune e10b0b7d22 fix(ui): edit sketch text from letter interiors (#694)
Edit Text now works from a letter's interior as well as its outline, from the tool and the context menu, and highlights the whole text while editing.
2026-10-13 09:28:05 -04:00
lucas-aguilar89 d1a773a684 Save changes prompt: arrow keys pick the button, Enter gives it (#683)
In the save-changes prompt, Left and Right pick a button and Enter gives it.
2026-10-13 09:27:46 -04:00
Jonas Wrede fe1e5f1b95 test: regression test for #623: blind bore into a joined boss fails to mesh (fails on v0.7.0, passes on main) (#654)
Adds a regression test for the blind bore into a joined boss (#623).
2026-10-13 09:27:36 -04:00
bherbruckandClaude Opus 5.5 1e18ca9a46 Sketch Dimension: D, click, type (#695)
As soon as a pick settles the dimension (one line, circle or arc, or the second point or line of a distance or angle), its value box takes the keyboard with the measured value selected. The text follows the cursor while typing and Enter places it there (or in its default place), as one undo step. A click before typing pins the text and keeps the box focused, a click on another entity makes a distance or angle, Tab hands back to placing first, Esc cancels. Letters go into the box, not to tool shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:23:41 -04:00
bherbruckandClaude Opus 5.5 5ef1dcba8e Sheet metal rims and holes, plastic features, lofts, raised pieces and merged walls take face attachments (#677)
Sheet metal rims and bend ends attach by name (<panel>:rim.<edge>, <flange>:rim.bend0/1), as do face profiles from sheet faces. A plain or counterbored Hole square to a panel is machined into the flat pattern. Boss, Lip, Rest and Snap Fit keep the body's history and name their own faces. Hole tools name their faces by the part they cut, and pattern copies as copies. A pattern's repeated fillets keep the body's history. Loft sides are named by their section curves. A piece Offset Face moved off its face's plane attaches by its own qualified name. Sides of a profile grown to overlap a flush body keep their sketch line's name, so walls a Join merged take a sketch (#681). Face sketch 75% -> 90%, offset plane 75% -> 90%, press-pull/extrude 64% -> 85% in the action matrix; baseline blessed.

Fixes #494, fixes #495, fixes #560, fixes #561, fixes #563, fixes #564, fixes #681.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:13:09 -04:00
bherbruckandClaude Opus 5.5 e30f3c9ebe fix: Keep the loading card's Cancel button still; deterministic Cancel test (#686)
The loading card was centred on its height, so Cancel moved when the progress bar or the saved preview appeared, and a click could miss it. That's what made the Cancel test flaky on slow runners. The card is now anchored at its top, Cancel sits in the heading row and the progress bar is always shown. Progress::hold_at lets the test hold a load at a feature boundary until it's cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:05:13 -04:00
bherbruckandClaude Opus 5.5 738d532ce6 README: add the SELENE rover by @0xlillith as a showcase (#690)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:55:12 -04:00
bherbruckandClaude Opus 5.5 434fbc2d73 test: seed 16's partial face contact is refused, not built wrong (#676)
Main was red since #577 and #644 landed together. Seed 16's pattern copy rests half past the block's edge; the join used to add 72 mm³ past the edge silently and is now correctly refused. The test records that; proper fusion is tracked separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:28:04 -04:00
bherbruckandClaude Opus 5.5 302dd7e343 README demo for 0.8: the Raspberry Pi 4 case again, with a rounded rim, engraved text and PETG (#671)
The `pi` story of examples/demo/enclosure_ui_mcp.py keeps the 0.6 flow and adds the rim
rounded in one pick (a tangent chain), "Pi 4" engraved in the floor in DejaVu Sans Bold, and a
PETG material defined in the design, with its mass in Properties, before the height change.
The 0.6 story stays as `pi-0.6` (it built demo/pi-case). The center rectangle now comes from
the Rectangle drop-down. The finished case is in the action matrix corpus as
demo/pi-case-0.8.

docs/screenshots/demo-0.8.gif (9.3 MB) and demo-0.8.mp4 (9.9 MB) replace the 0.6 files: 41 s
at 9x playback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:23:27 -04:00
bherbruckandClaude Opus 5.5 d2086835c1 flatpak-site: publish a release from before the app id change as it is (#668)
The Pages deploy failed while the latest published release (0.7.0) still carries ai.storyteller.solvecraft: the bundle check only accepted the new id and exited without a message. Accept either id (both architectures must agree), mark the old refs end-of-life only once the new app is in the repository, show the published id in the install instructions, and name the failing command on any error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:53:10 -04:00
520 changed files with 72647 additions and 8253 deletions

No files matched your search

+1
View File
@@ -10,6 +10,7 @@ not (
(package(solidcraft-engine) & (
test(=tests::hostile_params_never_panic)
| test(/^tests::robustness::/)
| test(/^tests::robustness_goldens::/)
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
+77 -49
View File
@@ -5,68 +5,96 @@ parameter to watch the timeline rebuild. [Documentation](https://bherbruck.githu
[issues](https://github.com/bherbruck/solidcraft/issues). AI agents can model with it over MCP:
`solidcraft-cli mcp` ([how](https://bherbruck.github.io/solidcraft/guide/mcp.html)).
**Why 0.8:** this is a minor bump because the design file format moved to version 16, and
**Why 0.9:** this is a minor bump because the design file format moved to version 17, and
because it adds many new features.
**What's new**
- **Face actions work on far more faces.** Sketches, offset planes and press-pull now attach to
Cut, Hole and chamfer faces, each pattern or mirror copy, side walls a Join merged and the
pieces a later feature split a face into
([#625](https://github.com/bherbruck/solidcraft/pull/625)), to moved or pasted components
([#610](https://github.com/bherbruck/solidcraft/pull/610)) and beside long thin holes
([#629](https://github.com/bherbruck/solidcraft/pull/629)). Across the test models, flat-face
actions pass 70% of the time, up from 61% (sketch 52% → 71%, offset plane 52% → 71%,
press-pull 41% → 60%).
- **Fillet and chamfer** handle a single edge whose ends meet curved or slanted faces, and blend
smooth chains of edges ([#646](https://github.com/bherbruck/solidcraft/pull/646)): 83% → 93%
of edges.
- **New:** user-defined physical materials stored in the design, linked components (insert
another design), saved body visibility, named views through `view.apply`, sketch text fonts,
corner reliefs, flanges on flanges, edge flanges that trim the base along their edge, grouped
sketch tools, Home thumbnails, and an improved Select Other list.
- **Fixes:** filleting beside a modeled thread no longer freezes the app, a loft standing on a
face fuses with it, and many sheet metal, hole and sketch fixes.
- **Booleans are checked.** A Boolean retry that would change the part away from the tool is now
refused instead of returning wrong geometry, which fixes stray cuts
([#616](https://github.com/bherbruck/solidcraft/issues/616)). MCP feature replies include
warnings.
- **Opening a design** keeps the window draggable, shows progress, and can be cancelled with
Cancel or Esc.
- **Under the hood:** compact MCP replies, a kernel-neutral native B-rep format, and an opt-in
preview from the native cache while a design rebuilds.
- **Quality:** every ordinary action is run on every face, edge, vertex and body of the test
models; CI blocks regressions and the full matrix runs nightly.
- **Security:** private vulnerability reporting is on; see
[SECURITY.md](https://github.com/bherbruck/solidcraft/blob/main/SECURITY.md).
- **Face actions work on about 90% of flat faces.** Sketches, offset planes and press-pull now
attach to sheet metal rims and holes, plastic features (Boss, Lip, Rest, Snap Fit), lofts,
revolved faces, pieces Offset Face moved and walls a Join merged
([#677](https://github.com/bherbruck/solidcraft/pull/677), fixing
[#681](https://github.com/bherbruck/solidcraft/issues/681)). Across the test models: sketch
75% → 90%, offset plane 75% → 90%, press-pull 64% → 85%.
- **Joins:** a logo-style part with pieces extruded up and down from one sketch now joins
([#679](https://github.com/bherbruck/solidcraft/issues/679)), faces that only partly overlap
fuse correctly ([#704](https://github.com/bherbruck/solidcraft/pull/704), fixing
[#674](https://github.com/bherbruck/solidcraft/issues/674)), and booleans build the same body
every time ([#748](https://github.com/bherbruck/solidcraft/pull/748)), so a design rebuilds
identically on every run.
- **Sketch:** type a dimension right after picking (D, click, type;
[#695](https://github.com/bherbruck/solidcraft/pull/695)); redundant but consistent
constraints are accepted, such as a rectangle clover
([#699](https://github.com/bherbruck/solidcraft/pull/699)); tool windows for Scale, Offset,
Mirror, Move/Copy, Fillet and Chamfer and the rectangular pattern (thanks @wdunn001), shown
next to the Sketch Palette, which now lists the running tool's options; a typed Rho for the
conic; a Construction Geometries row in the Sketch Palette that hides construction geometry
([#804](https://github.com/bherbruck/solidcraft/pull/804)); sketch tool shortcuts open a sketch on the selected plane; Edit Sketch from the viewport
menu.
- **Features:** feature history on the left side with folders (opt in under Preferences;
[#449](https://github.com/bherbruck/solidcraft/pull/449)); Convert to Sheet Metal from a solid
or shelled body by its bend edges ([#725](https://github.com/bherbruck/solidcraft/pull/725));
a tear corner relief ([#809](https://github.com/bherbruck/solidcraft/pull/809)); tapered and
variable fillet radii ([#675](https://github.com/bherbruck/solidcraft/pull/675),
[#749](https://github.com/bherbruck/solidcraft/pull/749)) with a radius spline you drag on the
edge ([#752](https://github.com/bherbruck/solidcraft/pull/752)); free-form cut-outs stay exact
in sheet-metal flat patterns ([#692](https://github.com/bherbruck/solidcraft/pull/692)) and edge
flanges run along what cut-outs leave of an edge
([#702](https://github.com/bherbruck/solidcraft/pull/702)); and Surface Extrude, sheets from
open curves and profile walls ([#815](https://github.com/bherbruck/solidcraft/pull/815)).
- **View:** Home and Fit frame only what is shown, and a middle double-click goes Home
([#701](https://github.com/bherbruck/solidcraft/pull/701)); a cube navigator option
(thanks @Karl-VonRichter, [#664](https://github.com/bherbruck/solidcraft/pull/664)); the window
stays responsive and draggable while a design opens, with a Cancel button that stays put
([#643](https://github.com/bherbruck/solidcraft/pull/643),
[#686](https://github.com/bherbruck/solidcraft/pull/686)).
- **Quality:** cuts no longer split walls they don't touch
([#670](https://github.com/bherbruck/solidcraft/pull/670)); booleans are faster and use less
memory ([#714](https://github.com/bherbruck/solidcraft/pull/714)); STL export welds shared
vertices ([#816](https://github.com/bherbruck/solidcraft/pull/816)); complete Japanese
translation ([#753](https://github.com/bherbruck/solidcraft/pull/753)).
- **Fixes:** pasted features act on the pasted bodies (#613); a refused OK's error clears when
you change the inputs (#641); dialogs show the units of their sizes (#630); the wheel over a
dialog's list scrolls only the list (#678); a Single hole picked at a circle's centre lands on
the centre (#657); axis or tangent plane on a drill tip works (#567); sketch text edits from
letter interiors and moves as a whole; a projected edge stays the edge you picked; a size from
a bare diameter keeps its unit (#716); Convert Mesh says how big a mesh is and what converts;
the Save changes prompt takes arrow keys and Enter.
- **MCP:** agent skills for modeling and editing over MCP
([#728](https://github.com/bherbruck/solidcraft/pull/728)).
- A new README demo and the SELENE rover showcase.
**File format 16.** Designs are now format 16 (`solidcraft/16`). SolidCraft 0.7 and older can't
open designs saved by 0.8, as a container or as JSON; keep a copy of a file you may still open
in 0.7. Files from every earlier version still open in 0.8.
**File format 17.** Designs are now format 17 (`solidcraft/17`). SolidCraft 0.8 and older can't
open designs saved by 0.9, as a container or as JSON; they ask you to update instead. Keep a
copy of a file you may still open in 0.8. Files from every earlier version still open in 0.9.
**Linux app ID.** The Linux app ID is now `ai.storyteller.solidcraft` (it was
`ai.storyteller.solvecraft`). Flatpak users from the SolidCraft feed: `flatpak update` reports
the old app as end-of-life and asks to replace it; say yes, and your settings come along. The
`.deb`, `.rpm` and AppImage remove the old menu entry on upgrade or first launch.
**Upgrading from 0.8.** Nothing to do: open your designs and save (they're saved as format 17
from then on). Feature-history folders on
the left are off by default; turn them on in Preferences.
**Known limitations**
- Some faces still can't take an attached sketch: after Revolve, Offset Face or Boss
([#494](https://github.com/bherbruck/solidcraft/issues/494)), sheet metal thickness faces
([#495](https://github.com/bherbruck/solidcraft/issues/495)), Loft, Sweep and Coil faces
([#561](https://github.com/bherbruck/solidcraft/issues/561)), faces after a Draft or a tapered
Extrude ([#563](https://github.com/bherbruck/solidcraft/issues/563)) and a Shell's rim
([#564](https://github.com/bherbruck/solidcraft/issues/564)).
- A Hole on a sheet metal face is refused
([#560](https://github.com/bherbruck/solidcraft/issues/560)); some through-all holes along a
wall or beside curved faces still fail
([#559](https://github.com/bherbruck/solidcraft/issues/559)).
- Some through-all holes along a wall or beside curved faces still fail
([#559](https://github.com/bherbruck/solidcraft/issues/559)), as does a hole crossing an
equal-radius hole ([#788](https://github.com/bherbruck/solidcraft/issues/788)).
- Some fillets are still refused, such as along an outline of lines and arcs
([#431](https://github.com/bherbruck/solidcraft/issues/431)).
- Cutting a coaxial cylinder from a thin disc can lose a little volume
([#500](https://github.com/bherbruck/solidcraft/issues/500)); an edge flange with different
- Some faces still can't take an attached sketch: a revolve's flat faces before a later feature
([#726](https://github.com/bherbruck/solidcraft/issues/726)) and faces after Split Body or
Combine Intersect ([#565](https://github.com/bherbruck/solidcraft/issues/565)).
- Fillet and chamfer accept an edge pick far from any edge and change the nearest one
([#783](https://github.com/bherbruck/solidcraft/issues/783)); Press Pull fails on faces beside
rounds ([#562](https://github.com/bherbruck/solidcraft/issues/562)).
- `sheet.fold` refuses a fixed point exactly on the sheet face
([#685](https://github.com/bherbruck/solidcraft/issues/685)), and some tabs on L-shaped or
collinear blanks can't be folded ([#638](https://github.com/bherbruck/solidcraft/issues/638),
[#639](https://github.com/bherbruck/solidcraft/issues/639)); an edge flange with different
per-edge angles leaves gaps at the corners
([#501](https://github.com/bherbruck/solidcraft/issues/501)).
- More in [ROADMAP.md](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations).
**Upgrading from 0.7 or earlier.** 0.8 changed the Linux app ID to `ai.storyteller.solidcraft`:
`flatpak update` offers to replace the old app, and the `.deb`, `.rpm` and AppImage remove the old
menu entry.
**Upgrading from 0.5 or earlier.** 0.6 renamed SolveCraft to SolidCraft and made the zip
container the default save format. `.solvecraft` designs, old settings folders and
`SOLVECRAFT_*` variables are still picked up, and `solvecraft-cli` still forwards to
+45 -4
View File
@@ -21,8 +21,9 @@ permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
# Every push to main gets its full run; a new PR push replaces the old run.
# A new PR push replaces the old run. Only one run per group may wait, so a busy main would
# cancel the waiting nightly (or a Run workflow) before it starts: they get groups of their own.
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
@@ -47,6 +48,7 @@ jobs:
runs-on: ubuntu-24.04
outputs:
native: ${{ steps.paths.outputs.native }}
kernel: ${{ steps.paths.outputs.kernel }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -59,12 +61,14 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
native=false
kernel=false
if [ "$EVENT_NAME" != pull_request ]; then
native=true
else
# Compare both old and new paths of renames; unknown paths run native checks.
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
while IFS= read -r -d '' path; do
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
case "$path" in
.github/workflows/ci.yml) native=true ;;
*.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;;
@@ -74,6 +78,7 @@ jobs:
done < "$RUNNER_TEMP/ci-paths"
fi
echo "native=$native" >> "$GITHUB_OUTPUT"
echo "kernel=$kernel" >> "$GITHUB_OUTPUT"
checks:
name: Format and repository checks
@@ -95,6 +100,8 @@ jobs:
run: cargo xtask licences --check
- name: Dependency layers
run: cargo xtask layers
- name: Web app offline use and updates (headless Chrome)
run: cargo xtask web-offline-test
clippy:
name: Native Linux Clippy
@@ -150,7 +157,7 @@ jobs:
tests:
name: Native Linux tests (${{ matrix.shard }}/4)
needs: build-tests
needs: [changes, build-tests]
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
@@ -177,6 +184,16 @@ jobs:
# the build job's path, which is the same checkout path here.
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --partition "slice:$SHARD/4" "${all[@]}"
- name: Robustness goldens sample (pull requests that touch the kernel)
# Every 8th seed of tests/robustness/goldens.json, a quarter per shard; pushes to main and
# the nightly run every seed in the step above.
if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true'
env:
SHARD: ${{ matrix.shard }}
run: |
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --ignore-default-filter \
-E "package(solidcraft-engine) & test(=tests::robustness_goldens::sample_$((SHARD - 1)))"
- name: Upload test timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -269,9 +286,31 @@ jobs:
name: oracle-report
path: docs/oracle.md
# D3D12's older shader compiler (FXC, used where dxcompiler.dll is absent) is stricter than
# DXC, Vulkan and Metal: a shader it cannot compile once crashed the app at startup. Build the
# GPU tessellation pipeline with FXC on the runner's WARP software adapter and compare its
# results with the CPU.
windows-fxc:
name: Windows D3D12 FXC shaders
needs: changes
if: needs.changes.outputs.native == 'true'
runs-on: windows-latest
timeout-minutes: 45
env:
WGPU_BACKEND: dx12
WGPU_DX12_COMPILER: fxc
SOLIDCRAFT_REQUIRE_GPU: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-fxc
- run: cargo test -p solidcraft-gpu --profile ci --locked
# Preserve the existing check name for branch protection, including docs-only PRs.
ci:
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle]
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc]
if: always()
runs-on: ubuntu-24.04
steps:
@@ -285,6 +324,7 @@ jobs:
TESTS: ${{ needs.tests.result }}
DOCS: ${{ needs.doc-tests.result }}
ORACLE: ${{ needs.oracle.result }}
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
run: |
test "$CHANGES" = success
test "$CHECKS" = success
@@ -294,6 +334,7 @@ jobs:
test "$TESTS" = success
test "$DOCS" = success
test "$ORACLE" = success
test "$WINDOWS_FXC" = success
fi
wasm:
+7 -7
View File
@@ -6,9 +6,9 @@ name: Packaging lint
on:
push:
branches: [main]
paths: [".github/workflows/**", "packaging/**"]
paths: [".github/workflows/**", "packaging/**", "assets/**"]
pull_request:
paths: [".github/workflows/**", "packaging/**"]
paths: [".github/workflows/**", "packaging/**", "assets/**"]
workflow_dispatch:
permissions:
@@ -52,13 +52,13 @@ jobs:
}
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
xmllint --noout packaging/windows/solidcraft.wxs packaging/windows/solidcraft.en-us.wxl packaging/macos/*.plist* packaging/linux/*.xml*
desktop-file-validate packaging/linux/ai.storyteller.solidcraft.desktop
xmllint --noout assets/platforms/windows/solidcraft.wxs assets/platforms/windows/solidcraft.en-us.wxl assets/platforms/macos/*.plist* assets/platforms/linux/*.xml*
desktop-file-validate assets/platforms/linux/ai.storyteller.solidcraft.desktop
# The window's X11 class is its app id; the old id stays only as the rename shims.
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' packaging/linux/ai.storyteller.solidcraft.desktop
grep -q '<id>ai.storyteller.solvecraft</id>' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' assets/platforms/linux/ai.storyteller.solidcraft.desktop
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in
grep -qF -e '--end-of-life-rebase=' packaging/flatpak-site.sh
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solidcraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solidcraft' and m['command'] == 'solidcraft'"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
+2
View File
@@ -50,6 +50,8 @@ jobs:
- name: Build the web app
working-directory: apps/solidcraft-web
run: trunk build --release --public-url /solidcraft/app/
- name: Test the web app offline and across an update (headless Chrome)
run: cargo xtask web-offline-test --site dist/web
- name: Assemble the site
run: |
mkdir -p site
+10 -4
View File
@@ -59,8 +59,8 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
operations are our own.
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
artwork from commercial CAD products — ever.** Every icon is drawn in code
(`crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code
(`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **Rust only.** Units are millimetres and radians internally; Z is up.
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
assets, layers). Commit after every feature arc that builds.
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
@@ -122,7 +125,10 @@ before the draft is verified.
## Running and looking at the app
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
request on a connection carries the launch's token as `"token"`: read it from
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
+77 -29
View File
@@ -8,45 +8,44 @@ file is missing from this table.
templates, materials or help text.** Every asset is original work by SolidCraft contributors or
third-party material under an open licence. Screenshots of Autodesk software are never committed.
Generated-in-code assets are original and have no file to list:
- the UI icon set (`crates/ui-egui/src/icons.rs`);
- the axis view, navigation bar and timeline glyphs (`crates/ui-egui/src/viewport.rs`,
`crates/ui-egui/src/timeline.rs`).
Editable drawing code, native projects and shipped exports are collected under `assets/`.
## Files
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
| assets/Assets.car | SolidCraft contributors | Apple actool compilation of assets/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
| assets/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/icon.svg from VectorCraft | MIT OR Apache-2.0 |
| assets/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/archive/icons/macos-v1/exports/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/archive/icons/macos-v1/exports/icon.svg from VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.6.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.6.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | MIT OR Apache-2.0 |
| docs/book/src/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| assets/branding/web/source/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
| docs/book/src/generated/sample-plate.png | SolidCraft contributors | copy of docs/screenshots/sample-plate.png made by `cargo xtask book` (gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/bracket.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot examples/bracket.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/enclosure.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/enclosure.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/revolve.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/revolve.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/sheet-metal.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/sheet-metal.json`; gitignored) | MIT OR Apache-2.0 |
| crates/sketch/fonts/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/fonts/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in apps/solidcraft-web/fonts/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| 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 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -68,6 +67,45 @@ Generated-in-code assets are original and have no file to list:
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | 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/ai.storyteller.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/ai.storyteller.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/ai.storyteller.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/256x256/apps/ai.storyteller.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/32x32/apps/ai.storyteller.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/48x48/apps/ai.storyteller.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/512x512/apps/ai.storyteller.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/64x64/apps/ai.storyteller.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/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | 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/windows/exports/solidcraft.ico | 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/ui/source/glyphs.rs | SolidCraft / VectorCraft contributors | Original glyphs; caption geometry adapted from VectorCraft titlebar.rs (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
| assets/icons/ui/source/icons.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/markers.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
## Code adapted from sibling projects
| SolidCraft file | Adapted from | Licence |
@@ -84,6 +122,16 @@ Generated-in-code assets are original and have no file to list:
| LICENSE-MIT, LICENSE-APACHE (licence text) | CADCraft | — |
| .github/workflows/release.yml, .github/workflows/packaging-lint.yml and packaging/ (release pipeline: Linux AppImage/deb/rpm/Flatpak/tar.gz, FreeBSD, macOS dmg, Windows MSI/portable zip, web zip; AppRun, nfpm, Flatpak, WiX and Info.plist templates) | PhotoCraft (https://github.com/storytold/photocraft) `.github/workflows/{release,packaging-lint}.yml`, `packaging/` | Apache-2.0 |
## Incremental HDR codec adapter
`crates/render/src/lighting/hdr.rs` adapts the scanline/component/old-RLE decoding
state machines from the Radiance decoder in `image` 0.25.10
([source](https://docs.rs/crate/image/0.25.10/source/src/codecs/hdr/decoder.rs)),
MIT OR Apache-2.0. Metadata parsing still uses that library. The adapter makes row
work cooperative and adds checked run lengths, zero-run rejection, width validation,
header/input/radiance bounds and cancellation. Original licence texts are retained
in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
## Recovered contributions
| SolidCraft file | Original contribution | Licence |
@@ -94,7 +142,7 @@ Generated-in-code assets are original and have no file to list:
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro (vendor/README.md) |
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
+8 -2
View File
@@ -75,7 +75,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **Rust only.** Units are millimetres and radians internally; Z is up.
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
assets, layers). Commit after every feature arc that builds.
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
@@ -99,7 +102,10 @@ before the draft is verified.
## Running and looking at the app
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
request on a connection carries the launch's token as `"token"`: read it from
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
+2 -1
View File
@@ -161,7 +161,8 @@ Pull requests skip a few heavy tests (one to four minutes each), which run on ev
`main` and nightly; check the `main` run after merging. They are listed in
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
`tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part),
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`
Generated
+32 -13
View File
@@ -3281,11 +3281,12 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"eframe",
"egui",
"embed-resource",
"getrandom 0.3.4",
"hidapi",
"log",
"quick-xml",
@@ -3298,7 +3299,7 @@ dependencies = [
[[package]]
name = "solidcraft-cli"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -3307,7 +3308,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"log",
"serde",
@@ -3322,7 +3323,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"log",
"serde",
@@ -3338,16 +3339,29 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.21",
]
[[package]]
name = "solidcraft-gpu"
version = "0.10.1"
dependencies = [
"log",
"naga",
"pollster 0.4.0",
"solidcraft-geom",
"solidcraft-kernel",
"thiserror 2.0.21",
"wgpu",
]
[[package]]
name = "solidcraft-io"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"quick-xml",
"serde",
@@ -3362,12 +3376,14 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"log",
"rayon",
"serde",
"serde_json",
"solidcraft-geom",
"spade",
"thiserror 2.0.21",
"truck-meshalgo",
"truck-modeling",
@@ -3378,7 +3394,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -3386,7 +3402,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"image",
"serde",
@@ -3396,7 +3412,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"serde",
"serde_json",
@@ -3407,22 +3423,24 @@ dependencies = [
[[package]]
name = "solidcraft-ui-egui"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"egui",
"egui-wgpu",
"image",
"log",
"pollster 0.4.0",
"serde",
"serde_json",
"solidcraft-engine",
"solidcraft-gpu",
"ttf-parser",
"web-time",
]
[[package]]
name = "solidcraft-web"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"eframe",
"egui",
@@ -4845,9 +4863,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.8.0"
version = "0.10.1"
dependencies = [
"serde_json",
"sha2",
]
[[package]]
+11 -2
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.8.0"
version = "0.10.1"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -18,6 +18,7 @@ solidcraft-geom = { path = "crates/geom" }
solidcraft-sketch = { path = "crates/sketch" }
solidcraft-kernel = { path = "crates/kernel" }
solidcraft-render = { path = "crates/render" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
solidcraft-io = { path = "crates/io" }
solidcraft-engine = { path = "crates/engine" }
@@ -37,6 +38,10 @@ truck-shapeops = { version = "0.4", default-features = false }
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
truck-stepio = { version = "0.3", default-features = false }
truck-topology = "0.6"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
# tree through truck-meshalgo.
spade = { version = "2.15", default-features = false, features = ["std"] }
rayon = "1.12"
# packages (3MF)
zip = { version = "3", default-features = false, features = ["deflate"] }
@@ -46,7 +51,7 @@ quick-xml = "0.41"
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
# images
image = { version = "0.25", default-features = false, features = ["png"] }
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
# ui
egui = "0.36"
@@ -54,6 +59,10 @@ 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.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 }
pollster = "0.4"
rfd = "0.17"
[workspace.lints.rust]
+5
View File
@@ -19,6 +19,11 @@ licensed MIT OR Apache-2.0; ATTRIBUTION.md lists the files.
The release packaging (.github/workflows/release.yml, packaging/) is adapted from PhotoCraft
(https://github.com/storytold/photocraft), licensed under the Apache License 2.0.
The incremental Radiance RGBE row adapter in crates/render/src/lighting/hdr.rs adapts
decoding state machines from image 0.25.10 (https://github.com/image-rs/image),
MIT OR Apache-2.0. ATTRIBUTION.md describes the bounded/cancellable changes; the
original licence texts are included in THIRD-PARTY-LICENSES.txt.
Most bundled assets are drawn in code or contributed originally; third-party fonts (Liberation Sans
for sketch text, BIZ UDPGothic for web Japanese display) are licensed under SIL OFL 1.1. The
per-file list is in ATTRIBUTION.md.
+18 -5
View File
@@ -6,9 +6,9 @@ Sketch, constrain, and build solids with a parametric feature timeline. Change a
and watch your model rebuild. Run it on your desktop, in your browser, or let AI agents
model through the built-in MCP server.
![SolidCraft 0.6: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, a colour and a height change in Parameters](docs/screenshots/demo-0.6.gif)
![SolidCraft 0.8: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo-0.8.gif)
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots and a colour, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.6.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
@@ -68,6 +68,16 @@ are listed by area in [docs/features.md](docs/features.md); status and plans are
Open work and known bugs are in [GitHub issues](https://github.com/bherbruck/solidcraft/issues),
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
## Built with SolidCraft
<a href="https://github.com/0xlillith/solidcraft-rover"><img src="docs/screenshots/rover-build.gif" alt="SELENE, a scientific rover by 0xlillith, assembling itself as the timeline plays through its 3,323 steps in SolidCraft: tub, wheels, electronics, suspension, sampling arm, solar wings, dish and camera turret" loading="lazy"></a>
*SELENE, a scientific rover built entirely through SolidCraft's MCP server by
[@0xlillith](https://github.com/0xlillith/solidcraft-rover): 3,323 editable timeline steps that
rebuild with no errors, replayed here at 2× speed.
[Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/rover-build.mp4)
· [download the editable model](https://github.com/0xlillith/solidcraft-rover/releases/tag/v1.0.0).*
## What works
- **Sketches** on origin planes, offset planes or planar faces: lines, rectangles, circles,
@@ -91,7 +101,7 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
by parameters. Over-constraining edits are rejected; fully constrained geometry turns black.
Dragging solves the sketch every frame and rebuilds the model once, when the drag ends.
Select a line or its two endpoints, type its dimension and press Enter to commit one undo
step. Fit keeps sketch geometry clear of the palette and navigation controls.
step; with Sketch Dimension it's D, click, type, Enter. Fit keeps sketch geometry clear of the palette and navigation controls.
- **Profiles** found automatically, including regions split by lines and holes inside outlines.
Newly captured complete adaptive boundaries retain their outer and hole source identities
when spline span counts change. Partial or ambiguous boundaries do not qualify.
@@ -115,7 +125,8 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
provisional picture from warm geometry caches while rebuilding before editing; caches never
replace the parametric design. Export to STEP (AP242 with body names, colours and
components as assemblies; revolved faces as native planes, cylinders, cones, spheres and tori),
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ;
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ, and
assemblies with joints as URDF for robot simulators (docs/urdf.md);
3MF, STL and OBJ import as mesh bodies (they render, measure, move and export; solid features
need B-rep bodies). Convert Mesh reconstructs supported closed constant-section parts
as parametric solids with planar and circular cylindrical faces;
@@ -189,7 +200,9 @@ stdio. Headless it runs its own design session; with `--connect 127.0.0.1:PORT`
started with `solidcraft --control PORT`, so you can watch the agent model. Tools:
`list_commands`, `execute`, `batch`, `inspect_design`, `measure`, `body_topology`,
`set_parameter`, `screenshot` (returns the image), `export`, `undo`, `redo`, `new_design`, `open`,
`save`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
`save`. Tool calls reach files only in the open design's folder (when it lies inside your home
folder, never home itself or a hidden folder), SolidCraft's recovery folder and folders given with
`--allow-path DIR`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
configuration and an example session: [docs/mcp.md](docs/mcp.md).
## Architecture
+59 -10
View File
@@ -36,7 +36,7 @@ In scope, in priority order:
metal rules (thickness, K-factor, bend radius) (M10).
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need, PCB / 3D
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
## Milestones
@@ -47,18 +47,48 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure. Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular) chosen by the rule | in progress | 36 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis. Proposed, in order: surface Extrude/Revolve/Sweep/Loft from open and closed curves (#757), Offset surface (#758), Unstitch and Reverse Normal (#766), Delete Face without healing (#768); Extend curved faces and edges (#760), mutual trim and trim by projected curves (#767), Untrim (#761), Thicken every surface kind (#765); Ruled (#762), Boundary surface and curve networks with G0/G1/G2 (#763), Patch with G1/G2 and general loops (#759), Blend surface (#764), sweep rails, twist and scale (#771, loft rails #201), fillet between surface bodies (#769); continuity analysis, surface curvature combs and isocurves (#770) | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
## Physical material data
- Physical material card validation (#923/#922) provides typed SI mechanical and
thermal data, per-property sources/conditions and isotropic/orthotropic stability
checks through read-only `material.properties.validate`. It does not assign or
persist properties, provide a solver or enable rendering; see
[the contract and staged follow-ups](docs/physical-material-cards.md).
## Library foundations (#785, #786, #787)
- Proposed phases cover offline bundled/project/user packs, physical materials and
appearances, editable wood mapping, and parameterized component/assembly families.
Stable identity/revision, copied design definitions, provenance and resource limits
apply from the first implementation. Existing material, appearance and standard-part
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
for source/license review and acceptance gates. This is planning only: pack loading,
texture rendering and expanded component families are not implemented by this entry.
## Render foundation
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
it does not yet enable HDRI/PBR rendering or a document import command. See
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
bounded image/scratch admission checks, exposed by the read-only `render.backends`
command. It describes the current CPU raster snapshot path without evaluating the model.
Existing snapshot and viewport routes remain unchanged. The Render workspace, HDRI/PBR,
render jobs and optional engines remain future work; see [the contract](docs/render-backends.md)
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
## Dimensioning and error reporting
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
@@ -81,11 +111,16 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
arguments reject such a name when an older design's construction plane already has it. Use
the other alias, the construction feature's numeric ID or an explicit plane; existing
stored references keep their saved meaning. Section Analysis uses the same check.
- Sketch text can be edited from a selected outline's context menu or the Edit Text command.
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
Interior selection requires every profile boundary to belong to the same text source;
the editor highlights the whole source without replacing profile selections.
Content, baseline X/Y, height and rotation update the original text source in one undo step;
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
use the document units captured when editing starts; generated outlines remain associated
with their text source, and untouched inputs preserve their saved internal values.
Dragging a text outline moves the entire source in one undo step; Escape restores the
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
about its baseline; copies keep an independent editable text source.
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
waits for a distinct point without a degenerate-shape error or history entry. Typed input
and programmatic commands retain their validation.
@@ -180,6 +215,9 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
- Associative shell-face sketches require a unique certified planar source. Nested shell
attachments and ambiguous coincident sources remain unsupported.
- G2 fillets: exact quintic blends of straight edges between planar faces, with one perpendicular
planar end face at each end. The UI and `solid.fillet` expose `curvature` (alias `g2`); the size
is an equivalent circular setback. Curved faces/edges, sloped ends and corner junctions remain unsupported.
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
meet (a sphere for equal radii); straight edges (convex or concave) between planar faces with perpendicular
@@ -197,7 +235,14 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
between two overlapping extruded arcs).
between two overlapping extruded arcs); knife edges, where the two faces have opposite
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
other, except a round after a round of the edge between the wall and a flat face that it
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
piece of sphere.
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
@@ -233,8 +278,12 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
certificate and refine within bounded node limits while retaining existing IDs. Singular,
self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
certificate and refine within bounded node limits while retaining existing IDs.
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
speeds without changing stored nodes or relaxing the distance tolerance (#971).
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
editable source updates the derived boundary live. Break Association restores point editing.
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
+2 -2
View File
@@ -13349,11 +13349,11 @@ truck-topology 0.6.0: Apache-2.0 (https://github.com/ricosjp/truck)
Bundled fonts (SIL Open Font License, Version 1.1)
================================================================================
- LiberationSans-Regular.ttf (crates/sketch/fonts/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
- BIZUDPGothic-Regular.ttf (apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf)
- BIZUDPGothic-Regular.ttf (assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf)
Copyright 2022 The BIZ UDGothic Project Authors (https://github.com/googlefonts/morisawa-biz-ud-mincho)
Source: https://github.com/googlefonts/morisawa-biz-ud-gothic
+29 -6
View File
@@ -9,7 +9,8 @@
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT] MCP server on stdio (docs/mcp.md)
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! MCP server on stdio (docs/mcp.md)
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
//! ```
@@ -25,7 +26,7 @@ mod spatial;
use std::process::ExitCode;
use serde_json::{Value, json};
#[path = "../../app_icon.rs"]
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
use solidcraft_engine::Session;
@@ -43,7 +44,7 @@ const USAGE: &str = "usage:
solidcraft-cli recipe <recipe.json>
solidcraft-cli oracle <case-dir>... [--json]
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT]
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
solidcraft-cli licences
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
@@ -312,11 +313,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
}
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
/// a running app's control port. stdout carries only protocol messages.
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
fn cmd_mcp(args: &[String]) -> Result<(), String> {
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();
eprintln!(
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
roots.join(", ")
);
let design_dir = flag(args, "--in")
.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) {
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
}
}
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect(addr).map_err(|e| {
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect_with_token(addr, flag(args, "--token")).map_err(|e| {
let port = addr.rsplit(':').next().filter(|p| p.parse::<u16>().is_ok()).unwrap_or("PORT");
format!(
"cannot connect to {addr}: {e}\nno SolidCraft app is listening there; start it with \
@@ -328,7 +345,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
};
let stdin = std::io::stdin();
let stdout = std::io::stdout();
solidcraft_mcp::Server::new(backend).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())
}
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
@@ -393,10 +410,13 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let read_ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", json!({"step": "read", "ms": read_ms, "bytes": bytes.len()}));
let t = std::time::Instant::now();
// The file as read, held until the timeline has evaluated it (as the import command does).
let mut read = None;
let doc = if step {
let imported = solidcraft_engine::io::step_import_feature(&bytes, path).map_err(|e| e.to_string())?;
let mut doc = solidcraft_engine::doc::Document::new(&imported.name);
doc.add_feature(imported.kind, Some(&imported.name)).map_err(|e| e.to_string())?;
read = Some(imported.import);
doc
} else {
solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?
@@ -408,12 +428,15 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
}
let t = std::time::Instant::now();
let s = Session::new(doc);
drop(read);
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0}));
for r in &s.model.results {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
let t = std::time::Instant::now();
let st = s.world_state();
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
Ok(())
+17 -1
View File
@@ -514,7 +514,23 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
out.push(json!({"command": "solid.mirror", "params": p}));
}
Some("fillet") => {
out.push(json!({"command": "solid.fillet", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
let mut p = json!({"edges": edge_points(f), "radius": f.get("radius"), "name": name});
// Constant radius (the default), chord length, or a radius varying along each edge
// (`radius` at the end nearest `start`, `radius2` at the other, `mid` between,
// `smooth`), or a B-spline along each edge (`radius` its first control radius,
// `radii` the rest, `knots`); `fit` for a radius too large for the geometry.
match f.get("type").and_then(Value::as_str) {
Some("chord_length") => p["type"] = json!("chord"),
Some("variable_radius") => p["type"] = json!("variable"),
Some("spline_radius") => p["type"] = json!("spline"),
_ => {}
}
for key in ["radius2", "start", "mid", "smooth", "fit", "radii", "knots"] {
if let Some(v) = f.get(key) {
p[key] = v.clone();
}
}
out.push(json!({"command": "solid.fillet", "params": p}))
}
Some("chamfer") => {
out.push(json!({"command": "solid.chamfer", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
+155 -8
View File
@@ -47,6 +47,10 @@ const SLOW: [&str; 7] = [
const EDIT_SAMPLE: u64 = 6;
/// An action that takes longer than this is recorded as a timeout.
const ACTION_TIMEOUT: Duration = Duration::from_secs(90);
/// Building a design and listing its actions, before the first one runs: the biggest designs
/// (oracle 41's tube with radial holes, about 25 s here) take several times that on a loaded CI
/// runner, and a design that never loads loses all its actions, not one.
const LOAD_TIMEOUT: Duration = Duration::from_secs(300);
/// Caps per body, so one large imported or patterned body cannot dominate the run.
const MAX_FACES: usize = 120;
const MAX_EDGES: usize = 240;
@@ -635,7 +639,14 @@ fn action_matrix_worker() {
}
let list = actions(&s);
let edit = driving_parameter(&s);
// Local runs only: `ACTION_MATRIX_ACTIONS=edge.fillet,edge.chamfer` tries just those actions
// (don't bless such a run), `ACTION_MATRIX_VALIDITY=1` adds each passing action's body check.
let only: Option<Vec<String>> = std::env::var("ACTION_MATRIX_ACTIONS").ok().map(|v| v.split(',').map(str::to_string).collect());
let validity = std::env::var_os("ACTION_MATRIX_VALIDITY").is_some();
for (i, a) in list.iter().enumerate().skip(from) {
if only.as_ref().is_some_and(|o| !o.iter().any(|x| x == a.action)) {
continue;
}
// Whole families of pieces are sampled together (see [`family`]).
let k = key(&design, &family(&a.entity), a.action);
if !stable_hash(&k).is_multiple_of(sample) {
@@ -646,12 +657,37 @@ fn action_matrix_worker() {
let mut scratch = s.scratch();
let r = perform(&mut scratch, &a.act, &before);
let mut line = outcome(&design, &a.entity, a.action, &r, t.elapsed().as_millis());
if validity && r.is_ok() {
let st = scratch.model.state();
let bad: Vec<String> = st.bodies.iter().flat_map(|b| b.body.validity().into_iter().map(|v| format!("{}: {v}", b.name))).collect();
line["validity"] = json!(bad);
}
// What was sent, for reproducing a failure (kept in the results, not the baseline).
line["command"] = match &a.act {
Act::Run(id, p) => json!({"command": id, "params": p}),
Act::Project { plane, edge } => json!({"command": "sketch.project", "params": {"refs": [edge]}, "sketch_plane": plane}),
};
emit(line);
// With ACTION_MATRIX_UNFOLD, sheet metal must still unfold and fold back after the action.
if r.is_ok() && std::env::var_os("ACTION_MATRIX_UNFOLD").is_some() {
let t = Instant::now();
let sheets: Vec<String> = scratch.model.state().sheets.iter().map(|sh| sh.body.clone()).collect();
let mut r = Ok(());
for body in &sheets {
for id in ["sheet.unfold", "sheet.refold"] {
if r.is_ok() {
r = scratch
.execute(id, &json!({"body": body}))
.map(|_| ())
.map_err(|e| (command_kind(&e).to_string(), format!("{body}: {e}")));
}
}
}
if !sheets.is_empty() {
let unfold: &'static str = Box::leak(format!("{}+unfold", a.action).into_boxed_str());
emit(outcome(&design, &a.entity, unfold, &r, t.elapsed().as_millis()));
}
}
// The edit round: a driving parameter changes; the new feature still resolves.
if r.is_ok()
&& let Some((name, expr)) = &edit
@@ -725,7 +761,9 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
.stderr(Stdio::null())
.spawn()
.unwrap();
let mut seen = 0usize;
// Until the child writes, it is loading the design (again, after a restart).
let loaded_at = std::fs::metadata(out).map(|m| m.len() as usize).unwrap_or(0);
let mut seen = loaded_at;
let mut last_change = Instant::now();
let (status, timed_out) = loop {
if let Some(status) = child.try_wait().unwrap() {
@@ -735,7 +773,7 @@ fn run_design(design: &str, out: &Path, sample: u64) -> Vec<Value> {
if n != seen {
seen = n;
last_change = Instant::now();
} else if last_change.elapsed() > ACTION_TIMEOUT {
} else if last_change.elapsed() > if seen == loaded_at { LOAD_TIMEOUT } else { ACTION_TIMEOUT } {
let _ = child.kill();
let _ = child.wait();
break (None, true);
@@ -1045,8 +1083,19 @@ fn a_through_hole_into_a_shelled_box_rim() {
);
}
/// #559: a 6 mm hole drilled at an angle through a 920 mm prism, its through-all tool nearly
/// 1.9 m long beside a thin bar it misses. The bar's cut came out empty: the tool filled too
/// little of its own box for the sampled shortcut, and the boolean's tolerances (from the
/// tool's length) were wider than the hole.
#[test]
fn an_oblique_through_hole_beside_a_thin_bar() {
reproduces(
Some("examples/regressions/oblique_cut_of_cylinder.json"),
json!([{"command": "solid.hole", "params": {"diameter": 6.0, "position": [296.66666666666663, -458.66666666666663, 848.7633333333333]}}]),
);
}
#[test]
#[ignore = "fails until #560 is fixed"]
fn a_hole_through_a_sheet_metal_base() {
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
c.extend([
@@ -1056,8 +1105,30 @@ fn a_hole_through_a_sheet_metal_base() {
reproduces(None, Value::Array(c));
}
/// `face.hole` on a rim of the sheet metal corpus (#560): drilled along the panel, the hole is a
/// bore of that panel, and the part still unfolds and folds back.
#[test]
fn a_hole_drilled_into_a_sheet_metal_rim() {
// What the matrix sends: the face's point, the hole square into it.
for (design, at, diameter) in [
// The base's end rim of the L bracket, through to the other end.
("typical/sheet-metal-bracket", [60.0, 7.777777777777779, 0.8333333333333333], 0.667),
// The end rim of a tray's wall, beside the corner.
("oracle/69-sm-tray", [99.58333333333333, 4.267766952966369, 15.0], 0.333),
] {
let Some((_, path)) = designs().into_iter().find(|(n, _)| n == design) else { panic!("no design {design}") };
let mut s = load(&path).unwrap();
let before = errors(&s);
let start = s.doc.features.len();
s.execute("solid.hole", &json!({"position": at, "diameter": diameter})).unwrap_or_else(|e| panic!("{design}: {e}"));
check(&s, start, &before).unwrap_or_else(|(kind, e)| panic!("{design}: {kind}: {e}"));
for id in ["sheet.unfold", "sheet.refold"] {
s.execute(id, &json!({})).unwrap_or_else(|e| panic!("{design} {id}: {e}"));
}
}
}
#[test]
#[ignore = "fails until #561 is fixed"]
fn sketches_on_loft_and_sweep_faces() {
let mut loft: Vec<Value> = rectangle(json!("XY"), [-20., -20.], [20., 20.]).into();
loft.extend([
@@ -1096,7 +1167,6 @@ fn press_pull_on_the_pi_case_floor() {
}
#[test]
#[ignore = "fails until #563 is fixed"]
fn sketches_after_a_draft_and_on_a_tapered_extrude() {
reproduces(
None,
@@ -1117,7 +1187,6 @@ fn sketches_after_a_draft_and_on_a_tapered_extrude() {
}
#[test]
#[ignore = "fails until #564 is fixed"]
fn a_sketch_on_the_rim_a_shell_leaves() {
let mut c: Vec<Value> = rectangle(json!("XY"), [-35., -25.], [35., 25.]).into();
c.extend([
@@ -1173,8 +1242,36 @@ fn a_shell_of_a_swept_bar() {
);
}
/// The construction a drill-tip script ends with.
struct TipConstruction(solidcraft_engine::doc::construct::ConstructGeom);
impl TipConstruction {
fn axis(&self) -> (Vec3, Vec3) {
match &self.0 {
solidcraft_engine::doc::construct::ConstructGeom::Axis { origin, dir } => (*origin, *dir),
other => panic!("expected an axis, got {other:?}"),
}
}
fn plane(&self) -> solidcraft_engine::geom::Plane {
match &self.0 {
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
other => panic!("expected a plane, got {other:?}"),
}
}
}
/// Replays the script and returns its last construction.
fn drill_tip_construction(commands: Vec<Value>) -> TipConstruction {
let mut s = Session::default();
for c in &commands {
let id = c["command"].as_str().unwrap();
s.execute(id, &c["params"]).unwrap_or_else(|e| panic!("{id}: {e}"));
}
let state = s.model.state();
TipConstruction(state.construct.last().map(|c| c.geom.clone()).expect("a construction"))
}
#[test]
#[ignore = "fails until #567 is fixed"]
fn an_axis_through_a_drill_tip() {
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
c.extend([
@@ -1182,7 +1279,30 @@ fn an_axis_through_a_drill_tip() {
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
json!({"command": "construct.axis.through_cylinder", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
]);
reproduces(None, Value::Array(c));
reproduces(None, Value::Array(c.clone()));
// Not just "no error": the axis runs along the drill, through the hole's centre line.
let (origin, dir) = drill_tip_construction(c).axis();
assert!((origin.x - 50.).abs() < 1e-6 && (origin.y - 20.).abs() < 1e-6, "{origin:?}");
assert!(dir.z.abs() > 1. - 1e-9, "{dir:?}");
}
#[test]
fn a_tangent_plane_on_a_drill_tip() {
let mut c: Vec<Value> = rectangle(json!("XY"), [0., 0.], [60., 40.]).into();
c.extend([
json!({"command": "solid.extrude", "params": {"distance": 15}}),
json!({"command": "solid.hole", "params": {"position": [50, 20, 15], "direction": [0, 0, -1], "diameter": 6, "depth": 10, "tip_angle": 118}}),
json!({"command": "construct.plane.tangent", "params": {"face": {"face": [48.44468907075447, 21.22448979591836, 4.399139380972439]}}}),
]);
reproduces(None, Value::Array(c.clone()));
// The plane touches the cone at the picked point and leans like the cone does: a 118 degree tip
// puts the wall 59 degrees off the drill axis, so its normal is 31 degrees off it.
let plane = drill_tip_construction(c).plane();
let picked = Vec3::new(48.44468907075447, 21.22448979591836, 4.399139380972439);
let distance = (picked - plane.origin).dot(plane.normal());
// The pick lies on the tessellated cone and the plane is the exact one, so allow for chord error.
assert!(distance.abs() < 0.05, "the pick is {distance} mm off the plane");
assert!((plane.normal().z.abs() - 31f64.to_radians().cos()).abs() < 1e-2, "{:?}", plane.normal());
}
#[test]
@@ -1199,3 +1319,30 @@ fn a_plane_on_a_patterned_counterbore_after_a_thickness_change() {
]);
reproduces(None, Value::Array(c));
}
/// #707: the same design builds the same body every time, in one process too. Faces were put in
/// the hash order of their addresses in a few places (truck's connected components, the planar
/// boolean's rebuild, healing's merges), so a later boolean on the ribbed lid passed or failed
/// from run to run.
#[test]
fn the_ribbed_lid_builds_the_same_body_every_time() {
let path = root().join("examples/regressions/lid_ribs_flush_with_window.json");
let fingerprint = || {
let s = load(&path).unwrap();
let st = s.model.state();
st.bodies
.iter()
.map(|b| {
let mesh = b.body.tessellate(0.1).unwrap();
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
});
format!("{} {} {bits:x}", b.name, b.body.face_count())
})
.collect::<Vec<_>>()
};
let first = fingerprint();
for _ in 0..5 {
assert_eq!(fingerprint(), first);
}
}
+1 -19
View File
@@ -137,13 +137,7 @@ fn body_names_saved_before_exact_history_keep_their_pieces() {
/// ignored test below that checks its faces; a listed plane whose face attaches fails the main
/// check until it is removed from here.
type Planes = &'static [([f64; 3], f64)];
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 5] = [
("m0-revolve-primitives", "Body1", None, 494),
("naming-split-pieces", "Body1", None, 494),
("modern-plastic", "Body1", None, 494),
("modern-sheet-metal", "Body1", None, 495),
("issue-616-lid-ribs", "Lid", None, 628),
];
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 1] = [("issue-616-lid-ribs", "Lid", None, 628)];
/// The issue a face is listed under, and whether it is listed by its plane (not as every face).
fn known_without_attachment(design: &str, body: &str, normal: [f64; 3], distance: f64) -> Option<(u32, bool)> {
@@ -262,18 +256,6 @@ fn known_faces_attach(issue: u32) {
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
#[test]
#[ignore = "fails until #494 is fixed"]
fn faces_after_revolve_offset_face_and_boss_take_an_attached_sketch() {
known_faces_attach(494);
}
#[test]
#[ignore = "fails until #495 is fixed"]
fn sheet_metal_thickness_faces_take_an_attached_sketch() {
known_faces_attach(495);
}
#[test]
#[ignore = "fails until #628 is fixed"]
fn faces_of_a_lid_with_joined_ribs_take_an_attached_sketch() {
@@ -0,0 +1,203 @@
//! A parameter change gives the same topology as building at the new value from scratch, and
//! changing it back gives the original topology: no stale splits, no faces added to regions
//! nothing touched. Over the design corpus (`tests/corpus`, `tests/action_matrix/designs`), and
//! the README demo's Raspberry Pi case pinned step by step.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
}
/// A folder of its own for test `name` (tests run side by side).
fn scratch_dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-rebuild-topology-{name}-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
/// A body's topology: its face and edge counts, and how many faces lie in each plane.
#[derive(Debug, PartialEq, Eq)]
struct Topology {
faces: usize,
edges: usize,
planes: BTreeMap<[i64; 4], usize>,
}
/// Every solid body's topology, by name.
fn topology(s: &Session) -> BTreeMap<String, Topology> {
let st = s.world_state();
st.bodies
.iter()
.filter(|b| !b.body.is_mesh())
.map(|b| {
let mut planes = BTreeMap::new();
for i in 0..b.body.face_count() {
if let Some(FaceSurface::Plane { normal, point }) = face_surface(&b.body, i) {
let key = [normal.x, normal.y, normal.z, normal.dot(point) / 100.].map(|x| (x * 1e4).round() as i64);
*planes.entry(key).or_insert(0) += 1;
}
}
(b.name.clone(), Topology { faces: b.body.face_count(), edges: b.body.edge_count(), planes })
})
.collect()
}
fn open(path: &Path) -> Session {
let mut s = Session::default();
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
s
}
/// The design built from scratch: saved as it is (timeline only, no cached geometry) and
/// opened again.
fn rebuilt(s: &Session, path: &Path) -> Session {
let mut t = s.scratch();
t.execute("file.save", &json!({"path": path.to_string_lossy(), "format": "json"})).unwrap();
open(path)
}
fn errors(s: &Session) -> Vec<u64> {
s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect()
}
/// The corpus's designs, as sessions.
fn designs() -> Vec<(String, Session)> {
let root = root();
let mut out = Vec::new();
for dir in ["apps/solidcraft-cli/tests/corpus", "tests/action_matrix/designs"] {
let mut files: Vec<PathBuf> = std::fs::read_dir(root.join(dir))
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "solidcraft" || x == "json") && !p.to_string_lossy().ends_with(".expected.json"))
.collect();
files.sort();
for p in files {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
open(&p)
} else {
let mut s = Session::default();
let script: Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap();
s.run_script(&script).unwrap_or_else(|e| panic!("{name}: {e}"));
if s.active_sketch.is_some() {
let _ = s.execute("sketch.finish", &json!({}));
}
s
};
out.push((name, s));
}
}
out
}
/// For each design and its first length parameter that edits cleanly (the user's first): the parameter
/// scaled by 1.1 gives what a fresh build at that value gives, and setting it back gives the
/// original topology.
#[test]
fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
let dir = scratch_dir("corpus");
let mut problems = Vec::new();
let mut checked = 0;
for (name, s) in designs() {
let original = topology(&s);
let before = errors(&s);
let mut params: Vec<_> = s.doc.params.iter().filter(|p| p.unit == "mm").cloned().collect();
params.sort_by_key(|p| p.model);
let mut tried = 0;
for p in params {
if tried == 1 {
break;
}
let mut t = s.scratch();
let expr = format!("({}) * 1.1", p.expr);
if t.execute("parameters.change", &json!({"name": p.name, "expression": expr})).is_err() || errors(&t) != before {
continue;
}
tried += 1;
checked += 1;
let edited = topology(&t);
let fresh = topology(&rebuilt(&t, &dir.join(format!("{name}-{}.solidcraft", p.name))));
if edited != fresh {
problems.push(format!("{name}: {} = {expr}: edited {edited:?}\n fresh {fresh:?}", p.name));
}
t.execute("parameters.change", &json!({"name": p.name, "expression": p.expr})).unwrap();
let back = topology(&t);
if back != original {
problems.push(format!("{name}: {} back to {}: {back:?}\n original {original:?}", p.name, p.expr));
}
}
}
let _ = std::fs::remove_dir_all(&dir);
assert!(checked >= 8, "only {checked} parameter edits checked");
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
/// face count after each feature, and each outer side (the four walls and the bottom) one face
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
/// wall at 24) into strips along the openings' tops and bottoms.
#[test]
fn pi_case_faces_per_step_at_both_heights() {
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
const FACES: [(usize, usize); 13] =
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
let dir = scratch_dir("pi");
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
let at = |height: &str, file: &str| {
let mut d = design.clone();
for p in d["params"].as_array_mut().unwrap() {
if p["name"] == "height" {
p["expr"] = json!(height);
}
}
let path = dir.join(file);
std::fs::write(&path, serde_json::to_vec(&d).unwrap()).unwrap();
open(&path)
};
let a = at("24 mm", "pi-24.solidcraft");
let mut b = at("24 mm", "pi-24-to-32.solidcraft");
b.execute("parameters.change", &json!({"name": "height", "expression": "32 mm"})).unwrap();
let c = at("32 mm", "pi-32.solidcraft");
let _ = std::fs::remove_dir_all(&dir);
let mut per_case = Vec::new();
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
let mut steps = Vec::new();
for (k, want) in FACES {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
// Each outer side: the faces in its plane.
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
let on = (0..body.body.face_count())
.filter(|&i| match face_surface(&body.body, i) {
Some(FaceSurface::Plane { normal, point }) => {
(normal.x - n[0]).abs() + (normal.y - n[1]).abs() + (normal.z - n[2]).abs() < 1e-6 && (normal.dot(point) - d).abs() < 1e-6
}
_ => false,
})
.count();
assert_eq!(on, 1, "{case}: after feature {k} the outer side facing {n:?} is {on} faces");
}
let bbox_top = body.mesh().bounds().max.z;
assert!((bbox_top - h).abs() < 1e-6, "{case}: height {bbox_top}");
steps.push(topology(&t));
}
per_case.push((case, steps));
}
// (b) and (c): the same topology after every feature.
let (b, c) = (&per_case[1].1, &per_case[2].1);
for (k, (x, y)) in b.iter().zip(c).enumerate() {
assert_eq!(x, y, "after feature {}: (b) differs from (c)", k + 2);
}
}
+6
View File
@@ -8,3 +8,9 @@ filehash = true
[serve]
addresses = ["127.0.0.1"]
port = 8781
# Offline use and updates: release builds get sw.js listing this build's files (xtask/src/web_offline.rs).
[[hooks]]
stage = "post_build"
command = "cargo"
command_arguments = ["xtask", "web-offline"]
+9 -8
View File
@@ -20,17 +20,18 @@
<meta name="twitter:image" content="https://bherbruck.github.io/solidcraft/og.png">
<meta name="twitter:image:alt" content="SolidCraft: open-source parametric CAD built in Rust">
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="copy-file" href="fonts/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="fonts/OFL.txt" data-target-path="fonts" />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
</head>
<body>
<canvas id="solidcraft_canvas"></canvas>
<div id="solidcraft_loading">Loading SolidCraft&hellip;</div>
<script>
// Offline use and updates (sw.js; written into release builds by `cargo xtask web-offline`).
if ("serviceWorker" in navigator) {
addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
}
</script>
</body>
</html>
+21 -8
View File
@@ -331,22 +331,34 @@ mod web {
};
if installed {
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
} else {
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
log::warn!("CJK font unavailable; showing English for this session");
}
self.font.finished(installed, &mut self.app.preferences.language);
ctx.request_repaint();
}
if self.font.tick(&mut self.app.preferences.language) {
let slot = self.font_result.clone();
let ctx = ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
self.fetch_font(ctx);
}
}
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
/// fetch the 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() {
self.fetch_font(ctx);
}
}
fn fetch_font(&self, ctx: &egui::Context) {
let slot = self.font_result.clone();
let ctx = ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
}
}
impl eframe::App for Shell {
@@ -365,6 +377,7 @@ mod web {
let ctx = ui.ctx().clone();
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
self.watch_text_for_font(&ctx);
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
// Persist the user's own language, not a session-only English fallback.
+227
View File
@@ -0,0 +1,227 @@
// SolidCraft's service worker: the web app keeps working offline and updates itself.
//
// `cargo xtask web-offline` (Trunk's post_build hook) writes the build into the line below: the
// version (workspace version + a hash of every built file) and the files to keep, each with its
// SHA-256, and the same as `build.json` next to it. Files the app only fetches when it needs
// them (the CJK font) aren't kept: they come from the network. Every download is checked
// against its SHA-256: a deployment that lands mid-download (another build's `index.html`) fails
// the download instead of mixing into the kept build. A new build changes this file, so the
// browser installs the new worker, which keeps the whole new build before taking over.
//
// A launch only ever starts a build whose every file is kept, so a half-downloaded update can't
// break it. It asks for `build.json` (waiting at most NAVIGATE_TIMEOUT_MS): offline, slow or
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
// started. When the site has no `build.json` (the app was removed), launches go to the network,
// the kept build answering when that fails. Each page then gets its files from the build it
// started (remembered in PAGES), never from another build with a file of the same name.
//
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
// an open page uses the build it started. Then unfinished downloads are dropped, and so are
// builds replaced by a newer worker (RETIRED). The origin is shared with other apps (github.io)
// and other copies of this one (the release zip works from any path): only caches named after
// this worker's path (CACHE_PREFIX…, PAGES) are ever touched.
//
// Unbuilt (VERSION "dev", as debug builds get it), the worker retires: it drops what a release
// build kept and unregisters, so `trunk serve` after `trunk serve --release --no-autoreload`
// shows the development build. Deploying this file unbuilt switches the worker off for everyone.
/* @build */ const VERSION = "dev", FILES = [];
const RETIRE = VERSION === "dev";
const PATH = new URL(self.registration.scope).pathname;
const CACHE_PREFIX = `solidcraft-app ${PATH} `;
const CACHE = CACHE_PREFIX + VERSION;
/** Which build each open page started: page id → cache name (or NETWORK). */
const PAGES = `solidcraft-app-pages ${PATH}`;
/** A page that came from the network gets its files from there too. */
const NETWORK = "network";
const NAVIGATE_TIMEOUT_MS = 4000;
const UPDATE_TIMEOUT_MS = 20000;
/** The entry that marks a cache as holding its whole build. */
const COMPLETE = "build-complete";
/** The entry that marks a build replaced by a newer worker. */
const RETIRED = "build-retired";
/** A page remembered this recently may still be loading (not yet listed among the clients). */
const PAGE_LOADING_MS = 30 * 1000;
const url = (file) => new URL(file, self.registration.scope).href;
const has = (name, entry) => caches.match(url(entry), { cacheName: name }).then(Boolean);
/** Run `fn` while using the cache `name`: it isn't deleted meanwhile. */
const using = (name, fn) => navigator.locks.request(name, { mode: "shared" }, fn);
/** Delete the cache `name` if nothing is using it and `unwanted()` (asked once no one can start to) agrees. */
const drop = (name, unwanted) =>
navigator.locks.request(name, { ifAvailable: true }, async (free) => free && (await unwanted()) && caches.delete(name));
const hex = (buffer) => Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
/**
* Keep every file of a build (an earlier partial download is resumed); resolves to its cache's
* name. The first file that fails cancels the others.
*/
function keep(version, files) {
const name = CACHE_PREFIX + version;
return using(name, async () => {
const cache = await caches.open(name);
await cache.delete(url(RETIRED)); // Back in use (a rollback).
if (await has(name, COMPLETE)) return name;
const cancel = new AbortController();
const results = await Promise.allSettled(
files.map(async ({ file, sha256 }) => {
try {
if (await cache.match(url(file))) return;
// `no-cache`: revalidate with the server, so a stale HTTP-cached page never mixes builds.
const response = await fetch(new Request(url(file), { cache: "no-cache" }), { signal: cancel.signal });
if (!response.ok) throw new Error(`${file}: HTTP ${response.status}`);
const body = await response.arrayBuffer();
if (hex(await crypto.subtle.digest("SHA-256", body)) !== sha256) throw new Error(`${file}: not this build's`);
await cache.put(url(file), new Response(body, { headers: response.headers }));
} catch (error) {
cancel.abort();
throw error;
}
}),
);
const failed = results.find((r) => r.status === "rejected");
if (failed) throw failed.reason;
await cache.put(url(COMPLETE), new Response(version));
return name;
});
}
const pageEntry = (id) => url(`~page/${encodeURIComponent(id)}`);
/** Remember which build a page started. */
async function started(id, build) {
if (!id) return;
const record = JSON.stringify({ build, at: Date.now() });
await (await caches.open(PAGES)).put(pageEntry(id), new Response(record));
}
/** The build a page started; a page started before it was remembered gets this worker's. */
async function startedBy(id) {
const response = id ? await caches.match(pageEntry(id), { cacheName: PAGES }) : null;
return response ? (await response.json()).build : CACHE;
}
/** The builds open pages use (pages closed since are forgotten). */
async function inUse() {
const open = new Set((await self.clients.matchAll({ includeUncontrolled: true })).map((c) => c.id));
const pages = await caches.open(PAGES);
const builds = new Set();
for (const request of await pages.keys()) {
const id = decodeURIComponent(request.url.split("~page/")[1] ?? "");
const record = await pages.match(request).then((r) => r?.json()).catch(() => null);
if (record && (open.has(id) || Date.now() - record.at < PAGE_LOADING_MS)) builds.add(record.build);
else await pages.delete(request);
}
return builds;
}
/** Drop unfinished downloads nothing is using, and replaced builds no open page uses. */
async function sweep() {
for (const name of await caches.keys()) {
if (!name.startsWith(CACHE_PREFIX) || name === CACHE) continue;
await drop(name, async () => !(await has(name, COMPLETE)) || ((await has(name, RETIRED)) && !(await inUse()).has(name)));
}
}
self.addEventListener("install", (event) => {
// Clean up first: a page open for days sees updates (and failed ones) without any launch.
const install = () => sweep().catch(() => {}).then(() => keep(VERSION, FILES));
event.waitUntil(RETIRE ? self.skipWaiting() : install().then(() => self.skipWaiting()));
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
if (RETIRE) {
for (const name of await caches.keys()) if (name.startsWith(CACHE_PREFIX) || name === PAGES) await caches.delete(name);
return self.registration.unregister();
}
for (const name of await caches.keys()) {
if (name.startsWith(CACHE_PREFIX) && name !== CACHE) await (await caches.open(name)).put(url(RETIRED), new Response(""));
}
await sweep();
await self.clients.claim();
})(),
);
});
/** The promise's value, or null when it fails or takes longer than `ms`. */
function within(ms, promise) {
return Promise.race([promise.catch(() => null), new Promise((resolve) => setTimeout(resolve, ms, null))]);
}
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
const GONE = "gone";
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
async function deployed() {
const response = await fetch(url("build.json"), { cache: "no-cache" });
if (response.status === 404 || response.status === 410) return GONE;
const build = response.ok ? await response.json() : null;
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
return valid && build.files.every((f) => typeof f.file === "string" && typeof f.sha256 === "string") ? build : null;
}
/** Keep the worker alive for `promise` (allowed while the launch is being answered). */
function extend(event, promise) {
try {
event.waitUntil(promise.catch(() => {}));
} catch {
// The event has ended; the work goes on while the worker runs.
}
}
/** A launch: the start page of a complete build (whatever query the URL has: `?sample`), and that build. */
async function launch(event) {
const kept = await caches.match(url("index.html"), { cacheName: CACHE });
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
extend(event, sweep());
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
if (build === GONE) {
const page = await fetch(event.request).catch(() => null);
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
}
if (!build || build.version === VERSION) return [kept, CACHE];
const download = keep(build.version, build.files);
extend(event, download);
const name = await within(UPDATE_TIMEOUT_MS, download);
const page = name && (await caches.match(url("index.html"), { cacheName: name }));
return page ? [page, name] : [kept, CACHE];
}
/**
* A launch that fails unexpectedly still starts the kept build (or asks the network). A newer
* worker only takes over once the launch has ended, by when its page is remembered (in use).
*/
async function launchOrKept(event) {
let page, build;
try {
[page, build] = await launch(event);
} catch {
page = await caches.match(url("index.html"), { cacheName: CACHE }).catch(() => null);
build = page ? CACHE : NETWORK;
page = page || (await fetch(event.request));
}
await started(event.resultingClientId, build).catch(() => {});
return page;
}
/** A page's files come from the build it started, else the network. */
async function asset(event) {
const build = await startedBy(event.clientId).catch(() => CACHE);
const kept = build === NETWORK ? null : await caches.match(event.request, { cacheName: build, ignoreSearch: true });
return kept || fetch(event.request);
}
self.addEventListener("fetch", (event) => {
const request = event.request;
if (RETIRE || request.method !== "GET" || new URL(request.url).origin !== self.location.origin) return;
const path = request.url.split(/[?#]/)[0];
const start = request.mode === "navigate" && (path === url("") || path === url("index.html"));
event.respondWith(start ? launchOrKept(event) : asset(event));
});
+1
View File
@@ -18,6 +18,7 @@ egui = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
rfd = { workspace = true }
getrandom = "0.3"
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
quick-xml = { workspace = true }
+5 -49
View File
@@ -1,61 +1,17 @@
#![forbid(unsafe_code)]
#[path = "../app_icon.rs"]
mod app_icon;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=../app_icon.rs");
let icon = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons/app/windows/exports/solidcraft-executable.ico");
println!("cargo:rerun-if-changed={}", icon.display());
println!("cargo:rerun-if-changed=build.rs");
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
return Ok(());
}
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or("missing OUT_DIR")?);
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
let mut images = Vec::new();
for size in sizes {
let rgba = app_icon::rgba(size)?;
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
let mask_stride = size.div_ceil(32) * 4;
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes());
dib.extend_from_slice(&(size as i32).to_le_bytes());
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&32u16.to_le_bytes());
dib.extend_from_slice(&[0; 24]);
for row in rgba.chunks_exact(size * 4).rev() {
for p in row.as_chunks::<4>().0.iter() {
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
for row in rgba.chunks_exact(size * 4).rev() {
let mut mask = vec![0u8; mask_stride];
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
if p[3] == 0 {
mask[x / 8] |= 0x80 >> (x % 8);
}
}
dib.extend_from_slice(&mask);
}
images.push((size, dib));
}
let mut ico = vec![0, 0, 1, 0];
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (size, data) in &images {
let dim = if *size == 256 { 0 } else { *size as u8 };
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
ico.extend_from_slice(&(offset as u32).to_le_bytes());
offset += data.len();
}
for (_, data) in images {
ico.extend_from_slice(&data);
}
let icon = out.join("solidcraft.ico");
std::fs::write(&icon, ico)?;
let copy = out.join("solidcraft.ico");
std::fs::copy(icon, &copy)?;
let rc = out.join("solidcraft.rc");
std::fs::write(&rc, format!("1 ICON \"{}\"\n", icon.display().to_string().replace('\\', "/")))?;
// Missing resource tools are a build error, rather than silently shipping a generic icon.
std::fs::write(&rc, format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/")))?;
embed_resource::compile_for(&rc, ["solidcraft"], embed_resource::NONE).manifest_required()?;
Ok(())
}
+133 -6
View File
@@ -9,18 +9,39 @@ use std::time::Duration;
use serde_json::{Value, json};
use solidcraft_engine::MAX_LINE;
use solidcraft_engine::control_auth;
use solidcraft_ui_egui::ControlRequest;
/// Connections served at once. The channel has no authentication beyond being bound to
/// loopback, so a flood must not be able to spawn threads without end.
/// Connections served at once, so a flood must not be able to spawn threads without end.
const MAX_CONNECTIONS: usize = 16;
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
const MAX_BAD_LINES: usize = 8;
static OPEN: AtomicUsize = AtomicUsize::new(0);
/// The token file this launch wrote, removed when the app ends.
static TOKEN_FILE: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
/// A new random token: 32 bytes from the operating system, as hex.
fn new_token() -> Option<String> {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes).ok()?;
Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
/// Remove this launch's token file (at exit).
pub fn remove_token_file() {
if let Some(p) = TOKEN_FILE.get() {
let _ = std::fs::remove_file(p);
}
}
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>();
// Every connection must show the token before anything runs (docs/control-protocol.md).
let Some(token) = control_auth::env_token().or_else(new_token) else {
eprintln!("solidcraft: control server not started: no random token available");
return rx;
};
let listener = match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => l,
Err(e) => {
@@ -28,7 +49,23 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
return rx;
}
};
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}");
match control_auth::token_path(port) {
Some(path) => match control_auth::write_token_file(&path, &token) {
Ok(()) => {
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
let _ = TOKEN_FILE.set(path);
}
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 => {
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);
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
@@ -39,8 +76,9 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
}
let tx = tx.clone();
let ctx = ctx.clone();
let token = token.clone();
std::thread::spawn(move || {
serve(stream, tx, ctx);
serve(stream, &token, tx, ctx);
OPEN.fetch_sub(1, Ordering::Relaxed);
});
}
@@ -80,11 +118,28 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
}
}
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
}
/// Refuse the connection: the reply, then the end of what we send, then what the client had
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
use std::io::Read;
let _ = writeln!(out, "{}", unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context) {
let Ok(read) = stream.try_clone() else { return };
let mut out = stream;
let mut reader = BufReader::new(read);
let mut bad = 0usize;
let mut authenticated = false;
loop {
let line = match read_line(&mut reader, MAX_LINE) {
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
@@ -99,20 +154,36 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
}
let reply = match serde_json::from_str::<Value>(&line) {
Ok(msg) => {
// The first request must carry the token; later ones on the same connection may.
if !authenticated {
match msg.get("token").and_then(Value::as_str) {
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
_ => {
refuse(&mut out, &mut reader);
break;
}
}
}
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
let params = msg.get("params").cloned().unwrap_or(json!({}));
// A batch replaces many short requests: the whole of it may take long (#743).
let wait = Duration::from_secs(if method == "engine.batch" { 3600 } else { 60 });
let (req, rrx) = ControlRequest::new(method, params);
if tx.send(req).is_err() {
break;
}
ctx.request_repaint();
let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
let mut r = rrx.recv_timeout(wait).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
if let Some(o) = r.as_object_mut() {
o.insert("id".into(), id);
}
r
}
Err(_) if !authenticated => {
refuse(&mut out, &mut reader);
break;
}
Err(e) => {
bad += 1;
json!({"ok": false, "error": format!("bad JSON: {e}")})
@@ -131,6 +202,62 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
mod tests {
use super::*;
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
/// need not be repeated on that connection.
#[test]
fn requests_run_only_after_the_token() {
use std::io::Read;
let token = "0123456789abcdef0123456789abcdef";
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
let ran = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let seen = ran.clone();
std::thread::spawn(move || {
for req in rx {
seen.lock().unwrap().push(req.method.clone());
let _ = req.reply.send(json!({"ok": true, "result": null}));
}
});
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
std::thread::spawn(move || serve(stream, token, tx, egui::Context::default()));
}
});
let session = |lines: &[String]| {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
for l in lines {
let _ = writeln!(s, "{l}");
}
let _ = s.shutdown(std::net::Shutdown::Write);
let mut text = String::new();
let _ = s.read_to_string(&mut text);
text.lines().map(|l| serde_json::from_str::<Value>(l).unwrap()).collect::<Vec<_>>()
};
for first in [
json!({"id": 1, "method": "app.quit", "params": {"force": true}}).to_string(),
json!({"id": 1, "method": "app.quit", "token": "0123456789abcdef0123456789abcdee"}).to_string(),
json!({"id": 1, "method": "app.quit", "token": "0123"}).to_string(),
json!({"id": 1, "method": "app.quit", "token": 7}).to_string(),
"not json".to_string(),
] {
let replies = session(&[first.clone(), json!({"id": 2, "method": "app.quit", "token": token}).to_string()]);
assert_eq!(replies.len(), 1, "{first}: closed after refusing: {replies:?}");
assert!(replies[0]["error"].as_str().is_some_and(|e| e.starts_with("unauthorized")), "{first}: {replies:?}");
}
assert!(ran.lock().unwrap().is_empty(), "nothing ran without the token");
let replies = session(&[
json!({"id": 1, "method": "ui.inspect", "token": token}).to_string(),
json!({"id": 2, "method": "engine.commands"}).to_string(),
]);
assert_eq!(replies.len(), 2, "{replies:?}");
assert!(replies.iter().all(|r| r["ok"] == true), "{replies:?}");
assert_eq!(*ran.lock().unwrap(), ["ui.inspect", "engine.commands"]);
}
/// A request longer than the cap is refused instead of allocated.
#[test]
fn lines_past_the_cap_are_refused() {
+5 -3
View File
@@ -13,7 +13,7 @@
#![forbid(unsafe_code)]
#[cfg(not(target_os = "macos"))]
#[path = "../../app_icon.rs"]
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
mod control_server;
#[cfg(any(target_os = "windows", target_os = "linux"))]
@@ -286,7 +286,7 @@ fn main() -> eframe::Result {
b.with_x11();
}));
}
eframe::run_native(
let result = eframe::run_native(
"SolidCraft",
options,
Box::new(move |cc| {
@@ -344,7 +344,9 @@ fn main() -> eframe::Result {
space_mouse,
}))
}),
)
);
control_server::remove_token_file();
result
}
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
+92 -35
View File
@@ -1,41 +1,98 @@
# SolidCraft macOS icon
# Application resources
`SolidCraft.icon/` is the approved Icon Composer source, containing separate
grid and solid SVG layers and the owner's appearance settings. The saturated
blue solid has transparent geometry cutouts instead of painted edge lines.
The solid and grid are both scaled to 90%, with separate Default and Dark grid artwork.
Icon Composer supplies the system backdrop and Mono material effects.
Artwork is original and attributed in `ATTRIBUTION.md`.
`assets/` is the design resource home for SolidCraft.
[ATTRIBUTION.md](../ATTRIBUTION.md) is the file inventory and provenance record.
Keep one editable master per resource and place shipped derivatives in `exports/`.
`Assets.car` is compiled by Apple's `actool` and supplies the layered icon on
macOS 26 and later. It is checked in so the existing release workflow can still
package on older Xcode runners. After editing and saving `SolidCraft.icon`,
regenerate the catalog with Xcode 26 or later from the repository root:
```sh
mkdir -p target/macos-icon
xcrun actool assets/SolidCraft.icon \
--compile target/macos-icon --platform macosx \
--minimum-deployment-target 11.0 --app-icon SolidCraft \
--output-partial-info-plist target/macos-icon/icon-info.plist \
--output-format human-readable-text
cp target/macos-icon/Assets.car assets/Assets.car
cp target/macos-icon/SolidCraft.icns assets/icon.icns
```text
assets/
icons/
app/
shared/source/ # original Rust mark and executable ICO encoder
macos/source/SolidCraft.icon/ # approved Composer project and linked SVGs
macos/exports/ # paired Assets.car and icon.icns
windows/exports/ # executable DIB ICO and installer PNG ICO
linux/exports/hicolor/ # Linux/FreeBSD package images, 16–512 px
ui/source/ # original drawing library and other UI glyphs
branding/web/source/og.png # shared social preview card
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/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
locales/{en,de,ja,ko}.json # shared offline catalogs
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
archive/icons/macos-v1/
source/icon.vectorcraft # earlier editable work, retained
exports/ # all historical SVG, PNG and iconset exports
```
`icon.icns` is the older-macOS fallback generated by the same Apple compiler
from the approved composition, retaining its layer sizes and positions.
Regenerate both compiled resources together with the commands above.
`icon.vectorcraft`, `icon.svg`, `icon.png` and `icon.iconset/` retain the earlier
VectorCraft artwork; packaging uses the Icon Composer compilation instead.
## Editing and regeneration
Only `packaging/macos/package.sh` consumes these resources. Its existing signed
app/DMG workflow copies the catalog and ICNS into `Contents/Resources`, and
`Info.plist` references `SolidCraft` through `CFBundleIconName` and
`CFBundleIconFile`. The macOS viewport opts out of eframe's runtime bitmap so
Finder, the Dock and the application switcher use the bundle icon. Other
platforms retain the existing code-drawn icon and packaging.
[UI artwork](icons/ui/README.md) identifies the drawing masters and their consumers.
Edit geometry and IDs together in the original Rust source and review changes in
both application themes. Palettes and model presets are also editable Rust data.
Design references: [Apple app-icon HIG](https://developer.apple.com/design/human-interface-guidelines/app-icons)
and [Icon Composer documentation](https://developer.apple.com/documentation/xcode/creating-your-app-icon-using-icon-composer).
No Apple artwork is shipped.
The current macOS master is `icons/app/macos/source/SolidCraft.icon/`, including
its linked SVG layers and appearance settings. The earlier VectorCraft document
is preserved in the archive. No other native design projects were available for
the current UI set or preview card. Keep any future `.vectorcraft`, `.ai` or other
native work and linked inputs under that resource's `source/work/`, with an
attribution row; retain the editable document alongside its exports.
Regenerate all exports of the shared Windows/Linux/FreeBSD mark together:
```sh
cargo xtask app-icons
```
For macOS, use Xcode with Icon Composer support:
```sh
mkdir -p target/resources/macos-icon
xcrun actool assets/icons/app/macos/source/SolidCraft.icon \
--compile target/resources/macos-icon --platform macosx \
--minimum-deployment-target 11.0 --app-icon SolidCraft \
--output-partial-info-plist target/resources/macos-icon/icon-info.plist \
--output-format human-readable-text
cp target/resources/macos-icon/Assets.car assets/icons/app/macos/exports/Assets.car
cp target/resources/macos-icon/SolidCraft.icns assets/icons/app/macos/exports/icon.icns
```
Review and commit source changes and required exports together. Temporary renders,
tool caches and package staging belong under ignored `target/` or `dist/`.
Normal builds use the committed exports and require no design applications.
The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
## Platform bindings
| Platform | Resources and output paths |
| --- | --- |
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
| Web | HTML/Trunk stays in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/` and uses the collected CSS; no favicon/PWA icon currently exists |
| 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` and `Tokens` at L5, model presets in L2 `doc`, and sketch
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.
`cargo xtask assets` requires an exact attribution row for every collected file
except Markdown documentation, and continues checking file assets elsewhere in
the repository. This includes native work, Rust masters, catalogs, shaders and
platform templates. Packaging selects explicit current files and fails when
required inputs are absent. Archived artwork has no production consumers.
Dependency-owned egui fonts and vendored renderer shaders stay with their
libraries; their licences remain in `THIRD-PARTY-LICENSES.txt` and `ATTRIBUTION.md`.
System fonts, optional packs, user fonts/canvases/decals/materials and native OS
controls retain external ownership. Root licence/notice texts stay canonical.
+23
View File
@@ -0,0 +1,23 @@
//! Built-in model appearances: names, sRGB colours and metalness.
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
("Aluminum - Satin", [204, 208, 214], 1.0),
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
("Steel - Satin", [158, 164, 172], 1.0),
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
("Cast Iron", [104, 104, 110], 1.0),
("Brass - Polished", [208, 172, 88], 1.0),
("Copper", [204, 122, 80], 1.0),
("Titanium", [150, 148, 158], 1.0),
("ABS - White", [236, 236, 230], 1.0),
("ABS - Black", [36, 36, 38], 1.0),
("ABS - Red", [196, 40, 40], 1.0),
("ABS - Blue", [40, 90, 186], 1.0),
("ABS - Yellow", [236, 196, 40], 1.0),
("ABS - Green", [52, 150, 80], 1.0),
("Rubber - Black", [28, 28, 28], 1.0),
("Glass - Clear", [190, 220, 228], 0.25),
("Glass - Smoked", [90, 96, 104], 0.45),
("Wood - Oak", [190, 146, 92], 1.0),
("Paint - Gloss White", [246, 246, 244], 1.0),
];
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,48 @@
//! Original executable ICO encoder: DIB images and transparency masks.
use super::app_icon;
pub fn encode() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
let mut images = Vec::new();
for size in sizes {
let rgba = app_icon::rgba(size)?;
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
let mask_stride = size.div_ceil(32) * 4;
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes());
dib.extend_from_slice(&(size as i32).to_le_bytes());
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&32u16.to_le_bytes());
dib.extend_from_slice(&[0; 24]);
for row in rgba.chunks_exact(size * 4).rev() {
for p in row.as_chunks::<4>().0.iter() {
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
for row in rgba.chunks_exact(size * 4).rev() {
let mut mask = vec![0u8; mask_stride];
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
if p[3] == 0 {
mask[x / 8] |= 0x80 >> (x % 8);
}
}
dib.extend_from_slice(&mask);
}
images.push((size, dib));
}
let mut ico = vec![0, 0, 1, 0];
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (size, data) in &images {
let dim = if *size == 256 { 0 } else { *size as u8 };
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
ico.extend_from_slice(&(offset as u32).to_le_bytes());
offset += data.len();
}
for (_, data) in images {
ico.extend_from_slice(&data);
}
Ok(ico)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+22
View File
@@ -0,0 +1,22 @@
# UI artwork
Edit the Rust drawing masters in `source/`; the UI crate compiles them through
`crates/ui-egui/src/icons.rs`. Use the 24 × 24 grid and semantic ink/fill/accent
colours. Icon IDs and their geometry live together in `source/icons.rs`;
unknown IDs retain the existing fallback. Add new artwork there without
maintaining a separate ID registry.
| Source | Artwork | Consumers |
| --- | --- | --- |
| `icons.rs` | Toolbar, browser, menu and constraint icons | `icons::paint` |
| `glyphs.rs` | Brand mark, captions, transport, badges, pin, caret, disclosure, radial centre and removal cross | toolbar, titlebar, timeline, browser, menus |
| `navigation.rs` | Signed axes, cube faces/labels and arrows | viewport/navigation cube |
| `markers.rs` | Constraint mappings/letters, snap markers and dimension arrowheads | sketch, assembly and dimensions |
| `pointer.rs` | Agent pointer and tag | agent cursor |
Widget state, layout, projection, picking and commands stay with their widgets.
Review drawing changes in the actual application in both light and dark themes.
No separate native design projects were available for this set. Preserve any
future native work and linked inputs under `source/work/`, with attribution.
Original artwork is MIT OR Apache-2.0; caption geometry retains VectorCraft
attribution in the root `ATTRIBUTION.md`.
+158
View File
@@ -0,0 +1,158 @@
//! Collected glyph artwork. Caption geometry adapted from VectorCraft (MIT OR Apache-2.0).
use crate::titlebar::Caption;
use egui::{Color32, Pos2, Rect, Stroke, Ui, pos2, vec2};
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
let tri = |dir: f32, dx: f32| {
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
};
let bar = |dx: f32| p.line_segment([pos2(c.x + dx, c.y - 5.0), pos2(c.x + dx, c.y + 5.0)], Stroke::new(1.6, col));
match what {
"first" => {
bar(-5.0);
tri(-1.0, 1.0);
}
"back" => tri(-1.0, 0.0),
"forward" => tri(1.0, 0.0),
_ => {
tri(1.0, -1.0);
bar(5.0);
}
}
}
pub(crate) fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
let p = ui.painter();
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
match c {
Caption::Minimize => {
p.line_segment([g.left_center(), g.right_center()], s);
}
// Restore: a front square with the corner of the one behind it.
Caption::Maximize if maximized => {
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
}
Caption::Maximize => {
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
}
Caption::Close => {
p.line_segment([g.left_top(), g.right_bottom()], s);
p.line_segment([g.right_top(), g.left_bottom()], s);
}
}
}
pub(crate) fn badge(p: &egui::Painter, r: Rect, name: &str, c: Color32) {
let s = Stroke::new(1.3, c);
match name {
"lock" => {
let body = Rect::from_min_max(pos2(r.left() + 1.5, r.center().y - 0.5), pos2(r.right() - 1.5, r.bottom() - 0.5));
p.rect_filled(body, 1.5, c);
let top = pos2(r.center().x, r.top() + 4.0);
p.add(egui::Shape::line(
(0..=8)
.map(|k| {
let a = std::f32::consts::PI * (k as f32 / 8.0);
pos2(top.x - 3.2 * a.cos(), top.y - 3.2 * a.sin() + 1.0)
})
.chain([pos2(top.x + 3.2, body.top())])
.collect(),
s,
));
p.line_segment([pos2(top.x - 3.2, top.y + 1.0), pos2(top.x - 3.2, body.top())], s);
}
"pin" => {
let head = pos2(r.center().x + 2.0, r.top() + 4.0);
p.circle_filled(head, 3.2, c);
p.line_segment([head, pos2(r.left() + 1.5, r.bottom() - 1.0)], Stroke::new(1.6, c));
}
_ => {}
}
}
pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
let c = r.center();
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
// Local coordinates: the pin points down, its head on top.
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
let stroke = Stroke::new(1.4, col);
if on {
p.add(egui::Shape::convex_polygon(head, col, stroke));
} else {
p.add(egui::Shape::closed_line(head, stroke));
}
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
}
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
let s = 20.0 / 412.217;
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
let face = |col: Color32, pts: &[(f32, f32)]| {
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
};
let (light, mid, dark) = (Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197));
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]);
face(mid, &[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)]);
face(mid, &[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)]);
face(dark, &[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)]);
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
// White edge lines.
let edge = Stroke::new(10.0 * s, Color32::WHITE);
let line = |pts: &[(f32, f32)]| {
p.add(egui::Shape::line(m(pts), edge));
};
line(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)]);
line(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)]);
line(&[(177.0, 206.1), (263.8, 156.0)]);
p.add(egui::Shape::closed_line(
m(&[
(178.5, 5.8),
(5.0, 105.9),
(5.0, 206.1),
(91.7, 256.2),
(91.7, 356.4),
(178.5, 406.4),
(352.0, 306.3),
(352.0, 206.1),
(265.2, 156.0),
(265.2, 55.9),
]),
edge,
));
}
pub(crate) fn cross(p: &egui::Painter, r: Rect, s: Stroke) {
p.line_segment([r.left_top(), r.right_bottom()], s);
p.line_segment([r.right_top(), r.left_bottom()], s);
}
pub(crate) fn radial_centre(p: &egui::Painter, c: Pos2, direction: Option<(egui::Vec2, bool)>, t: crate::theme::Tokens) {
p.circle(c, 15.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, t.border));
if let Some((d, enabled)) = direction {
p.line_segment([c + d * 6.0, c + d * 15.0], Stroke::new(3.0, if enabled { t.accent } else { t.border }));
} else {
p.circle_filled(c, 3.0, t.text_dim);
}
}
pub(crate) fn disclosure(p: &egui::Painter, ar: Rect, open: bool, ink: Color32) {
let pts = if open {
vec![pos2(ar.left() + 2.0, ar.top() + 4.0), pos2(ar.right() - 2.0, ar.top() + 4.0), pos2(ar.center().x, ar.bottom() - 3.0)]
} else {
vec![pos2(ar.left() + 4.0, ar.top() + 2.0), pos2(ar.right() - 3.0, ar.center().y), pos2(ar.left() + 4.0, ar.bottom() - 2.0)]
};
p.add(egui::Shape::convex_polygon(pts, ink, Stroke::NONE));
}
File diff suppressed because it is too large. Load diff
+86
View File
@@ -0,0 +1,86 @@
//! Constraint, assembly snap and dimension marker artwork.
use crate::dialogs_assembly::SnapKind;
use crate::theme::Tokens;
use egui::{Color32, Pos2, Stroke, vec2};
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
let halo = Stroke::new(4.5, Color32::from_black_alpha(150));
let white = Stroke::new(1.5, Color32::WHITE);
if let Some(tip) = tip {
let d = (tip - c).normalized();
let n = vec2(-d.y, d.x);
let head = vec![tip + d * 2.0, tip - d * 9.0 + n * 5.0, tip - d * 9.0 - n * 5.0];
painter.line_segment([c, tip], halo);
painter.add(egui::Shape::convex_polygon(head.clone(), Color32::TRANSPARENT, halo));
painter.line_segment([c, tip], Stroke::new(2.0, col));
painter.add(egui::Shape::convex_polygon(head, col, Stroke::NONE));
}
let shape = |fill: Color32, stroke: Stroke, grow: f32| -> egui::Shape {
match kind {
SnapKind::FaceCenter => egui::Shape::rect_filled(egui::Rect::from_center_size(c, vec2(12.0 + grow, 12.0 + grow)), 1.5, fill),
SnapKind::CircleCenter => egui::Shape::circle_filled(c, 7.0 + grow / 2.0, fill),
SnapKind::EdgeMid => {
let r = 8.0 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r * 0.93, r * 0.7), c + vec2(-r * 0.93, r * 0.7)], fill, stroke)
}
SnapKind::Vertex => {
let r = 7.5 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)], fill, stroke)
}
}
};
painter.add(shape(Color32::from_black_alpha(150), Stroke::NONE, 4.0));
painter.add(shape(col, Stroke::NONE, 0.0));
match kind {
SnapKind::FaceCenter => {
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(12.0, 12.0)), 1.5, white, egui::StrokeKind::Middle);
}
SnapKind::CircleCenter => {
painter.circle_stroke(c, 7.0, white);
painter.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], Stroke::new(1.2, Color32::WHITE));
painter.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], Stroke::new(1.2, Color32::WHITE));
}
SnapKind::EdgeMid | SnapKind::Vertex => {
painter.add(shape(Color32::TRANSPARENT, white, 0.0));
}
}
// What it snaps to (hovered), or which component it is (picked): a small label.
if let Some(tag) = tag {
let t = Tokens::get();
let font = egui::FontId::proportional(11.5);
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
let at = c + vec2(12.0, 10.0);
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
painter.rect(r, 3.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, col), egui::StrokeKind::Inside);
painter.galley(at, g, t.text);
}
}
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
Some(match name {
"Coincident" => "c_coincident",
"Parallel" => "c_parallel",
"Perpendicular" => "c_perpendicular",
"Collinear" => "c_collinear",
"Tangent" => "c_tangent",
"Smooth" => "c_smooth",
"Equal" => "c_equal",
"Concentric" => "c_concentric",
"MidPoint" => "c_midpoint",
"Symmetry" => "c_symmetry",
"Fix" => "c_fix",
_ => return None,
})
}
pub(crate) fn glyph_letter(name: &str) -> &'static str {
match name {
"Horizontal" => "H",
"Vertical" => "V",
_ => "?",
}
}
pub(crate) fn dimension_arrow(painter: &egui::Painter, t: Pos2, back: Pos2, col: Color32) {
let d = (t - back).normalized();
let n = vec2(-d.y, d.x);
painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE));
}
+167
View File
@@ -0,0 +1,167 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Align2, Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcraft_engine::geom::Vec3;
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Vec<Pos2>,
pub(crate) outer_edges: Vec<bool>,
pub(crate) centre: Pos2,
pub(crate) label: String,
pub(crate) text_axes: Option<(egui::Vec2, egui::Vec2)>,
pub(crate) depth: f64,
}
// Private marker for label meshes with atlas-pixel UVs. The output hook replaces
// it with the font texture before any renderer sees the frame.
pub(crate) const LABEL_PIXELS: egui::TextureId = egui::TextureId::User(u64::from_le_bytes(*b"cubetext"));
struct LabelAtlas;
impl egui::plugin::Plugin for LabelAtlas {
fn debug_name(&self) -> &'static str {
"navigation cube label atlas"
}
fn output_hook(&mut self, ctx: &egui::Context, output: &mut egui::FullOutput) {
let size = ctx.fonts(|f| f.font_image_size());
let normalize = vec2(1.0 / size[0].max(1) as f32, 1.0 / size[1].max(1) as f32);
for shape in &mut output.shapes {
if let Shape::Mesh(mesh) = &mut shape.shape
&& mesh.texture_id == LABEL_PIXELS
{
let mesh = std::sync::Arc::make_mut(mesh);
mesh.texture_id = egui::TextureId::default();
for v in &mut mesh.vertices {
v.uv = (v.uv.to_vec2() * normalize).to_pos2();
}
}
}
}
}
/// Warp the font mesh into the projected face, so labels turn with the model.
pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, font_size: f32) {
let Some((r, down)) = face.text_axes else { return };
let opacity = ((face.depth - 0.18) / 0.17).clamp(0.0, 1.0) as f32;
if opacity <= 0.0 {
return;
}
let ink = ink.gamma_multiply(opacity);
let galley = painter.layout_no_wrap(face.label.clone(), FontId::proportional(font_size), ink);
let centre = galley.rect.center();
// Keep atlas-pixel UVs until the frame is complete: another label or widget
// can grow the atlas after this mesh is created, invalidating early UVs.
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 { 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;
for v in &mut mesh.vertices {
v.uv = pos2(v.uv.x * atlas[0] as f32, v.uv.y * atlas[1] as f32);
let local = v.pos - centre;
v.pos = face.centre + r * local.x + down * (local.y * 1.35);
}
painter.add(Shape::mesh(mesh));
}
}
}
pub(crate) fn paint_cube(painter: &egui::Painter, faces: &[Facet], hovered: Option<usize>, t: Tokens, labels: bool, scale: f32) {
// A shared, opaque triangle mesh has no antialiasing fringes between facets.
// Per-polygon fringes and miter joins overlap as a face becomes edge-on.
let mut fill_mesh = egui::epaint::Mesh::default();
let mut edges: Vec<(Pos2, Pos2, usize, bool)> = Vec::new();
let hovered_direction = hovered.and_then(|i| faces.get(i)).map(|f| f.direction);
for face in faces {
let base = t.cube_face;
let shade = (0.72 + 0.28 * face.depth.min(1.0)) as f32;
let highlighted = hovered_direction == Some(face.direction);
let fill = if highlighted {
t.cube_hover
} else {
Color32::from_rgb((f32::from(base.r()) * shade) as u8, (f32::from(base.g()) * shade) as u8, (f32::from(base.b()) * shade) as u8)
};
let centre = fill_mesh.vertices.len() as u32;
fill_mesh.colored_vertex(face.centre, fill);
for &p in &face.points {
fill_mesh.colored_vertex(p, fill);
}
for (j, (&a, &b)) in face.points.iter().zip(face.points.iter().cycle().skip(1)).take(face.points.len()).enumerate() {
fill_mesh.add_triangle(centre, centre + 1 + j as u32, centre + 1 + ((j + 1) % face.points.len()) as u32);
// Coplanar picking regions have no visible seams. Only actual cube
// edges and the hovered target's boundary receive an outline.
let outer = face.outer_edges.get(j).copied().unwrap_or(false);
if !highlighted && !outer {
continue;
}
if let Some(edge) = edges
.iter_mut()
.find(|(p, q, _, _)| (p.distance_sq(a) < 1e-6 && q.distance_sq(b) < 1e-6) || (p.distance_sq(b) < 1e-6 && q.distance_sq(a) < 1e-6))
{
edge.2 += usize::from(highlighted);
edge.3 |= outer;
} else {
edges.push((a, b, usize::from(highlighted), outer));
}
}
}
painter.add(Shape::mesh(fill_mesh));
for (a, b, highlights, outer) in edges {
// Two highlighted coplanar patches share an internal subdivision, not
// a target boundary. Actual cube edges remain visible across faces.
if !outer && highlights != 1 {
continue;
}
let highlighted = highlights > 0;
painter.line_segment([a, b], Stroke::new(1.0, if highlighted { t.accent } else { t.cube_border }));
}
if labels {
for face in faces {
face_text(painter, face, t.cube_text, 14.0 * scale);
}
}
}
pub(crate) fn arrow(painter: &egui::Painter, at: Pos2, direction: egui::Vec2, hover: bool, t: Tokens) {
let side = vec2(-direction.y, direction.x);
painter.add(Shape::convex_polygon(
vec![at + direction * 7.0, at - direction * 5.0 + side * 7.0, at - direction * 5.0 - side * 7.0],
if hover { t.accent_soft } else { t.cube_face },
Stroke::new(1.0, if hover { t.accent } else { t.cube_border }),
));
}
pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, hover: bool, t: Tokens, scale: f32) {
let (start, end) =
if clockwise { (-std::f32::consts::FRAC_PI_4 + 0.12, 0.0) } else { (-std::f32::consts::FRAC_PI_4 - 0.12, -std::f32::consts::FRAC_PI_2) };
let points: Vec<_> = (0..=16)
.map(|i| {
let angle = egui::lerp(start..=end, f32::from(i as u8) / 16.0);
at + vec2(angle.cos(), angle.sin()) * (32.0 * scale)
})
.collect();
painter.add(Shape::line(points.clone(), Stroke::new(5.0 * scale, if hover { t.accent } else { t.cube_border })));
painter.add(Shape::line(points.clone(), Stroke::new(3.0 * scale, if hover { t.accent_soft } else { t.cube_face })));
if let Some(tip) = points.last() {
let sign = if clockwise { 1.0 } else { -1.0 };
arrow(painter, *tip, vec2(-end.sin(), end.cos()) * (sign * scale), hover, t);
}
}
pub(crate) const AXIS_RADIUS: f32 = 11.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
let col = if d.x != 0.0 {
solidcraft_engine::view::colors::AXIS_X
} else if d.y != 0.0 {
solidcraft_engine::view::colors::AXIS_Y
} else {
solidcraft_engine::view::colors::AXIS_Z
};
let col = Color32::from_rgb(col.0, col.1, col.2);
let positive = !name.starts_with('-');
let ink = if depth < -0.01 { col.gamma_multiply(0.55) } else { col };
painter.line_segment([c, at], Stroke::new(if positive { 2.0 } else { 1.0 }, ink));
painter.circle_filled(at, AXIS_RADIUS, if hovered { t.accent_soft } else { t.overlay });
painter.circle_stroke(at, AXIS_RADIUS, Stroke::new(if hovered { 2.0 } else { 1.0 }, if hovered { t.accent } else { ink }));
painter.text(at, Align2::CENTER_CENTER, name.to_uppercase(), FontId::proportional(11.0), ink);
}
+14
View File
@@ -0,0 +1,14 @@
//! Original agent pointer and label. Animation stays with the UI controller.
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
pub(crate) const AGENT: Color32 = Color32::from_rgb(168, 85, 247);
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
// An arrow pointer, tip at `p`.
let pts: Vec<Pos2> =
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
painter.add(Shape::convex_polygon(pts.clone(), AGENT, Stroke::NONE));
painter.add(Shape::closed_line(pts, Stroke::new(1.5, Color32::WHITE)));
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(11.0), Color32::WHITE);
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
painter.rect_filled(r, 4.0, AGENT);
painter.galley(r.min + vec2(5.0, 2.0), label, Color32::WHITE);
}
+701
View File
@@ -0,0 +1,701 @@
{
"browser.body_name": "Körper {number}",
"browser.component_name": "Komponente {number}",
"browser.parameters": "Parameter ({count})",
"browser.precision": "Genauigkeit: {precision}",
"browser.sketch_name": "Skizze {number}",
"browser.units": "Einheiten: {units}",
"dialog.custom_axis": "↖ Eigene Achse",
"dialog.edit_title": "{title} bearbeiten",
"dialog.select": "↖ Auswählen",
"dialog.selected_count": "↖ {count} ausgewählt",
"dialog.step": "Schritt",
"dialog.selection_step": "{step} von {total}: {input} auswählen",
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
"document.save_changes": "Änderungen in {name} speichern?",
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
"settings.reset_title": "Einstellungen wurden geändert",
"sketch.degrees_of_freedom": "{count} Freiheitsgrade",
"sketch.draw_curves": "Als {linetype} zeichnen",
"sketch.make_selection": "Auswahl als {linetype} markieren",
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
"ui.3d_sketch": "3D-Skizze",
"ui.about": "Über",
"ui.activate": "Aktivieren",
"ui.active_component": "Aktive Komponente",
"ui.agent_cursor": "Agent-Cursor",
"ui.agent_cursor_speed": "Agent-Cursor-Geschwindigkeit",
"ui.all": "Alle",
"ui.analysis": "Analyse",
"ui.angle": "Winkel",
"ui.angle_decimals": "Winkel-Dezimalstellen",
"ui.animate_view_changes": "Ansichtsänderungen animieren",
"ui.animation": "Animation",
"ui.animation_speed": "Animationsgeschwindigkeit",
"ui.anti_aliasing": "Kantenglättung",
"ui.apply": "Anwenden",
"ui.arc": "Kreisbogen",
"ui.assemble": "Zusammenbau",
"ui.autosave_every": "Autospeichern alle",
"ui.axes": "Achsen",
"ui.axis": "Achse",
"ui.base_point": "Basispunkt",
"ui.bodies": "Körper",
"ui.box": "Block",
"ui.break": "Unterbrechen",
"ui.cancel": "Abbrechen",
"ui.canvases": "Kanvas",
"ui.center": "Mittelpunkt",
"ui.centerline": "Mittellinie",
"ui.chain": "Kette",
"ui.chamfer": "Abschrägung",
"ui.chamfer_type": "Fasentyp",
"ui.change_workspace": "Arbeitsbereich wechseln",
"ui.chord_length": "Sehnenlänge",
"ui.curvature_g2": "Krümmungsstetig (G2)",
"ui.equivalent_radius": "Äquivalenter Radius",
"ui.circle": "Kreis",
"ui.circular_pattern": "Radiales Muster",
"ui.clear_recent": "Zuletzt verwendet leeren",
"ui.clear_the_selection": "Auswahl aufheben",
"ui.close": "Schließen",
"ui.coil": "Schraube",
"ui.coincident": "Koinzident",
"ui.collapse": "Einklappen",
"ui.collinear": "Kollinear",
"ui.combine": "Kombinieren",
"ui.coming_later": "bald verfügbar",
"ui.concentric": "Konzentrisch",
"ui.configurations": "Konfigurationen",
"ui.conic_curve": "Kegelschnitt",
"ui.constant": "Konstant",
"ui.constraints_conflict": "Bedingungen konfliktreich",
"ui.construct": "Konstruktion",
"ui.construction": "Konstruktionslinie",
"ui.copy": "Kopieren",
"ui.corners": "Ecken",
"ui.corner": "Ecke",
"ui.counterbore": "Tiefenbohrung",
"ui.countersink": "Senkung",
"ui.create": "Erstellen",
"ui.create_copy": "Kopie erstellen",
"ui.create_sketch": "Skizze erstellen",
"ui.current": "aktuell",
"ui.curvature": "Krümmung",
"ui.curves": "Kurven",
"ui.cut": "Ausschneiden",
"ui.cylinder": "Zylinder",
"ui.dark": "Dunkel",
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
"ui.degrees_of_freedom": "Freiheitsgrade",
"ui.delete": "Löschen",
"ui.depth": "Tiefe",
"ui.design": "Design",
"ui.diameter": "Durchmesser",
"ui.dimension": "Maß",
"ui.direction": "Richtung",
"ui.distance": "Abstand",
"ui.distance_1": "Abstand 1",
"ui.distance_2": "Abstand 2",
"ui.document_settings": "Dokumenteneinstellungen",
"ui.don_t_save": "Nicht speichern",
"ui.draft": "Abziehen",
"ui.drawing": "Zeichnung",
"ui.drill_point": "Bohrpunkt",
"ui.edit": "Bearbeiten",
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
"ui.ellipse": "Ellipse",
"ui.end_radius": "Endradius",
"ui.entities": "Elemente",
"ui.equal": "Gleich",
"ui.expand": "Aufklappen",
"ui.export_ellipsis": "Exportieren...",
"ui.extend": "Verlängern",
"ui.extent_type": "Ausdehnungstyp",
"ui.extrude": "Extrudieren",
"ui.factor": "Faktor",
"ui.feature_history": "Feature-Verlauf",
"ui.feature_options": "Funktionsoptionen",
"ui.features": "Funktionen",
"ui.file": "Datei",
"ui.fillet": "Vollrundung",
"ui.finish_sketch": "Skizze beenden",
"ui.fix_unfix": "Fixieren/Lösen",
"ui.flat": "Flach",
"ui.flip": "Umkehren",
"ui.front": "Vorne",
"ui.fully_constrained": "Vollständig bestimmt",
"ui.general": "Allgemein",
"ui.graphics": "Grafik",
"ui.heading_assemble": "ZUSAMMENBAU",
"ui.heading_box": "BLOCK",
"ui.heading_chamfer": "ABSCHRÄGUNG",
"ui.heading_circular_pattern": "RADIALES MUSTER",
"ui.heading_coil": "SCHRAUBE",
"ui.heading_combine": "KOMBINIEREN",
"ui.heading_construct": "KONSTRUKTION",
"ui.heading_create": "ERSTELLEN",
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
"ui.heading_cylinder": "ZYLINDER",
"ui.heading_design": "DESIGN",
"ui.heading_draft": "ABZIEHEN",
"ui.heading_edit": "BEARBEITEN",
"ui.heading_extrude": "EXTRUDIEREN",
"ui.heading_fillet": "VOLLRUNDUNG",
"ui.heading_finish_sketch": "SKIZZE BEENDEN",
"ui.heading_front": "VORNE",
"ui.heading_hole": "BOHRUNG",
"ui.heading_home": "START",
"ui.heading_insert": "EINFÜGEN",
"ui.heading_inspect": "PRÜFEN",
"ui.heading_loft": "LOFT",
"ui.heading_measure": "Messen",
"ui.heading_mesh": "MESH",
"ui.heading_mirror": "SPIEGELN",
"ui.heading_modify": "ÄNDERN",
"ui.heading_move": "VERSCHIEBEN",
"ui.heading_offset": "ABSTAND",
"ui.heading_move_copy": "VERSCHIEBEN/KOPIEREN",
"ui.heading_offset_plane": "ABSTANDSEBENE",
"ui.heading_ok": "OK",
"ui.heading_parameters": "PARAMETER",
"ui.heading_pattern_on_path": "PFADMUSTER",
"ui.heading_pipe": "ROHR",
"ui.heading_plane_at_angle": "EBENE IM WINKEL",
"ui.heading_plastic": "PLASTIK",
"ui.heading_rectangular_pattern": "RECHTECKMUSTER",
"ui.heading_revolve": "DREHEN",
"ui.heading_right": "RECHTS",
"ui.heading_scale": "SKALIEREN",
"ui.heading_section_analysis": "QUERSCHNITTSANALYSE",
"ui.heading_select": "AUSWÄHLEN",
"ui.heading_sheet_metal": "BLECH",
"ui.heading_shell": "HÜLLE",
"ui.heading_sketch": "SKIZZE",
"ui.heading_sketch_palette": "SKIZZENPALETTE",
"ui.heading_sketch_scale": "SKIZZE SKALIEREN",
"ui.heading_solid": "KÖRPER",
"ui.heading_sphere": "SPHÄRE",
"ui.heading_split_body": "KÖRPER TRENNEN",
"ui.heading_surface": "FLÄCHE",
"ui.heading_sweep": "FÜHREN",
"ui.heading_thread": "GEWINDE",
"ui.heading_top": "OBEN",
"ui.heading_torus": "TORUS",
"ui.help": "Hilfe",
"ui.hide": "Ausblenden",
"ui.hole": "Bohrung",
"ui.hole_tap_type": "Gewindetyp",
"ui.hole_type": "Bohrungstyp",
"ui.home": "Start",
"ui.horizontal_vertical": "Horizontal/Vertikal",
"ui.insert": "Einfügen",
"ui.inspect": "Prüfen",
"ui.intersect": "Schnitt",
"ui.join": "Verbinden",
"ui.joints": "Gelenke",
"ui.keyboard_shortcuts": "Tastenkürzel",
"ui.language": "Sprache",
"ui.length_decimals": "Längen-Dezimalstellen",
"ui.light": "Hell",
"ui.line": "Linie",
"ui.linetype": "Linientyp",
"ui.loft": "Loft",
"ui.look_at": "Ansehen",
"ui.manufacture": "Fertigung",
"ui.measure": "Messen",
"ui.midpoint": "Mittelpunkt",
"ui.mirror": "Spiegeln",
"ui.mirror_line": "Spiegelachse",
"ui.modify": "Ändern",
"ui.move": "Verschieben",
"ui.multiple": "Mehrfach",
"ui.named_views": "Benannte Ansichten",
"ui.navigation": "Navigation",
"ui.needs_design": "{command}: Öffnen Sie zuerst ein Design",
"ui.new_body": "Neuer Körper",
"ui.new_design": "Neues Design",
"ui.normal": "Normal",
"ui.object_type": "Objekttyp",
"ui.objects": "Objekte",
"ui.offer_to_stand_up_y_up_files": "Y-hoch-Dateien aufrecht anbieten",
"ui.offset": "Abstand",
"ui.offset_plane": "Abstandsebene",
"ui.one_side": "Einseitig",
"ui.open": "Öffnen",
"ui.open_ellipsis": "Öffnen...",
"ui.open_preferences": "Einstellungen öffnen",
"ui.open_recent": "Zuletzt verwendet",
"ui.operation": "Operation",
"ui.options": "Optionen",
"ui.orbit_around": "Umkreisen um",
"ui.origin": "Ursprung",
"ui.palette_points": "Punkte",
"ui.palette_profile": "Profil",
"ui.pan_zoom_orbit": "Verschieben, zoomen, umkreisen",
"ui.parallel": "Parallel",
"ui.parameters": "Parameter",
"ui.paste": "Einfügen",
"ui.path": "Pfad",
"ui.pattern_on_path": "Pfadmuster",
"ui.perpendicular": "Senkrecht",
"ui.pipe": "Rohr",
"ui.placement": "Platzierung",
"ui.plane": "Ebene",
"ui.plane_at_angle": "Ebene im Winkel",
"ui.point": "Punkt",
"ui.point_under_the_cursor": "Punkt unter dem Cursor",
"ui.points": "Punkte",
"ui.polygon": "Polygon",
"ui.preferences": "Einstellungen",
"ui.profile": "Profil",
"ui.profile_plane": "Profilebene",
"ui.profiles": "Profile",
"ui.project": "Projizieren",
"ui.quantity_2": "Anzahl 2",
"ui.quantity": "Anzahl",
"ui.quit": "Beenden",
"ui.radius": "Radius",
"ui.radius_type": "Radius-Typ",
"ui.rectangle": "Rechteck",
"ui.rectangular_pattern": "Rechteckmuster",
"ui.redo": "Wiederholen",
"ui.render": "Rendern",
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
"ui.restore_defaults": "Standardeinstellungen wiederherstellen",
"ui.reverse_zoom_direction": "Zoomrichtung umkehren",
"ui.revolve": "Drehen",
"ui.right": "Rechts",
"ui.save": "Speichern",
"ui.save_as": "Speichern unter",
"ui.save_as_ellipsis": "Speichern unter...",
"ui.scale": "Skalieren",
"ui.section_analysis": "Querschnittsanalyse",
"ui.select": "Auswählen",
"ui.selection": "Auswahl",
"ui.shell": "Hülle",
"ui.show": "Einblenden",
"ui.show_constraints": "Bedingungen",
"ui.show_dimensions": "Maße",
"ui.show_points": "Punkte anzeigen",
"ui.show_profile": "Profil anzeigen",
"ui.show_projected_geometries": "Projizierte Geometrien",
"ui.simple": "Einfach",
"ui.simulation": "Simulation",
"ui.single": "Einzel",
"ui.sketch": "Skizze",
"ui.sketch_dimension": "Skizzenmaß",
"ui.sketch_grid": "Skizzenraster",
"ui.sketch_scale": "Skizzenskalierung",
"ui.sketch_status": "Skizzenstatus",
"ui.sketches": "Skizzen",
"ui.slice": "Scheibe",
"ui.slot": "Langloch",
"ui.smooth": "Glatt",
"ui.snap": "Einrasten",
"ui.spacing_2": "Abstand 2",
"ui.spacing": "Abstand",
"ui.sphere": "Sphäre",
"ui.spline": "Spline",
"ui.split_body": "Körper trennen",
"ui.start": "Start",
"ui.start_radius": "Startradius",
"ui.sweep": "Führen",
"ui.symmetric": "Symmetrisch",
"ui.symmetry": "Symmetrie",
"ui.system": "System",
"ui.tangent": "Tangential",
"ui.taper_angle": "Kegelwinkel",
"ui.tapped": "Gewindebohrung",
"ui.text": "Text",
"ui.theme": "Design",
"ui.thickness": "Dicke",
"ui.thread": "Gewinde",
"ui.timeline_bottom": "Timeline unten",
"ui.timeline_left_note": "Im linken Layout wird die Rücksetzmarke über dem Teile- und Szenenbrowser eingeblendet.",
"ui.timeline_left_sidebar": "Linke Seitenleiste",
"ui.tip_angle": "Spitzenwinkel",
"ui.to_object": "Bis Objekt",
"ui.top": "Oben",
"ui.torus": "Torus",
"ui.trim": "Kürzen",
"ui.two_sides": "Beidseitig",
"ui.ui_scale": "UI-Skalierung",
"ui.undo": "Rückgängig",
"ui.units": "Einheiten",
"ui.units_of_new_designs": "Einheiten für neue Designs",
"ui.untitled": "Ohne Titel",
"ui.variable": "Variabel",
"ui.view_centre": "Ansichtszentrum",
"ui.visual_style": "Ansichtsstil",
"ui.workspace": "Arbeitsbereich",
"ui.y_distance": "Y-Abstand",
"ui.x_distance": "X-Abstand",
"ui.zoom_to_the_cursor": "Zum Cursor zoomen",
"ui.animateJoint": "Gelenk animieren",
"ui.calibrate": "Kalibrieren",
"ui.customizeToolbar": "Symbolleiste anpassen…",
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
"ui.exportDxf": "Als DXF exportieren…",
"ui.findInTimeline": "In der Zeitleiste suchen",
"ui.findInWindow": "Im Fenster suchen",
"ui.fit": "Einpassen",
"ui.groupSelected": "Auswahl gruppieren",
"ui.hideAxes": "Achsen ausblenden",
"ui.hideDimension": "Bemaßung ausblenden",
"ui.hideOrigin": "Ursprung ausblenden",
"ui.hidePlanes": "Ebenen ausblenden",
"ui.hideProfile": "Profil ausblenden",
"ui.homeView": "Startansicht",
"ui.isolate": "Isolieren",
"ui.lock": "Sperren",
"ui.newSketchGroup": "Neue Skizzengruppe",
"ui.normalConstruction": "Normal / Konstruktion",
"ui.orbit": "Drehen",
"ui.heading_position": "POSITION",
"ui.pan": "Verschieben",
"ui.pinToShortcuts": "An Verknüpfungen anheften",
"ui.pinToToolbar": "An Symbolleiste anheften",
"ui.properties": "Eigenschaften",
"ui.removeSection": "Schnitt entfernen",
"ui.removeFromToolbar": "Aus der Symbolleiste entfernen",
"ui.repeat": "Wiederholen",
"ui.report_a_bug": "Fehler melden",
"ui.restore": "Wiederherstellen",
"ui.heading_setup": "EINRICHTEN",
"ui.selectOther": "Andere auswählen…",
"ui.selectTangentChain": "Tangentialkette auswählen",
"ui.showAll": "Alle einblenden",
"ui.showAllBodies": "Alle Volumenkörper einblenden",
"ui.showAxes": "Achsen einblenden",
"ui.showDimension": "Bemaßung einblenden",
"ui.showOrigin": "Ursprung einblenden",
"ui.showPlanes": "Ebenen einblenden",
"ui.suppress": "Unterdrücken",
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
"ui.unground": "Fixierung lösen",
"ui.unisolate": "Isolierung aufheben",
"ui.unlock": "Entsperren",
"ui.unsuppress": "Unterdrückung aufheben",
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
"ui.zoom": "Zoomen",
"ui.command_done": "{command} fertig",
"cmd.appearance_assign": "Erscheinungsbild",
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
"cmd.appearance_list": "Erscheinungsbilder auflisten",
"cmd.body_rename": "Körper umbenennen",
"cmd.browser_group": "Neue Gruppe",
"cmd.browser_move": "Browser-Elemente verschieben",
"cmd.browser_order": "Browser-Elemente ordnen",
"cmd.browser_rename_group": "Gruppe umbenennen",
"cmd.browser_ungroup": "Gruppierung aufheben",
"cmd.browser_visibility": "Ein-/Ausblenden",
"cmd.canvas_calibrate": "Zeichenfläche kalibrieren",
"cmd.canvas_decal": "Dekal",
"cmd.canvas_delete": "Zeichenfläche löschen",
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
"cmd.canvas_insert": "Zeichenfläche",
"cmd.component_activate": "Komponente aktivieren",
"cmd.component_break_link": "Verknüpfung trennen",
"cmd.component_capture_position": "Position erfassen",
"cmd.component_create": "Neue Komponente",
"cmd.component_delete": "Komponente löschen",
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
"cmd.component_insert_design": "Konstruktion einfügen",
"cmd.component_links": "Verknüpfungen auflisten",
"cmd.component_list": "Komponenten auflisten",
"cmd.component_move_bodies": "Körper in Komponente verschieben",
"cmd.component_move_sketches": "Skizzen in Komponente verschieben",
"cmd.component_paste_new": "Neu einfügen",
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
"cmd.component_rename": "Komponente umbenennen",
"cmd.component_revert_position": "Position zurücksetzen",
"cmd.component_update_links": "Verknüpfungen aktualisieren",
"cmd.config_activate": "Konfiguration aktivieren",
"cmd.config_column": "Konfigurationsspalte",
"cmd.config_configure": "Konfigurieren",
"cmd.config_preview": "Konfiguration vorschauen",
"cmd.config_row": "Konfigurationszeile",
"cmd.config_rule": "Konfigurationsregel",
"cmd.config_rules": "Konfigurationsregeln",
"cmd.config_table": "Konfigurationstabelle anzeigen",
"cmd.construct_axis_edge": "Achse durch Kante",
"cmd.construct_axis_normal_to_face": "Achse senkrecht zur Fläche",
"cmd.construct_axis_through_cylinder": "Achse durch Zylinder/Kegel/Torus",
"cmd.construct_axis_two_planes": "Achse durch zwei Ebenen",
"cmd.construct_axis_two_points": "Achse durch zwei Punkte",
"cmd.construct_plane_along_path": "Ebene entlang Pfad",
"cmd.construct_plane_midplane": "Mittenebene",
"cmd.construct_plane_perpendicular": "Rechtebene",
"cmd.construct_plane_tangent": "Tangentialebene",
"cmd.construct_plane_three_points": "Ebene durch drei Punkte",
"cmd.construct_plane_two_edges": "Ebene durch zwei Kanten",
"cmd.construct_point_along_path": "Punkt entlang Pfad",
"cmd.construct_point_center": "Punkt im Mittelpunkt von Kreis/Kugel/Torus",
"cmd.construct_point_edge_plane": "Punkt an Kante und Ebene",
"cmd.construct_point_three_planes": "Punkt aus drei Ebenen",
"cmd.construct_point_two_edges": "Punkt aus zwei Kanten",
"cmd.construct_point_vertex": "Punkt an Scheitelpunkt",
"cmd.contact_check": "Kontakt prüfen",
"cmd.contact_create": "Neuer Kontaktsatz",
"cmd.contact_disable_all": "Kontakt deaktivieren",
"cmd.contact_edit": "Kontaktsatz bearbeiten",
"cmd.contact_enable_all": "Alle Kontakte aktivieren",
"cmd.contact_enable_sets": "Kontaktsätze aktivieren",
"cmd.contact_list": "Kontaktsätze auflisten",
"cmd.diagnostics_crash_report": "Absturzbericht",
"cmd.diagnostics_crash_reports": "Absturzberichte",
"cmd.diagnostics_crash_reports_clear": "Absturzberichte löschen",
"cmd.diagnostics_crash_test": "Absturztest",
"cmd.doc_recover": "Konstruktion wiederherstellen",
"cmd.doc_recovery_discard": "Wiederhergestellte Konstruktion verwerfen",
"cmd.doc_recovery_list": "Wiederherstellbare Konstruktionen",
"cmd.document_inspect": "Konstruktion prüfen",
"cmd.document_units": "Einheiten der Konstruktion ändern",
"cmd.edit_checkpoint": "Prüfpunkt",
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
"cmd.engine_commands": "Befehle auflisten",
"cmd.explode_create": "Explosionsdarstellung",
"cmd.explode_delete": "Explosionsdarstellung löschen",
"cmd.explode_list": "Explosionsdarstellungen auflisten",
"cmd.explode_show": "Explosionsdarstellung anzeigen",
"cmd.expr_evaluate": "Ausdruck auswerten",
"cmd.face_delete": "Fläche löschen",
"cmd.file_export": "Exportieren",
"cmd.file_insert_mesh": "Netz einfügen",
"cmd.file_insert_step": "STEP einfügen",
"cmd.file_save_mesh": "Als Netz speichern",
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
"cmd.inspect_curvature_map": "Krümmungskartenanalyse",
"cmd.inspect_draft": "Auswinkelanalyse",
"cmd.inspect_environment_map": "Umgebungskartenanalyse",
"cmd.inspect_interference": "Eingriff",
"cmd.inspect_isocurve": "Isokurvenanalyse",
"cmd.inspect_minimum_radius": "Mindestradiusanalyse",
"cmd.inspect_zebra": "Zebra-Analyse",
"cmd.joint_as_built": "Fertigungsgelenk",
"cmd.joint_create": "Gelenk",
"cmd.joint_delete": "Gelenk löschen",
"cmd.joint_drive": "Gelenke antreiben",
"cmd.joint_edit": "Gelenk bearbeiten",
"cmd.joint_limits": "Gelenkgrenzen",
"cmd.joint_list": "Gelenke auflisten",
"cmd.joint_motion_link": "Bewegungsverknüpfung",
"cmd.joint_origin": "Gelenkursprung",
"cmd.joint_rigid_group": "Starrgruppe",
"cmd.joint_solve": "Gelenke lösen",
"cmd.material_assign": "Physikalisches Material",
"cmd.material_create": "Neues physikalisches Material",
"cmd.material_delete": "Physisches Material löschen",
"cmd.material_edit": "Physisches Material bearbeiten",
"cmd.material_list": "Materialien auflisten",
"cmd.mesh_convert": "Netz konvertieren",
"cmd.model_edges": "Kanten auflisten",
"cmd.model_faces": "Flächen auflisten",
"cmd.model_threads": "Gewinde auflisten",
"cmd.motion_delete": "Bewegungsuntersuchung löschen",
"cmd.motion_export": "Bewegungsuntersuchung exportieren",
"cmd.motion_list": "Bewegungsuntersuchungen auflisten",
"cmd.motion_play": "Bewegungsuntersuchung abspielen",
"cmd.motion_study": "Bewegungsuntersuchung",
"cmd.occurrence_copy": "Einfügen (Instanz)",
"cmd.occurrence_ground": "Festsetzen",
"cmd.occurrence_move": "Instanz verschieben",
"cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz",
"cmd.parameters_add": "Benutzerparameter hinzufügen",
"cmd.parameters_change": "Parameter ändern",
"cmd.parameters_comment": "Parameter kommentieren",
"cmd.parameters_complete": "Ausdruck vervollständigen",
"cmd.parameters_delete": "Parameter löschen",
"cmd.parameters_export": "Parameter exportieren",
"cmd.parameters_favorite": "Favorisierter Parameter",
"cmd.parameters_graph": "Parametergraph",
"cmd.parameters_import": "Parameter importieren",
"cmd.parameters_list": "Parameter auflisten",
"cmd.parameters_rename": "Parameter umbenennen",
"cmd.parameters_users": "Parameterverwendungen",
"cmd.parts_fastener": "Verbindungselement einfügen",
"cmd.parts_insert": "Bauteil einfügen",
"cmd.parts_library": "Standardbauteile",
"cmd.parts_resize": "Bauteilgröße ändern",
"cmd.plastic_assign_rule": "Kunststoffregel zuweisen",
"cmd.plastic_boss": "Erhöhung",
"cmd.plastic_lip": "Lippe",
"cmd.plastic_manage_rules": "Kunststoffregeln verwalten",
"cmd.plastic_rest": "Auflage",
"cmd.plastic_rules": "Kunststoffregeln auflisten",
"cmd.plastic_snap_fit": "Rastverbindung",
"cmd.select_clear": "Auswahl aufheben",
"cmd.sheet_convert": "In Blech umwandeln",
"cmd.sheet_export_bend_table": "Biegungstabelle exportieren",
"cmd.sheet_export_dxf": "Flachmuster als DXF exportieren",
"cmd.sheet_flange": "Flansch",
"cmd.sheet_flat_pattern": "Flachmuster erstellen",
"cmd.sheet_fold": "Falten",
"cmd.sheet_hem": "Umkantung",
"cmd.sheet_manage_rules": "Blechregeln",
"cmd.sheet_refold": "Wieder falten",
"cmd.sheet_rules": "Blechregeln auflisten",
"cmd.sheet_unfold": "Entfalten",
"cmd.sketch_arc_center_point": "Mittelpunkt-Kreisbogen",
"cmd.sketch_arc_tangent": "Tangentialbogen",
"cmd.sketch_arc_three_point": "3-Punkt-Kreisbogen",
"cmd.sketch_auto_constrain": "Auto-Bedingung",
"cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug",
"cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren",
"cmd.sketch_blend_curve": "Übergangskurve",
"cmd.sketch_chain": "Kette",
"cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel",
"cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand",
"cmd.sketch_chamfer_two_distance": "Abschrägung mit zwei Abständen",
"cmd.sketch_circle_center": "Kreis aus Mittelpunkt und Durchmesser",
"cmd.sketch_circle_three_point": "3-Punkt-Kreis",
"cmd.sketch_circle_three_tangent": "3-Tangenten-Kreis",
"cmd.sketch_circle_two_point": "2-Punkt-Kreis",
"cmd.sketch_circle_two_tangent": "2-Tangenten-Kreis",
"cmd.sketch_constrainer": "Skizzen-Bedingung",
"cmd.sketch_constraint_fix": "Fixieren/Lösen",
"cmd.sketch_constraint_midpoint": "Mittelpunkt",
"cmd.sketch_construction": "Normal/Konstruktion",
"cmd.sketch_delete": "Skizzenentitäten löschen",
"cmd.sketch_dimension_text": "Maßtext verschieben",
"cmd.sketch_drag": "Skizzengeometrie ziehen",
"cmd.sketch_drag_end": "Skizzen-Ziehen beenden",
"cmd.sketch_draw_dimensioned": "Skizzengeometrie mit Maßen zeichnen",
"cmd.sketch_edit": "Skizze bearbeiten",
"cmd.sketch_edit_text": "Text bearbeiten",
"cmd.sketch_export_dxf": "Als DXF speichern",
"cmd.sketch_finish_auto_constrain": "Mit Auto-Bedingung fertigstellen",
"cmd.sketch_fit_curves_to_section": "Kurven an Netzquerschnitt anpassen",
"cmd.sketch_glyphs": "Bedingungssymbole",
"cmd.sketch_include_3d": "3D-Geometrie einbeziehen",
"cmd.sketch_insert_dxf": "DXF einfügen",
"cmd.sketch_insert_svg": "SVG einfügen",
"cmd.sketch_inspect": "Skizze prüfen",
"cmd.sketch_intersection_curve": "Schnittkurve",
"cmd.sketch_isoparametric_curve": "Isoparametrische Kurve",
"cmd.sketch_line3d": "3D-Linie",
"cmd.sketch_line_midpoint": "Linie über den Mittelpunkt",
"cmd.sketch_move": "Skizzenentitäten verschieben/kopieren",
"cmd.sketch_move3d": "3D-Punkt verschieben",
"cmd.sketch_move_point": "Skizzenpunkt ziehen",
"cmd.sketch_offset_break": "Versatzverknüpfung trennen",
"cmd.sketch_options": "Skizzenpalettenoptionen",
"cmd.sketch_point3d": "3D-Punkt",
"cmd.sketch_polygon_circumscribed": "Umgeschriebenes Polygon",
"cmd.sketch_polygon_edge": "Polygon über eine Kante",
"cmd.sketch_polygon_inscribed": "Eingeschriebenes Polygon",
"cmd.sketch_project_to_surface": "Auf Fläche projizieren",
"cmd.sketch_rectangle_center": "Rechteck vom Mittelpunkt",
"cmd.sketch_rectangle_three_point": "3-Punkt-Rechteck",
"cmd.sketch_rectangle_two_point": "2-Punkt-Rechteck",
"cmd.sketch_redefine": "Skizzierebene neu definieren",
"cmd.sketch_slot_arc_center": "Mittelpunkt-Bogenschlitz",
"cmd.sketch_slot_arc_three_point": "3-Punkt-Bogenschlitz",
"cmd.sketch_slot_center_point": "Mittelpunktschlitz",
"cmd.sketch_slot_center_to_center": "Schlitz von Mittelpunkt zu Mittelpunkt",
"cmd.sketch_slot_overall": "Gesamtschlitz",
"cmd.sketch_snap": "Einrastinferenz",
"cmd.sketch_solve": "Skizze lösen",
"cmd.sketch_spline3d": "3D-Spline",
"cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten",
"cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)",
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
"cmd.sketch_spun_profile": "Gedrehtes Profil",
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
"cmd.solid_align": "Ausrichten",
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
"cmd.solid_bounding_solid": "Begrenzender Körper",
"cmd.solid_emboss": "Prägen",
"cmd.solid_move": "Verschieben/Kopieren",
"cmd.solid_offset_face": "Fläche versetzen",
"cmd.solid_press_pull": "Drücken/Ziehen",
"cmd.solid_remove": "Entfernen",
"cmd.solid_replace_face": "Fläche ersetzen",
"cmd.solid_rib": "Rippe",
"cmd.solid_split_face": "Fläche teilen",
"cmd.solid_web": "Gewebe",
"cmd.surface_patch": "Patch",
"cmd.surface_stitch": "Nähen",
"cmd.surface_thicken": "Dicke vergeben",
"cmd.surface_extrude": "Flächen-Extrusion",
"cmd.surface_revolve": "Flächen-Rotation",
"cmd.surface_sweep": "Flächen-Sweep",
"cmd.surface_loft": "Flächen-Loft",
"cmd.surface_ruled": "Regelfläche",
"cmd.surface_boundary": "Begrenzungsfläche",
"cmd.surface_offset": "Offsetfläche",
"cmd.surface_blend": "Verbindungsfläche",
"cmd.surface_fillet": "Flächenverrundung",
"cmd.surface_untrim": "Trimmen aufheben",
"cmd.surface_unstitch": "Verbund aufheben",
"cmd.surface_reverse_normal": "Normale umkehren",
"cmd.inspect_continuity": "Kontinuitätsprüfung",
"cmd.timeline_compute_all": "Alles berechnen",
"cmd.timeline_copy": "Merkmale kopieren",
"cmd.timeline_dependents": "Merkmalsabhängigkeiten",
"cmd.timeline_inspect": "Merkmal prüfen",
"cmd.timeline_paste": "Merkmale einfügen",
"cmd.timeline_redefine": "Merkmal bearbeiten",
"cmd.timeline_rename": "Umbenennen",
"cmd.timeline_reorder": "Merkmal neu ordnen",
"cmd.timeline_roll_to": "Verlaufsmarkierung verschieben",
"cmd.timeline_rollback": "Zeitleistenmarkierung verschieben",
"cmd.timeline_suppress": "Merkmal unterdrücken",
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
"cmd.view_delete": "Benannte Ansicht löschen",
"cmd.view_list": "Benannte Ansichten auflisten",
"cmd.view_rename": "Benannte Ansicht umbenennen",
"cmd.view_save": "Neue benannte Ansicht",
"ui.export_ellipsis_keymap": "Exportieren…",
"ui.heading_configure": "KONFIGURIEREN",
"ui.heading_constraints": "RANDBEDINGUNGEN",
"ui.heading_utilities": "WERKZEUGE",
"BROWSER": "BROWSER",
"Drive Joint": "Gelenk antreiben",
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
"Find in Browser": "Im Browser suchen",
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
"Reset Toolbar": "Symbolleiste zurücksetzen",
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
"Selection filter": "Auswahlfilter",
"Revert Position": "Position zurücksetzen",
"Construction": "Konstruktionslinie",
"Faces": "Flächen",
"Edges": "Kanten",
"Vertices": "Scheitelpunkte",
"Sketch geometry and profiles": "Skizzengeometrie und Profile",
"Sketch curves": "Skizzenkurven",
"Sketch points": "Skizzenpunkte",
"Sketch dimensions": "Skizzenmaße",
"Sketch constraints": "Skizzenbedingungen",
"Flat": "Flach",
"cmd.render_backends": "Render-Backends",
"cmd.view_navigate": "Ansicht navigieren",
"timeline.group": "Neuen Merkmalsordner anlegen",
"timeline.ungroup": "Merkmalsordner entfernen",
"timeline.rename_group": "Merkmalsordner umbenennen",
"timeline.move_to_group": "Merkmale in Ordner verschieben",
"timeline.move_many": "Zeitleistenelemente verschieben",
"timeline.reorder_many": "Merkmale neu ordnen",
"ui.show_construction_geometries": "Konstruktionsgeometrien",
"ui.total_angle": "Gesamtwinkel",
"sketch_text.builtin_font": "{family} (integriert)",
"sketch_text.font_search": "Schriftarten suchen",
"sketch_text.loading_fonts": "Installierte Schriftarten werden gesucht…",
"sketch_text.use_font_file": "Schriftdatei {path} verwenden",
"sketch_text.bold": "Fett",
"sketch_text.italic": "Kursiv",
"sketch_text.align_left": "Links",
"sketch_text.align_center": "Mitte",
"sketch_text.align_right": "Rechts",
"sketch_text.path_hint": "Eine Skizzenkurve, der der Text folgt: hier klicken, dann die Kurve",
"sketch_text.path_clear": "Text von der Kurve lösen",
"sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage"
}
+961
View File
@@ -0,0 +1,961 @@
{
"browser.body_name": "Body{number}",
"browser.component_name": "Component{number}",
"browser.parameters": "Parameters ({count})",
"browser.precision": "Precision: {precision}",
"browser.sketch_name": "Sketch{number}",
"browser.units": "Units: {units}",
"cmd.appearance_assign": "Appearance",
"cmd.appearance_library": "Appearance Library",
"cmd.appearance_list": "List Appearances",
"cmd.body_rename": "Rename Body",
"cmd.browser_group": "New Group",
"cmd.browser_move": "Move Browser Items",
"cmd.browser_order": "Order Browser Items",
"cmd.browser_rename_group": "Rename Group",
"cmd.browser_ungroup": "Ungroup",
"cmd.browser_visibility": "Show/Hide",
"cmd.canvas_calibrate": "Calibrate Canvas",
"cmd.canvas_decal": "Decal",
"cmd.canvas_delete": "Delete Canvas",
"cmd.canvas_edit": "Edit Canvas",
"cmd.canvas_insert": "Canvas",
"cmd.component_activate": "Activate Component",
"cmd.component_break_link": "Break Link",
"cmd.component_capture_position": "Capture Position",
"cmd.component_create": "New Component",
"cmd.component_delete": "Delete Component",
"cmd.component_from_bodies": "Create Components from Bodies",
"cmd.component_insert_design": "Insert Design",
"cmd.component_links": "List Links",
"cmd.component_list": "List Components",
"cmd.component_move_bodies": "Move Bodies to Component",
"cmd.component_move_sketches": "Move Sketches to Component",
"cmd.component_paste_new": "Paste New",
"cmd.component_properties": "Component Physical Properties",
"cmd.component_rename": "Rename Component",
"cmd.component_revert_position": "Revert Position",
"cmd.component_update_links": "Update Links",
"cmd.config_activate": "Activate Configuration",
"cmd.config_column": "Configuration Column",
"cmd.config_configure": "Configure",
"cmd.config_preview": "Preview Configuration",
"cmd.config_row": "Configuration Row",
"cmd.config_rule": "Configuration Rule",
"cmd.config_rules": "Configuration Rules",
"cmd.config_table": "Display Configuration Table",
"cmd.construct_axis_edge": "Axis Through Edge",
"cmd.construct_axis_normal_to_face": "Axis Perpendicular To Face",
"cmd.construct_axis_through_cylinder": "Axis Through Cylinder/Cone/Torus",
"cmd.construct_axis_two_planes": "Axis Through Two Planes",
"cmd.construct_axis_two_points": "Axis Through Two Points",
"cmd.construct_plane_along_path": "Plane Along Path",
"cmd.construct_plane_midplane": "Midplane",
"cmd.construct_plane_perpendicular": "Perpendicular Plane",
"cmd.construct_plane_tangent": "Tangent Plane",
"cmd.construct_plane_three_points": "Plane Through Three Points",
"cmd.construct_plane_two_edges": "Plane Through Two Edges",
"cmd.construct_point_along_path": "Point Along Path",
"cmd.construct_point_center": "Point At Center Of Circle/Sphere/Torus",
"cmd.construct_point_edge_plane": "Point At Edge And Plane",
"cmd.construct_point_three_planes": "Point Through Three Planes",
"cmd.construct_point_two_edges": "Point Through Two Edges",
"cmd.construct_point_vertex": "Point At Vertex",
"cmd.contact_check": "Check Contact",
"cmd.contact_create": "New Contact Set",
"cmd.contact_disable_all": "Disable Contact",
"cmd.contact_edit": "Edit Contact Set",
"cmd.contact_enable_all": "Enable All Contact",
"cmd.contact_enable_sets": "Enable Contact Sets",
"cmd.contact_list": "List Contact Sets",
"cmd.diagnostics_crash_report": "Crash Report",
"cmd.diagnostics_crash_reports": "Crash Reports",
"cmd.diagnostics_crash_reports_clear": "Delete Crash Reports",
"cmd.diagnostics_crash_test": "Crash Test",
"cmd.doc_recover": "Recover Design",
"cmd.doc_recovery_discard": "Discard Recovered Design",
"cmd.doc_recovery_list": "Recoverable Designs",
"cmd.document_inspect": "Inspect Design",
"cmd.document_units": "Change Design Units",
"cmd.edit_checkpoint": "Checkpoint",
"cmd.edit_restore_checkpoint": "Restore Checkpoint",
"cmd.engine_commands": "List Commands",
"cmd.explode_create": "Exploded View",
"cmd.explode_delete": "Delete Exploded View",
"cmd.explode_list": "List Exploded Views",
"cmd.explode_show": "Show Exploded View",
"cmd.expr_evaluate": "Evaluate Expression",
"cmd.face_delete": "Delete Face",
"cmd.file_export": "Export",
"cmd.file_insert_mesh": "Insert Mesh",
"cmd.file_insert_step": "Insert STEP",
"cmd.file_save_mesh": "Save As Mesh",
"cmd.inspect_accessibility": "Accessibility Analysis",
"cmd.inspect_center_of_mass": "Center of Mass",
"cmd.inspect_curvature_comb": "Curvature Comb Analysis",
"cmd.inspect_curvature_map": "Curvature Map Analysis",
"cmd.inspect_draft": "Draft Analysis",
"cmd.inspect_environment_map": "Environment Map Analysis",
"cmd.inspect_interference": "Interference",
"cmd.inspect_isocurve": "Isocurve Analysis",
"cmd.inspect_minimum_radius": "Minimum Radius Analysis",
"cmd.inspect_zebra": "Zebra Analysis",
"cmd.joint_as_built": "As-Built Joint",
"cmd.joint_create": "Joint",
"cmd.joint_delete": "Delete Joint",
"cmd.joint_drive": "Drive Joints",
"cmd.joint_edit": "Edit Joint",
"cmd.joint_limits": "Joint Limits",
"cmd.joint_list": "List Joints",
"cmd.joint_motion_link": "Motion Link",
"cmd.joint_origin": "Joint Origin",
"cmd.joint_rigid_group": "Rigid Group",
"cmd.joint_solve": "Solve Joints",
"cmd.material_assign": "Physical Material",
"cmd.material_create": "New Physical Material",
"cmd.material_delete": "Delete Physical Material",
"cmd.material_edit": "Edit Physical Material",
"cmd.material_list": "List Materials",
"cmd.mesh_convert": "Convert Mesh",
"cmd.model_edges": "List Edges",
"cmd.model_faces": "List Faces",
"cmd.model_threads": "List Threads",
"cmd.motion_delete": "Delete Motion Study",
"cmd.motion_export": "Export Motion Study",
"cmd.motion_list": "List Motion Studies",
"cmd.motion_play": "Play Motion Study",
"cmd.motion_study": "Motion Study",
"cmd.occurrence_copy": "Paste (Instance)",
"cmd.occurrence_ground": "Ground",
"cmd.occurrence_move": "Move Occurrence",
"cmd.occurrence_properties": "Occurrence Physical Properties",
"cmd.parameters_add": "Add User Parameter",
"cmd.parameters_change": "Change Parameters",
"cmd.parameters_comment": "Comment Parameter",
"cmd.parameters_complete": "Complete Expression",
"cmd.parameters_delete": "Delete Parameter",
"cmd.parameters_export": "Export Parameters",
"cmd.parameters_favorite": "Favourite Parameter",
"cmd.parameters_graph": "Parameter Graph",
"cmd.parameters_import": "Import Parameters",
"cmd.parameters_list": "List Parameters",
"cmd.parameters_rename": "Rename Parameter",
"cmd.parameters_users": "Parameter Users",
"cmd.parts_fastener": "Insert Fastener",
"cmd.parts_insert": "Insert Part",
"cmd.parts_library": "Standard Parts",
"cmd.parts_resize": "Change Part Size",
"cmd.plastic_assign_rule": "Assign Plastic Rule",
"cmd.plastic_boss": "Boss",
"cmd.plastic_lip": "Lip",
"cmd.plastic_manage_rules": "Manage Plastic Rules",
"cmd.plastic_rest": "Rest",
"cmd.plastic_rules": "List Plastic Rules",
"cmd.plastic_snap_fit": "Snap Fit",
"cmd.select_clear": "Clear Selection",
"cmd.sheet_convert": "Convert to Sheet Metal",
"cmd.sheet_export_bend_table": "Export Bend Table",
"cmd.sheet_export_dxf": "Export Flat Pattern as DXF",
"cmd.sheet_flange": "Flange",
"cmd.sheet_flat_pattern": "Create Flat Pattern",
"cmd.sheet_fold": "Fold",
"cmd.sheet_hem": "Hem",
"cmd.sheet_manage_rules": "Sheet Metal Rules",
"cmd.sheet_refold": "Refold",
"cmd.sheet_rules": "List Sheet Metal Rules",
"cmd.sheet_unfold": "Unfold",
"cmd.sketch_arc_center_point": "Center Point Arc",
"cmd.sketch_arc_tangent": "Tangent Arc",
"cmd.sketch_arc_three_point": "3-Point Arc",
"cmd.sketch_auto_constrain": "AutoConstrain",
"cmd.sketch_auto_constrain_from_datum": "AutoConstrain from datum",
"cmd.sketch_auto_project": "Auto-Project Edges on Reference",
"cmd.sketch_blend_curve": "Blend Curve",
"cmd.sketch_chain": "Chain",
"cmd.sketch_chamfer_distance_angle": "Distance and Angle Chamfer",
"cmd.sketch_chamfer_equal_distance": "Equal Distance Chamfer",
"cmd.sketch_chamfer_two_distance": "Two Distance Chamfer",
"cmd.sketch_circle_center": "Center Diameter Circle",
"cmd.sketch_circle_three_point": "3-Point Circle",
"cmd.sketch_circle_three_tangent": "3-Tangent Circle",
"cmd.sketch_circle_two_point": "2-Point Circle",
"cmd.sketch_circle_two_tangent": "2-Tangent Circle",
"cmd.sketch_constrainer": "Sketch Constrainer",
"cmd.sketch_constraint_fix": "Fix/UnFix",
"cmd.sketch_constraint_midpoint": "MidPoint",
"cmd.sketch_construction": "Normal/Construction",
"cmd.sketch_delete": "Delete Sketch Entities",
"cmd.sketch_dimension_text": "Move Dimension Text",
"cmd.sketch_drag": "Drag Sketch Geometry",
"cmd.sketch_drag_end": "End Sketch Drag",
"cmd.sketch_draw_dimensioned": "Draw Dimensioned Sketch Geometry",
"cmd.sketch_edit": "Edit Sketch",
"cmd.sketch_edit_text": "Edit Text",
"cmd.sketch_export_dxf": "Save As DXF",
"cmd.sketch_finish_auto_constrain": "Finish with AutoConstrain",
"cmd.sketch_fit_curves_to_section": "Fit Curves to Mesh Section",
"cmd.sketch_glyphs": "Constraint Glyphs",
"cmd.sketch_include_3d": "Include 3D Geometry",
"cmd.sketch_insert_dxf": "Insert DXF",
"cmd.sketch_insert_svg": "Insert SVG",
"cmd.sketch_inspect": "Inspect Sketch",
"cmd.sketch_intersection_curve": "Intersection Curve",
"cmd.sketch_isoparametric_curve": "Isoparametric Curve",
"cmd.sketch_line3d": "3D Line",
"cmd.sketch_line_midpoint": "Midpoint Line",
"cmd.sketch_move": "Move/Copy Sketch Entities",
"cmd.sketch_move3d": "Move 3D Point",
"cmd.sketch_move_point": "Drag Sketch Point",
"cmd.sketch_offset_break": "Break Offset Association",
"cmd.sketch_options": "Sketch Palette Options",
"cmd.sketch_point3d": "3D Point",
"cmd.sketch_polygon_circumscribed": "Circumscribed Polygon",
"cmd.sketch_polygon_edge": "Edge Polygon",
"cmd.sketch_polygon_inscribed": "Inscribed Polygon",
"cmd.sketch_project_to_surface": "Project To Surface",
"cmd.sketch_rectangle_center": "Center Rectangle",
"cmd.sketch_rectangle_rounded": "Rounded Rectangle",
"cmd.sketch_rectangle_three_point": "3-Point Rectangle",
"cmd.sketch_rectangle_two_point": "2-Point Rectangle",
"cmd.sketch_redefine": "Redefine Sketch Plane",
"cmd.sketch_slot_arc_center": "Center Point Arc Slot",
"cmd.sketch_slot_arc_three_point": "Three Point Arc Slot",
"cmd.sketch_slot_center_point": "Center Point Slot",
"cmd.sketch_slot_center_to_center": "Center to Center Slot",
"cmd.sketch_slot_overall": "Overall Slot",
"cmd.sketch_snap": "Snap Inference",
"cmd.sketch_solve": "Solve Sketch",
"cmd.sketch_spline3d": "3D Spline",
"cmd.sketch_spline_control_point": "Control Point Spline",
"cmd.sketch_spline_control_point_5": "Control Point Spline (Degree 5)",
"cmd.sketch_spline_fit_point": "Fit Point Spline",
"cmd.sketch_spun_profile": "Spun Profile",
"cmd.sketch_toggle_driven": "Toggle Driven Dimension",
"cmd.solid_align": "Align",
"cmd.solid_boundary_fill": "Boundary Fill",
"cmd.solid_bounding_solid": "Bounding Solid",
"cmd.solid_emboss": "Emboss",
"cmd.solid_move": "Move/Copy",
"cmd.solid_offset_face": "Offset Face",
"cmd.solid_press_pull": "Press Pull",
"cmd.solid_remove": "Remove",
"cmd.solid_replace_face": "Replace Face",
"cmd.solid_rib": "Rib",
"cmd.solid_split_face": "Split Face",
"cmd.solid_web": "Web",
"cmd.surface_patch": "Patch",
"cmd.surface_stitch": "Stitch",
"cmd.surface_thicken": "Thicken",
"cmd.surface_extrude": "Surface Extrude",
"cmd.surface_revolve": "Surface Revolve",
"cmd.surface_sweep": "Surface Sweep",
"cmd.surface_loft": "Surface Loft",
"cmd.surface_ruled": "Ruled Surface",
"cmd.surface_boundary": "Boundary Surface",
"cmd.surface_offset": "Offset Surface",
"cmd.surface_blend": "Blend Surface",
"cmd.surface_fillet": "Surface Fillet",
"cmd.surface_untrim": "Untrim",
"cmd.surface_unstitch": "Unstitch",
"cmd.surface_reverse_normal": "Reverse Normal",
"cmd.inspect_continuity": "Continuity Analysis",
"cmd.timeline_compute_all": "Compute All",
"cmd.timeline_copy": "Copy Features",
"cmd.timeline_dependents": "Feature Dependents",
"cmd.timeline_inspect": "Inspect Feature",
"cmd.timeline_paste": "Paste Features",
"cmd.timeline_redefine": "Edit Feature",
"cmd.timeline_rename": "Rename",
"cmd.timeline_reorder": "Reorder Feature",
"cmd.timeline_roll_to": "Roll History Marker",
"cmd.timeline_rollback": "Move Timeline Marker",
"cmd.timeline_suppress": "Suppress Feature",
"cmd.view_apply": "Restore Named View",
"cmd.view_delete": "Delete Named View",
"cmd.view_list": "List Named Views",
"cmd.view_rename": "Rename Named View",
"cmd.view_save": "New Named View",
"dialog.custom_axis": "↖ Custom Axis",
"dialog.edit_title": "EDIT {title}",
"dialog.select": "↖ Select",
"dialog.selected_count": "↖ {count} selected",
"dialog.step": "Step",
"dialog.selection_step": "{step} of {total}: Select {input}",
"dialog.selection_guidance": "Select an input to change it. Click selected objects to remove them; Ctrl/Cmd-click also toggles them. Review the preview, then press OK.",
"document.save_changes": "Save changes to {name}?",
"document.unsaved_changes": "{name} has unsaved changes. If you don't save, they are lost.",
"settings.msaa_applies_on_restart": "Anti-aliasing applies the next time SolidCraft starts",
"settings.msaa_reset": "{setting}: {requested}× isn't supported by this graphics device; it is now {reset}× (supported: {supported}).",
"settings.msaa_unsupported": "Not supported by this graphics device",
"settings.reset_intro": "These settings don't work on this computer and would keep SolidCraft from starting, so they were changed:",
"settings.reset_title": "Settings were changed",
"sketch.degrees_of_freedom": "{count} degrees of freedom",
"sketch.draw_curves": "Draw as {linetype}",
"sketch.make_selection": "Convert the selection to {linetype}",
"ui.1_degree_of_freedom": "1 degree of freedom",
"ui.3d_mouse_speed": "3D mouse speed",
"ui.3d_mouse_windows": "3D mouse (Windows)",
"ui.3d_sketch": "3D Sketch",
"ui.about": "About",
"ui.activate": "Activate",
"ui.active_component": "Active component",
"ui.agent_cursor": "Agent cursor",
"ui.agent_cursor_speed": "Agent cursor speed",
"ui.all": "All",
"ui.analysis": "Analysis",
"ui.angle": "Angle",
"ui.angle_decimals": "Angle decimals",
"ui.animate_view_changes": "Animate view changes",
"ui.animation": "Animation",
"ui.animation_speed": "Animation speed",
"ui.anti_aliasing": "Anti-aliasing",
"ui.apply": "Apply",
"ui.arc": "Arc",
"ui.assemble": "Assemble",
"ui.autosave_every": "Autosave every",
"ui.axes": "Axes",
"ui.axis": "Axis",
"ui.base_point": "Base point",
"ui.bodies": "Bodies",
"ui.box": "Box",
"ui.break": "Break",
"ui.cancel": "Cancel",
"ui.canvases": "Canvases",
"ui.center": "Center",
"ui.centerline": "Centerline",
"ui.chain": "Chain",
"ui.chamfer": "Chamfer",
"ui.chamfer_type": "Chamfer Type",
"ui.change_workspace": "Change workspace",
"ui.chord_length": "Chord Length",
"ui.curvature_g2": "Curvature (G2)",
"ui.equivalent_radius": "Equivalent Radius",
"ui.circle": "Circle",
"ui.circular_pattern": "Circular Pattern",
"ui.clear": "Clear",
"ui.clear_recent": "Clear Recent",
"ui.clear_the_selection": "Clear the selection",
"ui.click_selects_whole_bodies": "Click selects whole bodies",
"ui.click_then_press_key": "Click, then press the new key",
"ui.close": "Close",
"ui.coil": "Coil",
"ui.coincident": "Coincident",
"ui.collapse": "Collapse",
"ui.collinear": "Collinear",
"ui.combine": "Combine",
"ui.coming_later": "coming later",
"ui.concentric": "Concentric",
"ui.configurations": "Configurations",
"ui.conic_curve": "Conic Curve",
"ui.constant": "Constant",
"ui.constraints_conflict": "Constraints conflict",
"ui.construct": "Construct",
"ui.construction": "Construction",
"ui.context_menu": "Context menu",
"ui.copy": "Copy",
"ui.corner": "Corner",
"ui.corners": "Corners",
"ui.counterbore": "Counterbore",
"ui.countersink": "Countersink",
"ui.create": "Create",
"ui.create_copy": "Create Copy",
"ui.create_sketch": "Create Sketch",
"ui.current": "current",
"ui.curvature": "Curvature",
"ui.curves": "Curves",
"ui.cut": "Cut",
"ui.cylinder": "Cylinder",
"ui.dark": "Dark",
"ui.default_modelling_orientation": "Default modelling orientation",
"ui.degrees_of_freedom": "degrees of freedom",
"ui.delete": "Delete",
"ui.depth": "Depth",
"ui.design": "Design",
"ui.diameter": "Diameter",
"ui.dimension": "Dimension",
"ui.direction": "Direction",
"ui.distance": "Distance",
"ui.distance_1": "Distance 1",
"ui.distance_2": "Distance 2",
"ui.document_settings": "Document Settings",
"ui.don_t_save": "Don't Save",
"ui.draft": "Draft",
"ui.drawing": "Drawing",
"ui.drill_point": "Drill Point",
"ui.edit": "Edit",
"ui.edit_section_analysis": "Edit Section Analysis",
"ui.ellipse": "Ellipse",
"ui.end_radius": "End Radius",
"ui.entities": "Entities",
"ui.equal": "Equal",
"ui.expand": "Expand",
"ui.export_ellipsis": "Export...",
"ui.export_ellipsis_keymap": "Export…",
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
"ui.extend": "Extend",
"ui.extent_type": "Extent Type",
"ui.extrude": "Extrude",
"ui.factor": "Factor",
"ui.feature_history": "Feature history",
"ui.feature_options": "Feature Options",
"ui.features": "Features",
"ui.file": "File",
"ui.fillet": "Fillet",
"ui.filter_commands_or_keys": "Filter commands or keys",
"ui.finish_sketch": "Finish Sketch",
"ui.fix_unfix": "Fix/Unfix",
"ui.flat": "Flat",
"ui.flip": "Flip",
"ui.front": "Front",
"ui.fully_constrained": "Fully constrained",
"ui.general": "General",
"ui.graphics": "Graphics",
"ui.ground_shadow": "Ground shadow",
"ui.heading_add_ins": "ADD-INS",
"ui.heading_assemble": "ASSEMBLE",
"ui.heading_box": "BOX",
"ui.heading_chamfer": "CHAMFER",
"ui.heading_circular_pattern": "CIRCULAR PATTERN",
"ui.heading_coil": "COIL",
"ui.heading_combine": "COMBINE",
"ui.heading_configure": "CONFIGURE",
"ui.heading_constraints": "CONSTRAINTS",
"ui.heading_construct": "CONSTRUCT",
"ui.heading_create": "CREATE",
"ui.heading_create_sketch": "CREATE SKETCH",
"ui.heading_cylinder": "CYLINDER",
"ui.heading_design": "DESIGN",
"ui.heading_draft": "DRAFT",
"ui.heading_edit": "EDIT",
"ui.heading_extrude": "EXTRUDE",
"ui.heading_fillet": "FILLET",
"ui.heading_finish_sketch": "FINISH SKETCH",
"ui.heading_front": "FRONT",
"ui.heading_hole": "HOLE",
"ui.heading_home": "HOME",
"ui.heading_insert": "INSERT",
"ui.heading_inspect": "INSPECT",
"ui.heading_loft": "LOFT",
"ui.heading_make": "MAKE",
"ui.heading_measure": "MEASURE",
"ui.heading_mesh": "MESH",
"ui.heading_mirror": "MIRROR",
"ui.heading_modify": "MODIFY",
"ui.heading_move": "MOVE",
"ui.heading_offset": "OFFSET",
"ui.heading_move_copy": "MOVE/COPY",
"ui.heading_offset_plane": "OFFSET PLANE",
"ui.heading_ok": "OK",
"ui.heading_parameters": "PARAMETERS",
"ui.heading_pattern_on_path": "PATTERN ON PATH",
"ui.heading_pipe": "PIPE",
"ui.heading_plane_at_angle": "PLANE AT ANGLE",
"ui.heading_plastic": "PLASTIC",
"ui.heading_prepare": "PREPARE",
"ui.heading_rectangular_pattern": "RECTANGULAR PATTERN",
"ui.heading_revolve": "REVOLVE",
"ui.heading_right": "RIGHT",
"ui.heading_scale": "SCALE",
"ui.heading_section_analysis": "SECTION ANALYSIS",
"ui.heading_select": "SELECT",
"ui.heading_sheet_metal": "SHEET METAL",
"ui.heading_shell": "SHELL",
"ui.heading_sketch": "SKETCH",
"ui.heading_sketch_palette": "SKETCH PALETTE",
"ui.heading_sketch_scale": "SKETCH SCALE",
"ui.heading_solid": "SOLID",
"ui.heading_sphere": "SPHERE",
"ui.heading_split_body": "SPLIT BODY",
"ui.heading_surface": "SURFACE",
"ui.heading_sweep": "SWEEP",
"ui.heading_thread": "THREAD",
"ui.heading_top": "TOP",
"ui.heading_torus": "TORUS",
"ui.heading_utilities": "UTILITIES",
"ui.heading_utility": "UTILITY",
"ui.help": "Help",
"ui.hide": "Hide",
"ui.hole": "Hole",
"ui.hole_tap_type": "Hole Tap Type",
"ui.hole_type": "Hole Type",
"ui.home": "Home",
"ui.horizontal_vertical": "Horizontal/Vertical",
"ui.import_ellipsis": "Import…",
"ui.insert": "Insert",
"ui.inspect": "Inspect",
"ui.intersect": "Intersect",
"ui.join": "Join",
"ui.joints": "Joints",
"ui.keyboard_shortcuts": "Keyboard Shortcuts",
"ui.language": "Language",
"ui.length_decimals": "Length decimals",
"ui.light": "Light",
"ui.line": "Line",
"ui.linetype": "Linetype",
"ui.loft": "Loft",
"ui.look_at": "Look At",
"ui.look_at_anim_desc": "Axis view, Look At, Home and Fit",
"ui.manufacture": "Manufacture",
"ui.measure": "Measure",
"ui.midpoint": "Midpoint",
"ui.mirror": "Mirror",
"ui.mirror_line": "Mirror line",
"ui.modify": "Modify",
"ui.move": "Move",
"ui.multiple": "Multiple",
"ui.named_views": "Named Views",
"ui.navigation": "Navigation",
"ui.needs_design": "{command}: open a design first",
"ui.new_body": "New Body",
"ui.new_design": "New Design",
"ui.normal": "Normal",
"ui.object_type": "Object Type",
"ui.objects": "Objects",
"ui.offer_to_stand_up_y_up_files": "Offer to stand up Y-up files",
"ui.offset": "Offset",
"ui.offset_plane": "Offset Plane",
"ui.one_side": "One Side",
"ui.open": "Open",
"ui.open_ellipsis": "Open...",
"ui.open_preferences": "Open Preferences",
"ui.open_recent": "Open Recent",
"ui.operation": "Operation",
"ui.options": "Options",
"ui.orbit_around": "Orbit around",
"ui.origin": "Origin",
"ui.palette_points": "Points",
"ui.palette_profile": "Profile",
"ui.pan_zoom_orbit": "Pan, zoom, orbit",
"ui.parallel": "Parallel",
"ui.parameters": "Parameters",
"ui.paste": "Paste",
"ui.path": "Path",
"ui.pattern_on_path": "Pattern on Path",
"ui.perpendicular": "Perpendicular",
"ui.perspective_view": "Perspective view",
"ui.pipe": "Pipe",
"ui.placement": "Placement",
"ui.plane": "Plane",
"ui.plane_at_angle": "Plane at Angle",
"ui.point": "Point",
"ui.point_under_the_cursor": "Point under the cursor",
"ui.points": "Points",
"ui.polygon": "Polygon",
"ui.preferences": "Preferences",
"ui.press_a_key": "press a key…",
"ui.profile": "Profile",
"ui.profile_plane": "Profile Plane",
"ui.profiles": "Profiles",
"ui.project": "Project",
"ui.project_face_edges_sketch": "Project face edges into new sketches",
"ui.quantity": "Quantity",
"ui.quantity_2": "Quantity 2",
"ui.quit": "Quit",
"ui.radial_context_menu": "Radial context menu",
"ui.radius": "Radius",
"ui.radius_type": "Radius Type",
"ui.rectangle": "Rectangle",
"ui.rectangular_pattern": "Rectangular Pattern",
"ui.redo": "Redo",
"ui.render": "Render",
"ui.renderer": "Renderer",
"ui.reset": "Reset",
"ui.reset_all": "Reset All",
"ui.reset_the_settings_on_this_page": "Reset the settings on this page",
"ui.restore_defaults": "Restore Defaults",
"ui.reverse_zoom_direction": "Reverse zoom direction",
"ui.revolve": "Revolve",
"ui.right": "Right",
"ui.save": "Save",
"ui.save_as": "Save As",
"ui.save_as_ellipsis": "Save As...",
"ui.scale": "Scale",
"ui.section_analysis": "Section Analysis",
"ui.select": "Select",
"ui.select_other_hint": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there.",
"ui.selection": "Selection",
"ui.shell": "Shell",
"ui.show": "Show",
"ui.show_constraints": "Constraints",
"ui.show_dimensions": "Dimensions",
"ui.show_points": "Show Points",
"ui.show_profile": "Show Profile",
"ui.show_projected_geometries": "Projected Geometries",
"ui.simple": "Simple",
"ui.simulation": "Simulation",
"ui.single": "Single",
"ui.sketch": "Sketch",
"ui.sketch_dimension": "Sketch Dimension",
"ui.sketch_grid": "Sketch Grid",
"ui.sketch_scale": "Sketch Scale",
"ui.sketch_status": "Sketch Status",
"ui.sketches": "Sketches",
"ui.slice": "Slice",
"ui.slot": "Slot",
"ui.smooth": "Smooth",
"ui.smooth_wheel_zoom": "Smooth wheel zoom",
"ui.smooth_wheel_zoom_tip": "Turn off to apply each wheel event immediately, without a smoothing tail",
"ui.snap": "Snap",
"ui.space_mouse_hint": "Move and rotate the view together; sketches use pan and zoom only.",
"ui.space_mouse_navigate": "Navigate with SpaceNavigator / SpaceMouse",
"ui.spacing": "Spacing",
"ui.spacing_2": "Spacing 2",
"ui.sphere": "Sphere",
"ui.spline": "Spline",
"ui.split_body": "Split Body",
"ui.stand_up_tip": "After a STEP or IGES file opens with Z up, offer a quarter turn about X",
"ui.start": "Start",
"ui.start_radius": "Start Radius",
"ui.sweep": "Sweep",
"ui.symmetric": "Symmetric",
"ui.symmetry": "Symmetry",
"ui.system": "System",
"ui.tangent": "Tangent",
"ui.taper_angle": "Taper Angle",
"ui.tapped": "Tapped",
"ui.text": "Text",
"ui.theme": "Theme",
"ui.thickness": "Thickness",
"ui.thread": "Thread",
"ui.timeline_bottom": "Bottom timeline",
"ui.timeline_left_note": "The left layout shows the rollback bar above the parts and scene browser.",
"ui.timeline_left_sidebar": "Left sidebar",
"ui.tip_angle": "Tip Angle",
"ui.to_object": "To Object",
"ui.top": "Top",
"ui.torus": "Torus",
"ui.trackpad_pan_tip": "Trackpad gestures pan (off: they zoom like the wheel); mouse wheel zooms, thumb wheel pans sideways, pinch always zooms",
"ui.trim": "Trim",
"ui.two_finger_scroll_pans": "Two-finger scroll pans",
"ui.two_sides": "Two Sides",
"ui.ui_scale": "UI scale",
"ui.undo": "Undo",
"ui.units": "Units",
"ui.units_of_new_designs": "Units of new designs",
"ui.untitled": "Untitled",
"ui.up_axis_note": "Which axis points up in the views (axis view, Home, Look At). Designs are unchanged.",
"ui.variable": "Variable",
"ui.view_centre": "View centre",
"ui.visual_shaded_hidden_edges": "Shaded with hidden edges",
"ui.visual_shaded_with_edges": "Shaded with edges",
"ui.visual_style": "Visual style",
"ui.visual_wireframe": "Wireframe",
"ui.workspace": "workspace",
"ui.x_distance": "X Distance",
"ui.y_distance": "Y Distance",
"ui.y_up": "Y up",
"ui.z_up": "Z up",
"ui.zoom_to_cursor_tip": "The wheel zooms toward the point under the cursor (off: toward the view's centre)",
"ui.zoom_to_the_cursor": "Zoom to the cursor",
"A new configuration, a copy of the active one": "A new configuration, a copy of the active one",
"About SolidCraft": "About SolidCraft",
"Active": "Active",
"Active rule": "Active rule",
"Add": "Add",
"Add Key": "Add Key",
"Add Mid Radius": "Add Mid Radius",
"Add Row": "Add Row",
"All Sketch Points": "All Sketch Points",
"All open designs": "All open designs",
"Allowed expressions, separated by semicolons": "Allowed expressions, separated by semicolons",
"Also deletes": "Also deletes",
"Angle 2": "Angle 2",
"Animate": "Animate",
"Appearance name": "Appearance name",
"Applied Radius": "Applied Radius",
"Apply To": "Apply To",
"Area": "Area",
"Attachment": "Attachment",
"BROWSER": "BROWSER",
"Bend Line Position": "Bend Line Position",
"Bend Position": "Bend Position",
"Bend Radius": "Bend Radius",
"Bends": "Bends",
"Body": "Body",
"Bounding box": "Bounding box",
"CSV or JSON: updates existing parameters, adds new ones": "CSV or JSON: updates existing parameters, adds new ones",
"Capture All Positions": "Capture All Positions",
"Catch Direction": "Catch Direction",
"Cells": "Cells",
"Center X": "Center X",
"Center Y": "Center Y",
"Center of mass": "Center of mass",
"Chamfer Type": "Chamfer Type",
"Check collisions": "Check collisions",
"Checked": "Checked",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.",
"Close draft": "Close draft",
"Collisions": "Collisions",
"Colour": "Colour",
"Component1": "Component1",
"Configuration": "Configuration",
"Configuration rules": "Configuration rules",
"Contact": "Contact",
"Copy all licences": "Copy all licences",
"Copy diagnostic information": "Copy diagnostic information",
"Copy report": "Copy report",
"Corner Relief Shape": "Corner Relief Shape",
"Counterbore depth": "Counterbore depth",
"Countersink angle": "Countersink angle",
"Create Issue — review draft": "Create Issue — review draft",
"Customize Toolbar": "Customize Toolbar",
"Delete all": "Delete all",
"Delete rule": "Delete rule",
"Delete the row": "Delete the row",
"Delete the set": "Delete the set",
"Delete this parameter": "Delete this parameter",
"Density": "Density",
"Designs you open or save appear here.": "Designs you open or save appear here.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Determined by To Object. Choose Angle to edit the angular extent.",
"Discard": "Discard",
"Double-click to rename": "Double-click to rename",
"Drive Joint": "Drive Joint",
"Edit Profile Sketch": "Edit Profile Sketch",
"Enable rule": "Enable rule",
"Enabled": "Enabled",
"Explode": "Explode",
"Extents": "Extents",
"Face attachment lost": "Face attachment lost",
"Faces / edges / vertices": "Faces / edges / vertices",
"Favourite": "Favourite",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Files from many CAD programs have Y up, so they open lying on their back here (Z up).",
"Filter": "Filter",
"Find in Browser": "Find in Browser",
"Flat size": "Flat size",
"Follow camera (Look At sketches)": "Follow camera (Look At sketches)",
"Font": "Font",
"Full Length": "Full Length",
"Gap": "Gap",
"Grid": "Grid",
"Height": "Height",
"Home view": "Home view",
"In This Design": "In This Design",
"Interferes": "Interferes",
"Issue title": "Issue title",
"Joint Origin1": "Joint Origin1",
"Keep Tools": "Keep Tools",
"Later": "Later",
"Leave it": "Leave it",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Legacy face attachment. Rebinding to a certified face creates an exact attachment.",
"Length": "Length",
"Length units": "Length units",
"Library": "Library",
"Loop": "Loop",
"Margin": "Margin",
"Mass": "Mass",
"Material": "Material",
"Mid Radius": "Mid Radius",
"Modeled": "Modeled",
"More toolbar panels": "More toolbar panels",
"Motion": "Motion",
"Move Type": "Move Type",
"Move earlier": "Move earlier",
"Move later": "Move later",
"Name": "Name",
"No crash reports.": "No crash reports.",
"No matching appearances": "No matching appearances",
"Not available yet": "Not available yet",
"Opacity": "Opacity",
"Open GitHub draft": "Open GitHub draft",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.",
"Other side": "Other side",
"Parameter values": "Parameter values",
"Per Edge": "Per Edge",
"Pick the tool bodies": "Pick the tool bodies",
"Place at the origin instead": "Place at the origin instead",
"Playback": "Playback",
"Position": "Position",
"Position (0 to 1)": "Position (0 to 1)",
"Position along the edge, 0 at the start end, 1 at the other": "Position along the edge, 0 at the start end, 1 at the other",
"Preview leaves the current design unchanged.": "Preview leaves the current design unchanged.",
"RECENT": "RECENT",
"Radius Fit": "Radius Fit",
"Ratio": "Ratio",
"Recover": "Recover",
"Recover unsaved design?": "Recover unsaved design?",
"Remove Tools": "Remove Tools",
"Remove from list": "Remove from list",
"Remove private design details or paths before sharing.": "Remove private design details or paths before sharing.",
"Remove the column": "Remove the column",
"Remove the track": "Remove the track",
"Remove this point": "Remove this point",
"Report a bug — review a screenshot and open a GitHub draft": "Report a bug — review a screenshot and open a GitHub draft",
"Reset Toolbar": "Reset Toolbar",
"Restore the inherited component or physical material appearance": "Restore the inherited component or physical material appearance",
"Result": "Result",
"Retake screenshot": "Retake screenshot",
"Reverse": "Reverse",
"Reverse Direction": "Reverse Direction",
"Revert": "Revert",
"Revert All Positions": "Revert All Positions",
"Review the report and screenshot before opening GitHub.": "Review the report and screenshot before opening GitHub.",
"Rim Edge": "Rim Edge",
"Roll History Marker Here": "Roll History Marker Here",
"Rule": "Rule",
"Saved preview. Geometry is being checked before editing.": "Saved preview. Geometry is being checked before editing.",
"Scale factor": "Scale factor",
"Search": "Search",
"Search commands": "Search commands",
"Select bodies (a face picks its body)": "Select bodies (a face picks its body)",
"Select bodies or faces instead": "Select bodies or faces instead",
"Selection filter": "Selection filter",
"Semantic faces share their appearance across instances.": "Semantic faces share their appearance across instances.",
"Shape": "Shape",
"Show Agent Cursor": "Show Agent Cursor",
"Show in folder": "Show in folder",
"Size": "Size",
"Sketch plane has no body-face attachment.": "Sketch plane has no body-face attachment.",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft closed before these designs were saved. Their last autosave can be opened.",
"Speed": "Speed",
"Stand it up": "Stand it up",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Stand it up: a quarter turn about X, as one Move feature you can undo.",
"Stand this part up?": "Stand this part up?",
"Step": "Step",
"Steps": "Steps",
"Suppressed": "Suppressed",
"Swap Ends": "Swap Ends",
"Tangent Chain": "Tangent Chain",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "The dashed sketch is a saved plane guide. Select a new face to repair the attachment.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "The saved author occurrence is missing. Select a valid author placement before rebinding.",
"Then": "Then",
"These features will be deleted:": "These features will be deleted:",
"These features will fail:": "These features will fail:",
"This child is shared. Deleting it removes it from every copy of its parent.": "This child is shared. Deleting it removes it from every copy of its parent.",
"This child is shared. Position changes apply to every copy of its parent.": "This child is shared. Position changes apply to every copy of its parent.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.",
"To Offset": "To Offset",
"Tolerance": "Tolerance",
"Type": "Type",
"Updating the preview": "Updating the preview",
"Use fixed plane": "Use fixed plane",
"VIEW": "VIEW",
"Value": "Value",
"Volume": "Volume",
"Wall": "Wall",
"When": "When",
"Width": "Width",
"Will fail": "Will fail",
"angle, height": "angle, height",
"command name": "command name",
"expression, e.g. 25 mm": "expression, e.g. 25 mm",
"fits the face, e.g. M8": "fits the face, e.g. M8",
"from the rule": "from the rule",
"needs two joints": "needs two joints",
"new configuration": "new configuration",
"new name": "new name",
"new parameter": "new parameter",
"no interference": "no interference",
"no joint that moves yet": "no joint that moves yet",
"no picture": "no picture",
"pick a hole to seat it, or OK places it at the origin": "pick a hole to seat it, or OK places it at the origin",
"pick one or two vertices, edges or faces": "pick one or two vertices, edges or faces",
"the parts library is not available": "the parts library is not available",
"to the next face": "to the next face",
"Revert Position": "Revert Position",
"Construction": "Construction",
"Faces": "Faces",
"Edges": "Edges",
"Vertices": "Vertices",
"Sketch geometry and profiles": "Sketch geometry and profiles",
"Sketch curves": "Sketch curves",
"Sketch points": "Sketch points",
"Sketch dimensions": "Sketch dimensions",
"Sketch constraints": "Sketch constraints",
"0 turns it off": "0 turns it off",
"Show the pointer an AI agent moves (#35)": "Show the pointer an AI agent moves (#35)",
"Follow the camera (Look At sketches)": "Follow the camera (Look At sketches)",
"Flat": "Flat",
"Save Rule": "Save Rule",
"Preview Row": "Preview Row",
"Add condition": "Add condition",
"Apply to selection": "Apply to selection",
"Remove override": "Remove override",
"Copy screenshot": "Copy screenshot",
"Remove screenshot": "Remove screenshot",
"Use active author placement": "Use active author placement",
"Reattach selected face": "Reattach selected face",
"Use saved fixed plane": "Use saved fixed plane",
"Use selected face as fixed plane": "Use selected face as fixed plane",
"View navigator": "View navigator",
"Signed axes": "Signed axes",
"Cube": "Cube",
"Middle double-click": "Middle double-click",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).",
"cmd.render_backends": "Render Backends",
"cmd.view_navigate": "Navigate View",
"ui.animateJoint": "Animate Joint",
"ui.calibrate": "Calibrate",
"ui.customizeToolbar": "Customize Toolbar…",
"ui.editJointLimits": "Edit Joint Limits",
"ui.exportDxf": "Export DXF…",
"ui.findInTimeline": "Find in Timeline",
"ui.findInWindow": "Find in Window",
"ui.fit": "Fit",
"ui.groupSelected": "Group Selected",
"ui.hideAxes": "Hide Axes",
"ui.hideDimension": "Hide Dimension",
"ui.hideOrigin": "Hide Origin",
"ui.hidePlanes": "Hide Planes",
"ui.hideProfile": "Hide Profile",
"ui.homeView": "Home View",
"ui.isolate": "Isolate",
"ui.lock": "Lock",
"ui.newSketchGroup": "New Sketch Group",
"ui.normalConstruction": "Normal / Construction",
"ui.orbit": "Orbit",
"ui.heading_position": "POSITION",
"ui.pan": "Pan",
"ui.pinToShortcuts": "Pin to Shortcuts",
"ui.pinToToolbar": "Pin to Toolbar",
"ui.properties": "Properties",
"ui.removeSection": "Remove Section",
"ui.removeFromToolbar": "Remove from Toolbar",
"ui.repeat": "Repeat",
"ui.report_a_bug": "Report a bug",
"ui.restore": "Restore",
"ui.heading_setup": "SETUP",
"ui.selectOther": "Select Other…",
"ui.selectTangentChain": "Select Tangent Chain",
"ui.showAll": "Show All",
"ui.showAllBodies": "Show All Bodies",
"ui.showAxes": "Show Axes",
"ui.showDimension": "Show Dimension",
"ui.showOrigin": "Show Origin",
"ui.showPlanes": "Show Planes",
"ui.suppress": "Suppress",
"ui.command_palette_hint": "Type a command, e.g. {example} — or press S to search",
"ui.unground": "Unground",
"ui.unisolate": "Unisolate",
"ui.unlock": "Unlock",
"ui.unsuppress": "Unsuppress",
"ui.updateToCurrentView": "Update to Current View",
"ui.zoom": "Zoom",
"ui.command_done": "{command} done",
"timeline.group": "New Feature Folder",
"timeline.ungroup": "Remove Feature Folders",
"timeline.rename_group": "Rename Feature Folder",
"timeline.move_to_group": "Move Features to Folder",
"timeline.move_many": "Move Timeline Items",
"timeline.reorder_many": "Reorder Features",
"ui.show_construction_geometries": "Construction Geometries",
"ui.total_angle": "Total angle",
"sketch_text.builtin_font": "{family} (built-in)",
"sketch_text.font_search": "Search fonts",
"sketch_text.loading_fonts": "Looking for installed fonts…",
"sketch_text.use_font_file": "Use the font file {path}",
"sketch_text.bold": "Bold",
"sketch_text.italic": "Italic",
"sketch_text.align_left": "Left",
"sketch_text.align_center": "Center",
"sketch_text.align_right": "Right",
"sketch_text.path_hint": "A curve of the sketch the text follows: click here, then the curve",
"sketch_text.path_clear": "Take the text off its curve",
"Style": "Style",
"Alignment": "Alignment",
"Letter Spacing": "Letter Spacing",
"Line Spacing": "Line Spacing",
"Path": "Path",
"sketch_text.on_path": "The text follows its path: the curve places it",
"cmd.material_properties_validate":"Validate Physical Material Data"
}
+961
View File
@@ -0,0 +1,961 @@
{
"browser.body_name": "ボディ{number}",
"browser.component_name": "コンポーネント{number}",
"browser.parameters": "パラメータ ({count})",
"browser.precision": "精度: {precision}",
"browser.sketch_name": "スケッチ{number}",
"browser.units": "単位: {units}",
"cmd.appearance_assign": "外観",
"cmd.appearance_library": "外観ライブラリ",
"cmd.appearance_list": "外観を一覧表示",
"cmd.body_rename": "ボディの名前を変更",
"cmd.browser_group": "新規グループ",
"cmd.browser_move": "ブラウザ項目を移動",
"cmd.browser_order": "ブラウザ項目の並び順を変更",
"cmd.browser_rename_group": "グループの名前を変更",
"cmd.browser_ungroup": "グループ解除",
"cmd.browser_visibility": "表示/非表示",
"cmd.canvas_calibrate": "キャンバスの尺度を設定",
"cmd.canvas_decal": "デカール",
"cmd.canvas_delete": "キャンバスを削除",
"cmd.canvas_edit": "キャンバスを編集",
"cmd.canvas_insert": "キャンバス",
"cmd.component_activate": "コンポーネントをアクティブ化",
"cmd.component_break_link": "リンクを解除",
"cmd.component_capture_position": "位置をキャプチャ",
"cmd.component_create": "新規コンポーネント",
"cmd.component_delete": "コンポーネントを削除",
"cmd.component_from_bodies": "ボディからコンポーネントを作成",
"cmd.component_insert_design": "デザインを挿入",
"cmd.component_links": "リンクを一覧表示",
"cmd.component_list": "コンポーネントを一覧表示",
"cmd.component_move_bodies": "ボディをコンポーネントに移動",
"cmd.component_move_sketches": "スケッチをコンポーネントに移動",
"cmd.component_paste_new": "新規として貼り付け",
"cmd.component_properties": "コンポーネントの物理的プロパティ",
"cmd.component_rename": "コンポーネントの名前を変更",
"cmd.component_revert_position": "位置を元に戻す",
"cmd.component_update_links": "リンクを更新",
"cmd.config_activate": "構成をアクティブ化",
"cmd.config_column": "構成列",
"cmd.config_configure": "構成を設定",
"cmd.config_preview": "構成をプレビュー",
"cmd.config_row": "構成行",
"cmd.config_rule": "構成ルール",
"cmd.config_rules": "構成ルール一覧",
"cmd.config_table": "構成テーブルを表示",
"cmd.construct_axis_edge": "エッジを通る軸",
"cmd.construct_axis_normal_to_face": "面に垂直な軸",
"cmd.construct_axis_through_cylinder": "円柱/円錐/トーラスを通る軸",
"cmd.construct_axis_two_planes": "2平面を通る軸",
"cmd.construct_axis_two_points": "2点を通る軸",
"cmd.construct_plane_along_path": "パスに沿った平面",
"cmd.construct_plane_midplane": "中間平面",
"cmd.construct_plane_perpendicular": "垂直平面",
"cmd.construct_plane_tangent": "正接平面",
"cmd.construct_plane_three_points": "3点を通る平面",
"cmd.construct_plane_two_edges": "2エッジを通る平面",
"cmd.construct_point_along_path": "パスに沿った点",
"cmd.construct_point_center": "円/球/トーラスの中心点",
"cmd.construct_point_edge_plane": "エッジと平面の交点",
"cmd.construct_point_three_planes": "3平面の交点",
"cmd.construct_point_two_edges": "2エッジの交点",
"cmd.construct_point_vertex": "頂点上の点",
"cmd.contact_check": "接触を確認",
"cmd.contact_create": "新規接触セット",
"cmd.contact_disable_all": "接触を無効化",
"cmd.contact_edit": "接触セットを編集",
"cmd.contact_enable_all": "すべての接触を有効化",
"cmd.contact_enable_sets": "接触セットを有効化",
"cmd.contact_list": "接触セットを一覧表示",
"cmd.diagnostics_crash_report": "クラッシュレポート",
"cmd.diagnostics_crash_reports": "クラッシュレポート一覧",
"cmd.diagnostics_crash_reports_clear": "クラッシュレポートを消去",
"cmd.diagnostics_crash_test": "クラッシュテスト",
"cmd.doc_recover": "デザインを復元",
"cmd.doc_recovery_discard": "復元デザインを破棄",
"cmd.doc_recovery_list": "復元可能なデザイン",
"cmd.document_inspect": "デザインを検査",
"cmd.document_units": "デザインの単位を変更",
"cmd.edit_checkpoint": "チェックポイント",
"cmd.edit_restore_checkpoint": "チェックポイントを復元",
"cmd.engine_commands": "コマンドを一覧表示",
"cmd.explode_create": "分解図",
"cmd.explode_delete": "分解図を削除",
"cmd.explode_list": "分解図を一覧表示",
"cmd.explode_show": "分解図を表示",
"cmd.expr_evaluate": "式を評価",
"cmd.face_delete": "面を削除",
"cmd.file_export": "エクスポート",
"cmd.file_insert_mesh": "メッシュを挿入",
"cmd.file_insert_step": "STEP を挿入",
"cmd.file_save_mesh": "メッシュとして保存",
"cmd.inspect_accessibility": "アクセシビリティ解析",
"cmd.inspect_center_of_mass": "重心",
"cmd.inspect_curvature_comb": "曲率コーム解析",
"cmd.inspect_curvature_map": "曲率マップ解析",
"cmd.inspect_draft": "勾配解析",
"cmd.inspect_environment_map": "環境マップ解析",
"cmd.inspect_interference": "干渉",
"cmd.inspect_isocurve": "アイソカーブ解析",
"cmd.inspect_minimum_radius": "最小半径解析",
"cmd.inspect_zebra": "ゼブラ解析",
"cmd.joint_as_built": "現位置ジョイント",
"cmd.joint_create": "ジョイント",
"cmd.joint_delete": "ジョイントを削除",
"cmd.joint_drive": "ジョイントを駆動",
"cmd.joint_edit": "ジョイントを編集",
"cmd.joint_limits": "ジョイントの制限",
"cmd.joint_list": "ジョイントを一覧表示",
"cmd.joint_motion_link": "モーション リンク",
"cmd.joint_origin": "ジョイント原点",
"cmd.joint_rigid_group": "リジッド グループ",
"cmd.joint_solve": "ジョイントを解決",
"cmd.material_assign": "物理マテリアル",
"cmd.material_create": "新規物理マテリアル",
"cmd.material_delete": "物理マテリアルを削除",
"cmd.material_edit": "物理マテリアルを編集",
"cmd.material_list": "マテリアルを一覧表示",
"cmd.mesh_convert": "メッシュを変換",
"cmd.model_edges": "エッジを一覧表示",
"cmd.model_faces": "面を一覧表示",
"cmd.model_threads": "ねじを一覧表示",
"cmd.motion_delete": "モーション スタディを削除",
"cmd.motion_export": "モーション スタディをエクスポート",
"cmd.motion_list": "モーション スタディを一覧表示",
"cmd.motion_play": "モーション スタディを再生",
"cmd.motion_study": "モーション スタディ",
"cmd.occurrence_copy": "貼り付け(インスタンス)",
"cmd.occurrence_ground": "空間固定",
"cmd.occurrence_move": "オカレンスを移動",
"cmd.occurrence_properties": "オカレンスの物理的プロパティ",
"cmd.parameters_add": "ユーザー パラメータを追加",
"cmd.parameters_change": "パラメータを変更",
"cmd.parameters_comment": "パラメータにコメントを付与",
"cmd.parameters_complete": "式を補完",
"cmd.parameters_delete": "パラメータを削除",
"cmd.parameters_export": "パラメータをエクスポート",
"cmd.parameters_favorite": "お気に入りパラメータ",
"cmd.parameters_graph": "パラメータ グラフ",
"cmd.parameters_import": "パラメータをインポート",
"cmd.parameters_list": "パラメータを一覧表示",
"cmd.parameters_rename": "パラメータの名前を変更",
"cmd.parameters_users": "パラメータの参照先",
"cmd.parts_fastener": "ファスナーを挿入",
"cmd.parts_insert": "パーツを挿入",
"cmd.parts_library": "標準パーツ",
"cmd.parts_resize": "パーツ サイズを変更",
"cmd.plastic_assign_rule": "プラスチック ルールを割り当て",
"cmd.plastic_boss": "ボス",
"cmd.plastic_lip": "リップ",
"cmd.plastic_manage_rules": "プラスチック ルールを管理",
"cmd.plastic_rest": "レスト",
"cmd.plastic_rules": "プラスチック ルールを一覧表示",
"cmd.plastic_snap_fit": "スナップ フィット",
"cmd.select_clear": "選択をクリア",
"cmd.sheet_convert": "板金に変換",
"cmd.sheet_export_bend_table": "曲げテーブルをエクスポート",
"cmd.sheet_export_dxf": "展開図を DXF としてエクスポート",
"cmd.sheet_flange": "フランジ",
"cmd.sheet_flat_pattern": "展開図を作成",
"cmd.sheet_fold": "折り曲げ",
"cmd.sheet_hem": "ヘミング",
"cmd.sheet_manage_rules": "板金ルール",
"cmd.sheet_refold": "折り戻し",
"cmd.sheet_rules": "板金ルールを一覧表示",
"cmd.sheet_unfold": "展開",
"cmd.sketch_arc_center_point": "中心点円弧",
"cmd.sketch_arc_tangent": "正接円弧",
"cmd.sketch_arc_three_point": "3点指定円弧",
"cmd.sketch_auto_constrain": "自動拘束",
"cmd.sketch_auto_constrain_from_datum": "基準から自動拘束",
"cmd.sketch_auto_project": "参照上のエッジを自動投影",
"cmd.sketch_blend_curve": "ブレンド曲線",
"cmd.sketch_chain": "チェーン",
"cmd.sketch_chamfer_distance_angle": "距離と角度の面取り",
"cmd.sketch_chamfer_equal_distance": "等距離面取り",
"cmd.sketch_chamfer_two_distance": "2距離面取り",
"cmd.sketch_circle_center": "中心と直径で指定した円",
"cmd.sketch_circle_three_point": "3点指定円",
"cmd.sketch_circle_three_tangent": "3接線指定の円",
"cmd.sketch_circle_two_point": "2点指定円",
"cmd.sketch_circle_two_tangent": "2接線指定の円",
"cmd.sketch_constrainer": "スケッチ拘束ツール",
"cmd.sketch_constraint_fix": "固定/固定解除",
"cmd.sketch_constraint_midpoint": "中点",
"cmd.sketch_construction": "標準/コンストラクション",
"cmd.sketch_delete": "スケッチ要素を削除",
"cmd.sketch_dimension_text": "寸法テキストを移動",
"cmd.sketch_drag": "スケッチ ジオメトリをドラッグ",
"cmd.sketch_drag_end": "スケッチのドラッグを終了",
"cmd.sketch_draw_dimensioned": "寸法付きスケッチ ジオメトリを描画",
"cmd.sketch_edit": "スケッチを編集",
"cmd.sketch_edit_text": "テキストを編集",
"cmd.sketch_export_dxf": "DXF として保存",
"cmd.sketch_finish_auto_constrain": "自動拘束で終了",
"cmd.sketch_fit_curves_to_section": "メッシュ断面に曲線をフィット",
"cmd.sketch_glyphs": "拘束記号",
"cmd.sketch_include_3d": "3D ジオメトリを含める",
"cmd.sketch_insert_dxf": "DXF を挿入",
"cmd.sketch_insert_svg": "SVG を挿入",
"cmd.sketch_inspect": "スケッチを検査",
"cmd.sketch_intersection_curve": "交差曲線",
"cmd.sketch_isoparametric_curve": "アイソパラメトリック曲線",
"cmd.sketch_line3d": "3D 線分",
"cmd.sketch_line_midpoint": "中点線分",
"cmd.sketch_move": "スケッチ要素を移動/コピー",
"cmd.sketch_move3d": "3D 点を移動",
"cmd.sketch_move_point": "スケッチ点をドラッグ",
"cmd.sketch_offset_break": "オフセット関連付けを解除",
"cmd.sketch_options": "スケッチ パレット オプション",
"cmd.sketch_point3d": "3D 点",
"cmd.sketch_polygon_circumscribed": "外接ポリゴン",
"cmd.sketch_polygon_edge": "エッジ ポリゴン",
"cmd.sketch_polygon_inscribed": "内接ポリゴン",
"cmd.sketch_project_to_surface": "サーフェスに投影",
"cmd.sketch_rectangle_center": "中心指定の長方形",
"cmd.sketch_rectangle_rounded": "角丸長方形",
"cmd.sketch_rectangle_three_point": "3点指定長方形",
"cmd.sketch_rectangle_two_point": "2点指定長方形",
"cmd.sketch_redefine": "スケッチ平面を再定義",
"cmd.sketch_slot_arc_center": "中心点円弧スロット",
"cmd.sketch_slot_arc_three_point": "3点円弧スロット",
"cmd.sketch_slot_center_point": "中心点スロット",
"cmd.sketch_slot_center_to_center": "中心間スロット",
"cmd.sketch_slot_overall": "全長指定スロット",
"cmd.sketch_snap": "スナップ推論",
"cmd.sketch_solve": "スケッチを解決",
"cmd.sketch_spline3d": "3D スプライン",
"cmd.sketch_spline_control_point": "制御点スプライン",
"cmd.sketch_spline_control_point_5": "制御点スプライン (次数 5)",
"cmd.sketch_spline_fit_point": "通過点スプライン",
"cmd.sketch_spun_profile": "回転プロファイル",
"cmd.sketch_toggle_driven": "従動寸法の切り替え",
"cmd.solid_align": "整列",
"cmd.solid_boundary_fill": "境界フィル",
"cmd.solid_bounding_solid": "バウンディング ソリッド",
"cmd.solid_emboss": "エンボス",
"cmd.solid_move": "移動/コピー",
"cmd.solid_offset_face": "面をオフセット",
"cmd.solid_press_pull": "プレス プル",
"cmd.solid_remove": "除去",
"cmd.solid_replace_face": "面を置換",
"cmd.solid_rib": "リブ",
"cmd.solid_split_face": "面を分割",
"cmd.solid_web": "ウェブ",
"cmd.surface_patch": "パッチ",
"cmd.surface_stitch": "ステッチ",
"cmd.surface_thicken": "厚み付け",
"cmd.surface_extrude": "サーフェス押し出し",
"cmd.surface_revolve": "サーフェス回転",
"cmd.surface_sweep": "サーフェススイープ",
"cmd.surface_loft": "サーフェスロフト",
"cmd.surface_ruled": "ルールドサーフェス",
"cmd.surface_boundary": "境界サーフェス",
"cmd.surface_offset": "オフセットサーフェス",
"cmd.surface_blend": "ブレンドサーフェス",
"cmd.surface_fillet": "サーフェスフィレット",
"cmd.surface_untrim": "トリム解除",
"cmd.surface_unstitch": "縫い合わせ解除",
"cmd.surface_reverse_normal": "法線を反転",
"cmd.inspect_continuity": "連続性解析",
"cmd.timeline_compute_all": "すべて再計算",
"cmd.timeline_copy": "フィーチャーをコピー",
"cmd.timeline_dependents": "従属フィーチャー",
"cmd.timeline_inspect": "フィーチャーを検査",
"cmd.timeline_paste": "フィーチャーを貼り付け",
"cmd.timeline_redefine": "フィーチャーを編集",
"cmd.timeline_rename": "名前を変更",
"cmd.timeline_reorder": "フィーチャーの順序を変更",
"cmd.timeline_roll_to": "履歴マーカーを移動",
"cmd.timeline_rollback": "タイムライン マーカーを移動",
"cmd.timeline_suppress": "フィーチャーを抑制",
"cmd.view_apply": "名前付きビューを復元",
"cmd.view_delete": "名前付きビューを削除",
"cmd.view_list": "名前付きビューを一覧表示",
"cmd.view_rename": "名前付きビューの名前を変更",
"cmd.view_save": "新規名前付きビュー",
"dialog.custom_axis": "↖ カスタム軸",
"dialog.edit_title": "{title}を編集",
"dialog.select": "↖ 選択",
"dialog.selected_count": "↖ {count} 個選択中",
"dialog.step": "ステップ",
"dialog.selection_step": "{total} ステップ中 {step}: {input} を選択",
"dialog.selection_guidance": "入力欄を選んで変更します。選択済みのオブジェクトをクリックすると解除できます。Ctrl/Cmd を押しながらクリックしても切り替えられます。プレビューを確認してから OK を押してください。",
"document.save_changes": "{name} への変更を保存しますか?",
"document.unsaved_changes": "{name} には保存されていない変更があります。保存しない場合、変更内容は失われます。",
"settings.msaa_applies_on_restart": "アンチエイリアスは次回 SolidCraft を起動したときに適用されます",
"settings.msaa_reset": "{setting}: {requested}× はこのグラフィックスデバイスではサポートされていないため、{reset}× に変更しました(サポート: {supported})。",
"settings.msaa_unsupported": "このグラフィックスデバイスではサポートされていません",
"settings.reset_intro": "次の設定はこのコンピューターでは使用できず、SolidCraft が起動できなくなるため変更されました:",
"settings.reset_title": "設定が変更されました",
"sketch.degrees_of_freedom": "自由度 {count}",
"sketch.draw_curves": "{linetype}として描く",
"sketch.make_selection": "選択を{linetype}にする",
"ui.1_degree_of_freedom": "自由度 1",
"ui.3d_mouse_speed": "3D マウス速度",
"ui.3d_mouse_windows": "3D マウス (Windows)",
"ui.3d_sketch": "3D スケッチ",
"ui.about": "SolidCraft について",
"ui.activate": "アクティブ化",
"ui.active_component": "アクティブなコンポーネント",
"ui.agent_cursor": "エージェントカーソル",
"ui.agent_cursor_speed": "エージェントカーソル速度",
"ui.all": "すべて",
"ui.analysis": "解析",
"ui.angle": "角度",
"ui.angle_decimals": "角度の小数点以下の桁数",
"ui.animate_view_changes": "視点変更をアニメーション表示",
"ui.animation": "アニメーション",
"ui.animation_speed": "アニメーション速度",
"ui.anti_aliasing": "アンチエイリアス",
"ui.apply": "適用",
"ui.arc": "円弧",
"ui.assemble": "アセンブリ",
"ui.autosave_every": "自動保存の間隔",
"ui.axes": "軸",
"ui.axis": "軸",
"ui.base_point": "基点",
"ui.bodies": "ボディ",
"ui.box": "直方体",
"ui.break": "分割",
"ui.cancel": "キャンセル",
"ui.canvases": "キャンバス",
"ui.center": "中心",
"ui.centerline": "中心線",
"ui.chain": "チェーン",
"ui.chamfer": "面取り",
"ui.chamfer_type": "面取りタイプ",
"ui.change_workspace": "ワークスペースの変更",
"ui.chord_length": "弦長",
"ui.curvature_g2": "曲率連続 (G2)",
"ui.equivalent_radius": "等価半径",
"ui.circle": "円",
"ui.circular_pattern": "円形パターン",
"ui.clear": "クリア",
"ui.clear_recent": "最近使った項目をクリア",
"ui.clear_the_selection": "選択をクリア",
"ui.click_selects_whole_bodies": "クリックでボディ全体を選択",
"ui.click_then_press_key": "クリックした後、割り当てるキーを押してください",
"ui.close": "閉じる",
"ui.coil": "コイル",
"ui.coincident": "一致",
"ui.collapse": "折りたたむ",
"ui.collinear": "同一直線上",
"ui.combine": "結合",
"ui.coming_later": "準備中",
"ui.concentric": "同心",
"ui.configurations": "構成",
"ui.conic_curve": "円錐曲線",
"ui.constant": "一定",
"ui.constraints_conflict": "拘束が競合しています",
"ui.construct": "構築",
"ui.construction": "コンストラクション",
"ui.context_menu": "コンテキスト メニュー",
"ui.copy": "コピー",
"ui.corner": "コーナー",
"ui.corners": "コーナー",
"ui.counterbore": "座ぐり",
"ui.countersink": "皿穴",
"ui.create": "作成",
"ui.create_copy": "コピーを作成",
"ui.create_sketch": "スケッチを作成",
"ui.current": "現在",
"ui.curvature": "曲率",
"ui.curves": "曲線",
"ui.cut": "切り取り",
"ui.cylinder": "円柱",
"ui.dark": "ダーク",
"ui.default_modelling_orientation": "モデリングのデフォルト方向",
"ui.degrees_of_freedom": "自由度",
"ui.delete": "削除",
"ui.depth": "深さ",
"ui.design": "デザイン",
"ui.diameter": "直径",
"ui.dimension": "寸法",
"ui.direction": "方向",
"ui.distance": "距離",
"ui.distance_1": "距離 1",
"ui.distance_2": "距離 2",
"ui.document_settings": "ドキュメントの設定",
"ui.don_t_save": "保存しない",
"ui.draft": "勾配",
"ui.drawing": "図面",
"ui.drill_point": "ドリル先端",
"ui.edit": "編集",
"ui.edit_section_analysis": "断面解析を編集",
"ui.ellipse": "楕円",
"ui.end_radius": "終了半径",
"ui.entities": "エンティティ",
"ui.equal": "等しい",
"ui.expand": "展開",
"ui.export_ellipsis": "エクスポート…",
"ui.export_ellipsis_keymap": "エクスポート…",
"ui.export_keymap_tip": "ショートカット、Sボックスのお気に入り、ツールバー設定をファイルに保存",
"ui.extend": "延長",
"ui.extent_type": "範囲タイプ",
"ui.extrude": "押し出し",
"ui.factor": "倍率",
"ui.feature_history": "フィーチャー履歴",
"ui.feature_options": "フィーチャーオプション",
"ui.features": "フィーチャー",
"ui.file": "ファイル",
"ui.fillet": "フィレット",
"ui.filter_commands_or_keys": "コマンドまたはキーを検索",
"ui.finish_sketch": "スケッチを終了",
"ui.fix_unfix": "固定/固定解除",
"ui.flat": "展開",
"ui.flip": "反転",
"ui.front": "正面",
"ui.fully_constrained": "完全拘束",
"ui.general": "一般",
"ui.graphics": "グラフィックス",
"ui.ground_shadow": "地面の影",
"ui.heading_add_ins": "アドイン",
"ui.heading_assemble": "アセンブリ",
"ui.heading_box": "直方体",
"ui.heading_chamfer": "面取り",
"ui.heading_circular_pattern": "円形パターン",
"ui.heading_coil": "コイル",
"ui.heading_combine": "結合",
"ui.heading_configure": "構成",
"ui.heading_constraints": "拘束",
"ui.heading_construct": "構築",
"ui.heading_create": "作成",
"ui.heading_create_sketch": "スケッチを作成",
"ui.heading_cylinder": "円柱",
"ui.heading_design": "デザイン",
"ui.heading_draft": "勾配",
"ui.heading_edit": "編集",
"ui.heading_extrude": "押し出し",
"ui.heading_fillet": "フィレット",
"ui.heading_finish_sketch": "スケッチを終了",
"ui.heading_front": "正面",
"ui.heading_hole": "穴",
"ui.heading_home": "ホーム",
"ui.heading_insert": "挿入",
"ui.heading_inspect": "検査",
"ui.heading_loft": "ロフト",
"ui.heading_make": "製造",
"ui.heading_measure": "計測",
"ui.heading_mesh": "メッシュ",
"ui.heading_mirror": "ミラー",
"ui.heading_modify": "修正",
"ui.heading_move": "移動",
"ui.heading_offset": "オフセット",
"ui.heading_move_copy": "移動/コピー",
"ui.heading_offset_plane": "オフセット平面",
"ui.heading_ok": "OK",
"ui.heading_parameters": "パラメータ",
"ui.heading_pattern_on_path": "パス上のパターン",
"ui.heading_pipe": "パイプ",
"ui.heading_plane_at_angle": "傾斜平面",
"ui.heading_plastic": "プラスチック",
"ui.heading_prepare": "準備",
"ui.heading_rectangular_pattern": "矩形パターン",
"ui.heading_revolve": "回転",
"ui.heading_right": "右側面",
"ui.heading_scale": "尺度",
"ui.heading_section_analysis": "断面解析",
"ui.heading_select": "選択",
"ui.heading_sheet_metal": "板金",
"ui.heading_shell": "シェル",
"ui.heading_sketch": "スケッチ",
"ui.heading_sketch_palette": "スケッチ パレット",
"ui.heading_sketch_scale": "スケッチの尺度",
"ui.heading_solid": "ソリッド",
"ui.heading_sphere": "球",
"ui.heading_split_body": "ボディを分割",
"ui.heading_surface": "サーフェス",
"ui.heading_sweep": "スイープ",
"ui.heading_thread": "ねじ",
"ui.heading_top": "上面",
"ui.heading_torus": "トーラス",
"ui.heading_utilities": "ユーティリティ",
"ui.heading_utility": "ユーティリティ",
"ui.help": "ヘルプ",
"ui.hide": "非表示",
"ui.hole": "穴",
"ui.hole_tap_type": "ねじ穴タイプ",
"ui.hole_type": "穴タイプ",
"ui.home": "ホーム",
"ui.horizontal_vertical": "水平/垂直",
"ui.import_ellipsis": "インポート…",
"ui.insert": "挿入",
"ui.inspect": "検査",
"ui.intersect": "交差",
"ui.join": "結合",
"ui.joints": "ジョイント",
"ui.keyboard_shortcuts": "キーボードショートカット",
"ui.language": "言語",
"ui.length_decimals": "長さの小数点以下の桁数",
"ui.light": "ライト",
"ui.line": "線分",
"ui.linetype": "線種",
"ui.loft": "ロフト",
"ui.look_at": "正面視",
"ui.look_at_anim_desc": "軸ビュー、正面視、ホーム、全体表示",
"ui.manufacture": "製造",
"ui.measure": "計測",
"ui.midpoint": "中点",
"ui.mirror": "ミラー",
"ui.mirror_line": "ミラー線",
"ui.modify": "修正",
"ui.move": "移動",
"ui.multiple": "複数",
"ui.named_views": "名前付きビュー",
"ui.navigation": "ナビゲーション",
"ui.needs_design": "{command}: 先にデザインを開いてください",
"ui.new_body": "新規ボディ",
"ui.new_design": "新規デザイン",
"ui.normal": "標準",
"ui.object_type": "オブジェクトタイプ",
"ui.objects": "オブジェクト",
"ui.offer_to_stand_up_y_up_files": "Y-upファイルを正立させるか確認",
"ui.offset": "オフセット",
"ui.offset_plane": "オフセット平面",
"ui.one_side": "片側",
"ui.open": "開く",
"ui.open_ellipsis": "開く…",
"ui.open_preferences": "環境設定を開く",
"ui.open_recent": "最近使った項目を開く",
"ui.operation": "操作",
"ui.options": "オプション",
"ui.orbit_around": "回転中心",
"ui.origin": "原点",
"ui.palette_points": "点",
"ui.palette_profile": "プロファイル",
"ui.pan_zoom_orbit": "パン、ズーム、オービット",
"ui.parallel": "平行",
"ui.parameters": "パラメータ",
"ui.paste": "貼り付け",
"ui.path": "パス",
"ui.pattern_on_path": "パス上のパターン",
"ui.perpendicular": "直交",
"ui.perspective_view": "パースペクティブ表示",
"ui.pipe": "パイプ",
"ui.placement": "配置",
"ui.plane": "平面",
"ui.plane_at_angle": "傾斜平面",
"ui.point": "点",
"ui.point_under_the_cursor": "カーソル下の点",
"ui.points": "点",
"ui.polygon": "ポリゴン",
"ui.preferences": "環境設定",
"ui.press_a_key": "キーを押してください…",
"ui.profile": "プロファイル",
"ui.profile_plane": "プロファイル平面",
"ui.profiles": "プロファイル",
"ui.project": "投影",
"ui.project_face_edges_sketch": "新規スケッチに面のエッジを自動投影",
"ui.quantity": "数量",
"ui.quantity_2": "数量 2",
"ui.quit": "終了",
"ui.radial_context_menu": "パイ メニュー",
"ui.radius": "半径",
"ui.radius_type": "半径タイプ",
"ui.rectangle": "長方形",
"ui.rectangular_pattern": "矩形パターン",
"ui.redo": "やり直す",
"ui.render": "レンダリング",
"ui.renderer": "レンダラー",
"ui.reset": "リセット",
"ui.reset_all": "すべてリセット",
"ui.reset_the_settings_on_this_page": "このページの設定をリセット",
"ui.restore_defaults": "初期値に戻す",
"ui.reverse_zoom_direction": "ズーム方向を反転",
"ui.revolve": "回転",
"ui.right": "右側面",
"ui.save": "保存",
"ui.save_as": "名前を付けて保存",
"ui.save_as_ellipsis": "名前を付けて保存…",
"ui.scale": "尺度",
"ui.section_analysis": "断面解析",
"ui.select": "選択",
"ui.select_other_hint": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。",
"ui.selection": "選択",
"ui.shell": "シェル",
"ui.show": "表示",
"ui.show_constraints": "拘束",
"ui.show_dimensions": "寸法",
"ui.show_points": "点を表示",
"ui.show_profile": "プロファイルを表示",
"ui.show_projected_geometries": "投影ジオメトリ",
"ui.simple": "単純",
"ui.simulation": "シミュレーション",
"ui.single": "単一",
"ui.sketch": "スケッチ",
"ui.sketch_dimension": "スケッチ寸法",
"ui.sketch_grid": "スケッチ グリッド",
"ui.sketch_scale": "スケッチ尺度",
"ui.sketch_status": "スケッチの状態",
"ui.sketches": "スケッチ",
"ui.slice": "スライス",
"ui.slot": "スロット",
"ui.smooth": "スムーズ",
"ui.smooth_wheel_zoom": "滑らかなホイールズーム",
"ui.smooth_wheel_zoom_tip": "オフにすると、ホイール操作のたびにズームが即座に反映され、滑らかな減速効果は適用されません。",
"ui.snap": "スナップ",
"ui.space_mouse_hint": "視点の移動と回転を同時に行います(スケッチ中はパンとズームのみ)",
"ui.space_mouse_navigate": "SpaceNavigator / SpaceMouseで視点を操作",
"ui.spacing": "間隔",
"ui.spacing_2": "間隔 2",
"ui.sphere": "球",
"ui.spline": "スプライン",
"ui.split_body": "ボディを分割",
"ui.stand_up_tip": "Z軸上向きで開いたSTEPまたはIGESファイルに対し、X軸周りに90度回転させるか確認します",
"ui.start": "開始",
"ui.start_radius": "開始半径",
"ui.sweep": "スイープ",
"ui.symmetric": "対称",
"ui.symmetry": "対称",
"ui.system": "システム",
"ui.tangent": "正接",
"ui.taper_angle": "テーパ角度",
"ui.tapped": "ねじ穴",
"ui.text": "テキスト",
"ui.theme": "テーマ",
"ui.thickness": "厚さ",
"ui.thread": "ねじ",
"ui.timeline_bottom": "下部タイムライン",
"ui.timeline_left_note": "左レイアウトでは、ロールバックバーがパーツとシーンブラウザーの上に表示されます。",
"ui.timeline_left_sidebar": "左サイドバー",
"ui.tip_angle": "先端角度",
"ui.to_object": "オブジェクトまで",
"ui.top": "上面",
"ui.torus": "トーラス",
"ui.trackpad_pan_tip": "トラックパッドのジェスチャーでパンします(オフの場合はマウスホイールと同様にズームします)。マウスホイールはズーム、サイドホイールは横方向のパン、ピンチ操作は常にズームになります。",
"ui.trim": "トリム",
"ui.two_finger_scroll_pans": "2本指スクロールでパン",
"ui.two_sides": "両側",
"ui.ui_scale": "UIスケール",
"ui.undo": "元に戻す",
"ui.units": "単位",
"ui.units_of_new_designs": "新規デザインの単位",
"ui.untitled": "無題",
"ui.up_axis_note": "ビュー(軸ビュー、ホーム、正面視)で上向きにする軸を指定します。デザインデータ自体は変更されません。",
"ui.variable": "可変",
"ui.view_centre": "ビュー中心",
"ui.visual_shaded_hidden_edges": "隠線付きシェーディング",
"ui.visual_shaded_with_edges": "エッジ付きシェーディング",
"ui.visual_style": "表示スタイル",
"ui.visual_wireframe": "ワイヤーフレーム",
"ui.workspace": "ワークスペース",
"ui.x_distance": "X 距離",
"ui.y_distance": "Y 距離",
"ui.y_up": "Y軸上向き",
"ui.z_up": "Z軸上向き",
"ui.zoom_to_cursor_tip": "マウスホイールでカーソル位置を基準にズームします(オフの場合はビューの中心を基準にズームします)。",
"ui.zoom_to_the_cursor": "カーソル位置へズーム",
"A new configuration, a copy of the active one": "アクティブな構成をコピーして新しい構成を作成",
"About SolidCraft": "SolidCraft について",
"Active": "アクティブ",
"Active rule": "有効なルール",
"Add": "追加",
"Add Key": "キーを追加",
"Add Mid Radius": "中間半径を追加",
"Add Row": "行を追加",
"All Sketch Points": "すべてのスケッチ点",
"All open designs": "すべての開いているデザイン",
"Allowed expressions, separated by semicolons": "使用可能な式(セミコロンで区切る)",
"Also deletes": "同時に削除される要素:",
"Angle 2": "角度 2",
"Animate": "アニメーション",
"Appearance name": "外観名",
"Applied Radius": "適用半径",
"Apply To": "適用先",
"Area": "表面積",
"Attachment": "アタッチメント",
"BROWSER": "ブラウザ",
"Bend Line Position": "曲げ線位置",
"Bend Position": "曲げ位置",
"Bend Radius": "曲げ半径",
"Bends": "曲げ",
"Body": "ボディ",
"Bounding box": "境界ボックス",
"CSV or JSON: updates existing parameters, adds new ones": "CSVまたはJSON:既存のパラメータを更新し、新しいパラメータを追加します",
"Capture All Positions": "すべての位置をキャプチャ",
"Catch Direction": "キャッチ方向",
"Cells": "セル",
"Center X": "中心 X",
"Center Y": "中心 Y",
"Center of mass": "重心",
"Chamfer Type": "面取りタイプ",
"Check collisions": "衝突チェック",
"Checked": "チェック済み",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "新しい平面を選択してください。スケッチジオメトリと寸法は2次元座標系のまま維持されます。",
"Close draft": "下書きを閉じる",
"Collisions": "衝突",
"Colour": "色",
"Component1": "コンポーネント1",
"Configuration": "構成",
"Configuration rules": "構成ルール",
"Contact": "接触",
"Copy all licences": "すべてのライセンスをコピー",
"Copy diagnostic information": "診断情報をコピー",
"Copy report": "レポートをコピー",
"Corner Relief Shape": "コーナーリリーフ形状",
"Counterbore depth": "座ぐり深さ",
"Countersink angle": "皿穴角度",
"Create Issue — review draft": "Issueを作成 — 下書きの確認",
"Customize Toolbar": "ツールバーのカスタマイズ",
"Delete all": "すべて削除",
"Delete rule": "ルールを削除",
"Delete the row": "行を削除",
"Delete the set": "セットを削除",
"Delete this parameter": "このパラメータを削除",
"Density": "密度",
"Designs you open or save appear here.": "開いたデザインや保存したデザインがここに表示されます。",
"Determined by To Object. Choose Angle to edit the angular extent.": "「対象オブジェクトまで」により決定されます。角度範囲を編集するには「角度」を選択してください。",
"Discard": "破棄",
"Double-click to rename": "ダブルクリックで名前変更",
"Drive Joint": "ジョイントを駆動",
"Edit Profile Sketch": "プロファイルスケッチを編集",
"Enable rule": "ルールを有効化",
"Enabled": "有効",
"Explode": "分解",
"Extents": "範囲",
"Face attachment lost": "面アタッチメントが失われました",
"Faces / edges / vertices": "面 / エッジ / 頂点",
"Favourite": "お気に入り",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "多くのCADではY軸を上向きとしているため、Z軸を上向きとするSolidCraftでは、読み込んだモデルが横倒しの向きで表示されることがあります。",
"Filter": "フィルター",
"Find in Browser": "ブラウザで検索",
"Flat size": "展開サイズ",
"Follow camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
"Font": "フォント",
"Full Length": "全長",
"Gap": "ギャップ",
"Grid": "グリッド",
"Height": "高さ",
"Home view": "ホームビュー",
"In This Design": "このデザイン内",
"Interferes": "干渉あり",
"Issue title": "Issueタイトル",
"Joint Origin1": "ジョイント原点1",
"Keep Tools": "ツールボディを保持",
"Later": "後で",
"Leave it": "そのままにする",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "従来の面アタッチメントです。確定された面に再バインドすると正確なアタッチメントが作成されます。",
"Length": "長さ",
"Length units": "長さの単位",
"Library": "ライブラリ",
"Loop": "ループ",
"Margin": "マージン",
"Mass": "質量",
"Material": "材料",
"Mid Radius": "中間半径",
"Modeled": "モデル化済み",
"More toolbar panels": "その他のツールバーパネル",
"Motion": "モーション",
"Move Type": "移動タイプ",
"Move earlier": "前へ移動",
"Move later": "後へ移動",
"Name": "名前",
"No crash reports.": "クラッシュレポートはありません。",
"No matching appearances": "一致する外観がありません",
"Not available yet": "まだ利用できません",
"Opacity": "不透明度",
"Open GitHub draft": "GitHubの下書きを開く",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Rust製オープンソース・パラメトリック3D CAD。ライセンス: MIT または Apache-2.0。",
"Other side": "反対側",
"Parameter values": "パラメータ値",
"Per Edge": "エッジごと",
"Pick the tool bodies": "ツールボディを選択",
"Place at the origin instead": "代わりに原点に配置",
"Playback": "再生",
"Position": "位置",
"Position (0 to 1)": "位置 (0〜1)",
"Position along the edge, 0 at the start end, 1 at the other": "エッジ上の位置(始点が0、終点が1)",
"Preview leaves the current design unchanged.": "プレビューは現在のデザインを変更しません。",
"RECENT": "最近使用した項目",
"Radius Fit": "半径フィット",
"Ratio": "比率",
"Recover": "復元",
"Recover unsaved design?": "未保存のデザインを復元しますか?",
"Remove Tools": "ツールボディを削除",
"Remove from list": "リストから削除",
"Remove private design details or paths before sharing.": "共有前に、非公開の設計情報やファイルパスを削除してください。",
"Remove the column": "列を削除",
"Remove the track": "トラックを削除",
"Remove this point": "この点を削除",
"Report a bug — review a screenshot and open a GitHub draft": "バグを報告 — スクリーンショットを確認してGitHub下書きを作成",
"Reset Toolbar": "ツールバーをリセット",
"Restore the inherited component or physical material appearance": "コンポーネントまたは物理マテリアルから継承した外観に戻す",
"Result": "結果",
"Retake screenshot": "スクリーンショットを再取得",
"Reverse": "反転",
"Reverse Direction": "方向を反転",
"Revert": "元に戻す",
"Revert All Positions": "すべての位置を元に戻す",
"Review the report and screenshot before opening GitHub.": "GitHubを開く前にレポートとスクリーンショットを確認してください。",
"Rim Edge": "リムエッジ",
"Roll History Marker Here": "履歴マーカーをここに移動",
"Rule": "ルール",
"Saved preview. Geometry is being checked before editing.": "保存済みのプレビューです。編集前にジオメトリを検証しています。",
"Scale factor": "尺度係数",
"Search": "検索",
"Search commands": "コマンドを検索",
"Select bodies (a face picks its body)": "ボディを選択(面をクリックするとそのボディが選択されます)",
"Select bodies or faces instead": "代わりにボディまたは面を選択",
"Selection filter": "選択フィルター",
"Semantic faces share their appearance across instances.": "セマンティック面はインスタンス間で外観を共有します。",
"Shape": "形状",
"Show Agent Cursor": "エージェントカーソルを表示",
"Show in folder": "フォルダに表示",
"Size": "サイズ",
"Sketch plane has no body-face attachment.": "スケッチ平面にボディ面のアタッチメントがありません。",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "これらのデザインが保存される前にSolidCraftが終了しました。最後の自動保存ファイルを開くことができます。",
"Speed": "速度",
"Stand it up": "正立させる",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "正立させる: 取り消し可能な1つの移動フィーチャーとして、X軸周りに90度回転します。",
"Stand this part up?": "このパーツを正立させますか?",
"Step": "ステップ",
"Steps": "ステップ数",
"Suppressed": "抑制",
"Swap Ends": "端点を入れ替え",
"Tangent Chain": "正接チェーン",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "破線のスケッチは保存された平面ガイドです。新しい面を選択してアタッチメントを修復してください。",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存された作成元のオカレンスが見つかりません。再バインドする前に有効な作成元配置を選択してください。",
"Then": "次を実行 (Then)",
"These features will be deleted:": "以下のフィーチャーが削除されます:",
"These features will fail:": "以下のフィーチャーでエラーが発生します:",
"This child is shared. Deleting it removes it from every copy of its parent.": "この子要素は共有されています。削除すると親のすべてのコピーから削除されます。",
"This child is shared. Position changes apply to every copy of its parent.": "この子要素は共有されています。位置の変更は親のすべてのコピーに適用されます。",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "この面は安定したアタッチメントを提供できません。別の面を選択するか、固定平面を明示的に使用してください。",
"To Offset": "オフセットまで",
"Tolerance": "公差",
"Type": "タイプ",
"Updating the preview": "プレビューを更新中",
"Use fixed plane": "固定平面を使用",
"VIEW": "表示",
"Value": "値",
"Volume": "体積",
"Wall": "肉厚",
"When": "条件 (When)",
"Width": "幅",
"Will fail": "失敗します",
"angle, height": "角度, 高さ",
"command name": "コマンド名",
"expression, e.g. 25 mm": "計算式 (例: 25 mm)",
"fits the face, e.g. M8": "面に適合するサイズ (例: M8)",
"from the rule": "ルールから",
"needs two joints": "2つのジョイントが必要",
"new configuration": "新規構成",
"new name": "新しい名前",
"new parameter": "新規パラメータ",
"no interference": "干渉なし",
"no joint that moves yet": "可動ジョイントがまだありません",
"no picture": "画像なし",
"pick a hole to seat it, or OK places it at the origin": "配置先の穴を選択するか、[OK]を押すと原点に配置します。",
"pick one or two vertices, edges or faces": "1つまたは2つの頂点、エッジ、面を選択",
"the parts library is not available": "パーツライブラリは利用できません",
"to the next face": "次の面まで",
"Revert Position": "位置を元に戻す",
"Construction": "コンストラクション",
"Flat": "展開",
"Faces": "面",
"Edges": "エッジ",
"Vertices": "頂点",
"Sketch geometry and profiles": "スケッチジオメトリとプロファイル",
"Sketch curves": "スケッチ曲線",
"Sketch points": "スケッチ点",
"Sketch dimensions": "スケッチ寸法",
"Sketch constraints": "スケッチ拘束",
"0 turns it off": "0で無効化",
"Show the pointer an AI agent moves (#35)": "エージェントが操作するポインターを表示 (#35)",
"Follow the camera (Look At sketches)": "カメラ追従(スケッチを正面視)",
"Save Rule": "ルールを保存",
"Preview Row": "行をプレビュー",
"Add condition": "条件を追加",
"Apply to selection": "選択項目に適用",
"Remove override": "オーバーライドを削除",
"Copy screenshot": "スクリーンショットをコピー",
"Remove screenshot": "スクリーンショットを削除",
"Use active author placement": "アクティブな作成元配置を使用",
"Reattach selected face": "選択した面を再アタッチ",
"Use saved fixed plane": "保存された固定平面を使用",
"Use selected face as fixed plane": "選択した面を固定平面として使用",
"View navigator": "ビューナビゲーター",
"Signed axes": "符号付き座標軸",
"Cube": "キューブ",
"Middle double-click": "中ボタンのダブルクリック",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "ビュー上で中ボタンをダブルクリックした時の動作: ホーム(全体を収めたホームビュー)、全体表示(向きを維持して全体を収める)、または無効",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "その他を選択: モデル上で左ボタンを長押し(または右クリック › その他を選択…)するとカーソル下の要素が一覧表示されます。同じ場所を再度クリックすると次の要素が選択されます。中ボタンをダブルクリックするとホームビューに戻ります(環境設定 › ナビゲーション)。",
"cmd.render_backends": "レンダリング バックエンド",
"cmd.view_navigate": "ビューナビゲーション",
"ui.animateJoint": "ジョイントをアニメート",
"ui.calibrate": "校正",
"ui.customizeToolbar": "ツールバーをカスタマイズ…",
"ui.editJointLimits": "ジョイント制限を編集",
"ui.exportDxf": "DXF をエクスポート…",
"ui.findInTimeline": "タイムラインで検索",
"ui.findInWindow": "ウィンドウで検索",
"ui.fit": "フィット",
"ui.groupSelected": "選択をグループ化",
"ui.hideAxes": "軸を非表示",
"ui.hideDimension": "寸法を非表示",
"ui.hideOrigin": "原点を非表示",
"ui.hidePlanes": "平面を非表示",
"ui.hideProfile": "プロファイルを非表示",
"ui.homeView": "ホームビュー",
"ui.isolate": "分離表示",
"ui.lock": "ロック",
"ui.newSketchGroup": "新しいスケッチグループ",
"ui.normalConstruction": "通常 / コンストラクション",
"ui.orbit": "回転",
"ui.heading_position": "配置",
"ui.pan": "パン",
"ui.pinToShortcuts": "ショートカットに固定",
"ui.pinToToolbar": "ツールバーに固定",
"ui.properties": "プロパティ",
"ui.removeSection": "セクションを解除",
"ui.removeFromToolbar": "ツールバーから削除",
"ui.repeat": "繰り返し",
"ui.report_a_bug": "不具合を報告",
"ui.restore": "復元",
"ui.heading_setup": "セットアップ",
"ui.selectOther": "その他を選択…",
"ui.selectTangentChain": "接線チェーンを選択",
"ui.showAll": "すべて表示",
"ui.showAllBodies": "すべてのボディを表示",
"ui.showAxes": "軸を表示",
"ui.showDimension": "寸法を表示",
"ui.showOrigin": "原点を表示",
"ui.showPlanes": "平面を表示",
"ui.suppress": "抑制",
"ui.command_palette_hint": "コマンドを入力(例: {example})— または S キーで検索",
"ui.unground": "固定を解除",
"ui.unisolate": "分離表示を解除",
"ui.unlock": "ロック解除",
"ui.unsuppress": "抑制を解除",
"ui.updateToCurrentView": "現在のビューに更新",
"ui.zoom": "ズーム",
"ui.command_done": "{command} 完了",
"timeline.group": "フィーチャーフォルダを新規作成",
"timeline.ungroup": "フィーチャーフォルダを削除",
"timeline.rename_group": "フィーチャーフォルダの名前を変更",
"timeline.move_to_group": "フィーチャーをフォルダへ移動",
"timeline.move_many": "タイムライン項目を移動",
"timeline.reorder_many": "フィーチャーの順序を変更",
"ui.show_construction_geometries": "構築ジオメトリ",
"ui.total_angle": "合計角度",
"sketch_text.builtin_font": "{family}(内蔵)",
"sketch_text.font_search": "フォントを検索",
"sketch_text.loading_fonts": "インストール済みフォントを検索中…",
"sketch_text.use_font_file": "フォント ファイル {path} を使用",
"sketch_text.bold": "太字",
"sketch_text.italic": "斜体",
"sketch_text.align_left": "左",
"sketch_text.align_center": "中央",
"sketch_text.align_right": "右",
"sketch_text.path_hint": "文字が沿うスケッチの曲線: ここをクリックしてから曲線をクリック",
"sketch_text.path_clear": "文字を曲線から外す",
"Style": "スタイル",
"Alignment": "配置",
"Letter Spacing": "文字間隔",
"Line Spacing": "行間",
"Path": "パス",
"sketch_text.on_path": "文字はパスに沿います: 位置は曲線で決まります",
"cmd.material_properties_validate":"物理材料データを検証"
}
+701
View File
@@ -0,0 +1,701 @@
{
"browser.body_name": "바디{number}",
"browser.component_name": "컴포넌트{number}",
"browser.parameters": "매개변수 ({count})",
"browser.precision": "정밀도: {precision}",
"browser.sketch_name": "스케치{number}",
"browser.units": "단위: {units}",
"dialog.custom_axis": "↖ 사용자 정의 축",
"dialog.edit_title": "{title} 편집",
"dialog.select": "↖ 선택",
"dialog.selected_count": "↖ {count}개 선택됨",
"dialog.step": "단계",
"dialog.selection_step": "{step}/{total} 단계: {input} 선택",
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
"settings.reset_intro": "다음 설정은 이 컴퓨터에서 작동하지 않아 SolidCraft를 시작할 수 없게 하므로 변경되었습니다:",
"settings.reset_title": "설정이 변경되었습니다",
"sketch.degrees_of_freedom": "자유도 {count}개",
"sketch.draw_curves": "{linetype}(으)로 그리기",
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
"ui.1_degree_of_freedom": "자유도 1개",
"ui.3d_sketch": "3D 스케치",
"ui.about": "정보",
"ui.activate": "활성화",
"ui.active_component": "활성 컴포넌트",
"ui.agent_cursor": "에이전트 커서",
"ui.agent_cursor_speed": "에이전트 커서 속도",
"ui.all": "모두",
"ui.analysis": "분석",
"ui.angle": "각도",
"ui.angle_decimals": "각도 소수점 자릿수",
"ui.animate_view_changes": "뷰 변경 애니메이션",
"ui.animation": "애니메이션",
"ui.animation_speed": "애니메이션 속도",
"ui.anti_aliasing": "안티앨리어싱",
"ui.apply": "적용",
"ui.arc": "호",
"ui.assemble": "조립",
"ui.autosave_every": "자동 저장 간격",
"ui.axes": "축",
"ui.axis": "축",
"ui.base_point": "기준점",
"ui.bodies": "바디",
"ui.box": "박스",
"ui.break": "끊기",
"ui.cancel": "취소",
"ui.canvases": "캔버스",
"ui.center": "중심",
"ui.centerline": "중심선",
"ui.chain": "체인",
"ui.chamfer": "모따기",
"ui.chamfer_type": "모따기 유형",
"ui.change_workspace": "작업 공간 변경",
"ui.chord_length": "현 길이",
"ui.curvature_g2": "곡률 연속 (G2)",
"ui.equivalent_radius": "등가 반지름",
"ui.circle": "원",
"ui.circular_pattern": "원형 패턴",
"ui.clear_recent": "최근 항목 지우기",
"ui.clear_the_selection": "선택 해제",
"ui.close": "닫기",
"ui.coil": "코일",
"ui.coincident": "일치",
"ui.collapse": "접기",
"ui.collinear": "동일 선상",
"ui.combine": "결합",
"ui.coming_later": "추후 지원 예정",
"ui.concentric": "동심",
"ui.configurations": "구성",
"ui.conic_curve": "원뿔 곡선",
"ui.constant": "상수",
"ui.constraints_conflict": "구속 조건 충돌",
"ui.construct": "구성",
"ui.construction": "구성선",
"ui.copy": "복사",
"ui.corners": "코너",
"ui.corner": "코너",
"ui.counterbore": "카운터보어",
"ui.countersink": "카운터싱크",
"ui.create": "만들기",
"ui.create_copy": "복사본 만들기",
"ui.create_sketch": "스케치 만들기",
"ui.current": "현재",
"ui.curvature": "곡률",
"ui.curves": "곡선",
"ui.cut": "잘라내기",
"ui.cylinder": "원통",
"ui.dark": "어둡게",
"ui.default_modelling_orientation": "기본 모델링 방향",
"ui.degrees_of_freedom": "자유도",
"ui.delete": "삭제",
"ui.depth": "깊이",
"ui.design": "디자인",
"ui.diameter": "지름",
"ui.dimension": "치수",
"ui.direction": "방향",
"ui.distance": "거리",
"ui.distance_1": "거리 1",
"ui.distance_2": "거리 2",
"ui.document_settings": "문서 설정",
"ui.don_t_save": "저장 안 함",
"ui.draft": "구배",
"ui.drawing": "도면",
"ui.drill_point": "드릴 선단",
"ui.edit": "편집",
"ui.edit_section_analysis": "단면 분석 편집",
"ui.ellipse": "타원",
"ui.end_radius": "끝 반지름",
"ui.entities": "요소",
"ui.equal": "동일",
"ui.expand": "펼치기",
"ui.export_ellipsis": "내보내기...",
"ui.extend": "연장",
"ui.extent_type": "범위 유형",
"ui.extrude": "돌출",
"ui.factor": "배율",
"ui.feature_history": "피처 기록",
"ui.feature_options": "피처 옵션",
"ui.features": "피처",
"ui.file": "파일",
"ui.fillet": "필렛",
"ui.finish_sketch": "스케치 종료",
"ui.fix_unfix": "고정/고정 해제",
"ui.flat": "플랫",
"ui.flip": "뒤집기",
"ui.front": "정면",
"ui.fully_constrained": "완전 구속",
"ui.general": "일반",
"ui.graphics": "그래픽",
"ui.heading_assemble": "조립",
"ui.heading_box": "박스",
"ui.heading_chamfer": "모따기",
"ui.heading_circular_pattern": "원형 패턴",
"ui.heading_coil": "코일",
"ui.heading_combine": "결합",
"ui.heading_construct": "구성",
"ui.heading_create": "만들기",
"ui.heading_create_sketch": "스케치 만들기",
"ui.heading_cylinder": "원통",
"ui.heading_design": "디자인",
"ui.heading_draft": "구배",
"ui.heading_edit": "편집",
"ui.heading_extrude": "돌출",
"ui.heading_fillet": "필렛",
"ui.heading_finish_sketch": "스케치 종료",
"ui.heading_front": "정면",
"ui.heading_hole": "구멍",
"ui.heading_home": "홈",
"ui.heading_insert": "삽입",
"ui.heading_inspect": "검사",
"ui.heading_loft": "로프트",
"ui.heading_measure": "측정",
"ui.heading_mesh": "메시",
"ui.heading_mirror": "대칭 복사",
"ui.heading_modify": "수정",
"ui.heading_move": "이동",
"ui.heading_offset": "오프셋",
"ui.heading_move_copy": "이동/복사",
"ui.heading_offset_plane": "오프셋 평면",
"ui.heading_ok": "확인",
"ui.heading_parameters": "매개변수",
"ui.heading_pattern_on_path": "경로 패턴",
"ui.heading_pipe": "파이프",
"ui.heading_plane_at_angle": "각도 평면",
"ui.heading_plastic": "플라스틱",
"ui.heading_rectangular_pattern": "직사각형 패턴",
"ui.heading_revolve": "회전",
"ui.heading_right": "우측면",
"ui.heading_scale": "축척",
"ui.heading_section_analysis": "단면 분석",
"ui.heading_select": "선택",
"ui.heading_sheet_metal": "판금",
"ui.heading_shell": "쉘",
"ui.heading_sketch": "스케치",
"ui.heading_sketch_palette": "스케치 팔레트",
"ui.heading_sketch_scale": "스케치 배율",
"ui.heading_solid": "솔리드",
"ui.heading_sphere": "구",
"ui.heading_split_body": "바디 분할",
"ui.heading_surface": "서피스",
"ui.heading_sweep": "스윕",
"ui.heading_thread": "나사산",
"ui.heading_top": "상면",
"ui.heading_torus": "토러스",
"ui.help": "도움말",
"ui.hide": "숨기기",
"ui.hole": "구멍",
"ui.hole_tap_type": "탭 유형",
"ui.hole_type": "구멍 유형",
"ui.home": "홈",
"ui.horizontal_vertical": "수평/수직",
"ui.insert": "삽입",
"ui.inspect": "검사",
"ui.intersect": "교차",
"ui.join": "접합",
"ui.joints": "조인트",
"ui.keyboard_shortcuts": "키보드 단축키",
"ui.language": "언어",
"ui.length_decimals": "길이 소수점 자릿수",
"ui.light": "밝게",
"ui.line": "선",
"ui.linetype": "선 종류",
"ui.loft": "로프트",
"ui.look_at": "바라보기",
"ui.manufacture": "제조",
"ui.measure": "측정",
"ui.midpoint": "중간점",
"ui.mirror": "대칭 복사",
"ui.mirror_line": "대칭선",
"ui.modify": "수정",
"ui.move": "이동",
"ui.multiple": "다중",
"ui.named_views": "명명된 뷰",
"ui.navigation": "탐색",
"ui.needs_design": "{command}: 먼저 디자인을 열어 주세요",
"ui.new_body": "새 바디",
"ui.new_design": "새 디자인",
"ui.normal": "일반",
"ui.object_type": "객체 유형",
"ui.objects": "객체",
"ui.offer_to_stand_up_y_up_files": "Y-up 파일을 Z-up으로 세우도록 제안",
"ui.offset": "오프셋",
"ui.offset_plane": "오프셋 평면",
"ui.one_side": "한 방향",
"ui.open": "열기",
"ui.open_ellipsis": "열기...",
"ui.open_preferences": "환경 설정 열기",
"ui.open_recent": "최근 항목 열기",
"ui.operation": "작업",
"ui.options": "옵션",
"ui.orbit_around": "궤도 회전 기준",
"ui.origin": "원점",
"ui.palette_points": "점",
"ui.palette_profile": "프로파일",
"ui.pan_zoom_orbit": "이동, 확대/축소, 궤도 회전",
"ui.parallel": "평행",
"ui.parameters": "매개변수",
"ui.paste": "붙여넣기",
"ui.path": "경로",
"ui.pattern_on_path": "경로 패턴",
"ui.perpendicular": "수직",
"ui.pipe": "파이프",
"ui.placement": "배치",
"ui.plane": "평면",
"ui.plane_at_angle": "각도 평면",
"ui.point": "점",
"ui.point_under_the_cursor": "커서 아래의 점",
"ui.points": "점",
"ui.polygon": "다각형",
"ui.preferences": "환경 설정",
"ui.profile": "프로파일",
"ui.profile_plane": "프로파일 평면",
"ui.profiles": "프로파일",
"ui.project": "투영",
"ui.quantity_2": "수량 2",
"ui.quantity": "수량",
"ui.quit": "종료",
"ui.radius": "반지름",
"ui.radius_type": "반지름 유형",
"ui.rectangle": "직사각형",
"ui.rectangular_pattern": "직사각형 패턴",
"ui.redo": "다시 실행",
"ui.render": "렌더",
"ui.reset_the_settings_on_this_page": "이 페이지의 설정 초기화",
"ui.restore_defaults": "기본값 복원",
"ui.reverse_zoom_direction": "확대/축소 방향 반전",
"ui.revolve": "회전",
"ui.right": "우측면",
"ui.save": "저장",
"ui.save_as": "다른 이름으로 저장",
"ui.save_as_ellipsis": "다른 이름으로 저장...",
"ui.scale": "축척",
"ui.section_analysis": "단면 분석",
"ui.select": "선택",
"ui.selection": "선택",
"ui.shell": "쉘",
"ui.show": "표시",
"ui.show_constraints": "구속 조건",
"ui.show_dimensions": "치수",
"ui.show_points": "점 표시",
"ui.show_profile": "프로파일 표시",
"ui.show_projected_geometries": "투영된 형상",
"ui.simple": "단순",
"ui.simulation": "시뮬레이션",
"ui.single": "단일",
"ui.sketch": "스케치",
"ui.sketch_dimension": "스케치 치수",
"ui.sketch_grid": "스케치 격자",
"ui.sketch_scale": "스케치 축척",
"ui.sketch_status": "스케치 상태",
"ui.sketches": "스케치",
"ui.slice": "슬라이스",
"ui.slot": "슬롯",
"ui.smooth": "부드럽게",
"ui.snap": "스냅",
"ui.spacing_2": "간격 2",
"ui.spacing": "간격",
"ui.sphere": "구",
"ui.spline": "스플라인",
"ui.split_body": "바디 분할",
"ui.start": "시작",
"ui.start_radius": "시작 반지름",
"ui.sweep": "스윕",
"ui.symmetric": "대칭",
"ui.symmetry": "대칭",
"ui.system": "시스템",
"ui.tangent": "접선",
"ui.taper_angle": "테이퍼 각도",
"ui.tapped": "탭",
"ui.text": "텍스트",
"ui.theme": "테마",
"ui.thickness": "두께",
"ui.thread": "나사산",
"ui.timeline_bottom": "하단 타임라인",
"ui.timeline_left_note": "왼쪽 레이아웃에서는 롤백 바가 부품 및 장면 브라우저 위에 표시됩니다.",
"ui.timeline_left_sidebar": "왼쪽 사이드바",
"ui.tip_angle": "선단 각도",
"ui.to_object": "객체까지",
"ui.top": "상면",
"ui.torus": "토러스",
"ui.trim": "트림",
"ui.two_sides": "양방향",
"ui.ui_scale": "UI 배율",
"ui.undo": "실행 취소",
"ui.units": "단위",
"ui.units_of_new_designs": "새 디자인의 단위",
"ui.untitled": "제목 없음",
"ui.variable": "가변",
"ui.view_centre": "뷰 중심",
"ui.visual_style": "시각 스타일",
"ui.workspace": "작업 공간",
"ui.y_distance": "Y 거리",
"ui.x_distance": "X 거리",
"ui.zoom_to_the_cursor": "커서 위치로 확대/축소",
"ui.animateJoint": "조인트 애니메이션",
"ui.calibrate": "캘리브레이션",
"ui.customizeToolbar": "도구 모음 사용자 지정…",
"ui.editJointLimits": "조인트 제한 편집",
"ui.exportDxf": "DXF 내보내기…",
"ui.findInTimeline": "타임라인에서 찾기",
"ui.findInWindow": "창에서 찾기",
"ui.fit": "맞춤",
"ui.groupSelected": "선택 항목 그룹화",
"ui.hideAxes": "축 숨기기",
"ui.hideDimension": "치수 숨기기",
"ui.hideOrigin": "원점 숨기기",
"ui.hidePlanes": "평면 숨기기",
"ui.hideProfile": "윤곽 숨기기",
"ui.homeView": "홈 보기",
"ui.isolate": "분리 표시",
"ui.lock": "잠금",
"ui.newSketchGroup": "새 스케치 그룹",
"ui.normalConstruction": "일반 / 생성",
"ui.orbit": "회전",
"ui.heading_position": "배치",
"ui.pan": "이동",
"ui.pinToShortcuts": "바로 가기에 고정",
"ui.pinToToolbar": "도구 모음에 고정",
"ui.properties": "속성",
"ui.removeSection": "단면 제거",
"ui.removeFromToolbar": "도구 모음에서 제거",
"ui.repeat": "반복",
"ui.report_a_bug": "버그 신고",
"ui.restore": "복원",
"ui.heading_setup": "설정",
"ui.selectOther": "기타 선택…",
"ui.selectTangentChain": "접선 체인 선택",
"ui.showAll": "모두 표시",
"ui.showAllBodies": "모두 표시",
"ui.showAxes": "축 표시",
"ui.showDimension": "치수 표시",
"ui.showOrigin": "원점 표시",
"ui.showPlanes": "평면 표시",
"ui.suppress": "비활성화",
"ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
"ui.unground": "고정 해제",
"ui.unisolate": "분리 표시 해제",
"ui.unlock": "잠금 해제",
"ui.unsuppress": "비활성화 해제",
"ui.updateToCurrentView": "현재 보기로 업데이트",
"ui.zoom": "확대/축소",
"ui.command_done": "{command} 완료",
"cmd.appearance_assign": "외관",
"cmd.appearance_library": "외관 라이브러리",
"cmd.appearance_list": "외관 목록",
"cmd.body_rename": "바디 이름 변경",
"cmd.browser_group": "새 그룹",
"cmd.browser_move": "브라우저 항목 이동",
"cmd.browser_order": "브라우저 항목 정렬",
"cmd.browser_rename_group": "그룹 이름 변경",
"cmd.browser_ungroup": "그룹 해제",
"cmd.browser_visibility": "표시/숨기기",
"cmd.canvas_calibrate": "캔버스 보정",
"cmd.canvas_decal": "데칼",
"cmd.canvas_delete": "캔버스 삭제",
"cmd.canvas_edit": "캔버스 편집",
"cmd.canvas_insert": "캔버스",
"cmd.component_activate": "구성 요소 활성화",
"cmd.component_break_link": "링크 끊기",
"cmd.component_capture_position": "위치 캡처",
"cmd.component_create": "새 구성 요소",
"cmd.component_delete": "구성 요소 삭제",
"cmd.component_from_bodies": "바디로 구성 요소 만들기",
"cmd.component_insert_design": "설계 삽입",
"cmd.component_links": "링크 목록",
"cmd.component_list": "구성 요소 목록",
"cmd.component_move_bodies": "바디를 구성 요소로 이동",
"cmd.component_move_sketches": "스케치를 구성 요소로 이동",
"cmd.component_paste_new": "새로 붙여넣기",
"cmd.component_properties": "구성 요소 물리 속성",
"cmd.component_rename": "구성 요소 이름 변경",
"cmd.component_revert_position": "위치 복원",
"cmd.component_update_links": "링크 업데이트",
"cmd.config_activate": "구성 활성화",
"cmd.config_column": "구성 열",
"cmd.config_configure": "구성",
"cmd.config_preview": "구성 미리 보기",
"cmd.config_row": "구성 행",
"cmd.config_rule": "구성 규칙",
"cmd.config_rules": "구성 규칙",
"cmd.config_table": "구성 표시",
"cmd.construct_axis_edge": "모서리를 통과하는 축",
"cmd.construct_axis_normal_to_face": "면에 수직인 축",
"cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축",
"cmd.construct_axis_two_planes": "두 평면을 통과하는 축",
"cmd.construct_axis_two_points": "두 점을 통과하는 축",
"cmd.construct_plane_along_path": "경로를 따르는 평면",
"cmd.construct_plane_midplane": "중간 평면",
"cmd.construct_plane_perpendicular": "수직 평면",
"cmd.construct_plane_tangent": "접선 평면",
"cmd.construct_plane_three_points": "세 점을 통과하는 평면",
"cmd.construct_plane_two_edges": "두 모서리를 통과하는 평면",
"cmd.construct_point_along_path": "경로를 따르는 점",
"cmd.construct_point_center": "원/구/토러스 중심의 점",
"cmd.construct_point_edge_plane": "모서리와 평면의 점",
"cmd.construct_point_three_planes": "세 평면이 만나는 점",
"cmd.construct_point_two_edges": "두 모서리가 만나는 점",
"cmd.construct_point_vertex": "꼭짓점의 점",
"cmd.contact_check": "접촉 확인",
"cmd.contact_create": "새 접촉 집합",
"cmd.contact_disable_all": "접촉 비활성화",
"cmd.contact_edit": "접촉 집합 편집",
"cmd.contact_enable_all": "모든 접촉 활성화",
"cmd.contact_enable_sets": "접촉 집합 활성화",
"cmd.contact_list": "접촉 집합 목록",
"cmd.diagnostics_crash_report": "충돌 보고서",
"cmd.diagnostics_crash_reports": "충돌 보고서",
"cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제",
"cmd.diagnostics_crash_test": "충돌 테스트",
"cmd.doc_recover": "설계 복구",
"cmd.doc_recovery_discard": "복구된 설계 폐기",
"cmd.doc_recovery_list": "복구 가능한 설계",
"cmd.document_inspect": "설계 검사",
"cmd.document_units": "설계 단위 변경",
"cmd.edit_checkpoint": "체크포인트",
"cmd.edit_restore_checkpoint": "체크포인트 복원",
"cmd.engine_commands": "명령 목록",
"cmd.explode_create": "분해 보기",
"cmd.explode_delete": "분해 보기 삭제",
"cmd.explode_list": "분해 보기 목록",
"cmd.explode_show": "분해 보기 표시",
"cmd.expr_evaluate": "식 평가",
"cmd.face_delete": "면 삭제",
"cmd.file_export": "내보내기",
"cmd.file_insert_mesh": "메쉬 삽입",
"cmd.file_insert_step": "STEP 삽입",
"cmd.file_save_mesh": "메쉬로 저장",
"cmd.inspect_accessibility": "접근성 분석",
"cmd.inspect_center_of_mass": "무게중심",
"cmd.inspect_curvature_comb": "곡률빗 분석",
"cmd.inspect_curvature_map": "곡률맵 분석",
"cmd.inspect_draft": "블록각 분석",
"cmd.inspect_environment_map": "환경 맵 분석",
"cmd.inspect_interference": "간섭",
"cmd.inspect_isocurve": "등가곡선 분석",
"cmd.inspect_minimum_radius": "최소 반경 분석",
"cmd.inspect_zebra": "얼룩말줄무늬 분석",
"cmd.joint_as_built": "조립 상태 관절",
"cmd.joint_create": "관절",
"cmd.joint_delete": "관절 삭제",
"cmd.joint_drive": "관절 구동",
"cmd.joint_edit": "관절 편집",
"cmd.joint_limits": "관절 제한",
"cmd.joint_list": "관절 목록",
"cmd.joint_motion_link": "모션 링크",
"cmd.joint_origin": "관절 원점",
"cmd.joint_rigid_group": "강체 그룹",
"cmd.joint_solve": "관절 해석",
"cmd.material_assign": "물리 재질",
"cmd.material_create": "새 물리 재질",
"cmd.material_delete": "물리 재질 삭제",
"cmd.material_edit": "물리 재질 편집",
"cmd.material_list": "재질 목록",
"cmd.mesh_convert": "메쉬 변환",
"cmd.model_edges": "모서리 목록",
"cmd.model_faces": "면 목록",
"cmd.model_threads": "나사산 목록",
"cmd.motion_delete": "운동 연구 삭제",
"cmd.motion_export": "운동 연구 내보내기",
"cmd.motion_list": "운동 연구 목록",
"cmd.motion_play": "운동 연구 재생",
"cmd.motion_study": "운동 연구",
"cmd.occurrence_copy": "붙여넣기(인스턴스)",
"cmd.occurrence_ground": "고정",
"cmd.occurrence_move": "인스턴스 이동",
"cmd.occurrence_properties": "인스턴스 물리 속성",
"cmd.parameters_add": "사용자 매개 변수 추가",
"cmd.parameters_change": "매개 변수 변경",
"cmd.parameters_comment": "매개 변수에 설명 추가",
"cmd.parameters_complete": "식 완성",
"cmd.parameters_delete": "매개 변수 삭제",
"cmd.parameters_export": "매개 변수 내보내기",
"cmd.parameters_favorite": "즐겨찾는 매개 변수",
"cmd.parameters_graph": "매개 변수 그래프",
"cmd.parameters_import": "매개 변수 가져오기",
"cmd.parameters_list": "매개 변수 목록",
"cmd.parameters_rename": "매개 변수 이름 변경",
"cmd.parameters_users": "매개 변수 사용처",
"cmd.parts_fastener": "체결 부품 삽입",
"cmd.parts_insert": "부품 삽입",
"cmd.parts_library": "표준 부품",
"cmd.parts_resize": "부품 크기 변경",
"cmd.plastic_assign_rule": "플라스틱 규칙 지정",
"cmd.plastic_boss": "보스",
"cmd.plastic_lip": "리프",
"cmd.plastic_manage_rules": "플라스틱 규칙 관리",
"cmd.plastic_rest": "레스트",
"cmd.plastic_rules": "플라스틱 규칙 목록",
"cmd.plastic_snap_fit": "스냅핏",
"cmd.select_clear": "선택 지우기",
"cmd.sheet_convert": "판금으로 변환",
"cmd.sheet_export_bend_table": "벤딩 표 내보내기",
"cmd.sheet_export_dxf": "전개도를 DXF로 내보내기",
"cmd.sheet_flange": "플랜지",
"cmd.sheet_flat_pattern": "전개도 만들기",
"cmd.sheet_fold": "접기",
"cmd.sheet_hem": "헴",
"cmd.sheet_manage_rules": "판금 규칙",
"cmd.sheet_refold": "다시 접기",
"cmd.sheet_rules": "판금 규칙 목록",
"cmd.sheet_unfold": "펼치기",
"cmd.sketch_arc_center_point": "중심점 호",
"cmd.sketch_arc_tangent": "접선 호",
"cmd.sketch_arc_three_point": "3점 호",
"cmd.sketch_auto_constrain": "자동 구속",
"cmd.sketch_auto_constrain_from_datum": "기준으로 자동 구속",
"cmd.sketch_auto_project": "참조에 모서리 자동 투영",
"cmd.sketch_blend_curve": "블렌드 곡선",
"cmd.sketch_chain": "연속",
"cmd.sketch_chamfer_distance_angle": "거리와 각도 모따기",
"cmd.sketch_chamfer_equal_distance": "등거리 모따기",
"cmd.sketch_chamfer_two_distance": "두 거리 모따기",
"cmd.sketch_circle_center": "중심 지름 원",
"cmd.sketch_circle_three_point": "3점 원",
"cmd.sketch_circle_three_tangent": "3접선 원",
"cmd.sketch_circle_two_point": "2점 원",
"cmd.sketch_circle_two_tangent": "2접선 원",
"cmd.sketch_constrainer": "스케치 구속기",
"cmd.sketch_constraint_fix": "고정/고정 해제",
"cmd.sketch_constraint_midpoint": "중간점",
"cmd.sketch_construction": "일반/구성선",
"cmd.sketch_delete": "스케치 개체 삭제",
"cmd.sketch_dimension_text": "치수 텍스트 이동",
"cmd.sketch_drag": "스케치 기하 드래그",
"cmd.sketch_drag_end": "스케치 드래그 종료",
"cmd.sketch_draw_dimensioned": "치수가 있는 스케치 개체 그리기",
"cmd.sketch_edit": "스케치 편집",
"cmd.sketch_edit_text": "텍스트 편집",
"cmd.sketch_export_dxf": "DXF로 저장",
"cmd.sketch_finish_auto_constrain": "자동 구속으로 완료",
"cmd.sketch_fit_curves_to_section": "메시 단면에 곡선 맞추기",
"cmd.sketch_glyphs": "구속 기호",
"cmd.sketch_include_3d": "3D 기하 포함",
"cmd.sketch_insert_dxf": "DXF 삽입",
"cmd.sketch_insert_svg": "SVG 삽입",
"cmd.sketch_inspect": "스케치 검사",
"cmd.sketch_intersection_curve": "교차 곡선",
"cmd.sketch_isoparametric_curve": "등매개 곡선",
"cmd.sketch_line3d": "3D 선",
"cmd.sketch_line_midpoint": "중점 선",
"cmd.sketch_move": "스케치 개체 이동/복사",
"cmd.sketch_move3d": "3D 점 이동",
"cmd.sketch_move_point": "스케치 점 드래그",
"cmd.sketch_offset_break": "오프셋 연결 끊기",
"cmd.sketch_options": "스케치 팔레트 옵션",
"cmd.sketch_point3d": "3D 점",
"cmd.sketch_polygon_circumscribed": "외접 다각형",
"cmd.sketch_polygon_edge": "변 다각형",
"cmd.sketch_polygon_inscribed": "내접 다각형",
"cmd.sketch_project_to_surface": "곡면으로 투영",
"cmd.sketch_rectangle_center": "중심 사각형",
"cmd.sketch_rectangle_three_point": "3점 사각형",
"cmd.sketch_rectangle_two_point": "2점 사각형",
"cmd.sketch_redefine": "스케치 평면 재정의",
"cmd.sketch_slot_arc_center": "중심점 호 슬롯",
"cmd.sketch_slot_arc_three_point": "3점 호 슬롯",
"cmd.sketch_slot_center_point": "중심점 슬롯",
"cmd.sketch_slot_center_to_center": "중심에서 중심 슬롯",
"cmd.sketch_slot_overall": "전장 슬롯",
"cmd.sketch_snap": "스냅 추론",
"cmd.sketch_solve": "스케치 해석",
"cmd.sketch_spline3d": "3D 스플라인",
"cmd.sketch_spline_control_point": "제어점 스플라인",
"cmd.sketch_spline_control_point_5": "제어점 스플라인(5차)",
"cmd.sketch_spline_fit_point": "맞춤점 스플라인",
"cmd.sketch_spun_profile": "회전 프로파일",
"cmd.sketch_toggle_driven": "종속 치수 전환",
"cmd.solid_align": "정렬",
"cmd.solid_boundary_fill": "경계 채우기",
"cmd.solid_bounding_solid": "둘러싸는 솔리드",
"cmd.solid_emboss": "엠보스",
"cmd.solid_move": "이동/복사",
"cmd.solid_offset_face": "면 오프셋",
"cmd.solid_press_pull": "밀기/당기기",
"cmd.solid_remove": "제거",
"cmd.solid_replace_face": "면 교체",
"cmd.solid_rib": "리브",
"cmd.solid_split_face": "면 분할",
"cmd.solid_web": "웹",
"cmd.surface_patch": "패치",
"cmd.surface_stitch": "재봉",
"cmd.surface_thicken": "두께 주기",
"cmd.surface_extrude": "서피스 돌출",
"cmd.surface_revolve": "서피스 회전",
"cmd.surface_sweep": "서피스 스윕",
"cmd.surface_loft": "서피스 로프트",
"cmd.surface_ruled": "룰드 서피스",
"cmd.surface_boundary": "바운더리 서피스",
"cmd.surface_offset": "오프셋 서피스",
"cmd.surface_blend": "블렌드 서피스",
"cmd.surface_fillet": "서피스 필렛",
"cmd.surface_untrim": "트림 해제",
"cmd.surface_unstitch": "스티치 해제",
"cmd.surface_reverse_normal": "법선 반전",
"cmd.inspect_continuity": "연속성 분석",
"cmd.timeline_compute_all": "모두 다시 계산",
"cmd.timeline_copy": "특성 복사",
"cmd.timeline_dependents": "특성 종속 항목",
"cmd.timeline_inspect": "특성 검사",
"cmd.timeline_paste": "특성 붙여넣기",
"cmd.timeline_redefine": "특성 편집",
"cmd.timeline_rename": "이름 변경",
"cmd.timeline_reorder": "특성 순서 변경",
"cmd.timeline_roll_to": "이력 마커 이동",
"cmd.timeline_rollback": "타임라인 마커 이동",
"cmd.timeline_suppress": "특성 비활성화",
"cmd.view_apply": "이름 있는 보기 복원",
"cmd.view_delete": "이름 있는 보기 삭제",
"cmd.view_list": "이름 있는 보기 목록",
"cmd.view_rename": "이름 있는 보기 이름 변경",
"cmd.view_save": "새 이름 있는 보기",
"ui.export_ellipsis_keymap": "내보내기…",
"ui.heading_configure": "구성",
"ui.heading_constraints": "구속 조건",
"ui.heading_utilities": "유틸리티",
"BROWSER": "브라우저",
"Drive Joint": "조인트 구동",
"Edit Profile Sketch": "윤곽 스케치 편집",
"Find in Browser": "브라우저에서 찾기",
"Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기",
"Reset Toolbar": "도구 모음 재설정",
"Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)",
"Selection filter": "선택 필터",
"Revert Position": "위치 복원",
"Construction": "구성선",
"Faces": "면",
"Edges": "모서리",
"Vertices": "꼭짓점",
"Sketch geometry and profiles": "스케치 기하 및 프로파일",
"Sketch curves": "스케치 곡선",
"Sketch points": "스케치 점",
"Sketch dimensions": "스케치 치수",
"Sketch constraints": "스케치 구속 조건",
"Flat": "플랫",
"cmd.render_backends": "렌더링 백엔드",
"cmd.view_navigate": "보기 탐색",
"timeline.group": "새 특성 폴더",
"timeline.ungroup": "특성 폴더 제거",
"timeline.rename_group": "특성 폴더 이름 변경",
"timeline.move_to_group": "특성을 폴더로 이동",
"timeline.move_many": "타임라인 항목 이동",
"timeline.reorder_many": "특성 순서 변경",
"ui.show_construction_geometries": "구성 형상",
"ui.total_angle": "전진 각도",
"sketch_text.builtin_font": "{family}(내장)",
"sketch_text.font_search": "글꼴 검색",
"sketch_text.loading_fonts": "설치된 글꼴을 찾는 중…",
"sketch_text.use_font_file": "글꼴 파일 {path} 사용",
"sketch_text.bold": "굵게",
"sketch_text.italic": "기울임꼴",
"sketch_text.align_left": "왼쪽",
"sketch_text.align_center": "가운데",
"sketch_text.align_right": "오른쪽",
"sketch_text.path_hint": "텍스트가 따라갈 스케치 곡선: 여기를 클릭한 다음 곡선을 클릭",
"sketch_text.path_clear": "곡선에서 텍스트 떼기",
"sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다"
}
+961
View File
@@ -0,0 +1,961 @@
{
"browser.body_name": "实体{number}",
"browser.component_name": "组件{number}",
"browser.parameters": "参数 ({count})",
"browser.precision": "精度: {precision}",
"browser.sketch_name": "草图{number}",
"browser.units": "单位: {units}",
"cmd.appearance_assign": "外观",
"cmd.appearance_library": "外观库",
"cmd.appearance_list": "列出外观",
"cmd.body_rename": "重命名实体",
"cmd.browser_group": "新建组",
"cmd.browser_move": "移动浏览器项目",
"cmd.browser_order": "排列浏览器项目",
"cmd.browser_rename_group": "重命名组",
"cmd.browser_ungroup": "取消编组",
"cmd.browser_visibility": "显示/隐藏",
"cmd.canvas_calibrate": "校准画布",
"cmd.canvas_decal": "贴花",
"cmd.canvas_delete": "删除画布",
"cmd.canvas_edit": "编辑画布",
"cmd.canvas_insert": "画布",
"cmd.component_activate": "激活组件",
"cmd.component_break_link": "断开链接",
"cmd.component_capture_position": "捕获位置",
"cmd.component_create": "新建组件",
"cmd.component_delete": "删除组件",
"cmd.component_from_bodies": "由实体创建组件",
"cmd.component_insert_design": "插入设计",
"cmd.component_links": "列出链接",
"cmd.component_list": "列出组件",
"cmd.component_move_bodies": "将实体移至组件",
"cmd.component_move_sketches": "将草图移至组件",
"cmd.component_paste_new": "粘贴为新对象",
"cmd.component_properties": "组件物理属性",
"cmd.component_rename": "重命名组件",
"cmd.component_revert_position": "恢复位置",
"cmd.component_update_links": "更新链接",
"cmd.config_activate": "激活配置",
"cmd.config_column": "配置列",
"cmd.config_configure": "配置",
"cmd.config_preview": "预览配置",
"cmd.config_row": "配置行",
"cmd.config_rule": "配置规则",
"cmd.config_rules": "配置规则",
"cmd.config_table": "显示配置表",
"cmd.construct_axis_edge": "过边的轴",
"cmd.construct_axis_normal_to_face": "垂直于面的轴",
"cmd.construct_axis_through_cylinder": "过圆柱/圆锥/圆环的轴",
"cmd.construct_axis_two_planes": "过两个平面的轴",
"cmd.construct_axis_two_points": "过两点的轴",
"cmd.construct_plane_along_path": "沿路径的平面",
"cmd.construct_plane_midplane": "中平面",
"cmd.construct_plane_perpendicular": "垂直平面",
"cmd.construct_plane_tangent": "相切平面",
"cmd.construct_plane_three_points": "过三点的平面",
"cmd.construct_plane_two_edges": "过两条边的平面",
"cmd.construct_point_along_path": "沿路径的点",
"cmd.construct_point_center": "圆/球/圆环中心的点",
"cmd.construct_point_edge_plane": "边与平面交点",
"cmd.construct_point_three_planes": "过三个平面的点",
"cmd.construct_point_two_edges": "过两条边的点",
"cmd.construct_point_vertex": "顶点处的点",
"cmd.contact_check": "检查接触",
"cmd.contact_create": "新建接触集",
"cmd.contact_disable_all": "禁用接触",
"cmd.contact_edit": "编辑接触集",
"cmd.contact_enable_all": "启用全部接触",
"cmd.contact_enable_sets": "启用接触集",
"cmd.contact_list": "列出接触集",
"cmd.diagnostics_crash_report": "崩溃报告",
"cmd.diagnostics_crash_reports": "崩溃报告",
"cmd.diagnostics_crash_reports_clear": "删除崩溃报告",
"cmd.diagnostics_crash_test": "崩溃测试",
"cmd.doc_recover": "恢复设计",
"cmd.doc_recovery_discard": "丢弃已恢复的设计",
"cmd.doc_recovery_list": "可恢复的设计",
"cmd.document_inspect": "检查设计",
"cmd.document_units": "更改设计单位",
"cmd.edit_checkpoint": "检查点",
"cmd.edit_restore_checkpoint": "恢复检查点",
"cmd.engine_commands": "列出命令",
"cmd.explode_create": "爆炸图",
"cmd.explode_delete": "删除爆炸图",
"cmd.explode_list": "列出爆炸图",
"cmd.explode_show": "显示爆炸图",
"cmd.expr_evaluate": "计算表达式",
"cmd.face_delete": "删除面",
"cmd.file_export": "导出",
"cmd.file_insert_mesh": "插入网格",
"cmd.file_insert_step": "插入 STEP",
"cmd.file_save_mesh": "另存为网格",
"cmd.inspect_accessibility": "可达性分析",
"cmd.inspect_center_of_mass": "质心",
"cmd.inspect_curvature_comb": "曲率梳分析",
"cmd.inspect_curvature_map": "曲率图分析",
"cmd.inspect_draft": "拔模分析",
"cmd.inspect_environment_map": "环境贴图分析",
"cmd.inspect_interference": "干涉",
"cmd.inspect_isocurve": "等参线分析",
"cmd.inspect_minimum_radius": "最小半径分析",
"cmd.inspect_zebra": "斑马纹分析",
"cmd.joint_as_built": "现状接头",
"cmd.joint_create": "接头",
"cmd.joint_delete": "删除接头",
"cmd.joint_drive": "驱动接头",
"cmd.joint_edit": "编辑接头",
"cmd.joint_limits": "接头限制",
"cmd.joint_list": "列出接头",
"cmd.joint_motion_link": "运动关联",
"cmd.joint_origin": "接头原点",
"cmd.joint_rigid_group": "刚性组",
"cmd.joint_solve": "解算接头",
"cmd.material_assign": "物理材质",
"cmd.material_create": "新建物理材质",
"cmd.material_delete": "删除物理材质",
"cmd.material_edit": "编辑物理材质",
"cmd.material_list": "列出材质",
"cmd.mesh_convert": "转换网格",
"cmd.model_edges": "列出边",
"cmd.model_faces": "列出面",
"cmd.model_threads": "列出螺纹",
"cmd.motion_delete": "删除运动研究",
"cmd.motion_export": "导出运动研究",
"cmd.motion_list": "列出运动研究",
"cmd.motion_play": "播放运动研究",
"cmd.motion_study": "运动研究",
"cmd.occurrence_copy": "粘贴(实例)",
"cmd.occurrence_ground": "固定",
"cmd.occurrence_move": "移动实例",
"cmd.occurrence_properties": "实例物理属性",
"cmd.parameters_add": "添加用户参数",
"cmd.parameters_change": "更改参数",
"cmd.parameters_comment": "注释参数",
"cmd.parameters_complete": "补全表达式",
"cmd.parameters_delete": "删除参数",
"cmd.parameters_export": "导出参数",
"cmd.parameters_favorite": "收藏参数",
"cmd.parameters_graph": "参数关系图",
"cmd.parameters_import": "导入参数",
"cmd.parameters_list": "列出参数",
"cmd.parameters_rename": "重命名参数",
"cmd.parameters_users": "参数使用者",
"cmd.parts_fastener": "插入紧固件",
"cmd.parts_insert": "插入零件",
"cmd.parts_library": "标准零件",
"cmd.parts_resize": "更改零件尺寸",
"cmd.plastic_assign_rule": "指定塑料规则",
"cmd.plastic_boss": "凸台",
"cmd.plastic_lip": "凸缘",
"cmd.plastic_manage_rules": "管理塑料规则",
"cmd.plastic_rest": "垫块",
"cmd.plastic_rules": "列出塑料规则",
"cmd.plastic_snap_fit": "卡扣",
"cmd.select_clear": "清除选择",
"cmd.sheet_convert": "转换为钣金",
"cmd.sheet_export_bend_table": "导出折弯表",
"cmd.sheet_export_dxf": "将展开图导出为 DXF",
"cmd.sheet_flange": "法兰",
"cmd.sheet_flat_pattern": "创建展开图",
"cmd.sheet_fold": "折叠",
"cmd.sheet_hem": "卷边",
"cmd.sheet_manage_rules": "钣金规则",
"cmd.sheet_refold": "重新折弯",
"cmd.sheet_rules": "列出钣金规则",
"cmd.sheet_unfold": "展开",
"cmd.sketch_arc_center_point": "中心点圆弧",
"cmd.sketch_arc_tangent": "相切圆弧",
"cmd.sketch_arc_three_point": "三点圆弧",
"cmd.sketch_auto_constrain": "自动约束",
"cmd.sketch_auto_constrain_from_datum": "基于基准自动约束",
"cmd.sketch_auto_project": "自动投影边到参考",
"cmd.sketch_blend_curve": "混合曲线",
"cmd.sketch_chain": "连续链",
"cmd.sketch_chamfer_distance_angle": "距离和角度倒角",
"cmd.sketch_chamfer_equal_distance": "等距倒角",
"cmd.sketch_chamfer_two_distance": "双距离倒角",
"cmd.sketch_circle_center": "圆心直径圆",
"cmd.sketch_circle_three_point": "三点圆",
"cmd.sketch_circle_three_tangent": "三切线圆",
"cmd.sketch_circle_two_point": "两点圆",
"cmd.sketch_circle_two_tangent": "两切线圆",
"cmd.sketch_constrainer": "草图约束器",
"cmd.sketch_constraint_fix": "固定/解除固定",
"cmd.sketch_constraint_midpoint": "中点",
"cmd.sketch_construction": "普通/构造",
"cmd.sketch_delete": "删除草图图元",
"cmd.sketch_dimension_text": "移动尺寸文字",
"cmd.sketch_drag": "拖动草图几何",
"cmd.sketch_drag_end": "结束草图拖动",
"cmd.sketch_draw_dimensioned": "绘制带尺寸的草图图元",
"cmd.sketch_edit": "编辑草图",
"cmd.sketch_edit_text": "编辑文字",
"cmd.sketch_export_dxf": "另存为 DXF",
"cmd.sketch_finish_auto_constrain": "使用自动约束完成",
"cmd.sketch_fit_curves_to_section": "将曲线拟合到网格截面",
"cmd.sketch_glyphs": "约束符号",
"cmd.sketch_include_3d": "包含三维几何",
"cmd.sketch_insert_dxf": "插入 DXF",
"cmd.sketch_insert_svg": "插入 SVG",
"cmd.sketch_inspect": "检查草图",
"cmd.sketch_intersection_curve": "相交曲线",
"cmd.sketch_isoparametric_curve": "等参数曲线",
"cmd.sketch_line3d": "三维直线",
"cmd.sketch_line_midpoint": "中点线",
"cmd.sketch_move": "移动/复制草图图元",
"cmd.sketch_move3d": "移动三维点",
"cmd.sketch_move_point": "拖动草图点",
"cmd.sketch_offset_break": "断开偏移关联",
"cmd.sketch_options": "草图选项板选项",
"cmd.sketch_point3d": "三维点",
"cmd.sketch_polygon_circumscribed": "外切多边形",
"cmd.sketch_polygon_edge": "边长多边形",
"cmd.sketch_polygon_inscribed": "内接多边形",
"cmd.sketch_project_to_surface": "投影到曲面",
"cmd.sketch_rectangle_center": "中心矩形",
"cmd.sketch_rectangle_rounded": "圆角矩形",
"cmd.sketch_rectangle_three_point": "三点矩形",
"cmd.sketch_rectangle_two_point": "两点矩形",
"cmd.sketch_redefine": "重定义草图平面",
"cmd.sketch_slot_arc_center": "中心点圆弧槽",
"cmd.sketch_slot_arc_three_point": "三点圆弧槽",
"cmd.sketch_slot_center_point": "中心点槽",
"cmd.sketch_slot_center_to_center": "中心距槽",
"cmd.sketch_slot_overall": "全长槽",
"cmd.sketch_snap": "捕捉推断",
"cmd.sketch_solve": "解算草图",
"cmd.sketch_spline3d": "三维样条曲线",
"cmd.sketch_spline_control_point": "控制点样条曲线",
"cmd.sketch_spline_control_point_5": "控制点样条曲线(5 次)",
"cmd.sketch_spline_fit_point": "拟合点样条曲线",
"cmd.sketch_spun_profile": "旋转轮廓",
"cmd.sketch_toggle_driven": "切换被驱动尺寸",
"cmd.solid_align": "对齐",
"cmd.solid_boundary_fill": "边界填充",
"cmd.solid_bounding_solid": "包围实体",
"cmd.solid_emboss": "压印",
"cmd.solid_move": "移动/复制",
"cmd.solid_offset_face": "偏移面",
"cmd.solid_press_pull": "按压拖动",
"cmd.solid_remove": "移除",
"cmd.solid_replace_face": "替换面",
"cmd.solid_rib": "筋板",
"cmd.solid_split_face": "拆分面",
"cmd.solid_web": "网格筋",
"cmd.surface_patch": "补面",
"cmd.surface_stitch": "缝合",
"cmd.surface_thicken": "加厚",
"cmd.surface_extrude": "曲面拉伸",
"cmd.surface_revolve": "曲面旋转",
"cmd.surface_sweep": "曲面扫掠",
"cmd.surface_loft": "曲面放样",
"cmd.surface_ruled": "直纹曲面",
"cmd.surface_boundary": "边界曲面",
"cmd.surface_offset": "偏移曲面",
"cmd.surface_blend": "混合曲面",
"cmd.surface_fillet": "曲面圆角",
"cmd.surface_untrim": "取消修剪",
"cmd.surface_unstitch": "取消缝合",
"cmd.surface_reverse_normal": "反转法向",
"cmd.inspect_continuity": "连续性分析",
"cmd.timeline_compute_all": "全部重新計算",
"cmd.timeline_copy": "复制特征",
"cmd.timeline_dependents": "特征依存项",
"cmd.timeline_inspect": "检查特征",
"cmd.timeline_paste": "粘贴特征",
"cmd.timeline_redefine": "编辑特征",
"cmd.timeline_rename": "重命名",
"cmd.timeline_reorder": "重排特征",
"cmd.timeline_roll_to": "滚动历史标记",
"cmd.timeline_rollback": "移动时间线标记",
"cmd.timeline_suppress": "抑制特征",
"cmd.view_apply": "还原命名视图",
"cmd.view_delete": "删除命名视图",
"cmd.view_list": "列出命名视图",
"cmd.view_rename": "重命名命名视图",
"cmd.view_save": "新建命名视图",
"dialog.custom_axis": "↖ 自定义轴",
"dialog.edit_title": "编辑 {title}",
"dialog.select": "↖ 选择",
"dialog.selected_count": "↖ 已选择 {count} 个",
"dialog.step": "步骤",
"dialog.selection_step": "第 {step} 步,共 {total} 步:选择{input}",
"dialog.selection_guidance": "选择输入项进行修改。点击已选对象可取消选择;按住 Ctrl/Cmd 点击也可切换选择。检查预览后,再按“确定”。",
"document.save_changes": "保存对 {name} 的更改?",
"document.unsaved_changes": "{name} 有未保存的更改。如果不保存,将丢失这些更改。",
"settings.msaa_applies_on_restart": "抗锯齿将在下次启动 SolidCraft 时生效",
"settings.msaa_reset": "{setting}: {requested}× 不受此图形设备支持;已重置为 {reset}× (支持: {supported})。",
"settings.msaa_unsupported": "此图形设备不支持",
"settings.reset_intro": "这些设置在此计算机上无法正常工作,会导致 SolidCraft 无法启动,因此已更改:",
"settings.reset_title": "设置已更改",
"sketch.degrees_of_freedom": "{count} 个自由度",
"sketch.draw_curves": "绘制为 {linetype}",
"sketch.make_selection": "将选择转换为 {linetype}",
"ui.1_degree_of_freedom": "1 个自由度",
"ui.3d_mouse_speed": "3D 鼠标速度",
"ui.3d_mouse_windows": "3D 鼠标(Windows)",
"ui.3d_sketch": "3D 草图",
"ui.about": "关于",
"ui.activate": "激活",
"ui.active_component": "活动组件",
"ui.agent_cursor": "代理光标",
"ui.agent_cursor_speed": "代理光标速度",
"ui.all": "全部",
"ui.angle": "角度",
"ui.angle_decimals": "角度小数位",
"ui.animate_view_changes": "视图变化动画",
"ui.animation": "动画",
"ui.animation_speed": "动画速度",
"ui.anti_aliasing": "抗锯齿",
"ui.apply": "应用",
"ui.arc": "圆弧",
"ui.assemble": "装配",
"ui.autosave_every": "自动保存间隔",
"ui.axes": "轴",
"ui.axis": "轴",
"ui.base_point": "基点",
"ui.bodies": "实体",
"ui.box": "长方体",
"ui.break": "打断",
"ui.cancel": "取消",
"ui.canvases": "画布",
"ui.center": "中心",
"ui.centerline": "中心线",
"ui.chain": "链",
"ui.chamfer": "倒角",
"ui.chamfer_type": "倒角类型",
"ui.change_workspace": "更改工作区",
"ui.chord_length": "弦长",
"ui.curvature_g2": "曲率连续 (G2)",
"ui.equivalent_radius": "等效半径",
"ui.circle": "圆",
"ui.circular_pattern": "圆形阵列",
"ui.clear": "清除",
"ui.clear_recent": "清除最近",
"ui.clear_the_selection": "清除选择",
"ui.click_selects_whole_bodies": "单击选择整个实体",
"ui.click_then_press_key": "单击,然后按新按键",
"ui.close": "关闭",
"ui.coil": "螺旋",
"ui.coincident": "重合",
"ui.collapse": "折叠",
"ui.collinear": "共线",
"ui.combine": "组合",
"ui.coming_later": "即将推出",
"ui.concentric": "同心",
"ui.configurations": "配置",
"ui.conic_curve": "圆锥曲线",
"ui.constant": "恒定",
"ui.constraints_conflict": "约束冲突",
"ui.construct": "构造",
"ui.construction": "构造",
"ui.context_menu": "上下文菜单",
"ui.copy": "复制",
"ui.corners": "角点",
"ui.corner": "角",
"ui.counterbore": "沉头孔",
"ui.countersink": "锥形沉孔",
"ui.create": "创建",
"ui.create_copy": "创建副本",
"ui.create_sketch": "创建草图",
"ui.current": "当前",
"ui.curvature": "曲率",
"ui.curves": "曲线",
"ui.cut": "剪切",
"ui.cylinder": "圆柱体",
"ui.dark": "深色",
"ui.default_modelling_orientation": "默认建模方向",
"ui.degrees_of_freedom": "自由度",
"ui.delete": "删除",
"ui.depth": "深度",
"ui.design": "设计",
"ui.diameter": "直径",
"ui.dimension": "尺寸",
"ui.direction": "方向",
"ui.distance": "距离",
"ui.distance_1": "距离 1",
"ui.distance_2": "距离 2",
"ui.document_settings": "文档设置",
"ui.don_t_save": "不保存",
"ui.draft": "拔模",
"ui.drawing": "绘图",
"ui.drill_point": "钻尖",
"ui.edit": "编辑",
"ui.ellipse": "椭圆",
"ui.end_radius": "结束半径",
"ui.entities": "实体",
"ui.equal": "相等",
"ui.expand": "展开",
"ui.export_ellipsis": "导出...",
"ui.export_ellipsis_keymap": "导出…",
"ui.export_keymap_tip": "将按键、S 框收藏和工具栏导出为文件",
"ui.extend": "延伸",
"ui.extent_type": "延伸类型",
"ui.extrude": "拉伸",
"ui.factor": "系数",
"ui.feature_history": "特征历史",
"ui.feature_options": "特征选项",
"ui.features": "特征",
"ui.file": "文件",
"ui.fillet": "圆角",
"ui.filter_commands_or_keys": "筛选命令或按键",
"ui.finish_sketch": "完成草图",
"ui.fix_unfix": "固定/取消固定",
"ui.flat": "平面",
"ui.flip": "翻转",
"ui.front": "前视",
"ui.fully_constrained": "完全约束",
"ui.general": "常规",
"ui.graphics": "图形",
"ui.ground_shadow": "地面阴影",
"ui.heading_add_ins": "加载项",
"ui.heading_assemble": "装配",
"ui.heading_box": "长方体",
"ui.heading_chamfer": "倒角",
"ui.heading_circular_pattern": "圆形阵列",
"ui.heading_coil": "螺旋",
"ui.heading_combine": "组合",
"ui.heading_configure": "配置",
"ui.heading_constraints": "约束",
"ui.heading_construct": "构造",
"ui.heading_create": "创建",
"ui.heading_create_sketch": "创建草图",
"ui.heading_cylinder": "圆柱体",
"ui.heading_design": "设计",
"ui.heading_draft": "拔模",
"ui.heading_edit": "编辑",
"ui.heading_extrude": "拉伸",
"ui.heading_fillet": "圆角",
"ui.heading_finish_sketch": "完成草图",
"ui.heading_front": "前视",
"ui.heading_hole": "孔",
"ui.heading_home": "主页",
"ui.heading_insert": "插入",
"ui.heading_inspect": "检查",
"ui.heading_loft": "放样",
"ui.heading_make": "制作",
"ui.heading_measure": "测量",
"ui.heading_mesh": "网格",
"ui.heading_mirror": "镜像",
"ui.heading_modify": "修改",
"ui.heading_move": "移动",
"ui.heading_offset": "偏移",
"ui.heading_move_copy": "移动/复制",
"ui.heading_offset_plane": "偏移平面",
"ui.heading_ok": "确定",
"ui.heading_parameters": "参数",
"ui.heading_pattern_on_path": "路径阵列",
"ui.heading_pipe": "管道",
"ui.heading_plane_at_angle": "角度平面",
"ui.heading_plastic": "塑料",
"ui.heading_prepare": "准备",
"ui.heading_rectangular_pattern": "矩形阵列",
"ui.heading_revolve": "旋转",
"ui.heading_right": "右视",
"ui.heading_scale": "缩放",
"ui.heading_section_analysis": "截面分析",
"ui.heading_select": "选择",
"ui.heading_sheet_metal": "钣金",
"ui.heading_shell": "抽壳",
"ui.heading_sketch": "草图",
"ui.heading_sketch_palette": "草图面板",
"ui.heading_sketch_scale": "草图缩放",
"ui.heading_solid": "实体",
"ui.heading_sphere": "球体",
"ui.heading_split_body": "分割实体",
"ui.heading_surface": "曲面",
"ui.heading_sweep": "扫掠",
"ui.heading_thread": "螺纹",
"ui.heading_top": "上视",
"ui.heading_torus": "圆环体",
"ui.heading_utilities": "工具",
"ui.heading_utility": "实用工具",
"ui.help": "帮助",
"ui.hide": "隐藏",
"ui.hole": "孔",
"ui.hole_tap_type": "孔攻丝类型",
"ui.hole_type": "孔类型",
"ui.home": "主页",
"ui.horizontal_vertical": "水平/垂直",
"ui.import_ellipsis": "导入…",
"ui.insert": "插入",
"ui.inspect": "检查",
"ui.intersect": "相交",
"ui.join": "连接",
"ui.joints": "关节",
"ui.keyboard_shortcuts": "键盘快捷键",
"ui.language": "语言",
"ui.length_decimals": "长度小数位",
"ui.light": "浅色",
"ui.line": "直线",
"ui.linetype": "线型",
"ui.loft": "放样",
"ui.look_at": "朝向",
"ui.look_at_anim_desc": "轴视图、注视、主视图与适合窗口",
"ui.manufacture": "制造",
"ui.measure": "测量",
"ui.midpoint": "中点",
"ui.mirror": "镜像",
"ui.mirror_line": "镜像线",
"ui.modify": "修改",
"ui.move": "移动",
"ui.multiple": "多个",
"ui.named_views": "命名视图",
"ui.navigation": "导航",
"ui.needs_design": "{command}: 请先打开一个设计",
"ui.new_body": "新建实体",
"ui.new_design": "新建设计",
"ui.normal": "法向",
"ui.object_type": "对象类型",
"ui.objects": "对象",
"ui.offer_to_stand_up_y_up_files": "提供站立 Y-up 文件",
"ui.offset": "偏移",
"ui.offset_plane": "偏移平面",
"ui.one_side": "单侧",
"ui.open": "打开",
"ui.open_ellipsis": "打开...",
"ui.open_preferences": "打开首选项",
"ui.open_recent": "打开最近",
"ui.operation": "操作",
"ui.options": "选项",
"ui.orbit_around": "环绕",
"ui.origin": "原点",
"ui.palette_points": "点",
"ui.palette_profile": "轮廓",
"ui.pan_zoom_orbit": "平移、缩放、环绕",
"ui.parallel": "平行",
"ui.parameters": "参数",
"ui.paste": "粘贴",
"ui.path": "路径",
"ui.pattern_on_path": "路径阵列",
"ui.perpendicular": "垂直",
"ui.perspective_view": "透视图",
"ui.pipe": "管道",
"ui.placement": "放置",
"ui.plane": "平面",
"ui.plane_at_angle": "角度平面",
"ui.point": "点",
"ui.point_under_the_cursor": "光标下的点",
"ui.points": "点",
"ui.polygon": "多边形",
"ui.preferences": "首选项",
"ui.press_a_key": "按一个键…",
"ui.profile": "轮廓",
"ui.profile_plane": "轮廓平面",
"ui.profiles": "轮廓",
"ui.project": "投影",
"ui.project_face_edges_sketch": "将面的边投影到新草图",
"ui.quantity_2": "数量 2",
"ui.quantity": "数量",
"ui.quit": "退出",
"ui.radial_context_menu": "环形上下文菜单",
"ui.radius": "半径",
"ui.radius_type": "半径类型",
"ui.rectangle": "矩形",
"ui.rectangular_pattern": "矩形阵列",
"ui.redo": "重做",
"ui.render": "渲染",
"ui.renderer": "渲染器",
"ui.reset": "重置",
"ui.reset_all": "全部重置",
"ui.reset_the_settings_on_this_page": "重置此页面上的设置",
"ui.restore_defaults": "恢复默认值",
"ui.reverse_zoom_direction": "反转缩放方向",
"ui.revolve": "旋转",
"ui.right": "右视",
"ui.save": "保存",
"ui.save_as": "另存为",
"ui.save_as_ellipsis": "另存为...",
"ui.scale": "缩放",
"ui.section_analysis": "截面分析",
"ui.select": "选择",
"ui.select_other_hint": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。",
"ui.selection": "选择",
"ui.shell": "抽壳",
"ui.show": "显示",
"ui.show_constraints": "约束",
"ui.show_dimensions": "尺寸",
"ui.show_points": "显示点",
"ui.show_profile": "显示轮廓",
"ui.show_projected_geometries": "投影几何",
"ui.simple": "简单",
"ui.simulation": "仿真",
"ui.single": "单个",
"ui.sketch": "草图",
"ui.sketch_dimension": "草图尺寸",
"ui.sketch_grid": "草图网格",
"ui.sketch_scale": "草图比例",
"ui.sketch_status": "草图状态",
"ui.sketches": "草图",
"ui.slice": "切片",
"ui.slot": "槽",
"ui.smooth": "平滑",
"ui.smooth_wheel_zoom": "平滑滚轮缩放",
"ui.smooth_wheel_zoom_tip": "关闭后每个滚轮事件立即生效,不做平滑延迟",
"ui.snap": "捕捉",
"ui.space_mouse_hint": "同时移动和旋转视图;草图中仅支持平移和缩放。",
"ui.space_mouse_navigate": "使用 SpaceNavigator / SpaceMouse 导航",
"ui.spacing_2": "间距 2",
"ui.spacing": "间距",
"ui.sphere": "球体",
"ui.spline": "样条曲线",
"ui.split_body": "分割实体",
"ui.stand_up_tip": "当 Z 朝上的 STEP 或 IGES 文件打开后,提示绕 X 轴旋转四分之一周",
"ui.start": "开始",
"ui.start_radius": "起始半径",
"ui.sweep": "扫掠",
"ui.symmetric": "对称",
"ui.symmetry": "对称",
"ui.system": "系统",
"ui.tangent": "相切",
"ui.taper_angle": "锥角",
"ui.tapped": "攻丝",
"ui.text": "文本",
"ui.theme": "主题",
"ui.thickness": "厚度",
"ui.thread": "螺纹",
"ui.timeline_bottom": "底部时间轴",
"ui.timeline_left_note": "左侧布局下,回退条显示在零件和场景浏览器的上方。",
"ui.timeline_left_sidebar": "左侧边栏",
"ui.tip_angle": "尖端角度",
"ui.to_object": "到对象",
"ui.top": "上视",
"ui.torus": "圆环体",
"ui.trackpad_pan_tip": "触控板手势用于平移(关闭:像滚轮一样缩放);鼠标滚轮缩放、拇指滚轮横向平移、双指捏合始终缩放",
"ui.trim": "修剪",
"ui.two_finger_scroll_pans": "双指滚动平移",
"ui.two_sides": "双侧",
"ui.ui_scale": "界面缩放",
"ui.undo": "撤销",
"ui.units": "单位",
"ui.units_of_new_designs": "新设计的单位",
"ui.untitled": "无标题",
"ui.up_axis_note": "视图中朝上的轴(轴视图、主视图、注视)。设计本身不变。",
"ui.variable": "变量",
"ui.view_centre": "视图中心",
"ui.visual_shaded_hidden_edges": "带隐藏边的着色",
"ui.visual_shaded_with_edges": "带边着色",
"ui.visual_style": "视觉样式",
"ui.visual_wireframe": "线框",
"ui.workspace": "工作区",
"ui.y_distance": "Y 距离",
"ui.x_distance": "X 距离",
"ui.y_up": "Y 朝上",
"ui.z_up": "Z 朝上",
"ui.zoom_to_cursor_tip": "滚轮向光标所在点缩放(关闭:向视图中心缩放)",
"ui.zoom_to_the_cursor": "缩放到光标",
"A new configuration, a copy of the active one": "新建配置,为当前配置的副本",
"About SolidCraft": "关于 SolidCraft",
"Active": "活动",
"Active rule": "活动规则",
"Add": "添加",
"Add Key": "添加按键",
"Add Mid Radius": "添加中间半径",
"Add Row": "添加行",
"All Sketch Points": "所有草图点",
"All open designs": "所有打开的设计",
"Allowed expressions, separated by semicolons": "允许的表达式,以分号分隔",
"Also deletes": "同时删除",
"Angle 2": "角度 2",
"Animate": "动画",
"Appearance name": "外观名称",
"Applied Radius": "应用半径",
"Apply To": "应用于",
"Area": "面积",
"Attachment": "附着",
"BROWSER": "浏览器",
"Bend Line Position": "弯折线位置",
"Bend Position": "折弯位置",
"Bend Radius": "折弯半径",
"Bends": "折弯",
"Body": "实体",
"Bounding box": "边界框",
"CSV or JSON: updates existing parameters, adds new ones": "CSV 或 JSON:更新现有参数、添加新参数",
"Capture All Positions": "捕获所有位置",
"Catch Direction": "卡扣方向",
"Cells": "区域",
"Center X": "中心 X",
"Center Y": "中心 Y",
"Center of mass": "质心",
"Chamfer Type": "倒角类型",
"Check collisions": "检查碰撞",
"Checked": "已检查",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "选择一个新平面。草图几何和尺寸在其二维坐标中保持不变。",
"Close draft": "关闭草稿",
"Collisions": "碰撞",
"Colour": "颜色",
"Component1": "组件1",
"Configuration": "配置",
"Configuration rules": "配置规则",
"Contact": "接触",
"Copy all licences": "复制所有许可证",
"Copy diagnostic information": "复制诊断信息",
"Copy report": "复制报告",
"Corner Relief Shape": "拐角释放形状",
"Counterbore depth": "沉孔深度",
"Countersink angle": "沉头角度",
"Create Issue — review draft": "创建 Issue——审阅草稿",
"Customize Toolbar": "自定义工具栏",
"Delete all": "全部删除",
"Delete rule": "删除规则",
"Delete the row": "删除此行",
"Delete the set": "删除此集合",
"Delete this parameter": "删除此参数",
"Density": "密度",
"Designs you open or save appear here.": "你打开或保存的设计会显示在此处。",
"Determined by To Object. Choose Angle to edit the angular extent.": "由“到对象”确定。选择“角度”可编辑角度范围。",
"Discard": "放弃",
"Double-click to rename": "双击重命名",
"Drive Joint": "驱动接头",
"Edit Profile Sketch": "编辑轮廓草图",
"Enable rule": "启用规则",
"Enabled": "已启用",
"Explode": "爆炸",
"Extents": "范围",
"Face attachment lost": "面附着丢失",
"Faces / edges / vertices": "面 / 边 / 顶点",
"Favourite": "收藏",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "许多 CAD 程序的文件以 Y 为上轴,因此在此处打开时会平躺(Z 朝上)。",
"Filter": "筛选",
"Find in Browser": "在浏览器中查找",
"Flat size": "平板尺寸",
"Follow camera (Look At sketches)": "跟随相机(注视草图)",
"Font": "字体",
"Full Length": "全长",
"Gap": "间隙",
"Grid": "网格",
"Height": "高度",
"Home view": "主视图",
"In This Design": "在此设计中",
"Interferes": "发生干涉",
"Issue title": "Issue 标题",
"Joint Origin1": "接头原点1",
"Keep Tools": "保留工具",
"Later": "稍后",
"Leave it": "保持原样",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "旧版面附着。重新绑定到经验证的面可创建精确附着。",
"Length": "长度",
"Length units": "长度单位",
"Library": "零件库",
"Loop": "循环",
"Margin": "余量",
"Mass": "质量",
"Material": "材质",
"Mid Radius": "中间半径",
"Modeled": "已建模",
"More toolbar panels": "更多工具栏面板",
"Motion": "运动",
"Move Type": "移动类型",
"Move earlier": "向前移动",
"Move later": "向后移动",
"Name": "名称",
"No crash reports.": "没有崩溃报告。",
"No matching appearances": "没有匹配的外观",
"Not available yet": "暂不可用",
"Opacity": "不透明度",
"Open GitHub draft": "打开 GitHub 草稿",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "使用 Rust 编写的开源参数化 3D CAD。基于 MIT OR Apache-2.0 许可。",
"Other side": "另一侧",
"Parameter values": "参数值",
"Per Edge": "每条边",
"Pick the tool bodies": "选择工具实体",
"Place at the origin instead": "改为放置在原点",
"Playback": "播放",
"Position": "位置",
"Position (0 to 1)": "位置(0 到 1)",
"Position along the edge, 0 at the start end, 1 at the other": "沿边的位置,起点端为 0,另一端为 1",
"Preview leaves the current design unchanged.": "预览不会更改当前设计。",
"RECENT": "最近使用",
"Radius Fit": "半径拟合",
"Ratio": "比率",
"Recover": "恢复",
"Recover unsaved design?": "恢复未保存的设计?",
"Remove Tools": "移除工具",
"Remove from list": "从列表移除",
"Remove private design details or paths before sharing.": "共享前请移除私密设计细节或路径。",
"Remove the column": "移除此列",
"Remove the track": "移除轨迹",
"Remove this point": "移除此点",
"Report a bug — review a screenshot and open a GitHub draft": "报告缺陷 — 复核截图并开启 GitHub 草稿",
"Reset Toolbar": "重置工具栏",
"Restore the inherited component or physical material appearance": "还原继承的组件或物理材质外观",
"Result": "结果",
"Retake screenshot": "重新截图",
"Reverse": "反向",
"Reverse Direction": "反向",
"Revert": "还原",
"Revert All Positions": "还原所有位置",
"Review the report and screenshot before opening GitHub.": "打开 GitHub 前请审阅报告和截图。",
"Rim Edge": "轮缘边",
"Roll History Marker Here": "将历史标记滚动到此处",
"Rule": "规则",
"Saved preview. Geometry is being checked before editing.": "已保存预览。编辑前正在检查几何。",
"Scale factor": "比例因子",
"Search": "搜索",
"Search commands": "搜索命令",
"Select bodies (a face picks its body)": "选择实体(单击面即选择其所属实体)",
"Select bodies or faces instead": "改为选择实体或面",
"Selection filter": "选择筛选",
"Semantic faces share their appearance across instances.": "语义面在各实例间共享外观。",
"Shape": "形状",
"Show Agent Cursor": "显示代理光标",
"Show in folder": "在文件夹中显示",
"Size": "大小",
"Sketch plane has no body-face attachment.": "草图平面没有实体面附着。",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在这些设计保存前已关闭。可以打开它们的上次自动保存。",
"Speed": "速度",
"Stand it up": "将其立起",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "将其立起:绕 X 轴旋转四分之一周,作为一个可撤销的移动特征。",
"Stand this part up?": "立起此零件?",
"Step": "步骤",
"Steps": "步数",
"Suppressed": "已抑制",
"Swap Ends": "交换端点",
"Tangent Chain": "相切链",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "虚线草图是已保存的平面引导。选择一个新面以修复附着。",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "保存的作者实例缺失。重新绑定前请选择有效的作者放置。",
"Then": "操作",
"These features will be deleted:": "以下特征将被删除:",
"These features will fail:": "以下特征将失败:",
"This child is shared. Deleting it removes it from every copy of its parent.": "此子项是共享的。删除它会从其父项的每个副本中移除。",
"This child is shared. Position changes apply to every copy of its parent.": "此子项是共享的。位置更改会应用到其父项的每个副本。",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "此面无法提供持久附着。请选择其他面或显式使用固定平面。",
"To Offset": "偏移",
"Tolerance": "公差",
"Type": "类型",
"Updating the preview": "正在更新预览",
"Use fixed plane": "使用固定平面",
"VIEW": "视图",
"Value": "值",
"Volume": "体积",
"Wall": "壁厚",
"When": "条件",
"Width": "宽度",
"Will fail": "将失败",
"angle, height": "角度、高度",
"command name": "命令名称",
"expression, e.g. 25 mm": "表达式,例如 25 mm",
"fits the face, e.g. M8": "适配该面,例如 M8",
"from the rule": "来自规则",
"needs two joints": "需要两个接头",
"new configuration": "新配置",
"new name": "新名称",
"new parameter": "新参数",
"no interference": "无干涉",
"no joint that moves yet": "尚无运动接头",
"no picture": "无图片",
"pick a hole to seat it, or OK places it at the origin": "选择一个孔放置,或单击确定放置在原点",
"pick one or two vertices, edges or faces": "选择一个或两个顶点、边或面",
"the parts library is not available": "零件库不可用",
"to the next face": "到下一个面",
"Revert Position": "恢复位置",
"Construction": "构造",
"Faces": "面",
"Edges": "边",
"Vertices": "顶点",
"Sketch geometry and profiles": "草图几何和轮廓",
"Sketch curves": "草图曲线",
"Sketch points": "草图点",
"Sketch dimensions": "草图尺寸",
"Sketch constraints": "草图约束",
"0 turns it off": "0 表示关闭",
"Show the pointer an AI agent moves (#35)": "显示 AI 代理移动的指针(#35)",
"Follow the camera (Look At sketches)": "跟随相机(注视草图)",
"Flat": "平面",
"Save Rule": "保存规则",
"Preview Row": "预览行",
"Add condition": "添加条件",
"Apply to selection": "应用到选择",
"Remove override": "移除覆盖",
"Copy screenshot": "复制截图",
"Remove screenshot": "移除截图",
"Use active author placement": "使用活动作者放置",
"Reattach selected face": "重新附着所选面",
"Use saved fixed plane": "使用已保存的固定平面",
"Use selected face as fixed plane": "使用所选面作为固定平面",
"View navigator": "视图导航器",
"Signed axes": "带符号轴",
"Cube": "立方体",
"Middle double-click": "中键双击",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "在视图中双击中键:主视图(主视图,框住所显示的内容)、适合窗口(框住所显示的内容,保持方向)或无动作",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "“选择其他”:在模型上按住左键(或右键单击 › 选择其他…)列出光标下的所有对象;在同一位置再次单击可选择该处的下一个对象。双击中键转到主视图(首选项 › 导航)。",
"cmd.render_backends": "渲染后端",
"cmd.view_navigate": "导航视图",
"ui.animateJoint": "动画接头",
"ui.calibrate": "校准",
"ui.customizeToolbar": "自定义工具栏…",
"ui.editJointLimits": "编辑接头限制",
"ui.exportDxf": "导出 DXF…",
"ui.findInTimeline": "在时间线中查找",
"ui.findInWindow": "在窗口中查找",
"ui.fit": "适合",
"ui.groupSelected": "将所选编组",
"ui.hideAxes": "隐藏轴",
"ui.hideDimension": "隐藏尺寸",
"ui.hideOrigin": "隐藏原点",
"ui.hidePlanes": "隐藏平面",
"ui.hideProfile": "隐藏轮廓",
"ui.homeView": "主视图",
"ui.isolate": "隔离",
"ui.lock": "锁定",
"ui.newSketchGroup": "新建草图组",
"ui.normalConstruction": "普通 / 构造",
"ui.orbit": "环绕",
"ui.heading_position": "位置",
"ui.pan": "平移",
"ui.pinToShortcuts": "固定到快捷方式",
"ui.pinToToolbar": "固定到工具栏",
"ui.properties": "属性",
"ui.removeSection": "移除剖面",
"ui.removeFromToolbar": "从工具栏移除",
"ui.repeat": "重复",
"ui.report_a_bug": "报告缺陷",
"ui.restore": "还原",
"ui.heading_setup": "设置",
"ui.selectOther": "选择其他…",
"ui.selectTangentChain": "选择相切链",
"ui.showAll": "显示全部",
"ui.showAllBodies": "显示所有实体",
"ui.showAxes": "显示轴",
"ui.showDimension": "显示尺寸",
"ui.showOrigin": "显示原点",
"ui.showPlanes": "显示平面",
"ui.suppress": "抑制",
"ui.command_palette_hint": "输入命令,例如 {example} — 或按 S 搜索",
"ui.unground": "取消固定",
"ui.unisolate": "取消隔离",
"ui.unlock": "解锁",
"ui.unsuppress": "取消抑制",
"ui.updateToCurrentView": "更新为当前视图",
"ui.zoom": "缩放",
"ui.command_done": "{command} 完成",
"timeline.group": "新建特征文件夹",
"timeline.ungroup": "移除特征文件夹",
"timeline.rename_group": "重命名特征文件夹",
"timeline.move_to_group": "将特征移至文件夹",
"timeline.move_many": "移动时间线项目",
"timeline.reorder_many": "特征重新排序",
"ui.show_construction_geometries": "构造几何图元",
"ui.total_angle": "总角度",
"sketch_text.builtin_font": "{family}(内置)",
"sketch_text.font_search": "搜索字体",
"sketch_text.loading_fonts": "正在查找已安装字体…",
"sketch_text.use_font_file": "使用字体文件 {path}",
"sketch_text.bold": "粗体",
"sketch_text.italic": "斜体",
"sketch_text.align_left": "左对齐",
"sketch_text.align_center": "居中",
"sketch_text.align_right": "右对齐",
"sketch_text.path_hint": "文字所跟随的草图曲线:先单击此处,再选择曲线",
"sketch_text.path_clear": "将文字移离其曲线",
"Style": "样式",
"Alignment": "对齐",
"Letter Spacing": "字间距",
"Line Spacing": "行间距",
"Path": "路径",
"sketch_text.on_path": "文字沿路径排列:由曲线放置",
"ui.analysis": "分析",
"ui.edit_section_analysis": "编辑截面分析",
"cmd.material_properties_validate":"验证物理材料数据"
}
+40
View File
@@ -0,0 +1,40 @@
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
pub const DEFAULT_DENSITY: f64 = 1.29;
/// Physical materials: name and density (g/cm³).
pub const MATERIALS: [(&str, f64); 14] = [
("Default", DEFAULT_DENSITY),
("Steel", 7.85),
("Stainless Steel", 8.0),
("Aluminum", 2.70),
("Brass", 8.50),
("Copper", 8.96),
("Titanium", 4.43),
("Cast Iron", 7.20),
("ABS Plastic", 1.06),
("PLA", 1.24),
("Nylon", 1.14),
("Polycarbonate", 1.20),
("Oak", 0.75),
("Glass", 2.50),
];
/// How a physical material looks (sRGB); the default material has no colour of its own.
pub fn material_color(name: &str) -> Option<[u8; 3]> {
Some(match name {
"Steel" => [158, 164, 172],
"Stainless Steel" => [186, 191, 198],
"Aluminum" => [204, 208, 214],
"Brass" => [208, 172, 88],
"Copper" => [204, 122, 80],
"Titanium" => [150, 148, 158],
"Cast Iron" => [104, 104, 110],
"ABS Plastic" => [222, 222, 216],
"PLA" => [110, 160, 222],
"Nylon" => [236, 230, 214],
"Polycarbonate" => [196, 216, 232],
"Oak" => [190, 146, 92],
"Glass" => [170, 208, 218],
_ => return None,
})
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
+316
View File
@@ -0,0 +1,316 @@
struct U {
vp: mat4x4<f32>,
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
screen: vec4<f32>, // viewport width, height (px), depth bias (see `biased_z`), silhouettes on (1)
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
cap: vec4<f32>, // colour of the inside of cut bodies
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
pull: vec4<f32>, // draft pull direction
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
acc_v: vec4<f32>,
eye: vec4<f32>, // eye position, w = 1 for perspective (0: orthographic, looking along -back)
};
@group(1) @binding(2) var acc_t: texture_2d<f32>;
fn clipped(p: vec3<f32>) -> bool {
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
}
@group(0) @binding(0) var<uniform> u: U;
fn to_linear(c: vec3<f32>) -> vec3<f32> {
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
}
fn out_color(c: vec4<f32>) -> vec4<f32> {
if (u.back.w > 0.5) {
return vec4<f32>(to_linear(c.rgb), c.a);
}
return c;
}
struct TOut {
@builtin(position) pos: vec4<f32>,
@location(0) n: vec3<f32>,
@location(1) c: vec4<f32>,
@location(2) wp: vec3<f32>,
};
@vertex
fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
var o: TOut;
o.pos = u.vp * vec4<f32>(p, 1.0);
o.n = n;
o.c = c;
o.wp = p;
return o;
}
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
@fragment
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
let c = solid(i, front);
return c;
}
struct CutOut {
@location(0) c: vec4<f32>,
@builtin(frag_depth) depth: f32,
};
/// The section cap (the inside seen through the cut, drawn on the far walls' back faces where
/// they are the visible surface) again, at the depth of the section plane rather than of the
/// far wall, so the cut face hides what lies inside the body instead of showing it like a
/// pane of glass.
@fragment
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
var o: CutOut;
o.c = solid(i, front);
o.depth = cap_depth(i.wp, i.pos.z);
return o;
}
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
/// never behind the wall itself (`z`).
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
var dir = -u.back.xyz;
if (u.eye.w > 0.5) { dir = wp - u.eye.xyz; }
let dn = dot(u.clip.xyz, dir);
if (abs(dn) < 1e-9) { return z; }
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
let c = u.vp * vec4<f32>(p, 1.0);
if (c.w <= 0.0) { return z; }
return clamp(c.z / c.w, 0.0, z);
}
fn solid(i: TOut, front: bool) -> vec4<f32> {
// Derivatives first, in uniform control flow (browsers' WGSL compilers reject them after
// a branch that returns; the whole shader module then fails and nothing is drawn).
let k = curvature(i);
let ndv = dot(normalize(i.n), normalize(u.back.xyz));
let fw = fwidth(ndv);
if (clipped(i.wp)) { discard; }
if (!front && length(u.clip.xyz) > 0.5) {
// The cut face in its body's colour (as Fusion's "from component"), tinted toward the
// cap colour so it reads as a cut, hatched at 45 degrees on screen.
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
let h = fract((i.pos.x + i.pos.y) / 9.0);
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
}
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
return analysis(i, k);
}
let c = shade(i);
// Silhouettes (styles with edges): where a curved surface turns away from the eye, a line
// about 1.5 px wide, on the visible surface only (depth-correct).
if (u.screen.w > 0.5 && length(i.n) > 0.5 && fw > 1e-6) {
let a = 1.0 - smoothstep(fw * 0.8, fw * 2.0, abs(ndv));
return vec4<f32>(mix(c.rgb, out_color(vec4<f32>(0.07, 0.08, 0.10, 1.0)).rgb, a), c.a);
}
return c;
}
/// How fast the normal turns across the pixel (per mm).
fn curvature(i: TOut) -> f32 {
let nn = normalize(i.n);
let kx = length(dpdx(nn)) / max(length(dpdx(i.wp)), 1e-6);
let ky = length(dpdy(nn)) / max(length(dpdy(i.wp)), 1e-6);
return max(kx, ky);
}
/// Surface analysis colours: zebra stripes, draft angle bands, curvature map (`k`: the
/// surface's curvature here, worked out by the caller).
fn analysis(i: TOut, k: f32) -> vec4<f32> {
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
let lit = 0.55 + 0.45 * max(dot(n, v), 0.0);
if (u.ana.x < 1.5) {
// Reflections of upright light bars standing around the viewer: the stripe follows
// the reflected ray's sideways direction (horizontal on screen).
let r = reflect(-v, n);
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
let c = select(0.08, 0.95, s > 0.0);
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
}
if (u.ana.x > 4.5) {
// Reachable from the access direction: facing it and nothing above.
let d = normalize(u.pull.xyz);
let size = i32(u.ana.w);
let px = vec2<i32>(floor(vec2<f32>(dot(i.wp, u.acc_u.xyz) + u.acc_u.w, dot(i.wp, u.acc_v.xyz) + u.acc_v.w) * u.ana.z));
var top = -1e30;
if (px.x >= 0 && px.y >= 0 && px.x < size && px.y < size) {
top = textureLoad(acc_t, px, 0).x;
}
let cn = dot(normalize(i.n), d);
let facing = cn > 0.02;
// Slack of a pixel and a half, more on slopes (the map holds the pixel's highest point).
let slack = u.ana.y * (1.0 + sqrt(max(1.0 - cn * cn, 0.0)) / max(cn, 0.05));
let clear = dot(i.wp, d) >= top - slack;
var c = vec3<f32>(0.85, 0.22, 0.20);
if (facing && clear) { c = vec3<f32>(0.20, 0.75, 0.30); }
return out_color(vec4<f32>(c * lit, 1.0));
}
if (u.ana.x > 3.5) {
// A studio around the model: sky above, a dark floor below, a bright horizon band and
// two soft light panels, seen in a mirror.
let r = reflect(-v, n);
var c = mix(vec3<f32>(0.32, 0.34, 0.38), vec3<f32>(0.80, 0.86, 0.95), smoothstep(-0.05, 0.6, r.z));
c = c + vec3<f32>(0.6) * exp(-abs(r.z) * 40.0);
let side = atan2(r.y, r.x);
c = c + vec3<f32>(0.9) * smoothstep(0.92, 0.98, cos(side * 2.0)) * smoothstep(0.1, 0.3, r.z) * smoothstep(0.8, 0.6, r.z);
if (r.z < -0.02) {
// The floor: lighter toward the horizon, with light-panel streaks along it.
c = mix(vec3<f32>(0.55, 0.56, 0.60), vec3<f32>(0.20, 0.20, 0.22), clamp(-r.z * 1.6, 0.0, 1.0));
c = c + vec3<f32>(0.35) * smoothstep(0.95, 0.99, cos(side * 3.0));
}
return out_color(vec4<f32>(min(c, vec3<f32>(1.0)), 1.0));
}
if (u.ana.x < 2.5) {
let a = degrees(asin(clamp(dot(normalize(i.n), normalize(u.pull.xyz)), -1.0, 1.0)));
var c = vec3<f32>(0.95, 0.80, 0.15);
if (a >= u.ana.y) { c = vec3<f32>(0.20, 0.75, 0.30); }
if (a <= -u.ana.y) { c = vec3<f32>(0.85, 0.22, 0.20); }
return out_color(vec4<f32>(c * lit, 1.0));
}
// Curvature from how fast the normal turns across the pixel.
let t = clamp(k * u.ana.y, 0.0, 1.0);
let c = select(mix(vec3<f32>(0.10, 0.80, 0.25), vec3<f32>(0.90, 0.15, 0.10), t * 2.0 - 1.0),
mix(vec3<f32>(0.15, 0.35, 0.95), vec3<f32>(0.10, 0.80, 0.25), t * 2.0), t < 0.5);
return out_color(vec4<f32>(c * lit, 1.0));
}
@fragment
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return shade(i);
}
fn shade(i: TOut) -> vec4<f32> {
// A zero normal marks flat colour (selection fills, translucent planes).
if (length(i.n) < 0.5) {
return out_color(i.c);
}
let v = normalize(u.back.xyz);
var n = normalize(i.n);
if (dot(n, v) < 0.0) { n = -n; }
let key = normalize(v * 0.8 + vec3<f32>(-0.3, 0.2, 0.6));
let diff = max(dot(n, key), 0.0);
let fill = max(dot(n, v), 0.0);
let h = normalize(key + v);
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
let k = 0.42 + 0.38 * diff + 0.25 * fill;
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
}
/// Clip-space z of `p` (clipped to `c`) pulled `s` depth biases toward the eye (pushed away
/// when negative). Orthographic depth is linear in distance, so the bias is a fixed step of it.
/// Perspective depth is not: near the far plane a fixed step spans tens of millimetres (edges
/// showed through walls), so the point moves toward the eye by a fraction of its distance.
fn biased_z(p: vec3<f32>, c: vec4<f32>, s: f32) -> f32 {
if (u.eye.w > 0.5) {
let q = u.vp * vec4<f32>(mix(p, u.eye.xyz, u.screen.z * 8.0 * s), 1.0);
return q.z / max(q.w, 1e-6) * c.w;
}
return c.z - u.screen.z * s * c.w;
}
@vertex
fn vs_ghost(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
var o: TOut;
o.pos = u.vp * vec4<f32>(p, 1.0);
o.pos.z = biased_z(p, o.pos, -4.0);
o.n = n;
o.c = c;
o.wp = p;
return o;
}
struct LOut {
@builtin(position) pos: vec4<f32>,
@location(0) c: vec4<f32>,
@location(1) wp: vec3<f32>,
};
@vertex
fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(1) b: vec3<f32>, @location(2) c: vec4<f32>, @location(3) w: f32) -> LOut {
var pa = a;
var pb = b;
var ca = u.vp * vec4<f32>(a, 1.0);
var cb = u.vp * vec4<f32>(b, 1.0);
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
// the segment at the plane instead; clip space is linear in the world point.
if (ca.z < 0.0 && cb.z > 0.0) {
let k = ca.z / (ca.z - cb.z);
ca = mix(ca, cb, k);
pa = mix(a, b, k);
} else if (cb.z < 0.0 && ca.z > 0.0) {
let k = cb.z / (cb.z - ca.z);
cb = mix(cb, ca, k);
pb = mix(b, a, k);
}
let half = u.screen.xy * 0.5;
let sa = ca.xy / max(ca.w, 1e-6) * half;
let sb = cb.xy / max(cb.w, 1e-6) * half;
var dir = sb - sa;
let len = length(dir);
if (len < 1e-6) { dir = vec2<f32>(1.0, 0.0); } else { dir = dir / len; }
let nrm = vec2<f32>(-dir.y, dir.x);
// Two triangles: corners (t, side).
var ts = array<f32, 6>(0.0, 1.0, 1.0, 0.0, 1.0, 0.0);
var ss = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
let t = ts[vi];
let s = ss[vi];
let base = select(ca, cb, t > 0.5);
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
var o: LOut;
o.pos = vec4<f32>(base.xy + off / half * base.w, biased_z(select(pa, pb, t > 0.5), base, 1.0), base.w);
o.c = c;
o.wp = select(pa, pb, t > 0.5);
return o;
}
@fragment
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
if (clipped(i.wp)) { discard; }
return out_color(i.c);
}
struct IOut {
@builtin(position) pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) a: f32,
@location(2) wp: vec3<f32>,
};
@group(1) @binding(0) var img_t: texture_2d<f32>;
@group(1) @binding(1) var img_s: sampler;
@vertex
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
var o: IOut;
o.pos = u.vp * vec4<f32>(p, 1.0);
// Pulled toward the eye like lines, so a decal on a face is not lost in it.
o.pos.z = biased_z(p, o.pos, 1.0);
o.uv = uv;
o.a = a;
o.wp = p;
return o;
}
@fragment
fn fs_img(i: IOut) -> @location(0) vec4<f32> {
let c = textureSample(img_t, img_s, i.uv);
if (clipped(i.wp)) { discard; }
return out_color(vec4<f32>(c.rgb, c.a * i.a));
}
/// Lines the section never cuts (the grid, highlights drawn on top).
@fragment
fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
return out_color(i.c);
}
+138
View File
@@ -0,0 +1,138 @@
//! Original light and dark palette values. Compiled by the UI theme boundary.
use crate::theme::Tokens;
use egui::Color32;
impl Tokens {
pub const fn light() -> Tokens {
Tokens {
app_bar: Color32::from_rgb(52, 56, 64),
toolbar: Color32::from_rgb(246, 247, 249),
panel: Color32::from_rgb(250, 251, 252),
panel_header: Color32::from_rgb(236, 238, 242),
border: Color32::from_rgb(208, 212, 220),
text: Color32::from_rgb(38, 42, 50),
text_dim: Color32::from_rgb(110, 116, 128),
accent: Color32::from_rgb(38, 120, 218),
accent_soft: Color32::from_rgb(214, 230, 250),
sketch_accent: Color32::from_rgb(46, 150, 90),
error: Color32::from_rgb(214, 54, 54),
warning: Color32::from_rgb(222, 150, 30),
icon: Color32::from_rgb(70, 78, 92),
icon_fill: Color32::from_rgb(178, 196, 222),
timeline: Color32::from_rgb(232, 235, 240),
hover: Color32::from_rgb(226, 234, 246),
sel_face: Color32::from_rgb(76, 127, 203),
sel_edge: Color32::from_rgb(0, 127, 255),
sel_edge_rim: Color32::from_rgb(127, 191, 255),
hover_edge: Color32::from_rgb(0, 0, 0),
hover_edge_halo: Color32::from_rgb(203, 203, 203),
hover_face_lift: 40,
sel_profile: Color32::from_rgb(26, 84, 167),
sel_profile_edge: Color32::from_rgb(47, 109, 198),
hover_profile: Color32::from_rgba_unmultiplied_const(75, 129, 253, 48),
hover_profile_edge: Color32::from_rgb(75, 129, 253),
sel_vertex: Color32::from_rgb(70, 110, 170),
origin_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 94),
origin_plane_edge: Color32::from_rgba_unmultiplied_const(214, 140, 84, 200),
origin_plane_hover: Color32::from_rgba_unmultiplied_const(120, 130, 230, 150),
origin_point: Color32::from_rgb(214, 215, 218),
axis_x: Color32::from_rgb(255, 0, 0),
axis_y: Color32::from_rgb(0, 255, 0),
axis_z: Color32::from_rgb(2, 2, 248),
construction_plane: Color32::from_rgba_unmultiplied_const(249, 184, 134, 70),
construction_geom: Color32::from_rgb(232, 140, 48),
body: Color32::from_rgb(176, 186, 198),
body_edge: Color32::from_rgb(40, 44, 52),
manipulator: Color32::from_rgb(40, 110, 220),
preview_add: Color32::from_rgb(112, 160, 226),
preview_sketch: Color32::from_rgb(32, 190, 96),
preview_cut: Color32::from_rgba_unmultiplied_const(232, 84, 44, 110),
cube_face: Color32::from_rgb(236, 239, 244),
cube_border: Color32::from_rgb(85, 94, 110),
cube_text: Color32::from_rgb(38, 42, 50),
cube_hover: Color32::from_rgb(190, 220, 252),
viewport_top: Color32::from_rgb(240, 243, 247),
viewport_bottom: Color32::from_rgb(203, 210, 221),
grid: Color32::from_rgb(196, 202, 212),
grid_major: Color32::from_rgb(170, 178, 190),
body_selected: Color32::from_rgb(110, 170, 235),
sketch: Color32::from_rgb(30, 90, 200),
sketch_fixed: Color32::from_rgb(20, 20, 20),
sketch_point: Color32::BLACK,
sketch_locked: Color32::from_rgb(0, 140, 60),
dimension: Color32::from_rgb(36, 40, 48),
dimension_driven: Color32::from_rgb(120, 126, 138),
sketch_point_free: Color32::from_rgb(30, 90, 200),
section_cap: Color32::from_rgb(214, 128, 72),
dialog_bg: Color32::from_rgb(250, 251, 252),
dialog_border: Color32::from_rgb(208, 212, 220),
chip: Color32::from_rgb(38, 120, 218),
heading_bg: Color32::from_rgb(218, 223, 231),
subheading_bg: Color32::from_rgb(234, 237, 242),
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
glyph_bg: Color32::WHITE,
glyph_edge: Color32::from_rgb(190, 110, 100),
glyph_text: Color32::from_rgb(150, 40, 35),
overlay: Color32::from_rgba_unmultiplied_const(255, 255, 255, 235),
rubber_band: Color32::from_rgb(30, 120, 230),
app_bar_text: Color32::from_rgb(230, 234, 242),
timeline_item: Color32::WHITE,
timeline_item_error: Color32::from_rgb(250, 220, 220),
timeline_marker: Color32::from_rgb(70, 76, 88),
field: Color32::WHITE,
box_window: Color32::from_rgb(38, 120, 218),
box_crossing: Color32::from_rgb(40, 160, 80),
}
}
pub const fn dark() -> Tokens {
let mut t = Tokens::light();
t.app_bar = Color32::from_rgb(28, 31, 38);
t.toolbar = Color32::from_rgb(37, 41, 49);
t.panel = Color32::from_rgb(39, 43, 52);
t.panel_header = Color32::from_rgb(47, 52, 62);
t.border = Color32::from_rgb(63, 69, 82);
t.text = Color32::from_rgb(222, 226, 232);
t.text_dim = Color32::from_rgb(148, 155, 168);
t.accent = Color32::from_rgb(58, 136, 238);
t.accent_soft = Color32::from_rgb(42, 70, 110);
t.sketch_accent = Color32::from_rgb(110, 200, 140);
t.error = Color32::from_rgb(240, 100, 96);
t.warning = Color32::from_rgb(236, 170, 70);
t.icon = Color32::from_rgb(208, 214, 226);
t.icon_fill = Color32::from_rgb(86, 112, 150);
t.timeline = Color32::from_rgb(33, 37, 45);
t.hover = Color32::from_rgb(54, 61, 74);
t.hover_edge_halo = Color32::from_rgb(203, 203, 203);
t.origin_point = Color32::from_rgb(214, 215, 218);
t.viewport_top = Color32::from_rgb(46, 52, 64);
t.viewport_bottom = Color32::from_rgb(46, 52, 64);
t.grid = Color32::from_rgb(59, 65, 76);
t.grid_major = Color32::from_rgb(73, 79, 90);
t.body = Color32::from_rgb(152, 156, 162);
t.body_edge = Color32::from_rgb(16, 16, 18);
t.sketch = Color32::from_rgb(92, 150, 255);
t.sketch_fixed = Color32::from_rgb(232, 234, 238);
t.sketch_point = Color32::from_rgb(232, 234, 238);
t.sketch_locked = Color32::from_rgb(96, 204, 116);
t.dimension = Color32::from_rgb(224, 228, 234);
t.dimension_driven = Color32::from_rgb(142, 150, 164);
t.sketch_point_free = Color32::from_rgb(92, 150, 255);
t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40);
t.dialog_bg = Color32::from_rgb(59, 68, 83);
t.dialog_border = Color32::from_rgb(72, 83, 99);
t.chip = Color32::from_rgb(59, 101, 168);
t.heading_bg = Color32::from_rgb(38, 44, 55);
t.subheading_bg = Color32::from_rgb(48, 55, 68);
t.glyph_bg = Color32::from_rgb(44, 50, 60);
t.glyph_edge = Color32::from_rgb(200, 130, 120);
t.glyph_text = Color32::from_rgb(240, 165, 155);
t.overlay = Color32::from_rgba_unmultiplied_const(34, 38, 46, 240);
t.rubber_band = Color32::from_rgb(92, 160, 255);
t.timeline_item = Color32::from_rgb(52, 58, 70);
t.timeline_item_error = Color32::from_rgb(110, 50, 54);
t.timeline_marker = Color32::from_rgb(170, 178, 192);
t.field = Color32::from_rgb(30, 34, 41);
t.box_window = Color32::from_rgb(70, 150, 255);
t.box_crossing = Color32::from_rgb(80, 200, 120);
t
}
}
+4
View File
@@ -0,0 +1,4 @@
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
+4 -23
View File
@@ -103,28 +103,9 @@ impl Appearances {
}
/// The built-in appearance library: (name, colour, opacity).
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
("Aluminum - Satin", [204, 208, 214], 1.0),
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
("Steel - Satin", [158, 164, 172], 1.0),
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
("Cast Iron", [104, 104, 110], 1.0),
("Brass - Polished", [208, 172, 88], 1.0),
("Copper", [204, 122, 80], 1.0),
("Titanium", [150, 148, 158], 1.0),
("ABS - White", [236, 236, 230], 1.0),
("ABS - Black", [36, 36, 38], 1.0),
("ABS - Red", [196, 40, 40], 1.0),
("ABS - Blue", [40, 90, 186], 1.0),
("ABS - Yellow", [236, 196, 40], 1.0),
("ABS - Green", [52, 150, 80], 1.0),
("Rubber - Black", [28, 28, 28], 1.0),
("Glass - Clear", [190, 220, 228], 0.25),
("Glass - Smoked", [90, 96, 104], 0.45),
("Wood - Oak", [190, 146, 92], 1.0),
("Paint - Gloss White", [246, 246, 244], 1.0),
];
#[path = "../../../assets/appearances/library.rs"]
mod defaults;
pub use defaults::LIBRARY;
/// A library appearance by name (case-insensitive).
pub fn library(name: &str) -> Option<Look> {
@@ -263,7 +244,7 @@ pub fn pick_tolerance(b: &crate::ModelBody) -> f64 {
}
/// The point of triangle abc nearest p.
fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
pub(crate) fn closest_on_triangle(p: Vec3, a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
let (ab, ac, ap) = (b - a, c - a, p - a);
let (d1, d2) = (ab.dot(ap), ac.dot(ap));
if d1 <= 0.0 && d2 <= 0.0 {
+26 -7
View File
@@ -124,6 +124,10 @@ fn identity() -> Mat {
IDENTITY
}
/// How deep and how many placements [`Document::placements`] walks.
pub const MAX_PLACEMENT_DEPTH: usize = 64;
pub const MAX_PLACEMENTS: usize = 100_000;
fn map_plane(m: &Mat, p: &Plane) -> Plane {
let (o, x, y) = (apply_point(m, p.origin), apply_vector(m, p.x), apply_vector(m, p.y));
Plane::new(o, x, y).unwrap_or(*p)
@@ -221,20 +225,22 @@ impl FeatureKind {
pt(&mut t.point);
}
}
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
FeatureKind::Revolve { axis: AxisRef::Line { origin, dir: d }, .. }
| FeatureKind::SurfaceRevolve { axis: AxisRef::Line { origin, dir: d }, .. } => {
pt(origin);
dir(d);
}
FeatureKind::Fillet { edges, style, .. } => {
edges.iter_mut().for_each(pt);
if let crate::FilletStyle::Variable { start, .. } = style {
if let crate::FilletStyle::Variable { start, .. } | crate::FilletStyle::Spline { start, .. } = style {
pt(start);
}
}
FeatureKind::Chamfer { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::Shell { faces, .. } | FeatureKind::OffsetFace { faces, .. } | FeatureKind::DeleteFace { faces, .. } => {
faces.iter_mut().for_each(pt)
}
FeatureKind::Shell { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SurfaceOffset { faces, .. } => faces.iter_mut().for_each(pt),
FeatureKind::Draft { faces, neutral, pull, .. } => {
faces.iter_mut().for_each(pt);
map_plane_ref(m, neutral);
@@ -265,7 +271,10 @@ impl FeatureKind {
FeatureKind::Thread { face, .. } => pt(face),
FeatureKind::SheetFlange { edges, .. } => edges.iter_mut().for_each(|e| pt(&mut e.point)),
FeatureKind::SheetHem { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::SheetConvert { face, .. } => pt(face),
FeatureKind::SheetConvert { face, bends, .. } => {
pt(face);
bends.iter_mut().for_each(pt);
}
FeatureKind::SheetFold { a, b, fixed, .. } => {
pt(a);
pt(b);
@@ -318,6 +327,10 @@ impl FeatureKind {
}
FeatureKind::Revolve { .. }
| FeatureKind::Extrude { .. }
| FeatureKind::SurfaceExtrude { .. }
| FeatureKind::SurfaceRevolve { .. }
| FeatureKind::SurfaceSweep { .. }
| FeatureKind::SurfaceLoft { .. }
| FeatureKind::Combine { .. }
| FeatureKind::Loft { .. }
| FeatureKind::Sweep { .. }
@@ -333,6 +346,7 @@ impl FeatureKind {
| FeatureKind::MeshConvert { .. }
| FeatureKind::BodyTransfer { .. } => {}
FeatureKind::Patch { edges, .. } | FeatureKind::SurfaceExtend { edges, .. } => edges.iter_mut().for_each(pt),
FeatureKind::Unstitch { faces, .. } | FeatureKind::ReverseNormal { faces, .. } => faces.iter_mut().for_each(pt),
FeatureKind::SurfaceTrim { plane, tool, keep, .. } => {
map_plane_ref(m, plane);
if let Some(t) = tool {
@@ -366,13 +380,18 @@ impl Document {
}
/// Every placement of each component: (component, occurrence path ids, world transform).
/// A branch stops past [`MAX_PLACEMENT_DEPTH`] levels, the walk past [`MAX_PLACEMENTS`] placements.
pub fn placements(&self) -> Vec<(u64, Vec<u64>, Mat)> {
let mut out = vec![(0, Vec::new(), IDENTITY)];
let mut stack: Vec<(u64, Vec<u64>, Mat)> = vec![(0, Vec::new(), IDENTITY)];
while let Some((comp, path, m)) = stack.pop() {
if path.len() > 64 || out.len() > 100_000 {
if out.len() > MAX_PLACEMENTS {
break;
}
// Only this branch stops past the depth limit; the others are still walked.
if path.len() > MAX_PLACEMENT_DEPTH {
continue;
}
for o in self.occurrences.iter().filter(|o| o.parent == comp) {
let mut p = path.clone();
p.push(o.id);
+12 -7
View File
@@ -31,11 +31,14 @@ pub fn validate_body_transfer(doc: &Document, bodies: &[String], from: u64, to:
Ok(())
}
/// Moves the bodies in `st`, which the evaluator discards when this fails (so it is changed in
/// place rather than copied: a copy of every body for each moved one was most of the cost).
pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], from: u64, to: u64, transform: Mat) -> Result<()> {
validate_body_transfer(doc, bodies, from, to, &transform)?;
let mut next = st.clone();
let mut moved = Vec::with_capacity(bodies.len());
for name in bodies {
let old = st.body(name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
let (slot, old) =
st.bodies.iter().enumerate().find(|(_, b)| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
if doc.model_body_component(old) != from {
return Err(DocError::Invalid(format!("body `{name}` no longer belongs to transfer source component {from}")));
}
@@ -47,12 +50,14 @@ pub(crate) fn evaluate(doc: &Document, st: &mut ModelState, bodies: &[String], f
body.source_transform = crate::mat_mul(&transform, &old.source_transform);
body.imported = old.imported.clone();
body.names = crate::naming::NameCell::same(old);
let slot = next.bodies.iter_mut().find(|b| b.name == *name).ok_or_else(|| DocError::Unknown(format!("transferred body `{name}`")))?;
*slot = body;
moved.push((slot, body));
}
crate::body_transfer_sheet::remap_sheet_metadata(&mut next, bodies, &transform)?;
*st = next;
Ok(())
for (slot, body) in moved {
if let Some(b) = st.bodies.get_mut(slot) {
*b = body;
}
}
crate::body_transfer_sheet::remap_sheet_metadata(st, bodies, &transform)
}
#[cfg(test)]
+92 -77
View File
@@ -47,19 +47,22 @@ impl FeatureKind {
}
}
FeatureKind::Extrude { sketch, .. }
| FeatureKind::SurfaceExtrude { sketch, .. }
| FeatureKind::SurfaceRevolve { sketch, .. }
| FeatureKind::Revolve { sketch, .. }
| FeatureKind::Emboss { sketch, .. }
| FeatureKind::SheetBase { sketch, .. }
| FeatureKind::SheetContour { sketch, .. }
| FeatureKind::Rib { sketch, .. } => sk(sketch),
FeatureKind::Patch { sketch: Some(s), .. } => sk(s),
FeatureKind::Sweep { sketch, path_sketch, .. } => {
FeatureKind::Sweep { sketch, path_sketch, .. } | FeatureKind::SurfaceSweep { sketch, path_sketch, .. } => {
sk(sketch);
sk(path_sketch);
}
FeatureKind::Pipe { path_sketch, .. } => sk(path_sketch),
FeatureKind::SheetFold { sketch: Some(s), .. } => sk(s),
FeatureKind::Loft { sections, .. } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().for_each(|s| sk(&mut s.sketch)),
FeatureKind::Hole { points: Some(p), .. } => sk(&mut p.sketch),
FeatureKind::Pattern { features, pattern, .. } => {
feats(features);
@@ -243,6 +246,92 @@ fn remap_link(
}
}
impl FeatureKind {
/// Point the body names a feature acts on (targets, tools, the body it rounds…) at the
/// copies in `bodies`; names not in the map stay as they are.
pub fn remap_bodies(&mut self, bodies: &BTreeMap<String, String>) {
use FeatureKind as K;
match self {
K::Emboss { face: Some(face), .. } => mapped_name(&mut face.body, bodies),
K::Split { tool: Some(tool), .. } | K::SplitFace { tool: Some(tool), .. } | K::SurfaceTrim { tool: Some(tool), .. } => {
mapped_name(&mut tool.body, bodies)
}
_ => {}
}
match self {
K::Extrude { targets, .. }
| K::Revolve { targets, .. }
| K::Loft { targets, .. }
| K::Sweep { targets, .. }
| K::Pipe { targets, .. }
| K::Emboss { targets, .. } => {
for n in targets {
mapped_name(n, bodies);
}
}
K::Combine { target, tools, .. } => {
mapped_name(target, bodies);
for n in tools {
mapped_name(n, bodies);
}
}
K::Pattern { bodies: list, .. }
| K::Mirror { bodies: list, .. }
| K::Scale { bodies: list, .. }
| K::BoundingSolid { bodies: list, .. }
| K::Align { bodies: list, .. }
| K::Remove { bodies: list }
| K::Stitch { bodies: list, .. }
| K::Move { bodies: list, .. }
| K::BodyTransfer { bodies: list, .. } => {
for n in list {
mapped_name(n, bodies);
}
}
K::Thicken { bodies: list, targets, .. } => {
for n in list.iter_mut().chain(targets) {
mapped_name(n, bodies);
}
}
K::BoundaryFill { tools, .. } => {
for n in tools {
mapped_name(n, bodies);
}
}
K::Fillet { body, .. }
| K::Chamfer { body, .. }
| K::Shell { body, .. }
| K::Draft { body, .. }
| K::SplitFace { body, .. }
| K::OffsetFace { body, .. }
| K::DeleteFace { body, .. }
| K::SurfaceOffset { body, .. }
| K::ReplaceFace { body, .. }
| K::SheetFlange { body, .. }
| K::SheetHem { body, .. }
| K::SheetFold { body, .. }
| K::SheetUnfold { body, .. }
| K::Boss { body, .. }
| K::Lip { body, .. }
| K::SnapFit { body, .. }
| K::Rest { body, .. }
| K::Patch { body, .. } => {
if let Some(n) = body {
mapped_name(n, bodies);
}
}
K::Split { body, .. }
| K::SheetConvert { body, .. }
| K::SurfaceExtend { body, .. }
| K::SurfaceTrim { body, .. }
| K::Unstitch { body, .. }
| K::ReverseNormal { body, .. }
| K::MeshConvert { body, .. } => mapped_name(body, bodies),
_ => {}
}
}
}
impl crate::Feature {
/// Clone identities through typed reference fields. Sketch-local entity IDs stay local.
pub fn remap_clone(
@@ -298,81 +387,7 @@ impl crate::Feature {
for e in self.kind.point_expr_mut().into_iter().flatten() {
*e = clone_expression(e, parameters);
}
match &mut self.kind {
K::Emboss { face: Some(face), .. } => mapped_name(&mut face.body, bodies),
K::Split { tool: Some(tool), .. } | K::SplitFace { tool: Some(tool), .. } | K::SurfaceTrim { tool: Some(tool), .. } => {
mapped_name(&mut tool.body, bodies)
}
_ => {}
}
match &mut self.kind {
K::Extrude { targets, .. }
| K::Revolve { targets, .. }
| K::Loft { targets, .. }
| K::Sweep { targets, .. }
| K::Pipe { targets, .. }
| K::Emboss { targets, .. } => {
for n in targets {
mapped_name(n, bodies);
}
}
K::Combine { target, tools, .. } => {
mapped_name(target, bodies);
for n in tools {
mapped_name(n, bodies);
}
}
K::Pattern { bodies: list, .. }
| K::Mirror { bodies: list, .. }
| K::Scale { bodies: list, .. }
| K::BoundingSolid { bodies: list, .. }
| K::Align { bodies: list, .. }
| K::Remove { bodies: list }
| K::Stitch { bodies: list, .. }
| K::Move { bodies: list, .. }
| K::BodyTransfer { bodies: list, .. } => {
for n in list {
mapped_name(n, bodies);
}
}
K::Thicken { bodies: list, targets, .. } => {
for n in list.iter_mut().chain(targets) {
mapped_name(n, bodies);
}
}
K::BoundaryFill { tools, .. } => {
for n in tools {
mapped_name(n, bodies);
}
}
K::Fillet { body, .. }
| K::Chamfer { body, .. }
| K::Shell { body, .. }
| K::Draft { body, .. }
| K::SplitFace { body, .. }
| K::OffsetFace { body, .. }
| K::DeleteFace { body, .. }
| K::ReplaceFace { body, .. }
| K::SheetFlange { body, .. }
| K::SheetHem { body, .. }
| K::SheetFold { body, .. }
| K::SheetUnfold { body, .. }
| K::Boss { body, .. }
| K::Lip { body, .. }
| K::SnapFit { body, .. }
| K::Rest { body, .. }
| K::Patch { body, .. } => {
if let Some(n) = body {
mapped_name(n, bodies);
}
}
K::Split { body, .. }
| K::SheetConvert { body, .. }
| K::SurfaceExtend { body, .. }
| K::SurfaceTrim { body, .. }
| K::MeshConvert { body, .. } => mapped_name(body, bodies),
_ => {}
}
self.kind.remap_bodies(bodies);
match &mut self.kind {
K::Sketch { plane, sketch } => {
remap_plane(plane, ids, names, bodies, 0)?;
@@ -383,7 +398,7 @@ impl crate::Feature {
}
for link in &mut sketch.links {
remap_link(&mut link.source, ids, names, bodies, 0);
if let Some(n) = &mut link.face_name {
for n in link.face_name.iter_mut().chain(&mut link.edge_name) {
*n = remap_topology(n, ids, bodies);
}
}
+132 -23
View File
@@ -421,6 +421,80 @@ impl<'a> ExactSketchCurves<'a> {
(None, None) => None,
}
}
/// [`find`](Self::find), else the one sketch line a generated straight segment runs along
/// and overlaps: a profile a feature grew to overlap a flush face (its neighbours lengthened
/// past the sketch's corners) still names its sides by the lines they were drawn on. Two
/// candidates stay ambiguous.
pub(crate) fn find_along(&self, generated: &Seg2) -> Option<String> {
if let Some(id) = self.find(generated) {
return Some(id);
}
let Seg2::Line { a: c, b: d } = *generated else { return None };
let mut found: Option<Option<&str>> = None;
for (id, linked, originals) in &self.segments {
if *linked {
continue;
}
let along = originals.iter().any(|original| {
let Seg2::Line { a, b } = *original else { return false };
let direction = b - a;
let scale = direction.len().max((d - c).len()).max(1.);
if direction.len() <= 1e-12
|| direction.cross(c - a).abs() > 1e-10 * scale * scale
|| direction.cross(d - a).abs() > 1e-10 * scale * scale
{
return false;
}
let (tc, td) = ((c - a).dot(direction) / direction.len2(), (d - a).dot(direction) / direction.len2());
tc.min(td).max(0.) < tc.max(td).min(1.) - 1e-9
});
if along {
found = Some(if found.is_some() { None } else { Some(*id) });
}
}
found.flatten().map(str::to_owned)
}
}
impl ExactSketchCurves<'_> {
/// The sketch curves a generated segment is drawn on: [`find_along`](Self::find_along)'s one
/// curve, else every drawn curve that contains it or runs along and overlaps it (projections
/// only where no drawn curve does), in the sketch's order. A segment two coincident curves
/// were drawn over (four squares drawn edge to edge), or one a profile merged from collinear
/// curves (the outline of several adjacent profiles extruded together), is drawn on all of
/// them: the set is exact and fixed by the sketch, so its face is named by it (the first its
/// name, the others aliases) rather than not at all. Empty when it lies on none.
pub(crate) fn find_all_along(&self, generated: &Seg2) -> Vec<String> {
if let Some(id) = self.find_along(generated) {
return vec![id];
}
let runs_along = |original: &Seg2| {
if exact_segment_contains(original, generated) {
return true;
}
let (Seg2::Line { a, b }, Seg2::Line { a: c, b: d }) = (*original, *generated) else { return false };
let direction = b - a;
let scale = direction.len().max((d - c).len()).max(1.);
if direction.len() <= 1e-12
|| direction.cross(c - a).abs() > 1e-10 * scale * scale
|| direction.cross(d - a).abs() > 1e-10 * scale * scale
{
return false;
}
let (tc, td) = ((c - a).dot(direction) / direction.len2(), (d - a).dot(direction) / direction.len2());
tc.min(td).max(0.) < tc.max(td).min(1.) - 1e-9
};
let pick = |linked: bool| -> Vec<String> {
self.segments
.iter()
.filter(|(_, l, originals)| *l == linked && originals.iter().any(runs_along))
.map(|(id, _, _)| (*id).to_owned())
.collect()
};
let drawn = pick(false);
if drawn.is_empty() { pick(true) } else { drawn }
}
}
fn exact_segment_contains(original: &Seg2, generated: &Seg2) -> bool {
@@ -581,6 +655,14 @@ pub fn lineage_frame(body: &ModelBody, plane: &solidcraft_geom::Plane) -> Option
solidcraft_geom::Plane::new(origin, x, n.cross(x))
}
/// Whether Split Face split `body`'s faces on `plane` (a split line lies on it).
fn split_on_purpose(body: &ModelBody, plane: &solidcraft_geom::Plane) -> bool {
let tolerance = 1e-6 * (1. + body.body.size());
let on_plane =
|line: &Vec<solidcraft_geom::Vec3>| !line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance);
body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane))
}
/// Whether two exact planes are one oriented plane (pieces of one face).
fn same_oriented_plane(a: &solidcraft_geom::Plane, b: &solidcraft_geom::Plane, scale: f64) -> bool {
a.normal().dot(b.normal()) >= 1. - 1e-8 && (b.origin - a.origin).dot(a.normal()).abs() <= 1e-7 * (1. + scale)
@@ -599,7 +681,9 @@ impl Document {
/// A target for flat face `index` of `body` by its exact history name
/// ([`FaceCertificate::Lineage`]), when that name follows exact history, names a feature of
/// the design and finds this face (with its other pieces, all on one oriented plane).
/// the design and finds this face (with its other pieces, all on one oriented plane). Where
/// the face's pieces no longer share a plane (Offset Face moved one of them), the piece by
/// its own name: the face's name qualified by the faces around it (`F2:top#[F2:side:l4,…]`).
pub(crate) fn lineage_face_target(
&self,
body: &ModelBody,
@@ -610,17 +694,24 @@ impl Document {
if !names.verified(index) {
return None;
}
let name = crate::naming::strip_piece(names.faces.get(index)?).to_string();
let origin = name.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
let full = names.faces.get(index)?;
let stem = crate::naming::strip_piece(full);
let origin = stem.strip_prefix('F')?.split_once(':')?.0.parse::<u64>().ok()?;
let inverse = crate::mat_inverse(&body.source_transform)?;
let target = crate::config::ConfigFaceTarget {
body: body.definition_name().into(),
origin_feature: origin,
name,
point: crate::apply_point(&inverse, point),
certificate: Some(FaceCertificate::Lineage),
};
self.configuration_face_indices(body, &target).ok()?.contains(&index).then_some(target)
let mut candidates = vec![stem];
if full != stem {
candidates.push(full);
}
candidates.into_iter().find_map(|name| {
let target = crate::config::ConfigFaceTarget {
body: body.definition_name().into(),
origin_feature: origin,
name: name.to_string(),
point: crate::apply_point(&inverse, point),
certificate: Some(FaceCertificate::Lineage),
};
self.configuration_face_indices(body, &target).ok()?.contains(&index).then_some(target)
})
}
/// The faces of `body` an exact history name finds: those named `name` or a piece of it, else
@@ -628,8 +719,20 @@ impl Document {
/// follow exact history and lie on one oriented plane.
pub(crate) fn lineage_face_indices(&self, body: &ModelBody, name: &str) -> Result<Vec<usize>> {
let names = crate::naming::names(body);
let mut indices: Vec<usize> =
names.faces.iter().enumerate().filter(|(_, face)| crate::naming::strip_piece(face) == name).map(|(index, _)| index).collect();
// `face` is `name` itself or a piece of it (a piece of a piece, …).
let piece_of = |face: &str| {
let mut n = face;
loop {
if n == name {
return true;
}
match crate::piece_names::parent_piece(n) {
Some(parent) => n = parent,
None => return false,
}
}
};
let mut indices: Vec<usize> = names.faces.iter().enumerate().filter(|(_, face)| piece_of(face)).map(|(index, _)| index).collect();
if indices.is_empty() {
indices =
names.info.iter().enumerate().filter(|(_, info)| info.aliases.iter().any(|alias| alias == name)).map(|(index, _)| index).collect();
@@ -638,6 +741,16 @@ impl Document {
return Err(DocError::Unknown(format!("face `{name}` on `{}` no longer exists", body.name)));
}
self.coplanar_pieces(body, &indices, |index| names.verified(index))?;
// A piece by its own name stands for itself, which a piece of a face split on purpose
// (Split Face) never does: that needs a fixed plane.
if crate::naming::strip_piece(name) != name
&& let Some(plane) = indices.first().and_then(|index| body.body.planar_face(*index))
&& split_on_purpose(body, &plane)
{
return Err(DocError::Invalid(
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
));
}
Ok(indices)
}
@@ -658,16 +771,12 @@ impl Document {
Some(_) => return Err(DocError::Invalid("attached face pieces no longer share one oriented plane".into())),
}
}
if let (Some(plane), true) = (first, indices.len() > 1) {
let tolerance = 1e-6 * (1. + scale);
let on_plane = |line: &Vec<solidcraft_geom::Vec3>| {
!line.is_empty() && line.iter().all(|p| (*p - plane.origin).dot(plane.normal()).abs() <= tolerance)
};
if body.body.split_lines().is_some_and(|lines| lines.iter().any(on_plane)) {
return Err(DocError::Invalid(
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
));
}
if let (Some(plane), true) = (first, indices.len() > 1)
&& split_on_purpose(body, &plane)
{
return Err(DocError::Invalid(
"this face was split on purpose (Split Face): pick an exact face piece's own plane with fixed:true".into(),
));
}
Ok(())
}
+6 -1
View File
@@ -124,7 +124,12 @@ impl Document {
target.validate()?;
let prefix = format!("F{}:", target.origin_feature);
let role = target.name.strip_prefix(&prefix).ok_or_else(|| DocError::Invalid("face lineage does not match its feature".into()))?;
if crate::naming::strip_piece(&target.name) != target.name {
// An exact history name may name a piece by the faces around it (`F2:top#[F2:side:l4,…]`),
// never by a number (an older design's piece, or a tie the faces around it left).
let numbered =
target.name.match_indices('#').any(|(i, _)| target.name.get(i + 1..).is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_digit())));
let lineage_piece = matches!(target.certificate, Some(super::FaceCertificate::Lineage)) && !numbered;
if crate::naming::strip_piece(&target.name) != target.name && !lineage_piece {
return Err(DocError::Invalid("configure a face lineage, not a numbered split piece".into()));
}
let feature = self.feature(target.origin_feature).ok_or_else(|| DocError::Unknown("face origin feature is gone".into()))?;
+294 -53
View File
@@ -485,7 +485,7 @@ pub enum FeatureKind {
depth: Option<String>,
#[serde(default)]
hole: HoleKind,
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch);
/// Drill at these sketch points instead (one hole each, perpendicular to the sketch, into the material: against the sketch normal, or along it when only that side has material);
/// the holes follow the points when the sketch changes.
#[serde(default, skip_serializing_if = "Option::is_none")]
points: Option<SketchPoints>,
@@ -607,6 +607,10 @@ pub enum FeatureKind {
faces: Vec<Vec3>,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<String>,
/// Close the gap by extending the neighbouring faces (the default); `false` leaves it
/// open, the faces left a surface body.
#[serde(default = "yes", skip_serializing_if = "is_yes")]
heal: bool,
},
/// A box around bodies, grown by a margin.
BoundingSolid {
@@ -782,12 +786,17 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
refold: bool,
},
/// Convert a plate body (constant thickness from the face at `face`) to sheet metal.
/// Convert a body to sheet metal from its fixed face at `face`: a plate of constant
/// thickness or, with `bends`, a solid or shelled body whose faces reached from the fixed
/// face across those edges become the sheet.
SheetConvert {
body: String,
face: Vec3,
#[serde(default, skip_serializing_if = "Option::is_none")]
rule: Option<String>,
/// Points on the sharp edges that become bends.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
bends: Vec<Vec3>,
},
/// Plastic boss: a round post on a face (draft, root fillet, hole, ribs).
Boss {
@@ -920,6 +929,77 @@ pub enum FeatureKind {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
targets: Vec<String>,
},
/// A surface body swept straight along the sketch normal from open curve chains and the
/// boundaries of profiles: walls only, no caps.
SurfaceExtrude {
sketch: u64,
/// Profiles whose boundary loops are swept (none: only `curves`).
#[serde(default, skip_serializing_if = "Option::is_none")]
profiles: Option<ProfileSel>,
/// Sketch curve ids, grouped into chains of curves that meet end to end.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
curves: Vec<String>,
extent: Extent,
},
/// A body taken apart: each face at `faces` (every face when empty) its own surface body,
/// the faces left one surface body under the body's name. A solid comes apart into surfaces.
Unstitch {
body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
faces: Vec<Vec3>,
},
/// A surface body with the faces at `faces` (every face when empty) turned over.
ReverseNormal {
body: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
faces: Vec<Vec3>,
},
/// A surface body turned about an axis from open curve chains and the boundaries of
/// profiles: walls only, no caps (a solid where a full turn closes them).
SurfaceRevolve {
sketch: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
profiles: Option<ProfileSel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
curves: Vec<String>,
axis: AxisRef,
angle: String,
/// Two Sides: the angle on the other side.
#[serde(default, skip_serializing_if = "Option::is_none")]
angle2: Option<String>,
/// Symmetric: `angle` split evenly either side of the sketch.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
symmetric: bool,
},
/// A surface body swept along a chain of sketch curves from open curve chains and the
/// boundaries of profiles: walls only, no caps.
SurfaceSweep {
sketch: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
profiles: Option<ProfileSel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
curves: Vec<String>,
path_sketch: u64,
path: Vec<String>,
},
/// A surface body lofted through sections in order, each one open or closed chain of
/// sketch curves: walls only, no caps.
SurfaceLoft {
sections: Vec<SheetSection>,
},
/// The faces at `faces` copied out at a signed `distance` along their normals as a new
/// surface body (0 copies them); `chain` takes each pick's whole smooth chain of faces;
/// fitted faces within `tolerance`.
SurfaceOffset {
faces: Vec<Vec3>,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<String>,
distance: String,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
chain: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
tolerance: Option<String>,
},
/// A planar surface body extended outward by `distance` past the edges through `edges`.
SurfaceExtend {
body: String,
@@ -952,8 +1032,9 @@ pub enum FeatureKind {
Import {
/// Source file name (display only).
file: String,
/// The STEP (ISO 10303-21) text.
step: String,
/// The STEP (ISO 10303-21) text, shared by every copy of the design (undo states,
/// previews, scratch sessions): a large file is held once, not once per edit.
step: std::sync::Arc<str>,
/// The file's product/assembly tree, kept for components.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
components: Vec<solidcraft_kernel::StepNode>,
@@ -979,6 +1060,9 @@ pub enum FilletStyle {
Constant,
/// `radius` is the chord: the width across the blend.
Chord,
/// G2 quintic blend of planar faces along a straight edge. `radius` sets the
/// equivalent circular setback, not a constant curvature radius.
Curvature,
/// `radius` at the end of each edge nearest `start`, `radius2` at the other; `mid` radii at
/// positions between (0 to 1 from the start end); straight between them, or `smooth`.
Variable {
@@ -989,16 +1073,23 @@ pub enum FilletStyle {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
smooth: bool,
},
/// The radius along each edge as a clamped B-spline of the position from the end nearest
/// `start` (0) to the other (1): `radius` is its first control radius and `radii` the rest,
/// its degree what `knots` leave (knots = control radii + degree + 1).
Spline { start: Vec3, knots: Vec<f64>, radii: Vec<String> },
}
/// A fillet radius too large for the geometry around its edges: refused (`Exact`, the default),
/// or reduced to the largest that works for all of them (`Reduce`), with a warning.
/// reduced to the largest that works for all of them (`Reduce`), or tapered along each edge it
/// doesn't fit to the room there, down to zero where a face closes to a point (`Taper`); a
/// warning says what was done.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilletFit {
#[default]
Exact,
Reduce,
Taper,
}
impl FilletFit {
@@ -1011,6 +1102,13 @@ impl FilletStyle {
pub fn is_constant(&self) -> bool {
*self == FilletStyle::Constant
}
/// The spline of a [`FilletStyle::Spline`] with its control radii `values` (the first is
/// the feature's `radius`).
pub fn spline(knots: &[f64], values: Vec<f64>) -> solidcraft_kernel::RadiusSpline {
let degree = knots.len().saturating_sub(values.len() + 1);
solidcraft_kernel::RadiusSpline { degree, knots: knots.to_vec(), radii: values }
}
}
/// A face picked by a point on it, on a named body.
@@ -1037,6 +1135,9 @@ pub struct MeshData {
pub struct SketchPoints {
pub sketch: u64,
pub ids: Vec<String>,
/// Drill the other way: away from the side the holes would go into by themselves.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub flip: bool,
}
/// A component: a node of the design tree that owns sketches, features and bodies. The root
@@ -1060,6 +1161,17 @@ pub struct LoftSection {
pub point: Option<String>,
}
/// A section of a surface loft: one chain from a sketch (one profile's boundary, or curves
/// meeting end to end).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SheetSection {
pub sketch: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profiles: Option<ProfileSel>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub curves: Vec<String>,
}
/// Coil cross-sections.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -1206,14 +1318,17 @@ impl FeatureKind {
use FeatureKind as K;
match self {
K::Extrude { sketch, .. }
| K::SurfaceExtrude { sketch, .. }
| K::SurfaceRevolve { sketch, .. }
| K::Revolve { sketch, .. }
| K::Emboss { sketch, .. }
| K::Rib { sketch, .. }
| K::SheetBase { sketch, .. }
| K::SheetContour { sketch, .. } => vec![*sketch],
K::Sweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
K::Sweep { sketch, path_sketch, .. } | K::SurfaceSweep { sketch, path_sketch, .. } => vec![*sketch, *path_sketch],
K::Pipe { path_sketch, .. } | K::Pattern { pattern: PatternKind::Path { path_sketch, .. }, .. } => vec![*path_sketch],
K::Loft { sections, .. } => sections.iter().map(|s| s.sketch).collect(),
K::SurfaceLoft { sections } => sections.iter().map(|s| s.sketch).collect(),
K::SheetFold { sketch, .. } | K::SplitFace { sketch, .. } | K::Patch { sketch, .. } => sketch.iter().copied().collect(),
K::Hole { points, .. } => points.iter().map(|p| p.sketch).collect(),
K::Sketch { .. }
@@ -1236,6 +1351,7 @@ impl FeatureKind {
| K::Scale { .. }
| K::OffsetFace { .. }
| K::DeleteFace { .. }
| K::SurfaceOffset { .. }
| K::BoundingSolid { .. }
| K::ReplaceFace { .. }
| K::Align { .. }
@@ -1254,6 +1370,8 @@ impl FeatureKind {
| K::Thicken { .. }
| K::SurfaceExtend { .. }
| K::SurfaceTrim { .. }
| K::Unstitch { .. }
| K::ReverseNormal { .. }
| K::Move { .. }
| K::Import { .. }
| K::MeshImport { .. }
@@ -1313,6 +1431,7 @@ impl FeatureKind {
FeatureKind::Scale { .. } => "ScaleFeature",
FeatureKind::OffsetFace { .. } => "OffsetFacesFeature",
FeatureKind::DeleteFace { .. } => "DeleteFaceFeature",
FeatureKind::SurfaceOffset { .. } => "OffsetSurfaceFeature",
FeatureKind::BoundingSolid { .. } => "BoundingSolidFeature",
FeatureKind::Pipe { .. } => "PipeFeature",
FeatureKind::Emboss { .. } => "EmbossFeature",
@@ -1337,10 +1456,16 @@ impl FeatureKind {
FeatureKind::Align { .. } => "AlignFeature",
FeatureKind::Remove { .. } => "RemoveFeature",
FeatureKind::Patch { .. } => "PatchFeature",
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrudeFeature",
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolveFeature",
FeatureKind::SurfaceSweep { .. } => "SurfaceSweepFeature",
FeatureKind::SurfaceLoft { .. } => "SurfaceLoftFeature",
FeatureKind::Stitch { .. } => "StitchFeature",
FeatureKind::Thicken { .. } => "ThickenFeature",
FeatureKind::SurfaceTrim { .. } => "TrimFeature",
FeatureKind::SurfaceExtend { .. } => "ExtendFeature",
FeatureKind::Unstitch { .. } => "UnstitchFeature",
FeatureKind::ReverseNormal { .. } => "ReverseNormalFeature",
FeatureKind::Import { .. } => "BaseFeature",
FeatureKind::MeshImport { .. } => "MeshFeature",
FeatureKind::MeshConvert { .. } => "MeshConvertFeature",
@@ -1379,6 +1504,7 @@ impl FeatureKind {
FeatureKind::Scale { .. } => "Scale",
FeatureKind::OffsetFace { .. } => "OffsetFace",
FeatureKind::DeleteFace { .. } => "DeleteFace",
FeatureKind::SurfaceOffset { .. } => "OffsetSurface",
FeatureKind::BoundingSolid { .. } => "BoundingSolid",
FeatureKind::Pipe { .. } => "Pipe",
FeatureKind::Emboss { .. } => "Emboss",
@@ -1403,10 +1529,16 @@ impl FeatureKind {
FeatureKind::Align { .. } => "Align",
FeatureKind::Remove { .. } => "Remove",
FeatureKind::Patch { .. } => "Patch",
FeatureKind::SurfaceExtrude { .. } => "SurfaceExtrude",
FeatureKind::SurfaceRevolve { .. } => "SurfaceRevolve",
FeatureKind::SurfaceSweep { .. } => "SurfaceSweep",
FeatureKind::SurfaceLoft { .. } => "SurfaceLoft",
FeatureKind::Stitch { .. } => "Stitch",
FeatureKind::Thicken { .. } => "Thicken",
FeatureKind::SurfaceTrim { .. } => "Trim",
FeatureKind::SurfaceExtend { .. } => "Extend",
FeatureKind::Unstitch { .. } => "Unstitch",
FeatureKind::ReverseNormal { .. } => "ReverseNormal",
FeatureKind::Import { .. } => "Import",
FeatureKind::MeshImport { .. } => "Mesh",
FeatureKind::MeshConvert { .. } => "ConvertMesh",
@@ -1423,8 +1555,14 @@ impl FeatureKind {
| FeatureKind::Sweep { sketch, profiles, .. }
| FeatureKind::Emboss { sketch, profiles, .. }
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sk), profiles, .. } => vec![(*sk, profiles)],
FeatureKind::Patch { sketch: Some(sk), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch: sk, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch: sk, profiles: Some(profiles), .. } => {
vec![(*sk, profiles)]
}
FeatureKind::Loft { sections, .. } => sections.iter_mut().filter(|s| s.point.is_none()).map(|s| (s.sketch, &mut s.profiles)).collect(),
FeatureKind::SurfaceLoft { sections } => sections.iter_mut().filter_map(|s| Some((s.sketch, s.profiles.as_mut()?))).collect(),
_ => Vec::new(),
}
}
@@ -1437,10 +1575,16 @@ impl FeatureKind {
| FeatureKind::Sweep { sketch, profiles, .. }
| FeatureKind::Emboss { sketch, profiles, .. }
| FeatureKind::SheetBase { sketch, profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sketch), profiles, .. } => vec![(*sketch, profiles)],
FeatureKind::Patch { sketch: Some(sketch), profiles, .. }
| FeatureKind::SurfaceExtrude { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceRevolve { sketch, profiles: Some(profiles), .. }
| FeatureKind::SurfaceSweep { sketch, profiles: Some(profiles), .. } => {
vec![(*sketch, profiles)]
}
FeatureKind::Loft { sections, .. } => {
sections.iter().filter(|section| section.point.is_none()).map(|section| (section.sketch, &section.profiles)).collect()
}
FeatureKind::SurfaceLoft { sections } => sections.iter().filter_map(|s| Some((s.sketch, s.profiles.as_ref()?))).collect(),
_ => Vec::new(),
}
}
@@ -1451,6 +1595,7 @@ impl FeatureKind {
| FeatureKind::Draft { faces, .. }
| FeatureKind::OffsetFace { faces, .. }
| FeatureKind::DeleteFace { faces, .. }
| FeatureKind::SurfaceOffset { faces, .. }
| FeatureKind::SplitFace { faces, .. }
| FeatureKind::ReplaceFace { faces, .. } => faces.iter_mut().collect(),
FeatureKind::Hole { position, points: None, to, .. } => std::iter::once(position).chain(to.iter_mut()).collect(),
@@ -1460,6 +1605,8 @@ impl FeatureKind {
FeatureKind::Thread { face, .. } => vec![face],
FeatureKind::Emboss { face: Some(face), .. } => vec![&mut face.at],
FeatureKind::Boss { position, .. } | FeatureKind::SnapFit { position, .. } | FeatureKind::Rest { position, .. } => vec![position],
// With bend edges the fixed face is found by name (a plate keeps its point).
FeatureKind::SheetConvert { face, bends, .. } if !bends.is_empty() => vec![face],
_ => Vec::new(),
}
}
@@ -1483,7 +1630,7 @@ impl FeatureKind {
| FeatureKind::ConstructionPlane { plane }
| FeatureKind::Split { plane, .. }
| FeatureKind::SplitFace { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::Extrude { extent, .. } => {
FeatureKind::Extrude { extent, .. } | FeatureKind::SurfaceExtrude { extent, .. } => {
v.push(&extent.distance);
if let Some(d) = &extent.distance2 {
v.push(d);
@@ -1498,15 +1645,19 @@ impl FeatureKind {
v.push(d);
}
}
FeatureKind::Revolve { angle, angle2, .. } => {
FeatureKind::Revolve { angle, angle2, .. } | FeatureKind::SurfaceRevolve { angle, angle2, .. } => {
v.push(angle);
v.extend(angle2.iter().map(String::as_str));
}
FeatureKind::Fillet { radius, style, .. } => {
v.push(radius);
if let FilletStyle::Variable { radius2, mid, .. } = style {
v.push(radius2);
v.extend(mid.iter().map(|(_, r)| r.as_str()));
match style {
FilletStyle::Variable { radius2, mid, .. } => {
v.push(radius2);
v.extend(mid.iter().map(|(_, r)| r.as_str()));
}
FilletStyle::Spline { radii, .. } => v.extend(radii.iter().map(String::as_str)),
FilletStyle::Constant | FilletStyle::Chord | FilletStyle::Curvature => {}
}
}
FeatureKind::Chamfer { distance, distance2, angle, .. } => {
@@ -1542,7 +1693,7 @@ impl FeatureKind {
},
FeatureKind::Mirror { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::Shell { thickness, .. } => v.push(thickness),
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } => {}
FeatureKind::Loft { .. } | FeatureKind::Sweep { .. } | FeatureKind::SurfaceSweep { .. } | FeatureKind::SurfaceLoft { .. } => {}
FeatureKind::Draft { angle, neutral, .. } => {
v.push(angle);
plane_exprs(neutral, &mut v);
@@ -1620,11 +1771,18 @@ impl FeatureKind {
}
FeatureKind::ReplaceFace { target, .. } => plane_exprs(target, &mut v),
FeatureKind::Align { .. } | FeatureKind::Remove { .. } | FeatureKind::BoundaryFill { .. } | FeatureKind::DeleteFace { .. } => {}
FeatureKind::SurfaceOffset { distance, tolerance, .. } => {
v.push(distance);
if let Some(t) = tolerance {
v.push(t);
}
}
FeatureKind::Stitch { tolerance, .. } => v.push(tolerance),
FeatureKind::MeshConvert { tolerance, .. } => v.push(tolerance),
FeatureKind::Thicken { thickness, .. } => v.push(thickness),
FeatureKind::SurfaceTrim { plane, .. } => plane_exprs(plane, &mut v),
FeatureKind::SurfaceExtend { distance, .. } => v.push(distance),
FeatureKind::Unstitch { .. } | FeatureKind::ReverseNormal { .. } => {}
FeatureKind::Patch { .. } => {}
FeatureKind::Import { .. } | FeatureKind::MeshImport { .. } | FeatureKind::BodyTransfer { .. } => {}
}
@@ -1681,6 +1839,14 @@ fn all_profiles() -> ProfileSel {
ProfileSel::All
}
fn yes() -> bool {
true
}
fn is_yes(b: &bool) -> bool {
*b
}
fn zero_expr() -> String {
"0".into()
}
@@ -1777,6 +1943,10 @@ pub struct Document {
/// Browser groups (folders of bodies, sketches or construction planes).
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub browser_groups: Vec<BrowserGroup>,
/// Named folders used to organise features in the timeline. They do not affect evaluation
/// order; feature ids remain the source of truth when features are renamed or reordered.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub timeline_groups: Vec<TimelineGroup>,
/// Browser item order per component folder (`"<component>/<folder>"` → item keys).
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub browser_order: std::collections::BTreeMap<String, Vec<String>>,
@@ -1843,6 +2013,66 @@ pub struct BrowserGroup {
pub items: Vec<String>,
}
/// A saved feature-history folder. Folders may nest, and a feature can belong to at most one folder.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct TimelineGroup {
pub id: u64,
pub name: String,
/// The containing feature folder. `None` places the folder at the timeline root.
#[serde(default)]
pub parent: Option<u64>,
#[serde(default)]
pub features: Vec<u64>,
}
/// The first feature-history folder whose own and nested features do not form one
/// uninterrupted run in the timeline.
pub fn non_contiguous_timeline_group(features: &[Feature], groups: &[TimelineGroup]) -> Option<u64> {
let positions: std::collections::BTreeMap<u64, usize> = features.iter().enumerate().map(|(index, feature)| (feature.id, index)).collect();
let mut stats = std::collections::BTreeMap::new();
let mut children = std::collections::BTreeMap::new();
for group in groups {
let mut count = 0usize;
let mut first = usize::MAX;
let mut last = 0usize;
for index in group.features.iter().filter_map(|feature| positions.get(feature)) {
count = count.saturating_add(1);
first = first.min(*index);
last = last.max(*index);
}
stats.insert(group.id, (count, first, last));
children.entry(group.id).or_insert(0usize);
if let Some(parent) = group.parent {
let child_count = children.entry(parent).or_insert(0);
*child_count = child_count.saturating_add(1);
}
}
let parents: std::collections::BTreeMap<u64, Option<u64>> = groups.iter().map(|group| (group.id, group.parent)).collect();
let mut pending: Vec<u64> = groups.iter().filter(|group| children.get(&group.id) == Some(&0)).map(|group| group.id).collect();
while let Some(id) = pending.pop() {
let (count, first, last) = stats.get(&id).copied().unwrap_or_default();
if count > 0 && last.saturating_sub(first).saturating_add(1) != count {
return Some(id);
}
if let Some(parent) = parents.get(&id).copied().flatten() {
if let Some((parent_count, parent_first, parent_last)) = stats.get_mut(&parent) {
*parent_count = parent_count.saturating_add(count);
if count > 0 {
*parent_first = (*parent_first).min(first);
*parent_last = (*parent_last).max(last);
}
}
if let Some(remaining) = children.get_mut(&parent) {
*remaining = remaining.saturating_sub(1);
if *remaining == 0 {
pending.push(parent);
}
}
}
}
None
}
fn default_format() -> String {
crate::format::format_string(crate::format::FORMAT)
}
@@ -1883,6 +2113,7 @@ impl Document {
canvases: Vec::new(),
param_comments: Default::default(),
browser_groups: Vec::new(),
timeline_groups: Vec::new(),
browser_order: Default::default(),
named_views: Vec::new(),
visibility: Visibility::default(),
@@ -2067,6 +2298,40 @@ impl Document {
}
}
/// The deepest timeline folder spanning the insertion slot between `marker - 1` and
/// `marker`. A slot at a folder's outer boundary is not inside that folder.
fn timeline_group_at_insertion(&self, marker: usize) -> Option<u64> {
let before = self.features.get(marker.checked_sub(1)?)?.id;
let after = self.features.get(marker)?.id;
let direct = |feature| self.timeline_groups.iter().find(|group| group.features.contains(&feature)).map(|group| group.id);
let parents: std::collections::BTreeMap<u64, Option<u64>> = self.timeline_groups.iter().map(|group| (group.id, group.parent)).collect();
let mut before_ancestors = std::collections::BTreeSet::new();
let mut current = direct(before);
let mut steps = 0usize;
while let Some(id) = current {
if !before_ancestors.insert(id) || steps >= self.timeline_groups.len() {
return None;
}
current = parents.get(&id).copied().flatten();
steps = steps.saturating_add(1);
}
current = direct(after);
steps = 0;
while let Some(id) = current {
if before_ancestors.contains(&id) {
return Some(id);
}
if steps >= self.timeline_groups.len() {
return None;
}
current = parents.get(&id).copied().flatten();
steps = steps.saturating_add(1);
}
None
}
/// Insert a feature at the marker (or the end) and return its id.
pub fn add_feature(&mut self, kind: FeatureKind, name: Option<&str>) -> Result<u64> {
if self.features.len() >= MAX_FEATURES {
@@ -2098,6 +2363,7 @@ impl Document {
shell_piece_order: false,
kind,
};
let insertion_group = self.marker.and_then(|marker| self.timeline_group_at_insertion(marker));
match self.marker {
Some(m) if m < self.features.len() => {
self.features.insert(m, f);
@@ -2105,6 +2371,14 @@ impl Document {
}
_ => self.features.push(f),
}
if let Some(group_id) = insertion_group {
let positions: std::collections::BTreeMap<u64, usize> =
self.features.iter().enumerate().map(|(index, feature)| (feature.id, index)).collect();
if let Some(group) = self.timeline_groups.iter_mut().find(|group| group.id == group_id) {
group.features.push(id);
group.features.sort_by_key(|feature| positions.get(feature).copied().unwrap_or(usize::MAX));
}
}
self.name_feature_inputs(id);
Ok(id)
}
@@ -2119,6 +2393,9 @@ impl Document {
.rules
.retain(|rule| !rule.references_features(&gone) && !rule.parameter_references().iter().any(|name| gone_params.contains(name)));
self.features.retain(|f| !gone.contains(&f.id));
for group in &mut self.timeline_groups {
group.features.retain(|feature| !gone.contains(feature));
}
// Configuration columns of deleted features go with them.
let keep: Vec<bool> = self
.configs
@@ -2402,45 +2679,9 @@ impl Document {
}
/// Density of the default material (g/cm³), as Fusion's default.
pub const DEFAULT_DENSITY: f64 = 1.29;
/// Physical materials: name and density (g/cm³).
pub const MATERIALS: [(&str, f64); 14] = [
("Default", DEFAULT_DENSITY),
("Steel", 7.85),
("Stainless Steel", 8.0),
("Aluminum", 2.70),
("Brass", 8.50),
("Copper", 8.96),
("Titanium", 4.43),
("Cast Iron", 7.20),
("ABS Plastic", 1.06),
("PLA", 1.24),
("Nylon", 1.14),
("Polycarbonate", 1.20),
("Oak", 0.75),
("Glass", 2.50),
];
/// How a physical material looks (sRGB); the default material has no colour of its own.
pub fn material_color(name: &str) -> Option<[u8; 3]> {
Some(match name {
"Steel" => [158, 164, 172],
"Stainless Steel" => [186, 191, 198],
"Aluminum" => [204, 208, 214],
"Brass" => [208, 172, 88],
"Copper" => [204, 122, 80],
"Titanium" => [150, 148, 158],
"Cast Iron" => [104, 104, 110],
"ABS Plastic" => [222, 222, 216],
"PLA" => [110, 160, 222],
"Nylon" => [236, 230, 214],
"Polycarbonate" => [196, 216, 232],
"Oak" => [190, 146, 92],
"Glass" => [170, 208, 218],
_ => return None,
})
}
#[path = "../../../assets/materials/library.rs"]
mod materials;
pub use materials::{DEFAULT_DENSITY, MATERIALS, material_color};
/// A physical material defined in a design (`material.create`): its name, its density and,
/// optionally, the look it gives bodies without an appearance of their own.
+664 -100
View File
File diff suppressed because it is too large. Load diff
+277 -67
View File
@@ -300,6 +300,17 @@ fn replace_sheet_body(mb: &mut ModelBody, body: Body) {
*mb = next;
}
/// Put a converted sheet in the place of the body it was made from.
fn put_sheet(st: &mut ModelState, sh: sheet::SheetBody) -> Result<()> {
let solid = sh.solid()?;
if let Some(b) = st.bodies.iter_mut().find(|b| b.name == sh.body) {
replace_sheet_body(b, solid);
}
st.sheets.retain(|s| s.body != sh.body);
st.sheets.push(sh);
Ok(())
}
fn rebuild_sheet(st: &mut ModelState, idx: usize) -> Result<()> {
let sheet = st.sheets.get(idx).cloned().ok_or_else(|| DocError::Invalid("sheet".into()))?;
let solid = sheet.solid()?;
@@ -334,26 +345,26 @@ fn on_sheet_material(st: &ModelState, i: usize, p: Vec3) -> bool {
if mesh.triangles.is_empty() {
return true;
}
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`).
// Twice the display mesh's chord tolerance (see `ModelBody::mesh`), and a quarter of the
// thickness: on a large thin sheet the first steps from a point on its face clean through
// it, so a point on the face would touch nothing (#685).
let step = 2.0 * (mb.body.size() * 1e-3).max(1e-3);
let offsets = [-step, 0.0, step];
let thin = st.sheets.get(i).map_or(step, |sh| (sh.t * 0.25).min(step));
let offsets = [-step, -thin, 0.0, thin, step];
offsets.iter().any(|&dx| offsets.iter().any(|&dy| offsets.iter().any(|&dz| mesh.contains(p + Vec3::new(dx, dy, dz)))))
}
/// A region mirrored in the sketch x axis (y → −y); curves other than lines and arcs as polylines.
/// A region mirrored in the sketch x axis (y → −y), exactly: free-form curves through their
/// control points.
fn mirror_y(r: &Region2) -> Region2 {
let f = |p: Vec2| Vec2::new(p.x, -p.y);
let m = |l: &Loop2| {
let segs: Vec<Seg2> = l
.segs
.iter()
.flat_map(|s| match *s {
Seg2::Line { a, b } => vec![Seg2::Line { a: f(a), b: f(b) }],
Seg2::Arc { center, radius, start, sweep } => vec![Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep }],
other => {
let pts: Vec<Vec2> = (0..=16).map(|k| f(other.point_at(k as f64 / 16.0))).collect();
pts.windows(2).map(|w| Seg2::Line { a: w[0], b: w[1] }).collect()
}
.map(|s| match *s {
Seg2::Arc { center, radius, start, sweep } => Seg2::Arc { center: f(center), radius, start: -start, sweep: -sweep },
other => sheet::map_controls(&other, f),
})
.collect();
Loop2 { segs }.ccw()
@@ -549,7 +560,34 @@ pub(super) fn sheet_eval(
}
rebuild_sheet(st, i)
}
FeatureKind::SheetConvert { body, face, rule } => {
// With bend edges: a solid or shelled body whose faces become the sheet.
FeatureKind::SheetConvert { body, face, rule, bends } if !bends.is_empty() => {
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let r = doc.sheet_rule(rule.as_deref());
let rv = doc.rule_values(vals, &r)?;
// The bend edges by name first; a name that is gone keeps its point, with a warning.
let mut picks = Vec::with_capacity(bends.len());
for (i, p) in bends.iter().enumerate() {
let name = f.edge_names.get(i).filter(|n| !n.is_empty());
picks.push(match name.map(|n| (n, crate::naming::locate_edge(&mb, n, *p))) {
Some((n, Ok(hit))) => {
if let Some(note) = hit.note(n) {
append_warning(warning, note);
}
hit.point
}
Some((n, Err(_))) => {
append_warning(warning, format!("edge `{n}` no longer exists; using the edge at the picked point"));
*p
}
None => *p,
});
}
let mut sh = sheet::convert_body(&mb, &r.name, &rv, *face, &picks)?;
tag_flanges(&mut sh, 0, f.id);
put_sheet(st, sh)
}
FeatureKind::SheetConvert { body, face, rule, .. } => {
let mb = st.body(body).cloned().ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let n = planar_normal_at(&mb.body, *face).ok_or_else(|| DocError::Invalid("pick a planar face of the plate".into()))?;
let tol = (mb.body.size() * 1e-3).max(1e-3);
@@ -558,30 +596,31 @@ pub(super) fn sheet_eval(
let ds: Vec<f64> = mesh.positions.iter().map(|p| (*p - *face).dot(n)).collect();
let (lo, hi) = (ds.iter().cloned().fold(f64::MAX, f64::min), ds.iter().cloned().fold(f64::MIN, f64::max));
let t = hi - lo;
// The face outline: boundary edges of the triangles on that face's plane.
// The face outline: boundary edges of the triangles on that face's plane, where it runs
// along a round wall an arc of the wall's circle (not chords: the plate check below
// compares its area with the exact volume).
let plane = Plane::from_normal(*face + n * lo, n * -1.0).ok_or_else(|| DocError::Invalid("face".into()))?;
let mut loops = face_loops(&mesh, &plane, *face, n, lo, tol);
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
let mut walls = std::collections::HashMap::new();
let mut wall = |f: u32| *walls.entry(f).or_insert_with(|| round_wall(&mb.body, &plane, n, f));
let mut loops: Vec<Loop2> =
face_outline(&mesh, &plane, *face, n, lo, tol).iter().map(|l| outline_loop(l, &mut wall, tol * 1e-2)).collect();
loops.sort_by(|a, b| b.signed_area().abs().total_cmp(&a.signed_area().abs()));
let mut it = loops.into_iter();
let outer = Loop2::polygon(&it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?).ccw();
let region = Region2 { outer, holes: it.map(|l| Loop2::polygon(&l).ccw().reversed()).collect() };
let outer = it.next().ok_or_else(|| DocError::Invalid("no outline on that face".into()))?.ccw();
let region = Region2 { outer, holes: it.map(|l| l.ccw().reversed()).collect() };
let area = region.area().abs();
let vol = kernel::measure(&mb.body)?.volume;
if (area * t - vol).abs() > 1e-3 * vol.max(1.0) {
return Err(DocError::Invalid("not supported yet: converting bodies that are not a plate of even thickness".into()));
return Err(DocError::Invalid(
"not supported yet: converting bodies that are not a plate of even thickness without bend edges (`bends`)".into(),
));
}
let r = doc.sheet_rule(rule.as_deref());
let mut rv = doc.rule_values(vals, &r)?;
rv.thickness = t;
let mut sh = sheet::base_flange(body, &r.name, &rv, plane, Region2 { outer: region.outer.clone(), holes: Vec::new() });
sh.holes = region.holes.iter().map(|h| Region2 { outer: h.ccw(), holes: Vec::new() }).collect();
let solid = sh.solid()?;
if let Some(b) = st.bodies.iter_mut().find(|b| &b.name == body) {
replace_sheet_body(b, solid);
}
st.sheets.retain(|s| &s.body != body);
st.sheets.push(sh);
Ok(())
put_sheet(st, sh)
}
_ => Err(DocError::Invalid(format!("{} is not a sheet metal feature", f.name))),
}
@@ -695,9 +734,17 @@ fn any_eligible_sheet(f: &Feature, st: &ModelState, targets: &[String]) -> bool
/// A Box or Cylinder cut (or intersect) whose tool is a straight prism along the normal of
/// the sheet it meets: machined like the equivalent extrude of its footprint, so the sheet
/// keeps it as a flat cut-out or pocket. Returns the sheet bodies it handled; none when the
/// keeps it as a flat cut-out or pocket. A hole's drill (`drill`) may also be square to a
/// folded panel of the sheet, a flange's. Returns the sheet bodies it handled; none when the
/// primitive is not such a prism (the generic preflight then refuses real 3D machining).
pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body, prism: PrimitivePrism, operation: Operation) -> Result<Vec<String>> {
pub(super) fn sheet_primitive_cut(
f: &Feature,
st: &mut ModelState,
tool: &Body,
prism: PrimitivePrism,
operation: Operation,
drill: bool,
) -> Result<Vec<String>> {
if !matches!(operation, Operation::Cut | Operation::Intersect) || !any_eligible_sheet(f, st, &[]) {
return Ok(Vec::new());
}
@@ -707,7 +754,14 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
if let Some(mb) = st.body(&sh.body)
&& kernel::boolean(&mb.body, tool, kernel::BoolOp::Intersect)?.is_some()
{
normal = Some(sh.frame.normal());
normal = Some(match prism {
PrimitivePrism::Cylinder { axis, .. }
if drill && !sh.flat && Plane::from_normal(Vec3::ZERO, axis).is_some_and(|p| !sh.parallel_panels(&p).is_empty()) =>
{
axis
}
_ => sh.frame.normal(),
});
break;
}
}
@@ -717,7 +771,47 @@ pub(super) fn sheet_primitive_cut(f: &Feature, st: &mut ModelState, tool: &Body,
sheet_prism_cut(f, st, &plane, std::slice::from_ref(&region), operation, &tools, &[], None, false)
}
/// A hole's straight drill along a panel of a sheet (into its rim), kept as a bore of each
/// panel it meets (see [`crate::sheet::SheetBore`]), so the sheet still unfolds. Returns the
/// sheet bodies it machined; None, with the model as it was, when the drill is not a plain
/// cylinder or a sheet it meets can't keep it that way (it runs into a bend, or crosses a
/// panel at an angle).
pub(super) fn sheet_bore_cut(f: &Feature, st: &mut ModelState, prism: PrimitivePrism) -> Result<Option<Vec<String>>> {
let PrimitivePrism::Cylinder { base, axis, radius, height } = prism else { return Ok(None) };
let mut plans = Vec::new();
for original in &st.sheets {
if !sheet_eligible(f, st, &[], original) {
continue;
}
let mut sh = original.clone();
match sh.add_bore(base, axis, radius, height) {
Ok(true) => {}
Ok(false) => continue,
// Refused here, the hole keeps the message of the machining it was first tried as.
Err(DocError::Invalid(_)) => return Ok(None),
Err(e) => return Err(e),
}
let body = sh.solid()?;
plans.push((sh, body));
}
if plans.is_empty() {
return Ok(None);
}
let mut handled = Vec::new();
for (sh, body) in plans {
if let Some(mb) = st.bodies.iter_mut().find(|b| b.name == sh.body) {
replace_sheet_body(mb, body);
}
handled.push(sh.body.clone());
if let Some(slot) = st.sheets.iter_mut().find(|s| s.body == sh.body) {
*slot = sh;
}
}
Ok(Some(handled))
}
/// A primitive tool's exact geometry, for [`sheet_primitive_cut`].
#[derive(Clone, Copy, Debug)]
pub(super) enum PrimitivePrism {
/// An axis-aligned box between two corners.
Box { lo: Vec3, hi: Vec3 },
@@ -918,9 +1012,13 @@ fn sheet_prism_cut(
}
modeled = true;
} else if z1 > z0 {
// Each tool meets only the panel's cut-outs near it: arranging every one of a
// perforation's holes against every new hole would grow with their square (#732).
let near = sheet::NearHoles::new(&panel)?;
for flat in mapped {
let local = near.around(&flat)?;
let clipped =
sheet::material_boolean(std::slice::from_ref(&panel), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
sheet::material_boolean(std::slice::from_ref(&local), std::slice::from_ref(&flat), kernel::BoolOp::Intersect, &mut work)?;
if !clipped.is_empty() && !depth_resolved {
return Err(DocError::Invalid(
"sheet machining depth is below the supported 0.000001 mm extrusion resolution; increase the layer depth".into(),
@@ -978,50 +1076,78 @@ fn sheet_prism_cut(
/// Outline loops (in `plane` coordinates) of the mesh triangles lying in the plane at distance
/// `level` from `at` along `n`: the edges used by one of them only, chained.
fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<Vec2>> {
let on: Vec<[u32; 3]> = mesh
.triangles
.iter()
.copied()
.filter(|t| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol)))
.collect();
face_outline(mesh, plane, at, n, level, tol).into_iter().map(|l| l.into_iter().map(|(p, _)| p).collect()).collect()
}
/// [`face_loops`] with each point's B-rep face across the outline from it to the next point (the
/// wall the outline runs along there): the face off the plane that meets both points.
fn face_outline(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, level: f64, tol: f64) -> Vec<Vec<(Vec2, Option<u32>)>> {
let on_plane = |t: &[u32; 3]| t.iter().all(|k| mesh.positions.get(*k as usize).is_some_and(|p| ((*p - at).dot(n) - level).abs() < tol));
let on: Vec<[u32; 3]> = mesh.triangles.iter().copied().filter(on_plane).collect();
// Boundary edges by position (meshes may repeat vertices per triangle).
let pos = |k: u32| mesh.positions.get(k as usize).copied().unwrap_or_default();
let q = |p: Vec3| ((p.x * 1e6).round() as i64, (p.y * 1e6).round() as i64, (p.z * 1e6).round() as i64);
let key = |a: Vec3, b: Vec3| {
let (ka, kb) = (q(a), q(b));
(ka.min(kb), ka.max(kb))
};
let mut count: std::collections::HashMap<((i64, i64, i64), (i64, i64, i64)), i32> = Default::default();
let edges: Vec<(Vec3, Vec3)> = on.iter().flat_map(|t| [(pos(t[0]), pos(t[1])), (pos(t[1]), pos(t[2])), (pos(t[2]), pos(t[0]))]).collect();
for (a, b) in &edges {
let (ka, kb) = (q(*a), q(*b));
*count.entry((ka.min(kb), ka.max(kb))).or_insert(0) += 1;
*count.entry(key(*a, *b)).or_insert(0) += 1;
}
let mut boundary: Vec<(Vec3, Vec3)> = edges
.into_iter()
.filter(|(a, b)| {
let (ka, kb) = (q(*a), q(*b));
count.get(&(ka.min(kb), ka.max(kb))) == Some(&1)
})
.collect();
// The faces off the plane at each point on it, by position to a millionth of a millimetre
// either way (a neighbouring face's mesh may round a shared point differently).
let mut touch: std::collections::HashMap<(i64, i64, i64), Vec<u32>> = Default::default();
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face).filter(|(t, _)| !on_plane(t)) {
for p in t.iter().map(|k| pos(*k)).filter(|p| ((*p - at).dot(n) - level).abs() < tol) {
let faces = touch.entry(q(p)).or_default();
if !faces.contains(f) {
faces.push(*f);
}
}
}
let near = |p: Vec3| {
let (x, y, z) = q(p);
let cells = (-1..=1).flat_map(|i| (-1..=1).flat_map(move |j| (-1..=1).map(move |k| (x + i, y + j, z + k))));
let mut faces: Vec<u32> = cells.filter_map(|c| touch.get(&c)).flatten().copied().collect();
faces.sort_unstable();
faces.dedup();
faces
};
let across = |a: Vec3, b: Vec3| {
let at_b = near(b);
near(a).into_iter().find(|f| at_b.contains(f))
};
let mut boundary: Vec<(Vec3, Vec3)> = edges.into_iter().filter(|(a, b)| count.get(&key(*a, *b)) == Some(&1)).collect();
// Chain into loops (plane coordinates).
let mut loops: Vec<Vec<Vec2>> = Vec::new();
let mut loops: Vec<Vec<(Vec2, Option<u32>)>> = Vec::new();
for _ in 0..10_000 {
let Some((s0, mut cur)) = boundary.pop() else { break };
let mut lp = vec![s0];
let mut lp = vec![(s0, across(s0, cur))];
for _ in 0..boundary.len() + 1 {
if q(cur) == q(s0) {
break;
}
lp.push(cur);
match boundary.iter().position(|(a, _)| q(*a) == q(cur)) {
Some(k) => cur = boundary.remove(k).1,
None => break,
Some(k) => {
let b = boundary.remove(k).1;
lp.push((cur, across(cur, b)));
cur = b;
}
None => {
lp.push((cur, None));
break;
}
}
}
let raw: Vec<Vec2> = lp.iter().map(|p| plane.to_local(*p)).collect();
let raw: Vec<(Vec2, Option<u32>)> = lp.iter().map(|(p, f)| (plane.to_local(*p), *f)).collect();
// Drop points in the middle of straight runs.
let m = raw.len();
let pts: Vec<Vec2> = (0..m)
let pts: Vec<(Vec2, Option<u32>)> = (0..m)
.filter_map(|i| {
let (p, c, n) = (*raw.get((i + m - 1) % m)?, *raw.get(i)?, *raw.get((i + 1) % m)?);
((c - p).cross(n - c).abs() > 1e-9 * (c - p).len().max(1e-9) * (n - c).len().max(1e-9)).then_some(c)
let (p, c, n) = (raw.get((i + m - 1) % m)?.0, *raw.get(i)?, raw.get((i + 1) % m)?.0);
((c.0 - p).cross(n - c.0).abs() > 1e-9 * (c.0 - p).len().max(1e-9) * (n - c.0).len().max(1e-9)).then_some(c)
})
.collect();
if pts.len() >= 3 {
@@ -1031,6 +1157,62 @@ fn face_loops(mesh: &solidcraft_geom::Mesh, plane: &Plane, at: Vec3, n: Vec3, le
loops
}
/// The circle (centre in `plane` coordinates, radius) of face `f` when it is a round wall
/// standing square on the plane: a cylinder about the plane's normal `n`.
fn round_wall(body: &Body, plane: &Plane, n: Vec3, f: u32) -> Option<(Vec2, f64)> {
match kernel::face_surface(body, f as usize)? {
kernel::FaceSurface::Cylinder { origin, axis, radius } if axis.cross(n).len() < 1e-6 => Some((plane.to_local(origin), radius)),
_ => None,
}
}
/// A loop of [`face_outline`] as segments: each run along one round wall (`wall` gives a face's
/// circle) is an arc of that circle, the rest stays straight. Coaxial cylinders of one radius are
/// one wall, however many faces it comes in (many systems write a round hole's wall as two
/// half-cylinders), so such a hole is one circle. `eps`: how far off its circle a point may be.
fn outline_loop(pts: &[(Vec2, Option<u32>)], wall: &mut impl FnMut(u32) -> Option<(Vec2, f64)>, eps: f64) -> Loop2 {
use std::f64::consts::{PI, TAU};
let m = pts.len();
let point = |i: usize| pts.get(i % m).map(|x| x.0).unwrap_or_default();
// The circle each segment runs along: its wall's, when both its ends lie on it.
let circles: Vec<Option<(Vec2, f64)>> = (0..m)
.map(|i| {
let (c, r) = wall(pts.get(i)?.1?)?;
((point(i).dist(c) - r).abs() <= eps && (point(i + 1).dist(c) - r).abs() <= eps).then_some((c, r))
})
.collect();
let circle = |i: usize| circles.get(i % m).copied().flatten();
let same = |i: usize, (c, r): (Vec2, f64)| circle(i).is_some_and(|(d, s)| d.dist(c) <= eps && (s - r).abs() <= eps);
// Start where no arc runs on across the start (anywhere when the whole loop is one circle).
let start = (0..m).find(|i| circle(*i + m - 1).is_none_or(|c| !same(*i, c))).unwrap_or(0);
let mut segs = Vec::new();
let mut i = 0;
while i < m {
let k = start + i;
let Some((c, r)) = circle(k) else {
segs.push(Seg2::Line { a: point(k), b: point(k + 1) });
i += 1;
continue;
};
let mut j = i + 1;
while j < m && same(start + j, (c, r)) {
j += 1;
}
let sweep: f64 = (i..j)
.map(|s| {
let d = (point(start + s + 1) - c).angle() - (point(start + s) - c).angle();
(d + PI).rem_euclid(TAU) - PI
})
.sum();
// Arcs of up to half a turn each.
let pieces = ((sweep.abs() / PI - 1e-9).ceil() as usize).clamp(1, 4);
let (a0, step) = ((point(k) - c).angle(), sweep / pieces as f64);
segs.extend((0..pieces).map(|p| Seg2::Arc { center: c, radius: r, start: a0 + step * p as f64, sweep: step }));
i = j;
}
Loop2 { segs }
}
/// The body a placed plastic feature joins: by name, else the one under the point.
fn body_under(st: &ModelState, body: &Option<String>, p: Vec3) -> Result<String> {
let i = body_at(st, body, &[p])?;
@@ -1117,11 +1299,12 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let base = Plane::new(*position - z * delta, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let r_in = r0 + delta * dr.tan();
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, r_in), holes: Vec::new() };
let mut tools = if dr.abs() > 1e-12 {
let post = if dr.abs() > 1e-12 {
vec![kernel::extrude_tapered(&base, &disc, h + delta, 1.0, -dr)?]
} else {
kernel::extrude(&base, &[disc], 0.0, h + delta)?
};
let mut tools: Vec<Traced> = post.into_iter().map(|b| crate::lineage::part_tool(b, "post")).collect();
if let Some(n) = ribs {
let n = val(vals, n, Kind::Unitless)?.round();
if !(0.0..=64.0).contains(&n) {
@@ -1156,19 +1339,19 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
]),
holes: Vec::new(),
};
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?);
tools.extend(kernel::extrude(&rp, &[rect], -delta, hr)?.into_iter().map(|b| crate::lineage::part_tool(b, &format!("rib{k}"))));
}
}
apply_op(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
apply_traced(st, f, tools, Operation::Join, std::slice::from_ref(&target))?;
// The root fillet: rounds the circle where the post meets the face.
if rf > 1e-9 {
let i = st.bodies.iter().position(|b| b.name == target).ok_or_else(|| DocError::Invalid("boss body".into()))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("boss body".into())) };
let mut warn = None;
let edges = resolve_edges(&mb.body, &[*position + x * r0], &[], &mut warn)?;
let nb = kernel::fillet(&mb.body, &edges, rf)?;
let nb = mb.traced().then(kernel::fillet(&mb.body, &edges, rf)?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::new(mb.name, nb, mb.feature);
*slot = ModelBody::traced_new(mb.name, nb, mb.feature);
}
}
if let Some(hd) = hole_diameter {
@@ -1182,7 +1365,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
}
let hp = Plane::new(*position, x, z.cross(x)).ok_or_else(|| DocError::Invalid("hole frame".into()))?;
let tool = kernel::extrude(&hp, &[Region2 { outer: Loop2::circle(Vec2::ZERO, rh), holes: Vec::new() }], h - depth, h + delta)?;
apply_op(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "hole")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))?;
}
Ok(())
}
@@ -1205,8 +1389,32 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let mut loops = face_loops(&mesh, &plane, *face, n, 0.0, tol);
loops.sort_by(|a, b| poly_area(b).abs().total_cmp(&poly_area(a).abs()));
let ccw = |l: &[Vec2]| if poly_area(l) < 0.0 { l.iter().rev().copied().collect::<Vec<_>>() } else { l.to_vec() };
let (Some(outer), Some(inner)) = (loops.first().map(|l| ccw(l)), loops.get(1).map(|l| ccw(l))) else {
return Err(DocError::Invalid("pick the rim face of a shelled body (a face with an inner and an outer edge)".into()));
// The inner edge is an opening (the cavity under a rim): not the foot of a boss or a
// wall standing on the face, whose side the band would run against.
let opening = |l: &[Vec2]| {
let eps = (tol * 4.0).max(1e-3);
l.iter().zip(l.iter().cycle().skip(1)).filter_map(|(a, b)| Some(((*a + *b) * 0.5, (*b - *a).normalized()?))).take(3).any(|(m, t)| {
// Just over the face, inside the loop (to the left of a ccw loop's edges).
let q = plane.to_world(m + Vec2::new(-t.y, t.x) * eps) + n * eps;
!mesh.contains(q)
})
};
let outer = loops.first().map(|l| ccw(l));
let openings: Vec<Vec<Vec2>> = loops.iter().skip(1).map(|l| ccw(l)).filter(|l| opening(l)).collect();
// A rim runs round one opening, the cavity; smaller ones (a screw hole) may sit in it.
// Openings alike in size (a plate's holes) leave no edge that is the rim's.
let alike = match openings.as_slice() {
[a, b, ..] => poly_area(b).abs() > 0.5 * poly_area(a).abs(),
_ => false,
};
let (Some(outer), Some(inner), false) = (outer, openings.first().cloned(), alike) else {
let what = match (loops.len(), openings.len()) {
(0 | 1, _) => "pick the rim face of a shelled body (a face with an inner and an outer edge)".to_string(),
(_, 0) => "pick the rim face of a shelled body: this face's inner edges are where other features stand on it, not an opening"
.to_string(),
(_, k) => format!("pick the rim face of a shelled body (round one opening): this face has {k} openings alike in size"),
};
return Err(DocError::Invalid(what));
};
let bw = if *groove { w + g } else { w };
// A lip: the band raised from the rim's own faces (its walls continue the walls
@@ -1215,7 +1423,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
&& let Ok(b) = kernel::raise_band(&mb.body, *face, w, h, *outside)
&& let Some(slot) = st.bodies.iter_mut().find(|x| x.name == target)
{
*slot = ModelBody::new(target.clone(), b, slot.feature);
*slot = ModelBody::traced_new(target.clone(), Traced::step(b, &[&mb.traced()]), slot.feature);
return Ok(());
}
// The band along the chosen edge, inside the rim face. A groove's cutter reaches past
@@ -1234,11 +1442,11 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
};
let delta = (h * 0.05).clamp(1e-3, 0.5);
if *groove {
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?;
apply_op(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
let tool = kernel::extrude(&plane, &[band], -(h + g), delta)?.into_iter().map(|b| crate::lineage::part_tool(b, "groove")).collect();
apply_traced(st, f, tool, Operation::Cut, std::slice::from_ref(&target))
} else {
let tool = kernel::extrude(&plane, &[band], -delta, h)?;
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
let tool = kernel::extrude(&plane, &[band], -delta, h)?.into_iter().map(|b| crate::lineage::part_tool(b, "lip")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
}
FeatureKind::SnapFit { position, direction, hook, length, thickness, width, catch_depth, catch_length, body } => {
@@ -1261,7 +1469,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let sgn = if pl.normal().dot(y) >= 0.0 { 1.0 } else { -1.0 };
let (lo, hi) = if sgn > 0.0 { (0.0, w) } else { (-w, 0.0) };
let tool = kernel::extrude(&pl, &[Region2 { outer: lp, holes: Vec::new() }], lo, hi)?;
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "arm")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
FeatureKind::Rest { position, direction, along, width, length, height, draft, thickness, body } => {
let target = body_under(st, body, *position)?;
@@ -1310,7 +1519,8 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
} else {
kernel::extrude(&base, &[region], 0.0, h + delta)?
};
apply_op(st, f, tool, Operation::Join, std::slice::from_ref(&target))
let tool = tool.into_iter().map(|b| crate::lineage::part_tool(b, "rest")).collect();
apply_traced(st, f, tool, Operation::Join, std::slice::from_ref(&target))
}
_ => Err(DocError::Invalid(format!("{} is not a plastic feature", f.name))),
}
+223 -1
View File
@@ -1,6 +1,9 @@
//! Evaluation of the surface features: Patch, Stitch, Thicken and Trim (kernel `surfaces`).
//! Evaluation of the surface features: Patch, Stitch, Thicken, Trim, Extend, Surface Extrude,
//! Revolve, Sweep and Loft, Offset, Unstitch and Reverse Normal (kernel `surfaces`,
//! `surface_offset`).
use super::*;
use solidcraft_geom::Seg2;
pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st: &mut ModelState) -> Result<()> {
match &f.kind {
@@ -84,6 +87,225 @@ pub(super) fn eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature,
}
Ok(())
}
FeatureKind::SurfaceExtrude { sketch, profiles, curves, extent } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
if extent.through_all || extent.taper.is_some() || extent.to.is_some() || extent.from.is_some() {
return Err(DocError::Invalid("not supported yet: a surface extrude through all, to or from an object, or with a taper".into()));
}
let d = val(vals, &extent.distance, Kind::Length)?;
let off = extent.start_offset.as_deref().map(|e| val(vals, e, Kind::Length)).transpose()?.unwrap_or(0.0);
let (a, b) = extent_sides(vals, extent, d, extent.direction)?;
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
let body = kernel::extrude_sheet(&ss.plane, &chains, a + off, b + off)?;
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
Ok(())
}
FeatureKind::SurfaceRevolve { sketch, profiles, curves, axis, angle, angle2, symmetric } => {
let ss = sketch_in_feature_frame(
doc,
st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?,
f.component,
)?;
let (o, d) = revolve_axis(&ss, axis)?;
let a = val(vals, angle, Kind::Angle)?;
let (start, sweep) = match (angle2, symmetric) {
(Some(a2), _) => {
let a2 = val(vals, a2, Kind::Angle)?;
(-a2, a + a2)
}
(None, true) => (-a / 2.0, a),
(None, false) => (0.0, a),
};
let chains = sheet_chains(&ss, profiles.as_ref(), curves)?;
let body = kernel::revolve_sheet(&ss.plane, &chains, o, d, start, sweep)?;
push_sheet(st, f, crate::lineage::ChainRoles { sketch: &ss, chains: &chains }.tool(body));
Ok(())
}
FeatureKind::Unstitch { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let (pieces, rest) = kernel::unstitch(&mb.body, faces)?;
let source = mb.traced();
// The faces left keep the body's name; each face taken out is a new surface body.
match rest {
Some(rest) => {
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name.clone(), source.then(rest), mb.feature);
}
}
None => {
st.bodies.remove(i);
}
}
for (k, piece) in pieces.into_iter().enumerate() {
let name = new_name(st, f, k);
st.bodies.push(ModelBody::traced_new(name, source.then(piece), f.id));
}
Ok(())
}
FeatureKind::ReverseNormal { body, faces } => {
let i = st.bodies.iter().position(|b| &b.name == body).ok_or_else(|| DocError::Unknown(format!("body `{body}`")))?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let turned = mb.traced().then(kernel::reverse_faces(&mb.body, faces)?);
if let Some(slot) = st.bodies.get_mut(i) {
*slot = ModelBody::traced_new(mb.name, turned, mb.feature);
}
Ok(())
}
FeatureKind::SurfaceSweep { sketch, profiles, curves, path_sketch, path } => {
let ss = st.sketch(*sketch).ok_or_else(|| DocError::Unknown(format!("sketch {sketch} (it must come earlier in the timeline)")))?;
let ps =
st.sketch(*path_sketch).ok_or_else(|| DocError::Unknown(format!("sketch {path_sketch} (it must come earlier in the timeline)")))?;
// As Sweep: a 3D path sketch is followed in the feature's frame, the curves with it.
let profile;
let ss = if !ps.sketch.wires.is_empty() {
profile = profile_in_frame(doc, ss, f.component)?;
&profile
} else {
ss
};
let mapped = path_in_frame(doc, ps, f.component)?;
let ps = mapped.as_ref();
let chains = sheet_chains(ss, profiles.as_ref(), curves)?;
// The path runs on from the end nearest the middle of the curves.
let pts: Vec<Vec2> = chains.iter().flatten().flat_map(|s| s.polyline(1e-2)).collect();
let start = ss.plane.to_world(pts.iter().fold(Vec2::default(), |a, p| a + *p) * (1.0 / pts.len().max(1) as f64));
let body = match path_points(ps, path, start)? {
Some(pts) => kernel::sweep_sheet_points(&ss.plane, &chains, &pts)?,
None => kernel::sweep_sheet(&ss.plane, &chains, &path_segments(ps, path, start)?)?,
};
let traced = crate::lineage::ChainRoles { sketch: ss, chains: &chains }.tool(body);
push_sheet(st, f, traced);
Ok(())
}
FeatureKind::SurfaceLoft { sections } => {
let mut secs = Vec::with_capacity(sections.len());
let mut sketches = Vec::with_capacity(sections.len());
for sec in sections {
let ss = sketch_in_feature_frame(
doc,
st.sketch(sec.sketch)
.ok_or_else(|| DocError::Unknown(format!("sketch {} (it must come earlier in the timeline)", sec.sketch)))?,
f.component,
)?;
let profiles = match (&sec.profiles, sec.curves.is_empty()) {
(None, true) => Some(&ProfileSel::All),
(p, _) => p.as_ref(),
};
let mut chains = sheet_chains(&ss, profiles, &sec.curves)?.into_iter();
let (Some(chain), None) = (chains.next(), chains.next()) else {
return Err(DocError::Invalid(
"each loft section must be one chain: one profile without holes, or curves meeting end to end".into(),
));
};
secs.push((ss.plane, chain));
sketches.push(ss);
}
let body = kernel::loft_sheet(&secs)?;
// Each wall named by the section curves it runs between, as a loft's sides are.
let loops: Vec<solidcraft_geom::Loop2> = secs.iter().map(|(_, c)| solidcraft_geom::Loop2 { segs: c.clone() }).collect();
let traced = crate::lineage::SectionRoles { sections: sketches.iter().zip(&loops).collect() }.tool(body);
push_sheet(st, f, traced);
Ok(())
}
FeatureKind::SurfaceOffset { faces, body, distance, chain, tolerance } => {
let d = val(vals, distance, Kind::Length)?;
let tol = match tolerance {
Some(t) => val(vals, t, Kind::Length)?,
None => 0.01,
};
let i = body_at(st, body, faces)?;
let Some(mb) = st.bodies.get(i).cloned() else { return Err(DocError::Invalid("body".into())) };
let picks = if *chain { smooth_chain(&mb, faces) } else { faces.clone() };
let mut picked: Vec<usize> = Vec::new();
for p in &picks {
let fi =
crate::appearance::face_index_at(&mb, *p).ok_or_else(|| DocError::Invalid(format!("Offset: no face of {} at {p:?}", mb.name)))?;
if !picked.contains(&fi) {
picked.push(fi);
}
}
let made = Traced::step(kernel::offset_surface(&mb.body, &picked, d, tol)?, &[&mb.traced()]);
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, made, f.id));
Ok(())
}
_ => Err(DocError::Invalid("not a surface feature".into())),
}
}
/// A surface builder's body, added to the model with what its faces came from.
fn push_sheet(st: &mut ModelState, f: &Feature, traced: crate::lineage::Traced) {
let name = new_name(st, f, 0);
st.bodies.push(ModelBody::traced_new(name, traced, f.id));
}
/// What a surface builder sweeps: the boundary loops of the picked profiles, then the picked
/// curves chained end to end.
fn sheet_chains(ss: &SolvedSketch, profiles: Option<&ProfileSel>, curves: &[String]) -> Result<Vec<Vec<Seg2>>> {
let mut chains: Vec<Vec<Seg2>> = Vec::new();
if let Some(sel) = profiles {
for r in solidcraft_sketch::merge_regions(&select_profiles(ss, sel)?) {
chains.extend(std::iter::once(r.outer).chain(r.holes).map(|lp| lp.segs));
}
}
chains.extend(curve_chains(ss, curves)?);
if chains.is_empty() {
return Err(DocError::Invalid("pick the profiles or curves".into()));
}
Ok(chains)
}
/// Most sketch curves one feature chains.
const MAX_CHAIN_CURVES: usize = 1000;
/// The sketch curves `ids` (free-form ones exactly where they can be) grouped into chains of segments that meet end to end, each oriented
/// to run one way. Curves are joined greedily at their free ends: where three meet, one of them
/// starts a chain of its own.
fn curve_chains(ss: &SolvedSketch, ids: &[String]) -> Result<Vec<Vec<Seg2>>> {
if ids.len() > MAX_CHAIN_CURVES {
return Err(DocError::Invalid(format!("at most {MAX_CHAIN_CURVES} curves")));
}
let mut pieces: std::collections::VecDeque<Vec<Seg2>> = ids
.iter()
.map(|id| {
let ci = ss.sketch.curve_index(id).ok_or_else(|| DocError::Unknown(format!("curve `{id}`")))?;
Ok(ss.sketch.exact_segs(ci).unwrap_or_else(|| ss.sketch.segs(ci)))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.filter(|p| !p.is_empty())
.collect();
let reversed = |p: Vec<Seg2>| -> Vec<Seg2> { p.iter().rev().map(Seg2::reversed).collect() };
let near = |p: Vec2, q: Vec2| p.dist(q) <= 1e-6;
let mut chains = Vec::new();
while let Some(mut chain) = pieces.pop_front() {
while let (Some(head), Some(tail)) = (chain.first().map(Seg2::start), chain.last().map(Seg2::end)) {
if near(head, tail) {
break;
}
let ends = |p: &Vec<Seg2>| p.first().map(Seg2::start).zip(p.last().map(Seg2::end));
let Some(i) = pieces.iter().position(|p| ends(p).is_some_and(|(s, e)| near(s, tail) || near(e, tail) || near(s, head) || near(e, head)))
else {
break;
};
let Some(p) = pieces.remove(i) else { break };
let Some((s, e)) = ends(&p) else { continue };
if near(s, tail) {
chain.extend(p);
} else if near(e, tail) {
chain.extend(reversed(p));
} else if near(e, head) {
chain = p.into_iter().chain(chain).collect();
} else {
chain = reversed(p).into_iter().chain(chain).collect();
}
}
chains.push(chain);
}
Ok(chains)
}
+13
View File
@@ -350,6 +350,17 @@ pub fn is_literal(s: &str) -> bool {
}
}
/// Is the expression a bare number without a unit, like `20` or `-1.5`?
pub fn is_bare_number(s: &str) -> bool {
is_literal(s) && s.trim().trim_start_matches(['-', '+']).trim().parse::<f64>().is_ok()
}
/// The length unit of a design with these units: `mm` unless they name another length unit.
pub fn length_unit_or_mm(units: &str) -> &str {
let u = units.trim();
if unit_info(u).is_some_and(|(k, _)| k == Kind::Length) { u } else { "mm" }
}
struct Parser<'a> {
toks: Vec<Tok>,
pos: usize,
@@ -942,5 +953,7 @@ mod tests {
assert_eq!(rename_reference("10 mm", "mm", "x"), "10 mm");
assert!(is_literal("20") && is_literal("-1.5 in") && is_literal("3 deg"));
assert!(!is_literal("d1") && !is_literal("2 * 3") && !is_literal("2 width"));
assert!(is_bare_number(" -1.5 ") && is_bare_number("+2") && !is_bare_number("20 mm") && !is_bare_number("(2)"));
assert_eq!((length_unit_or_mm(" in "), length_unit_or_mm("deg"), length_unit_or_mm("")), ("in", "mm", "mm"));
}
}
+104 -2
View File
@@ -73,13 +73,78 @@
//! certificate: the target's `name`, e.g. a cut wall `F5:side:l1` or a pattern copy
//! `F4:top@F6.2`), where no construction certificate names it alone. Anchors with
//! construction certificates resolve as before.
//! - 17: a `lineage` face name can be a piece's own, qualified by the faces around it
//! (`F2:top#[F2:side:l4,F4:side:l4]`), where Offset Face moved one piece of a face off the
//! others' plane; never a numbered piece. Sheet face names can also name a rim, the sheet's
//! thickness along a panel's straight edge (`sheet:<body>:<panel>:rim.<edge>`, the edge a base
//! sketch line's id, `edge<k>`, or a flange's `far`, `side0` or `side1`), or a flat end of a
//! bend (`rim.bend0`, `rim.bend1`). Version 16 designs need no transformation; the version
//! stamp makes 0.8.0 (which reads up to 16) ask for an update instead of meeting names it
//! can't resolve.
//! Convert to Sheet Metal can take bend edges (`bends` in the feature: points on the sharp
//! edges of a solid or shelled body that become bends). A conversion without `bends` is the
//! plate conversion it always was and saves as before; 0.8.0 would drop the bend edges and
//! refuse the body as no plate.
//! A sketch's link to a projected edge can also keep the edge's persistent name (`edge_name`,
//! beside `face_name`) and is then found by it, wherever the edge went. A link without it is
//! the edge nearest its point, as it always was, and takes the name when its sketch is next
//! edited.
//! A fillet's `fit` can be `taper` (the radius follows the room along each edge it doesn't
//! fit). Fillets with `exact` or `reduce` save as before; 0.8.0 would refuse the unknown fit.
//! Feature-history folders are also first saved in 17 (`timeline_groups`: stable folder id,
//! name, optional parent folder and feature ids). Version 16 designs have no saved timeline
//! folders and open with the empty default.
//! A fillet's `type` can be `spline` (the radius along each edge a B-spline: `start`, `knots`,
//! and the control radii after `radius` in `radii`). Other fillets save as before; 0.8.0 would
//! refuse the unknown type.
//! Under the same version, a sheet metal rule's corner relief shape can also be `tear` (no
//! relief: each bend is cut straight from the bends' corner to where its flange's end
//! starts); a rule that says `notch`, `round` or `rectangular` builds what it did.
//! A sketch's view options can also say `hide_construction` (the Sketch Palette's
//! Construction Geometries); a sketch that shows its construction geometry saves as before.
//! A `SurfaceExtrude` feature (Surface Extrude: `sketch`, optional `profiles`, `curves` and
//! `extent`, walls only) is first saved in 17; 0.8.0 would refuse the unknown feature.
//! - 18: surface features 0.9.0 doesn't know: `Unstitch` (a body's faces taken out as surface
//! bodies) and `ReverseNormal` (a surface body's faces turned over), each a `body` and
//! optional `faces` points. Version 17 designs need no transformation; the version stamp makes
//! 0.9.0 (which reads up to 17) ask for an update instead of meeting a feature it can't read.
//! Under the same version, a sketch text's link source can carry a `style`: `bold` and
//! `italic` (the font's bold or italic face), `align` (`left`, `center` or `right`),
//! `letter_spacing` (mm), `line_spacing` (times the font's line distance), `path` (the id of a
//! curve of the same sketch the baseline follows) and `reverse`. A text without `style` is set
//! as it always was; 0.9.0 would drop the style and set the text plain, off its curve.
//! Under the same version, Delete Face can leave the gap open (`heal: false` on the feature:
//! the faces left are a surface body); a Delete Face without it heals, as it always did, and
//! saves as before. 0.9.0 would heal a gap the design leaves open.
//! A `SurfaceRevolve` feature (Surface Revolve: `sketch`, optional `profiles`, `curves`,
//! `axis`, `angle`, optional `angle2` and `symmetric`) is also first saved in 18.
//! A `SurfaceSweep` feature (Surface Sweep: `sketch`, optional `profiles` and `curves`,
//! `path_sketch`, `path`) is also first saved in 18.
//! A `SurfaceLoft` feature (Surface Loft: `sections`, each a `sketch` with optional `profiles`
//! and `curves`) is also first saved in 18.
//! A `SurfaceOffset` feature (Offset Surface: `faces` points, optional `body`, `distance`,
//! `chain` and `tolerance`) is also first saved in 18.
//! Nothing new is saved for holes, but a hole on a face is now drilled into the material: a
//! saved `direction` that leaves the face the hole starts on (designs up to 17 could save one,
//! taken from another face whose plane passed near the point, such as the far side of a thin
//! plate or a face of another body at the same height) is mirrored into that face when the
//! design is evaluated. Holes whose direction enters their face build as before.
//! Holes at sketch points can be flipped (`flip: true` in the hole's `points`): they drill away
//! from the side they would go into by themselves. Holes without it save as before; 0.9.0 would
//! drop the flip and drill the other way.
//! - 19: editable associative offset-distance dimensions. Older offsets retain their saved
//! fixed distances; migration does not invent dimensions or change geometry.
//! Under the same version, a fillet's style can be `curvature` (a G2 quintic blend; `radius`
//! is the equivalent circular setback). Fillets of the other styles save as before; 0.9.0
//! would refuse the unknown style.
use serde_json::Value;
use crate::{DocError, Document, Result};
/// The format this build writes.
pub const FORMAT: u32 = 16;
pub const FORMAT: u32 = 19;
pub fn format_string(n: u32) -> String {
format!("solidcraft/{n}")
@@ -96,7 +161,7 @@ pub fn version_of(s: &str) -> Option<u32> {
/// JSON-level upgrades: entry `i` turns format `i + 1` into `i + 2`.
type JsonStep = fn(&mut Value) -> Result<()>;
const JSON_STEPS: [JsonStep; 15] = [
const JSON_STEPS: [JsonStep; 18] = [
|_| Ok(()),
v2_to_v3,
v3_to_v4,
@@ -112,6 +177,9 @@ const JSON_STEPS: [JsonStep; 15] = [
v13_to_v14,
|_| Ok(()),
|_| Ok(()),
|_| Ok(()),
|_| Ok(()),
|_| Ok(()),
];
/// Stored face and edge names were worked out from geometry before format 14; they get the
@@ -345,9 +413,43 @@ pub fn read(s: &str) -> Result<Document> {
if d.features.len() > crate::document::MAX_FEATURES
|| d.params.len() > crate::document::MAX_PARAMS
|| d.components.len() > crate::document::MAX_FEATURES
|| d.timeline_groups.len() > crate::document::MAX_FEATURES
|| d.timeline_groups.iter().any(|group| group.features.len() > crate::document::MAX_FEATURES)
{
return Err(DocError::Invalid("document too large".into()));
}
let feature_ids: std::collections::BTreeSet<_> = d.features.iter().map(|feature| feature.id).collect();
let mut group_ids = std::collections::BTreeSet::new();
let mut grouped_features = std::collections::BTreeSet::new();
for group in &d.timeline_groups {
if group.name.trim().is_empty() || group.name.len() > 128 || !group_ids.insert(group.id) {
return Err(DocError::Invalid("damaged design: invalid timeline folder".into()));
}
if group.parent.is_some_and(|parent| parent == group.id) {
return Err(DocError::Invalid("damaged design: timeline folder cannot contain itself".into()));
}
if group.features.iter().any(|feature| !feature_ids.contains(feature) || !grouped_features.insert(*feature)) {
return Err(DocError::Invalid("damaged design: invalid feature in a timeline folder".into()));
}
}
for group in &d.timeline_groups {
if let Some(parent) = group.parent {
if !group_ids.contains(&parent) {
return Err(DocError::Invalid("damaged design: timeline folder has an unknown parent".into()));
}
let mut seen = std::collections::BTreeSet::new();
let mut current = Some(parent);
while let Some(id) = current {
if !seen.insert(id) {
return Err(DocError::Invalid("damaged design: cyclic timeline folders".into()));
}
current = d.timeline_groups.iter().find(|candidate| candidate.id == id).and_then(|candidate| candidate.parent);
}
}
}
if let Some(id) = crate::non_contiguous_timeline_group(&d.features, &d.timeline_groups) {
return Err(DocError::Invalid(format!("damaged design: timeline folder {id} is not contiguous")));
}
for o in &d.occurrences {
if !rigid(&o.transform) {
return Err(DocError::Invalid(format!("damaged design: occurrence `{}` has an invalid placement", o.name)));
Loaded 100 of 520 files, more files were not shown because too many files have changed in this diff. Show more