mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 12:31:56 +00:00
Compare commits
520 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5965439a15 | |||
| 0b2d3ecd44 | |||
| 96e43bb4ef | |||
| 955f5aea43 | |||
| 739e442824 | |||
| 30f951ecf9 | |||
| 02458892d9 | |||
| 1d02a5f2d4 | |||
| c391ef17cb | |||
| e413327821 | |||
| a1eb63c169 | |||
| 610f2e479c | |||
| f49cd33db1 | |||
| 4c7087ec16 | |||
| f87b657c82 | |||
| 64ba396c05 | |||
| 1720c56840 | |||
| b28decfaf6 | |||
| 9e4dc48165 | |||
| 3de2f96b7f | |||
| 436ee95095 | |||
| 1ee2532678 | |||
| 3152e02fe5 | |||
| fbcd4852c1 | |||
| 97cce94a1c | |||
| 5b41d61f1e | |||
| 2863b80574 | |||
| 03a701d678 | |||
| a2ccbd0684 | |||
| 2e566e2c58 | |||
| 47285ccb88 | |||
| da16362626 | |||
| 13cd45d1f1 | |||
| c5e63b7871 | |||
| 2755c9ccfb | |||
| 4ab6d92952 | |||
| e063a7778e | |||
| 8630a3f0e5 | |||
| 0e0c23b080 | |||
| e29075376c | |||
| 44b0056d48 | |||
| 3bcf77ee32 | |||
| 91638cb1f3 | |||
| 66b872a749 | |||
| 2016ba71f4 | |||
| 7556e23fe7 | |||
| 4d06daf87b | |||
| c7e53899af | |||
| 18ca30a311 | |||
| b55f30959d | |||
| 8b98f6c7e3 | |||
| 3a504f5347 | |||
| 7f2acbc004 | |||
| d3720041b6 | |||
| a3c7a6ff93 | |||
| e6fb3793dc | |||
| c6d361347f | |||
| a078d4e7e9 | |||
| c71234edfb | |||
| 00ff63f75e | |||
| 82cd5cc52e | |||
| 7d7016b356 | |||
| cd66495e14 | |||
| 484f0cf8ae | |||
| daecea6edf | |||
| bd38bc40be | |||
| 19c8eb846e | |||
| 6f5f64f2d9 | |||
| b2525ed34e | |||
| 72a76edcf7 | |||
| 710b7a664e | |||
| 0d32821e9e | |||
| d9c5e4c325 | |||
| 12fe17d1b7 | |||
| cf7fbeaba1 | |||
| e2c9f908f8 | |||
| fa961eaa31 | |||
| aa88360357 | |||
| e1ff3fa9c0 | |||
| 3afcd69a5c | |||
| a3483ed451 | |||
| d7252f2240 | |||
| 30e9b87b48 | |||
| b20f522f5d | |||
| d895fef354 | |||
| 070b1414ee | |||
| 111c662fe4 | |||
| ab709e6c46 | |||
| 03bf992bc1 | |||
| 586a358ed4 | |||
| 4d61100534 | |||
| 25ad6c7266 | |||
| cfb3f52aec | |||
| 7c31631ea6 | |||
| c13e516cd5 | |||
| c264c7a42a | |||
| 56f09f3993 | |||
| 6e63f8bd97 | |||
| b01f361edf | |||
| c482dab4a1 | |||
| 189b47c750 | |||
| 1530a8e247 | |||
| 1d2674ebc4 | |||
| 10b5cc5914 | |||
| 41a938e731 | |||
| 9d0b509961 | |||
| 7ae17e6dd5 | |||
| 1cd14e2412 | |||
| 7014b62388 | |||
| adc568a715 | |||
| 12b0d9dbff | |||
| 613903229e | |||
| 0c4eb458df | |||
| b00c432ac9 | |||
| 8a28d13161 | |||
| 74321f8bbd | |||
| a67b3bfdf3 | |||
| abdf9d79cf | |||
| b1bbca97ef | |||
| 2ec11cfc3a | |||
| 2205955329 | |||
| bd9d0778f6 | |||
| a0f7529fd6 | |||
| 4a0d519727 | |||
| ffbcdc614c | |||
| 06af6e1107 | |||
| 84b1aeecae | |||
| a6354dfec5 | |||
| 8108646e2c | |||
| b3cbbe7bd5 | |||
| ae2d2145b2 | |||
| a800a40099 | |||
| dc3061a039 | |||
| 102314dbb2 | |||
| 4ced1d7bcf | |||
| a82f839255 | |||
| 08484af293 | |||
| d29e70bad9 | |||
| 1d482aff74 | |||
| 2445c0f13d | |||
| 3d0e2a8b3c | |||
| 2a01cca00e | |||
| d7cc5621fa | |||
| be6c7fc17c | |||
| f006eefd6e | |||
| 15269f85a1 | |||
| e53390ea1b | |||
| c8314687fc | |||
| 878339cac8 | |||
| 348bbc1dfa | |||
| 262f5c61c0 | |||
| ece2a8693a | |||
| 0f77199449 | |||
| e02ef3246f | |||
| ffbeb42b5c | |||
| 816bcf10de | |||
| 28689e784e | |||
| d396be78a2 | |||
| cbac207852 | |||
| ff2ca2d462 | |||
| 7770f8982d | |||
| 8e61eb7d6f | |||
| ab80a410a1 | |||
| b94692497e | |||
| 7bcc1ee0eb | |||
| 36a60a70d9 | |||
| 5d17fa970b | |||
| b5bab3f663 | |||
| ccea34e73c | |||
| c7f86c7309 | |||
| 4355d117ef | |||
| 39951a22ff | |||
| a3890b3bd5 | |||
| 59cad5f0bb | |||
| 5fbe058051 | |||
| 0752d8d79c | |||
| 0344a41dc4 | |||
| dba8108369 | |||
| 57d787f5ba | |||
| 2eaa15ea04 | |||
| f512d4a004 | |||
| 62d55c3b29 | |||
| 826244bb28 | |||
| 111dbc6855 | |||
| ab7602fd0e | |||
| b1d4293cf0 | |||
| 99d5b43024 | |||
| f937905cf3 | |||
| 46f7d4d8dc | |||
| 8071096a99 | |||
| d17b70241f | |||
| fbe778a1e8 | |||
| 0252f0bf30 | |||
| 633ce20563 | |||
| 0c0c4db612 | |||
| 16bb97f065 | |||
| 3e6c039985 | |||
| 7734ba2300 | |||
| 0e916ee598 | |||
| 87c9a37663 | |||
| 10124c8ccf | |||
| 0870d6474a | |||
| e9b9c8f697 | |||
| cbcf1fe716 | |||
| e21760e46d | |||
| df01e59716 | |||
| 3045d0787e | |||
| d876f475a6 | |||
| ef1a400bb3 | |||
| 53c84eb3a7 | |||
| 9a7a75788e | |||
| 92dc6bc987 | |||
| b5af4a80fa | |||
| 309eda87a2 | |||
| c3fd7aa048 | |||
| 2e6e05bc9a | |||
| 7cea146438 | |||
| 492bba9242 | |||
| 9be14db8c0 | |||
| b8c496f37a | |||
| aeb406188f | |||
| ceed604498 | |||
| 44bd29acc3 | |||
| aa8e8cf69c | |||
| 669e14b3bf | |||
| 1ee65cf70a | |||
| 129364be5e | |||
| 3be4727ca6 | |||
| a53eb927d1 | |||
| c3dd40405d | |||
| 5012f33c85 | |||
| eb37a82bc0 | |||
| b1da434458 | |||
| e1d99ef0ce | |||
| 4f2c133d01 | |||
| 0f90fb7e8d | |||
| 52e5bc114e | |||
| 4cd9a4f4d4 | |||
| 72ad6383ef | |||
| 2cf05388f1 | |||
| 4577dffb36 | |||
| 093571cfe4 | |||
| a1b9ca13e3 | |||
| 699f526429 | |||
| 16a5254e97 | |||
| e5613ebfdf | |||
| dfde1e032d | |||
| 10da6b9efc | |||
| 5a5a98ee80 | |||
| 4e2270517a | |||
| 0312808b92 | |||
| 9d45fa6b34 | |||
| e4f11c8df6 | |||
| bbc3aa4301 | |||
| 611d4eb6b8 | |||
| c50f5364c6 | |||
| beb38b44aa | |||
| a49698ee3d | |||
| 919f54a23e | |||
| 1d0e371e64 | |||
| 43565cd33b | |||
| ab9d9ac50f | |||
| 157c0d68d0 | |||
| 38d7ea4198 | |||
| 7c2b2f1559 | |||
| cc1e654690 | |||
| 7af97e3729 | |||
| 138bb0261c | |||
| dbe7dfa4bc | |||
| 1ceab2ed4c | |||
| 743accca7a | |||
| 128692da0c | |||
| 13b4f6e98c | |||
| ac53fc1e6f | |||
| 8d63a9f4cb | |||
| a150822d9b | |||
| a9fd4b1b05 | |||
| face5e2d31 | |||
| efd0e70f7d | |||
| 3f4c6e4571 | |||
| abc39685b1 | |||
| f93913b3d3 | |||
| 765c5fd143 | |||
| 6494f8fe18 | |||
| 1623034973 | |||
| 74004841b4 | |||
| be08517de2 | |||
| 24ed21998b | |||
| 3592031d5e | |||
| b8d9db7440 | |||
| 2b620338df | |||
| a5b6408587 | |||
| 78de687902 | |||
| 2741cd9a8b | |||
| 86f5500914 | |||
| 7804f9938a | |||
| ab03aa5bc5 | |||
| 2e3f27f06e | |||
| bda32419bd | |||
| c242c484a8 | |||
| 5d37cb01d5 | |||
| 7313fd8684 | |||
| 6f340ade69 | |||
| e260437846 | |||
| f54ad0f12f | |||
| 43f21e8fec | |||
| 3e1f1a89f8 | |||
| 82253c6f35 | |||
| f4d937d8a2 | |||
| 75fb613e25 | |||
| ce614044a7 | |||
| 91549d29fb | |||
| b6a5a9166a | |||
| 0063954286 | |||
| 6f6b64d484 | |||
| 52e496d017 | |||
| d557c09c3b | |||
| 7046f7f0e7 | |||
| 16286249ed | |||
| 19e2800472 | |||
| a5f7f5a921 | |||
| a402c5e502 | |||
| cdde7d62b2 | |||
| 3f6b9fe89f | |||
| b4acd83c78 | |||
| f7233c4418 | |||
| 5573ae06fc | |||
| 89a4a96630 | |||
| 34d1ac0376 | |||
| 0e9e175e79 | |||
| f11ad0ce28 | |||
| 9ad96fb81b | |||
| b671265faa | |||
| 832ff941d1 | |||
| 9869997733 | |||
| 745a24bbc8 | |||
| 26340882c0 | |||
| 8e2b8f2ace | |||
| 34e75979b2 | |||
| d5eca6cc78 | |||
| 77c77829e3 | |||
| f01ef3c6aa | |||
| f5a6c8fa51 | |||
| 865575e410 | |||
| 992c93fe5b | |||
| 0343960bd6 | |||
| 1d48707d0c | |||
| 443baa41f0 | |||
| d366b8922a | |||
| 692265980b | |||
| 28fe5fdb43 | |||
| 26c9d1c445 | |||
| f157f5489f | |||
| d1ec8dfd5a | |||
| 08d094db39 | |||
| 5fa6361ff2 | |||
| 1f93b6d39c | |||
| 0477e0ba4e | |||
| ae7176cc86 | |||
| eef9a68809 | |||
| f06875128b | |||
| 9d226b3535 | |||
| 7a1f903216 | |||
| b88673f366 | |||
| 67adf5abb5 | |||
| adbd2adf89 | |||
| 96303873fe | |||
| b925a1af3d | |||
| 05d43d3542 | |||
| c0640cb95e | |||
| 2ff17c0fcb | |||
| ecf57d131a | |||
| 32ad6e0832 | |||
| 13c4a62a71 | |||
| bba1953623 | |||
| 0537623a75 | |||
| 2a0096fe4a | |||
| 4b119dd471 | |||
| 6224cbf526 | |||
| e89a8f56c1 | |||
| 4cd1a51f48 | |||
| 88f079bd8c | |||
| dbdeeec2e9 | |||
| 1dbb13cad2 | |||
| 0b91ff8a34 | |||
| 1e81e65e7e | |||
| 9227685483 | |||
| 582e6097be | |||
| 22190e703c | |||
| f7b08ab8c4 | |||
| ae0b1edeac | |||
| cf1b0d67cc | |||
| b702ce8c7d | |||
| 60105172b2 | |||
| e2b8bf5a05 | |||
| 9e065aa0d5 | |||
| 2fd7b6f08d | |||
| 59795872f6 | |||
| 37b7b0bd3e | |||
| 7e24f0e35a | |||
| 152efb5774 | |||
| e726f780d1 | |||
| b201c283c7 | |||
| a2ae3ffbe0 | |||
| 96661111f9 | |||
| cc9d76cd86 | |||
| c61ffb54a8 | |||
| ab889a8fff | |||
| fcc3753a4c | |||
| 2c9d0a2847 | |||
| 99fd05afeb | |||
| a8051760f7 | |||
| c795a3f581 | |||
| 2cab9dc142 | |||
| 6d8d53aac8 | |||
| 4e6939b5cf | |||
| f6ff2ff139 | |||
| 048c1ae8a8 | |||
| b00a1e87f4 | |||
| 8cbde001ef | |||
| ad8b2f91bb | |||
| 9919825ea3 | |||
| 97e0ff9050 | |||
| c48c1a09d3 | |||
| ee3d41210d | |||
| be685d064f | |||
| 32e613aa8c | |||
| 58a9d33f02 | |||
| a8e6154c0d | |||
| d61dff69a6 | |||
| 8c853fff3e | |||
| daec1091f4 | |||
| 0520910bfd | |||
| 46f24f2394 | |||
| 1236863b17 | |||
| 4f4b1fc2d2 | |||
| d5a79a77e6 | |||
| 5d47946e4d | |||
| 69cfd89ae8 | |||
| 9e6e0b8df1 | |||
| b7b25f9e2b | |||
| a633a67e63 | |||
| 5ca7c2fb95 | |||
| 592f6a6fee | |||
| 4612c5e6a9 | |||
| c0da9fe861 | |||
| 19db763899 | |||
| 9a737bd63c | |||
| 8b65d41b31 | |||
| 4dc30c092d | |||
| e67ae66f3e | |||
| 5e0e7a4c6a | |||
| 807e367bdb | |||
| 400e06c202 | |||
| 1fa0371c75 | |||
| fc3e3b6c8d | |||
| d1a6262861 | |||
| 612d9d08dd | |||
| d2eb8cf639 | |||
| 1f6a2d4daf | |||
| 6c6ffe4802 | |||
| 46fca160e1 | |||
| 16727f331c | |||
| 01220d3eb5 | |||
| 89c1f02850 | |||
| 45edd5baf5 | |||
| 7dd5baac5c | |||
| 2e5924ed5e | |||
| a199a2cc25 | |||
| fb374b543b | |||
| 0dffb206b1 | |||
| 30b01e8d79 | |||
| 1ed394f8d6 | |||
| d5a046c3d3 | |||
| 4353f34bd1 | |||
| 71f7a8e697 | |||
| 6eaf723041 | |||
| bb155d446a | |||
| 98c4f335e8 | |||
| 4bdeaacde6 | |||
| e5dffc7052 | |||
| 43fa25458d | |||
| 9b8fa3d357 | |||
| 755ad9af07 | |||
| 46aa3d5d47 | |||
| 125b8d5387 | |||
| b5edd98bc9 | |||
| 647a52d0fb | |||
| 8c977131d9 | |||
| 30a1fe74ca | |||
| fa2c1c31f6 | |||
| 7e472bc4da | |||
| 145c6ad60d | |||
| e1bf784014 | |||
| 1bbc5ab0f7 | |||
| c1e9d2f057 | |||
| 68abb15d7f | |||
| 945e6261cc | |||
| d6b1608f9d | |||
| 2d54cb31bc | |||
| 3acb81964a | |||
| 4e7bf2b63a | |||
| 3093ce1d87 | |||
| ca9dd02201 | |||
| 1d9bdb1ca3 | |||
| d78494e016 | |||
| d18cc0df4e | |||
| 04dc68d988 | |||
| efb45d7411 | |||
| 426f8ddd18 | |||
| 27d2fe6ec0 | |||
| 6fb72c7401 | |||
| 14ccc71291 | |||
| ec56df3ffd | |||
| 8565f3f1b3 | |||
| 12c823ffdd | |||
| 5d92ad1e23 | |||
| e8a6a5d3ff | |||
| 605d32ceb0 | |||
| b79c03d227 |
+60
-2
@@ -1,5 +1,6 @@
|
||||
# cargo-nextest settings for CI (`cargo xtask test` does not use nextest).
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml).
|
||||
# CI builds the tests once and runs them in one job on a 32 vCPU runner (.github/workflows/ci.yml),
|
||||
# except the GPU path tracer's tests (the gpu-path group below), which run in four shards of their own.
|
||||
|
||||
[profile.ci]
|
||||
# Heavy tests (one to twelve minutes each on a CI runner) that pull requests skip. They run on
|
||||
@@ -42,7 +43,6 @@ filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = 12
|
||||
priority = 100
|
||||
|
||||
|
||||
# The slowest tests start first, so a shard does not end with one of them running alone on a
|
||||
# core while the others sit idle (seed_81 alone takes about 20 minutes under CI load). Medians
|
||||
# from the CI test timings (the junit artifacts), slowest first; a test over ~100 s belongs here.
|
||||
@@ -69,3 +69,61 @@ filter = """
|
||||
))
|
||||
"""
|
||||
priority = 100
|
||||
|
||||
# Each nextest test has its own process/device. Concurrent cold lavapipe path
|
||||
# compiles contend for CPU and can exhaust the 600 s pipeline setup budget.
|
||||
# Serialize this module per CI shard; other tests retain their normal parallelism.
|
||||
# CI runs these in their own job (GPU path tests in ci.yml), split into count partitions.
|
||||
[test-groups.gpu-path]
|
||||
max-threads = 1
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = "package(=solidcraft-gpu) & test(/^path::/)"
|
||||
test-group = "gpu-path"
|
||||
|
||||
# Full local validation opts into its own scheduling policy. Keep GitHub CI's
|
||||
# worker reservation and slow-test priorities above independent of this runner.
|
||||
[profile.local-ci]
|
||||
inherits = "ci"
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = "binary(action_matrix) & test(=action_matrix_sample)"
|
||||
threads-required = "num-cpus"
|
||||
priority = 100
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = "(package(solidcraft-engine) & test(=tests::robustness_goldens::seed_81)) | (package(solidcraft-kernel) & test(=tspline::tests::hostile_tsplines_never_panic))"
|
||||
priority = 99
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
filter = """
|
||||
(package(solidcraft-engine) & (
|
||||
test(/^tests::robustness_goldens::seed_(81|45|20|24|95|02|57|40|18|48|32|46|85|61)$/)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(=tests::hostile_params_never_panic)
|
||||
| test(=cmd::clearance::tests::distances_are_exact_on_edges_and_curved_faces)
|
||||
| test(=cmd::clipboard::tests::a_pasted_coil_joins_the_pasted_body)
|
||||
| test(=tests::pleat_join::the_pleats_join_at_every_wall_from_2_4_to_2_6_mm)
|
||||
| test(=tests::flush_join_loft::a_flange_flush_with_the_end_of_a_bent_tapered_tube_joins)
|
||||
| test(=cmd::sheet::tests::perforated_sheet_takes_a_thousand_round_holes)
|
||||
| test(=boolean_regression_tests::joined_loft_frames_plan_their_display_at_every_quality)
|
||||
))
|
||||
| (package(solidcraft-engine) & (binary(atomic_commands) | binary(step_export_regressions)))
|
||||
| (package(solidcraft-kernel) & (
|
||||
test(=curveblend::tests::rolling_ball_turns_about_a_rounded_corner_of_its_radius)
|
||||
| test(=tests::fillet_tabbed_plate_outer_loop_and_partial_selection_after_hole)
|
||||
| test(=tspline::tests::hostile_tsplines_never_panic)
|
||||
| test(=fill::tests::regular_rims_of_three_to_eight_sides_fill_with_one_face_and_close_the_cup)
|
||||
| test(/^issue_788_tests::equal_bores_crossing_/)
|
||||
))
|
||||
"""
|
||||
priority = 98
|
||||
|
||||
# Native Windows shader/device creation must not overlap within the local run.
|
||||
[test-groups.windows-gpu]
|
||||
max-threads = 1
|
||||
|
||||
[[profile.local-ci.overrides]]
|
||||
platform = "cfg(windows)"
|
||||
filter = "package(solidcraft-gpu) | package(solidcraft) | (package(solidcraft-ui-egui) & test(/(offscreen|gpu)/))"
|
||||
test-group = "windows-gpu"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Preserve canonical source receipts and the runtime icon-discovery snapshot.
|
||||
.gitattributes text eol=lf
|
||||
/assets/icons/ui/design/requested_icons.txt text eol=lf
|
||||
@@ -6,6 +6,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report this! Inside the app, **Report an Issue** fills most of this in for you, with the command, its settings and your environment.
|
||||
Before reporting a boolean or modelling failure, please try to reproduce it on the latest nightly or a build from current `main`, and say which build you used.
|
||||
- type: textarea
|
||||
id: what
|
||||
attributes:
|
||||
@@ -19,6 +20,7 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
What you clicked or typed, in order. For a problem on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
|
||||
Using an AI agent with SolidCraft's MCP server? The `solidcraft-reporting` skill in [`skills/`](https://github.com/tangency-labs/solidcraft/tree/main/skills) has it reproduce the problem in a fresh design and write these steps for you.
|
||||
placeholder: |
|
||||
1. Sketch a circle on the XY plane
|
||||
2. Extrude it 10 mm
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
Describe the problem and resulting behavior. Link the issue when applicable.
|
||||
|
||||
Changes a default: no
|
||||
|
||||
<!-- Required line, replace "no" if this changes what a user gets out of the box (a default value, key binding, setting, tool behaviour):
|
||||
Changes a default: yes — <what>, old behaviour kept behind preference <name>
|
||||
Docs-only and bot PRs are exempt. -->
|
||||
|
||||
## Validation
|
||||
|
||||
What did you run, and what were the results? Include expected and measured values for geometry changes.
|
||||
|
||||
+159
-23
@@ -9,12 +9,14 @@
|
||||
# Documentation, packaging, the other workflows and the GitHub bots' scripts select nothing:
|
||||
# no native job runs for them, on pull requests or on main.
|
||||
# Tests are built and run in one job on a big runner, slowest first, starting beside the repository
|
||||
# checks (no archive to upload and download). A few heavy tests (see .config/nextest.toml) are
|
||||
# skipped on pull requests and run on pushes to main and nightly. The Windows jobs (D3D12 shaders
|
||||
# and app startup), the reference tests and the doc tests run on main and nightly only, not on
|
||||
# pull requests (the fast lane, in Select checks); a break there is fixed forward on main. Only
|
||||
# main saves the build caches; pull requests restore them. sccache is on a Blacksmith sticky disk that
|
||||
# only main commits.
|
||||
# checks (no archive to upload and download). The GPU path tracer's tests run one at a time
|
||||
# (.config/nextest.toml), so they have a job of their own in four shards (GPU path tests). A few
|
||||
# heavy tests (see .config/nextest.toml) are skipped on pull requests and run on pushes to main and
|
||||
# nightly. The reference tests and the doc tests run in the merge queue, on main and nightly, not
|
||||
# on pull requests (the fast lane, in Select checks); a break there is fixed forward on main. The
|
||||
# Windows jobs (D3D12 shaders and app startup) run nightly, and in the merge queue only for a group
|
||||
# that changes what they exercise. Only main saves the build caches; pull requests restore them.
|
||||
# sccache is on a Blacksmith sticky disk that only main commits.
|
||||
# A push to main runs only the repository checks when a passing merge-queue run tested that very
|
||||
# commit, or when nothing a build reads changed since the last push to main whose run passed.
|
||||
# Otherwise it runs everything.
|
||||
@@ -30,7 +32,7 @@
|
||||
# reads `ci` from the newest run of a commit, so a run without it would block a PR that passed.
|
||||
# Cross-compilation is opt-in through Run workflow; release builds remain in release.yml.
|
||||
# Every Linux job runs on Blacksmith's Ubuntu 24.04 runners (blacksmith-<N>vcpu-ubuntu-2404), sized
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# per job: 32 vCPU for the tests, 16 for Clippy, the GPU path tests, the archive build (nightly) and the merge queue's, 8 for compiles, doc and reference tests, 4 for the light checks, 2 for Select checks and ci.
|
||||
# The Windows shader job runs on Blacksmith's Windows Server 2025 runners (blacksmith-8vcpu-windows-2025);
|
||||
# the Windows app startup smoke stays on windows-latest, which has a desktop session that presents frames.
|
||||
# Pull requests, forks included, the merge queue, pushes to main and the nightly run all use them:
|
||||
@@ -103,6 +105,9 @@ jobs:
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
goldens: ${{ steps.paths.outputs.goldens }}
|
||||
windows: ${{ steps.paths.outputs.windows }}
|
||||
# The GPU path tests run: native, and the whole workspace or solidcraft-gpu selected. The
|
||||
# job's `if` and the `ci` job's check both read this, so they cannot disagree.
|
||||
gpu: ${{ steps.paths.outputs.gpu }}
|
||||
full: ${{ steps.paths.outputs.full }}
|
||||
# Code changed on a pull request that has no approved-for-ci label yet: only Quick compile.
|
||||
quick: ${{ steps.paths.outputs.quick }}
|
||||
@@ -122,6 +127,8 @@ jobs:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PUSH_SHA: ${{ github.sha }}
|
||||
GROUP_BASE_SHA: ${{ github.event.merge_group.base_sha }}
|
||||
GROUP_HEAD_SHA: ${{ github.event.merge_group.head_sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
@@ -144,6 +151,23 @@ jobs:
|
||||
full=$(sed -n 's/^full=//p' "$RUNNER_TEMP/ci-affected")
|
||||
packages=$(sed -n 's/^packages=//p' "$RUNNER_TEMP/ci-affected")
|
||||
}
|
||||
# Whether the NUL-separated paths in $1 (the diff $2) reach what the Windows D3D12 jobs
|
||||
# exercise: the GPU crate and its shaders, the app and its startup, the viewport's wgpu
|
||||
# glue, the vendored egui-wgpu, their scripts, or a lockfile change that moves the
|
||||
# graphics stack (wgpu, naga, egui, winit, windows). Pull requests and merge groups share it.
|
||||
windows_paths() {
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in
|
||||
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
|
||||
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
|
||||
esac
|
||||
done < "$1"
|
||||
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$1" \
|
||||
&& git diff "$2" -- Cargo.lock \
|
||||
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
|
||||
windows=true
|
||||
fi
|
||||
}
|
||||
if [ "$EVENT_NAME" = push ]; then
|
||||
# The queue lands the very commit its merge_group run tested. If that run passed,
|
||||
# testing the push again proves nothing new; otherwise (a merge that skipped the
|
||||
@@ -164,13 +188,25 @@ jobs:
|
||||
# push before may never have run: a newer push cancels a waiting one).
|
||||
echo "$tested..$PUSH_SHA changes no code: repository checks only."
|
||||
else
|
||||
# Code changed (or the push can't be compared): main runs everything.
|
||||
# Code changed (or the push can't be compared): main runs everything but Windows.
|
||||
# The merge queue ran Windows for the changes that reach it, and the nightly runs it
|
||||
# for everything else (it passed 129 of 129 non-PR runs before this).
|
||||
full=true
|
||||
packages=
|
||||
native=true
|
||||
fi
|
||||
elif [ "$EVENT_NAME" = merge_group ]; then
|
||||
native=true
|
||||
# Windows (about 16 runner-minutes) only for a group that changes what it exercises;
|
||||
# the nightly runs it whatever changed. A group that can't be compared runs it.
|
||||
if git diff --no-renames --name-only -z "$GROUP_BASE_SHA...$GROUP_HEAD_SHA" > "$RUNNER_TEMP/ci-paths"; then
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$GROUP_BASE_SHA...$GROUP_HEAD_SHA"
|
||||
else
|
||||
windows=true
|
||||
fi
|
||||
echo "windows=$windows for $GROUP_BASE_SHA...$GROUP_HEAD_SHA"
|
||||
elif [ "$EVENT_NAME" != pull_request ]; then
|
||||
# The nightly run and Run workflow: everything, Windows included.
|
||||
native=true
|
||||
windows=true
|
||||
else
|
||||
@@ -185,19 +221,9 @@ jobs:
|
||||
case "$path" in
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
# What the Windows D3D12 jobs exercise: the GPU crate and its shaders, the app and
|
||||
# its startup, the viewport's wgpu glue, the vendored egui-wgpu, their scripts.
|
||||
case "$path" in
|
||||
crates/gpu/*|apps/solidcraft/*|crates/ui-egui/src/gpu.rs|vendor/egui-wgpu/*) windows=true ;;
|
||||
tools/windows-*.ps1|.github/workflows/ci.yml|rust-toolchain*|.cargo/*) windows=true ;;
|
||||
esac
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
# A lockfile change that moves the graphics stack (wgpu, naga, egui, winit, windows).
|
||||
if [ "$windows" = false ] && grep -qxz 'Cargo.lock' "$RUNNER_TEMP/ci-paths" \
|
||||
&& git diff "$BASE_SHA...$HEAD_SHA" -- Cargo.lock \
|
||||
| grep -Eq '^[ +-]name = "(wgpu[a-z_-]*|naga|egui[a-z_-]*|eframe|winit|windows[a-z_-]*|d3d12)"'; then
|
||||
windows=true
|
||||
fi
|
||||
# Computed as for a merge group, then turned off below (the fast lane).
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
|
||||
# Pull requests build and test the touched packages and their dependents; nothing
|
||||
# selected means no code changed.
|
||||
select_packages "$RUNNER_TEMP/ci-paths"
|
||||
@@ -242,8 +268,16 @@ jobs:
|
||||
goldens=false
|
||||
fi
|
||||
fi
|
||||
# The GPU path tests (their own job) run with the other tests, when solidcraft-gpu is
|
||||
# among the packages they test.
|
||||
gpu=false
|
||||
if [ "$native" = true ]; then
|
||||
case " $packages " in *" solidcraft-gpu "*) gpu=true ;; esac
|
||||
if [ "$full" = true ]; then gpu=true; fi
|
||||
fi
|
||||
{
|
||||
echo "native=$native"
|
||||
echo "gpu=$gpu"
|
||||
echo "kernel=$kernel"
|
||||
echo "goldens=$goldens"
|
||||
echo "windows=$windows"
|
||||
@@ -286,6 +320,10 @@ jobs:
|
||||
cargo test -p xtask ui_containers --locked
|
||||
- name: Sorted lists
|
||||
run: cargo xtask lists
|
||||
- name: Agent router and skills cite real files
|
||||
run: |
|
||||
cargo xtask skills
|
||||
cargo test -p xtask skills --locked
|
||||
- name: Design format pending changes
|
||||
run: bash scripts/test-format-pending.sh
|
||||
- name: Web app offline use and updates (headless Chrome)
|
||||
@@ -295,7 +333,7 @@ jobs:
|
||||
- name: Merge-of-main detection self-test
|
||||
run: python3 -I scripts/ci/test_main_merge.py
|
||||
- name: Claim, triage and kernel-paused bot tests
|
||||
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js
|
||||
run: node --test scripts/claim/*.test.js scripts/triage/*.test.js scripts/kernel-paused/*.test.js scripts/pr-checks/*.test.js
|
||||
|
||||
# Fast feedback for a pull request that has not been approved for the full CI yet.
|
||||
quick-compile:
|
||||
@@ -487,8 +525,10 @@ jobs:
|
||||
scope=(--workspace)
|
||||
if [ "$FULL" != true ]; then scope=(); for p in $PACKAGES; do scope+=(-p "$p"); done; fi
|
||||
status=0
|
||||
# The GPU path tracer's tests run in their own job (GPU path tests): one at a time, they
|
||||
# took about 36 of this job's 45 minutes.
|
||||
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E "$base" || status=1
|
||||
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=1
|
||||
if [ "$status" != 0 ]; then
|
||||
# A failed golden gets the hint to re-record it (junit.xml lists the failures).
|
||||
if [ -f "$junit" ] && tr '\n' ' ' < "$junit" | grep -q '<testcase name="tests::robustness_goldens::[^"]*"[^>]*>[[:space:]]*<failure'; then
|
||||
@@ -508,6 +548,96 @@ jobs:
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
gpu-path-tests:
|
||||
name: GPU path tests (${{ matrix.shard }}/4)
|
||||
# The GPU path tracer's tests (about 108) compile their pipelines on lavapipe, one test at a
|
||||
# time (the gpu-path test group in .config/nextest.toml keeps them within the 600 s pipeline
|
||||
# setup budget). Serial, they took about 36 minutes, the longest chain in Native Linux tests;
|
||||
# here four shards run beside it. Their filter is the one Native Linux tests leaves out, so
|
||||
# every test runs exactly once.
|
||||
# 16 vCPU, not 8: lavapipe compiles on every core, and on 8 vCPU one test went past the 600 s
|
||||
# budget (665 s, against 204 s for the same test in another run) and a shard took 35 minutes.
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && needs.changes.result == 'success' && needs.changes.outputs.gpu == 'true' }}
|
||||
runs-on: blacksmith-16vcpu-ubuntu-2404
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
env:
|
||||
# As in Native Linux tests: lavapipe draws, and a test without an adapter fails.
|
||||
SOLIDCRAFT_GPU: software
|
||||
SOLIDCRAFT_REQUIRE_GPU: "1"
|
||||
# lavapipe's own shader cache (below).
|
||||
MESA_SHADER_CACHE_DIR: /home/runner/.cache/mesa-gpu
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 20G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install a software Vulkan driver (lavapipe)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends mesa-vulkan-drivers libvulkan1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# sccache lives on a Blacksmith sticky disk (an ext4 volume cloned from the last snapshot,
|
||||
# no 10 GB limit and no download). Only main commits it; pull requests, the merge queue and
|
||||
# forks mount a clone and never write back, so nothing they build can reach main's cache.
|
||||
# The shards build the same thing, so only the first one commits it (and saves the cache).
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-gpu
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
|
||||
# The first test in a shard to build the path pipeline compiles it cold on lavapipe (335 to
|
||||
# 665 s, against the 600 s setup budget); every later one reads Mesa's disk cache (about
|
||||
# 30 s). Keeping that cache from main's last run lets the first test read it too, until the
|
||||
# shaders change. Same sticky disk rules as the compiler cache.
|
||||
- name: Mount lavapipe's shader cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-mesa-gpu
|
||||
path: /home/runner/.cache/mesa-gpu
|
||||
commit: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 && 'on-change' || 'false' }}
|
||||
# Main saves the cache; every other run restores it, read-only.
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-gpu
|
||||
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: Run one shard of the GPU path tests
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
# None of these is in the default filter (the heavy tests), so pull requests, the merge
|
||||
# queue, main and nightly all run every one of them, as before. `count` deals the tests
|
||||
# out in turn (27 each); `hash` split them 41/40/27 across three shards.
|
||||
cargo nextest run -p solidcraft-gpu --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
- name: Upload test timings
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: junit-gpu-${{ matrix.shard }}
|
||||
path: ${{ env.CARGO_TARGET_DIR || 'target' }}/nextest/ci/junit.xml
|
||||
retention-days: 7
|
||||
|
||||
action-matrix:
|
||||
# Every ordinary action on every face, edge, vertex and body of the design corpus, against
|
||||
# tests/action_matrix/baseline.json (docs/action-matrix.md). Every test run checks a sample;
|
||||
@@ -769,7 +899,7 @@ jobs:
|
||||
# commit's real run instead: the merge queue reads `ci` from the newest run of a commit, so a
|
||||
# run without a passing `ci` would block a pull request that passed (#1850, #3182, #3252).
|
||||
name: ci
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
needs: [changes, checks, quick-compile, clippy, build-tests, tests, gpu-path-tests, doc-tests, oracle, windows-fxc, windows-startup]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
permissions:
|
||||
@@ -817,6 +947,8 @@ jobs:
|
||||
BUILD: ${{ needs.build-tests.result }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
TESTS: ${{ needs.tests.result }}
|
||||
GPU_RUN: ${{ needs.changes.outputs.gpu }}
|
||||
GPU: ${{ needs.gpu-path-tests.result }}
|
||||
DOCS: ${{ needs.doc-tests.result }}
|
||||
ORACLE: ${{ needs.oracle.result }}
|
||||
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
|
||||
@@ -836,6 +968,10 @@ jobs:
|
||||
fi
|
||||
test "$TESTS" = success
|
||||
fi
|
||||
# The same output decides whether the GPU path tests run (their job's `if`).
|
||||
if [ "$GPU_RUN" = true ]; then
|
||||
test "$GPU" = success
|
||||
fi
|
||||
if [ "$DOCTESTS" = true ]; then
|
||||
test "$DOCS" = success
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Nightly import checks on real-world STEP files (#1397): the external corpus in
|
||||
# tests/external-corpus.toml is downloaded (hash-verified), imported headless, and checked
|
||||
# against tolerance-based expectations — body counts, total volume and area, time and memory
|
||||
# budgets. It is not part of `cargo xtask ci`: it downloads from the internet.
|
||||
#
|
||||
# Import results should improve over time (healing, edge merging, meshing). When a check fails
|
||||
# because the numbers moved for the better (or the source changed upstream), re-record with
|
||||
# `cargo xtask external-corpus --bless "why"` in a pull request, never by editing numbers.
|
||||
name: External corpus
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '40 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
only:
|
||||
description: "Run only this corpus entry name (empty = every entry)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
corpus:
|
||||
name: Import the external STEP corpus
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Build the CLI
|
||||
run: cargo build --profile ci -p solidcraft-cli
|
||||
- name: Import and check every entry
|
||||
env:
|
||||
ONLY: ${{ inputs.only }}
|
||||
run: |
|
||||
if [ -z "$ONLY" ]; then
|
||||
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli
|
||||
else
|
||||
cargo run -q -p xtask -- external-corpus --cli target/ci/solidcraft-cli --only "$ONLY"
|
||||
fi
|
||||
@@ -1,6 +1,8 @@
|
||||
# Files generated from the code are rewritten here, on main, after each merge, so pull requests
|
||||
# never edit them (two pull requests that each regenerated one would always conflict):
|
||||
# THIRD-PARTY-LICENSES.txt (`cargo xtask licences`) and docs/features.md (`cargo xtask features`).
|
||||
# Pending files (assets/locales/pending, attribution/pending) are folded into the catalogs and
|
||||
# ATTRIBUTION.md by `cargo xtask fold-pending`.
|
||||
name: Generated files
|
||||
|
||||
on:
|
||||
@@ -17,6 +19,8 @@ on:
|
||||
- "crates/engine/**"
|
||||
- "apps/solidcraft-cli/**"
|
||||
- "xtask/**"
|
||||
- "assets/locales/pending/*.json"
|
||||
- "attribution/pending/*.md"
|
||||
- ".github/workflows/generated.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -48,13 +52,15 @@ jobs:
|
||||
run: cargo xtask licences
|
||||
- name: docs/features.md
|
||||
run: cargo xtask features
|
||||
- name: Pending locale strings and attribution rows
|
||||
run: cargo xtask fold-pending
|
||||
- name: Commit if changed
|
||||
run: |
|
||||
if git diff --quiet -- THIRD-PARTY-LICENSES.txt docs/features.md; then echo "unchanged"; exit 0; fi
|
||||
if [ -z "$(git status --porcelain)" ]; then echo "unchanged"; exit 0; fi
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add THIRD-PARTY-LICENSES.txt docs/features.md
|
||||
git commit -m "Regenerate THIRD-PARTY-LICENSES.txt and docs/features.md [skip ci]"
|
||||
git add -A THIRD-PARTY-LICENSES.txt docs/features.md assets/locales ATTRIBUTION.md attribution
|
||||
git commit -m "Regenerate generated files and fold pending files [skip ci]"
|
||||
for _ in 1 2 3; do
|
||||
git pull --rebase origin main && git push origin HEAD:main && exit 0
|
||||
sleep 10
|
||||
|
||||
@@ -16,7 +16,7 @@ on:
|
||||
permissions:
|
||||
contents: read # to check out scripts/kernel-paused
|
||||
issues: write
|
||||
pull-requests: read # to list a PR's changed paths
|
||||
pull-requests: write # to list a PR's changed paths and add its label
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
|
||||
@@ -39,6 +39,8 @@ jobs:
|
||||
run: python3 packaging/tests/nightly_test.py
|
||||
- name: Windows executable icon validation regression
|
||||
run: python3 packaging/tests/windows_icon_test.py
|
||||
- name: AppImage CLI wrapper regression
|
||||
run: python3 packaging/tests/apprun_test.py
|
||||
- name: Flatpak desktop smoke validation regression
|
||||
run: python3 packaging/tests/flatpak_test.py
|
||||
- name: PowerShell
|
||||
|
||||
@@ -65,6 +65,7 @@ jobs:
|
||||
run: |
|
||||
mkdir -p site
|
||||
cp -r target/book/. site/
|
||||
bash docs/book/seo.sh site # canonical links, robots.txt, sitemap.xml
|
||||
cp -r dist/web site/app
|
||||
- uses: actions/configure-pages@v5
|
||||
id: pages
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Two quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, and the "Changes a default:" line is filled in.
|
||||
# Neither is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
name: PR checks
|
||||
|
||||
on:
|
||||
# pull_request, never pull_request_target: nothing here checks out or runs the PR's code, and the
|
||||
# token is read-only for fork PRs. Only scripts/pr-checks from the PR's own merge commit is run.
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened, ready_for_review, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: pr-checks-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
duplicate:
|
||||
name: Duplicate PR check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
const r = await checks.checkDuplicate({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
defaults:
|
||||
name: Defaults line check
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
const r = await checks.checkDefaults({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
# Labelling needs write access, which a fork PR's token never has, so it is its own job and best
|
||||
# effort: a fork PR just doesn't get the `changes-default` label from here.
|
||||
label:
|
||||
name: Label changes-default
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
sparse-checkout: scripts/pr-checks
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const checks = require('./scripts/pr-checks/pr-checks.js');
|
||||
await checks.syncDefaultLabel({ github, context, core });
|
||||
@@ -4,11 +4,14 @@
|
||||
# comes from [workspace.package] in Cargo.toml. An agent verifies the artifacts and publishes
|
||||
# the draft separately; see docs/releasing.md. Tags and pushes to main do not trigger releases.
|
||||
# At midnight Eastern, changed main commits build and publish a nightly pre-release.
|
||||
# A version with a suffix (0.2.0-rc.1) is a pre-release. Nothing is signed yet.
|
||||
# A version with a suffix (0.2.0-rc.1) is a pre-release.
|
||||
# The macOS app is signed and notarized when the APPLE_* secrets exist. The Windows binaries and
|
||||
# MSI are Authenticode-signed through SignPath Foundation when the SIGNPATH_API_TOKEN secret exists
|
||||
# (see the windows job). Nothing else is signed yet.
|
||||
#
|
||||
# Linux x86_64, aarch64 AppImage (+ .zsync), .deb, .rpm, .flatpak, .snap, .tar.gz packaging/linux
|
||||
# FreeBSD x86_64 .tar.gz (built in a FreeBSD VM) packaging/freebsd
|
||||
# macOS universal .dmg, and the CLI as a .zip packaging/macos
|
||||
# macOS universal .dmg, and the CLI as a .zip (both archs built in parallel) packaging/macos
|
||||
# Windows x64, x86, arm64 .msi and a portable .zip; x64 also the plain .exe packaging/windows
|
||||
# Web a .zip of the static site packaging/web
|
||||
#
|
||||
@@ -17,6 +20,8 @@
|
||||
# releases/latest/download/<name> links in the README keep working across releases.
|
||||
#
|
||||
# A dry run: dispatch with draft=false (the default). Only the release branch may create drafts.
|
||||
# A dry run from any branch also test-signs the Windows files when SIGNPATH_API_TOKEN exists:
|
||||
# gh workflow run release.yml --ref <branch> -f draft=false [-f signpath_signing_policy=test-signing]
|
||||
name: Release
|
||||
|
||||
on:
|
||||
@@ -31,6 +36,10 @@ on:
|
||||
description: "Create/update a draft (release branch only; off: build artifacts only)"
|
||||
type: boolean
|
||||
default: false
|
||||
signpath_signing_policy:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -295,11 +304,19 @@ jobs:
|
||||
path: dist/release/*
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
name: macOS universal
|
||||
# The two halves of the universal binary build in parallel on their own runners (each alone
|
||||
# took about half of the old single job's 45 minutes); `macos` below joins and packages them.
|
||||
macos-build:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { arch: aarch64, target: aarch64-apple-darwin }
|
||||
- { arch: x86_64, target: x86_64-apple-darwin }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -307,12 +324,58 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-darwin, x86_64-apple-darwin
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: release-macos-universal
|
||||
- name: Build and package (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: packaging/macos/package.sh --arch universal
|
||||
key: release-macos-${{ matrix.arch }}
|
||||
- name: Build
|
||||
run: packaging/macos/package.sh --build-only ${{ matrix.target }}
|
||||
# Not named release-*: the release job collects every release-* artifact as an asset.
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: macos-bin-${{ matrix.arch }}
|
||||
path: |
|
||||
target/${{ matrix.target }}/release/solidcraft
|
||||
target/${{ matrix.target }}/release/solidcraft-cli
|
||||
target/${{ matrix.target }}/release/solvecraft-cli
|
||||
if-no-files-found: error
|
||||
|
||||
macos:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-aarch64
|
||||
path: target/aarch64-apple-darwin/release
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: macos-bin-x86_64
|
||||
path: target/x86_64-apple-darwin/release
|
||||
- name: Set up Developer ID signing and notarization
|
||||
# Optional until the APPLE_* secrets exist (docs/releasing.md, "macOS signing"). Without
|
||||
# them (forks, dry runs) the app keeps its ad-hoc signature and is not notarized.
|
||||
env:
|
||||
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
|
||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||
run: packaging/macos/signing.sh --setup
|
||||
- name: Join, sign, package and check (dmg, CLI zip; the dmg is mounted and checked)
|
||||
run: |
|
||||
# Artifacts don't keep the executable bit.
|
||||
chmod +x target/*-apple-darwin/release/solidcraft target/*-apple-darwin/release/solidcraft-cli target/*-apple-darwin/release/solvecraft-cli
|
||||
packaging/macos/package.sh --arch universal --skip-build ${MACOS_SIGN_IDENTITY:+--sign}
|
||||
- name: Remove the signing keychain and API key
|
||||
if: always()
|
||||
run: packaging/macos/signing.sh --cleanup
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-macos
|
||||
@@ -323,7 +386,14 @@ jobs:
|
||||
name: Windows ${{ matrix.arch }}
|
||||
needs: [version, icons]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# Must stay a GitHub-hosted runner, like every job before it: SignPath's open-source policy
|
||||
# only signs builds whose jobs all ran on GitHub-hosted runners.
|
||||
runs-on: windows-latest
|
||||
# actions: read lets the SignPath action read this job's details and fetch the uploaded
|
||||
# artifact with GITHUB_TOKEN (the workflow default above grants contents: read only).
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -334,6 +404,16 @@ jobs:
|
||||
- { arch: arm64, target: aarch64-pc-windows-msvc }
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
# Authenticode signing through SignPath Foundation (docs/releasing.md, "Windows signing").
|
||||
# Only when the API token secret exists: forks and repositories without it build unsigned,
|
||||
# exactly as before. `if:` can't read secrets, so the check goes through this variable.
|
||||
SIGN_WINDOWS: ${{ secrets.SIGNPATH_API_TOKEN != '' }}
|
||||
# The one switch between the self-signed test certificate (test-signing) and the real one
|
||||
# (release-signing): a dispatch input, else the SIGNPATH_SIGNING_POLICY repository variable,
|
||||
# else test-signing.
|
||||
SIGNPATH_SIGNING_POLICY: ${{ inputs.signpath_signing_policy || vars.SIGNPATH_SIGNING_POLICY || 'test-signing' }}
|
||||
SIGNPATH_ORGANIZATION_ID: fa603ff4-c702-404b-8588-a524371c7a4e
|
||||
SIGNPATH_PROJECT_SLUG: solidcraft
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
@@ -352,10 +432,109 @@ jobs:
|
||||
dotnet tool install --global wix --version 5.0.2
|
||||
wix extension add -g WixToolset.UI.wixext/5.0.2 WixToolset.Util.wixext/5.0.2
|
||||
- name: Build and package (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS != 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }}
|
||||
|
||||
# ---- signed path ---------------------------------------------------------------------------
|
||||
# SignPath's GitHub integration (docs.signpath.io/trusted-build-systems/github): the files go
|
||||
# up as a workflow artifact, SignPath fetches that artifact itself (so it can verify it came
|
||||
# from this workflow run), signs it, and the action downloads the signed copy. GitHub stores
|
||||
# artifacts as zips, so packaging/windows/signpath-artifact-configuration.xml (pasted into the
|
||||
# SignPath project) describes a zip holding the files at its top level.
|
||||
# Two rounds: the .exe files first, so the MSI, the portable zip and the plain .exe all carry
|
||||
# signed binaries; then the MSI built from them.
|
||||
- name: Build and stage the binaries for signing
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase stage
|
||||
- name: Upload the unsigned binaries
|
||||
id: unsigned-exes
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signpath-unsigned-exes-windows-${{ matrix.arch }}
|
||||
path: target/windows-package/${{ matrix.arch }}/*.exe
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: Sign the binaries (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
|
||||
github-artifact-id: ${{ steps.unsigned-exes.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
# release-signing requests wait for a person to approve them in SignPath; give that an
|
||||
# hour instead of the default 10 minutes. test-signing requests finish in seconds.
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: target/windows-package/signed-exes-${{ matrix.arch }}
|
||||
- name: Put the signed binaries in place
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$stage = "target/windows-package/${{ matrix.arch }}"
|
||||
foreach ($exe in 'solidcraft.exe', 'solidcraft-cli.exe', 'solvecraft-cli.exe') {
|
||||
$signed = "target/windows-package/signed-exes-${{ matrix.arch }}/$exe"
|
||||
# The artifact configuration matches each file optionally, so check that every file
|
||||
# actually came back signed rather than trusting the request's success.
|
||||
$sig = Get-AuthenticodeSignature $signed
|
||||
if (-not $sig.SignerCertificate) { throw "$exe came back from SignPath without a signature ($($sig.Status))" }
|
||||
# The test certificate is self-signed, so Status is not Valid until release-signing.
|
||||
Write-Output "$exe signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
|
||||
Copy-Item -Force $signed "$stage/$exe"
|
||||
}
|
||||
- name: Package the signed binaries (msi, portable zip)
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
env:
|
||||
SOLIDCRAFT_ICONS: ${{ github.workspace }}/icons
|
||||
run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} -Phase package
|
||||
- name: Upload the unsigned MSI
|
||||
id: unsigned-msi
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: signpath-unsigned-msi-windows-${{ matrix.arch }}
|
||||
path: dist/release/*.msi
|
||||
if-no-files-found: error
|
||||
retention-days: 3
|
||||
- name: Sign the MSI (SignPath, ${{ env.SIGNPATH_SIGNING_POLICY }})
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
uses: signpath/github-action-submit-signing-request@f6d04783b4569d051e0c80105fe66e82819d0092 # v3.0
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ env.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: ${{ env.SIGNPATH_PROJECT_SLUG }}
|
||||
signing-policy-slug: ${{ env.SIGNPATH_SIGNING_POLICY }}
|
||||
github-artifact-id: ${{ steps.unsigned-msi.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: target/windows-package/signed-msi-${{ matrix.arch }}
|
||||
- name: Put the signed MSI in place
|
||||
if: env.SIGN_WINDOWS == 'true'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$msi = Get-Item "dist/release/*.msi"
|
||||
$signed = "target/windows-package/signed-msi-${{ matrix.arch }}/$($msi.Name)"
|
||||
$sig = Get-AuthenticodeSignature $signed
|
||||
if (-not $sig.SignerCertificate) { throw "$($msi.Name) came back from SignPath without a signature ($($sig.Status))" }
|
||||
Write-Output "$($msi.Name) signed by $($sig.SignerCertificate.Subject) (status: $($sig.Status))"
|
||||
Copy-Item -Force $signed $msi.FullName
|
||||
"Windows ${{ matrix.arch }}: Authenticode-signed through SignPath (policy $env:SIGNPATH_SIGNING_POLICY, $($sig.SignerCertificate.Subject))." >> $env:GITHUB_STEP_SUMMARY
|
||||
- name: Note the unsigned build
|
||||
if: env.SIGN_WINDOWS != 'true'
|
||||
shell: pwsh
|
||||
run: '"Windows ${{ matrix.arch }}: unsigned (no SIGNPATH_API_TOKEN secret)." >> $env:GITHUB_STEP_SUMMARY'
|
||||
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: release-windows-${{ matrix.arch }}
|
||||
|
||||
@@ -11,3 +11,7 @@ __pycache__/
|
||||
|
||||
# cargo-mutants output (docs/mutation-testing.md)
|
||||
mutants.out*/
|
||||
|
||||
# external corpus downloads (cargo xtask external-corpus, #1397): fetched at test time,
|
||||
# hash-pinned in tests/external-corpus.toml; never commit case data
|
||||
/corpus/
|
||||
|
||||
@@ -1,216 +1,48 @@
|
||||
# SolidCraft — instructions for agents
|
||||
# SolidCraft: instructions for agents
|
||||
|
||||
SolidCraft is a clean-room, open-source, pure-Rust parametric 3D CAD application:
|
||||
sketches with a constraint solver, a parametric timeline of features, a B-rep
|
||||
kernel, and a desktop UI that agents can drive. Projects published by
|
||||
[storytold](https://github.com/storytold) can serve as references for implementation conventions.
|
||||
Any code adapted from other projects (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
|
||||
SolidCraft is a clean-room, pure-Rust parametric 3D CAD app (sketch solver, feature timeline, B-rep kernel, an egui UI that agents drive). This file is a router: the hard rules, then the skill to read for your task. Humans: CONTRIBUTING.md.
|
||||
|
||||
## Product name
|
||||
SolidCraft everywhere: crates `solidcraft-*`, binaries `solidcraft` and `solidcraft-cli`,
|
||||
`.solidcraft` designs, the `solidcraft/N` format tag, `SOLIDCRAFT_*` environment variables,
|
||||
settings and recovery folders, the MCP server name, docs, CI and packaging. The old name
|
||||
survives only in the explicit compatibility shims listed in `docs/compatibility.md`; don't add
|
||||
new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pages
|
||||
`/solidcraft/`; do not add permanent old-name Pages aliases.
|
||||
## Start here
|
||||
Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelled `help wanted`, `good first issue`, `needs-triage`; architecture in `docs/architecture.md`. Act when the code, docs and rules settle a question; ask only when it changes what users see. Local `plan/` notes are optional.
|
||||
|
||||
## Start every session here
|
||||
1. Read `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md`
|
||||
and the relevant `plan/architecture.md` section.
|
||||
2. Kernel and library decisions: `plan/adr/0001-geometry-kernel.md` (what we may depend on, what
|
||||
is rejected for licence reasons).
|
||||
3. Follow the autonomous operation protocol (`plan/execution-plan.md` §6). Don't stop to ask
|
||||
unless it lists the decision as the owner's.
|
||||
## Hard rules (what enforces each)
|
||||
- Independent: never copy another product's code, icons, text or templates; never drive or take data from commercial CAD. `skills/solidcraft-ground-rules/SKILL.md`.
|
||||
- No GPL/LGPL/AGPL code, not even reading it (OpenCASCADE, FreeCAD, SolveSpace, CadQuery, LibreCAD, QCAD): `cargo xtask licences --check`.
|
||||
- Every asset is original or openly licensed with a row in ATTRIBUTION.md; read `assets/AGENTS.md` before icon work: `cargo xtask assets`, `cargo xtask icons`.
|
||||
- Never crash: no unwrap/expect/panic/unsafe in non-test code (unsafe allowlist in crash-safety), errors are `Result`, input is hostile: clippy `-D warnings`, `hostile_params_never_panic`.
|
||||
- Everything is a command and programmatic calls never open dialogs: `skills/solidcraft-commands/SKILL.md`.
|
||||
- Layering (geom, sketch/kernel/render, doc, io, engine, ui-egui, apps); only `crates/kernel` uses truck: `cargo xtask layers`.
|
||||
- One outer UI container, one camera path (`navigation::handle`), colours from theme tokens: `cargo xtask ui-containers`.
|
||||
- Application code is Rust only and builds for wasm: `cargo xtask wasm`, `cargo xtask web-offline-test`.
|
||||
- The product name is SolidCraft; the old name lives only in `docs/compatibility.md`.
|
||||
- Sorted lists (message catalogs, `mod` lines, languages) stay sorted: `cargo xtask lists`. Never edit generated files (`docs/features.md`, `THIRD-PARTY-LICENSES.txt`) in a PR.
|
||||
- A design-format change is ONE new file in `crates/doc/src/format/pending/`: `scripts/test-format-pending.sh`.
|
||||
- A command has input and output schemas (will become required; `schema/unconverted.txt` only shrinks): `skills/solidcraft-commands/SKILL.md`.
|
||||
- New UI strings go in `assets/locales/pending/<slug>.json`, ATTRIBUTION rows in `attribution/pending/<slug>.md`: `cargo xtask lists`.
|
||||
- A changed default keeps the old behaviour behind a preference and fills the PR's `Changes a default:` line, and no second PR for an issue a lower-numbered one closes: `pr-checks.yml`.
|
||||
- A regression test goes in the area's existing test file, never a per-bug file (#3226).
|
||||
- Look at open PRs first, claim with a draft PR (lowest number wins), one topic per PR, never force-push `main`, no pushes just to catch up with `main`: `skills/solidcraft-contributing/SKILL.md`.
|
||||
- Show observable changes with real screenshots or a recording, captured yourself.
|
||||
- Run `cargo xtask ci` before every commit, without switching branches while it runs. `cargo xtask skills` keeps this file and `skills/` honest.
|
||||
|
||||
`plan/` is gitignored (local only).
|
||||
|
||||
## Never crash
|
||||
People trust SolidCraft with their designs; a crash loses their work. **This outranks feature work.**
|
||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The exceptions, in the app crate and in
|
||||
`crates/threedx-client` (which deny it instead), have no safe API: the macOS-only
|
||||
`crates/threedx-client/src/macos.rs` (loads the 3Dconnexion driver's client framework at run time
|
||||
and calls through its function pointers), two functions in
|
||||
`apps/solidcraft/src/open_documents.rs` (macOS hands Finder files only to an Objective-C
|
||||
method), the Windows-only `apps/solidcraft/src/native_crash.rs` (the exception filter that
|
||||
records crashes inside native code such as graphics drivers), the Windows-only
|
||||
`apps/solidcraft/src/native_frame.rs` (the window procedure that answers the resize-border hit
|
||||
test of the window without OS decorations) and the Windows-only
|
||||
`apps/solidcraft/src/space_mouse/windows_hid.rs` (asks Plug and Play which HID interfaces are 3D
|
||||
mice, so no other device is sent anything).
|
||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||
returns an error ("not supported yet: …"); it never panics.
|
||||
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
|
||||
input): `get()` not `[i]`, finite checks, caps on sizes and loop counts.
|
||||
- **Third-party kernel calls are guarded:** everything that calls `truck` goes through
|
||||
`solidcraft_kernel::guard`, which turns a panic into `KernelError::Internal`.
|
||||
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an escaped
|
||||
panic restores the design and reports `EngineError::Internal`.
|
||||
- **Crash reports:** the desktop app writes every panic and every unclean exit to a local crash folder,
|
||||
with the design and the commands that reproduce it (`solidcraft_engine::crash`, docs/crash-reports.md).
|
||||
When someone says the app crashed, look there first: `solidcraft-cli crashes`, `crashes show <id>`,
|
||||
`crashes replay <id>` (or the `diagnostics.crash_reports` command / MCP `crash_reports` tool).
|
||||
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`); a
|
||||
crash report's `design.solidcraft` + `replay.json` is the test's starting point.
|
||||
- **Where tests go:** a regression test goes into the existing test file for its area (booleans, blends,
|
||||
shell, sweep, STEP, naming…), never a new file named after the bug (#3226).
|
||||
- Every production crate root carries
|
||||
`#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]`.
|
||||
|
||||
## Non-negotiables
|
||||
- **Independent.** SolidCraft is independent. Behaviour is designed from public documentation,
|
||||
general CAD conventions and user feedback. Reference tests use analytic values and open-source
|
||||
tools. Never copy another product's code, icons, artwork, help text, tooltips, templates or
|
||||
materials. File formats come from public specifications.
|
||||
- **No commercial CAD products.** Agents must not drive, script, automate or extract data from
|
||||
commercial CAD products (Fusion, SolidWorks, Onshape, …) for this project: no test data,
|
||||
measurements, command lists, ids or files produced with them.
|
||||
- **Never read, copy or link GPL/LGPL/AGPL code**: OpenCASCADE, FreeCAD (incl. planegcs),
|
||||
SolveSpace, CadQuery/OCP, LibreCAD, QCAD — don't even read their sources. (`tools/freecad-oracle/`
|
||||
runs FreeCAD as a separate program through its public scripting API to compute reference values;
|
||||
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.** Command icons are the original SVG masters in
|
||||
`assets/icons/ui/design/svg/`, compiled through `crates/ui-egui/src/icon_svg.rs`.
|
||||
**Before any icon, icon assignment or media edit, read `assets/AGENTS.md`,
|
||||
`assets/icons/ui/AGENTS.md` and `assets/icons/ui/STYLE_GUIDE.md`.** Use the canonical
|
||||
bases and named finished references there; never invent a parallel icon style. 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/*`
|
||||
(`CommandSpec`: id = our own dotted, lower-case id `<area>.<thing>[.<variant>]` such as
|
||||
`solid.extrude` or `sketch.rectangle.two_point`, never another product's command id; label;
|
||||
toolbar tab and panel; icon; shortcut; params doc; `enabled`; JSON `run`) + tests. The toolbar,
|
||||
dialogs, sketch tools, palette, scripts, CLI and control channel all reach the same commands.
|
||||
- **Programmatic calls never open dialogs.** `engine.execute` runs the JSON form; only toolbar
|
||||
clicks (`ui.start`) start interactive tools and dialogs, which then run commands.
|
||||
- **Layering** is enforced by `cargo xtask layers`: L0 `geom` → L1 `sketch`, `kernel`, `render`
|
||||
→ L2 `doc` → L3 `io` → L4 `engine` → L5 `ui-egui` → apps. Nothing below L5 depends on
|
||||
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`.
|
||||
- **One outer UI container**: all panels, floating sheets, menus, overlays and platform UI
|
||||
windows use `ui_kit::Container` (directly or through `Panel`, `Window`, `Confirm`, `Menu`).
|
||||
Declare the semantic `Surface`; workspace panels use its shared layout policy and chrome.
|
||||
System/report windows use `ui_kit::Window`; its low-level host builder is kit-private.
|
||||
Do not construct raw egui hosts, outer popup/window frames or shadows in feature modules.
|
||||
`cargo xtask ui-containers` recursively checks source/examples in all current and future crates
|
||||
and apps. It runs in the fast repository CI job and `cargo xtask ci`, with no caller allowances.
|
||||
See `docs/ui-container-audit.md` for the inventory and legitimate content/native exceptions.
|
||||
- **One set of camera controls**: every page or canvas with a camera (viewport, Render
|
||||
preview, future flat pattern or drawing pages) calls `navigation::handle` with its `NavCaps`
|
||||
(orbit off for flat 2D pages). New canvases must use `navigation::handle`; never hand-roll
|
||||
camera input (a test rejects it).
|
||||
- **Application code is Rust only.** Units are millimetres and radians internally; Z is up.
|
||||
Local asset authoring, lint and review tools use Python 3.11+ as documented in
|
||||
`assets/AGENTS.md`; Python is not an application runtime dependency. The application 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**: the repository is github.com/tangency-labs/solidcraft. Changes land through
|
||||
**pull requests** and the **merge queue**: push a branch and open a PR. The heavy CI jobs start
|
||||
when a maintainer adds `approved-for-ci` after reviewing the code. Approved PRs go into the queue
|
||||
with `gh pr merge N -R tangency-labs/solidcraft --squash --auto`; the queue runs the full suite
|
||||
on each group before it lands on `main`. Keep each PR to one topic and one issue, and never
|
||||
force-push `main`. Never commit `plan/`, build dirs (`target*/`) or generated reference data;
|
||||
check `git ls-files` before pushing.
|
||||
- **Never break wasm**: everything below the apps must build for `wasm32-unknown-unknown`
|
||||
(`cargo xtask wasm`, part of `cargo xtask ci`).
|
||||
|
||||
## Showing and following through on changes
|
||||
|
||||
- Help reviewers see what is broken or changed. For observable CAD, UI or model behavior,
|
||||
issues and PRs must include actual descriptive screenshots or a short recording from a
|
||||
small reproduction. This includes kernel, geometry, assembly and backend changes whose
|
||||
effects can be shown in the app. Coding agents should run the app and capture this evidence
|
||||
themselves; do not ask a human for evidence the agent can produce. Show the actual failure
|
||||
or result, preferably before and after, with brief reproduction steps, expected/actual
|
||||
behavior and annotations that point out the relevant detail.
|
||||
- Flowcharts and Mermaid diagrams cannot substitute for screenshots or recordings of visible
|
||||
behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate
|
||||
the point. Changes with no useful observable behavior do not need screenshot or video evidence.
|
||||
- Use a small public-safe example and check for private designs, paths, account details or
|
||||
other sensitive content before sharing. Attach media to the issue/PR or link to an accessible
|
||||
recording; do not commit generated screenshots or videos just to provide review evidence.
|
||||
- Throughout the work and before handoff, check the relevant PR's merge status. Don't push to
|
||||
an open PR only to catch up with `main` (no merge of `main`, no Update branch, no rebase for
|
||||
that alone): every push restarts the whole CI run, and `main`'s own run catches what lands in
|
||||
between, to be fixed forward. When you push a change anyway, or GitHub reports a conflict,
|
||||
rebase onto the latest `main` (push your own branch with `--force-with-lease`); never merge
|
||||
`main` into a PR. Resolve conflicts without discarding others' work; never force-push `main`.
|
||||
Don't re-run CI on an unchanged commit: a real failure fails again. Re-run only a job that
|
||||
runner trouble cancelled, with "Re-run failed jobs". Report the PR state you checked and any
|
||||
conflicts resolved. If a PR remains open, keep monitoring it while the task is active and
|
||||
make its pending status clear at handoff; do not claim it merged without checking.
|
||||
|
||||
## Before you start: look at what is already open
|
||||
- Work with an authenticated GitHub CLI (`gh auth status`) and `git`; the checks below need both.
|
||||
- Pull the whole list, not one search: `gh pr list -R tangency-labs/solidcraft --state open --limit 500`
|
||||
(drafts included) and `gh issue list -R tangency-labs/solidcraft --state open --limit 500`. Look for
|
||||
work on the same issue, the same feature under another number, and PRs that change the files
|
||||
you are about to change (`gh pr list ... --json number,title,files`).
|
||||
- If someone already has it, comment on their PR or issue instead of opening a second one. Two
|
||||
PRs for one change can't both land, and one of them is wasted work and CI.
|
||||
|
||||
## Claim = a draft PR
|
||||
- Before working on issue #N, check nobody has it: `gh pr list -R tangency-labs/solidcraft --state open --search "#N"`.
|
||||
- If none, branch from the latest `origin/main`, `git commit --allow-empty -m "claim: #N"`, push,
|
||||
and open a **draft** PR titled `<summary> (#N)` right away. **The lowest-numbered open PR for an
|
||||
issue wins**; if yours lost, close it. Opening the PR is the claim: the bot assigns you and
|
||||
labels the issue `in progress` (or says on your PR who holds it). Reference `#N` in the PR title
|
||||
so the bot can find it. Don't claim by editing files; comment `/claim` on the issue only to stake
|
||||
a claim before you have a branch.
|
||||
- Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over:
|
||||
comment on it first, then open your own claim (you may start from its branch).
|
||||
This takeover policy is separate from the bot's automatic cleanup deadlines in CONTRIBUTING.md.
|
||||
- On a shared build machine, check the load before a big build (`cut -d' ' -f1 /proc/loadavg`)
|
||||
and wait while it is over 24; build with `nice`.
|
||||
|
||||
## Design file format
|
||||
A change that saves something new in a design adds ONE file, `crates/doc/src/format/pending/<name>.rs`
|
||||
(a `//!` note, and `pub fn step` if older designs must be rewritten). Never edit `FORMAT`,
|
||||
`JSON_STEPS`, the history in `format.rs`, another change's file or a `v<N>/` folder: the build
|
||||
works out the number (the next one while anything is pending), and the release folds pending
|
||||
changes into `v<N>/` with `scripts/fold-format-pending.sh`. See CONTRIBUTING.md.
|
||||
|
||||
## Application releases
|
||||
Development pushes are not application releases. Follow `docs/releasing.md` when asked to
|
||||
prepare or ship a release: choose the version from the actual changes, update Cargo.toml and
|
||||
Cargo.lock, pass the quality gates, advance `release` without force-pushing, and verify the
|
||||
resulting draft and its artifacts. When shipping is authorized, publish that verified draft
|
||||
and confirm its tag and downstream deployment. Do not auto-release every push to `main`, infer
|
||||
version bumps solely from commit prefixes, overwrite published versions, or push release tags
|
||||
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`). 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.
|
||||
- **For UI work, look at the result** (screenshot, read the PNG). Without a window use
|
||||
`ui.render` or `solidcraft-cli snapshot`.
|
||||
- Headless: `solidcraft-cli run script.json --out part.step`, `solidcraft-cli eval design.solidcraft`,
|
||||
`solidcraft-cli snapshot script.json --out shot.png`, `solidcraft-cli commands`.
|
||||
- Reference tests: `cargo xtask oracle` replays `tests/oracle/*/recipe.json` and compares with
|
||||
each case's `expected.json` (analytic or FreeCAD values; writes `docs/oracle.md`). Expected
|
||||
values never come from a commercial CAD product.
|
||||
- Build with `CARGO_TARGET_DIR=<repo>/target`. Windows: `cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft`.
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and estimates. Update it whenever a
|
||||
milestone task lands. `docs/features.md` (SolidCraft's commands by area, from the registry) and
|
||||
`THIRD-PARTY-LICENSES.txt` are regenerated on `main` after each merge by the Generated files
|
||||
workflow; never regenerate them in a PR.
|
||||
|
||||
## Lists many PRs add to
|
||||
Keep them conflict-free (CONTRIBUTING.md, "Lists many changes add to"): message catalogs sorted
|
||||
(`cargo xtask lists --fix`), `mod` lines in their sorted group, the language list English first
|
||||
then by id, generated files left to `main`, and a design-format change as one new file.
|
||||
## Working on X: read the skill first
|
||||
| Working on | Read |
|
||||
|---|---|
|
||||
| A PR, a claim, review, CI or evidence | `skills/solidcraft-contributing/SKILL.md` |
|
||||
| Licences, assets, clean-room, the name | `skills/solidcraft-ground-rules/SKILL.md` |
|
||||
| Non-test Rust, hostile input, a crash report | `skills/solidcraft-crash-safety/SKILL.md` |
|
||||
| A command, a crate boundary | `skills/solidcraft-commands/SKILL.md` |
|
||||
| A dialog, panel, camera, icon, colour | `skills/solidcraft-ui/SKILL.md` |
|
||||
| Labels, messages, translations | `skills/solidcraft-ui-text/SKILL.md` |
|
||||
| Saving something new in a design, import, export | `skills/solidcraft-file-formats/SKILL.md` |
|
||||
| Preferences, a changed default | `skills/solidcraft-preferences/SKILL.md` |
|
||||
| Writing or running tests | `skills/solidcraft-tests/SKILL.md` |
|
||||
| Slow work, the window thread, wasm | `skills/solidcraft-performance/SKILL.md` |
|
||||
| Releases, installers, packages | `skills/solidcraft-packaging/SKILL.md` |
|
||||
| Running the app, control channel, MCP tools | `skills/solidcraft-agent-tools/SKILL.md` |
|
||||
| Building a part over MCP | `skills/solidcraft-modeling/SKILL.md` |
|
||||
| Changing an existing design | `skills/solidcraft-editing/SKILL.md` |
|
||||
| A bug report, a crash, a wrong result | `skills/solidcraft-reporting/SKILL.md` |
|
||||
| Checking, porting, designs in git | `skills/solidcraft-checking/SKILL.md` |
|
||||
| Assemblies, joints, motion | `skills/solidcraft-mechanisms/SKILL.md` |
|
||||
>>>>>>> 4a3e0772a (Agents: AGENTS.md becomes a router to focused skills, with a staleness check (Refs #6439))
|
||||
|
||||
+503
-10
@@ -14,9 +14,33 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/catalog/capabilities.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/capabilities.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/environments.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/environments.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/lighting.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/lighting.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/materials.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/materials.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/quality-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/quality-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| assets/render/catalog/scene-presets.json | SolidCraft contributors | Byte-identical runtime export of docs/render-materials/scene-presets.json; generated and checked by tools/render-library/generate.py | MIT OR Apache-2.0 |
|
||||
| examples/chebyshev-motion-trace.json | SolidCraft contributors | Original parametric Chebyshev lambda mechanism and associative-trace demo, constructed from analytic circle intersections; classical link proportions documented at https://doi.org/10.1002/rcs.70095; no external CAD geometry or artwork | MIT OR Apache-2.0 |
|
||||
| examples/material-card.json | SolidCraft contributors | original analytic box and explicitly synthetic engineering card; invented illustrative constants, no external material dataset | MIT OR Apache-2.0 |
|
||||
| examples/studio/author_sweep.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/author_scene.py | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/materials.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded-sweep.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded-render.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-exploded.json | SolidCraft contributors | original editable CAD scene and authoring metadata; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/stage1-sweep.json | SolidCraft contributors | original tangent circular-arc CAD cyclorama; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/stage1.json | SolidCraft contributors | original editable CAD studio scene; no imported geometry or external image assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/stage1-render.json | SolidCraft contributors | original studio camera and lighting parameters | MIT OR Apache-2.0 |
|
||||
| examples/render-studio.json | SolidCraft contributors | original procedural box/cylinder enclosure script for the integrated Render workspace | MIT OR Apache-2.0 |
|
||||
| examples/render-opaque-studio.json | SolidCraft contributors | original procedural sphere study varying opaque metal fraction and roughness independently; no external assets or engineering data | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/denoise.wgsl | SolidCraft contributors | original bounded edge-aware a-trous spatial filter; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/denoise_guides.wgsl | SolidCraft contributors | original deterministic centre-ray material and geometry guide pass; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/shadows.wgsl | SolidCraft contributors | original bounded area, sun and environment visibility transport; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/textures.wgsl | SolidCraft contributors | original planar colour and normal map sampling; GPU2 donor d710e31, no external shader source | MIT OR Apache-2.0 |
|
||||
| assets/appearances/starter_pack.rs | SolidCraft contributors | original illustrative optical recipes, authored scalar values and future procedural resource descriptors; no measured or external material dataset | MIT OR Apache-2.0 |
|
||||
| examples/material-finishes.json | SolidCraft contributors | original procedural sphere study of six illustrative opaque finishes with generic material densities; no external assets or measured optical data | MIT OR Apache-2.0 |
|
||||
| examples/render-path-studio.json | SolidCraft contributors | original procedural opaque enclosure, teal insert and metal fasteners for progressive rendering; no imported models | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/path/path.wgsl | SolidCraft contributors | original software BVH traversal and path integration using the existing SolidCraft surface/studio equations; no external shader source | MIT OR Apache-2.0 |
|
||||
| crates/gpu/src/pbr.rs | SolidCraft contributors | original WGSL translation of SolidCraft's opaque surface and fixed studio reference, embedded as Rust source; no external shader code | 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 |
|
||||
@@ -25,10 +49,12 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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 | retained editable historical VectorCraft source, with named face and background layers; redundant exports retired | 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 |
|
||||
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: two boxes bridged by `surface.blend` (a 40x10 sheet, `solidcraft-cli snapshot` of plan/evidence script ev_blend) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a saddle rim (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch through two sketch lines built by `surface.boundary` with `crown: 0.4` (`solidcraft-cli snapshot`) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
|
||||
| assets/models/test_boat.bin | 3DBenchy.com | 3DBenchy.com | Public Domain |
|
||||
| assets/models/make_test_boat.py | SolidCraft contributors | original script that packs a model into the compact file above | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-blend.png | SolidCraft contributors | screenshot of SolidCraft itself: a 24 mm and a 10 mm box bridged by a `surface.blend` sheet (G1 at both edges), rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-crown.png | SolidCraft contributors | screenshot of SolidCraft itself: a patch crowned with `surface.patch crown=0.3` over a skew four-line rim, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/class-a-boundary.png | SolidCraft contributors | screenshot of SolidCraft itself: a surface through two 3D sketch splines built by `surface.boundary`, rendered by the app's `ui.render` in the dark theme with examples/demo/class_a_shots.py | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft 0.22.1 itself (`solidcraft --sample`, default dark theme, 1600x1000) | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/0.png | SolidCraft contributors | original Sample Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/1.png | SolidCraft contributors | original Sheet Metal Bracket from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/2.png | SolidCraft contributors | original Hinge Assembly from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
@@ -36,10 +62,10 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/samples/exports/4.png | SolidCraft contributors | original Bolted Flange from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/5.png | SolidCraft contributors | original Vent Plate from `crates/engine/src/sample.rs`, exported by the SolidCraft thumbnail renderer | MIT OR Apache-2.0 |
|
||||
| assets/samples/exports/scripts.json | SolidCraft contributors | SHA-256 fingerprints of the original sample scripts, generated with the shipped thumbnails | 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 |
|
||||
| docs/screenshots/demo.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) 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 (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/demo.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.22.1 English desktop UI (default dark theme, software renderer) 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 (6× playback, idle pauses cut) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) 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 (15× playback, idle pauses cut, constant-rate camera orbit) | MIT OR Apache-2.0 |
|
||||
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft 0.22.1 desktop UI (default dark theme, software renderer) 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 |
|
||||
| docs/design/ux-redesign/model-floating.png | SolidCraft contributors | original UX mockup (Model tab, floating layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/model-compact.png | SolidCraft contributors | original UX mockup (Model tab, compact docked layout) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
| docs/design/ux-redesign/sketch.png | SolidCraft contributors | original UX mockup (Sketch tab) rendered from an HTML mockup that uses the original Technical Accent icon set; no third-party artwork, and not a screenshot of any other CAD product | MIT OR Apache-2.0 |
|
||||
@@ -75,6 +101,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| tests/step-conformance/tiny_pin.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/torus_ring_nurbs.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-conformance/washer.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
| tests/step-regressions/rod_doubled_edge.step | SolidCraft contributors | written by SolidCraft from the commands of #4598 (a bored rod moved 100 mm and exported); import regression fixture | MIT OR Apache-2.0 |
|
||||
| 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 |
|
||||
@@ -96,6 +123,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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/appearances/opaque_finishes.rs | SolidCraft contributors | original illustrative opaque finish values; no external dataset, textures or measured engineering data | 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/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
@@ -130,6 +158,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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/ms.json | A.Hakim (a-hakim) | Malay translation of the English message catalog | MIT OR Apache-2.0 |
|
||||
| assets/locales/nb.json | SolidCraft contributors | Original Norwegian Bokmål translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/nl.json | SolidCraft contributors | Original Dutch translation of SolidCraft's English catalog; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/locales/pl.json | cyryllo | Polish translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
|
||||
@@ -145,6 +174,10 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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/dmg/background.tiff | SolidCraft contributors | original disk image window background (blueprint sheet: grid, double border, circles, centre lines, leader arrow, title block) drawn by assets/platforms/macos/dmg/make_background.py in IBM Plex Sans/Mono; 1x and 2x representations | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/make_background.py | SolidCraft contributors | authoring script that draws the disk image background (Pillow); not part of any build | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/layout.json | SolidCraft contributors | disk image window size, icon size and icon positions shared by the background script and settings.py | MIT OR Apache-2.0 |
|
||||
| assets/platforms/macos/dmg/settings.py | SolidCraft contributors | dmgbuild settings (MIT, https://github.com/dmgbuild/dmgbuild) for the SolidCraft disk image; original settings, no dmgbuild code copied | MIT OR 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 |
|
||||
@@ -171,6 +204,8 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/styles/ui/themes/graphite.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/nosferatu.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Dracula by Zeno Rocha and the Dracula Theme contributors, https://github.com/dracula/dracula-theme, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/catppuccin-mocha.json | SolidCraft contributors | original SolidCraft theme file adapting the colours of a published palette (Catppuccin by the Catppuccin organisation, https://github.com/catppuccin/catppuccin, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/okibi.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Okibi palette (dark) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/akari.json | SolidCraft contributors (Kai Evans) | original SolidCraft theme file mapping SolidCraft's UI colours to the Akari palette (light) of the Okibi design system by Kai Evans (https://kaievns.github.io/okibi-akiri-design-system-spec/) | MIT OR Apache-2.0 |
|
||||
| assets/styles/web/loading-logo.svg | SolidCraft contributors | Animation of the SolidCraft mark, built from the paths and gradients of assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg; original animation | 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 |
|
||||
| crates/io/src/native_cad/fixtures/block.FCStd | SolidCraft contributors | original 10 × 20 × 30 mm test box saved by FreeCAD 1.1.4 run as an external program (`make_block_fcstd.py` beside it); no third-party geometry | MIT OR Apache-2.0 |
|
||||
@@ -253,6 +288,8 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/model/circle_tt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/circle_ttt.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/coil.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/colored_components_off.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/combine.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/component.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/compute.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -343,6 +380,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/icons/ui/design/svg/flat/stop.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/shield.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/status_recovery.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/frame_member.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/placeholder.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_angle.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
@@ -484,6 +522,11 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
|
||||
| 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 |
|
||||
| docs/sponsors/blacksmith-ci-powered.png | Blacksmith (blacksmith.sh) | "CI powered by Blacksmith" sponsor banner supplied by Blacksmith for the sponsorship credit; used only to credit the sponsor, not part of the application | © Blacksmith, used with permission |
|
||||
| assets/icons/ui/design/svg/model/path_flange.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/hem.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/hem.json`; gitignored), original sheet metal demonstration | MIT OR Apache-2.0 |
|
||||
| docs/book/scripts/hem.json | SolidCraft contributors | original parametric recipe for an 80 × 50 mm sheet with an 8 mm returned edge, authored for the command guide | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/clay.json | SolidCraft contributors | original SolidCraft theme file (Clay, light): warm paper palette chosen for SolidCraft | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/clay-dark.json | SolidCraft contributors | original SolidCraft theme file (Clay Dark): warm charcoal palette chosen for SolidCraft | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -526,9 +569,9 @@ crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
0.6.1 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
0.6.3 (Apache-2.0), linked as the crates `cadmpeg-core`, `cadmpeg-ir`, `cadmpeg-registry`,
|
||||
`cadmpeg-container`, `cadmpeg-codec-freecad`, `cadmpeg-codec-rhino` and `cadmpeg-codec-step`
|
||||
in native builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
|
||||
in native and browser builds (`crates/io/src/native_cad.rs`). Only those open-format codecs are built: no
|
||||
default features, so cadmpeg's codecs for commercial CAD formats and its ShapeManager/ACIS
|
||||
codec are not compiled in. Its licence text is in THIRD-PARTY-LICENSES.txt; no code from it is
|
||||
copied into SolidCraft.
|
||||
@@ -561,3 +604,453 @@ A frame whose meshes the GPU refuses to stage (device lost or out of memory) is
|
||||
skipped with a warning instead of panicking (#1469).
|
||||
Paint callbacks are prepared only after a drawable is acquired, so a skipped
|
||||
surface frame cannot strand cached meshes without their GPU initialization.
|
||||
|
||||
## Runtime studio lighting mathematics
|
||||
|
||||
`crates/gpu/src/path/path.wgsl` implements the power-heuristic estimator from the
|
||||
mathematical description in [PBRT fourth edition, section 13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
|
||||
No PBRT code or assets were copied. `crates/render/src/job/lighting.rs` evaluates
|
||||
[Krzysztof Narkowicz's published 2016 ACES rational fit](https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/)
|
||||
using its published coefficients (author licenses the fit under CC0/MIT).
|
||||
These original implementations are under SolidCraft's MIT OR Apache-2.0 licence.
|
||||
|
||||
## Original coffee framing variant
|
||||
|
||||
| File | Author | Provenance | Licence |
|
||||
|---|---|---|---|
|
||||
| examples/studio/author_coffee_framing.py | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-framing.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-framing-sweep.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-framing-render.json | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-framing.md | SolidCraft contributors | original editable composition, camera and documentation; no external assets | MIT OR Apache-2.0 |
|
||||
|
||||
## Original cylindrical coffee housing variant
|
||||
|
||||
| File | Author | Provenance | Licence |
|
||||
|---|---|---|---|
|
||||
| examples/studio/author_coffee_cylinder.py | SolidCraft contributors | original parametric cylindrical CAD construction and editable command recipes; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-cylinder.json | SolidCraft contributors | original cylindrical housing on the original exploded coffee composition; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-cylinder-sweep.json | SolidCraft contributors | original cylindrical housing and continuous CAD cyclorama; no external assets | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-cylinder-render.json | SolidCraft contributors | original camera, studio lighting and optical settings retained from the framing variant | MIT OR Apache-2.0 |
|
||||
| examples/studio/coffee-cylinder.md | SolidCraft contributors | original dimensional recipe, provenance and validation limitations | MIT OR Apache-2.0 |
|
||||
|
||||
## Runtime environment sampling
|
||||
|
||||
`crates/render/src/lighting/environment.rs` and the environment/sun portions of
|
||||
`crates/gpu/src/path/path.wgsl` are original SolidCraft implementations of
|
||||
solid-angle importance sampling and MIS, informed by the mathematical description
|
||||
in [PBRT fourth edition, sections 12.5](https://pbr-book.org/4ed/Light_Sources/Infinite_Area_Lights)
|
||||
and [13.4](https://pbr-book.org/4ed/Light_Transport_I_Surface_Reflection/A_Better_Path_Tracer).
|
||||
The procedural gradient and inline RGBE test fixtures are original mathematical
|
||||
colours, not downloaded imagery. No third-party environment images or shader code
|
||||
are included. Licence: MIT OR Apache-2.0.
|
||||
|
||||
## Original tumbler showcase assets
|
||||
|
||||
The CAD room, tumblers, window opening, wood/weave patterns and RGBE sky below
|
||||
are original SolidCraft contributor work. No third-party scene, photograph, HDRI,
|
||||
texture, or AI-generated artwork was used. Python authoring sources are included.
|
||||
All listed files use the repository MIT OR Apache-2.0 licence.
|
||||
|
||||
| File | Source / author | Use | License |
|
||||
| --- | --- | --- | --- |
|
||||
| examples/studio/author_showcase.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/author_tumblers.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/textures/ATTRIBUTION.md | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/textures/author_textures.py | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/textures/environment-manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/textures/manifest.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-hdr-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-hdr-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-materials.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-sun-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-sun-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-sun-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-sun.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-textures-blocked-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers-textures-render.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
| examples/studio/tumblers.json | Original SolidCraft contributors | Editable scene / runtime resource / reproducible authoring metadata | MIT OR Apache-2.0 |
|
||||
|
||||
The original studio generators produce local ignored PNG maps and an RGBE gradient.
|
||||
Generated image bytes are not source assets; their authorship, licences and expected
|
||||
hashes are documented under `examples/studio/textures`. Rendered evidence belongs
|
||||
on the results branch. No photographs, scans or external image assets are included.
|
||||
|
||||
## Additional render and showcase resources
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/starter/tools/check_all.py | SolidCraft contributors | original read-only authored/native resource validation orchestration and evidence receipts | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_check_all.py | SolidCraft contributors | original validation-runner regression fixtures and subprocess tests | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/requirements.txt | SolidCraft contributors | pinned offline authoring tool requirements; no application dependency changes | MIT OR Apache-2.0 |
|
||||
| crates/render/examples/validate_starter.rs | SolidCraft contributors | original native resource conformance checker using public SolidCraft decoding, normalization, sampling and mip APIs | MIT OR Apache-2.0 |
|
||||
| .gitattributes | SolidCraft contributors | original canonical line-ending policy for the existing runtime icon-discovery manifest; no artwork or icon-assignment changes | MIT OR Apache-2.0 |
|
||||
| assets/render/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/.gitattributes | SolidCraft contributors | original canonical line-ending policy for SHA-256-pinned renderer authoring resources | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/assembly-bench-soft.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/automotive-turntable.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/black-metal-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/cnc-overhead.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/esd-bench-neutral.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/fdm-neutral-inspection.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/garage-door.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/hangar-skylight.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/home-workbench.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/makerspace-worklights.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/mechanical-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/neutral-comparison.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/neutral-side-softbox.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/paint-inspection-bands.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/printed-part-photo.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/rework-offset-strips.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/robot-led-editorial.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/showroom-ribbons.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/spacecraft-clean-bay.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/sun-free-overcast.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/vacuum-black.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/warm-table-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/white-enveloping-studio.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/exports/woodshop-window.hdr | SolidCraft contributors | original analytic relative-radiance HDR export; recipe, linear-sRGB primaries and SHA-256 in assets/render/starter/environments/manifest.json; not measured photometry | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/environments/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/lighting/exports/rigs.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/lighting/recipe.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-001/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-002/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-003/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-004/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-005/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-006/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-007/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-008/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-009/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-010/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-011/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-012/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-013/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-014/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-015/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-016/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-017/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-018/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-019/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-020/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-021/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-022/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-023/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-024/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-025/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-026/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-027/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-028/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-029/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-030/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-031/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-032/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-033/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-034/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-035/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-036/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-037/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-038/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-039/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-040/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-041/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-042/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-043/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-044/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-045/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-046/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-047/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/base_color.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/height.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/metallic.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/normal_open_gl.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/exports/SURF-048/roughness.png | SolidCraft contributors | original deterministic periodic PBR map export; recipe, role, scale and SHA-256 in assets/render/starter/materials/manifest.json; not measured material data | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/manifest.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/materials/source/recipes.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/pack.json | SolidCraft contributors | original clean-room renderer resource recipe or manifest; authored/unbound scope and source receipts in assets/render/starter/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_environments.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_lighting.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/test_materials.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/starter/tools/validate_pack.py | SolidCraft contributors | original clean-room offline resource authoring, numerical verification or validation tool; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/licenses/UFL.txt | Canonical Ltd. | exact Ubuntu Font Licence 1.0 notice retained from https://github.com/0xlillith/solidcraft-rover/blob/b32437915372d32b71419dc8f5143fe22ff1e869/UFL.txt for the modelled Ubuntu Light lettering | Ubuntu Font Licence 1.0 |
|
||||
| assets/render/selene/materials/appearance-pack.json | SolidCraft contributors | original clean-room English appearance authoring data: 48 illustrative opaque PBR projections with separate advanced target specifications; no measured dataset or external assets | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/materials/appearance-swatches.json | SolidCraft contributors | original analytic command script for 48 opaque box swatches and a support base, derived only from the adjacent original appearance pack; no external artwork or engineering properties | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/models/SELENE-functional.solidcraft | SolidCraft contributors (native enhancements); 0xlillith (original rover); SychPL / Mateusz Sych (supplied showcase assembly); Canonical Ltd. (lettering outlines) | native-command derivative of the unchanged author-supplied source, with original bearing, suspension, gripper, electronics and presentation work; source and final hashes in assets/render/selene/SOURCE.md and VALIDATION.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
|
||||
| assets/render/selene/motion/interference-pairs.json | SolidCraft contributors | original authored exact-body pair selection for bounded native clearance checks on the functional SELENE derivative; not a generated collision result | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/mechanism-acceptance.json | SolidCraft contributors | original kinematic acceptance criteria, inspected source-container bindings and analytic planning checks from cited public SolidCraft documentation and the admitted native source; derivative runtime acceptance is recorded separately | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/preflight-commands.json | SolidCraft contributors | original read-only inventory script using existing project commands; no source-model selectors or state mutations | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/motion/sample-sequence.json | SolidCraft contributors | original future lunar sampling and sealed-vial dynamics state specification; no simulation or physical calibration result | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/roles.json | SolidCraft contributors | original semantic role and candidate-material authoring specification for the prepared SELENE demo; no native geometry or external assets bundled | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/shots.json | SolidCraft contributors | original shot, framing and staged-sequence authoring specification for the prepared SELENE demo; no render or simulation result claimed | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/scenes/stages.json | SolidCraft contributors | original stage composition and light-ownership authoring specification derived from the project render catalog; scientific context linked in scenes/README.md | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/source-lock.json | SolidCraft contributors | original source-admission metadata and public-reference inventory pinning the supplied improved SELENE native file; original-model and font notices preserved in SOURCE.md | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/source/SELENE-showcase.solidcraft | 0xlillith (original rover); SychPL / Mateusz Sych (showcase assembly and helpers); SolidCraft contributors (emblem); Canonical Ltd. (lettering outlines) | unchanged author-published native file from https://github.com/SychPL/solidcraft/blob/32ec1720447e3fcc6bb6cc53a63fb5b39cfa21b9/SELENE-showcase.solidcraft; source hash and license evidence in assets/render/selene/SOURCE.md | Model and showcase contributions: MIT OR Apache-2.0 (explicit SychPL grant https://github.com/tangency-labs/solidcraft/pull/5494#issuecomment-6375788360); lettering: Ubuntu Font Licence 1.0, notice retained in assets/render/selene/licenses/UFL.txt |
|
||||
| assets/render/selene/tools/bearings.py | SolidCraft contributors | original native-command authoring of grooved bearings, rolling-element kinematics, clamp hubs and arm/head clearances; analytic geometry, no copied CAD source | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/build.py | SolidCraft contributors | original Python development tool for pinned-source native command replay, staged checkpoints, command-trace and geometry health checks; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/electronics.py | SolidCraft contributors | original native-command Raspberry Pi 5 visual reconstruction using public manufacturer mechanical dimensions; original package envelopes and connector internals, no manufacturer CAD or artwork copied | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/export_views.py | SolidCraft contributors | original native MCP preview export tool for three reusable showcase views; changes only transient visibility and cameras and verifies the source master hash | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/finishes.py | SolidCraft contributors | original native physical-material proxy and opaque PBR assignment generator preserving the supplied rover palette | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/mechanisms.py | SolidCraft contributors | original native-command gripper sleeves, pads, wheel ownership, joint limits and inspection studies with independent analytic suspension relation | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/prepare.py | SolidCraft contributors | original Python development tool for bounded native-container inspection and reviewed opaque body-assignment preparation; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/presentation.py | SolidCraft contributors | original native-command browser organization, camera/render setups and closed sample-vial glass preparation | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/suspension.py | SolidCraft contributors | original native-command trailing-arm suspension, telescoping coilover loops, plain bushings, carrier gussets and local motion clearances derived from the admitted rover geometry; independent analytic pin-distance relation, no copied external CAD | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/validate_native.py | SolidCraft contributors | original bounded native MCP verification tool for sampled SELENE kinematics, independent analytic follower and world-coordinate checks, optional exact-name interference pairs and unchanged input hashes; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/validate_prepared.py | SolidCraft contributors | original Python development validator for prepared SELENE resources, source status, references, hashes and command projections; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/tools/yaw_supports.py | SolidCraft contributors | original native-command plain yaw journals, flanged brass thrust bushings, recessed axial retaining pins and mounting-head clearances derived from the admitted rover geometry; no copied external CAD | MIT OR Apache-2.0 |
|
||||
| assets/render/selene/ui/demo-flow.json | SolidCraft contributors | original progressive-disclosure UI integration specification for the existing Render workspace; no copied UI artwork or runtime registration | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/baseline-setup-commands.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/capabilities.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/environment-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/environments.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/lighting.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/lunar-physics-fixtures.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/materials.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/provenance.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/quality-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/roadmap.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/scene-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/selene-bindings.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/simulation-requirements.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/studio-presets.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/ui-integration.json | SolidCraft contributors | original English UI integration specification and exact source-revision mappings from SolidCraft's own implemented shell, UI kit and accepted icons; no copied artwork or runtime registration | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/ux.json | SolidCraft contributors | original clean-room English catalog, preset or analytic planning data; external references documented in docs/render-materials/SOURCES.md; no external assets bundled | MIT OR Apache-2.0 |
|
||||
| docs/render-materials/validate_catalog.py | SolidCraft contributors | original Python development validator for catalog references, preset composition and analytic specification fixtures; no application runtime dependency | MIT OR Apache-2.0 |
|
||||
|
||||
### Built-in render resource adapters (personal integration batch, 2026-10-22)
|
||||
|
||||
The original adapters in `crates/engine/src/cmd/render/library.rs`, the generated lookup `library_data.rs` and its generator `tools/render-library/generate.py` project the already attributed starter and SELENE resources without replacing their bytes or authoring manifests. The added scalar texture channels, explicit color/V conventions and immutable-source/GPU-payload sharing are original contributor work under MIT OR Apache-2.0. No external code, commercial CAD data or new third-party imagery was imported. Source metadata and the original authoring attribution remain authoritative; pending runtime acceptance is not a photometric or engineering qualification.
|
||||
|
||||
## Physical reference resource additions
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| assets/render/physical/bindings.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/exports/coverage.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/exports/native-cards.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/profiles.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/quantities.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/sources.json | SolidCraft contributors | original physical-reference curation, associations or derived schema-1 export; per-fact public sources retained without relicensing their publications | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/check_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/test_physical.py | SolidCraft contributors | original offline physical-reference validation and projection tool | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/export_library.py | SolidCraft contributors | original explicit reference-library planner and native no-loss export verifier; no external code | MIT OR Apache-2.0 |
|
||||
| assets/render/physical/tools/test_export_library.py | SolidCraft contributors | original regressions for native capability refusal, source identity and physical-card preservation | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/selene_studio.py | SolidCraft contributors | original bounded native MCP studio orchestration with pinned editable master, explicit GPU policy and request/export receipts; no generated model or image substitutes | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/selene_recipe.py | SolidCraft contributors | original full-detail SELENE material/light composition and analytic camera fit; unchanged pinned native master, no external imagery or measured optical dataset | MIT OR Apache-2.0 |
|
||||
| tools/render-library/test_selene_recipe.py | SolidCraft contributors | original independent projection, scaling, nonmutation and input-refusal fixtures for the SELENE studio recipe | MIT OR Apache-2.0 |
|
||||
|
||||
| assets/render/selene/scenes/studio-hero.json | SolidCraft contributors | original executable render intent and verified provenance for the selected native SELENE studio showcase; authored RGB appearance and relative light units, not measured optics or simulation results | MIT OR Apache-2.0 |
|
||||
| tools/render-library/publish_selene.py | SolidCraft contributors | original bounded native-frame publication checks for explicit visual review, source identity, resource hashes, PNG CRCs and scanline limits; copies completed native bytes without image synthesis or resampling | MIT OR Apache-2.0 |
|
||||
| tools/render-library/test_publish_selene.py | SolidCraft contributors | original tiny analytic PNG fixtures for source-size, CRC, expansion, dimensions and explicit-publication-consent regressions | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/package_selene.py | SolidCraft contributors | original byte-preserving packaging of the reviewed native SELENE PNG, public provenance and checksums; bounded resource/PNG verification, no fonts, executables or new render execution | MIT OR Apache-2.0 |
|
||||
| .github/workflows/selene-showcase.yml | SolidCraft contributors | original read-only artifact-delivery workflow for the reviewed native SELENE export; existing pinned repository actions, no compilation, package installation, secret use or renderer claims | MIT OR Apache-2.0 |
|
||||
|
||||
| tools/render-library/verify_linear_master.py | SolidCraft contributors | original independent validation driver for native linear EXR output using the official OpenEXR Python reader as an external test oracle; no codec implementation copied or production dependency added | MIT OR Apache-2.0 |
|
||||
|
||||
## Aging, effects and backgrounds showcase, 2026-10-23
|
||||
|
||||
| File | Author | Source | Licence |
|
||||
|---|---|---|---|
|
||||
| tools/render-showcase/instrument.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
| tools/render-showcase/compose.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
| tools/render-showcase/package.py | SolidCraft contributors | original native-scene authoring, checksum-verified editorial assembly or bounded delivery tool; no third-party source or font binary copied | MIT OR Apache-2.0 |
|
||||
|
||||
| distribution/SolidCraft-Render-Showcase.zip | SolidCraft contributors and the explicitly credited original model/UI/resource authors | byte-verified selected collaborator package of reviewed native media, editable sample, recipes and public provenance; contained attribution and existing SELENE lettering notices retained; no fonts or executable binaries | MIT OR Apache-2.0 for new contributions; referenced original notices retained |
|
||||
| distribution/SolidCraft-Render-Showcase.json | SolidCraft contributors | original verified delivery archive manifest with exact SHA-256, member count and bounded scope | MIT OR Apache-2.0 |
|
||||
|
||||
+38
-6
@@ -1,8 +1,10 @@
|
||||
# Contributing to SolidCraft
|
||||
|
||||
Agents: start from the router in [AGENTS.md](AGENTS.md), which points to a short skill in [`skills/`](skills/README.md) for each kind of work. This page stays the full human guide.
|
||||
|
||||
SolidCraft is an open-source parametric 3D CAD app written in pure Rust.
|
||||
You sketch on a plane, constrain the sketch, turn profiles into solids, and
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, macOS, Linux), in
|
||||
the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can
|
||||
model with.
|
||||
|
||||
@@ -11,7 +13,7 @@ build it, how the code is laid out, the few rules that aren't negotiable, and wh
|
||||
needed most.
|
||||
|
||||
Before choosing a task, check the [command inventory](docs/features.md) and
|
||||
[roadmap](ROADMAP.md) for implemented capabilities and current priorities.
|
||||
[roadmap](docs/roadmap-detail.md) for implemented capabilities and current priorities.
|
||||
|
||||
Questions, ideas or want to say what you're working on? Join the [Discord](https://discord.gg/FtU4NQNVDH).
|
||||
|
||||
@@ -130,7 +132,9 @@ The [architecture page](docs/architecture.md) has the layer table. The short ver
|
||||
- Every call into truck goes through `solidcraft_kernel::guard`.
|
||||
- Every crash fix comes with a regression test.
|
||||
5. **Everything is a command, with a test.** A new feature means a `CommandSpec` in
|
||||
`crates/engine/src/cmd/`, engine tests that measure the result (volume, area,
|
||||
`crates/engine/src/cmd/` with input and output schemas (see
|
||||
[Adding a command](docs/book/src/dev/commands.md#adding-a-command); a test warns about a
|
||||
command without them, and will fail it once required), engine tests that measure the result (volume, area,
|
||||
face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
|
||||
6. **One set of camera controls.** Every page or canvas that shows a view (the modelling
|
||||
viewport, the Render preview, and any new page such as a flat pattern or drawing) calls
|
||||
@@ -186,8 +190,16 @@ no LTO, and debug assertions off (truck has debug-only assertions that reject va
|
||||
builds several times faster than `--release`. If a test fails only in a plain debug build, say so
|
||||
in the PR. `cargo xtask test` builds the tests with it and then runs every test binary at once
|
||||
(`cargo test` runs them one after another); arguments after `--` go to each binary, e.g.
|
||||
`cargo xtask test -- shell`. Tests run beside each other on a busy machine, so a test must not
|
||||
depend on wall-clock timing.
|
||||
`cargo xtask test -- shell`. Set `SOLIDCRAFT_TEST_JOBS` to a positive integer to opt into a
|
||||
shared bound on top-level test/side commands for `cargo xtask test` and `cargo xtask ci`.
|
||||
For example, `SOLIDCRAFT_TEST_JOBS=2` allows two commands at once; the sequential wasm/doc
|
||||
worker consumes one of those slots. Every binary and side check still runs after failures.
|
||||
Without the variable the existing concurrency is unchanged. With it set, each test binary
|
||||
inherits `RUST_TEST_THREADS` and `ACTION_MATRIX_JOBS` of `max(1, cpus / jobs)` — the machine's
|
||||
parallelism shared between the binaries in flight — unless you set either one explicitly;
|
||||
explicit caller overrides always win. Cargo's build-job setting remains an independent caller
|
||||
override; this limit does not change test filters, timeouts or baselines.
|
||||
Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.
|
||||
|
||||
**Ready pull requests test what they touch.** CI maps changed files to workspace packages and
|
||||
runs Clippy, the test build and tests for those and their dependents. The
|
||||
@@ -268,12 +280,18 @@ and a fresh CI run:
|
||||
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
|
||||
- **`mod` lines**: one per line, in the sorted group they belong to (`cargo fmt` keeps each group
|
||||
sorted). Add a new module into the existing group, not as a new group at the end.
|
||||
- **Commands without schemas** (`crates/engine/src/schema/unconverted.txt`): sorted, one id per
|
||||
line, and it only shrinks. Converting a command removes its line; a new command is never added
|
||||
to it.
|
||||
- **Languages** (`crates/ui-egui/src/i18n.rs`): one per line, English first, then by id (a test
|
||||
checks it).
|
||||
- **Generated files**: `THIRD-PARTY-LICENSES.txt` and `docs/features.md` are rewritten on `main`
|
||||
after each merge by the Generated files workflow (`cargo xtask licences`, `cargo xtask
|
||||
features`). Don't regenerate or edit them in a pull request.
|
||||
- **Design format**: one file per change, never an edit to a shared number (above).
|
||||
- **New UI strings and attribution rows**: one new file per change, `assets/locales/pending/<slug>.json`
|
||||
and `attribution/pending/<slug>.md`, instead of editing the catalogs or the ATTRIBUTION.md table;
|
||||
`cargo xtask fold-pending` folds them on `main` (see the README in each folder).
|
||||
|
||||
## Sending a change
|
||||
|
||||
@@ -325,6 +343,16 @@ Changes with no useful observable behavior do not need screenshot or video evide
|
||||
small example without private designs or sensitive details; attach the media or link to an
|
||||
accessible recording rather than committing generated review artifacts.
|
||||
|
||||
To show a picture without committing it, attach the media to the PR (it goes to the PR's own
|
||||
attachment storage and renders inline in the comment). A gist is the better home for a log or
|
||||
another text file, and you link it; a gist does not render a picture inline. The review artifacts
|
||||
stay out of the diff that way, and nothing has to be pushed to a branch of its own.
|
||||
|
||||
Attaching works in the browser for anyone who can comment: drag the file into a comment on the
|
||||
pull request. A command-line agent without a browser session usually cannot attach a file (the
|
||||
upload needs write access to this repository). Say so in the pull request, and the person running
|
||||
the agent or a maintainer can attach it.
|
||||
|
||||
Keep an eye on your PR's status, but don't push to it only to catch up with `main` (no merge of
|
||||
`main`, no Update branch button, no rebase for that alone): every push restarts the whole CI
|
||||
run for everyone waiting behind it, and `main`'s own run catches what lands in between. When you
|
||||
@@ -340,6 +368,10 @@ owning one area, with a human setting direction and reporting bugs. Their workin
|
||||
[AGENTS.md](AGENTS.md). Human contributors are just as welcome. Use whatever tools you like, as
|
||||
long as the change meets the rules above.
|
||||
|
||||
Two quick checks run on every pull request (`.github/workflows/pr-checks.yml`, not required): it
|
||||
fails when a lower-numbered open PR by someone else already closes the same issue (the label
|
||||
`not-a-duplicate` skips it), or when the description lacks the `Changes a default: no` / `Changes a default: yes — <what>, old behaviour kept behind preference <name>` line (docs-only and bot PRs are exempt).
|
||||
|
||||
## Labels and triage
|
||||
|
||||
The triage bot labels every new issue and PR (`.github/workflows/triage.yml`). It only adds labels;
|
||||
@@ -376,7 +408,7 @@ In order of how much they'd help users:
|
||||
2. **Boolean robustness.** A random-design fuzzer currently gets about half of its designs
|
||||
through without a kernel error. Each failing seed is a reproducible bug.
|
||||
3. **Drawings, CAM and mesh tools.** These are out of scope for now (see
|
||||
[ROADMAP.md](ROADMAP.md)), but anyone who wants to own one is welcome.
|
||||
[ROADMAP.md](docs/roadmap-detail.md)), but anyone who wants to own one is welcome.
|
||||
|
||||
## Updating the generated feature list
|
||||
|
||||
|
||||
Generated
+65
-98
@@ -590,9 +590,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-freecad"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a5257265c2549cf59a8457b3c7aadcc96c95abe20205232553766993a808812"
|
||||
checksum = "3c4a8fa61748cbb3ce80838b00a8cebfe553460583941f5aba6e2b97c16b4d37"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -601,14 +601,14 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"zip 8.6.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-rhino"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d88b42fb91c7e5af4c75c0433029771f7e3233a0b72ec3d0b2e5ff2c72bed07"
|
||||
checksum = "d7af41440d4c1d69e88e612de02cc3c9bad79d03429531e640ac924fef53c6bf"
|
||||
dependencies = [
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
@@ -625,53 +625,53 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-codec-step"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e83a52258c129a683f77fb360ca30af54ae34339625ad890be12621128a5811c"
|
||||
checksum = "35e1b6d17cf26b31f59f2edab8ef5c29d2a33216c581fd913f012ad4869bd559"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-container",
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
"encoding_rs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"zip 8.6.0",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-container"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c110508cd9155e905f52906de505dfd42dc70a0321f5e90b5f68c28ba207686e"
|
||||
checksum = "5ab7bcef48ac7480b4917820272d85cd2c9533e17904e15b0e4d22f19a6045bd"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"zip 8.6.0",
|
||||
"zstd 0.14.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-core"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b1147e51a74e7c52da5b393a0344597238cdc622fac6443eb2fd526759f6fa"
|
||||
checksum = "e86bb7a40521832504bb1f5953a1c706b2c52aad73bf6d84e1b35e5cb0beae3f"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"memchr",
|
||||
"roxmltree",
|
||||
"ruzstd",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
"zstd 0.14.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-ir"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71ff19f019f013d97e353bb802c5d2e253c2859aaa5422009aa933dc2f2e7dbc"
|
||||
checksum = "b570a1a8c579e407a9fd7bb10fa1900f8827f1d578b8fcb4ea2a056b5f323c72"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cadmpeg-core",
|
||||
@@ -684,9 +684,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cadmpeg-registry"
|
||||
version = "0.6.1"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66046f1a694926707bea8944f861e699964b04c68972708588f97e14ccee3c5d"
|
||||
checksum = "b2be23469a9bca0b684f1811118fffb581ebd32ded08e6cc8b0716a41f8c1377"
|
||||
dependencies = [
|
||||
"cadmpeg-codec-freecad",
|
||||
"cadmpeg-codec-rhino",
|
||||
@@ -3786,6 +3786,15 @@ version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "ruzstd"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69e4ce4a039f42bf25920b95667e4d789e98749ab11bbb8d2c2936f788421bb5"
|
||||
dependencies = [
|
||||
"twox-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -4199,7 +4208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4231,11 +4240,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4247,7 +4256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4256,7 +4265,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4273,7 +4282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"schemars",
|
||||
@@ -4287,12 +4296,13 @@ dependencies = [
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
"toml_edit",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4301,15 +4311,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"naga",
|
||||
"pollster 0.4.0",
|
||||
"robust",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-render",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4318,13 +4331,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
"cadmpeg-registry",
|
||||
"chardetng",
|
||||
"encoding_rs",
|
||||
"gethostname",
|
||||
"gltf",
|
||||
"log",
|
||||
"quick-xml",
|
||||
@@ -4336,12 +4350,12 @@ dependencies = [
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"thiserror 2.0.21",
|
||||
"zip 9.0.4",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4361,7 +4375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4370,7 +4384,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4384,7 +4398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4395,14 +4409,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4410,6 +4424,7 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"minisign-verify",
|
||||
"nix 0.31.3",
|
||||
"pollster 0.4.0",
|
||||
"proc-macro2",
|
||||
"semver",
|
||||
@@ -4425,20 +4440,22 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
"web-time",
|
||||
"winreg",
|
||||
"zip 9.0.4",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
"js-sys",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-ui-egui",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -4685,6 +4702,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
@@ -4879,6 +4897,12 @@ version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
|
||||
|
||||
[[package]]
|
||||
name = "type-map"
|
||||
version = "0.5.1"
|
||||
@@ -5960,12 +5984,15 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"syn 2.0.119",
|
||||
"toml",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6253,20 +6280,6 @@ dependencies = [
|
||||
"syn 3.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "8.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap",
|
||||
"memchr",
|
||||
"typed-path",
|
||||
"zstd 0.13.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.0.4"
|
||||
@@ -6306,52 +6319,6 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe 7.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "057cfd910cfac363a0ada849592624b4c9ff2e10bef504c3433810d78ed96f93"
|
||||
dependencies = [
|
||||
"zstd-safe 8.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "8.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdd44c6a7284e91f3717755b24315a302edd9153a01f753c3cba3d765e8eafac"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.1.1+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aeec9eaf2dffbbd09201e23bd0ffcbaa33bb8e9266a10734fd7ed90a85eca078"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.3"
|
||||
|
||||
+12
-5
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.21.0"
|
||||
version = "0.24.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -13,6 +13,7 @@ authors = ["SolidCraft contributors"]
|
||||
repository = "https://github.com/bherbruck/solidcraft"
|
||||
|
||||
[workspace.dependencies]
|
||||
robust = "1.2"
|
||||
# internal
|
||||
solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
@@ -66,18 +67,24 @@ encoding_rs = "0.8"
|
||||
# glTF 2.0 import (MIT OR Apache-2.0): the document model and GLB container only; buffers are
|
||||
# read by crates/io itself, so no image decoding or file access comes with it.
|
||||
gltf = { version = "1.4", default-features = false, features = ["names"] }
|
||||
# The computer's name in a design's "open elsewhere" lock file (MIT OR Apache-2.0; already in the
|
||||
# tree through x11rb).
|
||||
gethostname = "1"
|
||||
|
||||
# FreeCAD and Rhino import (Apache-2.0, pure Rust apart from zstd; native builds only).
|
||||
# FreeCAD and Rhino import (Apache-2.0, pure Rust; native and browser builds).
|
||||
# Pinned: its IR and codec APIs change between minor versions. Open formats only: no default
|
||||
# features, so cadmpeg's codecs for commercial formats (f3d, sldprt, inventor, catia, creo, nx)
|
||||
# and its ShapeManager/ACIS `sat` codec are not compiled in. `step` is the writer we restate through.
|
||||
cadmpeg-core = "=0.6.1"
|
||||
cadmpeg-ir = "=0.6.1"
|
||||
cadmpeg-registry = { version = "=0.6.1", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
cadmpeg-core = "=0.6.3"
|
||||
cadmpeg-ir = "=0.6.3"
|
||||
cadmpeg-registry = { version = "=0.6.3", default-features = false, features = ["fcstd", "rhino", "step"] }
|
||||
|
||||
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
|
||||
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
|
||||
|
||||
# Ends a process group with the system call (MIT; already in the tree through zip).
|
||||
nix = { version = "0.31", default-features = false, features = ["signal"] }
|
||||
|
||||
# images
|
||||
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
|
||||
png = "0.18.1"
|
||||
|
||||
@@ -46,13 +46,13 @@
|
||||
<a href="https://trendshift.io/repositories/296178?utm_source=trendshift-badge&utm_medium=badge&utm_campaign=badge-trendshift-296178" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/trendshift/repositories/296178/daily?language=Rust" alt="tangency-labs%2Fsolidcraft | Trendshift (Rust)" width="250" height="55"/></a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
*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/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.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. 6× playback with the idle pauses cut. [Full video](https://github.com/tangency-labs/solidcraft/raw/refs/heads/main/docs/screenshots/demo.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
|
||||
|
||||
## Download
|
||||
|
||||
- **Windows:** the [installer][windows-msi], or `winget install bherbruck.SolidCraft`.
|
||||
- **Windows:** the [installer][windows-msi].
|
||||
- **macOS:** the [disk image][macos-dmg] (Apple silicon and Intel).
|
||||
- **Linux:** the [AppImage][linux-appimage], or `sudo snap install solidcraft` ([Snap Store][snap-store]).
|
||||
- **Web:** [try it in your browser][web-app], no install.
|
||||
@@ -96,8 +96,10 @@ More commands, the Windows cross-build and the browser build are in [docs/buildi
|
||||
|
||||
`solidcraft-cli mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. Headless it
|
||||
runs its own design session, or with `--connect` it drives the running app so you can watch.
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Tools, access rules and an
|
||||
example session: [docs/mcp.md](docs/mcp.md).
|
||||
Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. If a terminal doesn't find
|
||||
`solidcraft-cli` (older Windows installers don't put it on `PATH`), give its full path (by default
|
||||
`"C:\Program Files\SolidCraft\solidcraft-cli.exe"`). Tools, access rules and an example session:
|
||||
[docs/mcp.md](docs/mcp.md).
|
||||
|
||||
## Scripts
|
||||
|
||||
|
||||
+63
-653
@@ -1,672 +1,82 @@
|
||||
# SolidCraft roadmap
|
||||
|
||||
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
|
||||
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
|
||||
The short version, for people who use SolidCraft. The detailed developer status (milestone
|
||||
tables, limitations, measurements) is in [docs/roadmap-detail.md](docs/roadmap-detail.md).
|
||||
|
||||
## At a glance
|
||||
## What SolidCraft is
|
||||
|
||||
| Question | Answer |
|
||||
|---|---|
|
||||
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; FreeCAD/Rhino import; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
|
||||
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
|
||||
| Reference tests | [Current case count and results](docs/oracle.md): public cases in `tests/oracle/` with analytic or FreeCAD-computed expected values, checked by the reference-test job in [CI](.github/workflows/ci.yml). |
|
||||
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
|
||||
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
|
||||
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
|
||||
SolidCraft is open-source parametric 3D CAD written in Rust. You sketch, constrain, extrude, and
|
||||
change a parameter to watch the timeline rebuild. It runs as a desktop app and in the browser
|
||||
([try it](https://solidcraft.io/app)), and it has a command-line tool and an MCP server so that
|
||||
scripts and AI agents can drive the same commands you use.
|
||||
|
||||
Optional GPU viewport ambient occlusion is available in View settings and Graphics preferences,
|
||||
with persistent settings, Performance/Quality graphics presets (Quality combines AO, Fine tessellation and adaptive 2× supersampling; manual combinations show Custom), and the
|
||||
`view.ambient_occlusion` and `view.graphics_preset` commands. See [display settings](docs/ambient-occlusion.md).
|
||||
## Works today
|
||||
|
||||
## Scope (owner decision, 2026-10-07)
|
||||
- Sketching with a constraint solver, and the usual create and modify tools.
|
||||
- Extrude, revolve, loft, sweep, coil, fillet, chamfer, shell, draft, holes, threads,
|
||||
patterns, mirror and split.
|
||||
- Parameters with units and expressions, and configurations.
|
||||
- Faces and edges keep their names through edits, so references survive changes.
|
||||
- Components, assemblies, joints, motion studies and exploded views.
|
||||
- Sheet metal with flat patterns and DXF export, and a plastic-enclosure subset.
|
||||
- A Render workspace with GPU rendering, studio lighting, materials and image export
|
||||
(first release in 0.22.0).
|
||||
- Import and export of STEP, IGES, 3MF, STL, OBJ and DXF, and FreeCAD and Rhino files.
|
||||
- Autosave and crash recovery, and the browser build of the same app.
|
||||
|
||||
The issue batch adds preservation of blind pockets in through cuts, associative projected
|
||||
offsets and plastic face placements, XZ (+Y normal) with a v6 migration preserving older
|
||||
designs, interactive angle placement, file-picker sketch imports, Home/recent controls,
|
||||
toolbar overflow, and the remaining component-frame and assembly WIP work. The enclosure
|
||||
acceptance scenarios now include board resizes; STEP reopening is automated. See [WIP reconciliation](docs/wip-reconciliation.md).
|
||||
Face-based Extrude Join (including outward Press Pull) now defaults to the picked body,
|
||||
so edge-touching assembly neighbours stay separate regardless of body order (#226).
|
||||
The full list of commands is in [docs/features.md](docs/features.md). Release notes for each
|
||||
version are on the [releases page](https://github.com/tangency-labs/solidcraft/releases).
|
||||
|
||||
In scope, in priority order:
|
||||
1. SOLID and SKETCH (now).
|
||||
2. Parameters / functional constraints (done: see `parameters.*`, `expr.evaluate`; unit-aware,
|
||||
cycles reported with their path, rename follows references, deletes list their users):
|
||||
user parameters with units and expressions (+-*/, ^,
|
||||
parentheses, sin/cos/sqrt/min/max/floor/ceil/round, unit conversion), model parameters
|
||||
d1, d2… for every dimension and feature input, references between parameters with a
|
||||
dependency graph and cycle detection, re-evaluation on change, comments, favourites,
|
||||
CSV/JSON import and export.
|
||||
3. Components and assemblies with joints (M9).
|
||||
4. SHEET METAL: flange, contour flange, bend, unfold/refold, flat pattern with DXF export, sheet
|
||||
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.
|
||||
## In progress now
|
||||
|
||||
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.
|
||||
Each feature below is a [milestone](https://github.com/tangency-labs/solidcraft/milestones) holding
|
||||
the issues for it, so the count shows how far along it is. Counts are as of 2026-10-25, one per
|
||||
feature or request. Work merged on a feature branch (`drawings`, `render`) counts as built but
|
||||
reaches the app only when that branch merges. They are rough, not a promise.
|
||||
|
||||
## Milestones
|
||||
- **[Rendering & materials](https://github.com/tangency-labs/solidcraft/milestone/17)**: in progress, 9 of 34 pieces done, 3 of them built only on the `render` branch and in the app when it merges. The basic Render workspace is already in releases (0.22.0); the interface rework (#6281) and the material and light libraries are what is left.
|
||||
- **[Drawings](https://github.com/tangency-labs/solidcraft/milestone/8)**: in progress, 3 of 21 pieces built (2 in releases, 1 on the `drawings` branch), and 3 more partly built there (sheets and borders, bill-of-materials types, the paper scene). Not in the app yet: it lands when the `drawings` branch merges. Today you can export the current view as a DXF.
|
||||
- **[Sheet metal](https://github.com/tangency-labs/solidcraft/milestone/15)**: in progress, 1 of 30 requests done. Flanges, hems, unfold and flat patterns already work; the 30 are extensions, mostly the per-edge flange options (segments, miters, partial flanges, reliefs) and bend allowance.
|
||||
- **[Sketching](https://github.com/tangency-labs/solidcraft/milestone/14)**: in progress, 12 of 50 pieces done. Dimension types, selection and snapping options.
|
||||
- **[Solid modeling](https://github.com/tangency-labs/solidcraft/milestone/2)**: in progress, 9 of 50 pieces done. Blends, holes, threads and patterns.
|
||||
- **[Assemblies & joints](https://github.com/tangency-labs/solidcraft/milestone/3)**: in progress, 4 of 19 pieces done. Joints, motion studies and exploded views already work; placement rules, linked sub-assemblies, fastener stacks and weldment corners are the pieces left.
|
||||
- **[Import & export](https://github.com/tangency-labs/solidcraft/milestone/5)**: in progress, 7 of 39 pieces done. FreeCAD, SolidWorks and point-cloud import, and mesh export options.
|
||||
- **[AI agents & MCP](https://github.com/tangency-labs/solidcraft/milestone/18)**: in progress, 2 of 8 pieces done. Agent chat and the MCP server.
|
||||
- **[Interface, web app & plugins](https://github.com/tangency-labs/solidcraft/milestone/4)**: in progress, 31 of 96 pieces done. Ribbon redesign, navigation, themes, the web app and plugins.
|
||||
- **[SMLib kernel migration](https://github.com/tangency-labs/solidcraft/milestone/1)**: in progress, 1 tracking issue open. The kernel is moving from truck to NVIDIA's SMLib (usd-brep, Apache-2.0) on the `kernel-smlib` branch; it is not in releases yet. Tracked in [#4012](https://github.com/tangency-labs/solidcraft/issues/4012).
|
||||
|
||||
| # | Milestone | Status | Hours left |
|
||||
|---|---|---|---|
|
||||
| M0 | Vertical slice | done | — |
|
||||
| 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, Shift-picked line midpoints in Horizontal/Vertical using persisted points and ordinary constraints (#2087), 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; tiny rim edges remain selected instead of nearby wall seams when filleting (#2498) | in progress | 30 |
|
||||
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), FreeCAD/Rhino solids through cadmpeg (done, desktop and CLI), 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, 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; responsive feature history with width-driven icons/names/title, snapping width resize, bottom/left/browser-stack docking, shared-column collapse, rollback bar, multi-selection and persistent folders), 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, extrude taper), 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 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), 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 (#772). Done: Patch (sketch regions; edge loops filled G0, plus `crown`), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane, a face or sketch curves swept onto the surface, keeping a side or the picked regions; mutual trim of two surfaces, extended to meet and stitched, #767), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis, Surface Extrude/Revolve/Sweep/Loft from open and closed curves, Offset, Unstitch and Reverse Normal; the continuity-driven core (`kernel/src/bridge.rs`, G0–G3 Hermite rows) with **Blend** (`surface.blend`, #764: edge chains along one or several faces, closed rims paired seam to seam or from picked seams, a dialog), **Match** (`surface.match`), **Boundary** (`surface.boundary`, #763 — two sketch curves exact with G0–G3, three or four as the flat patch of their loop, and curve networks: Coons and Gordon surfaces through sketch, 3D-sketch or edge curves, a point for a three-sided side, G1/G2 against the faces along one direction's edges, joined into their body on request; G1/G2 on two sides that meet at a corner waits for the Gregory-type corner), **Fair** (`surface.fair`, CV fairing), **continuity analysis** (`inspect.continuity`, #770; a toolbar tool with edge markers and a legend, and curvature combs along face isocurves) and a multi-section **loft** with per-end continuity and crown, and guide rails on solid and surface lofts (one or two, scaling or stretching the sections between; #201). Also in the Class-A pass: G1…G4 blend curves in the sketch, chord and variable-radius fillets on curved edges (with honest refusals where the fit cannot hold), and a G2 corner patch where filleted edges meet (kernel; the command refuses until the patch's sections can be sewn to the blends' contacts). Left: bias/form-factor fillet sections (design recorded), a Gregory-type corner, surface Fillet between separate surface bodies (#769), Ruled (#762), Untrim (#761), curved-face Extend (#760), and every remaining #757–#772 item | 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, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), cams (`joint.cam`: a revolute cam drives a slider or revolute follower along a closed sketch profile or the cam's edges, knife, roller or flat followers, worked out again when the profile changes; drives, studies, closed loops, contact sets), 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), motion analysis (joint values and a point's path charted over a study as value, velocity and acceleration; the parts that collide along a study, with the steps where they do), 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, Cam 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 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 |
|
||||
## Next
|
||||
|
||||
M6 navigation preferences include **Touchpad**: Shift + pointer movement pans, Alt/Option +
|
||||
pointer movement orbits without a mouse button, and vertical scrolling zooms. Other presets
|
||||
retain their saved two-finger scroll preference; camera motion is available through
|
||||
`view.navigate` and modifier-aware `ui.move` automation.
|
||||
- **[Surfacing](https://github.com/tangency-labs/solidcraft/milestone/16)**: next, 6 of 14 pieces done. Patch, trim, blend and match exist; the rest of the surface toolset is planned.
|
||||
- **[Manufacturing / CAM](https://github.com/tangency-labs/solidcraft/milestone/9)**: next, 1 pieces tracked, none done. There is no CAM in SolidCraft yet. Send to Slicer and a first toolpath slice are in review; proposals are [#1153](https://github.com/tangency-labs/solidcraft/issues/1153) and [#1621](https://github.com/tangency-labs/solidcraft/issues/1621).
|
||||
|
||||
UI redesign (#2016): the File/start view (#2027) uses shared light/dark navigation,
|
||||
action tiles, recent rows and sample cards, with recovery review and Save As/Export
|
||||
entry points. Document loading, sample caches and recovery safety retain their
|
||||
existing command paths.
|
||||
Features are not tied to a release or a date. Work lands in the daily releases as it is finished.
|
||||
Anything without a milestone, such as bug fixes, is not a feature and is not listed here.
|
||||
|
||||
M11 precursor: File → Export Current View as DXF exports shown placed bodies and sketches from the current
|
||||
camera as a 2D millimetre drawing, with hidden edges omitted or dashed. Vector occlusion splits
|
||||
partly hidden edges and includes curved silhouettes. Repeated part meshes and triangle
|
||||
hierarchies are shared; bounded desktop preparation and occlusion use up to eight workers.
|
||||
Curves are approximated; drawing sheets,
|
||||
annotations and exact projected curves remain deferred. See [view DXF](docs/view-dxf.md).
|
||||
## How releases work
|
||||
|
||||
Drawing foundations: [RFC #860](https://github.com/tangency-labs/solidcraft/issues/860)
|
||||
and the [foundation ledger](docs/drawing-foundations.md) track the bounded implementation
|
||||
packages. The first geometry increment defines validated section records in a plane-local
|
||||
frame; the ledger separates that contract from section producers and the Drawing workspace.
|
||||
- Releases ship daily, as numbered versions (v0.22.0, v0.22.1) with nightly pre-releases in
|
||||
between. They are delivered through the in-app updater.
|
||||
- SolidCraft tells you when a newer stable release exists and never updates without asking.
|
||||
Where it can replace itself safely (AppImage, Windows installer and portable builds) there is
|
||||
an **Update now** button; downloads are checked against the release's SHA-256 list. See
|
||||
[updates and privacy](docs/book/src/guide/updates.md).
|
||||
- Design files carry a format number. A new version opens designs from older versions, but an
|
||||
older version refuses a design saved by a newer one (0.22.0 writes format 35, and 0.21.0 and
|
||||
earlier will not open it). Each release's notes say when the format changed. Keep a copy of
|
||||
an important design before you move to a new version.
|
||||
|
||||
Desktop view DXF export runs in the file-operation worker with five-stage progress and
|
||||
cooperative cancellation. Curve tolerance (0.0001–1 mm, default 0.01 mm) is remembered alongside
|
||||
the hidden-edge setting. Cancelled exports preserve the model and the previous destination;
|
||||
completed drawings publish atomically. The browser still exports synchronously.
|
||||
## Licence
|
||||
|
||||
## Interactive modelling
|
||||
SolidCraft is licensed MIT OR Apache-2.0, at your option; see [LICENSE-MIT](LICENSE-MIT) and
|
||||
[LICENSE-APACHE](LICENSE-APACHE). Versions already released stay available under those licences
|
||||
permanently.
|
||||
|
||||
- Modelled threads preview their cut before OK using the background worker on desktop.
|
||||
Browser users apply the cut with OK to avoid blocking on every edit.
|
||||
## How to follow along
|
||||
|
||||
- Thread resolves a suggested metric size when a cylindrical face is picked. Diameter and
|
||||
pitch selectors expose the existing metric designation parameter; custom designations stay
|
||||
editable, and Automatic size returns to the face's suggestion. Cosmetic/modelled and
|
||||
full/partial-length options remain available. This does not add tolerance classes or
|
||||
left-hand threads.
|
||||
- New Body extrusion and face-offset previews show completed intermediate geometry while a
|
||||
drag continues, then catch up to the exact final value. Changes to the selected geometry,
|
||||
operation or feature still discard obsolete results. Distance arrows and their value boxes
|
||||
follow the current frame's input.
|
||||
- Large fine modelled threads use pitch-based helix resolution and tighter faceted
|
||||
reconstruction fallbacks, preserving thin crests and closed shells on large shafts.
|
||||
|
||||
## 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).
|
||||
|
||||
- Directional elasticity (#994) queries the validated card in material-local axes
|
||||
through read-only `material.properties.directional`, with source/condition review.
|
||||
It does not assign body frames, predict structural behavior or enable a solver.
|
||||
|
||||
## Library foundations (#785, #786, #787)
|
||||
|
||||
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
|
||||
selected appearance/material revisions into existing design definitions in one undo step.
|
||||
Appearance offers local loading/search/export; physical materials use existing commands.
|
||||
This slice is independent of the render stack and does not change the document format.
|
||||
See [the contract and limits](docs/book/src/dev/local-libraries.md).
|
||||
|
||||
- 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
|
||||
|
||||
- Explicit texture normalization (#1218) produces completed float RGBA with
|
||||
role-aware linear color/data semantics, bounded matrix/TRC ICC conversion and
|
||||
opt-in EXIF orientation. It retains high-precision raw samples and records lossy
|
||||
gamut clipping only when explicitly requested. It is not a renderer/resource
|
||||
consumer; see [the supported policy and limits](docs/render-texture-normalization.md).
|
||||
|
||||
- Opt-in opaque PBR (#1046) connects persisted surface parameters to the integrated
|
||||
product viewport and CPU PNG, using a fixed direct-light studio. Geometry and mass
|
||||
stay unchanged. HDRI, transmission and textures remain separate gates.
|
||||
See [the consumer and limitations](docs/render-opaque-pbr.md).
|
||||
|
||||
- Combined integration is tracked in #842, with independent scalar libraries in
|
||||
#863 and richer PBR/material-card transport in #1086. See [phase boundaries and validation gates](docs/render-integration.md).
|
||||
Render/library consumers use the merged asynchronous picker (#1127). Material-card
|
||||
UI (#1094), texture mapping and HDRI lighting remain explicit follow-ups.
|
||||
|
||||
- R4a adds bounded CPU product-image jobs, cancellation and atomic PNG export through
|
||||
engine commands, the Render panel, CLI and persistent MCP. Immutable captures and
|
||||
results stay outside persistence/undo; native workers and cooperative browser tasks
|
||||
share the same work protocol. See [jobs, limits and export](docs/render-jobs.md).
|
||||
|
||||
- R3 adds an overlay-free product scene and the integrated Render workspace, with
|
||||
runtime GPU host capabilities, per-document drafts and named camera/background
|
||||
controls. Design keeps its camera and drawing policy. Raster transparency remains
|
||||
approximate; PBR and environments are later slices. See
|
||||
[workspace and scene contracts](docs/render-workspace.md).
|
||||
|
||||
- R2 adds bounded named render setups in document format 21 and the read/save
|
||||
`render.settings.get/set` commands. Proven render-only changes preserve geometry
|
||||
and prepared world data through undo/redo and checkpoint restore. This slice does
|
||||
not add a Render workspace, PBR, environments or image jobs; see
|
||||
[settings and refresh policy](docs/render-settings.md).
|
||||
- Bounded PNG/JPEG texture admission (#900) validates in-memory sources and returns
|
||||
completed RGBA8 pixels with explicit color/data roles. It is an input utility only:
|
||||
ICC/16-bit normalization, local wood mapping, resource persistence and the renderer
|
||||
consumer remain separate work. See [limits and licensed-file probes](docs/render-texture-input.md).
|
||||
|
||||
- The initial opaque surface numerical contract for #1004 provides bounded linear
|
||||
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
|
||||
The numerical module alone advertises no capabilities; the installed consumer above
|
||||
provides rendering. See
|
||||
[the model, evidence and remaining consumer gates](docs/render-surface.md).
|
||||
|
||||
- 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.
|
||||
The legacy Design snapshot route remains unchanged. Later slices above provide the
|
||||
workspace/jobs and opaque PBR; HDRI 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
|
||||
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
|
||||
Both tools retain full replies by default.
|
||||
|
||||
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
|
||||
Polygon and Spline. Each open part remembers its last interactive choice per family
|
||||
across sketch sessions and document-tab switches; closing the part resets these UI choices.
|
||||
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
|
||||
placement, including orientation and the selected side count.
|
||||
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
|
||||
dimension values, so line type can change while a shape is in progress.
|
||||
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
|
||||
committed geometry; centerlines retain their dash-dot pattern.
|
||||
|
||||
- Point has a default Sketch CREATE toolbar button alongside the existing drawing tools.
|
||||
Saved custom layouts are preserved; Reset Toolbar restores the default Point button.
|
||||
- Construction planes cannot take an origin-plane name (XY, XZ, YZ, Top, Front, Right). Plane
|
||||
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 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.
|
||||
- Circular-pattern and angled-plane axes reject explicitly supplied malformed or expression
|
||||
origins instead of silently rotating about zero. Numeric origins remain in millimetres;
|
||||
associative axis expressions and construction-axis references remain unsupported.
|
||||
- Single-line and rectangle-side dimensioning supports selecting a line or its two endpoints.
|
||||
Type a value and press Enter to commit one undo step; rejected values remain editable and
|
||||
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
|
||||
and navigation controls, including enlarged displays and nested pending placements.
|
||||
- Sketch Dimension takes a line and a circle or arc (#1803): it measures from the line to the
|
||||
circle's edge (the gap, 0 when tangent, or how far the edge reaches past a line that cuts
|
||||
it), drives it like any other dimension and keeps the circle on its side of the line.
|
||||
- Sketch annotations, live drawing lengths and Measure use document units. Inches show
|
||||
three decimals and retain a meaningful fourth digit. Bare length dimension entry captures
|
||||
the current design unit; changing units leaves existing geometry and expressions intact.
|
||||
Component/canvas distance controls, sketch cursor coordinates, flat-pattern dimensions
|
||||
and pocket depths, interference volumes and parts-library lengths also use design units.
|
||||
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
|
||||
consumes complete point/curve selections consistently; mixed or ambiguous selections
|
||||
retain the picking workflow. Rejected constraints preserve selection and undo history.
|
||||
- Contextual error reporting: a **Create Issue** action beside feature errors opens a
|
||||
reviewable GitHub issue draft with the error, operation settings, build diagnostics and
|
||||
useful reproduction context. The draft is editable, with optional capture of the actual
|
||||
viewport and copy/save controls. Opening GitHub does not submit the issue. Help also exposes
|
||||
reporting for unexpected results that did not produce an error.
|
||||
- The header exposes **Report a bug**, including an accessible compact control on
|
||||
narrow windows. It opens the same editable report and screenshot review without
|
||||
changing the active model or a pending dimension.
|
||||
- Crash reports: every panic (in a command, in the kernel, anywhere else) and every unclean
|
||||
exit (abort, stack overflow, kill: found at the next start through a locked per-run command
|
||||
journal) leaves a local report with the backtrace, version and commit, the command and the
|
||||
design that reproduce it (`solidcraft-cli crashes replay <id>`). Repeats are counted, not
|
||||
rewritten; at most 20 reports, 50 MB and 90 days. The app shows them after a crash (Help >
|
||||
Crash Reports…) with a reviewed issue draft; agents read them through
|
||||
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
|
||||
anywhere. See [crash reports](docs/crash-reports.md).
|
||||
- Agent chat (off by default, desktop): a side panel that drives a coding agent the user
|
||||
already has (Claude Code through its ACP adapter, Codex, Gemini CLI, OpenCode, Grok Build, …) over the
|
||||
Agent Client Protocol, under the user's own login; SolidCraft stores no keys. The agent works
|
||||
through SolidCraft's MCP tools on a gated endpoint of its own: file actions wait for Allow or
|
||||
Deny, crash reports are off limits, 24 changes per message, one undo step per message, and
|
||||
the agent's whole process group ends with the panel or the app. Docks left or right,
|
||||
minimises to a strip. Grok Build discovery includes `~/.grok/bin` and reuses the user's
|
||||
Grok login. See [agent chat](docs/agent-chat.md).
|
||||
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
|
||||
writes a design's history as a script that `eval` and `run` replay into the same design —
|
||||
user parameters, units, sheet metal and plastic rules and materials as their own commands,
|
||||
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
|
||||
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
|
||||
script after a step gives the design at that step. Appearances, folders, views and
|
||||
configurations are listed as left out; components and joints are not supported yet.
|
||||
|
||||
## 0.2.0 release batch
|
||||
|
||||
- Includes the new axis navigation, adaptive sketch drawing/picking, origin-corner repairs,
|
||||
durable supported profile/face references, endpoint dimensioning and contextual reporting.
|
||||
- Smooth ruled-wall display joins are hidden only within the supported geometric certificate;
|
||||
authored splits and actual creases remain protected. Wireframe retains physical edges.
|
||||
- Paste New uses a bounded current-geometry comparison for supported convex line/circle
|
||||
prisms with separated circular holes; unsupported shapes retain the existing strict check.
|
||||
- Common-bore Union retries check additional boundary curves before recutting the bore,
|
||||
preventing the reproduced thin-web loss. Unknown additional trims refuse; inherited
|
||||
common-cylinder trims retain their existing policy, so this is not a general Boolean proof.
|
||||
- Exact full-path assembly joint editing remains separate work and is not part of this batch.
|
||||
- Stable 0.2.0 is published: all release payload checksums, native cold reopening, deployed
|
||||
web app and both Flatpak architectures were verified. The replacement README demo uses
|
||||
the original electronics enclosure story and animated pointer in the actual English
|
||||
0.2.0 UI, at 4× playback. Publication follows [the release workflow](docs/releasing.md).
|
||||
|
||||
## 0.2.1 critical fixes
|
||||
|
||||
- Separated multi-profile Join/Cut extrusions into one solid target batch their tools using
|
||||
conservative construction bounds, avoiding a Boolean rebuild for every profile. Live
|
||||
extrusion previews build only their tools; confirmation validates the final Boolean.
|
||||
- Coplanar selected body faces share one linked sketch and one extrusion. Automatic Join/Cut
|
||||
keeps the evaluator's participant scope, including parts of an empty imported component. Desktop OK/Enter
|
||||
computes on a scratch session in the background, with atomic acceptance and cancellation.
|
||||
Planar section stitching projects lines/arcs analytically and removes duplicate endpoints.
|
||||
- Imported rotated face groups can check all full start-face seams in one guarded
|
||||
construction. Separated tools may batch a Join when each meets the same single body;
|
||||
nearby uninvolved bodies retain the original contact decisions. Regression comparisons
|
||||
cover all four extrusion operations.
|
||||
- Inward multi-face cuts retain every eligible target and subtract separated tools together.
|
||||
Safe interior classification handles roundoff at shared imported line/arc endpoints.
|
||||
Straight extrusion feedback caches existing face/profile triangles and updates the display
|
||||
directly, including a newly opened STEP's single identity component placement; other
|
||||
extents, moved or repeated components and curved sketch profiles keep the Engine preview.
|
||||
- The extrusion fixes retain tracked Boolean history and the current `solidcraft` packages.
|
||||
Confirmation and tool previews use main's shared background calculation. Main's collapsed
|
||||
blend-wall refusal and its tests are retained, including smaller valid radii and mirrored parts.
|
||||
- Axis view and navigation controls stay anchored to the viewport when feature dialogs or
|
||||
the command palette open, at normal and enlarged display scales. Fit still accounts for
|
||||
dialog coverage; covered controls do not receive clicks through dialogs.
|
||||
- Eligible planar faces survive fillet, chamfer and shell operations with exact source
|
||||
certificates. Sketches placed directly on the enclosure wall and inner floor follow
|
||||
width and wall changes, including after the USB cut and cold reopening.
|
||||
- Remove artificial straight-wall and rounded-corner subdivisions left by shell and
|
||||
extrusion cuts. Identical generated wall segments span adjacent slabs as one physical
|
||||
face, with exact full-height supports and boundary stitching. Preserve authored face
|
||||
splits and face appearances. Permanent regressions cover material, STEP, reopening and
|
||||
unaffected bodies across the extrusion end plane.
|
||||
- Feature dialogs use the available screen height for their fields, with OK and Cancel
|
||||
outside the scrolling region, including enlarged display scales.
|
||||
- Design format 13 records shell-face attachments. Older designs remain readable;
|
||||
newly saved designs require 0.2.1 or later. Ambiguous or unsupported faces still refuse
|
||||
attachment rather than guessing a plane.
|
||||
- Saved projections from a removed strip retain their geometry and report the lost
|
||||
reference. Explicitly reselect the wall to repair that projection.
|
||||
|
||||
## Workspace layouts (#2029)
|
||||
|
||||
- General preferences save Floating or Compact using the universal UI containers.
|
||||
Compact joins the workspace selector and ribbon, docks the content-hugging Browser
|
||||
flush beneath them with a square join, and opens tool sheets below the compass.
|
||||
Sheets remain draggable, rounding all corners away from their right dock; footer
|
||||
backgrounds follow the same corners. A separate preference hides ribbon icon labels
|
||||
for a shorter ribbon in either layout.
|
||||
Assistant keeps the outermost edge and History keeps its chosen dock. Switching
|
||||
retains the active tool, camera, selection and design; system windows and menus
|
||||
keep their existing behavior.
|
||||
|
||||
## View navigation
|
||||
|
||||
- Navigation has an opt-in Free orbit preference for mouse and 3D-mouse rotation through
|
||||
both poles with roll. It defaults off, preserving existing preset snapping and pole limits.
|
||||
The SolidWorks face menu offers Normal to Selection (`view.normal_to`), shared with Look At
|
||||
and Ctrl+8, including repeat-to-flip, preserving scale and projection.
|
||||
|
||||
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
|
||||
its layout stable during a drag and applies the selected scale on release.
|
||||
|
||||
- OpenSCAD navigation preset: left drag orbits, right/middle drag pans, Shift+right/middle
|
||||
drag zooms, and the wheel zooms. Available in the shared desktop/web Navigation preferences.
|
||||
- Compact clickable signed X/Y/Z axis tips, smooth drag-to-orbit, and Shift-orbit snapping
|
||||
to 26 standard and diagonal views. Pointer motion accumulates independently of a lightly
|
||||
damped spring, with a boundary margin to prevent flicker. Z-up/Y-up and the nearest square
|
||||
top/bottom heading are preserved. Diagonals include 45-degree edge and isometric corner views.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Scheduled and manual CI scan locked dependency advisories, bans and sources with
|
||||
cargo-deny. Vulnerabilities fail the scan; unmaintained crates warn. Local vendored
|
||||
patches require separate upstream review because advisory databases cannot version-match
|
||||
their path dependencies. Pull requests retain the existing licence checks.
|
||||
|
||||
- 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 planar
|
||||
end face at each end, including sloped ends (#2708). The UI and `solid.fillet` expose
|
||||
`curvature` (alias `g2`); the size is an equivalent circular setback. Curved faces/edges,
|
||||
multi-face 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); perpendicular corner fillets after a closed ring of earlier
|
||||
rounds preserve the ring's radii and the enclosed planar face (#1557);
|
||||
mitres between separate rounds of the same radius keep their endpoint tangent, so a later
|
||||
fillet can select an upright and its adjoining bottom mitre as one chain (#2422);
|
||||
straight edges (convex or concave) between planar faces with perpendicular
|
||||
planar end faces (equal-distance chamfers also meet sloped planar end faces, including inner
|
||||
basin rims within the rim width); convex fillets also meet sloped planar ends and reflex
|
||||
corners around pocket rims, with unequal wall slopes and extensions confined to the face;
|
||||
connected equal-distance chamfers also trim each other across unequal sloped rim walls;
|
||||
curved chamfer chains with straight planar ends can terminate across two existing planar
|
||||
bevel faces, splitting their shared seam; picks on the same tangent chain are applied once (#1303);
|
||||
whole smooth loops of a planar face, picked whole or by one edge (tangent
|
||||
chain), including circles and walls at any angle (boss bases and tops, bores, pocket floors,
|
||||
plate outlines), loops with sharp corners between straight or mixed arc/line edges (straight
|
||||
blends meet in mitres; perpendicular curved walls use offset intersections, with D-rim removal
|
||||
checked within 0.1% of analytic offset slices), runs of
|
||||
neighbouring edges along a planar face's loop (two top edges of a box: mitred, ending square;
|
||||
with the edge between them too, the corner becomes a sphere octant),
|
||||
rounded corners rounded again (sphere octants, tori at concave corners); every edge of a
|
||||
convex planar body or of an extruded part; fillets inside patterns; closed chains of curved
|
||||
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); 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. A limiting blend that collapses a side wall into coincident boundary
|
||||
curves is refused as too large rather than returning an unmeshable face (#1113).
|
||||
- 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;
|
||||
curved wall qualification rejects false transverse axes instead of filling holes.
|
||||
Bodies touching along one whole face
|
||||
(a part and its mirror image) are stitched; coincident planar faces are pushed apart when
|
||||
their neighbours are planes or cylinders. Other coincident curved configurations can still
|
||||
fail. Bounded containment classification preserves fully internal cavities and nested
|
||||
material islands independently of shell order, with STEP round-trip and material-membership
|
||||
regressions. Near-contact curved shells use the general boolean path rather than a
|
||||
containment shortcut. Spheres are built from six pole-free patches.
|
||||
- Mass properties come from fine tessellation (curved faces within ~1e-4 relative); triangles
|
||||
that cut through curved faces (trimmed faces after booleans) are split onto the surface first.
|
||||
- STEP import (our own reader, crates/kernel/src/step_in) reads solids (B-rep with analytic and B-spline geometry), units,
|
||||
product names, colours and assemblies; analytic offsets on planes, cylinders, cones, spheres
|
||||
and regular tori are read exactly (signed/nested distances, units and face sense). Freeform
|
||||
offset surfaces and pcurve-only edges are not read yet,
|
||||
and assembly components are flattened into bodies. Closed periodic faces (torus, B-spline
|
||||
bands) are split in two for meshing; measure() uses a finer tolerance for small radii.
|
||||
- Persistent naming follows the kernel's exact face history (docs/book/src/dev/naming.md):
|
||||
references resolve by name first, follow splits and merges, and warn when the entity split,
|
||||
merged or vanished, or was found by geometry only (operations that report no history yet).
|
||||
New planar sketch attachments use exact box, extrusion cap/side and immutable imported-face
|
||||
source certificates with checked target and author occurrence paths. A missing or changed
|
||||
source fails explicitly; Redefine Sketch Plane previews reattachment or a fixed plane while
|
||||
retaining current geometry and dimension identities. Unsupported modified faces need an
|
||||
explicit fixed plane. Legacy attachments retain their original resolver behavior.
|
||||
- Modelled threads are cut as a faceted body (curved faces within 1/200 of the pitch); Extrude
|
||||
and Revolve To Object on a curved face cut the tool back to the target's surface.
|
||||
- Explicit sketch breaks preserve exact ellipses, fit/control splines (including high degrees)
|
||||
and conics; analytic trim/break also sees free-form cutters. Free-form curves cut at a tangent
|
||||
touch (no clean crossing) enter that profile as polylines.
|
||||
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
|
||||
A sweep with a twist, an end scale or a parallel or direction orientation (#771) follows its
|
||||
path that way too, even along sketch lines and arcs: B-spline walls through stations, ruled
|
||||
walls through denser stations where the path has corners or the profile has holes.
|
||||
Guide rails (one or two, #771) do the same: each station meets every rail once, and the rail
|
||||
turns the profile and scales it (or stretches it across to the rail) against the start; two
|
||||
rails carry it between them. Rails along a path with sharp corners are refused for now.
|
||||
- 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.
|
||||
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.
|
||||
Newly captured adaptive profile references with complete source coverage retain their outer
|
||||
and hole identities through span-count changes, including parameter edits after extrusion.
|
||||
Partial, intersected or ambiguous boundaries do not qualify for these whole-source references.
|
||||
Legacy curve-span multisets remain exact and unchanged; already-lost selections may need
|
||||
reselection. Format 12 stores the new bounded whole-boundary references.
|
||||
Projected analytic boundaries follow nested loop identities through resizing, including
|
||||
stationary holes and rounded corners. Supported convex, tangent line-and-arc boundaries
|
||||
can lose or regain corner arcs during an inset while preserving reserved curve identities
|
||||
and linked profile selections. Format 12 saves those associations; compatible older closed
|
||||
offsets upgrade structurally without changing their curve IDs. Breaking an association
|
||||
freezes its current boundary and materializes downstream profile references. Child offsets
|
||||
of a boundary with retired arcs still require breaking its association first; mixed or
|
||||
nonconvex junctions and retained references to disappearing individual arcs remain limited.
|
||||
- Extrude and Revolve editors retain original direction, extent expressions, explicit targets,
|
||||
axes, From/To inputs and All-profile intent. No-op edits preserve downstream dimension names
|
||||
and selected occurrence frames; cancel and successful edits preserve one-step undo history.
|
||||
Placed feature edits preview through the edited timeline entry, and cylindrical Emboss guides
|
||||
use the sketch's actual target, author or uniquely placed third-component frame.
|
||||
Revolve rotators follow the actual signed axis and support symmetric and two-sided extents;
|
||||
dragging a To Object extent converts its derived value to an explicit angle. Legacy reversed-axis
|
||||
symmetric geometry is preserved through format 11's explicit legacy convention. New revolves
|
||||
use the sketch-normal direction for each selected profile, including To Object and curved
|
||||
target trimming, independently of the selected axis sign.
|
||||
- Cylindrical emboss/deboss wraps bounded orthogonal line profiles, including holes, concave
|
||||
outlines and nested islands, onto complete exterior cylinders or coaxial sleeves with exact
|
||||
radial walls. New features infer a unique eligible cylinder when the face field is empty;
|
||||
ambiguous targets require an explicit face. Saved target
|
||||
and author occurrence paths retain the chosen placements; missing/split faces refuse rather
|
||||
than retarget. General free-form profiles, seams, stepped cylinders and other curved surfaces
|
||||
remain unsupported. STEP round-trip and independent radial/volume checks cover this subset.
|
||||
- Shell and offset faces work on bodies of planes, cylinders, cones, spheres and tori (convex or not:
|
||||
filleted boxes, bosses, domes, countersinks; corners where four or more planes meet, such as a
|
||||
floor falling to a drain, as long as the corner's edges are all convex or all concave); draft on convex planar bodies and on walls (planes, cylinders → cones) between caps; lofts through three or more sections are smooth (B-spline sides through every section), two
|
||||
sections ruled. Circular-meridian barrels retain curved inner walls through either axial
|
||||
opening (#2421). Solid lofts preserve holes through sections with matching hole counts
|
||||
(#2435), including inner walls and caps with openings; a hole cannot appear or disappear
|
||||
between sections, and sections with holes cannot collapse to a point.
|
||||
|
||||
- Sketch corner tools: fillet and all three chamfer modes pick a connected corner even when
|
||||
its endpoint overlaps the fixed origin or a loose point. Ordinary drawing still snaps to
|
||||
the origin; clicked UI regressions verify geometry, closed profiles and undo.
|
||||
- Sketch display: drawing, picking, highlighting, tool previews and profile shading share
|
||||
adaptive projected curve boundaries. Fit, zoom, display scale and placed sketch frames
|
||||
invalidate the display cache without changing CAD geometry or timeline history. Work
|
||||
budgets and clipped paths prevent unbounded sampling or invented edges across the eye plane.
|
||||
The Sketch Palette scrolls its options on short windows and enlarged displays while
|
||||
keeping constraint status and Finish Sketch visible.
|
||||
- Face projection: closed boundaries have a deterministic outer-first order. Refresh matches
|
||||
existing geometry before assigning positions, retaining saved curve IDs and constraints
|
||||
across enumeration changes and resize, including concentric circles. Ambiguous identities
|
||||
retain the last geometry and report a lost reference; permutation matching is bounded.
|
||||
- Project accepts complete supported preselection in one undo step. Visible external sketch
|
||||
curves and points retain their source sketch identity; model geometry, axes and planes use
|
||||
the existing linked projection commands. Rejection preserves the document and selection;
|
||||
mixed selections retain ordinary tool-first picking.
|
||||
- Inline sketch dimensions: Tab and Shift+Tab cycle within the active shape's value boxes,
|
||||
selecting visited values for replacement. Expression completion retains forward-Tab
|
||||
priority; real keyboard regressions cover rectangles, lines and single-value circles.
|
||||
Typed lengths, radii, line angles, signed arc sweeps and slot lengths persist as constraints
|
||||
and expressions through save/reopen. Shape creation and its dimensions form one undo step;
|
||||
rejected values keep the editable field focused.
|
||||
- Repeated components: Browser selection, visibility, properties, copy and Move/Copy use
|
||||
checked occurrence paths. Active nested occurrences author planar sketches and solids in
|
||||
their selected frame. Pending moves compose through parents; dialog cancellation restores
|
||||
its selected move. Source materials and body/face appearances follow transformed instances.
|
||||
Shared nested definitions remain shared. Deep Paste New copies reachable definition graphs,
|
||||
independent feature/input histories, styles and internal motion records, with bounded geometry
|
||||
checks and explicit external-link notices. Paste New selects the copy while preserving the active
|
||||
authoring context. Imported periodic STEP seams survive placement and reexport with shared
|
||||
topology and appearances intact. Recorded body transfers preserve historical Join/Cut membership,
|
||||
material, placements and machining data through edits, copying, undo and reopen. New 3D wires
|
||||
record their coordinate frame; legacy wires migrate from known provenance without changing
|
||||
coordinates. Broken legacy links with missing provenance can require explicit refresh or relink.
|
||||
See [occurrence workflows](docs/occurrence-workflows.md).
|
||||
- Toolbar drops on panel labels, gaps and margins append commands with insertion feedback;
|
||||
button drops keep insert-before ordering.
|
||||
- Parameters and Configurations have distinct actions in the SOLID toolbar's CONFIGURE panel.
|
||||
Parameters opens the user/model variable editor; Configurations opens the configuration table.
|
||||
Configure remains available in the panel dropdown.
|
||||
- Parameter creation supports Enter with validation and continued keyboard entry. Shared
|
||||
expression autocomplete supports Up/Down selection, Enter/Tab acceptance and Escape dismissal;
|
||||
accepting a suggestion does not submit a parameter or trigger viewport shortcuts.
|
||||
- Movable window title bars follow the app's dark/light header palette. Radial right-click
|
||||
menus are opt-in in Preferences > Navigation; ordinary context menus remain the default.
|
||||
- Desktop distribution: running Windows windows and executable resources use the app icon;
|
||||
packaging validates icon resources. Nix installs launchers, MIME data and generated icons,
|
||||
with explicit launch apps and Vulkan-loader/driver discovery. Flatpak feed instructions
|
||||
register the required runtime source; release checks exercise desktop modelling on X11/Wayland.
|
||||
- OpenGL rendering clamps effective anti-aliasing to 1x across UI pipelines, textures and CAD
|
||||
callbacks to prevent black frames. Other backends retain the requested sample count, and
|
||||
stored preferences remain unchanged.
|
||||
- Configuration rules: ordered typed conditions can drive parameters and suppressions or
|
||||
restrict values. Preview explains effective values without modifying the document; applying
|
||||
rules validates rows atomically. Length expressions retain their authoring units across
|
||||
document-unit changes. The UI supports structured All/Any/Not conditions and rule ordering.
|
||||
Component styles and certified semantic face styles are available; unsupported lineage refuses
|
||||
atomically, and stale certified face paint cannot jump to another surface. See [configurations](docs/configurations.md).
|
||||
- Sheet-metal material: inherited holes, crossing edge notches, overlapping cuts and retained
|
||||
islands share the same boundaries in flat preview, DXF and unfolded solids. Detached material
|
||||
pieces survive unfold/refold as one logical sheet group, including split flanged sheets.
|
||||
Cut-outs made in the flat pattern cross bends at any angle or curved: their edges fold to
|
||||
helices and curves on the bend, their walls square to the sheet. Free-form bases use bounded
|
||||
sampling.
|
||||
Parallel prismatic partial-depth cuts retain pocket side and depth through transfer and
|
||||
unfold/refold, with separate pocket contours in preview and DXF. Planar Intersect preserves
|
||||
exact holes and retained islands, including the union of multiple selected profiles, and
|
||||
records complementary depth layers as pockets while retaining the stock gauge. Unsupported
|
||||
tapered or bend-crossing machining refuses atomically instead of restoring material later.
|
||||
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
|
||||
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
|
||||
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
|
||||
clear of the flange beside it, and the returns of one feature are mitred toward each other
|
||||
with the rule's gap where their flanges meet at a corner; returns of separate features, and
|
||||
flanges on returns, are not closed toward each other.
|
||||
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
|
||||
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
|
||||
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
|
||||
check. Under-resolved fits return an error.
|
||||
- Contact driving checks the trajectory between clear endpoints, including linked joints,
|
||||
playback and sparse motion exports. Checks use bounded tessellation samples, not exact
|
||||
continuous collision detection; local thin walls and small details remain limitations.
|
||||
See [contact-set limits and recovery](docs/contact-sets.md).
|
||||
|
||||
## 3D sketch bridges
|
||||
|
||||
3D sketch drawing snaps to points on other visible sketch planes without flattening them onto
|
||||
the active plane. Bridge endpoints capture their source positions; associative endpoint links,
|
||||
construction-plane selection of 3D wires and loft guide rails remain separate work.
|
||||
Feature dialog header chevrons collapse and restore the form without discarding selections
|
||||
or edited values, including the Appearance panel.
|
||||
## Construction on 3D paths
|
||||
|
||||
Plane Along Path has a viewport path selector, fractional position, live preview and timeline
|
||||
editing. Construction planes and points resolve drawn 3D wires without applying their sketch
|
||||
plane a second time. Curved paths use sampled segments; multi-curve chains, absolute distance
|
||||
and reference-object placement remain separate work, as do loft rails and centerlines.
|
||||
|
||||
## Design properties (#1015)
|
||||
|
||||
- Designs carry project, part number, revision, author, organization, description and licence
|
||||
in an optional `metadata` object (File → Design Properties…, `document.metadata.*`,
|
||||
`inspect_design`). STEP and IGES headers and 3MF model metadata take the fields their format
|
||||
has a place for. Other tools keep their own data under a reverse-DNS key, kept as it is
|
||||
through open, edit and save and never exported. See [design properties](docs/book/src/guide/files.md#design-properties).
|
||||
Per-component properties (a part number per component) can follow with the same type.
|
||||
|
||||
## Document-opening performance
|
||||
|
||||
- Desktop STEP import prepares consecutive independent solids in bounded batches with up
|
||||
to eight workers, retaining serial traversal, result order and persistent source identities.
|
||||
The web path remains serial. Original multi-solid fixtures show 64–81% lower complete
|
||||
import times; single-solid assembly parts are not scheduled across nodes. See
|
||||
[measurements and limits](docs/perf.md#parallel-step-body-preparation-2026-10-12).
|
||||
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
|
||||
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
|
||||
format. Generated-mesh storage measurements reduce serialization and decoding work;
|
||||
a seekable reader defers original blobs until after provisional display.
|
||||
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
|
||||
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
|
||||
and geometry records, with a separate truck adapter and bounded binary reader. Exact
|
||||
face-source certificates and operation history survive codec round trips. A provisional
|
||||
picture can be generated before original blobs are read; editing and measurement wait
|
||||
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
|
||||
the same document despite concurrent file writes. Default saves omit this cache and never force cold
|
||||
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
|
||||
Keys, resource budgets and document/revision guards reject incompatible, hostile or
|
||||
stale results. A second modelling kernel still requires its own tested adapter.
|
||||
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
|
||||
Home reads it without rebuilding the design when its local picture is missing;
|
||||
mismatched or malformed thumbnails fall back to normal generation. The image is
|
||||
kernel-independent and saving never forces cold meshing to produce it.
|
||||
|
||||
- Cold rebuild profiling separates parsing, geometry, placement, display meshing and
|
||||
thumbnails. Straight-line closest-point projection and exact curved-surface evaluation
|
||||
reuse reduce repeated work without relaxing geometry checks. Three alternating optimized
|
||||
benchmark runs reduce median rebuild time by 26% on the public sensor enclosure and 22%
|
||||
on a local case assembly; shape measurements and topology match the original build.
|
||||
See [performance measurements](docs/perf.md). Further cold Boolean and import costs remain.
|
||||
- Prepared assembly geometry survives selection and view changes while real geometry and
|
||||
placement changes invalidate it. A Pi assembly edge-selection probe drops from 9.24 s to
|
||||
0.57 s; initial opening is unchanged. Actual Windows desktop profiling measures 15.87 s
|
||||
median opening, with 82% spent rebuilding geometry; halving full opening remains open.
|
||||
|
||||
## Kernel
|
||||
|
||||
- Successful Boolean retries report their strategy and numerical adjustments as
|
||||
feature warnings and structured inspection data. Diagnostics survive cached
|
||||
evaluation, undo/redo and cold reopening; the robustness runner counts retry
|
||||
events by strategy and reports any bounded-log truncation. Existing geometry
|
||||
acceptance checks remain in force.
|
||||
|
||||
- Cylinders of one radius whose axes cross (a second bore across a bore of the same size, a pipe
|
||||
tee of equal pipes) touch at two points, where the mesh intersection cannot trace the curves
|
||||
and a boolean cannot tell the four arms of the crossing apart. When the ordinary boolean fails
|
||||
on such a pair, both faces are split along the line of their cylinder through the touching
|
||||
points (the solids are unchanged), the boolean is taken of those, and its history is composed
|
||||
back over the operands as given (#788). The result keeps the split lines as edges of the bore
|
||||
walls. A crossing the ordinary route fails on and the split faces do not help (a radius of 17.5
|
||||
in a 45 mm block with a tool of 80 mm) is still refused.
|
||||
- Boolean intersection-curve construction refuses point-only, collapsed and non-finite
|
||||
polylines without panicking. Direct constructor regressions retain valid open endpoints
|
||||
and closed spherical intersections; the original interactive trigger remains unidentified.
|
||||
- Shell of a stepped, joined planar body no longer creates horizontal subdivisions across
|
||||
continuous walls. Fresh axis-aligned Cut walls are stitched using their exact construction
|
||||
support and shared topology, preserving real steps and intentional boundaries. The local
|
||||
reported case returns from 86 to 49 faces with unchanged material and bounds; regressions
|
||||
cover stepped stock, STEP round trips and saved projections from removed pieces.
|
||||
|
||||
truck (Apache-2.0) behind our own `kernel` crate, with our own blend operation and boolean
|
||||
retry/validation; see `plan/adr/0001-geometry-kernel.md` (local) — summary in README.
|
||||
- Feature milestones: <https://github.com/tangency-labs/solidcraft/milestones>
|
||||
- Issues: <https://github.com/tangency-labs/solidcraft/issues> (area labels such as
|
||||
`area:kernel`, `area:sketch` and `area:ui` group them)
|
||||
- Discord: <https://discord.gg/FtU4NQNVDH>
|
||||
- Releases: <https://github.com/tangency-labs/solidcraft/releases>
|
||||
- Contributing: [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
+70
-105
@@ -1,6 +1,6 @@
|
||||
Third-party software in SolidCraft
|
||||
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 563 crates below.
|
||||
SolidCraft (the desktop app, solidcraft-cli and the web app) is built from the 559 crates below.
|
||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||
SolidCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||
|
||||
@@ -60,13 +60,13 @@ bytemuck_derive 1.12.1 (Zlib OR Apache-2.0 OR MIT)
|
||||
byteorder 1.5.0 (Unlicense OR MIT)
|
||||
byteorder-lite 0.1.0 (Unlicense OR MIT)
|
||||
bytes 1.12.1 (MIT)
|
||||
cadmpeg-codec-freecad 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.1 (Apache-2.0)
|
||||
cadmpeg-container 0.6.1 (Apache-2.0)
|
||||
cadmpeg-core 0.6.1 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.1 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.1 (Apache-2.0)
|
||||
cadmpeg-codec-freecad 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-rhino 0.6.3 (Apache-2.0)
|
||||
cadmpeg-codec-step 0.6.3 (Apache-2.0)
|
||||
cadmpeg-container 0.6.3 (Apache-2.0)
|
||||
cadmpeg-core 0.6.3 (Apache-2.0)
|
||||
cadmpeg-ir 0.6.3 (Apache-2.0)
|
||||
cadmpeg-registry 0.6.3 (Apache-2.0)
|
||||
calloop 0.13.0 (MIT)
|
||||
calloop 0.14.5 (MIT)
|
||||
calloop-wayland-source 0.3.0 (MIT)
|
||||
@@ -355,6 +355,7 @@ rustls-platform-verifier 0.7.1 (MIT OR Apache-2.0)
|
||||
rustls-platform-verifier-android 0.2.0 (MIT OR Apache-2.0)
|
||||
rustls-webpki 0.103.15 (ISC)
|
||||
rustversion 1.0.23 (MIT OR Apache-2.0)
|
||||
ruzstd 0.9.1 (MIT)
|
||||
schannel 0.1.29 (MIT)
|
||||
schemars 1.2.3 (MIT)
|
||||
schemars_derive 1.2.3 (MIT)
|
||||
@@ -432,6 +433,7 @@ truck-shapeops 0.4.0 (Apache-2.0)
|
||||
truck-stepio 0.3.0 (Apache-2.0)
|
||||
truck-topology 0.6.0 (Apache-2.0)
|
||||
ttf-parser 0.25.1 (MIT OR Apache-2.0)
|
||||
twox-hash 2.1.5 (MIT)
|
||||
type-map 0.5.1 (MIT/Apache-2.0)
|
||||
typed-path 0.12.3 (MIT OR Apache-2.0)
|
||||
typeid 1.0.3 (MIT OR Apache-2.0)
|
||||
@@ -549,16 +551,10 @@ zeroize_derive 1.5.0 (Apache-2.0 OR MIT)
|
||||
zerotrie 0.2.5 (Unicode-3.0)
|
||||
zerovec 0.11.8 (Unicode-3.0)
|
||||
zerovec-derive 0.11.6 (Unicode-3.0)
|
||||
zip 8.6.0 (MIT)
|
||||
zip 9.0.4 (MIT)
|
||||
zlib-rs 0.6.8 (Zlib)
|
||||
zmij 1.0.23 (MIT)
|
||||
zopfli 0.8.3 (Apache-2.0)
|
||||
zstd 0.13.3 (MIT)
|
||||
zstd 0.14.2 (BSD-3-Clause)
|
||||
zstd-safe 7.3.0 (BSD-3-Clause)
|
||||
zstd-safe 8.1.0 (BSD-3-Clause)
|
||||
zstd-sys 2.1.1+zstd.1.5.7 (BSD-3-Clause)
|
||||
zune-core 0.5.3 (MIT OR Apache-2.0 OR Zlib)
|
||||
zune-jpeg 0.5.15 (MIT OR Apache-2.0 OR Zlib)
|
||||
zvariant 4.2.0 (MIT)
|
||||
@@ -12457,6 +12453,32 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
The files under third-party/chromium are licensed as described in
|
||||
third-party/chromium/LICENSE.
|
||||
|
||||
================================================================================
|
||||
ruzstd 0.9.1
|
||||
================================================================================
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Moritz Borcherding
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
schannel 0.1.29
|
||||
================================================================================
|
||||
@@ -13718,6 +13740,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
twox-hash 2.1.5
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Jake Goulding
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
type-map 0.5.1
|
||||
================================================================================
|
||||
@@ -16339,7 +16387,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
zip 8.6.0, zip 9.0.4
|
||||
zip 9.0.4
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
@@ -16594,89 +16642,6 @@ Apache License
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
================================================================================
|
||||
zstd 0.13.3
|
||||
================================================================================
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2016 Alexandre Bury
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
================================================================================
|
||||
zstd 0.14.2, zstd-safe 7.3.0, zstd-safe 8.1.0, zstd-sys 2.1.1+zstd.1.5.7
|
||||
================================================================================
|
||||
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, Alexandre Bury
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
zstd-sys 2.1.1+zstd.1.5.7
|
||||
================================================================================
|
||||
|
||||
The auto-generated bindings are under the 3-clause BSD license:
|
||||
|
||||
BSD License
|
||||
|
||||
For Zstandard software
|
||||
|
||||
Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name Facebook, nor Meta, nor the names of its contributors may
|
||||
be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
================================================================================
|
||||
zune-core 0.5.3, zune-jpeg 0.5.15
|
||||
================================================================================
|
||||
@@ -16744,13 +16709,13 @@ accesskit_winit 0.32.2: Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||
array-macro 2.1.8: MIT OR Apache-2.0 (https://codeberg.org/xfix/array-macro)
|
||||
block2 0.5.1: MIT (https://github.com/madsmtm/objc2)
|
||||
block2 0.6.2: MIT (https://github.com/madsmtm/objc2)
|
||||
cadmpeg-codec-freecad 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.1: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-freecad 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-rhino 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-codec-step 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-container 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-core 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-ir 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
cadmpeg-registry 0.6.3: Apache-2.0 (https://github.com/cadmpeg/cadmpeg)
|
||||
clipboard-win 5.4.1: BSL-1.0 (https://github.com/DoumanAsh/clipboard-win)
|
||||
convert_case 0.4.0: MIT (https://github.com/rutrum/convert-case)
|
||||
dispatch 0.2.0: MIT (http://github.com/SSheldon/rust-dispatch)
|
||||
|
||||
@@ -19,6 +19,11 @@ path = "src/main.rs"
|
||||
name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-gpu = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Reproducible evidence through the production command/job interfaces.
|
||||
//! Usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png
|
||||
//! Writes the editable design, command timings and actual adapter/job metadata beside PNG.
|
||||
//! Accepts runtime requests as well as inline or named saved render jobs.
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, ErrorCode, JobId, Phase},
|
||||
};
|
||||
use std::{
|
||||
io::Read,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn read_json(path: &str) -> Result<Value, String> {
|
||||
let mut bytes = Vec::new();
|
||||
std::fs::File::open(path).map_err(|e| format!("{path}: {e}"))?.take(16_777_217).read_to_end(&mut bytes).map_err(|e| e.to_string())?;
|
||||
if bytes.len() > 16_777_216 {
|
||||
return Err("evidence input exceeds 16 MiB".into());
|
||||
}
|
||||
serde_json::from_slice(&bytes).map_err(|e| format!("{path}: {e}"))
|
||||
}
|
||||
|
||||
// Only decide which optional host must be installed. The production command
|
||||
// still validates and resolves the actual request without a second wire format.
|
||||
fn needs_gpu(session: &Session, params: &Value) -> Result<bool, String> {
|
||||
let inline = params.get("settings").filter(|value| !value.is_null());
|
||||
let named = params.get("setup").filter(|value| !value.is_null());
|
||||
if inline.is_some() && named.is_some() {
|
||||
return Err("choose either setup or settings".into());
|
||||
}
|
||||
let inline_setup = inline
|
||||
.filter(|value| value.get("job").is_some_and(|job| !job.is_null()))
|
||||
.map(|value| serde_json::from_value::<RenderSetup>(value.clone()).map_err(|e| e.to_string()))
|
||||
.transpose()?;
|
||||
let setup = if let Some(name) = named.and_then(Value::as_str) {
|
||||
Some(session.doc.render.setups.get(name).ok_or("no saved render setup with that name")?)
|
||||
} else {
|
||||
inline_setup.as_ref()
|
||||
};
|
||||
if let Some(setup) = setup
|
||||
&& let Some(job) = &setup.job
|
||||
{
|
||||
if params.as_object().is_some_and(|object| object.keys().any(|key| !matches!(key.as_str(), "setup" | "settings" | "replace"))) {
|
||||
return Err("saved render jobs cannot be mixed with runtime controls".into());
|
||||
}
|
||||
setup.validate().map_err(|e| e.to_string())?;
|
||||
return Ok(matches!(job, RenderJob::GpuPath { .. }));
|
||||
}
|
||||
Ok(params.get("progressive").and_then(Value::as_bool) == Some(true))
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let [recipe_path, params_path, output_path] = args.as_slice() else {
|
||||
return Err("usage: studio_evidence RECIPE.json RENDER-PARAMS.json OUTPUT.png".into());
|
||||
};
|
||||
let output = Path::new(output_path);
|
||||
if !output.extension().is_some_and(|e| e.eq_ignore_ascii_case("png")) {
|
||||
return Err("output must have a .png extension".into());
|
||||
}
|
||||
let recipe = read_json(recipe_path)?;
|
||||
let params = read_json(params_path)?;
|
||||
let mut session = Session::default();
|
||||
eprintln!("Generating original CAD scene from {recipe_path}");
|
||||
let generation_start = Instant::now();
|
||||
let (commands, command_timings) = session.run_script_timed(&recipe, false).map_err(|e| e.to_string())?;
|
||||
let generation_seconds = generation_start.elapsed().as_secs_f64();
|
||||
let design_path = output.with_extension("solidcraft");
|
||||
session.execute("file.save_as", &json!({"path":design_path})).map_err(|e| e.to_string())?;
|
||||
let scene = solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?;
|
||||
let bodies: Vec<Value> = scene
|
||||
.meshes
|
||||
.iter()
|
||||
.map(|mesh| json!({"name":mesh.name,"vertices":mesh.mesh.positions.len(),"triangles":mesh.mesh.triangles.len()}))
|
||||
.collect();
|
||||
drop(scene);
|
||||
|
||||
let cpu_host_start = Instant::now();
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
let cpu_host_seconds = cpu_host_start.elapsed().as_secs_f64();
|
||||
// Match runtime and saved-job dispatch. CPU evidence must not require a
|
||||
// Vulkan device, parse GPU policy or initialize a GPU host.
|
||||
let (adapter_report, gpu_host_seconds) = if needs_gpu(&session, ¶ms)? {
|
||||
let gpu_host_start = Instant::now();
|
||||
let policy = solidcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
|
||||
let (adapter, report) =
|
||||
solidcraft_gpu::select::pick_vulkan(policy, |_| true).ok_or("no Vulkan adapter satisfies the requested GPU policy")?;
|
||||
session.register_render_job_host(solidcraft_gpu::path::host_on_adapter(&adapter).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
(Some(report), Some(gpu_host_start.elapsed().as_secs_f64()))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let start = Instant::now();
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let executor = started
|
||||
.get("resolved")
|
||||
.and_then(|resolved| resolved.get("executor"))
|
||||
.and_then(Value::as_str)
|
||||
.ok_or("started job did not report the resolved image executor")?
|
||||
.to_owned();
|
||||
let id: JobId = serde_json::from_value(started.get("job").cloned().ok_or("missing job id")?).map_err(|e| e.to_string())?;
|
||||
let handle = session.render_job_handle(id).map_err(|e| e.to_string())?;
|
||||
eprintln!("Rendering through production {executor} executor");
|
||||
if let Some(adapter) = &adapter_report {
|
||||
eprintln!("GPU adapter: {adapter:?}");
|
||||
}
|
||||
let status = loop {
|
||||
match handle.wait_timeout(Duration::from_secs(10)) {
|
||||
Ok(status) => break status,
|
||||
Err(error) if error.code == ErrorCode::TimedOut && start.elapsed() < Duration::from_secs(3600) => {
|
||||
let status = handle.status().map_err(|e| e.to_string())?;
|
||||
eprintln!(
|
||||
"Render progress: {:?} {}/{} after {:.1}s",
|
||||
status.progress.phase,
|
||||
status.progress.done,
|
||||
status.progress.total,
|
||||
start.elapsed().as_secs_f64()
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = handle.cancel();
|
||||
return Err(error.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
let render_and_encode_seconds = start.elapsed().as_secs_f64();
|
||||
if status.progress.phase != Phase::Completed {
|
||||
return Err(status.error.map_or_else(|| "render did not complete".into(), |e| e.to_string()));
|
||||
}
|
||||
let exported = session.execute("render.export", &json!({"job":id,"path":output_path,"overwrite":true})).map_err(|e| e.to_string())?;
|
||||
let report = json!({
|
||||
"recipe":recipe_path,"render_params":params,"executor":executor,"adapter":adapter_report,
|
||||
"environment":{"SOLIDCRAFT_GPU":std::env::var("SOLIDCRAFT_GPU").ok(),"WGPU_BACKEND":std::env::var("WGPU_BACKEND").ok()},
|
||||
"cad_generation_seconds":generation_seconds,"cpu_host_setup_seconds":cpu_host_seconds,"gpu_host_setup_seconds":gpu_host_seconds,
|
||||
"render_and_encode_seconds":render_and_encode_seconds,
|
||||
"timing_scope":"wall time from render.start through completed PNG encoding; includes job preparation and readback, excludes CAD generation, preflight and initial CPU/GPU host setup; GPU setup timing includes adapter selection and pipelines when requested",
|
||||
"bodies":bodies,"commands":commands,"command_timings":command_timings,
|
||||
"preflight":preflight,"export":exported,"editable_design":design_path
|
||||
});
|
||||
std::fs::write(output.with_extension("json"), serde_json::to_vec_pretty(&report).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
println!("{}", serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
run()
|
||||
}
|
||||
@@ -36,8 +36,9 @@ impl Script {
|
||||
);
|
||||
}
|
||||
|
||||
/// A part placed after it is built: free, even as the design's first part (#2804).
|
||||
fn component(&mut self, name: &str) {
|
||||
self.c("component.create", json!({"name": name}));
|
||||
self.c("component.create", json!({"name": name, "grounded": false}));
|
||||
}
|
||||
|
||||
fn root(&mut self) {
|
||||
|
||||
@@ -69,6 +69,7 @@ impl Control {
|
||||
return Err("--connect requires a loopback address and a nonzero port (the app listens on 127.0.0.1)".into());
|
||||
}
|
||||
let token = solidcraft_engine::control_auth::client_token(addr.port(), token)
|
||||
.map(|t| t.token)
|
||||
.ok_or("no control token: pass --token, set SOLIDCRAFT_CONTROL_TOKEN, or run on the app's computer (its token file)")?;
|
||||
if token.len() > 4096 {
|
||||
return Err("the control token is longer than 4096 bytes".into());
|
||||
|
||||
@@ -6,11 +6,14 @@ use std::sync::Mutex;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::crash;
|
||||
|
||||
const USAGE: &str = "usage: solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]";
|
||||
/// The command's usage, from the same lines the top-level usage lists.
|
||||
fn usage() -> String {
|
||||
crate::command_usage("crashes")
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let dir = crash::default_dir().ok_or("no crash folder on this system")?;
|
||||
let id = || args.get(1).map(String::as_str).ok_or_else(|| USAGE.to_string());
|
||||
let id = || args.get(1).map(String::as_str).ok_or_else(usage);
|
||||
match args.first().map(String::as_str) {
|
||||
None | Some("list") => {
|
||||
let reports = crash::list(&dir);
|
||||
@@ -43,7 +46,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
println!("deleted {} report(s)", crash::clear(&dir, args.get(1).map(String::as_str)));
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.to_string()),
|
||||
_ => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,16 @@ fn flag<T: std::str::FromStr>(args: &[String], name: &str, default: T) -> T {
|
||||
args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
}
|
||||
|
||||
/// The options `fuzz` takes, and the ones that stand alone.
|
||||
const OPTIONS: &[&str] = &["--from", "--count", "--jobs", "--timeout", "--steps", "--out", "--report"];
|
||||
const SWITCHES: &[&str] = &["--minimise"];
|
||||
|
||||
/// One seed: a design and a boolean case, as JSON on stdout (progress on stderr).
|
||||
pub fn one(args: &[String]) -> Result<(), String> {
|
||||
let seed: u64 = args.first().and_then(|s| s.parse().ok()).ok_or("fuzz-one <seed> [--steps N] [--minimise]")?;
|
||||
// The seed is the first argument; the rest are options, and an option `fuzz-one` doesn't
|
||||
// take is refused rather than ignored.
|
||||
crate::known_args("fuzz-one", &args[1..], &["--steps"], SWITCHES, false)?;
|
||||
let steps = flag(args, "--steps", 8usize);
|
||||
let minimise = args.iter().any(|a| a == "--minimise");
|
||||
// Panics caught inside the kernel: harmless natively, fatal in the web build.
|
||||
@@ -64,6 +71,9 @@ struct Job {
|
||||
/// Run seeds `--from`…`--from + --count` in `--jobs` processes, `--timeout` seconds each;
|
||||
/// write failing scripts to `--out` and a summary to `--report`.
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
// An option `fuzz` doesn't take is refused before a seed runs: it would otherwise be
|
||||
// ignored, and the run would go on for minutes with the wrong settings.
|
||||
crate::known_args("fuzz", args, OPTIONS, SWITCHES, false)?;
|
||||
let from = flag(args, "--from", 0u64);
|
||||
let count = flag(args, "--count", 100u64);
|
||||
let last = from.checked_add(count.saturating_sub(1)).ok_or("seed range exceeds u64::MAX: reduce --from or --count")?;
|
||||
|
||||
+191
-68
@@ -15,9 +15,10 @@
|
||||
//! 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 [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
//! solidcraft-cli mcp setup [--dry-run] [--client NAME] [--live] (alias: mcp install) add it to OpenCode, Claude Desktop, Claude Code, Cursor
|
||||
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
|
||||
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
|
||||
//!
|
||||
@@ -46,58 +47,98 @@ mod app_icon;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::view::Guides;
|
||||
|
||||
const USAGE: &str = "usage:
|
||||
solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]
|
||||
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
|
||||
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
|
||||
(--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;
|
||||
--profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)
|
||||
solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)
|
||||
(--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,
|
||||
as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)
|
||||
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
|
||||
(the design's history as a command script that eval and run replay)
|
||||
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]
|
||||
(--ops: after opening, run each command of the script and undo it, timing the command, the
|
||||
evaluations it caused and the view it leaves: placing and meshing what changed)
|
||||
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
|
||||
solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]
|
||||
solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
[--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...
|
||||
(as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;
|
||||
--view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;
|
||||
--camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);
|
||||
--no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;
|
||||
--body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside
|
||||
it shows) and color #rrggbb, either or both, once per body;
|
||||
--tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body
|
||||
is visible inside the material it meets; a later --body overrides it;
|
||||
--section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world
|
||||
coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)
|
||||
solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]
|
||||
solidcraft-cli exec <command> [json-params]
|
||||
solidcraft-cli commands
|
||||
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 [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
[--param-check off|warn|strict]
|
||||
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||
solidcraft-cli licences
|
||||
solidcraft-cli gpu [--policy auto|hardware|software|cpu]
|
||||
(the GPU adapters a headless render can open, best first, the one the policy picks and the tier it
|
||||
lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults
|
||||
to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)
|
||||
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
|
||||
--debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its
|
||||
model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate
|
||||
";
|
||||
/// One command the CLI takes: the name to type, the usage it prints for `--help`,
|
||||
/// and whether the top-level usage lists it (an alias answers to another command's name).
|
||||
struct Command {
|
||||
name: &'static str,
|
||||
usage: &'static str,
|
||||
listed: bool,
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.solidcraft|part.step> [--in design.solidcraft|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.solidcraft|part.step> [--json-out FILE] (alias: inspect; or --in FILE)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.solidcraft|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] [--save OUT.solidcraft] [--display LEVEL [--edit NAME EXPR]] [--ops script.json]\n (--ops: after opening, run each command of the script and undo it, timing the command, the\n evaluations it caused and the view it leaves: placing and meshing what changed)\n";
|
||||
/// `bench-agent`'s lines in the usage.
|
||||
const USAGE_BENCH_AGENT: &str = " solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]\n";
|
||||
/// `bench-assistant`'s lines in the usage.
|
||||
const USAGE_BENCH_ASSISTANT: &str = " solidcraft-cli bench-assistant --connect 127.0.0.1:PORT [--token T] --base-url URL --model NAME [--tasks 1,2,5] [--repeat N] [--timeout-s 300] [--json out.json] [--markdown out.md]\n";
|
||||
/// `snapshot`'s lines in the usage.
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.solidcraft|part.step] --out shot.png [--width W] [--height H] [--view NAME | --camera JSON] [--no-grid] [--no-axes]\n [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]\n solidcraft-cli snapshot --in <design.solidcraft|part.step> --out shot.png ...\n (as with run, --in starts from a design and the script runs on top of it; an option snapshot doesn't take is an error;\n --view: iso, front, back, top, bottom, left, right, or the name of a view saved in the design with view.save;\n --camera: {target: [x,y,z], yaw, pitch, distance, fov?} in mm and radians, as the MCP screenshot takes it (docs/mcp.md);\n --no-grid and --no-axes leave the ground grid (on Z 0) and the origin's axes out of the picture;\n --body: how one body is drawn, as appearance.assign takes it: opacity 0-1 (below 1 is see-through, so what is inside\n it shows) and color #rrggbb, either or both, once per body;\n --tool: draw this body solid in the tool colour and every other body see-through, so a cut's or a join's tool body\n is visible inside the material it meets; a later --body overrides it;\n --section: cut the model as inspect.section does, so the inside shows: z=27 (the plane through that world\n coordinate, keeping the smaller side), add ,flip to keep the other side, or XY, XZ, YZ, or its own JSON)\n";
|
||||
/// `render`'s lines in the usage.
|
||||
const USAGE_RENDER: &str = " solidcraft-cli render <script|design> --out image.png [--setup NAME] [--width W] [--height H] [--transparent] [--overwrite] [--timeout SECONDS]\n [--progressive] [--samples N] [--max-depth N] [--seed N] [--exposure EV] [--lighting FILE.json] [--environment FILE.json] [--tone-map reinhard|aces_fitted]\n";
|
||||
/// `exec`'s lines in the usage.
|
||||
const USAGE_EXEC: &str = " solidcraft-cli exec <command> [json-params]\n";
|
||||
/// `commands`'s lines in the usage.
|
||||
const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
|
||||
/// `recipe`'s lines in the usage.
|
||||
const USAGE_RECIPE: &str = " solidcraft-cli recipe <recipe.json>\n";
|
||||
/// `oracle`'s lines in the usage.
|
||||
const USAGE_ORACLE: &str = " solidcraft-cli oracle <case-dir>... [--json]\n";
|
||||
/// `fuzz`'s lines in the usage.
|
||||
const USAGE_FUZZ: &str =
|
||||
" solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]\n";
|
||||
/// `mcp`'s lines in the usage.
|
||||
const USAGE_MCP: &str = " solidcraft-cli mcp [--headless] [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]\n [--param-check off|warn|strict] [--render-gpu]\n solidcraft-cli mcp setup [--dry-run] [--client opencode|claude-desktop|claude-code|cursor] [--live]\n (adds a `solidcraft` server, with this program's path, to each installed AI client; --live adds `solidcraft-live` for the running app; alias: mcp install)\n";
|
||||
/// `crashes`'s lines in the usage.
|
||||
const USAGE_CRASHES: &str = " solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]\n";
|
||||
/// `licences`'s lines in the usage.
|
||||
const USAGE_LICENCES: &str = " solidcraft-cli licences\n";
|
||||
/// `gpu`'s lines in the usage.
|
||||
const USAGE_GPU: &str = " solidcraft-cli gpu [--policy auto|hardware|software|cpu]\n (the GPU adapters a headless render can open, best first, the one the policy picks and the tier it\n lands on: gpu, gpu-software, or the CPU rasterizer (auto with no adapter, or cpu); the policy defaults\n to SOLIDCRAFT_GPU, else auto; exits with an error when hardware or software finds no such adapter)\n";
|
||||
/// `icon`'s lines in the usage.
|
||||
const USAGE_ICON: &str = " solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico\n";
|
||||
/// `fuzz-one`'s lines in the usage: a seed and the flags of one fuzz run.
|
||||
const USAGE_FUZZ_ONE: &str = " solidcraft-cli fuzz-one <seed> [--steps N] [--minimise]\n";
|
||||
/// `bench-edit`'s lines in the usage: the parameter a script changes.
|
||||
const USAGE_BENCH_EDIT: &str = " solidcraft-cli bench-edit <script.json|recipe.json> <param> <expr>\n";
|
||||
/// `save-stress`'s lines in the usage: the file saved over and over.
|
||||
const USAGE_SAVE_STRESS: &str = " solidcraft-cli save-stress <path> <rounds>\n";
|
||||
/// The flags every command takes, at the end of the top-level usage.
|
||||
const USAGE_DEBUG: &str = " --debug, with any command: what SolidCraft logs (why a body has no display plan, why a save left its\n model cache out), on stderr; RUST_LOG=error|warn|info|debug|trace sets one level for every crate\n";
|
||||
|
||||
/// The commands the CLI takes, in the order the top-level usage lists them.
|
||||
const COMMANDS: &[Command] = &[
|
||||
Command { name: "run", usage: USAGE_RUN, listed: true },
|
||||
Command { name: "eval", usage: USAGE_EVAL, listed: true },
|
||||
Command { name: "inspect", usage: USAGE_EVAL, listed: false },
|
||||
Command { name: "script", usage: USAGE_SCRIPT, listed: true },
|
||||
Command { name: "bench-open", usage: USAGE_BENCH_OPEN, listed: true },
|
||||
Command { name: "bench-agent", usage: USAGE_BENCH_AGENT, listed: true },
|
||||
Command { name: "bench-assistant", usage: USAGE_BENCH_ASSISTANT, listed: true },
|
||||
Command { name: "snapshot", usage: USAGE_SNAPSHOT, listed: true },
|
||||
Command { name: "render", usage: USAGE_RENDER, listed: true },
|
||||
Command { name: "exec", usage: USAGE_EXEC, listed: true },
|
||||
Command { name: "commands", usage: USAGE_COMMANDS, listed: true },
|
||||
Command { name: "recipe", usage: USAGE_RECIPE, listed: true },
|
||||
Command { name: "oracle", usage: USAGE_ORACLE, listed: true },
|
||||
Command { name: "fuzz", usage: USAGE_FUZZ, listed: true },
|
||||
Command { name: "fuzz-one", usage: USAGE_FUZZ_ONE, listed: false },
|
||||
Command { name: "mcp", usage: USAGE_MCP, listed: true },
|
||||
Command { name: "crashes", usage: USAGE_CRASHES, listed: true },
|
||||
Command { name: "licences", usage: USAGE_LICENCES, listed: true },
|
||||
Command { name: "licenses", usage: USAGE_LICENCES, listed: false },
|
||||
Command { name: "gpu", usage: USAGE_GPU, listed: true },
|
||||
Command { name: "icon", usage: USAGE_ICON, listed: true },
|
||||
Command { name: "bench-edit", usage: USAGE_BENCH_EDIT, listed: false },
|
||||
Command { name: "save-stress", usage: USAGE_SAVE_STRESS, listed: false },
|
||||
];
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
logger::init(&mut args);
|
||||
let r = match args.first().map(String::as_str) {
|
||||
// Asked-for help is not an error: the command's usage on stdout, exit 0. It is read
|
||||
// before the command runs, so `--help` never starts a run (a fuzz run, a server).
|
||||
Some(cmd) if wants_help(&args[1..]) && is_command(cmd) => {
|
||||
use std::io::Write as _;
|
||||
let _ = std::io::stdout().lock().write_all(command_usage(cmd).as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
Some("run") => cmd_run(&args[1..]),
|
||||
Some("eval" | "inspect") => cmd_eval(&args[1..]),
|
||||
Some("script") => cmd_script(&args[1..]),
|
||||
@@ -118,7 +159,7 @@ fn main() -> ExitCode {
|
||||
}
|
||||
})
|
||||
}
|
||||
Some("recipe") => args.get(1).ok_or_else(|| USAGE.to_string()).and_then(|p| {
|
||||
Some("recipe") => args.get(1).ok_or_else(usage).and_then(|p| {
|
||||
let v = read_json(p)?;
|
||||
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
|
||||
}),
|
||||
@@ -150,10 +191,10 @@ fn main() -> ExitCode {
|
||||
// Asked-for help is not an error: usage on stdout, exit 0.
|
||||
Some("--help" | "-h" | "help") => {
|
||||
use std::io::Write as _;
|
||||
let _ = std::io::stdout().lock().write_all(USAGE.as_bytes());
|
||||
let _ = std::io::stdout().lock().write_all(usage().as_bytes());
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.to_string()),
|
||||
_ => Err(usage()),
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
@@ -168,6 +209,35 @@ fn pretty(v: &Value) -> String {
|
||||
serde_json::to_string_pretty(v).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The whole usage: every command's lines under one `usage:` line. The lines come from the
|
||||
/// same place a command's own `--help` prints, so the two cannot drift apart.
|
||||
fn usage() -> String {
|
||||
let mut s = String::from("usage:\n");
|
||||
for c in COMMANDS.iter().filter(|c| c.listed) {
|
||||
s.push_str(c.usage);
|
||||
}
|
||||
s.push_str(USAGE_DEBUG);
|
||||
s
|
||||
}
|
||||
|
||||
/// A command's own usage: its lines under a `usage:` line.
|
||||
fn command_usage(cmd: &str) -> String {
|
||||
match COMMANDS.iter().find(|c| c.name == cmd) {
|
||||
Some(c) => format!("usage:\n{}", c.usage),
|
||||
None => usage(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the name is a command the CLI takes.
|
||||
fn is_command(cmd: &str) -> bool {
|
||||
COMMANDS.iter().any(|c| c.name == cmd)
|
||||
}
|
||||
|
||||
/// Whether the arguments ask for help.
|
||||
fn wants_help(args: &[String]) -> bool {
|
||||
args.iter().any(|a| a == "--help" || a == "-h")
|
||||
}
|
||||
|
||||
pub fn read_json(path: &str) -> Result<Value, String> {
|
||||
// Through the engine's file access, which refuses anything that is not a regular file and
|
||||
// reads no more than the cap: a character device or a FIFO would otherwise never end.
|
||||
@@ -192,8 +262,7 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
|
||||
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut session = Session::new(doc);
|
||||
session.path = Some(path.to_owned());
|
||||
let mut session = Session::opened_at(doc, None, Some(path));
|
||||
session.param_check = check;
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -270,10 +339,11 @@ fn print_json(v: &Value, file: Option<&str>) -> Result<(), String> {
|
||||
fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
let json_out = json_out(args)?;
|
||||
known_args("run", args, &["--in", "--out", "--save", "--json-out"], &["--quiet", "--results", "--timings", "--profile", "--allow-errors"], true)?;
|
||||
let base = flag(args, "--in");
|
||||
let path = positional(args);
|
||||
if path.is_none() && base.is_none() {
|
||||
return Err(USAGE.to_string());
|
||||
return Err(usage());
|
||||
}
|
||||
let want_results = args.iter().any(|a| a == "--results");
|
||||
// `--profile` times each command and splits out its kernel time; it implies `--timings`.
|
||||
@@ -333,13 +403,14 @@ fn open(base: Option<&str>, path: Option<&String>) -> Result<Session, String> {
|
||||
}
|
||||
(Some(b), None) => load(b),
|
||||
(None, Some(p)) => load(p),
|
||||
(None, None) => Err(USAGE.to_string()),
|
||||
(None, None) => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Refuses an option `cmd` doesn't take, and one missing its value: a typo, or another
|
||||
/// command's option, would otherwise be ignored without a word.
|
||||
fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -> Result<(), String> {
|
||||
/// command's option, would otherwise be ignored without a word. `positionals` says whether the
|
||||
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
|
||||
fn known_args(cmd: &str, args: &[String], valued: &[&str], switches: &[&str], positionals: bool) -> Result<(), String> {
|
||||
let mut i = 0;
|
||||
while let Some(a) = args.get(i) {
|
||||
if valued.contains(&a.as_str()) {
|
||||
@@ -347,7 +418,9 @@ fn known_flags(cmd: &str, args: &[String], valued: &[&str], switches: &[&str]) -
|
||||
Some(v) if !v.starts_with("--") => i += 2,
|
||||
_ => return Err(format!("{cmd}: {a} needs a value")),
|
||||
}
|
||||
} else if a.starts_with("--") && !switches.contains(&a.as_str()) {
|
||||
} else if switches.contains(&a.as_str()) {
|
||||
i += 1;
|
||||
} else if a.starts_with("--") || !positionals {
|
||||
return Err(format!("{cmd} doesn't take {a} (it takes {})", [valued, switches].concat().join(", ")));
|
||||
} else {
|
||||
i += 1;
|
||||
@@ -371,7 +444,8 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
|
||||
|
||||
fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
let json_out = json_out(args)?;
|
||||
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(|| USAGE.to_string())?;
|
||||
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
|
||||
let path = flag(args, "--in").or_else(|| positional(args).map(String::as_str)).ok_or_else(usage)?;
|
||||
let mut s = load(path)?;
|
||||
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
|
||||
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
|
||||
@@ -389,7 +463,7 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
/// A design's history as a command script (`timeline.export_script`): printed, or written to
|
||||
/// `--out`; what the script leaves out goes to stderr.
|
||||
fn cmd_script(args: &[String]) -> Result<(), String> {
|
||||
let path = positional(args).ok_or_else(|| USAGE.to_string())?;
|
||||
let path = positional(args).ok_or_else(usage)?;
|
||||
let s = &mut load(path)?;
|
||||
let out = flag(args, "--out");
|
||||
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
|
||||
@@ -450,14 +524,15 @@ fn install_renderer() {
|
||||
|
||||
fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
known_flags(
|
||||
known_args(
|
||||
"snapshot",
|
||||
args,
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
|
||||
&["--no-grid", "--no-axes"],
|
||||
true,
|
||||
)?;
|
||||
if positional(args).is_none() && flag(args, "--in").is_none() {
|
||||
return Err(USAGE.to_string());
|
||||
return Err(usage());
|
||||
}
|
||||
let out = flag(args, "--out").ok_or("snapshot needs --out FILE.png")?;
|
||||
let size = |name: &str, default: u32| match flag(args, name) {
|
||||
@@ -541,7 +616,7 @@ fn ico(images: &[(usize, Vec<u8>)]) -> Result<Vec<u8>, String> {
|
||||
|
||||
fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
install_renderer();
|
||||
let id = args.first().ok_or_else(|| USAGE.to_string())?;
|
||||
let id = args.first().ok_or_else(usage)?;
|
||||
let p: Value = match args.get(1) {
|
||||
Some(t) => serde_json::from_str(t).map_err(|e| format!("params: {e}"))?,
|
||||
None => json!({}),
|
||||
@@ -552,10 +627,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `mcp setup`: add the server to each installed AI client, with this program's own path.
|
||||
fn cmd_mcp_setup(args: &[String]) -> Result<(), String> {
|
||||
use solidcraft_engine::agent_setup as setup;
|
||||
let (lines, ok) = setup::setup(args, &setup::Env::from_process(), &setup::current_exe()?)?;
|
||||
for l in lines {
|
||||
println!("{l}");
|
||||
}
|
||||
if ok { Ok(()) } else { Err("mcp setup: some clients could not be configured".into()) }
|
||||
}
|
||||
|
||||
/// 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. 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> {
|
||||
if matches!(args.first().map(String::as_str), Some("setup" | "install")) {
|
||||
return cmd_mcp_setup(&args[1..]);
|
||||
}
|
||||
let render_gpu = args.iter().any(|arg| arg == "--render-gpu");
|
||||
if render_gpu && flag(args, "--connect").is_some() {
|
||||
return Err("mcp: --render-gpu installs a headless image host; a connected application owns its own device".into());
|
||||
}
|
||||
install_renderer();
|
||||
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)));
|
||||
@@ -599,18 +691,49 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
Box::new(remote)
|
||||
}
|
||||
(None, Some(path)) => Box::new(solidcraft_mcp::Headless::new(load_checked(path, param_check)?)),
|
||||
(None, None) => {
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
(None, Some(path)) => {
|
||||
let mut headless = solidcraft_mcp::Headless::new(load_checked(path, param_check)?);
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Box::new(headless)
|
||||
}
|
||||
(None, None) => {
|
||||
let auto = !args.iter().any(|a| a == "--headless");
|
||||
if auto && let Some(port) = discover_app() {
|
||||
eprintln!("solidcraft mcp: bridged to the running app at 127.0.0.1:{port} (--headless starts a session of its own)");
|
||||
return serve_mcp(Box::new(solidcraft_mcp::Remote::new(&format!("127.0.0.1:{port}"), flag(args, "--token"))), scope);
|
||||
}
|
||||
let mut headless = solidcraft_mcp::Headless::default();
|
||||
headless.session.param_check = param_check;
|
||||
if render_gpu {
|
||||
headless.session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
if auto {
|
||||
// The app may start after the agent: look for it again until there is work here.
|
||||
Box::new(solidcraft_mcp::Auto::new(headless, Box::new(discover_app), flag(args, "--token")))
|
||||
} else {
|
||||
Box::new(headless)
|
||||
}
|
||||
}
|
||||
};
|
||||
serve_mcp(backend, scope)
|
||||
}
|
||||
|
||||
fn serve_mcp(backend: Box<dyn solidcraft_mcp::Backend>, scope: Option<solidcraft_engine::io::vfs::PathScope>) -> Result<(), String> {
|
||||
let stdin = std::io::stdin();
|
||||
let stdout = std::io::stdout();
|
||||
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The port of the running desktop app: the one its discovery file names, if something listens
|
||||
/// there (a file left by an app that crashed names a closed port).
|
||||
fn discover_app() -> Option<u16> {
|
||||
let port = solidcraft_engine::control_auth::read_current_port()?;
|
||||
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
|
||||
std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(500)).ok().map(|_| port)
|
||||
}
|
||||
|
||||
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
|
||||
/// parameter `save_round` set to the round number each time.
|
||||
fn cmd_save_stress(args: &[String]) -> Result<(), String> {
|
||||
@@ -721,7 +844,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
return Ok(());
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let s = Session::opened(doc, saved.as_ref());
|
||||
let s = Session::opened_at(doc, saved.as_ref(), (!step).then_some(path.as_str()));
|
||||
drop(read);
|
||||
// Taken from the file: nothing evaluated (SOLIDCRAFT_NO_MODEL_CACHE evaluates anyway), or
|
||||
// only the features whose state a thinned file left out.
|
||||
|
||||
@@ -1,25 +1,89 @@
|
||||
//! Complete headless image-job workflow through the same engine commands as the UI/MCP.
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::{
|
||||
doc::render_settings::RenderSetup,
|
||||
render::job::{CpuHost, JobId, Phase},
|
||||
Session,
|
||||
doc::render_settings::{RenderJob, RenderSetup},
|
||||
render::job::{CpuHost, JobId, Phase, ToneMap},
|
||||
};
|
||||
|
||||
/// Only explicit runtime CLI flags are stored here. A typed setup owns these
|
||||
/// values even when a supplied flag happens to match the saved value.
|
||||
#[derive(Default)]
|
||||
struct RuntimeOptions {
|
||||
progressive: bool,
|
||||
transparent: bool,
|
||||
samples: Option<u32>,
|
||||
max_depth: Option<u32>,
|
||||
seed: Option<u32>,
|
||||
lighting: Option<String>,
|
||||
tone_map: Option<ToneMap>,
|
||||
environment: Option<String>,
|
||||
}
|
||||
|
||||
impl RuntimeOptions {
|
||||
fn explicit_flag(&self) -> Option<&'static str> {
|
||||
[
|
||||
(self.progressive, "--progressive"),
|
||||
(self.transparent, "--transparent"),
|
||||
(self.samples.is_some(), "--samples"),
|
||||
(self.max_depth.is_some(), "--max-depth"),
|
||||
(self.seed.is_some(), "--seed"),
|
||||
(self.lighting.is_some(), "--lighting"),
|
||||
(self.tone_map.is_some(), "--tone-map"),
|
||||
(self.environment.is_some(), "--environment"),
|
||||
]
|
||||
.into_iter()
|
||||
.find_map(|(present, flag)| present.then_some(flag))
|
||||
}
|
||||
|
||||
fn request(&self, session: &Session, settings: RenderSetup) -> Result<(Value, bool), String> {
|
||||
if let Some(job) = &settings.job {
|
||||
if let Some(flag) = self.explicit_flag() {
|
||||
return Err(format!("{flag} cannot accompany a typed saved render setup; change the saved job options instead"));
|
||||
}
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
session.doc.render.validate_resources(&settings).map_err(|e| e.to_string())?;
|
||||
let needs_gpu = matches!(job, RenderJob::GpuPath { .. });
|
||||
return Ok((json!({"settings":settings}), needs_gpu));
|
||||
}
|
||||
// Legacy setups and unnamed requests retain their runtime wire contract.
|
||||
settings.validate().map_err(|e| e.to_string())?;
|
||||
let mut params = json!({"settings":settings,"transparent_background":self.transparent,
|
||||
"progressive":self.progressive,"samples":self.samples,"max_depth":self.max_depth,"seed":self.seed});
|
||||
if let Some(path) = &self.lighting {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read lighting: {e}"))?;
|
||||
let lighting: solidcraft_engine::render::job::StudioLighting = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
params["lighting"] = json!(lighting);
|
||||
}
|
||||
if let Some(tone_map) = self.tone_map {
|
||||
params["tone_map"] = json!(tone_map);
|
||||
}
|
||||
if let Some(path) = &self.environment {
|
||||
let text = solidcraft_engine::io::vfs::read_to_string(path, 65536).map_err(|e| format!("could not read environment: {e}"))?;
|
||||
params["environment"] = serde_json::from_str(&text).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok((params, self.progressive))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(args: &[String]) -> Result<(), String> {
|
||||
let input = args.first().filter(|p| !p.starts_with('-')).ok_or("render requires an input design or script")?;
|
||||
let mut out = None;
|
||||
let mut setup = None;
|
||||
let mut width = None;
|
||||
let mut height = None;
|
||||
let mut transparent = false;
|
||||
let mut exposure = None;
|
||||
let mut runtime = RuntimeOptions::default();
|
||||
let mut overwrite = false;
|
||||
let mut timeout = 120u64;
|
||||
let mut i = 1;
|
||||
while let Some(arg) = args.get(i) {
|
||||
match arg.as_str() {
|
||||
"--transparent" => transparent = true,
|
||||
"--progressive" => runtime.progressive = true,
|
||||
"--transparent" => runtime.transparent = true,
|
||||
"--overwrite" => overwrite = true,
|
||||
"--out" | "--setup" | "--width" | "--height" | "--timeout" => {
|
||||
"--out" | "--setup" | "--width" | "--height" | "--timeout" | "--samples" | "--max-depth" | "--seed" | "--exposure" | "--lighting"
|
||||
| "--tone-map" | "--environment" => {
|
||||
i += 1;
|
||||
let value = args.get(i).ok_or_else(|| format!("missing value for {arg}"))?;
|
||||
match arg.as_str() {
|
||||
@@ -27,6 +91,20 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
"--setup" => setup = Some(value.clone()),
|
||||
"--width" => width = Some(value.parse::<u32>().map_err(|_| "invalid width")?),
|
||||
"--height" => height = Some(value.parse::<u32>().map_err(|_| "invalid height")?),
|
||||
"--samples" => runtime.samples = Some(value.parse::<u32>().map_err(|_| "invalid samples")?),
|
||||
"--max-depth" => runtime.max_depth = Some(value.parse::<u32>().map_err(|_| "invalid depth")?),
|
||||
"--seed" => runtime.seed = Some(value.parse::<u32>().map_err(|_| "invalid seed")?),
|
||||
"--exposure" => exposure = Some(value.parse::<f64>().map_err(|_| "invalid exposure")?),
|
||||
// Defer file reads until a saved job has rejected conflicting controls.
|
||||
"--environment" => runtime.environment = Some(value.clone()),
|
||||
"--lighting" => runtime.lighting = Some(value.clone()),
|
||||
"--tone-map" => {
|
||||
runtime.tone_map = Some(match value.as_str() {
|
||||
"reinhard" => ToneMap::Reinhard,
|
||||
"aces_fitted" => ToneMap::AcesFitted,
|
||||
_ => return Err("tone-map must be reinhard or aces_fitted".into()),
|
||||
})
|
||||
}
|
||||
_ => timeout = value.parse::<u64>().map_err(|_| "invalid timeout")?,
|
||||
}
|
||||
}
|
||||
@@ -39,18 +117,10 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
let out = out.ok_or("render requires --out FILE.png")?;
|
||||
let mut session = crate::load(input)?;
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
let mut settings = if let Some(name) = setup {
|
||||
session.doc.render.setups.get(&name).cloned().ok_or("no saved render setup with that name")?
|
||||
let mut settings = if let Some(name) = setup.as_deref() {
|
||||
session.doc.render.setups.get(name).cloned().ok_or("no saved render setup with that name")?
|
||||
} else {
|
||||
let mut settings = RenderSetup::default();
|
||||
if let Some(camera) = session.camera {
|
||||
settings.camera = camera;
|
||||
} else {
|
||||
settings.camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
}
|
||||
settings
|
||||
RenderSetup::default()
|
||||
};
|
||||
if let Some(width) = width {
|
||||
settings.width = width;
|
||||
@@ -58,7 +128,27 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
if let Some(height) = height {
|
||||
settings.height = height;
|
||||
}
|
||||
let params = json!({"settings":settings,"transparent_background":transparent});
|
||||
if let Some(exposure) = exposure {
|
||||
settings.exposure_ev = exposure;
|
||||
}
|
||||
// Resolve saved intent before opening a GPU or explicitly settling geometry.
|
||||
// Width/height/exposure only change this validated inline settings clone.
|
||||
let (mut params, needs_gpu) = runtime.request(&session, settings)?;
|
||||
session.set_render_job_host(Some(CpuHost::for_platform().map_err(|e| e.to_string())?)).map_err(|e| e.to_string())?;
|
||||
if needs_gpu {
|
||||
session.register_render_job_host(solidcraft_gpu::path::host().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
}
|
||||
session.execute("render.scene", &json!({})).map_err(|e| e.to_string())?;
|
||||
if setup.is_none() {
|
||||
let camera = if let Some(camera) = session.camera {
|
||||
camera
|
||||
} else {
|
||||
let mut camera = RenderSetup::default().camera;
|
||||
camera.fit(&solidcraft_engine::product_scene::extract(&session).map_err(|e| e.to_string())?.bounds);
|
||||
camera
|
||||
};
|
||||
params["settings"]["camera"] = json!(camera);
|
||||
}
|
||||
let preflight = session.execute("render.preflight", ¶ms).map_err(|e| e.to_string())?;
|
||||
let started = session.execute("render.start", ¶ms).map_err(|e| e.to_string())?;
|
||||
let id: JobId = serde_json::from_value(started["job"].clone()).map_err(|e| e.to_string())?;
|
||||
|
||||
@@ -395,8 +395,10 @@ fn actions(s: &Session) -> Vec<Action> {
|
||||
let r = round3((shortest * 0.1).max(0.01));
|
||||
let mut add = |action, act| out.push(Action { entity: entity.clone(), action, act });
|
||||
if !smooth(e, mid) {
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"edges": [pt(mid)], "distance": r})));
|
||||
// The midpoint is interior to this edge, but other bodies (including component
|
||||
// copies in their local frames) can have coincident edges. Retain the picked body.
|
||||
add("edge.fillet", Act::Run("solid.fillet", json!({"body": world, "edges": [pt(mid)], "radius": r})));
|
||||
add("edge.chamfer", Act::Run("solid.chamfer", json!({"body": world, "edges": [pt(mid)], "distance": r})));
|
||||
}
|
||||
let dir = straight(pts);
|
||||
if dir.is_some() {
|
||||
@@ -1278,6 +1280,41 @@ fn rectangle(plane: Value, p0: [f64; 2], p1: [f64; 2]) -> [Value; 3] {
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_round_actions_keep_the_picked_body() {
|
||||
for (corner, length) in [([10, 0, 20], 4), ([0, 0, 0], 20)] {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Plate"})).unwrap();
|
||||
s.execute(
|
||||
"solid.box",
|
||||
&json!({"corner": corner, "length": length, "width": length, "height": length, "body_name": "Block", "operation": "new"}),
|
||||
)
|
||||
.unwrap();
|
||||
let mut tried = 0;
|
||||
for a in actions(&s) {
|
||||
if !a.entity.starts_with("Plate/") || !matches!(a.action, "edge.chamfer" | "edge.fillet") {
|
||||
continue;
|
||||
}
|
||||
let Act::Run(id, ref p) = a.act else { continue };
|
||||
let q = &p["edges"][0];
|
||||
let x = q[0].as_f64().unwrap();
|
||||
if q[1] != 0.0 || q[2] != 20.0 || !(0.0..20.0).contains(&x) {
|
||||
continue;
|
||||
}
|
||||
// Block either meets this midpoint or coincides with the whole edge.
|
||||
assert_eq!(p["body"], "Plate", "{}: {p}", a.action);
|
||||
assert_eq!(q, &json!([10.0, 0.0, 20.0]));
|
||||
let mut unscoped = p.clone();
|
||||
unscoped.as_object_mut().unwrap().remove("body");
|
||||
let err = s.scratch().execute(id, &unscoped).unwrap_err().to_string();
|
||||
assert!(err.contains("edges meet"), "{err}");
|
||||
perform(&mut s.scratch(), &a.act, &errors(&s)).unwrap();
|
||||
tried += 1;
|
||||
}
|
||||
assert_eq!(tried, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
for (cmd, size) in [("solid.fillet", "radius"), ("solid.chamfer", "distance")] {
|
||||
@@ -1412,7 +1449,7 @@ fn a_sketch_on_a_patterned_boss_top() {
|
||||
fn construction_geometry_and_holes_on_a_moved_component() {
|
||||
let moved = || {
|
||||
vec![
|
||||
json!({"command": "component.create", "params": {"name": "Arm"}}),
|
||||
json!({"command": "component.create", "params": {"name": "Arm", "grounded": false}}),
|
||||
json!({"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}}),
|
||||
json!({"command": "component.activate", "params": {"component": 0}}),
|
||||
json!({"command": "occurrence.move", "params": {"occurrence": "Arm:1", "translate": [0, 50, 0]}}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{"commands": [
|
||||
{"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10, "body_name": "Base"}},
|
||||
{"command": "component.create", "params": {"name": "Arm"}},
|
||||
{"command": "component.create", "params": {"name": "Arm", "grounded": false}},
|
||||
{"command": "solid.box", "params": {"corner": [20, 0, 0], "length": 10, "width": 4, "height": 4, "body_name": "ArmBody"}},
|
||||
{"command": "solid.fillet", "params": {"edges": [[25, 4, 4]], "radius": 1}},
|
||||
{"command": "component.activate", "params": {"component": 0}},
|
||||
|
||||
@@ -1,6 +1,84 @@
|
||||
//! An explicit help request prints usage to stdout and succeeds; an unknown command still fails.
|
||||
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Every command the CLI takes, with the name its own usage names: `inspect` is `eval`, and
|
||||
/// `licenses` is `licences`.
|
||||
const COMMANDS: &[(&str, &str)] = &[
|
||||
("run", "run"),
|
||||
("eval", "eval"),
|
||||
("inspect", "eval"),
|
||||
("script", "script"),
|
||||
("bench-open", "bench-open"),
|
||||
("bench-agent", "bench-agent"),
|
||||
("snapshot", "snapshot"),
|
||||
("render", "render"),
|
||||
("exec", "exec"),
|
||||
("commands", "commands"),
|
||||
("recipe", "recipe"),
|
||||
("oracle", "oracle"),
|
||||
("fuzz", "fuzz"),
|
||||
("fuzz-one", "fuzz-one"),
|
||||
("mcp", "mcp"),
|
||||
("crashes", "crashes"),
|
||||
("licences", "licences"),
|
||||
("licenses", "licences"),
|
||||
("gpu", "gpu"),
|
||||
("icon", "icon"),
|
||||
("bench-edit", "bench-edit"),
|
||||
("save-stress", "save-stress"),
|
||||
];
|
||||
|
||||
/// The commands the usage lists: the aliases answer to another command's name.
|
||||
const LISTED: &[&str] = &[
|
||||
"run",
|
||||
"eval",
|
||||
"script",
|
||||
"bench-open",
|
||||
"bench-agent",
|
||||
"snapshot",
|
||||
"render",
|
||||
"exec",
|
||||
"commands",
|
||||
"recipe",
|
||||
"oracle",
|
||||
"fuzz",
|
||||
"mcp",
|
||||
"crashes",
|
||||
"licences",
|
||||
"gpu",
|
||||
"icon",
|
||||
];
|
||||
|
||||
/// The command names a text mentions after `solidcraft-cli `, each one only once.
|
||||
fn mentioned(text: &str) -> Vec<&str> {
|
||||
let mut names: Vec<&str> = text.split("solidcraft-cli ").skip(1).filter_map(|rest| rest.split_whitespace().next()).collect();
|
||||
names.sort_unstable();
|
||||
names.dedup();
|
||||
names
|
||||
}
|
||||
|
||||
/// Runs the binary and waits at most `limit` for it to finish. `None` means it was still
|
||||
/// running and was killed: the fuzz run `fuzz --help` used to start goes on for minutes.
|
||||
fn run_within(limit: Duration, args: &[&str]) -> Option<(Option<i32>, String, String)> {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(args).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn().unwrap();
|
||||
let started = Instant::now();
|
||||
loop {
|
||||
match child.try_wait().unwrap() {
|
||||
Some(status) => {
|
||||
let out = child.wait_with_output().unwrap();
|
||||
return Some((status.code(), String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned()));
|
||||
}
|
||||
None if started.elapsed() >= limit => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
None => std::thread::sleep(Duration::from_millis(25)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_prints_usage_and_succeeds() {
|
||||
@@ -21,3 +99,61 @@ fn unknown_command_still_fails_with_usage() {
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).starts_with("error: usage:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_command_prints_its_own_usage() {
|
||||
for (cmd, named) in COMMANDS {
|
||||
for flag in ["--help", "-h"] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args([*cmd, flag]).output().unwrap();
|
||||
assert!(output.status.success(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(stdout.starts_with("usage:"), "{cmd} {flag}: {stdout}");
|
||||
assert_eq!(mentioned(&stdout), vec![*named], "{cmd} {flag} printed another command's usage: {stdout}");
|
||||
assert!(output.stderr.is_empty(), "{cmd} {flag}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_top_level_usage_still_lists_every_command() {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("--help").output().unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for cmd in LISTED {
|
||||
assert!(stdout.contains(&format!("solidcraft-cli {cmd}")), "{cmd}: {stdout}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_help_does_not_start_a_fuzz_run() {
|
||||
let Some((code, stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", "--help"]) else {
|
||||
panic!("`fuzz --help` kept running: it started a fuzz run instead of printing the usage");
|
||||
};
|
||||
assert_eq!(code, Some(0), "{stderr}");
|
||||
assert!(stdout.starts_with("usage:"), "{stdout}");
|
||||
assert_eq!(mentioned(&stdout), vec!["fuzz"], "{stdout}");
|
||||
assert!(stderr.is_empty(), "{stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_refuses_an_argument_it_does_not_take() {
|
||||
for arg in ["--not-a-flag", "stray"] {
|
||||
let Some((code, _stdout, stderr)) = run_within(Duration::from_secs(20), &["fuzz", arg]) else {
|
||||
panic!("`fuzz {arg}` kept running: it started a fuzz run instead of refusing the argument");
|
||||
};
|
||||
assert_ne!(code, Some(0), "`fuzz {arg}` succeeded");
|
||||
assert!(stderr.contains(arg), "`fuzz {arg}`: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_help_keeps_the_registered_gpu_request_controls() {
|
||||
for args in [vec!["--help"], vec!["render", "--help"]] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(&args).output().unwrap();
|
||||
assert!(output.status.success(), "{args:?}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stderr.is_empty());
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
for flag in ["--progressive", "--samples", "--max-depth", "--seed", "--exposure", "--lighting", "--environment", "--tone-map"] {
|
||||
assert!(stdout.contains(flag), "{args:?} lost {flag}: {stdout}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,3 +89,177 @@ fn mcp_connect_without_a_port_is_refused_at_start() {
|
||||
assert!(stderr.contains("HOST:PORT"), "{addr}: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `mcp` with no arguments finds the running app through the discovery file and uses that
|
||||
/// launch's token; with no app behind the file it stays a headless session.
|
||||
#[test]
|
||||
fn mcp_without_arguments_finds_the_running_app() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let control_dir = std::env::temp_dir().join(format!("solidcraft-mcp-discover-{}-{port}", std::process::id()));
|
||||
std::fs::create_dir_all(&control_dir).unwrap();
|
||||
let token = "fedcba9876543210".repeat(4);
|
||||
std::fs::write(control_dir.join(format!("{port}.token")), &token).unwrap();
|
||||
std::fs::write(control_dir.join("current.port"), format!("{port}\n")).unwrap();
|
||||
let app = std::thread::spawn(move || {
|
||||
// The discovery probe connects and leaves without a request; the tool call follows.
|
||||
let (stream, line) = loop {
|
||||
let (stream, _) = listener.accept().unwrap();
|
||||
let mut line = String::new();
|
||||
BufReader::new(stream.try_clone().unwrap()).read_line(&mut line).unwrap();
|
||||
if !line.is_empty() {
|
||||
break (stream, line);
|
||||
}
|
||||
};
|
||||
let req: Value = serde_json::from_str(&line).unwrap();
|
||||
let ok = req["token"] == json!(token);
|
||||
let reply = json!({"id": req["id"], "ok": ok, "result": {"bodies": [], "method": req["method"]}, "error": "unauthorized"});
|
||||
writeln!(&stream, "{reply}").unwrap();
|
||||
});
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "--allow-any-path"])
|
||||
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
|
||||
.env_remove("SOLIDCRAFT_CONTROL_TOKEN")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
let mut stdout = BufReader::new(child.stdout.take().unwrap());
|
||||
writeln!(stdin, "{}", json!({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": "measure", "arguments": {}}})).unwrap();
|
||||
let mut line = String::new();
|
||||
stdout.read_line(&mut line).unwrap();
|
||||
let r: Value = serde_json::from_str(&line).unwrap_or_else(|e| panic!("{e}: {line:?}"));
|
||||
assert_ne!(r["result"]["isError"], true, "{r}");
|
||||
assert!(r["result"]["content"][0]["text"].as_str().unwrap_or_default().contains("engine.execute"), "{r}");
|
||||
app.join().unwrap();
|
||||
drop(stdin);
|
||||
assert!(child.wait().unwrap().success());
|
||||
|
||||
// The file names a port nothing listens on (the app crashed): a headless session instead.
|
||||
let closed = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let dead = closed.local_addr().unwrap().port();
|
||||
drop(closed);
|
||||
std::fs::write(control_dir.join("current.port"), format!("{dead}\n")).unwrap();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "--allow-any-path"])
|
||||
.env("SOLIDCRAFT_CONTROL_DIR", &control_dir)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!String::from_utf8_lossy(&out.stderr).contains("bridged to the running app"), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
std::fs::remove_dir_all(&control_dir).ok();
|
||||
}
|
||||
|
||||
/// `mcp setup` against a temporary home: the client folders it finds are the only ones it writes.
|
||||
mod setup {
|
||||
use serde_json::{Value, json};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn home(name: &str) -> PathBuf {
|
||||
let h = std::env::temp_dir().join(format!("solidcraft-mcp-setup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&h);
|
||||
std::fs::create_dir_all(&h).unwrap();
|
||||
h
|
||||
}
|
||||
|
||||
fn run(home: &Path, args: &[&str]) -> std::process::Output {
|
||||
Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"))
|
||||
.args(["mcp", "setup"])
|
||||
.args(args)
|
||||
.env_clear()
|
||||
.env("HOME", home)
|
||||
.env("USERPROFILE", home)
|
||||
.env("APPDATA", home.join("AppData/Roaming"))
|
||||
.env("LOCALAPPDATA", home.join("AppData/Local"))
|
||||
.env("XDG_CONFIG_HOME", home.join(".config"))
|
||||
.env("PATH", "")
|
||||
.output()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn json_of(p: &Path) -> Value {
|
||||
serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn exe() -> String {
|
||||
let p = Path::new(env!("CARGO_BIN_EXE_solidcraft-cli")).canonicalize().unwrap();
|
||||
p.to_string_lossy().trim_start_matches(r"\\?\").to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merges_into_opencode_and_cursor_keeping_other_servers() {
|
||||
let h = home("merge");
|
||||
let oc = h.join(".config/opencode");
|
||||
std::fs::create_dir_all(&oc).unwrap();
|
||||
let old = json!({"$schema": "https://opencode.ai/config.json", "theme": "x", "mcp": {"other": {"type": "local", "command": ["o"]}}});
|
||||
std::fs::write(oc.join("opencode.json"), old.to_string()).unwrap();
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), r#"{"mcpServers": {"other": {"command": "o"}}}"#).unwrap();
|
||||
|
||||
let out = run(&h, &["--live"]);
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let v = json_of(&oc.join("opencode.json"));
|
||||
assert_eq!(v["theme"], "x");
|
||||
assert_eq!(v["mcp"]["other"], old["mcp"]["other"]);
|
||||
assert_eq!(v["mcp"]["solidcraft"], json!({"type": "local", "command": [exe(), "mcp"], "enabled": true}));
|
||||
assert_eq!(v["mcp"]["solidcraft-live"]["command"], json!([exe(), "mcp", "--connect", "127.0.0.1:7878"]));
|
||||
assert_eq!(json_of(&oc.join("opencode.json.bak")), old);
|
||||
let c = json_of(&h.join(".cursor/mcp.json"));
|
||||
assert_eq!(c["mcpServers"]["other"], json!({"command": "o"}));
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"command": exe(), "args": ["mcp"]}));
|
||||
|
||||
// Codex (TOML) and Claude Code (a big file the CLI owns) are edited in place too.
|
||||
std::fs::create_dir_all(h.join(".codex")).unwrap();
|
||||
std::fs::write(h.join(".codex/config.toml"), "# mine\nmodel = \"x\"\n").unwrap();
|
||||
let cc = "{\n \"numStartups\": 3,\n \"projects\": {}\n}\n";
|
||||
std::fs::write(h.join(".claude.json"), cc).unwrap();
|
||||
assert!(run(&h, &[]).status.success());
|
||||
let toml = std::fs::read_to_string(h.join(".codex/config.toml")).unwrap();
|
||||
assert!(toml.starts_with("# mine\nmodel = \"x\"\n") && toml.contains("[mcp_servers.solidcraft]"), "{toml}");
|
||||
let c = json_of(&h.join(".claude.json"));
|
||||
assert_eq!(c["numStartups"], 3);
|
||||
assert_eq!(c["mcpServers"]["solidcraft"], json!({"type": "stdio", "command": exe(), "args": ["mcp"]}));
|
||||
assert_eq!(std::fs::read_to_string(h.join(".claude.json.bak")).unwrap(), cc);
|
||||
|
||||
// Running again updates the same entries and leaves one of each.
|
||||
assert!(run(&h, &[]).status.success());
|
||||
assert_eq!(json_of(&h.join(".cursor/mcp.json"))["mcpServers"].as_object().unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dry_run_writes_nothing_and_missing_clients_are_skipped() {
|
||||
let h = home("dry");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
let out = run(&h, &["--dry-run"]);
|
||||
assert!(out.status.success());
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(text.contains("would write") && text.contains("solidcraft"), "{text}");
|
||||
assert!(!h.join(".cursor/mcp.json").exists());
|
||||
assert!(!h.join(".config/opencode").exists(), "OpenCode was not installed, so nothing is created for it");
|
||||
let out = run(&h, &["--client", "claude-desktop"]);
|
||||
assert!(String::from_utf8_lossy(&out.stdout).contains("not found"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_that_is_not_json_is_left_alone() {
|
||||
let h = home("bad");
|
||||
std::fs::create_dir_all(h.join(".cursor")).unwrap();
|
||||
std::fs::write(h.join(".cursor/mcp.json"), "{ // comment").unwrap();
|
||||
let out = run(&h, &[]);
|
||||
assert!(!out.status.success());
|
||||
assert_eq!(std::fs::read_to_string(h.join(".cursor/mcp.json")).unwrap(), "{ // comment");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claude_desktop_msix_folder_is_found() {
|
||||
let h = home("msix");
|
||||
let pkg = h.join("AppData/Local/Packages/Claude_abc/LocalCache/Roaming/Claude");
|
||||
std::fs::create_dir_all(&pkg).unwrap();
|
||||
// The MSIX folder is only read through LOCALAPPDATA, set for every platform here.
|
||||
assert!(run(&h, &["--client", "claude-desktop"]).status.success());
|
||||
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Explicit native GPU registration must not change bridged MCP ownership.
|
||||
use std::process::Command;
|
||||
#[test]
|
||||
fn connected_application_rejects_headless_gpu_installation_before_connecting() {
|
||||
let result = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).args(["mcp", "--render-gpu", "--connect", "127.0.0.1:1"]).output().unwrap();
|
||||
assert!(!result.status.success());
|
||||
let message = String::from_utf8_lossy(&result.stderr);
|
||||
assert!(message.contains("connected application owns its own device"), "{message}");
|
||||
}
|
||||
@@ -22,6 +22,19 @@ impl Scratch {
|
||||
std::fs::write(&path, json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}}]}).to_string()).unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
fn saving_script(&self) -> (PathBuf, PathBuf) {
|
||||
let path = self.0.join("saving.json");
|
||||
let saved = self.0.join("script-ran.solidcraft");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands": [{"command": "solid.box", "params": {"length": 2, "width": 3, "height": 4}},
|
||||
{"command": "file.save_as", "params": {"path": saved}}]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
(path, saved)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Scratch {
|
||||
@@ -43,6 +56,92 @@ fn body_count(path: &Path) -> Value {
|
||||
json_output(cli().arg("eval").arg(path).output().unwrap())["body_count"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_rejects_invalid_options_before_script_execution_or_output_writes() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--out"][..], "--out needs a value"),
|
||||
(&["--save"][..], "--save needs a value"),
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--out", "--quiet"][..], "--out needs a value"),
|
||||
(&["--ouut", "part.step"][..], "doesn't take --ouut"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for existing in [false, true] {
|
||||
let saved = dir.0.join(format!("{existing}.solidcraft"));
|
||||
let exported = dir.0.join(format!("{existing}.step"));
|
||||
let report = dir.0.join(format!("{existing}.json"));
|
||||
let outputs = [&saved, &exported, &report];
|
||||
if existing {
|
||||
for path in outputs {
|
||||
std::fs::write(path, b"existing output").unwrap();
|
||||
}
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli()
|
||||
.arg("run")
|
||||
.arg(&script)
|
||||
.arg("--save")
|
||||
.arg(&saved)
|
||||
.arg("--out")
|
||||
.arg(&exported)
|
||||
.arg("--json-out")
|
||||
.arg(&report)
|
||||
.args(extra)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{extra:?}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains(message), "{extra:?}: {}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
for path in outputs {
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(path).unwrap(), b"existing output");
|
||||
} else {
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eval_and_inspect_reject_invalid_options_before_script_execution_or_reporting() {
|
||||
let dir = Scratch::new();
|
||||
let (script, script_saved) = dir.saving_script();
|
||||
let cases = [
|
||||
(&["--in"][..], "--in needs a value"),
|
||||
(&["--in", "--quiet"][..], "--in needs a value"),
|
||||
(&["--out", "part.step"][..], "doesn't take --out"),
|
||||
(&["--unknown"][..], "doesn't take --unknown"),
|
||||
];
|
||||
for command in ["eval", "inspect"] {
|
||||
for existing in [false, true] {
|
||||
let report = dir.0.join(format!("{command}-{existing}.json"));
|
||||
if existing {
|
||||
std::fs::write(&report, b"existing report").unwrap();
|
||||
}
|
||||
for (extra, message) in cases {
|
||||
let output = cli().arg(command).arg(&script).arg("--json-out").arg(&report).args(extra).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1), "{command} {extra:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains(message),
|
||||
"{command} {extra:?}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(!script_saved.exists(), "invalid arguments must be rejected before the script runs");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&report).unwrap(), b"existing report");
|
||||
} else {
|
||||
assert!(!report.exists());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_loads_both_design_encodings_by_contents() {
|
||||
let dir = Scratch::new();
|
||||
@@ -59,6 +158,84 @@ fn cli_loads_both_design_encodings_by_contents() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #1369: a pending concept that did not land in the numbered release must not be silently
|
||||
/// dropped on open/resave, in either encoding or before replacing an existing destination.
|
||||
#[test]
|
||||
fn unknown_pending_concepts_preserve_the_open_design_and_output_files() {
|
||||
use solidcraft_engine::{Session, doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
for version in [33, doc::format::FORMAT] {
|
||||
let mut experimental = doc::Document::new("Experimental");
|
||||
experimental.format = doc::format::format_string(version);
|
||||
experimental.format_pending.push("experimental-card".into());
|
||||
for (extension, bytes) in [("json", io::write_design(&experimental)), ("solidcraft", io::write_design_container(&experimental).unwrap())] {
|
||||
let source = dir.0.join(format!("experimental-{version}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let mut session = Session::default();
|
||||
session.execute("solid.box", &json!({"length": 2, "width": 3, "height": 4})).unwrap();
|
||||
let original = session.doc.clone();
|
||||
let original_path = session.path.clone();
|
||||
let undo = session.undo.len();
|
||||
let dirty = session.is_dirty();
|
||||
let err = session.execute("doc.open", &json!({"path": source})).unwrap_err().to_string();
|
||||
assert!(err.contains("experimental-card") && err.contains("update SolidCraft"), "{err}");
|
||||
assert_eq!(session.doc, original);
|
||||
assert_eq!(session.path, original_path);
|
||||
assert_eq!(session.undo.len(), undo);
|
||||
assert_eq!(session.is_dirty(), dirty);
|
||||
|
||||
for existing in [false, true] {
|
||||
let destination = dir.0.join(format!("output-{version}-{extension}-{existing}.{extension}"));
|
||||
let backup = io::backup_path(&destination);
|
||||
if existing {
|
||||
std::fs::write(&destination, b"existing design").unwrap();
|
||||
std::fs::write(&backup, b"existing backup").unwrap();
|
||||
}
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("experimental-card") && error.contains("update SolidCraft"), "{error}");
|
||||
if existing {
|
||||
assert_eq!(std::fs::read(&destination).unwrap(), b"existing design");
|
||||
assert_eq!(std::fs::read(&backup).unwrap(), b"existing backup");
|
||||
} else {
|
||||
assert!(!destination.exists() && !backup.exists());
|
||||
}
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
|
||||
use solidcraft_engine::{doc, io};
|
||||
|
||||
let dir = Scratch::new();
|
||||
let current = doc::Document::new("Supported");
|
||||
let mut released = current.clone();
|
||||
released.format = doc::format::format_string(33);
|
||||
released.format_pending.clear();
|
||||
let mut experimental = released.clone();
|
||||
experimental.format_pending.push("reserved-body-names".into());
|
||||
for (index, mut document) in [current, released, experimental].into_iter().enumerate() {
|
||||
document.metadata.0.insert("com.example.tool".into(), json!({"format_pending": ["experimental-card"], "physical_card": {"density": 700}}));
|
||||
for (extension, bytes) in [("json", io::write_design(&document)), ("solidcraft", io::write_design_container(&document).unwrap())] {
|
||||
let source = dir.0.join(format!("supported-{index}.{extension}"));
|
||||
let destination = dir.0.join(format!("resaved-{index}.{extension}"));
|
||||
std::fs::write(&source, &bytes).unwrap();
|
||||
let output = cli().args(["run", "--in"]).arg(&source).arg("--save").arg(&destination).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let reopened = io::read_design(&std::fs::read(&destination).unwrap()).unwrap();
|
||||
assert_eq!(reopened.metadata, document.metadata);
|
||||
assert_eq!(reopened.name, document.name);
|
||||
assert_eq!(reopened.features, document.features);
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_still_save_and_export_the_script_once() {
|
||||
let dir = Scratch::new();
|
||||
@@ -209,3 +386,175 @@ fn timings_and_profile_report_each_command() {
|
||||
assert!(!output.status.success());
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains("--timings needs a script"));
|
||||
}
|
||||
|
||||
fn render_setup_script(dir: &Scratch, job: Value) -> PathBuf {
|
||||
let path = dir.0.join("render.json");
|
||||
std::fs::write(
|
||||
&path,
|
||||
json!({"commands":[
|
||||
{"command":"solid.box","params":{"length":10,"width":10,"height":10}},
|
||||
{"command":"render.settings.set","params":{"name":"Portable setup","set":{"width":16,"height":16,"job":job}}}
|
||||
]})
|
||||
.to_string(),
|
||||
)
|
||||
.unwrap();
|
||||
path
|
||||
}
|
||||
|
||||
#[cfg(feature = "raytrace-cpu")]
|
||||
#[test]
|
||||
fn render_uses_typed_cpu_setups_from_scripts_and_saved_designs() {
|
||||
for options in [json!({}), json!({"progressive":{"samples_per_pixel":3,"seed":81},"lights":[]})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":options}));
|
||||
let image = dir.0.join("script.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&image)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["integrator"], "raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(&image).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (16, 16));
|
||||
|
||||
let saved = dir.0.join("portable.solidcraft");
|
||||
json_output(cli().arg("run").arg(&script).arg("--save").arg(&saved).output().unwrap());
|
||||
let original = std::fs::read(&saved).unwrap();
|
||||
let overridden = dir.0.join("overridden.png");
|
||||
let result = json_output(
|
||||
cli()
|
||||
.arg("render")
|
||||
.arg(&saved)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&overridden)
|
||||
.args(["--width", "12", "--height", "8", "--exposure", "1.25"])
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-cpu")
|
||||
.output()
|
||||
.unwrap(),
|
||||
);
|
||||
assert_eq!(result["preflight"]["request"]["width"], 12);
|
||||
assert_eq!(result["preflight"]["request"]["height"], 8);
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"]["exposure_ev"], 1.25);
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-raytrace");
|
||||
assert_eq!(result["preflight"]["request"]["raytrace"].get("progressive"), options.get("progressive"));
|
||||
let png = image::load_from_memory(&std::fs::read(overridden).unwrap()).unwrap();
|
||||
assert_eq!((png.width(), png.height()), (12, 8));
|
||||
assert_eq!(std::fs::read(saved).unwrap(), original, "CLI overrides must not rewrite saved intent");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preserves_typed_raster_and_legacy_runtime_controls() {
|
||||
for job in [Value::Null, json!({"method":"raster","transparent_background":true})] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("raster.png");
|
||||
let mut command = cli();
|
||||
command
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-raster");
|
||||
if job.is_null() {
|
||||
command.arg("--transparent");
|
||||
}
|
||||
let result = json_output(command.output().unwrap());
|
||||
assert_eq!(result["preflight"]["resolved"]["executor"], "cpu-product");
|
||||
assert_eq!(result["export"]["frame"]["executor"], "cpu-product");
|
||||
assert_eq!(result["preflight"]["request"]["transparent_background"], true);
|
||||
assert!(std::fs::read(out).unwrap().starts_with(b"\x89PNG\r\n\x1a\n"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_render_rejects_explicit_runtime_flags_before_gpu_or_option_file_reads() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"gpu_path","options":{"samples":64,"seed":0}}));
|
||||
let out = dir.0.join("existing.png");
|
||||
std::fs::write(&out, b"previous completed image").unwrap();
|
||||
for flags in [
|
||||
&["--progressive"][..],
|
||||
&["--transparent"][..],
|
||||
&["--samples", "64"][..],
|
||||
&["--max-depth", "8"][..],
|
||||
&["--seed", "0"][..],
|
||||
&["--lighting", "deliberately-missing-lighting.json"][..],
|
||||
&["--tone-map", "reinhard"][..],
|
||||
&["--environment", "deliberately-missing-environment.json"][..],
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.arg("--overwrite")
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-conflicts")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(&format!("{} cannot accompany a typed saved render setup", flags[0])), "{error}");
|
||||
assert!(output.stdout.is_empty());
|
||||
assert_eq!(std::fs::read(&out).unwrap(), b"previous completed image");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_gpu_render_selects_its_host_without_a_progressive_runtime_flag() {
|
||||
// A deliberately invalid policy proves that dispatch reached the GPU host
|
||||
// without depending on an adapter or accepting a CPU fallback. The legacy
|
||||
// --progressive path must still reach that same host-selection boundary.
|
||||
for job in [json!({"method":"gpu_path","options":{"samples":2}}), Value::Null] {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, job.clone());
|
||||
let out = dir.0.join("gpu.png");
|
||||
let mut command = cli();
|
||||
command.arg("render").arg(&script).args(["--setup", "Portable setup", "--out"]).arg(&out).env("SOLIDCRAFT_GPU", "not-a-gpu-policy");
|
||||
if job.is_null() {
|
||||
command.arg("--progressive");
|
||||
}
|
||||
let output = command.output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains("SOLIDCRAFT_GPU must be auto, hardware, software or cpu"), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_setup_overrides_obey_shared_validation_before_host_initialization() {
|
||||
let dir = Scratch::new();
|
||||
let script = render_setup_script(&dir, json!({"method":"cpu_raytrace","options":{"progressive":{"samples_per_pixel":64}}}));
|
||||
let out = dir.0.join("invalid.png");
|
||||
// 1024 * 1024 * 64 exceeds the unchanged default Compact camera-sample
|
||||
// budget. Admission must name that envelope before initializing any host.
|
||||
for (flags, message) in [
|
||||
(&["--width", "1024", "--height", "1024"][..], "CPU Compact exceeds its 16777216 camera-sample admission"),
|
||||
(&["--width", "0"][..], "render output must be at least"),
|
||||
(&["--exposure", "NaN"][..], "render exposure must be finite"),
|
||||
] {
|
||||
let output = cli()
|
||||
.arg("render")
|
||||
.arg(&script)
|
||||
.args(["--setup", "Portable setup", "--out"])
|
||||
.arg(&out)
|
||||
.args(flags)
|
||||
.env("SOLIDCRAFT_GPU", "must-not-be-read-for-invalid-settings")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(error.contains(message), "{error}");
|
||||
assert!(!out.exists());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,17 @@ workspace = true
|
||||
name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
url = "2"
|
||||
percent-encoding = "2"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
@@ -30,5 +36,6 @@ web-sys = { version = "0.3", features = [
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"Headers", "ReadableStream", "ReadableStreamDefaultReader",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
] }
|
||||
|
||||
@@ -114,6 +114,46 @@ pub fn opens_design(extensions: &[&str]) -> bool {
|
||||
extensions.is_empty() || extensions.iter().any(|e| solidcraft_engine::io::DESIGN_EXTS.contains(e))
|
||||
}
|
||||
|
||||
/// Where an Open for `extensions` (what the command reads; `&[]`: anything) is answered.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OpenRoute {
|
||||
/// The browser's open dialog for a design: the file keeps its handle, so Save writes back.
|
||||
Disk,
|
||||
/// The "Files in this browser" window, where the browser can't open a design from the disk.
|
||||
Panel,
|
||||
/// A plain file input that offers these types: the file is read into the engine's files and
|
||||
/// the command runs on it (Insert DXF or SVG, images, meshes, imported settings).
|
||||
Upload,
|
||||
}
|
||||
|
||||
/// Choose how an Open for `extensions` asks for its file; `api`: the browser can open a file
|
||||
/// from the disk and keep its handle.
|
||||
pub fn open_route(api: bool, extensions: &[&str]) -> OpenRoute {
|
||||
match (opens_design(extensions), api) {
|
||||
(true, true) => OpenRoute::Disk,
|
||||
(true, false) => OpenRoute::Panel,
|
||||
(false, _) => OpenRoute::Upload,
|
||||
}
|
||||
}
|
||||
|
||||
/// The `accept` list of a file input for `extensions` (with or without their dot): ".dxf,.svg".
|
||||
pub fn accept_list(extensions: &[&str]) -> String {
|
||||
let list: Vec<String> = extensions
|
||||
.iter()
|
||||
.map(|e| e.trim().trim_start_matches('.'))
|
||||
.filter(|e| !e.is_empty())
|
||||
.map(|e| format!(".{}", e.to_ascii_lowercase()))
|
||||
.collect();
|
||||
list.join(",")
|
||||
}
|
||||
|
||||
/// The largest file an upload for `extensions` reads: images go into a canvas, which has a size
|
||||
/// limit, so a bigger one is refused before it is read. `None`: the importer checks the file.
|
||||
pub fn upload_limit(extensions: &[&str]) -> Option<usize> {
|
||||
let image = |e: &&str| ["png", "jpg", "jpeg"].contains(&e.trim_start_matches('.').to_ascii_lowercase().as_str());
|
||||
(!extensions.is_empty() && extensions.iter().all(image)).then_some(solidcraft_engine::doc::canvas::MAX_CANVAS_BYTES)
|
||||
}
|
||||
|
||||
/// The file type a Save As dialog offers for `name`: (description, MIME type, extension).
|
||||
pub fn save_type(name: &str) -> (&'static str, &'static str, &'static str) {
|
||||
let json = std::path::Path::new(name).extension().is_some_and(|e| e.eq_ignore_ascii_case("json"));
|
||||
@@ -140,7 +180,7 @@ mod web {
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
use web_sys::{FileSystemFileHandle, IdbDatabase, IdbObjectStore, IdbOpenDbRequest, IdbRequest, IdbTransactionMode};
|
||||
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, opens_design, parse, path, plan_prune, save_type};
|
||||
use super::{HANDLE_MAX_AGE_MS, is_abort, make_id, parse, path, plan_prune, save_type};
|
||||
use crate::files::{Files, err, read_handle};
|
||||
|
||||
const DB: &str = "solidcraft-disk-handles";
|
||||
@@ -294,7 +334,7 @@ mod web {
|
||||
let options = Object::new();
|
||||
let _ = Reflect::set(&options, &"multiple".into(), &JsValue::FALSE);
|
||||
let extensions = Array::new();
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf"] {
|
||||
for e in [".solidcraft", ".solvecraft", ".json", ".step", ".stp", ".3mf", ".stl", ".obj", ".glb", ".gltf", ".fcstd", ".3dm", ".dxf", ".svg"] {
|
||||
extensions.push(&e.into());
|
||||
}
|
||||
let accept = Object::new();
|
||||
@@ -315,11 +355,6 @@ mod web {
|
||||
p
|
||||
}
|
||||
|
||||
/// Does an Open for `extensions` use the browser's open dialog?
|
||||
pub fn handles_open(extensions: &[&str]) -> bool {
|
||||
supported() && opens_design(extensions)
|
||||
}
|
||||
|
||||
// ---- writing ------------------------------------------------------------------------
|
||||
|
||||
/// Queue what a command wrote to a disk path. Writes go one at a time; a newer write of the
|
||||
@@ -621,6 +656,21 @@ mod tests {
|
||||
assert!(!opens_design(&["json"]));
|
||||
}
|
||||
|
||||
/// Insert DXF or SVG, images and other non-design files get a file input that offers their
|
||||
/// types in every browser (#2052); designs keep the disk dialog or the browser's files.
|
||||
#[test]
|
||||
fn opens_of_other_files_use_a_file_input_for_their_types() {
|
||||
assert_eq!(open_route(true, &["dxf"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(false, &["png", "jpg", "jpeg"]), OpenRoute::Upload);
|
||||
assert_eq!(open_route(true, &[]), OpenRoute::Disk);
|
||||
assert_eq!(open_route(false, &["solidcraft"]), OpenRoute::Panel);
|
||||
assert_eq!(accept_list(&["dxf"]), ".dxf");
|
||||
assert_eq!(accept_list(&["png", ".JPG", "", "jpeg"]), ".png,.jpg,.jpeg");
|
||||
assert_eq!(accept_list(&[]), "");
|
||||
assert!(upload_limit(&["png", "jpg", "jpeg"]).is_some());
|
||||
assert_eq!(upload_limit(&["dxf"]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_save_dialog_offers_the_type_of_the_name() {
|
||||
assert_eq!(save_type("Plate.solidcraft").2, ".solidcraft");
|
||||
|
||||
@@ -448,6 +448,18 @@ pub fn recovery_path() -> String {
|
||||
format!("{OPFS}{RECOVERY}")
|
||||
}
|
||||
|
||||
/// A recovery file lives in OPFS, but its relative links belong to the original design's
|
||||
/// folder (which can be on disk). Resolve them before Open sees the temporary file location.
|
||||
pub fn prepare_recovery(original_path: Option<&str>) -> Result<String, String> {
|
||||
let path = recovery_path();
|
||||
let bytes = vfs::read(&path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| e.to_string())?;
|
||||
let mut doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?;
|
||||
solidcraft_engine::resolve_design_links(&mut doc, original_path);
|
||||
let bytes = solidcraft_engine::io::write_design_container(&doc).map_err(|e| e.to_string())?;
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Pick files on the computer: the File System Access picker where there is one, else an
|
||||
/// `<input type=file>`. Read files arrive in `files.incoming`.
|
||||
pub fn pick_from_disk(files: &Rc<Files>, intent: Intent) {
|
||||
@@ -499,8 +511,10 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
// Everything the browser build imports, from the format registry (no cadmpeg formats here).
|
||||
let accept = solidcraft_engine::io::formats::open_extensions(false).iter().map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
// Everything the browser build imports, from the format registry: cadmpeg is pure Rust, so
|
||||
// FreeCAD and Rhino files are part of it; plus the DXF and SVG drawings Open and Insert take.
|
||||
let accept =
|
||||
solidcraft_engine::io::formats::open_extensions(true).iter().chain(&["dxf", "svg"]).map(|e| format!(".{e}")).collect::<Vec<_>>().join(",");
|
||||
input.set_accept(&accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
let _ = input.set_attribute("style", "display:none");
|
||||
@@ -526,6 +540,67 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
input.click();
|
||||
}
|
||||
|
||||
/// Ask for one file of these types (`extensions`, e.g. `["dxf"]`) with a plain file input, the
|
||||
/// dialog every browser has. The answer is the file's path in the engine's files (under
|
||||
/// [`UPLOAD`], as for a file picked in the disk dialog), so the command that asked runs on it;
|
||||
/// `None` when the dialog was closed or the file could not be read.
|
||||
pub fn pick_upload(files: &Rc<Files>, extensions: &[&str]) -> impl std::future::Future<Output = Option<String>> + use<> {
|
||||
// Shown now, in the click: a file dialog needs the user's gesture.
|
||||
let chosen = choose_file(&disk::accept_list(extensions));
|
||||
let limit = disk::upload_limit(extensions);
|
||||
let files = files.clone();
|
||||
async move {
|
||||
let read = async {
|
||||
let value = JsFuture::from(chosen?).await.map_err(err)?;
|
||||
// Closing the dialog answers no file: nothing to say.
|
||||
let Ok(file) = value.dyn_into::<web_sys::File>() else { return Ok(None) };
|
||||
let name = file.name();
|
||||
if limit.is_some_and(|max| !file.size().is_finite() || file.size() > max as f64) {
|
||||
return Err(format!("{name}: the file is too large"));
|
||||
}
|
||||
let bytes = blob_bytes(&file).await.map_err(|e| format!("{name}: {e}"))?;
|
||||
let path = format!("{UPLOAD}{name}");
|
||||
vfs::insert(&path, bytes);
|
||||
Ok(Some(path))
|
||||
};
|
||||
match read.await {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
files.say(format!("open: {e}"), true);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a hidden `<input type=file>` offering `accept` (call in the click). The promise answers
|
||||
/// the picked `File`, or `null` when the dialog is closed (the input's `cancel` event).
|
||||
fn choose_file(accept: &str) -> Result<js_sys::Promise, String> {
|
||||
let doc = web_sys::window().and_then(|w| w.document()).ok_or("this page can't show a file dialog")?;
|
||||
let input = doc.create_element("input").map_err(err)?.dyn_into::<web_sys::HtmlInputElement>().map_err(|_| "no file input".to_string())?;
|
||||
input.set_type("file");
|
||||
input.set_accept(accept);
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
input.set_attribute("style", "display:none").map_err(err)?;
|
||||
doc.body().ok_or("this page has no body")?.append_child(&input).map_err(err)?;
|
||||
let promise = js_sys::Promise::new(&mut |resolve: Function, _reject: Function| {
|
||||
// One answer for a pick or a cancel; read from the event, so it does not keep the input.
|
||||
let done = Closure::once_into_js(move |event: web_sys::Event| {
|
||||
let file = event.target().and_then(|t| t.dyn_into::<web_sys::HtmlInputElement>().ok()).and_then(|input| {
|
||||
input.set_onchange(None);
|
||||
input.set_oncancel(None);
|
||||
input.remove();
|
||||
input.files().and_then(|list| list.get(0))
|
||||
});
|
||||
let _ = resolve.call1(&JsValue::UNDEFINED, &file.map_or(JsValue::NULL, JsValue::from));
|
||||
});
|
||||
input.set_onchange(Some(done.unchecked_ref()));
|
||||
input.set_oncancel(Some(done.unchecked_ref()));
|
||||
});
|
||||
input.click();
|
||||
Ok(promise)
|
||||
}
|
||||
|
||||
/// Read files dropped on the page.
|
||||
pub fn take_drops(files: &Rc<Files>, raw: &mut egui::RawInput) {
|
||||
for f in std::mem::take(&mut raw.dropped_files) {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//! Bounded HTTPS or same-origin downloads, handed to the normal design-open command as unsaved copies.
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use js_sys::{Reflect, Uint8Array};
|
||||
use solidcraft_engine::io::vfs;
|
||||
use solidcraft_ui_egui::{SolveApp, ui_kit};
|
||||
use wasm_bindgen::{JsCast as _, JsValue};
|
||||
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||
|
||||
use crate::{check_download_size, design_url};
|
||||
|
||||
type Reply = Result<(String, Vec<u8>), String>;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Download {
|
||||
reply: Rc<RefCell<Option<Reply>>>,
|
||||
pending: Option<(String, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
pub fn start(url: Option<&str>, ctx: &egui::Context) -> Self {
|
||||
let download = Self::default();
|
||||
if let Some(url) = url {
|
||||
let url = url.to_owned();
|
||||
let slot = download.reply.clone();
|
||||
let ctx = ctx.clone();
|
||||
spawn_local(async move {
|
||||
*slot.borrow_mut() = Some(fetch(&url).await);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
}
|
||||
download
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if let Some(reply) = self.reply.borrow_mut().take() {
|
||||
match reply {
|
||||
Ok(file) => self.pending = Some(file),
|
||||
Err(error) => app.set_status(format!("Could not open design link: {error}"), true),
|
||||
}
|
||||
}
|
||||
if self.pending.is_none() || app.file_picker_busy() {
|
||||
return;
|
||||
}
|
||||
if solidcraft_ui_egui::documents::any_dirty(app) {
|
||||
let mut answer = None;
|
||||
let closed = ui_kit::Window::new(egui::Id::new("open_link"), "status_warning", solidcraft_ui_egui::i18n::tr("web.open_link"))
|
||||
.required()
|
||||
.show_body(ctx, |ui| {
|
||||
ui.label(solidcraft_ui_egui::i18n::tr("web.open_link_confirm"));
|
||||
ui.horizontal(|ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.open"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(true);
|
||||
}
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("ui.cancel"), ui_kit::Button::Normal).clicked() {
|
||||
answer = Some(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
match answer {
|
||||
Some(true) => {}
|
||||
Some(false) => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None if closed => {
|
||||
self.pending = None;
|
||||
return;
|
||||
}
|
||||
None => return,
|
||||
}
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = "/linked/design.solidcraft";
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
} else {
|
||||
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
|
||||
}
|
||||
let _ = vfs::remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch(value: &str) -> Reply {
|
||||
let window = web_sys::window().ok_or("No browser window")?;
|
||||
let page_origin = window.location().origin().map_err(|_| "Cannot read page origin")?;
|
||||
let url = design_url(value, &page_origin)?;
|
||||
let name = url.path_segments().and_then(|mut segments| segments.next_back()).filter(|name| !name.is_empty()).unwrap_or("Design.solidcraft");
|
||||
let decoded = js_sys::decode_uri_component(name).ok().and_then(|name| name.as_string()).unwrap_or_else(|| name.to_owned());
|
||||
let name: String = decoded.strip_suffix(".solidcraft").unwrap_or(&decoded).chars().take(255).collect();
|
||||
let response = JsFuture::from(window.fetch_with_str(url.as_str())).await.map_err(|_| "Network request failed or the host refused CORS access")?;
|
||||
let response: web_sys::Response = response.dyn_into().map_err(|_| "Invalid download response")?;
|
||||
design_url(&response.url(), &page_origin).map_err(|_| "Design redirected to a URL that does not use HTTPS or the page's own origin")?;
|
||||
if !response.ok() {
|
||||
return Err(format!("HTTP {} {}", response.status(), response.status_text()));
|
||||
}
|
||||
let length = response
|
||||
.headers()
|
||||
.get("Content-Length")
|
||||
.map_err(|_| "Cannot read download size")?
|
||||
.map(|value| value.parse::<u64>().map_err(|_| "Invalid Content-Length"))
|
||||
.transpose()?;
|
||||
if let Some(length) = length {
|
||||
check_download_size(0, length)?;
|
||||
}
|
||||
let stream = response.body().ok_or("Download has no body")?;
|
||||
let reader: web_sys::ReadableStreamDefaultReader = stream.get_reader().dyn_into().map_err(|_| "Cannot read download stream")?;
|
||||
let result = read(&reader).await;
|
||||
if result.is_err() {
|
||||
let _ = JsFuture::from(reader.cancel()).await;
|
||||
}
|
||||
// Fetch rejects interrupted HTTP bodies and decodes Content-Encoding. Content-Length
|
||||
// can describe compressed bytes, so the streamed limit also checks the decoded size.
|
||||
let bytes = result?;
|
||||
Ok((name, bytes))
|
||||
}
|
||||
|
||||
async fn read(reader: &web_sys::ReadableStreamDefaultReader) -> Result<Vec<u8>, String> {
|
||||
let mut bytes = Vec::new();
|
||||
// Bound even a stream which keeps returning empty chunks.
|
||||
for _ in 0..1_000_000 {
|
||||
let chunk = JsFuture::from(reader.read()).await.map_err(|_| "Download interrupted")?;
|
||||
if Reflect::get(&chunk, &JsValue::from_str("done")).map_err(|_| "Invalid download stream")?.as_bool() == Some(true) {
|
||||
return Ok(bytes);
|
||||
}
|
||||
let value = Reflect::get(&chunk, &JsValue::from_str("value")).map_err(|_| "Invalid download chunk")?;
|
||||
let chunk: Uint8Array = value.dyn_into().map_err(|_| "Invalid download bytes")?;
|
||||
check_download_size(bytes.len(), u64::from(chunk.length()))?;
|
||||
bytes.extend_from_slice(&chunk.to_vec());
|
||||
}
|
||||
Err("Download returned too many chunks".into())
|
||||
}
|
||||
+259
-25
@@ -14,6 +14,8 @@ mod disk;
|
||||
mod files;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod install;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod linked;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
@@ -25,6 +27,89 @@ fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
|
||||
ctrl && key == "Tab"
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn query_flag(query: &str, name: &str) -> bool {
|
||||
url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()).any(|(key, _)| key == name)
|
||||
}
|
||||
|
||||
/// Launch options use exact query keys; URL values are decoded only once.
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
#[derive(Default, Debug)]
|
||||
struct Launch {
|
||||
open: Option<String>,
|
||||
embed: bool,
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
impl Launch {
|
||||
fn parse(query: &str) -> Self {
|
||||
let mut launch = Self::default();
|
||||
let mut embed = false;
|
||||
for (key, value) in url::form_urlencoded::parse(query.trim_start_matches('?').as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"open" if launch.open.is_none() => launch.open = Some(value.into_owned()),
|
||||
"embed" => embed = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
launch.embed = embed && launch.open.is_some();
|
||||
launch
|
||||
}
|
||||
|
||||
fn persists(&self) -> bool {
|
||||
!self.embed
|
||||
}
|
||||
|
||||
fn full_app_url(&self, mut url: url::Url) -> String {
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
if let Some(open) = &self.open {
|
||||
url.query_pairs_mut().append_pair("open", open);
|
||||
}
|
||||
url.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn design_url(value: &str, page_origin: &str) -> Result<url::Url, String> {
|
||||
if value.len() > 16_384 {
|
||||
return Err("Design URL is too long".into());
|
||||
}
|
||||
let decoded = percent_encoding::percent_decode_str(value).decode_utf8().map_err(|_| "Invalid design URL encoding")?;
|
||||
if decoded.chars().any(|ch| ch.is_control() || ch == '\\') {
|
||||
return Err("Design links must not contain backslashes or control characters".into());
|
||||
}
|
||||
// Check before URL parsing normalizes away dot segments. Decode escaped slashes too.
|
||||
let path = value.split(['?', '#']).next().unwrap_or_default();
|
||||
let path = percent_encoding::percent_decode_str(path).collect::<Vec<_>>();
|
||||
if path.split(|byte| *byte == b'/').any(|segment| segment == b"..") {
|
||||
return Err("Design links must not contain parent path segments".into());
|
||||
}
|
||||
let page = url::Url::parse(page_origin).map_err(|_| "Invalid page origin")?;
|
||||
let url = if value.starts_with('/') && !value.starts_with("//") {
|
||||
page.join(value).map_err(|_| "Invalid design path")?
|
||||
} else {
|
||||
url::Url::parse(value).map_err(|_| "Invalid design URL")?
|
||||
};
|
||||
if (url.scheme() != "https" && url.origin() != page.origin())
|
||||
|| url.host_str().is_none()
|
||||
|| !url.username().is_empty()
|
||||
|| url.password().is_some()
|
||||
{
|
||||
return Err("Design links must use HTTPS or the page's own origin without credentials".into());
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn check_download_size(current: usize, incoming: u64) -> Result<(), String> {
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES as u64;
|
||||
if current as u64 > cap || incoming > cap.saturating_sub(current as u64) {
|
||||
return Err(format!("Design exceeds the {} MB size limit", cap / (1024 * 1024)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -112,6 +197,9 @@ mod web {
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
launch: crate::Launch,
|
||||
linked: crate::linked::Download,
|
||||
full_url: Option<String>,
|
||||
api: Api,
|
||||
files: Rc<Files>,
|
||||
/// When the last autosave was (seconds), and the revision it holds (none stored: `None`).
|
||||
@@ -137,6 +225,9 @@ mod web {
|
||||
impl Shell {
|
||||
/// Files from the computer, writes to store, messages and the autosave.
|
||||
fn files_tick(&mut self, ctx: &egui::Context) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
if self.files.ctx.borrow().is_none() {
|
||||
*self.files.ctx.borrow_mut() = Some(ctx.clone());
|
||||
}
|
||||
@@ -230,13 +321,15 @@ mod web {
|
||||
fn sync_recents(&mut self) {
|
||||
let mut entries = self.files.entries.borrow().clone();
|
||||
entries.sort_by(|a, b| b.modified.total_cmp(&a.modified));
|
||||
let forgotten = self.app.home.forgotten.clone();
|
||||
let recent = &mut self.app.home.recent;
|
||||
let known = self.files.disk_known.borrow().clone();
|
||||
// A design on the computer's disk stays listed while its file is remembered.
|
||||
recent.retain(|p| vfs::exists(p) || disk::parse(p).is_some_and(|(id, _)| known.contains(id)));
|
||||
for e in entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)) {
|
||||
let p = format!("{}{}", files::OPFS, e.name);
|
||||
if !recent.contains(&p) {
|
||||
// A design the user removed from the list is not added back while it is stored.
|
||||
if !recent.contains(&p) && !forgotten.contains(&p) {
|
||||
recent.push(p);
|
||||
}
|
||||
}
|
||||
@@ -245,6 +338,10 @@ mod web {
|
||||
let recovered = self.files.recovery.borrow().as_ref().and_then(|(_, p)| p.clone());
|
||||
let keep = recent.iter().chain(recovered.as_ref()).filter_map(|p| disk::parse(p)).map(|(id, _)| id.to_string()).collect();
|
||||
disk::prune(keep);
|
||||
// Drop forgotten entries whose stored design is gone, so the set does not grow without bound.
|
||||
let stored: std::collections::BTreeSet<String> =
|
||||
entries.iter().filter(|e| solidcraft_engine::io::is_design_path(&e.name)).map(|e| format!("{}{}", files::OPFS, e.name)).collect();
|
||||
self.app.home.forgotten.retain(|p| stored.contains(p));
|
||||
}
|
||||
|
||||
/// The stored designs: open, insert, rename, delete, download; open from the computer.
|
||||
@@ -261,7 +358,7 @@ mod web {
|
||||
files::pick_from_disk(&self.files, Intent::Open);
|
||||
}
|
||||
if ui_kit::button(ui, "Insert from computer…", ui_kit::Button::Normal)
|
||||
.on_hover_text("STEP, 3MF or STL into the open design")
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
files::pick_from_disk(&self.files, Intent::Insert);
|
||||
@@ -314,7 +411,7 @@ mod web {
|
||||
let design = solidcraft_engine::io::is_design_path(&e.name);
|
||||
if !design
|
||||
&& ui_kit::button(ui, "Insert", ui_kit::Button::Small)
|
||||
.on_hover_text("Into the open design (STEP, 3MF, STL)")
|
||||
.on_hover_text(solidcraft_ui_egui::i18n::tr("web.files.insert_stored_drawings_hint"))
|
||||
.clicked()
|
||||
{
|
||||
act = Some(("insert", e.name.clone()));
|
||||
@@ -383,6 +480,8 @@ mod web {
|
||||
"delete" => {
|
||||
files::delete(&self.files, &name);
|
||||
self.app.home.recent.retain(|r| *r != path);
|
||||
// The file is gone, so there is nothing left to keep forgotten.
|
||||
self.app.home.forgotten.remove(&path);
|
||||
}
|
||||
"rename_cancel" => self.renaming = None,
|
||||
"rename" => {
|
||||
@@ -396,6 +495,10 @@ mod web {
|
||||
*r = new.clone();
|
||||
}
|
||||
}
|
||||
// A forgotten design follows its new name.
|
||||
if self.app.home.forgotten.remove(&old) {
|
||||
self.app.home.forgotten.insert(new);
|
||||
}
|
||||
}
|
||||
Err(e) => self.app.set_status(e, true),
|
||||
}
|
||||
@@ -423,15 +526,25 @@ mod web {
|
||||
});
|
||||
});
|
||||
let Some(recover) = answer else { return };
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
if recover {
|
||||
self.app.open_path(&files::recovery_path());
|
||||
let recovery = match files::prepare_recovery(path.as_deref()) {
|
||||
Ok(recovery) => recovery,
|
||||
Err(error) => {
|
||||
self.app.set_status(error, true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
self.app.open_path(&recovery);
|
||||
if self.app.session.path.as_deref() != Some(recovery.as_str()) {
|
||||
return;
|
||||
}
|
||||
// It belongs to its own file again (saving goes there); it is unsaved.
|
||||
self.app.session.path = path;
|
||||
self.app.session.doc_mut().name = name;
|
||||
self.app.session.mark_unsaved();
|
||||
self.app.home.open = false;
|
||||
}
|
||||
*self.files.recovery.borrow_mut() = None;
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
@@ -514,19 +627,42 @@ mod web {
|
||||
if let Some(delay) = self.wheel_gestures.update(raw, now) {
|
||||
ctx.request_repaint_after(delay);
|
||||
}
|
||||
files::take_drops(&self.files, raw);
|
||||
if self.launch.persists() {
|
||||
files::take_drops(&self.files, raw);
|
||||
} else {
|
||||
raw.dropped_files.clear();
|
||||
}
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
self.space_mouse.raw_input_hook(ctx, self.app.preferences.space_mouse_enabled, raw);
|
||||
self.app.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.linked.tick(&mut self.app, ui.ctx());
|
||||
UNSAVED.with(|u| u.set(self.launch.persists() && solidcraft_ui_egui::documents::any_dirty(&self.app)));
|
||||
if self.launch.embed {
|
||||
ui_kit::Container::new(ui.ctx(), ui_kit::Surface::Chrome).edge("embed_header", ui_kit::Edge::Top).show(ui, |ui| {
|
||||
if ui_kit::button(ui, solidcraft_ui_egui::i18n::tr("web.open_full"), ui_kit::Button::Normal).clicked()
|
||||
&& let (Some(window), Some(url)) = (web_sys::window(), &self.full_url)
|
||||
{
|
||||
let _ = window.open_with_url_and_target_and_features(url, "_blank", "noopener");
|
||||
}
|
||||
});
|
||||
}
|
||||
self.app.ui(ui);
|
||||
let ctx = ui.ctx().clone();
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
if self.launch.persists() {
|
||||
self.file_panel(&ctx);
|
||||
self.recovery_prompt(&ctx);
|
||||
}
|
||||
self.watch_text_for_font(&ctx);
|
||||
}
|
||||
fn persist_egui_memory(&self) -> bool {
|
||||
self.launch.persists()
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
if !self.launch.persists() {
|
||||
return;
|
||||
}
|
||||
// Persist the user's own language, not a session-only English fallback.
|
||||
let saved = self.font.persisted_language(&self.app.preferences.language).to_owned();
|
||||
let live = std::mem::replace(&mut self.app.preferences.language, saved);
|
||||
@@ -545,15 +681,15 @@ mod web {
|
||||
|
||||
/// Render with WebGL2: asked for now (`?webgl`, remembered) or before (until `?webgpu`).
|
||||
fn webgl() -> bool {
|
||||
let storage = web_sys::window().and_then(|w| w.local_storage().ok().flatten());
|
||||
let storage = crate::Launch::parse(&query()).persists().then(|| web_sys::window().and_then(|w| w.local_storage().ok().flatten())).flatten();
|
||||
let q = query();
|
||||
if q.contains("webgl") {
|
||||
if crate::query_flag(&q, "webgl") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.set_item(WEBGL_KEY, "1");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if q.contains("webgpu") {
|
||||
if crate::query_flag(&q, "webgpu") {
|
||||
if let Some(s) = &storage {
|
||||
let _ = s.remove_item(WEBGL_KEY);
|
||||
}
|
||||
@@ -579,7 +715,10 @@ mod web {
|
||||
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(window) = web_sys::window() else { return };
|
||||
ask_before_unload(&window);
|
||||
let launch = crate::Launch::parse(&query());
|
||||
if launch.persists() {
|
||||
ask_before_unload(&window);
|
||||
}
|
||||
leave_tab_switching_to_browser(&window);
|
||||
let Some(document) = window.document() else { return };
|
||||
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||
@@ -599,19 +738,24 @@ mod web {
|
||||
Box::new(move |cc| {
|
||||
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
|
||||
let files = Rc::new(Files::default());
|
||||
files::start(&files);
|
||||
if launch.persists() {
|
||||
files::start(&files);
|
||||
}
|
||||
let (panel, opener, saver, shown) = (files.clone(), files.clone(), files.clone(), files.clone());
|
||||
let (restorable, restorer) = (files.clone(), files.clone());
|
||||
let services = Services {
|
||||
let services = if launch.embed { Services::default() } else { Services {
|
||||
// Open shows the browser's open dialog for a design where it has one
|
||||
// (the file keeps its handle, so Save writes back to it); otherwise,
|
||||
// and for inserting STEP or meshes, the browser's files window.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| {
|
||||
if disk::handles_open(exts) {
|
||||
return Box::pin(disk::pick_open(&opener));
|
||||
// (the file keeps its handle, so Save writes back to it), else the
|
||||
// browser's files window. A command that reads another kind of file
|
||||
// (Insert DXF or SVG, an image, a mesh, settings) gets a file dialog
|
||||
// offering that kind, and runs on the picked file.
|
||||
pick_open: Some(Box::new(move |exts: &[&str]| match disk::open_route(disk::supported(), exts) {
|
||||
disk::OpenRoute::Disk => Box::pin(disk::pick_open(&opener)),
|
||||
disk::OpenRoute::Upload => Box::pin(files::pick_upload(&opener, exts)),
|
||||
disk::OpenRoute::Panel => {
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
}
|
||||
panel.panel.set(true);
|
||||
Box::pin(std::future::ready(None))
|
||||
})),
|
||||
// A design is saved to a file the user picks, or kept in the browser
|
||||
// where the browser can't pick one; everything else is downloaded.
|
||||
@@ -636,26 +780,27 @@ mod web {
|
||||
browser_files: Some(Box::new(move || shown.panel.set(true))),
|
||||
install_app: Some(crate::install::service(&cc.egui_ctx)),
|
||||
..Default::default()
|
||||
};
|
||||
}};
|
||||
// Thumbnails of saves and start-page cards.
|
||||
solidcraft_ui_egui::gpu::install_cpu_picture_renderer();
|
||||
let (sender, receiver) = std::sync::mpsc::channel();
|
||||
let mut app = SolveApp::new(Session::default(), services).with_control(receiver);
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.embedded = launch.embed;
|
||||
if launch.persists() && let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
// eframe's web painter has no stencil buffer.
|
||||
app.set_wgpu(rs, DEPTH_BITS, 0, 1);
|
||||
}
|
||||
if query().contains("sample") {
|
||||
if launch.open.is_none() && crate::query_flag(&query(), "sample") {
|
||||
if let Err(e) = app.session.run_script(&solidcraft_engine::sample::script()) {
|
||||
app.set_status(format!("sample: {e}"), true);
|
||||
}
|
||||
app.session.doc_mut().name = "Sample Plate".into();
|
||||
app.session.mark_saved();
|
||||
}
|
||||
if !query().contains("sample") {
|
||||
if launch.open.is_none() && !crate::query_flag(&query(), "sample") {
|
||||
// The empty design takes Units of new designs (#1205). The sample
|
||||
// part is built in millimetres and is left that way.
|
||||
solidcraft_ui_egui::prefs::apply_new_design(&mut app);
|
||||
@@ -664,8 +809,13 @@ mod web {
|
||||
}
|
||||
let api = Api::install(sender, cc.egui_ctx.clone()).map_err(|e| format!("SolidCraft browser API: {e:?}"))?;
|
||||
let font_language = app.preferences.language.clone();
|
||||
let linked = crate::linked::Download::start(launch.open.as_deref(), &cc.egui_ctx);
|
||||
let full_url = window.location().href().ok().and_then(|value| url::Url::parse(&value).ok()).map(|url| launch.full_app_url(url));
|
||||
Ok(Box::new(Shell {
|
||||
app,
|
||||
launch,
|
||||
linked,
|
||||
full_url,
|
||||
api,
|
||||
files,
|
||||
autosaved_at: f64::NEG_INFINITY,
|
||||
@@ -709,6 +859,90 @@ fn main() {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn link_query_decodes_once_and_embed_disables_persistence() {
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "webgl"));
|
||||
assert!(!query_flag("?open=https://example.org/webgl?sample", "sample"));
|
||||
assert!(query_flag("?embed&webgl", "webgl"));
|
||||
let launch = Launch::parse("?embed&open=https%3A%2F%2Fexample.org%2Farm.solidcraft%3Fx%3Da%2520b%26y%3D1");
|
||||
assert_eq!(launch.open.as_deref(), Some("https://example.org/arm.solidcraft?x=a%20b&y=1"));
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
assert!(!Launch::parse("?embed").embed);
|
||||
assert!(!Launch::parse("?other=embed&reopen=https://example.org").embed);
|
||||
assert!(Launch::parse("?open=https://example.org/a").persists());
|
||||
assert_eq!(Launch::parse("?open=first&open=second").open.as_deref(), Some("first"));
|
||||
let launch = Launch::parse("?embed&open=/dutchman/jobs/abc/part.solidcraft%3Fx%3Da%2520b");
|
||||
assert!(launch.embed);
|
||||
assert!(!launch.persists());
|
||||
let page = url::Url::parse("http://solidcraft.io/?embed#view").expect("synthetic page");
|
||||
let full = url::Url::parse(&launch.full_app_url(page)).expect("full app URL");
|
||||
let reopened = Launch::parse(full.query().unwrap_or_default());
|
||||
assert_eq!(reopened.open, launch.open);
|
||||
assert_eq!(reopened.open.as_deref(), Some("/dutchman/jobs/abc/part.solidcraft?x=a%20b"));
|
||||
assert!(!reopened.embed);
|
||||
assert!(reopened.persists());
|
||||
assert!(full.fragment().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_refuse_hostile_schemes_credentials_and_sizes() {
|
||||
assert!(design_url("https://example.org/arm.solidcraft?download=1", "http://solidcraft.io").is_ok());
|
||||
for value in [
|
||||
"http://example.org/a",
|
||||
"data:application/json,{}",
|
||||
"blob:https://example.org/a",
|
||||
"javascript:alert(1)",
|
||||
"https://user:secret@example.org/a",
|
||||
"//evil.example/x",
|
||||
"/../x",
|
||||
"/%2e%2e/x",
|
||||
"/a/%2E./x",
|
||||
"/a%2f..%2fx",
|
||||
"/a\\x",
|
||||
"/a%5cx",
|
||||
"/a\nx",
|
||||
"/a%00x",
|
||||
"/a%7fx",
|
||||
"/a%C2%85x",
|
||||
"/a%3f/../x",
|
||||
"/a/..#view",
|
||||
"/a/..?download=1",
|
||||
"https://solidcraft.io/a/../x",
|
||||
"http://user:secret@solidcraft.io/x",
|
||||
"http://solidcraft.io.evil.example/x",
|
||||
"http://solidcraft.io:8080/x",
|
||||
"relative.solidcraft",
|
||||
"",
|
||||
] {
|
||||
assert!(design_url(value, "http://solidcraft.io").is_err(), "{value}");
|
||||
}
|
||||
assert!(design_url(&format!("https://example.org/{}", "x".repeat(16_384)), "http://solidcraft.io").is_err());
|
||||
assert!(design_url("http://redirect.example.org/a", "http://solidcraft.io").is_err());
|
||||
|
||||
let cap = solidcraft_engine::io::MAX_DESIGN_BYTES;
|
||||
assert!(check_download_size(0, cap as u64).is_ok());
|
||||
assert!(check_download_size(0, cap as u64 + 1).is_err());
|
||||
assert!(check_download_size(cap - 2, 2).is_ok());
|
||||
assert!(check_download_size(cap - 2, 3).is_err());
|
||||
assert!(check_download_size(0, u64::MAX).is_err());
|
||||
assert!(check_download_size(cap, 1).is_err());
|
||||
assert!(check_download_size(usize::MAX, 0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn design_links_allow_same_origin_paths_and_redirects() {
|
||||
for origin in ["http://solidcraft.io", "https://solidcraft.io", "http://solidcraft.io:8080"] {
|
||||
assert_eq!(design_url("/a/b.solidcraft", origin).map(|url| url.to_string()), Ok(format!("{origin}/a/b.solidcraft")));
|
||||
assert!(design_url(&format!("{origin}/x"), origin).is_ok());
|
||||
// Final response URLs follow the same origin and scheme rules as the request.
|
||||
assert!(design_url("https://docs.solidcraft.io/x", origin).is_ok());
|
||||
assert!(design_url("http://other/x", origin).is_err());
|
||||
}
|
||||
assert!(design_url("http://solidcraft.io/x", "https://solidcraft.io").is_err());
|
||||
assert!(design_url("http://SOLIDCRAFT.io:80/x", "http://solidcraft.io").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_ctrl_tab_is_left_to_the_browser() {
|
||||
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
|
||||
|
||||
@@ -246,11 +246,11 @@ mod tests {
|
||||
let axis =
|
||||
|source, rest, full| solidcraft_ui_egui::space_mouse::AxisCalibration { source, reversed: false, rest, dead_zone: 0, full: [full; 2] };
|
||||
// A short-range device: 15 of its 50 is under the raw dead zone, but 105 of 350 calibrated.
|
||||
let short = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 0, 50)) };
|
||||
let short = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 0, 50)) };
|
||||
let push = Axes { translation: [15, 0, 0], rotation: [0; 3] };
|
||||
assert!(!push.moving() && moving(push, Some(short)));
|
||||
// A device resting at 100 on every axis: past the raw dead zone, but at rest calibrated.
|
||||
let offset = Calibration { dead_zone_share: 0.0, axes: std::array::from_fn(|r| axis(r, 100, 350)) };
|
||||
let offset = Calibration { dead_zone_share: 0.0, kind: Default::default(), axes: std::array::from_fn(|r| axis(r, 100, 350)) };
|
||||
let resting = Axes { translation: [100; 3], rotation: [100; 3] };
|
||||
assert!(resting.moving() && !moving(resting, Some(offset)));
|
||||
// Without a calibration, as read.
|
||||
|
||||
@@ -7,9 +7,14 @@ license.workspace = true
|
||||
publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
|
||||
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs
|
||||
# (macOS file-open events), src/native_crash.rs (Windows exception filter) and src/native_frame.rs
|
||||
# (Windows resize border) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
# (Windows resize border, taskbar icon) can allow it: none has a safe API. Keep the rest in sync with [workspace.lints].
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
@@ -49,7 +54,8 @@ hidapi = { version = "2.6.7", default-features = false, features = ["windows-nat
|
||||
# The main window's handle for src/native_frame.rs (already in the tree through eframe and winit).
|
||||
raw-window-handle = "0.6"
|
||||
# Native crash capture (src/native_crash.rs): the exception filter, the faulting module, a minidump.
|
||||
# The window's resize border and DPI size (src/native_frame.rs): the window procedure, hit test, DPI.
|
||||
# The window's resize border, DPI size and class icon (src/native_frame.rs): the window procedure, hit
|
||||
# test, DPI, the exe's icon.
|
||||
# Finding 3D mice through Plug and Play without asking other devices anything (src/space_mouse/windows_hid.rs).
|
||||
windows-sys = { version = "0.61", features = [
|
||||
"Win32_Devices_DeviceAndDriverInstallation",
|
||||
@@ -76,9 +82,10 @@ solidcraft-3dx-client = { workspace = true }
|
||||
# The 3D mouse (src/space_mouse.rs), in the shared-device open mode so that a running 3DxWare
|
||||
# driver does not shut the app out.
|
||||
hidapi = { version = "2.6.7", default-features = false, features = ["macos-shared-device"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs), and the fullscreen title-bar
|
||||
# inset (src/main.rs); all already come with eframe and rfd.
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder", "NSWindow"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSProcessInfo", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
|
||||
|
||||
@@ -11,15 +11,20 @@ use std::time::{Duration, Instant};
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::MAX_LINE;
|
||||
use solidcraft_engine::control_auth;
|
||||
use solidcraft_engine::control_pending;
|
||||
use solidcraft_ui_egui::ControlRequest;
|
||||
use solidcraft_ui_egui::control::{DESIGN_WATCH, DesignWatch, Subscriber};
|
||||
|
||||
/// 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;
|
||||
/// How long a subscribed connection waits after a `design.changed` line before the next: what
|
||||
/// changes meanwhile (a parameter dragged) goes out as one line, the latest (#6985).
|
||||
const EVENT_GAP: Duration = Duration::from_millis(100);
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token files this launch wrote (its `--control` port, the agent chat's endpoint), removed
|
||||
/// The token files this launch wrote (its control port, the agent chat's endpoint), removed
|
||||
/// when the app ends.
|
||||
static TOKEN_FILES: std::sync::Mutex<Vec<std::path::PathBuf>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
@@ -34,17 +39,45 @@ fn new_token() -> Option<String> {
|
||||
pub fn remove_token_file() {
|
||||
if let Ok(files) = TOKEN_FILES.lock() {
|
||||
for p in files.iter() {
|
||||
let _ = std::fs::remove_file(p);
|
||||
control_auth::remove_token_file(p);
|
||||
}
|
||||
}
|
||||
if let Some(port) = APP_PORT.lock().ok().and_then(|p| *p) {
|
||||
control_auth::remove_current_port(port);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
/// The port the app's own channel listens on, for removing the discovery file at exit.
|
||||
static APP_PORT: std::sync::Mutex<Option<u16>> = std::sync::Mutex::new(None);
|
||||
|
||||
pub fn start(listen_on: control_auth::Listen, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// Every connection must show the token before anything runs (docs/control-protocol.md).
|
||||
match control_auth::env_token().or_else(new_token) {
|
||||
Some(token) => {
|
||||
listen(port, token, tx, ctx, false);
|
||||
let listener = match listen_on {
|
||||
control_auth::Listen::Exact(port) => TcpListener::bind(("127.0.0.1", port)),
|
||||
control_auth::Listen::Default => control_auth::bind_with_fallback(control_auth::DEFAULT_PORT, control_auth::PORT_TRIES),
|
||||
};
|
||||
match listener {
|
||||
Ok(l) => {
|
||||
if let Some(port) = listen_on_listener(l, token, tx, ctx, false) {
|
||||
if matches!(listen_on, control_auth::Listen::Default) {
|
||||
eprintln!("solidcraft: control channel on the default port 127.0.0.1:{port} (Preferences > Assistant turns it off)");
|
||||
}
|
||||
// The discovery file: where `solidcraft-cli mcp` finds this app with no arguments.
|
||||
match control_auth::write_current_port(port) {
|
||||
Ok(_) => {
|
||||
if let Ok(mut p) = APP_PORT.lock() {
|
||||
*p = Some(port);
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: could not write the control discovery file ({e})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}"),
|
||||
}
|
||||
}
|
||||
None => eprintln!("solidcraft: control server not started: no random token available"),
|
||||
}
|
||||
@@ -60,20 +93,20 @@ pub fn listen_agent(tx: Sender<ControlRequest>, ctx: egui::Context) -> Option<u1
|
||||
eprintln!("solidcraft: agent chat endpoint not started: no random token available");
|
||||
return None;
|
||||
};
|
||||
listen(0, token, tx, ctx, true)
|
||||
}
|
||||
|
||||
/// Listen on `port` (0: a free port the system picks) with `token`; the port it listens on.
|
||||
/// `patient`: every request may wait as long as a batch (a person decides on it).
|
||||
fn listen(port: u16, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
let listener = match TcpListener::bind(("127.0.0.1", 0)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||
eprintln!("solidcraft: control server failed to bind 127.0.0.1: {e}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let port = listener.local_addr().map(|a| a.port()).unwrap_or(port);
|
||||
listen_on_listener(listener, token, tx, ctx, true)
|
||||
}
|
||||
|
||||
/// Serve `listener` (loopback) with `token`; the port it listens on.
|
||||
/// `patient`: every request may wait as long as a batch (a person decides on it).
|
||||
fn listen_on_listener(listener: TcpListener, token: String, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) -> Option<u16> {
|
||||
let port = listener.local_addr().ok()?.port();
|
||||
match control_auth::token_path(port) {
|
||||
Some(path) => match control_auth::write_token_file(&path, &token) {
|
||||
Ok(()) => {
|
||||
@@ -98,7 +131,7 @@ fn listen(port: u16, token: String, tx: Sender<ControlRequest>, ctx: egui::Conte
|
||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||
OPEN.fetch_sub(1, Ordering::Relaxed);
|
||||
let mut refused = stream;
|
||||
let _ = writeln!(refused, "{}", json!({"ok": false, "error": "too many connections"}));
|
||||
let _ = send(&mut refused, &json!({"ok": false, "error": "too many connections"}));
|
||||
continue;
|
||||
}
|
||||
let tx = tx.clone();
|
||||
@@ -145,6 +178,15 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
|
||||
}
|
||||
}
|
||||
|
||||
/// A reply line, written to the socket at once. `writeln!` with a JSON value hands the socket each
|
||||
/// of its pieces apart, a system call for every string, number and comma: half a second for a
|
||||
/// design's component tree when it has thousands of components.
|
||||
fn send(out: &mut impl Write, reply: &Value) -> std::io::Result<()> {
|
||||
let mut line = serde_json::to_vec(reply).map_err(std::io::Error::other)?;
|
||||
line.push(b'\n');
|
||||
out.write_all(&line)
|
||||
}
|
||||
|
||||
/// 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)"})
|
||||
@@ -155,7 +197,7 @@ fn unauthorized() -> Value {
|
||||
/// 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 _ = send(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());
|
||||
@@ -176,6 +218,8 @@ const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::fro
|
||||
struct Pending {
|
||||
id: Value,
|
||||
method: String,
|
||||
/// The request in messages ("`engine.execute` (solid.fillet)").
|
||||
what: String,
|
||||
reply: Receiver<Value>,
|
||||
}
|
||||
|
||||
@@ -296,10 +340,120 @@ fn watch(reply: Receiver<Value>, ticket: Ticket) -> Receiver<Value> {
|
||||
late_rx
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING);
|
||||
/// The reply text for a request that outlasts its wait. A request that only reads changes
|
||||
/// nothing when it finishes; an export or a save still writes its file, and any other request
|
||||
/// still applies, as it was not cancelled.
|
||||
fn timeout_message(what: &str, effect: control_pending::Effect) -> String {
|
||||
match effect {
|
||||
control_pending::Effect::ReadsOnly => {
|
||||
format!("timeout: {what} is still running in the app ({}); ask `control.pending` for its answer", effect.when_done())
|
||||
}
|
||||
control_pending::Effect::WritesFile => format!(
|
||||
"timeout: {what} is still running in the app and will still write the file when it finishes (it was not cancelled: \
|
||||
don't send it again; the design does not change); ask `control.pending` for its result"
|
||||
),
|
||||
control_pending::Effect::Changes => format!(
|
||||
"timeout: {what} is still running in the app and will still apply when it finishes (it was not cancelled: don't send it \
|
||||
again); ask `control.pending` for its result"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reply text for a request refused because an earlier one on the connection still runs.
|
||||
fn busy_message(p: &Pending, method: &str) -> String {
|
||||
let method: String = method.chars().take(128).collect();
|
||||
format!(
|
||||
"busy: request {} ({}) is still running, so `{method}` was not run; ask `control.pending` for that request's result, then \
|
||||
send this one again",
|
||||
p.id, p.what
|
||||
)
|
||||
}
|
||||
|
||||
/// A subscribed connection's socket, for its replies and its events: each line is written whole
|
||||
/// and alone, so an event never lands inside a reply (#6985).
|
||||
struct Lines {
|
||||
socket: TcpStream,
|
||||
turn: Mutex<()>,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn send(&self, line: &Value) -> std::io::Result<()> {
|
||||
let _turn = self.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
send(&mut &self.socket, line)
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection's `control.subscribe` (#6985): its `design.changed` lines, sent from a thread of
|
||||
/// their own until the connection ends.
|
||||
struct Subscription {
|
||||
lines: Arc<Lines>,
|
||||
stop: Arc<AtomicBool>,
|
||||
_counted: Subscriber,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
/// Write `reply`, the window's answer to `control.subscribe` naming the design `seen`, on
|
||||
/// `out`, then send `design`'s changes after it: the reply goes out before any event. `None`,
|
||||
/// with nothing written, when no thread or socket was left for the events.
|
||||
fn start(
|
||||
design: &'static DesignWatch,
|
||||
counted: Subscriber,
|
||||
out: &TcpStream,
|
||||
seen: (String, u64),
|
||||
reply: &Value,
|
||||
) -> Option<(Self, std::io::Result<()>)> {
|
||||
let lines = Arc::new(Lines { socket: out.try_clone().ok()?, turn: Mutex::new(()) });
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let (l, s) = (lines.clone(), stop.clone());
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen)).ok()?;
|
||||
let sent = send(&mut &lines.socket, reply);
|
||||
drop(first);
|
||||
Some((Subscription { lines, stop, _counted: counted }, sent))
|
||||
}
|
||||
}
|
||||
|
||||
/// The design a `control.subscribe` reply names.
|
||||
fn design_of(reply: &Value) -> Option<(String, u64)> {
|
||||
let result = reply.get("result")?;
|
||||
Some((result.get("document_id")?.as_str()?.to_string(), result.get("revision")?.as_u64()?))
|
||||
}
|
||||
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
// The socket closes now, not when the thread next looks; a line blocked on a client that
|
||||
// stopped reading ends too.
|
||||
let _ = self.lines.socket.shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
}
|
||||
|
||||
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
|
||||
/// last one it was told of (`seen`), until `stop`.
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64)) {
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
let Some(now) = design.wait(&seen, stop, Duration::from_secs(1)).filter(|now| *now != seen) else { continue };
|
||||
if lines.send(&json!({"event": "design.changed", "params": {"document_id": now.0, "revision": now.1}})).is_err() {
|
||||
break;
|
||||
}
|
||||
seen = now;
|
||||
std::thread::sleep(EVENT_GAP);
|
||||
}
|
||||
}
|
||||
|
||||
/// Write `reply` on the connection: through its subscription once it has one.
|
||||
fn answer(out: &mut TcpStream, events: Option<&Subscription>, reply: &Value) -> std::io::Result<()> {
|
||||
match events {
|
||||
Some(e) => e.lines.send(reply),
|
||||
None => send(out, reply),
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context, patient: bool) {
|
||||
serve_with(stream, token, tx, ctx, patient, WAITS, &RUNNING, &DESIGN_WATCH);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn serve_with(
|
||||
stream: TcpStream,
|
||||
token: &str,
|
||||
@@ -308,6 +462,7 @@ fn serve_with(
|
||||
patient: bool,
|
||||
waits: Waits,
|
||||
running: &'static Running,
|
||||
design: &'static DesignWatch,
|
||||
) {
|
||||
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
@@ -316,12 +471,15 @@ fn serve_with(
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
let mut pending: Option<Pending> = None;
|
||||
let mut events: Option<Subscription> = None;
|
||||
loop {
|
||||
// A first `control.subscribe` on its way to the window (#6985).
|
||||
let mut joining = None;
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
Ok(None) => break,
|
||||
Err(e) => {
|
||||
let _ = writeln!(out, "{}", json!({"ok": false, "error": e.to_string()}));
|
||||
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
@@ -364,7 +522,15 @@ fn serve_with(
|
||||
}
|
||||
} else if let Some(p) = pending.as_ref().filter(|_| late.is_none()) {
|
||||
// Nothing more is queued behind it, to run unseen when it ends.
|
||||
json!({"ok": false, "busy": true, "error": format!("busy: request {} (`{}`) is still running; ask `control.pending` for its result", p.id, p.method)})
|
||||
json!({"ok": false, "busy": true, "error": busy_message(p, &method)})
|
||||
} else if method == "control.subscribe" && events.is_some() {
|
||||
if late.is_some() {
|
||||
// As below: finished, and the client moved on without asking.
|
||||
pending = None;
|
||||
}
|
||||
// Its events already say how the design moves; a second answer could only
|
||||
// arrive behind a newer one.
|
||||
json!({"ok": false, "error": "already subscribed: this connection is sent design.changed lines; ask document.revision for the design now"})
|
||||
} else {
|
||||
if late.is_some() {
|
||||
// Finished, and the client moved on without asking: it is dropped.
|
||||
@@ -372,6 +538,11 @@ fn serve_with(
|
||||
}
|
||||
// A batch replaces many short requests: the whole of it may take long (#743).
|
||||
let wait = if patient || method == "engine.batch" { waits.long } else { waits.short };
|
||||
let what = control_pending::describe(&method, ¶ms);
|
||||
let effect = control_pending::effect(&method, ¶ms);
|
||||
// Counted before the window answers, so it publishes every change from its
|
||||
// answer on.
|
||||
joining = (method == "control.subscribe").then(|| design.join());
|
||||
let (req, rrx) = ControlRequest::new(method.clone(), params);
|
||||
let ticket = running.add(connection, &id, &req);
|
||||
if tx.send(req).is_err() {
|
||||
@@ -381,8 +552,8 @@ fn serve_with(
|
||||
match rrx.recv_timeout(wait) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
let r = json!({"ok": false, "running": true, "error": format!("timeout: `{method}` is still running in the app and will still apply; ask `control.pending` for its result")});
|
||||
pending = Some(Pending { id: id.clone(), method, reply: watch(rrx, ticket) });
|
||||
let r = json!({"ok": false, "running": true, "error": timeout_message(&what, effect)});
|
||||
pending = Some(Pending { id: id.clone(), method, what, reply: watch(rrx, ticket) });
|
||||
r
|
||||
}
|
||||
}
|
||||
@@ -401,7 +572,20 @@ fn serve_with(
|
||||
json!({"ok": false, "error": format!("bad JSON: {e}")})
|
||||
}
|
||||
};
|
||||
if writeln!(out, "{reply}").is_err() {
|
||||
// Subscribed once the window named the design (not when it refused or timed out).
|
||||
let sent = match joining.and_then(|counted| design_of(&reply).map(|seen| (counted, seen))) {
|
||||
Some((counted, seen)) => match Subscription::start(design, counted, &out, seen, &reply) {
|
||||
Some((subscription, sent)) => {
|
||||
events = Some(subscription);
|
||||
sent
|
||||
}
|
||||
None => {
|
||||
send(&mut out, &json!({"id": reply.get("id"), "ok": false, "error": "not subscribed: no thread or socket left for the events"}))
|
||||
}
|
||||
},
|
||||
None => answer(&mut out, events.as_ref(), &reply),
|
||||
};
|
||||
if sent.is_err() {
|
||||
break;
|
||||
}
|
||||
if bad >= MAX_BAD_LINES {
|
||||
@@ -441,7 +625,7 @@ mod tests {
|
||||
static RUNNING: Running = Running::new();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(stream) = listener.incoming().flatten().next() {
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING);
|
||||
serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH);
|
||||
}
|
||||
});
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
@@ -507,7 +691,7 @@ mod tests {
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING));
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, waits, &RUNNING, &DESIGN_WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
@@ -577,6 +761,114 @@ mod tests {
|
||||
assert_eq!((&r["result"]["pending"], &r["result"]["done"]), (&json!(7), &json!(true)), "{r}");
|
||||
}
|
||||
|
||||
/// #6985: a connection that sends `control.subscribe` gets the design in the reply, then a
|
||||
/// `design.changed` line each time the window publishes another: never before the reply,
|
||||
/// none for the same design, the latest after a burst, each line whole between its replies.
|
||||
/// A second subscribe is refused without reaching the window. A connection that did not
|
||||
/// subscribe gets no events, and one that closed is no longer counted.
|
||||
#[test]
|
||||
fn subscribed_connections_are_told_of_design_changes() {
|
||||
static RUNNING: Running = Running::new();
|
||||
static WATCH: DesignWatch = DesignWatch::new();
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
// The window: it answers `control.subscribe` as the app does, publishing the design first;
|
||||
// then the design changes at once, before the reply may have been written.
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
if req.method == "control.subscribe" {
|
||||
WATCH.publish("d-1", 3);
|
||||
let _ = req.reply.send(json!({"ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
WATCH.publish("d-1", 4);
|
||||
} else {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
std::thread::spawn(move || serve_with(stream, token, tx, egui::Context::default(), false, WAITS, &RUNNING, &WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
(s.try_clone().unwrap(), BufReader::new(s))
|
||||
};
|
||||
let next = |lines: &mut BufReader<TcpStream>| -> Value {
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
serde_json::from_str(&l).unwrap()
|
||||
};
|
||||
let changed = |id: &str, revision: u64| json!({"event": "design.changed", "params": {"document_id": id, "revision": revision}});
|
||||
|
||||
let (mut quiet_out, mut quiet) = open();
|
||||
writeln!(quiet_out, "{}", json!({"id": 1, "method": "ui.view", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut quiet)["id"], 1);
|
||||
|
||||
let (mut out, mut lines) = open();
|
||||
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 1, "ok": true, "result": {"document_id": "d-1", "revision": 3}}));
|
||||
assert_eq!(next(&mut lines), changed("d-1", 4), "the change after the window's answer comes after the reply");
|
||||
assert!(WATCH.subscribed());
|
||||
// The design it was told of is no news; another revision is.
|
||||
WATCH.publish("d-1", 4);
|
||||
WATCH.publish("d-1", 5);
|
||||
assert_eq!(next(&mut lines), changed("d-1", 5));
|
||||
// A burst ends with its latest.
|
||||
for revision in 6..=10 {
|
||||
WATCH.publish("d-1", revision);
|
||||
}
|
||||
let mut last = 5;
|
||||
while last < 10 {
|
||||
let e = next(&mut lines);
|
||||
let revision = e["params"]["revision"].as_u64().unwrap();
|
||||
assert!(e["event"] == "design.changed" && revision > last, "{e} after {last}");
|
||||
last = revision;
|
||||
}
|
||||
// Another tab: another id, whatever its revision.
|
||||
WATCH.publish("d-2", 2);
|
||||
assert_eq!(next(&mut lines), changed("d-2", 2));
|
||||
// Subscribed already: refused here (the window would have published d-1 again).
|
||||
writeln!(out, "{}", json!({"id": 2, "method": "control.subscribe"})).unwrap();
|
||||
let r = next(&mut lines);
|
||||
assert!(r["id"] == 2 && r["ok"] == false && r["error"].as_str().is_some_and(|e| e.starts_with("already subscribed")), "{r}");
|
||||
// Replies still come, whole, and nothing else was sent meanwhile.
|
||||
writeln!(out, "{}", json!({"id": 3, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut lines), json!({"id": 3, "ok": true, "result": "ui.view"}));
|
||||
writeln!(quiet_out, "{}", json!({"id": 2, "method": "ui.view"})).unwrap();
|
||||
assert_eq!(next(&mut quiet), json!({"id": 2, "ok": true, "result": "ui.view"}), "no events without subscribing");
|
||||
// Closed, it is no longer counted.
|
||||
drop((out, lines));
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while WATCH.subscribed() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(!WATCH.subscribed());
|
||||
}
|
||||
|
||||
/// A timed-out request that only reads is not said to "still apply", nor is one that only
|
||||
/// writes a file (an export, #1989); a change is, by name.
|
||||
#[test]
|
||||
fn timeout_text_tells_reads_from_changes() {
|
||||
let says = |method: &str, p: Value| timeout_message(&control_pending::describe(method, &p), control_pending::effect(method, &p));
|
||||
let read = says("ui.inspect", json!({}));
|
||||
assert!(read.starts_with("timeout") && read.contains("only reads") && !read.contains("will still apply"), "{read}");
|
||||
let read = says("engine.execute", json!({"command": "sketch.inspect"}));
|
||||
assert!(read.contains("only reads") && read.contains("sketch.inspect"), "{read}");
|
||||
let change = says("engine.execute", json!({"command": "solid.fillet"}));
|
||||
assert!(change.starts_with("timeout") && change.contains("will still apply") && change.contains("solid.fillet"), "{change}");
|
||||
let export = says("engine.execute", json!({"command": "file.export", "params": {"path": "/tmp/wall.step"}}));
|
||||
assert!(export.contains("will still write the file") && !export.contains("apply") && export.contains("file.export"), "{export}");
|
||||
let save = says("engine.execute", json!({"command": "file.save"}));
|
||||
assert!(save.contains("will still write the file") && !save.contains("apply"), "{save}");
|
||||
let render = says("ui.render", json!({"path": "/tmp/a.png"}));
|
||||
assert!(render.contains("will still write the file"), "{render}");
|
||||
}
|
||||
|
||||
/// #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.
|
||||
@@ -669,4 +961,31 @@ mod tests {
|
||||
let mut r = BufReader::with_capacity(64, text.as_slice());
|
||||
assert!(read_line(&mut r, 1000).is_err());
|
||||
}
|
||||
|
||||
/// A large reply reaches the socket in one write, not one per JSON piece (a component tree of
|
||||
/// thousands of components took half a second to send), and reads back as it was.
|
||||
#[test]
|
||||
fn a_large_reply_is_written_at_once() {
|
||||
struct Counted {
|
||||
writes: usize,
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
impl Write for Counted {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
self.writes += 1;
|
||||
self.bytes.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
let components: Vec<Value> = (0..2000).map(|i| json!({"id": i, "name": format!("Part {i}"), "bodies": ["a", "b"], "parent": 0})).collect();
|
||||
let reply = json!({"ok": true, "id": 7, "result": {"components": components}});
|
||||
let mut out = Counted { writes: 0, bytes: Vec::new() };
|
||||
send(&mut out, &reply).unwrap();
|
||||
assert_eq!(out.writes, 1);
|
||||
assert_eq!(out.bytes.last(), Some(&b'\n'));
|
||||
assert_eq!(serde_json::from_slice::<Value>(&out.bytes).unwrap(), reply);
|
||||
}
|
||||
}
|
||||
|
||||
+378
-52
@@ -8,8 +8,10 @@
|
||||
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
|
||||
//! `.3mf`/`.stl`/`.obj`/`.glb`/`.gltf` file as a new design holding mesh bodies.
|
||||
//!
|
||||
//! `SOLIDCRAFT_SETTINGS_DIR` selects an explicit absolute settings directory without legacy adoption.
|
||||
//! `--control <port>` (or `SOLIDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control
|
||||
//! server: `{"id":1,"method":"engine.execute","params":{"command":"solid.box","params":{…}}}`.
|
||||
//! Without them the channel still opens, on the default port, unless Preferences turns it off.
|
||||
//! See `solidcraft_ui_egui::control` and docs/control-protocol.md.
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
@@ -26,6 +28,7 @@ mod native_crash;
|
||||
mod native_frame;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_menu;
|
||||
mod nav_bridge;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
@@ -85,6 +88,8 @@ struct App {
|
||||
space_mouse: space_mouse::SpaceMouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu: Option<native_menu::NativeMenu>,
|
||||
/// A device's own app moving the view (Preferences › Navigation, off by default).
|
||||
nav_bridge: nav_bridge::NavBridge,
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -109,6 +114,13 @@ impl eframe::App for App {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Focus);
|
||||
}
|
||||
solidcraft_engine::crash::phase(solidcraft_engine::crash::RUNNING);
|
||||
// A DMG install has no installer step: put solidcraft-cli on PATH, quietly.
|
||||
#[cfg(target_os = "macos")]
|
||||
std::thread::spawn(solidcraft_ui_egui::cli_link::install_on_launch);
|
||||
// Automation runs (a control port was asked for) leave the user's AI apps alone.
|
||||
if !self.take_focus_unwanted {
|
||||
solidcraft_ui_egui::ai_connect::begin(&mut self.solve);
|
||||
}
|
||||
if let Some(dir) = self.starting.take() {
|
||||
startup::started(&dir);
|
||||
}
|
||||
@@ -136,22 +148,29 @@ impl eframe::App for App {
|
||||
if !self.solve.file_picker_busy() {
|
||||
self.space_mouse.tick(&mut self.solve, ctx);
|
||||
}
|
||||
// Every frame, so switching the bridge off reaches the reader while a picker is open too.
|
||||
self.nav_bridge.tick(&mut self.solve, ctx);
|
||||
if self.solve.quit_requested {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
}
|
||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(menu) = &mut self.native_menu {
|
||||
menu.raw_input_hook(&mut self.solve, _ctx, raw);
|
||||
menu.raw_input_hook(&mut self.solve, ctx, raw);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_fullscreen_content(ctx, raw);
|
||||
// Before the app's own hook, which adds the control channel's events.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
self.space_mouse.raw_input_hook(self.solve.preferences.space_mouse_enabled, raw);
|
||||
// The 3D mouse's buttons, before the app reads the frame: Esc and held modifiers join its input.
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
solidcraft_ui_egui::space_mouse::buttons_hook(&mut self.solve, ctx, self.space_mouse.buttons(), raw);
|
||||
self.solve.raw_input_hook(raw);
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
self.solve.ui(ui);
|
||||
content_ui(&mut self.solve, ui);
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
|
||||
@@ -161,7 +180,17 @@ impl eframe::App for App {
|
||||
solidcraft_ui_egui::agent_chat::shutdown(&mut self.solve);
|
||||
// A save still being written ends before the app does.
|
||||
solidcraft_ui_egui::save_job::tick(&mut self.solve, true);
|
||||
// Pictures still being drawn off the window thread (Home cards, file previews) end
|
||||
// before the GPU is torn down: one drawn while the process exits can crash the driver.
|
||||
if !solidcraft_engine::capture::close(std::time::Duration::from_secs(5)) {
|
||||
log::warn!("a picture was still being drawn as the app closed");
|
||||
}
|
||||
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
|
||||
solidcraft_ui_egui::open_elsewhere::close(&mut self.solve);
|
||||
// Before a restart below starts the next instance, which would otherwise get nothing from
|
||||
// the 3D mouse driver (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
solidcraft_3dx_client::stop();
|
||||
solidcraft_engine::crash::end_session();
|
||||
if let Some(lost_at) = self.gpu.as_ref().and_then(|g| g.lost_at) {
|
||||
device_lost::restart(self.gpu.as_ref().is_some_and(|g| g.restart_on_software), lost_at);
|
||||
@@ -227,6 +256,37 @@ fn case_variants(exts: &[&str]) -> Vec<String> {
|
||||
all
|
||||
}
|
||||
|
||||
/// egui's root Ui spans the whole viewport, including native safe areas. Lay out the app
|
||||
/// inside the unobscured rectangle so its application bar can receive native pointer input.
|
||||
fn content_ui(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let content = ui.ctx().content_rect();
|
||||
let above = egui::Rect::from_min_max(ui.max_rect().min, egui::pos2(ui.max_rect().right(), content.top()));
|
||||
ui.painter().rect_filled(above, 0.0, solidcraft_ui_egui::theme::Tokens::get().app_bar);
|
||||
ui.scope_builder(egui::UiBuilder::new().max_rect(content), |ui| app.ui(ui));
|
||||
}
|
||||
|
||||
/// A transparent macOS title bar still intercepts clicks at the top in fullscreen (#3643).
|
||||
/// `contentLayoutRect` reports the whole content view once that bar auto-hides, so reserve
|
||||
/// the native titled-window height even then. AppKit supplies the height in logical points;
|
||||
/// egui's UI zoom is separate from the display's Retina scale.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn macos_fullscreen_content(ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||
let Some(mtm) = objc2::MainThreadMarker::new() else { return };
|
||||
let frame = objc2_app_kit::NSWindow::frameRectForContentRect_styleMask(Default::default(), objc2_app_kit::NSWindowStyleMask::Titled, mtm);
|
||||
fullscreen_content_insets(raw, frame.size.height as f32, ctx.zoom_factor());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", test))]
|
||||
fn fullscreen_content_insets(raw: &mut egui::RawInput, title_height: f32, zoom: f32) {
|
||||
let fullscreen = raw.viewports.get(&raw.viewport_id).and_then(|v| v.fullscreen).unwrap_or(false);
|
||||
let mut insets = raw.safe_area_insets.unwrap_or_default();
|
||||
if fullscreen && title_height.is_finite() && title_height > 0.0 && zoom.is_finite() && zoom > 0.0 {
|
||||
insets.0.top = insets.0.top.max(title_height / zoom);
|
||||
}
|
||||
// Explicitly reset on leaving fullscreen: None would retain the preceding frame's inset.
|
||||
raw.safe_area_insets = Some(insets);
|
||||
}
|
||||
|
||||
fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
|
||||
let open_parent = parent.clone();
|
||||
@@ -241,7 +301,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
dialog = dialog.add_filter(label, &case_variants(first));
|
||||
}
|
||||
// The groups come from the format registry, so a new format joins this dialog by itself.
|
||||
dialog = dialog.add_filter("Designs, CAD files and meshes", &case_variants(&io::formats::open_extensions(true)));
|
||||
let mut everything = io::formats::open_extensions(true);
|
||||
everything.extend(["dxf", "svg"]);
|
||||
dialog = dialog.add_filter(solidcraft_ui_egui::i18n::tr("file.open.drawing_filter"), &case_variants(&everything));
|
||||
for (label, exts) in io::formats::import_filters(true) {
|
||||
dialog = dialog.add_filter(label, &case_variants(&exts));
|
||||
}
|
||||
@@ -284,7 +346,9 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services {
|
||||
let future = dialog.save_file();
|
||||
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
|
||||
})),
|
||||
graphics_adapters: Vec::new(),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
window_system: window_system_now(),
|
||||
http_get: Some(std::sync::Arc::new(http_get)),
|
||||
http_download: Some(std::sync::Arc::new(http_download)),
|
||||
// Already an installed app: only the browser build offers to install.
|
||||
@@ -363,7 +427,7 @@ fn agent_http(r: &solidcraft_ui_egui::agent_chat::backend::HttpRequest) -> Resul
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.tls_config(tls)
|
||||
.http_status_as_error(false)
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 900))))
|
||||
.timeout_global(Some(Duration::from_secs(r.timeout_s.clamp(1, 3600))))
|
||||
.timeout_connect(Some(Duration::from_secs(10)))
|
||||
.user_agent("SolidCraft")
|
||||
.max_redirects(0)
|
||||
@@ -454,14 +518,15 @@ fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Resp
|
||||
}
|
||||
|
||||
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
|
||||
fn settings_dir() -> Option<std::path::PathBuf> {
|
||||
eframe::storage_dir("SolidCraft")
|
||||
fn settings_dir() -> Result<Option<std::path::PathBuf>, String> {
|
||||
let selected = std::env::var_os("SOLIDCRAFT_SETTINGS_DIR");
|
||||
startup::settings_directory(selected.as_deref(), eframe::storage_dir("SolidCraft"))
|
||||
}
|
||||
|
||||
/// The anti-aliasing preference, kept next to eframe's storage (it is needed before the
|
||||
/// window opens).
|
||||
fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
settings_dir().map(|d| d.join("graphics.json"))
|
||||
settings_dir().ok().flatten().map(|d| d.join("graphics.json"))
|
||||
}
|
||||
|
||||
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
|
||||
@@ -471,7 +536,7 @@ fn graphics_path() -> Option<std::path::PathBuf> {
|
||||
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
|
||||
/// are never changed.
|
||||
fn adopt_legacy_settings() {
|
||||
let (Some(current), Some(legacy)) = (settings_dir(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let (Some(current), Some(legacy)) = (settings_dir().ok().flatten(), eframe::storage_dir("SolveCraft")) else { return };
|
||||
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
|
||||
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(¤t);
|
||||
if let Err(e) = adopt_settings(¤t, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
|
||||
@@ -522,13 +587,22 @@ fn scene_samples(backend: eframe::wgpu::Backend, samples: u32, supported: &[u32]
|
||||
}
|
||||
}
|
||||
|
||||
/// Anti-aliasing samples from Preferences (4 when unset).
|
||||
fn msaa() -> u16 {
|
||||
/// Keep the default until the installed renderer's startup check proves a fallback is needed.
|
||||
fn scene_msaa_request(saved: Option<u16>) -> u16 {
|
||||
saved.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Query the scene's colour/depth intersection, rather than the depthless window's counts.
|
||||
fn scene_sample_choices(depth_bits: u8, stencil_bits: u8, query: impl FnOnce(Option<eframe::wgpu::TextureFormat>) -> Vec<u32>) -> Vec<u32> {
|
||||
query(eframe::egui_wgpu::depth_format_from_bits(depth_bits, stencil_bits))
|
||||
}
|
||||
|
||||
/// An explicitly saved anti-aliasing choice, absent until Graphics changes it.
|
||||
fn msaa() -> Option<u16> {
|
||||
graphics()
|
||||
.and_then(|v| v.get("msaa").and_then(serde_json::Value::as_u64))
|
||||
.and_then(|m| u16::try_from(m).ok())
|
||||
.filter(|m| [1, 2, 4, 8].contains(m))
|
||||
.unwrap_or(MSAA)
|
||||
}
|
||||
|
||||
/// Windows and Linux: no OS title bar; the application bar is the title bar
|
||||
@@ -544,7 +618,8 @@ const FLOAT_DEPTH_BITS: u8 = 32;
|
||||
const STENCIL_BITS: u8 = 8;
|
||||
const MSAA: u16 = 4;
|
||||
|
||||
fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
/// `custom_titlebar`: no OS decorations, the application bar is the title bar.
|
||||
fn app_viewport(custom_titlebar: bool) -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
// Let macOS use the bundle's layered icon; eframe's default bitmap would replace it.
|
||||
#[cfg(target_os = "macos")]
|
||||
let icon = egui::IconData::default();
|
||||
@@ -557,7 +632,7 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
.with_inner_size([1600.0, 1000.0])
|
||||
.with_min_inner_size([960.0, 600.0])
|
||||
.with_drag_and_drop(true)
|
||||
.with_decorations(!CUSTOM_TITLEBAR)
|
||||
.with_decorations(!custom_titlebar)
|
||||
.with_fullsize_content_view(true)
|
||||
.with_titlebar_shown(false)
|
||||
.with_title_shown(false)
|
||||
@@ -567,11 +642,27 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
|
||||
/// The window's app id: it names the desktop entry, so it is the packages' reverse-DNS id.
|
||||
const APP_ID: &str = solidcraft_engine::io::APP_ID;
|
||||
|
||||
/// Disconnects from the 3D mouse driver when dropped, as `main` returns or a panic unwinds out
|
||||
/// of it (`solidcraft_3dx_client::stop`).
|
||||
#[cfg(target_os = "macos")]
|
||||
struct DriverClient;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl Drop for DriverClient {
|
||||
fn drop(&mut self) {
|
||||
solidcraft_3dx_client::stop();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
adopt_legacy_settings();
|
||||
// Validate before legacy adoption, crash markers or any user preference read.
|
||||
let settings = settings_dir().map_err(|error| eframe::Error::AppCreation(error.into()))?;
|
||||
if std::env::var_os("SOLIDCRAFT_SETTINGS_DIR").is_none() {
|
||||
adopt_legacy_settings();
|
||||
}
|
||||
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
|
||||
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
@@ -608,7 +699,6 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
// Settings that don't read are moved aside; launches that never got a window are counted,
|
||||
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
|
||||
let settings = settings_dir();
|
||||
let mut notes: Vec<String> = Vec::new();
|
||||
let mut failed_starts = 0;
|
||||
if let Some(dir) = &settings {
|
||||
@@ -628,7 +718,13 @@ fn main() -> eframe::Result {
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
}
|
||||
let samples = if safe_mode { MSAA } else { msaa() };
|
||||
let fallback_note = std::env::var(startup::FALLBACK_NOTE_ENV).ok().map(|s| s.chars().take(4096).collect::<String>());
|
||||
if let Some(note) = &fallback_note {
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
notes.push(note.clone());
|
||||
}
|
||||
let saved_samples = if safe_mode { None } else { msaa() };
|
||||
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
|
||||
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||
@@ -646,10 +742,16 @@ fn main() -> eframe::Result {
|
||||
// module on Windows, and on every system the startup step the app was in.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::install();
|
||||
// Windows with only OpenGL to start: the system title bar instead of ours (#2063).
|
||||
let started = match &wgpu_options.wgpu_setup {
|
||||
eframe::egui_wgpu::WgpuSetup::CreateNew(setup) => setup.instance_descriptor.backends,
|
||||
_ => eframe::wgpu::Backends::all(),
|
||||
};
|
||||
let custom_titlebar = CUSTOM_TITLEBAR && !startup::system_title_bar(cfg!(target_os = "windows"), started);
|
||||
eframe::egui_wgpu::set_startup_hook(startup_step);
|
||||
solidcraft_engine::crash::phase("window");
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: app_viewport()?,
|
||||
viewport: app_viewport(custom_titlebar)?,
|
||||
// The window's pass (egui's) has no depth and one sample: the viewport draws its scene
|
||||
// offscreen with its own depth and MSAA, then blits it, so the UI pass pays for neither.
|
||||
depth_buffer: 0,
|
||||
@@ -671,12 +773,24 @@ fn main() -> eframe::Result {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
let mut options = options;
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if prefer_x11(
|
||||
std::env::var_os("DISPLAY").is_some(),
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some(),
|
||||
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
|
||||
solidcraft_engine::geom::env::var("WAYLAND").as_deref(),
|
||||
) {
|
||||
let x11 = {
|
||||
// SOLIDCRAFT_WAYLAND wins over Preferences › Graphics › Window system (#3083).
|
||||
let environment = solidcraft_engine::geom::env::var("WAYLAND");
|
||||
let preference = window_system_preference();
|
||||
let wayland_override = environment.as_deref().or(window_system_override(preference.as_deref()));
|
||||
let wayland_display = std::env::var_os("WAYLAND_DISPLAY").is_some();
|
||||
let x11 = prefer_x11(
|
||||
std::env::var_os("DISPLAY").is_some(),
|
||||
wayland_display,
|
||||
std::env::var("XDG_CURRENT_DESKTOP").ok().as_deref(),
|
||||
wayland_override,
|
||||
);
|
||||
// Without a Wayland socket winit opens on X11 anyway.
|
||||
let _ = WINDOW_SYSTEM.set(if x11 || !wayland_display { "x11" } else { "wayland" });
|
||||
x11
|
||||
};
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
if x11 {
|
||||
options.event_loop_builder = Some(Box::new(|b| {
|
||||
use winit::platform::x11::EventLoopBuilderExtX11;
|
||||
b.with_x11();
|
||||
@@ -688,6 +802,10 @@ fn main() -> eframe::Result {
|
||||
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
let created_in_app = created.clone();
|
||||
let starting = settings.clone();
|
||||
// A panic on the window's thread ends the app without `on_exit`; this disconnects from the
|
||||
// 3D mouse driver then too.
|
||||
#[cfg(target_os = "macos")]
|
||||
let _driver_client = DriverClient;
|
||||
mark("run_native");
|
||||
let result = eframe::run_native(
|
||||
"SolidCraft",
|
||||
@@ -701,18 +819,24 @@ fn main() -> eframe::Result {
|
||||
mark("fonts");
|
||||
let mut app = SolveApp::new(Session::default(), services(cc));
|
||||
app.apply_off_thread = Some(solidcraft_ui_egui::dialogs::APPLY_AT_ONCE);
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
// Windows: the OS hit test offers the resize border, at the window's DPI, from the
|
||||
// first message (#1381).
|
||||
app.custom_titlebar = custom_titlebar;
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
app.native_resize_border = CUSTOM_TITLEBAR && native_frame::install(cc);
|
||||
if CUSTOM_TITLEBAR && !app.native_resize_border {
|
||||
// Windows: the taskbar button shows the mark even when slow first frames keep the
|
||||
// taskbar waiting for the window's icon.
|
||||
if !native_frame::set_class_icon(cc) {
|
||||
eprintln!("SolidCraft window: no class icon; the taskbar may show a generic one");
|
||||
}
|
||||
// The OS hit test offers the resize border, at the window's DPI, from the first
|
||||
// message (#1381).
|
||||
app.native_resize_border = custom_titlebar && native_frame::install(cc);
|
||||
if custom_titlebar && !app.native_resize_border {
|
||||
eprintln!("SolidCraft window: no native resize border; egui edge zones instead");
|
||||
}
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.cpu_meshing = safe_mode;
|
||||
app.graphics_fallback = fallback_note.clone();
|
||||
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
|
||||
app.load_prefs(&p);
|
||||
}
|
||||
@@ -722,27 +846,47 @@ fn main() -> eframe::Result {
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string)).unwrap_or_default();
|
||||
}
|
||||
if let Some(rs) = &cc.wgpu_render_state {
|
||||
let scene_samples = scene_samples(rs.adapter.get_info().backend, u32::from(samples), &rs.msaa_supported);
|
||||
app.services.graphics_adapters = rs
|
||||
.available_adapters
|
||||
.iter()
|
||||
.map(|adapter| {
|
||||
let info = adapter.get_info();
|
||||
(info.name.clone(), format!("{} ({:?}, {:?})", info.name, info.device_type, info.backend))
|
||||
})
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
let samples = scene_msaa_request(saved_samples);
|
||||
let supported = scene_sample_choices(depth_bits, STENCIL_BITS, |depth| {
|
||||
eframe::egui_wgpu::supported_msaa_samples(&rs.adapter, &rs.device, rs.target_format, depth)
|
||||
});
|
||||
let scene_samples = scene_samples(info.backend, u32::from(samples), &supported);
|
||||
eprintln!("SolidCraft graphics: {:?}, MSAA {scene_samples}x (requested {}x)", rs.adapter.get_info().backend, samples);
|
||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||
app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
let scene_samples = app.set_wgpu_split(rs, depth_bits, STENCIL_BITS, scene_samples);
|
||||
app.msaa_supported = supported;
|
||||
app.preferences.msaa = u32::from(samples);
|
||||
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
||||
// The graphics driver is loaded now; it may have put its own exception filter
|
||||
// in front of ours.
|
||||
#[cfg(target_os = "windows")]
|
||||
native_crash::reinstall();
|
||||
// A saved anti-aliasing the device can't do: back to the default, with a notice.
|
||||
// Reflect a confirmed probe fallback or an unsupported saved count in Graphics.
|
||||
solidcraft_ui_egui::prefs::check_msaa(&mut app, u32::from(samples));
|
||||
let info = rs.adapter.get_info();
|
||||
solidcraft_engine::crash::set_environment(&format!(
|
||||
"Renderer: {:?} {} ({}), MSAA {scene_samples}x",
|
||||
info.backend, info.name, info.driver
|
||||
));
|
||||
}
|
||||
mark("viewport pipelines");
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
// `--control`/the environment name a port; otherwise the preference (on by default)
|
||||
// opens the default one (docs/control-protocol.md).
|
||||
if let Some(listen_on) = solidcraft_engine::control_auth::plan(control_port, app.preferences.agent_control) {
|
||||
let rx = control_server::start(listen_on, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
// The agent chat's MCP bridge reaches the app through an endpoint of its own, started
|
||||
@@ -757,6 +901,9 @@ fn main() -> eframe::Result {
|
||||
app.session.mark_saved();
|
||||
app.session.undo.clear();
|
||||
}
|
||||
// Lock files next to open designs: a design open on another computer says so (#5467),
|
||||
// the files given at launch included.
|
||||
solidcraft_ui_egui::open_elsewhere::start(&mut app);
|
||||
// A launch from Finder: its files arrived before the window.
|
||||
#[cfg(target_os = "macos")]
|
||||
let files = {
|
||||
@@ -791,8 +938,9 @@ fn main() -> eframe::Result {
|
||||
if !notes.is_empty() {
|
||||
app.set_status(notes.join(" "), true);
|
||||
}
|
||||
let nav_bridge = nav_bridge::NavBridge::start(&cc.egui_ctx);
|
||||
#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))]
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
|
||||
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx, nav_bridge.claim());
|
||||
#[cfg(target_os = "macos")]
|
||||
let native_menu = match native_menu::NativeMenu::new(&app, &cc.egui_ctx) {
|
||||
Ok(menu) => Some(menu),
|
||||
@@ -817,12 +965,13 @@ fn main() -> eframe::Result {
|
||||
space_mouse,
|
||||
#[cfg(target_os = "macos")]
|
||||
native_menu,
|
||||
nav_bridge,
|
||||
}))
|
||||
}),
|
||||
);
|
||||
control_server::remove_token_file();
|
||||
match result {
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
|
||||
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, started, settings.as_deref()),
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
@@ -843,13 +992,19 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
|
||||
if let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
|
||||
let env_set = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
// A retry applies only to this launch. WGPU_BACKEND always has priority.
|
||||
let retry = std::env::var(startup::RETRY_BACKEND_ENV).ok();
|
||||
let preference = if cpu { preference } else { retry.as_deref().or(preference) };
|
||||
if let Some(backends) = startup::choose_backends(cfg!(target_os = "windows"), env_set, cpu, preference, startup::has_hardware_adapter) {
|
||||
eprintln!("SolidCraft graphics: starting {backends:?}");
|
||||
setup.instance_descriptor.backends = backends;
|
||||
}
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||
}
|
||||
let intel_mac = cfg!(all(target_os = "macos", target_arch = "x86_64"));
|
||||
let indirect = std::env::var(startup::INDIRECT_VALIDATION_ENV).ok();
|
||||
setup.instance_descriptor.flags = startup::window_instance_flags(setup.instance_descriptor.flags, intel_mac, indirect.as_deref());
|
||||
let environment = std::env::var("WGPU_ADAPTER_NAME").ok();
|
||||
let adapter = graphics().and_then(|v| v.get("adapter").and_then(serde_json::Value::as_str).map(str::to_string));
|
||||
startup::configure_adapter(setup, cpu, environment, adapter);
|
||||
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
|
||||
let base = setup.device_descriptor.clone();
|
||||
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
|
||||
@@ -861,29 +1016,82 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
||||
options
|
||||
}
|
||||
|
||||
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
|
||||
/// the software renderer, else a message box saying what failed instead of a silent exit.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
/// A failed renderer returns here before app creation. A fresh process avoids reusing winit's
|
||||
/// event loop or a failed device; each retry advances through the APIs, then software once.
|
||||
fn no_window(error: eframe::Error, tried_cpu: bool, failed: eframe::wgpu::Backends, settings: Option<&std::path::Path>) -> eframe::Result {
|
||||
log::error!("the window could not open: {error}");
|
||||
eprintln!("SolidCraft: the window could not open: {error}");
|
||||
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
|
||||
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
|
||||
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
|
||||
// This failure was handled, so neither the marker nor the journal should call it a crash.
|
||||
if let Some(dir) = settings {
|
||||
startup::started(dir);
|
||||
}
|
||||
solidcraft_engine::crash::end_session();
|
||||
let explicit = std::env::var(startup::WGPU_BACKEND_ENV).is_ok_and(|v| !v.trim().is_empty());
|
||||
if let Some(next) = startup::fallback_backend(cfg!(target_os = "windows"), explicit, tried_cpu, failed)
|
||||
&& let Ok(exe) = std::env::current_exe()
|
||||
{
|
||||
let previous = std::env::var(startup::FALLBACK_NOTE_ENV).unwrap_or_default();
|
||||
let note = startup::fallback_note(&previous, failed, next, &error.to_string());
|
||||
log::warn!("{note}");
|
||||
eprintln!("SolidCraft: {note}");
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(startup::FALLBACK_NOTE_ENV, note);
|
||||
if next == "cpu" {
|
||||
command.env(startup::RENDERER_ENV, "cpu").env_remove(startup::RETRY_BACKEND_ENV);
|
||||
} else {
|
||||
command.env(startup::RETRY_BACKEND_ENV, next);
|
||||
}
|
||||
match command.status() {
|
||||
Ok(status) if status.success() => return Ok(()),
|
||||
// The second try showed its own message.
|
||||
// The final try showed its own message.
|
||||
Ok(_) => return Err(error),
|
||||
Err(e) => log::error!("could not start again with the software renderer: {e}"),
|
||||
Err(e) => log::error!("could not start again on {next}: {e}"),
|
||||
}
|
||||
}
|
||||
let message = if explicit {
|
||||
format!("SolidCraft could not open its window with WGPU_BACKEND.\n\n{error}\n\nChange or remove WGPU_BACKEND to try another graphics API.")
|
||||
} else {
|
||||
startup::no_window_message(&error.to_string(), settings)
|
||||
};
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("SolidCraft could not start")
|
||||
.set_description(startup::no_window_message(&error.to_string(), settings))
|
||||
.set_description(message)
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
Err(error)
|
||||
}
|
||||
|
||||
/// Linux: the window system this run opened with (`wayland` or `x11`), for Preferences (#3083).
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
static WINDOW_SYSTEM: std::sync::OnceLock<&'static str> = std::sync::OnceLock::new();
|
||||
|
||||
/// The window system this run uses, where there is a choice (Linux).
|
||||
fn window_system_now() -> Option<&'static str> {
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
return WINDOW_SYSTEM.get().copied();
|
||||
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||
None
|
||||
}
|
||||
|
||||
/// Preferences › Graphics › Window system (`graphics.json`'s `window_system`), read before the
|
||||
/// window opens.
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn window_system_preference() -> Option<String> {
|
||||
graphics().and_then(|v| v.get("window_system").and_then(serde_json::Value::as_str).map(str::to_string))
|
||||
}
|
||||
|
||||
/// The window system preference as the `SOLIDCRAFT_WAYLAND` value it stands for: `wayland` is
|
||||
/// `1`, `x11` is `0`, and `auto` (or anything else) leaves the choice to [`prefer_x11`].
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn window_system_override(preference: Option<&str>) -> Option<&'static str> {
|
||||
match preference.map(str::trim) {
|
||||
Some("wayland") => Some("1"),
|
||||
Some("x11") => Some("0"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
|
||||
/// on Wayland (only on X11), so dropping files from the file manager showed a "no" cursor: use
|
||||
/// XWayland when it's there. Except on niri, whose XWayland (xwayland-satellite) breaks window
|
||||
@@ -921,14 +1129,27 @@ mod backend_tests {
|
||||
assert!(prefer_x11(true, true, Some("niri"), Some("0")));
|
||||
assert!(!prefer_x11(false, true, Some("niri"), Some("0")));
|
||||
}
|
||||
|
||||
/// Preferences › Graphics › Window system stands for the same values (#3083).
|
||||
#[test]
|
||||
fn the_window_system_preference_maps_to_the_override() {
|
||||
use super::window_system_override as over;
|
||||
assert_eq!(
|
||||
(over(Some("wayland")), over(Some("x11")), over(Some("auto")), over(None), over(Some("junk"))),
|
||||
(Some("1"), Some("0"), None, None, None)
|
||||
);
|
||||
// Wayland chosen on GNOME keeps native Wayland; X11 chosen on niri goes through XWayland.
|
||||
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod icon_tests {
|
||||
mod viewport_tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_preserves_the_bundle_icon_instead_of_setting_a_runtime_bitmap() {
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
assert_eq!(viewport.icon.as_deref(), Some(&eframe::egui::IconData::default()));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
@@ -936,12 +1157,89 @@ mod icon_tests {
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[test]
|
||||
fn desktop_viewport_supplies_the_original_mark_to_the_window() {
|
||||
let viewport = super::app_viewport().expect("viewport");
|
||||
let viewport = super::app_viewport(super::CUSTOM_TITLEBAR).expect("viewport");
|
||||
let icon = viewport.icon.expect("running window must have an icon");
|
||||
assert_eq!((icon.width, icon.height), (64, 64));
|
||||
assert_eq!(icon.rgba, super::app_icon::rgba(64).expect("original mark"));
|
||||
assert_eq!(viewport.app_id.as_deref(), Some(super::APP_ID));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_bar_clears_native_chrome_and_undo_redo_keep_working() {
|
||||
use egui::{Event, PointerButton, Pos2, RawInput, Rect, ViewportId, vec2};
|
||||
use serde_json::json;
|
||||
use solidcraft_ui_egui::scenario::handle_at;
|
||||
|
||||
// AppKit points are independent of Retina scale and of the user's UI zoom.
|
||||
for (zoom, retina) in [(1.0, 1.0), (1.0, 2.0), (1.5, 2.0), (2.0, 2.0)] {
|
||||
let ctx = egui::Context::default();
|
||||
solidcraft_ui_egui::fonts::setup_bundled_fonts(&ctx);
|
||||
ctx.set_zoom_factor(zoom);
|
||||
let mut app = super::SolveApp::new(super::Session::default(), Default::default());
|
||||
app.integrated_titlebar = true;
|
||||
for (command, params) in [
|
||||
("sketch.create", json!({"plane":"XY"})),
|
||||
("sketch.rectangle.two_point", json!({"p0":[0,0],"p1":[40,30]})),
|
||||
("sketch.finish", json!({})),
|
||||
("solid.extrude", json!({"distance":15})),
|
||||
] {
|
||||
app.run(command, params).unwrap();
|
||||
}
|
||||
let mut time = 0.0;
|
||||
let mut frame = |app: &mut super::SolveApp, fullscreen, events| {
|
||||
time += 0.05;
|
||||
let mut raw = RawInput {
|
||||
screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1600.0, 1000.0))),
|
||||
time: Some(time),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let viewport = raw.viewports.get_mut(&ViewportId::ROOT).unwrap();
|
||||
viewport.fullscreen = Some(fullscreen);
|
||||
viewport.native_pixels_per_point = Some(retina);
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, zoom);
|
||||
let _ = ctx.run_ui(raw, |ui| super::content_ui(app, ui));
|
||||
};
|
||||
// Enter, leave, and enter again: no stale inset when returning to a normal window.
|
||||
for fullscreen in [false, true, false, true] {
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
frame(&mut app, fullscreen, vec![]);
|
||||
let top = if fullscreen { 32.0 / zoom } else { 0.0 };
|
||||
for name in ["appbar:file", "appbar:undo", "appbar:redo", "appbar:help"] {
|
||||
let at = handle_at(name).unwrap();
|
||||
assert!((at.y - top - 17.0).abs() < 0.1, "{name}: {at:?}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
for (handle, bodies) in [("appbar:undo", 0), ("appbar:redo", 1)] {
|
||||
let at = handle_at(handle).unwrap();
|
||||
frame(&mut app, fullscreen, vec![Event::PointerMoved(at)]);
|
||||
for pressed in [true, false] {
|
||||
frame(
|
||||
&mut app,
|
||||
fullscreen,
|
||||
vec![Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() }],
|
||||
);
|
||||
}
|
||||
assert_eq!(app.session.model.state().bodies.len(), bodies, "{handle}, fullscreen={fullscreen}, zoom={zoom}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_inset_preserves_existing_safe_areas_and_resets_on_exit() {
|
||||
use egui::epaint::MarginF32;
|
||||
use egui::{RawInput, SafeAreaInsets, ViewportId};
|
||||
let mut raw = RawInput::default();
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(true);
|
||||
raw.safe_area_insets = Some(SafeAreaInsets(MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 }));
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 2.0);
|
||||
assert_eq!(raw.safe_area_insets.unwrap().0, MarginF32 { top: 40.0, left: 5.0, right: 6.0, bottom: 7.0 });
|
||||
|
||||
raw.viewports.get_mut(&ViewportId::ROOT).unwrap().fullscreen = Some(false);
|
||||
raw.safe_area_insets = None;
|
||||
super::fullscreen_content_insets(&mut raw, 32.0, 1.0);
|
||||
assert_eq!(raw.safe_area_insets, Some(SafeAreaInsets::default()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -979,6 +1277,34 @@ mod msaa_tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_sample_choices_include_the_scene_depth_format() {
|
||||
use eframe::wgpu::TextureFormat;
|
||||
for (bits, expected) in [(24, TextureFormat::Depth24PlusStencil8), (32, TextureFormat::Depth32FloatStencil8)] {
|
||||
// A synthetic colour target supports 8x, but the scene depth supports only 1x/4x.
|
||||
let supported = super::scene_sample_choices(bits, 8, |depth| match depth {
|
||||
Some(format) if format == expected => vec![1, 4],
|
||||
None => vec![1, 4, 8],
|
||||
other => panic!("unexpected scene depth: {other:?}"),
|
||||
});
|
||||
assert_eq!(supported, [1, 4]);
|
||||
assert_eq!(super::scene_samples(eframe::wgpu::Backend::Dx12, 8, &supported), 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_msaa_keeps_four_samples_by_default_and_explicit_choices() {
|
||||
use eframe::wgpu::Backend;
|
||||
let default = super::scene_msaa_request(None);
|
||||
assert_eq!(default, 4);
|
||||
for backend in [Backend::Dx12, Backend::Vulkan, Backend::Metal] {
|
||||
assert_eq!(super::scene_samples(backend, u32::from(default), &[1, 4]), 4);
|
||||
}
|
||||
for saved in [1, 2, 4, 8] {
|
||||
assert_eq!(super::scene_msaa_request(Some(saved)), saved);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_scene_follows_the_backend_policy_too() {
|
||||
use eframe::wgpu::Backend;
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
//! window rescales once at the crossing and stays under the pointer, instead of the OS's move
|
||||
//! loop and winit disagreeing about a frame the window doesn't have.
|
||||
//!
|
||||
//! Replacing a window procedure has no safe API, hence the `unsafe_code` allowance on this
|
||||
//! module (the app crate denies it; see `native_crash.rs` and `open_documents.rs` for the others).
|
||||
//! Replacing a window procedure and setting a window class's icon have no safe API, hence the
|
||||
//! `unsafe_code` allowance on this module (the app crate denies it; see `native_crash.rs` and
|
||||
//! `open_documents.rs` for the others).
|
||||
//! The procedure is installed once, on the thread that owns the window, for the main window only,
|
||||
//! and calls the previous one (winit's) for everything it doesn't answer.
|
||||
//!
|
||||
//! [`set_class_icon`] gives the window's class the exe's icon, so the taskbar button shows the
|
||||
//! mark even when the window is too busy to answer the taskbar's request for its icon.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::sync::atomic::{AtomicIsize, Ordering};
|
||||
@@ -27,12 +31,18 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
use solidcraft_ui_egui::titlebar;
|
||||
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, RECT, SIZE, WPARAM};
|
||||
use windows_sys::Win32::Graphics::Gdi::{GetMonitorInfoW, MONITOR_DEFAULTTONEAREST, MONITORINFO, MonitorFromWindow};
|
||||
use windows_sys::Win32::System::LibraryLoader::GetModuleHandleW;
|
||||
use windows_sys::Win32::UI::HiDpi::{GetDpiForWindow, GetSystemMetricsForDpi};
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::{
|
||||
CallWindowProcW, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT,
|
||||
HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IsIconic, IsZoomed, SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SetWindowLongPtrW, WM_GETDPISCALEDSIZE,
|
||||
WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
CallWindowProcW, GCLP_HICON, GCLP_HICONSM, GWL_STYLE, GWLP_WNDPROC, GetClientRect, GetWindowLongPtrW, GetWindowRect, HTBOTTOM, HTBOTTOMLEFT,
|
||||
HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, IMAGE_ICON, IsIconic, IsZoomed, LR_DEFAULTCOLOR, LoadImageW, SM_CXICON,
|
||||
SM_CXPADDEDBORDER, SM_CXSIZEFRAME, SM_CXSMICON, SetWindowLongPtrW, WM_GETDPISCALEDSIZE, WM_NCDESTROY, WM_NCHITTEST, WNDPROC, WS_THICKFRAME,
|
||||
};
|
||||
// windows-sys has SetClassLongPtrW only on 64-bit targets; on 32-bit Windows it is SetClassLongW.
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongPtrW;
|
||||
#[cfg(target_pointer_width = "32")]
|
||||
use windows_sys::Win32::UI::WindowsAndMessaging::SetClassLongW as SetClassLongPtrW;
|
||||
|
||||
/// The work area (the monitor without the taskbar) of the monitor nearest the window, in physical
|
||||
/// pixels: left, top, width, height.
|
||||
@@ -54,6 +64,48 @@ pub fn work_area(window: &impl HasWindowHandle) -> Option<(i32, i32, i32, i32)>
|
||||
Some((rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top))
|
||||
}
|
||||
|
||||
/// The exe's icon resource (`build.rs` embeds it as `1 ICON`).
|
||||
const ICON_RESOURCE: usize = 1;
|
||||
|
||||
/// Give the main window's class the exe's icon, big and small. Call it before the window is
|
||||
/// shown (eframe's app creator).
|
||||
///
|
||||
/// The taskbar asks a window for its icon when the window appears. eframe sets the window's
|
||||
/// icon before that, while the window is still hidden, and then shows the window at the end of
|
||||
/// its first frame; if that frame is slow, or startup kept the thread busy long enough for
|
||||
/// Windows to mark the window as not responding, the request isn't answered in time. The taskbar then
|
||||
/// takes the window class's icon and doesn't ask again. winit registers its class without one,
|
||||
/// so the button showed Windows' generic icon for as long as the window was open.
|
||||
/// `false` if it isn't a Win32 window or the exe's icon doesn't load; SetClassLongPtrW's own
|
||||
/// failure can't be told from a class that had no icon (both return 0), so it isn't reported.
|
||||
pub fn set_class_icon(window: &impl HasWindowHandle) -> bool {
|
||||
let Ok(handle) = window.window_handle() else { return false };
|
||||
let RawWindowHandle::Win32(w) = handle.as_raw() else { return false };
|
||||
let hwnd = w.hwnd.get() as HWND;
|
||||
// SAFETY: a plain query on the live main window.
|
||||
let dpi = unsafe { GetDpiForWindow(hwnd) };
|
||||
let (Some(big), Some(small)) = (exe_icon(SM_CXICON, dpi), exe_icon(SM_CXSMICON, dpi)) else { return false };
|
||||
// SAFETY: `hwnd` is the live main window and both icons are valid handles. The class keeps
|
||||
// them for the rest of the process, so they are never destroyed. `as _`: on 32-bit Windows
|
||||
// this is SetClassLongW (see the imports), which takes an i32.
|
||||
unsafe {
|
||||
SetClassLongPtrW(hwnd, GCLP_HICON, big as _);
|
||||
SetClassLongPtrW(hwnd, GCLP_HICONSM, small as _);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// The exe's icon at the size of system metric `metric` (`SM_CXICON`, `SM_CXSMICON`) at `dpi`.
|
||||
fn exe_icon(metric: i32, dpi: u32) -> Option<windows_sys::Win32::Foundation::HANDLE> {
|
||||
// SAFETY: GetModuleHandleW(null) is this exe, loaded for the life of the process; the name
|
||||
// is an integer resource id (MAKEINTRESOURCE), which LoadImageW accepts in place of a string.
|
||||
let icon = unsafe {
|
||||
let size = GetSystemMetricsForDpi(metric, dpi);
|
||||
LoadImageW(GetModuleHandleW(std::ptr::null()), std::ptr::without_provenance(ICON_RESOURCE), IMAGE_ICON, size, size, LR_DEFAULTCOLOR)
|
||||
};
|
||||
(!icon.is_null()).then_some(icon)
|
||||
}
|
||||
|
||||
/// The window procedure ours replaced (winit's), as an address; 0 before [`install`].
|
||||
static PREVIOUS: AtomicIsize = AtomicIsize::new(0);
|
||||
|
||||
|
||||
@@ -105,6 +105,7 @@ impl NativeMenu {
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
("crashes", "Crash Reports\u{2026}"),
|
||||
("install_cli", "Install solidcraft-cli command"),
|
||||
] {
|
||||
let item = MenuItem::new(app.tr_label(label), true, None);
|
||||
help.append(&item)?;
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
//! The navigation bridge: a 3D input device's own program (a homemade device's driver app) moves
|
||||
//! the view over a loopback connection, the way a 3D mouse driver hands its axes to the programs
|
||||
//! it supports, so the device emulates no mouse and the pointer stays the user's.
|
||||
//!
|
||||
//! The device app listens on `127.0.0.1:<port>`; with Preferences › Navigation › "Navigate with a
|
||||
//! device app" on (off by default), SolidCraft connects to it, says hello and reads one JSON
|
||||
//! object per line (docs/3d-mouse.md). The reader thread only sums the motion; the egui thread
|
||||
//! hands it to the view once per frame (`space_mouse::bridge_frame`). A device the app says it
|
||||
//! reads is left to it by the 3D mouse reader ([`Claim`]), so the view doesn't move twice.
|
||||
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddr, TcpStream};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use egui::Context;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_ui_egui::SolveApp;
|
||||
use solidcraft_ui_egui::space_mouse::{BRIDGE_VIEWS, BridgeMotion, bridge_frame, bridge_step};
|
||||
|
||||
/// The longest line read; a longer one is skipped to its end.
|
||||
const MAX_LINE: usize = 4096;
|
||||
/// Standard views waiting for the next frame; past this many, more are a flood and dropped.
|
||||
const MAX_VIEWS: usize = 8;
|
||||
/// The longest name a device app may give for itself or its device, in characters.
|
||||
const MAX_NAME: usize = 64;
|
||||
/// How long a connection attempt waits.
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
/// How long a read waits for data before the reader looks at the preferences again.
|
||||
const READ_WAKE: Duration = Duration::from_millis(250);
|
||||
/// The wait before connecting again when no device app answered or one went away.
|
||||
const RETRY_AFTER: Duration = Duration::from_secs(1);
|
||||
/// In zoom mode, a motion of this many points along Y is one zoom step: with 8 % a step, the 1 %
|
||||
/// a point that a zoom drag moves.
|
||||
const POINTS_PER_ZOOM_STEP: f64 = 8.0;
|
||||
|
||||
/// What the device's motion drives, as the device app last said (`mode`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
#[default]
|
||||
Orbit,
|
||||
Pan,
|
||||
Zoom,
|
||||
}
|
||||
|
||||
/// What the reader thread and the egui thread share.
|
||||
#[derive(Debug, Default)]
|
||||
struct Shared {
|
||||
/// The port the preferences ask to connect to; none while the bridge is off.
|
||||
wanted: Option<u16>,
|
||||
/// The device app connected, by the name it gave ("?" until it gives one).
|
||||
connected: Option<String>,
|
||||
/// The device the connected app reads, as it named it: the 3D mouse reader leaves that one alone.
|
||||
device: Option<String>,
|
||||
/// Motion since the egui thread last took it.
|
||||
motion: BridgeMotion,
|
||||
/// Standard views asked for since then.
|
||||
views: Vec<String>,
|
||||
}
|
||||
|
||||
fn lock(shared: &Mutex<Shared>) -> Option<MutexGuard<'_, Shared>> {
|
||||
shared.lock().ok()
|
||||
}
|
||||
|
||||
/// A name a device app gave, without control characters and cut to [`MAX_NAME`]; none when empty.
|
||||
fn name(value: Option<&Value>) -> Option<String> {
|
||||
let name: String = value?.as_str()?.chars().filter(|c| !c.is_control()).take(MAX_NAME).collect();
|
||||
let name = name.trim();
|
||||
(!name.is_empty()).then(|| name.to_string())
|
||||
}
|
||||
|
||||
/// A number of a message, finite and in range; none when it isn't one.
|
||||
fn number(msg: &Value, key: &str) -> f64 {
|
||||
bridge_step(msg.get(key).and_then(Value::as_f64).unwrap_or(0.0))
|
||||
}
|
||||
|
||||
/// Takes one line from the device app into `shared`. Lines that aren't a message this version
|
||||
/// knows are ignored. Returns whether the view has something new to show.
|
||||
fn consume(line: &[u8], mode: &mut Mode, shared: &mut Shared) -> bool {
|
||||
let Ok(msg) = serde_json::from_slice::<Value>(line) else { return false };
|
||||
let add = |shared: &mut Shared, motion: BridgeMotion| {
|
||||
shared.motion = shared.motion.plus(motion);
|
||||
!motion.is_still()
|
||||
};
|
||||
match msg.get("t").and_then(Value::as_str) {
|
||||
Some("axis") => {
|
||||
let (x, y) = (number(&msg, "x"), number(&msg, "y"));
|
||||
let motion = match mode {
|
||||
Mode::Orbit => BridgeMotion { orbit: [x, y], ..Default::default() },
|
||||
Mode::Pan => BridgeMotion { pan: [x, y], ..Default::default() },
|
||||
// Up zooms in, as a zoom drag does.
|
||||
Mode::Zoom => BridgeMotion { zoom: -y / POINTS_PER_ZOOM_STEP, ..Default::default() },
|
||||
};
|
||||
add(shared, motion)
|
||||
}
|
||||
Some("zoom") => add(shared, BridgeMotion { zoom: number(&msg, "d"), ..Default::default() }),
|
||||
Some("mode") => {
|
||||
let on = msg.get("on").and_then(Value::as_bool).unwrap_or(false);
|
||||
*mode = match msg.get("mode").and_then(Value::as_str) {
|
||||
Some("pan") if on => Mode::Pan,
|
||||
Some("zoom") if on => Mode::Zoom,
|
||||
_ => Mode::Orbit,
|
||||
};
|
||||
false
|
||||
}
|
||||
Some("view") => {
|
||||
let view = msg.get("name").and_then(Value::as_str).and_then(|n| BRIDGE_VIEWS.iter().find(|v| **v == n));
|
||||
match view {
|
||||
Some(view) if shared.views.len() < MAX_VIEWS => {
|
||||
shared.views.push((*view).to_string());
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
Some("hello") => {
|
||||
shared.connected = name(msg.get("server")).or_else(|| shared.connected.clone());
|
||||
shared.device = name(msg.get("device"));
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits what arrives into lines, across reads. A line longer than [`MAX_LINE`] is dropped up to
|
||||
/// its newline, so a peer that never sends one can't make the reader hold more than that.
|
||||
#[derive(Default)]
|
||||
struct Lines {
|
||||
line: Vec<u8>,
|
||||
skipping: bool,
|
||||
}
|
||||
|
||||
impl Lines {
|
||||
fn feed(&mut self, mut data: &[u8], mut each: impl FnMut(&[u8])) {
|
||||
while !data.is_empty() {
|
||||
let end = data.iter().position(|b| *b == b'\n');
|
||||
let (part, rest) = match end {
|
||||
Some(i) => (data.get(..i).unwrap_or_default(), data.get(i + 1..).unwrap_or_default()),
|
||||
None => (data, &[][..]),
|
||||
};
|
||||
if !self.skipping {
|
||||
let room = (MAX_LINE + 1).saturating_sub(self.line.len());
|
||||
self.line.extend_from_slice(part.get(..room.min(part.len())).unwrap_or_default());
|
||||
if self.line.len() > MAX_LINE {
|
||||
self.line.clear();
|
||||
self.skipping = true;
|
||||
}
|
||||
}
|
||||
if end.is_some() {
|
||||
if !self.skipping {
|
||||
each(self.line.strip_suffix(b"\r").unwrap_or(&self.line));
|
||||
}
|
||||
self.line.clear();
|
||||
self.skipping = false;
|
||||
}
|
||||
data = rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What SolidCraft says first, so the device app knows which program it is driving.
|
||||
fn hello() -> Value {
|
||||
json!({"t": "hello", "app": "solidcraft", "version": env!("CARGO_PKG_VERSION")})
|
||||
}
|
||||
|
||||
/// Reads one connection until the device app closes it, it fails, or the preferences no longer
|
||||
/// ask for this port; then forgets what it had.
|
||||
fn serve(mut stream: TcpStream, port: u16, ctx: &Context, shared: &Mutex<Shared>) {
|
||||
// Without a read timeout the reader couldn't notice the bridge being switched off.
|
||||
if stream.set_read_timeout(Some(READ_WAKE)).is_err() || writeln!(stream, "{}", hello()).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = stream.set_nodelay(true);
|
||||
log::info!("navigation bridge: connected to 127.0.0.1:{port}");
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
shared.connected = Some("?".into());
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let (mut lines, mut mode) = (Lines::default(), Mode::default());
|
||||
let mut chunk = [0_u8; 4096];
|
||||
while lock(shared).is_some_and(|shared| shared.wanted == Some(port)) {
|
||||
match stream.read(&mut chunk) {
|
||||
Ok(0) => break,
|
||||
Ok(size) => {
|
||||
let mut news = false;
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
lines.feed(chunk.get(..size).unwrap_or_default(), |line| news |= consume(line, &mut mode, &mut shared));
|
||||
}
|
||||
if news {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
}
|
||||
Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut | ErrorKind::Interrupted) => {}
|
||||
Err(error) => {
|
||||
log::info!("navigation bridge: connection lost: {error}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(mut shared) = lock(shared) {
|
||||
*shared = Shared { wanted: shared.wanted, ..Shared::default() };
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// Connects to the device app while the preferences ask for it, again whenever it goes away.
|
||||
fn reader(ctx: Context, shared: Arc<Mutex<Shared>>) {
|
||||
loop {
|
||||
let Some(port) = lock(&shared).and_then(|shared| shared.wanted) else {
|
||||
std::thread::sleep(READ_WAKE);
|
||||
continue;
|
||||
};
|
||||
if let Ok(stream) = TcpStream::connect_timeout(&SocketAddr::from((Ipv4Addr::LOCALHOST, port)), CONNECT_TIMEOUT) {
|
||||
serve(stream, port, &ctx, &shared);
|
||||
}
|
||||
std::thread::sleep(RETRY_AFTER);
|
||||
}
|
||||
}
|
||||
|
||||
/// Which 3D mouse the connected device app reads, for the 3D mouse reader to leave alone. Only
|
||||
/// the Windows and macOS readers know a device's strings to compare.
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub struct Claim(Arc<Mutex<Shared>>);
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
impl Claim {
|
||||
/// Whether a device with these manufacturer and product strings is the one the connected
|
||||
/// device app reads (its name appears in one, ignoring case).
|
||||
pub fn claims(&self, strings: &[String]) -> bool {
|
||||
let Some(device) = lock(&self.0).and_then(|shared| shared.device.clone()) else { return false };
|
||||
let device = device.to_lowercase();
|
||||
strings.iter().any(|s| s.to_lowercase().contains(&device))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavBridge {
|
||||
shared: Arc<Mutex<Shared>>,
|
||||
}
|
||||
|
||||
impl NavBridge {
|
||||
pub fn start(ctx: &Context) -> Self {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let (worker_ctx, worker_state) = (ctx.clone(), Arc::clone(&shared));
|
||||
if let Err(error) = std::thread::Builder::new().name("solidcraft-nav-bridge".into()).spawn(move || reader(worker_ctx, worker_state)) {
|
||||
log::warn!("navigation bridge: reader could not start: {error}");
|
||||
}
|
||||
Self { shared }
|
||||
}
|
||||
|
||||
#[cfg_attr(not(any(target_os = "windows", target_os = "macos")), allow(dead_code))]
|
||||
pub fn claim(&self) -> Claim {
|
||||
Claim(Arc::clone(&self.shared))
|
||||
}
|
||||
|
||||
/// Each frame: passes the preferences to the reader, and the motion and views since the last
|
||||
/// frame to the view. Motion that arrived while the window had no focus is dropped with it.
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &Context) {
|
||||
let wanted = Some(app.preferences.nav_bridge_port).filter(|port| app.preferences.nav_bridge_enabled && *port != 0);
|
||||
let Some((motion, views, connected)) = lock(&self.shared).map(|mut shared| {
|
||||
shared.wanted = wanted;
|
||||
(std::mem::take(&mut shared.motion), std::mem::take(&mut shared.views), shared.connected.clone())
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
app.space_mouse.bridge = connected.filter(|_| wanted.is_some());
|
||||
if !motion.is_still() || !views.is_empty() {
|
||||
bridge_frame(app, ctx, motion, &views);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::net::TcpListener;
|
||||
use std::time::Instant;
|
||||
|
||||
fn feed(shared: &mut Shared, mode: &mut Mode, text: &str) -> bool {
|
||||
let mut news = false;
|
||||
Lines::default().feed(text.as_bytes(), |line| news |= consume(line, mode, shared));
|
||||
news
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axis_motion_orbits_pans_or_zooms_as_the_mode_says() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"axis\",\"x\":3,\"y\":-2}\n{\"t\":\"axis\",\"x\":1.5,\"y\":0}\n"));
|
||||
assert_eq!(shared.motion, BridgeMotion { orbit: [4.5, -2.0], ..Default::default() });
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":true}\n{\"t\":\"axis\",\"x\":5,\"y\":6}\n");
|
||||
assert_eq!(mode, Mode::Pan);
|
||||
assert_eq!(shared.motion.pan, [5.0, 6.0]);
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"zoom\",\"on\":true}\n{\"t\":\"axis\",\"x\":0,\"y\":-16}\n");
|
||||
assert_eq!(shared.motion.zoom, 2.0, "up zooms in, 8 points a step");
|
||||
// Off, whichever mode it names, is back to orbit.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"mode\",\"mode\":\"pan\",\"on\":false}\n");
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// The ring's steps add up with the zoom mode's.
|
||||
feed(&mut shared, &mut mode, "{\"t\":\"zoom\",\"d\":-0.5}\n");
|
||||
assert_eq!(shared.motion.zoom, 1.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn views_are_queued_by_their_known_names_only() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
assert!(feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"iso\"}\n{\"t\":\"view\",\"name\":\"fit\"}\n"));
|
||||
assert!(!feed(&mut shared, &mut mode, "{\"t\":\"view\",\"name\":\"ISO\"}\n{\"t\":\"view\",\"name\":\"../etc\"}\n{\"t\":\"view\"}\n"));
|
||||
assert_eq!(shared.views, ["iso", "fit"]);
|
||||
let flood = "{\"t\":\"view\",\"name\":\"top\"}\n".repeat(100);
|
||||
feed(&mut shared, &mut mode, &flood);
|
||||
assert_eq!(shared.views.len(), MAX_VIEWS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostile_lines_never_move_the_view_out_of_range() {
|
||||
let (mut shared, mut mode) = (Shared::default(), Mode::default());
|
||||
let lines = [
|
||||
"not json",
|
||||
"{\"t\":\"axis\",\"x\":\"5\",\"y\":null}",
|
||||
"{\"t\":\"axis\",\"x\":1e308,\"y\":-1e308}",
|
||||
"{\"t\":\"axis\",\"x\":1e999}",
|
||||
"{\"t\":\"zoom\",\"d\":[1]}",
|
||||
"{\"t\":42}",
|
||||
"[1,2,3]",
|
||||
"{\"t\":\"mode\",\"mode\":{\"x\":1},\"on\":\"yes\"}",
|
||||
"\u{0}\u{1}\u{2}",
|
||||
];
|
||||
for line in lines {
|
||||
Lines::default().feed(format!("{line}\n").as_bytes(), |l| {
|
||||
consume(l, &mut mode, &mut shared);
|
||||
});
|
||||
}
|
||||
let limit = solidcraft_ui_egui::space_mouse::BRIDGE_MAX_STEP;
|
||||
let values = shared.motion.orbit.into_iter().chain(shared.motion.pan).chain([shared.motion.zoom]);
|
||||
for value in values {
|
||||
assert!(value.is_finite() && value.abs() <= limit, "{value}");
|
||||
}
|
||||
assert_eq!(mode, Mode::Orbit);
|
||||
// Invalid UTF-8 is no message either.
|
||||
assert!(!consume(&[0xff, 0xfe, b'{', b'}'], &mut mode, &mut shared));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lines_split_across_reads_and_long_ones_are_skipped_to_their_end() {
|
||||
let mut got = Vec::new();
|
||||
let mut lines = Lines::default();
|
||||
for part in ["{\"t\":", "\"zoom\"}\r\n{\"a\"", ":1}\n"] {
|
||||
lines.feed(part.as_bytes(), |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
}
|
||||
assert_eq!(got, ["{\"t\":\"zoom\"}", "{\"a\":1}"]);
|
||||
got.clear();
|
||||
let long = vec![b'x'; MAX_LINE * 3];
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
lines.feed(&long, |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert!(lines.line.len() <= MAX_LINE + 1, "never holds more than one line");
|
||||
lines.feed(b"tail\nnext\n", |line| got.push(String::from_utf8_lossy(line).to_string()));
|
||||
assert_eq!(got, ["next"], "the long line is dropped to its newline, the next one read");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_device_app_names_itself_and_the_device_it_leaves_alone() {
|
||||
let shared = Arc::new(Mutex::new(Shared::default()));
|
||||
let claim = Claim(Arc::clone(&shared));
|
||||
let strings = ["STMicroelectronics".to_string(), "MAKMOUSE 3D".to_string()];
|
||||
assert!(!claim.claims(&strings), "nothing is claimed before an app says so");
|
||||
let mut mode = Mode::default();
|
||||
if let Ok(mut shared) = shared.lock() {
|
||||
let hello = format!("{{\"t\":\"hello\",\"server\":\"Dev\\u0007App {}\",\"device\":\"makmouse\"}}\n", "x".repeat(200));
|
||||
feed(&mut shared, &mut mode, &hello);
|
||||
assert_eq!(shared.connected.as_deref().map(|n| n.chars().count()), Some(MAX_NAME));
|
||||
assert!(shared.connected.as_deref().is_some_and(|n| n.starts_with("DevApp")));
|
||||
}
|
||||
assert!(claim.claims(&strings), "the name matches ignoring case");
|
||||
assert!(!claim.claims(&["3Dconnexion".to_string(), "SpaceMouse Compact".to_string()]), "another 3D mouse stays the reader's");
|
||||
}
|
||||
|
||||
/// Waits until `check` holds, or fails after a few seconds.
|
||||
fn wait_for(what: &str, mut check: impl FnMut() -> bool) {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !check() {
|
||||
assert!(Instant::now() < deadline, "timed out waiting for {what}");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_connection_says_hello_reads_the_stream_and_ends_when_the_bridge_is_switched_off() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (mut server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
let mut hello_line = String::new();
|
||||
BufReader::new(server.try_clone().unwrap()).read_line(&mut hello_line).unwrap();
|
||||
let hello: Value = serde_json::from_str(&hello_line).unwrap();
|
||||
assert_eq!((hello["t"].as_str(), hello["app"].as_str()), (Some("hello"), Some("solidcraft")));
|
||||
server
|
||||
.write_all(b"{\"t\":\"hello\",\"server\":\"MAKMOUSE\",\"device\":\"MAKMOUSE\"}\n{\"t\":\"axis\",\"x\":2,\"y\":1}\n{\"t\":\"view\",\"name\":\"top\"}\n")
|
||||
.unwrap();
|
||||
wait_for("the stream", || shared.lock().is_ok_and(|s| s.motion.orbit == [2.0, 1.0] && s.views == ["top"]));
|
||||
assert_eq!(shared.lock().unwrap().connected.as_deref(), Some("MAKMOUSE"));
|
||||
// Switched off: the connection ends and what it had is forgotten.
|
||||
shared.lock().unwrap().wanted = None;
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert!(after.connected.is_none() && after.device.is_none() && after.motion.is_still() && after.views.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_closed_connection_forgets_the_app() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).unwrap();
|
||||
let port = listener.local_addr().unwrap().port();
|
||||
let shared = Arc::new(Mutex::new(Shared { wanted: Some(port), ..Default::default() }));
|
||||
let client = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap();
|
||||
let (server, _) = listener.accept().unwrap();
|
||||
let worker = {
|
||||
let shared = Arc::clone(&shared);
|
||||
std::thread::spawn(move || serve(client, port, &Context::default(), &shared))
|
||||
};
|
||||
wait_for("the connection", || shared.lock().is_ok_and(|s| s.connected.is_some()));
|
||||
drop(server);
|
||||
worker.join().unwrap();
|
||||
let after = shared.lock().unwrap();
|
||||
assert_eq!((after.connected.as_deref(), after.wanted), (None, Some(port)), "still wanted, so the reader connects again");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_frame_hands_the_view_the_motion_and_passes_the_preferences_on() {
|
||||
let mut app = SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default());
|
||||
app.home.open = false;
|
||||
let ctx = Context::default();
|
||||
let _ = ctx.run_ui(egui::RawInput { focused: true, ..Default::default() }, |_| {});
|
||||
let mut bridge = NavBridge { shared: Arc::new(Mutex::new(Shared::default())) };
|
||||
let queue = |bridge: &NavBridge| {
|
||||
let mut shared = bridge.shared.lock().unwrap();
|
||||
shared.connected = Some("MAKMOUSE".into());
|
||||
shared.motion = BridgeMotion { orbit: [10.0, 0.0], ..Default::default() };
|
||||
};
|
||||
// Off (the default): nothing is wanted, nothing reaches the view.
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
assert!(app.viewport.bridge_motion.is_none() && app.space_mouse.bridge.is_none());
|
||||
// On: the port is passed to the reader and the motion to the view, once.
|
||||
app.preferences.nav_bridge_enabled = true;
|
||||
app.preferences.nav_bridge_port = 9123;
|
||||
queue(&bridge);
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, Some(9123));
|
||||
assert_eq!(app.space_mouse.bridge.as_deref(), Some("MAKMOUSE"));
|
||||
assert_eq!(app.viewport.bridge_motion.map(|m| m.orbit), Some([10.0, 0.0]));
|
||||
assert!(bridge.shared.lock().unwrap().motion.is_still(), "taken, not handed over twice");
|
||||
// Port 0 is no port.
|
||||
app.preferences.nav_bridge_port = 0;
|
||||
bridge.tick(&mut app, &ctx);
|
||||
assert_eq!(bridge.shared.lock().unwrap().wanted, None);
|
||||
}
|
||||
}
|
||||
+1565
-87
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,12 @@ const DEV: &str = "/dev";
|
||||
/// a read with nothing to read returns at once and the reader can see whether it should stop.
|
||||
pub const O_NONBLOCK: i32 = 0o4000;
|
||||
|
||||
/// Opens a hidraw node the way the Direct reader reads it: read-only and non-blocking.
|
||||
pub fn open(path: &Path) -> std::io::Result<std::fs::File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
std::fs::OpenOptions::new().read(true).custom_flags(O_NONBLOCK).open(path)
|
||||
}
|
||||
|
||||
/// How long the reader waits when the device has nothing to say, which is also the most a report
|
||||
/// can be delayed.
|
||||
const IDLE_POLL: Duration = Duration::from_millis(5);
|
||||
@@ -76,6 +82,44 @@ fn ids(uevent: &str) -> Option<(u16, u16)> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A real EACCES, as a user without the udev rule gets: a file of the user's own with no permission
|
||||
/// bits. Skipped for root, which can open anything.
|
||||
#[test]
|
||||
fn a_device_without_permission_is_denied_and_the_daemon_takes_over_within_the_backoff() {
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
if std::fs::metadata("/proc/self").is_ok_and(|meta| meta.uid() == 0) {
|
||||
return;
|
||||
}
|
||||
let dir = std::env::temp_dir().join(format!("sc-hidraw-denied-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let node = dir.join("hidraw0");
|
||||
std::fs::write(&node, [0_u8; 4]).unwrap();
|
||||
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||
let denied = open(&node);
|
||||
std::fs::set_permissions(&node, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
assert!(open(&node).is_ok(), "the same node opens once it is readable");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
assert_eq!(denied.err().map(|error| error.kind()), Some(std::io::ErrorKind::PermissionDenied));
|
||||
// The failure is the choice's `since_failed`, with an injected clock: inside the backoff the
|
||||
// daemon is read, whichever driver was asked for; after it the device is tried again.
|
||||
use super::super::{Pick, SpaceMouseDriver, UNREADABLE_RETRY, choose, direct_usable};
|
||||
for wanted in [SpaceMouseDriver::Automatic, SpaceMouseDriver::Spacenavd, SpaceMouseDriver::Direct] {
|
||||
for ago in [Duration::ZERO, Duration::from_secs(29)] {
|
||||
assert_eq!(choose(wanted, true, direct_usable(true, Some(ago), UNREADABLE_RETRY)), Some(Pick::Spacenavd), "{wanted:?} {ago:?}");
|
||||
}
|
||||
}
|
||||
assert_eq!(choose(SpaceMouseDriver::Direct, true, direct_usable(true, Some(UNREADABLE_RETRY), UNREADABLE_RETRY)), Some(Pick::Direct));
|
||||
// And the daemon's own reader delivers over a fake connection.
|
||||
let (mut daemon, client) = std::os::unix::net::UnixStream::pair().unwrap();
|
||||
let mut record = [0_u8; 32];
|
||||
record[4..8].copy_from_slice(&100_i32.to_ne_bytes());
|
||||
std::io::Write::write_all(&mut daemon, &record).unwrap();
|
||||
drop(daemon);
|
||||
let mut seen = 0;
|
||||
super::super::spacenavd::read(client, |_| seen += 1, |_, _| {}, || false, Duration::from_secs(3600));
|
||||
assert_eq!(seen, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_come_through_idle_gaps_until_the_device_ends_or_a_stop() {
|
||||
use crate::space_mouse::fake::Script;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
//! to its clients, so a program that reads the HID device itself bypasses that setup. Over its
|
||||
//! socket the daemon sends fixed 32-byte records: eight native-endian `i32`, the first the kind of
|
||||
//! record (0 for motion, then x y z rx ry rz and the milliseconds since the last record).
|
||||
//! Other records (button presses, from newer daemons also configuration changes) are ignored.
|
||||
//! Button records (1 pressed, 2 released, then the button's number from 0) are read too; other
|
||||
//! records (from newer daemons also configuration changes) are ignored.
|
||||
//!
|
||||
//! The socket is plain file I/O, so no library is needed, and it needs no udev rule: the daemon
|
||||
//! is what has access to the device.
|
||||
@@ -11,6 +12,7 @@
|
||||
use std::io::Read;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use solidcraft_ui_egui::space_mouse::{Axes, FULL_SCALE};
|
||||
|
||||
@@ -54,6 +56,22 @@ fn decode(record: &[u8; RECORD_BYTES]) -> Option<Axes> {
|
||||
})
|
||||
}
|
||||
|
||||
/// The button a button record is about, from 0, and whether it went down; `None` for any other
|
||||
/// record, or a button past the 32 a bitmask holds.
|
||||
fn button(record: &[u8; RECORD_BYTES]) -> Option<(u32, bool)> {
|
||||
let mut data = [0_i32; 2];
|
||||
for (slot, bytes) in data.iter_mut().zip(record.as_chunks::<4>().0) {
|
||||
*slot = i32::from_ne_bytes(*bytes);
|
||||
}
|
||||
let [kind, number] = data;
|
||||
let number = u32::try_from(number).ok().filter(|&n| n < 32)?;
|
||||
match kind {
|
||||
1 => Some((number, true)),
|
||||
2 => Some((number, false)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A daemon value in our raw units. Out-of-range values stay in `i16`: they are input from another
|
||||
/// process.
|
||||
fn unit(value: impl Into<i64>) -> i16 {
|
||||
@@ -61,14 +79,30 @@ fn unit(value: impl Into<i64>) -> i16 {
|
||||
i16::try_from(scaled.clamp(i64::from(i16::MIN), i64::from(i16::MAX))).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Hands every motion record to `publish` until the daemon closes the connection or fails, or
|
||||
/// `stop` says so. A stream with a read timeout (`set_read_timeout`) times out when the daemon is
|
||||
/// quiet, which is no record and no disconnect: `stop` is asked, and the read goes on, with what
|
||||
/// of a record had arrived kept. A partial record at the end is dropped.
|
||||
pub fn read(mut stream: impl Read, mut publish: impl FnMut(Axes), mut stop: impl FnMut() -> bool) {
|
||||
/// Hands every motion record to `publish` and every button record to `pressed` until the daemon
|
||||
/// closes the connection or fails, or `stop` says so. A stream with a read timeout
|
||||
/// (`set_read_timeout`) times out when the daemon is quiet, which is no record and no disconnect:
|
||||
/// `stop` is asked, and the read goes on, with what of a record had arrived kept. `stop` is also asked
|
||||
/// at least every `check_every` while records keep arriving, so a daemon that never pauses for that
|
||||
/// long (a moving cap sends every few milliseconds) does not keep the reader from noticing that the
|
||||
/// device it prefers is back. A partial record at the end is dropped.
|
||||
pub fn read(
|
||||
mut stream: impl Read,
|
||||
mut publish: impl FnMut(Axes),
|
||||
mut pressed: impl FnMut(u32, bool),
|
||||
mut stop: impl FnMut() -> bool,
|
||||
check_every: Duration,
|
||||
) {
|
||||
let mut record = [0_u8; RECORD_BYTES];
|
||||
let mut filled = 0;
|
||||
let mut asked = Instant::now();
|
||||
loop {
|
||||
if asked.elapsed() >= check_every {
|
||||
asked = Instant::now();
|
||||
if stop() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
match stream.read(record.get_mut(filled..).unwrap_or_default()) {
|
||||
Ok(0) => {
|
||||
log::info!("3D mouse: spacenavd connection ended");
|
||||
@@ -80,11 +114,14 @@ pub fn read(mut stream: impl Read, mut publish: impl FnMut(Axes), mut stop: impl
|
||||
filled = 0;
|
||||
if let Some(axes) = decode(&record) {
|
||||
publish(axes);
|
||||
} else if let Some((number, down)) = button(&record) {
|
||||
pressed(number, down);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(error) if matches!(error.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
|
||||
asked = Instant::now();
|
||||
if stop() {
|
||||
return;
|
||||
}
|
||||
@@ -134,6 +171,10 @@ mod tests {
|
||||
fn other_records_and_hostile_values_are_harmless() {
|
||||
// A button record (kind 1, press) and one of an unknown kind are not motion.
|
||||
assert_eq!(decode(&record([1, 0, 1, 0, 0, 0, 0, 0])), None);
|
||||
// Nor are hostile button numbers a button.
|
||||
assert_eq!(button(&record([1, -1, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(button(&record([1, i32::MAX, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(button(&record([7, 3, 0, 0, 0, 0, 0, 0])), None);
|
||||
assert_eq!(decode(&record([7, 500, 500, 500, 500, 500, 500, 0])), None);
|
||||
// Nothing overflows on the extremes of `i32`.
|
||||
let extreme = decode(&record([0, i32::MIN, i32::MIN, i32::MAX, i32::MAX, i32::MIN, i32::MIN, -1]));
|
||||
@@ -147,14 +188,17 @@ mod tests {
|
||||
let (mut daemon, client) = UnixStream::pair().expect("a socket pair");
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend(record([0, 100, 0, 0, 0, 0, 0, 8]));
|
||||
bytes.extend(record([2, 1, 0, 0, 0, 0, 0, 0])); // a button release: skipped
|
||||
bytes.extend(record([1, 4, 0, 0, 0, 0, 0, 0])); // button 4 down
|
||||
bytes.extend(record([2, 4, 0, 0, 0, 0, 0, 0])); // and up
|
||||
bytes.extend(record([1, 40, 0, 0, 0, 0, 0, 0])); // past the 32 a bitmask holds: skipped
|
||||
bytes.extend(record([0, 0, 0, 0, 0, 0, 0, 900]));
|
||||
bytes.extend([1, 2, 3]); // the daemon went away mid-record
|
||||
daemon.write_all(&bytes).expect("written");
|
||||
drop(daemon);
|
||||
let mut seen = Vec::new();
|
||||
read(client, |axes| seen.push(axes.translation), || false);
|
||||
let (mut seen, mut buttons) = (Vec::new(), Vec::new());
|
||||
read(client, |axes| seen.push(axes.translation), |number, down| buttons.push((number, down)), || false, Duration::from_secs(3600));
|
||||
assert_eq!(seen, [[70, 0, 0], [0, 0, 0]]);
|
||||
assert_eq!(buttons, [(4, true), (4, false)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -168,10 +212,12 @@ mod tests {
|
||||
read(
|
||||
script,
|
||||
|axes| seen.push(axes.translation),
|
||||
|_, _| {},
|
||||
|| {
|
||||
asked += 1;
|
||||
false
|
||||
},
|
||||
Duration::from_secs(3600),
|
||||
);
|
||||
assert_eq!(seen, [[70, 0, 0]]);
|
||||
assert_eq!(asked, 4, "each timeout asks whether to stop");
|
||||
@@ -184,10 +230,44 @@ mod tests {
|
||||
let mut script = Script::new();
|
||||
script.data(&bytes).timeout(std::io::ErrorKind::WouldBlock).data(&bytes);
|
||||
let mut seen = 0;
|
||||
read(script, |_| seen += 1, || true);
|
||||
read(script, |_| seen += 1, |_, _| {}, || true, Duration::from_secs(3600));
|
||||
assert_eq!(seen, 1, "the record before the timeout arrived; the one after never was read");
|
||||
}
|
||||
|
||||
/// A daemon that never goes quiet (a moving cap sends every few milliseconds) is still asked whether to
|
||||
/// stop, at the interval given, so the reader can go back to a device that returned.
|
||||
#[test]
|
||||
fn a_busy_daemon_is_asked_whether_to_stop_on_a_timer() {
|
||||
// Ends by itself after half a second, so a read that never asks fails the assertion below
|
||||
// instead of hanging the test.
|
||||
struct Busy(Instant);
|
||||
impl Read for Busy {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.0.elapsed() > Duration::from_millis(500) {
|
||||
return Ok(0);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
let bytes = record([0, 100, 0, 0, 0, 0, 0, 8]);
|
||||
let size = bytes.len().min(buf.len());
|
||||
buf[..size].copy_from_slice(&bytes[..size]);
|
||||
Ok(size)
|
||||
}
|
||||
}
|
||||
let (mut seen, mut asked) = (0, 0);
|
||||
read(
|
||||
Busy(Instant::now()),
|
||||
|_| seen += 1,
|
||||
|_, _| {},
|
||||
|| {
|
||||
asked += 1;
|
||||
asked == 3
|
||||
},
|
||||
Duration::from_millis(10),
|
||||
);
|
||||
assert_eq!(asked, 3, "asked on the timer although no read ever timed out");
|
||||
assert!(seen > 3, "records kept coming meanwhile: {seen}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_daemon_means_no_connection() {
|
||||
let missing = std::env::temp_dir().join(format!("sc-no-spnav-{}.sock", std::process::id()));
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
//! row the next launch starts in safe mode by itself.
|
||||
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
|
||||
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
|
||||
//! - **No usable graphics device**: one retry on the software renderer
|
||||
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
|
||||
//! - **A renderer that cannot start**: on Windows, try the remaining graphics APIs in order,
|
||||
//! then the software renderer (`SOLIDCRAFT_RENDERER=cpu`). An explicit `WGPU_BACKEND` fails
|
||||
//! cleanly without switching. Exhausted retries show a message box with the error.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -21,6 +22,25 @@ pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
|
||||
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
|
||||
const MARKER: &str = "starting";
|
||||
|
||||
/// Explicit application-settings isolation for side-by-side builds and native UI
|
||||
/// acceptance. A supplied invalid path refuses startup rather than falling back
|
||||
/// to the user's ordinary settings. No directory is read or created here.
|
||||
pub fn settings_directory(override_path: Option<&std::ffi::OsStr>, default: Option<PathBuf>) -> Result<Option<PathBuf>, String> {
|
||||
match override_path {
|
||||
None => Ok(default),
|
||||
Some(raw) => {
|
||||
let path = PathBuf::from(raw);
|
||||
if raw.is_empty() || !path.is_absolute() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR must be a nonempty absolute directory path".into());
|
||||
}
|
||||
if path.exists() && !path.is_dir() {
|
||||
return Err("SOLIDCRAFT_SETTINGS_DIR names a file, not a directory".into());
|
||||
}
|
||||
Ok(Some(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an environment value switches something on.
|
||||
pub fn env_on(v: Option<&str>) -> bool {
|
||||
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
|
||||
@@ -110,6 +130,48 @@ pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&efr
|
||||
})
|
||||
}
|
||||
|
||||
/// Adapter properties used to choose a card without opening a device.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AdapterChoice {
|
||||
pub name: String,
|
||||
pub compatible: bool,
|
||||
}
|
||||
|
||||
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
|
||||
/// environment override follows wgpu's case-insensitive substring convention.
|
||||
/// No match leaves selection to the normal wgpu request.
|
||||
pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, preference: Option<&str>) -> Option<usize> {
|
||||
// wgpu's environment helper panics on a missing name; an override must remain recoverable.
|
||||
let compatible = || adapters.iter().enumerate().filter(|(_, a)| a.compatible);
|
||||
if let Some(name) = environment.filter(|s| !s.is_empty()) {
|
||||
let name = name.to_lowercase();
|
||||
if let Some((index, _)) = compatible().find(|(_, a)| a.name.to_lowercase().contains(&name)) {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
if let Some(name) = preference.filter(|s| !s.is_empty())
|
||||
&& let Some((index, _)) = compatible().find(|(_, a)| a.name == name)
|
||||
{
|
||||
return Some(index);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
|
||||
pub fn configure_adapter(setup: &mut eframe::egui_wgpu::WgpuSetupCreateNew, cpu: bool, environment: Option<String>, preference: Option<String>) {
|
||||
if cpu {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(|adapters, surface| pick_cpu_adapter(adapters, surface).map(Some)));
|
||||
} else if environment.as_deref().is_some_and(|s| !s.is_empty()) || preference.as_deref().is_some_and(|s| !s.is_empty()) {
|
||||
setup.native_adapter_selector = Some(std::sync::Arc::new(move |adapters, surface| {
|
||||
let choices: Vec<_> = adapters
|
||||
.iter()
|
||||
.map(|a| AdapterChoice { name: a.get_info().name, compatible: surface.is_none_or(|s| a.is_surface_supported(s)) })
|
||||
.collect();
|
||||
Ok(choose_adapter(&choices, environment.as_deref(), preference.as_deref()).and_then(|i| adapters.get(i)).cloned())
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// `WGPU_BACKEND`: when set, wgpu picks the backends from it and the order below is not used.
|
||||
pub const WGPU_BACKEND_ENV: &str = "WGPU_BACKEND";
|
||||
|
||||
@@ -157,6 +219,34 @@ pub fn choose_backends(
|
||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||
}
|
||||
|
||||
/// Internal, process-local retry choice; never written to the graphics preference.
|
||||
pub const RETRY_BACKEND_ENV: &str = "SOLIDCRAFT_STARTUP_BACKEND";
|
||||
/// Why this launch is using a fallback, carried to the new process for status and Preferences.
|
||||
pub const FALLBACK_NOTE_ENV: &str = "SOLIDCRAFT_GRAPHICS_FALLBACK";
|
||||
|
||||
/// A handled startup failure tries each remaining Windows API once, then software rendering.
|
||||
/// An explicit environment choice is final, including when software rendering was requested.
|
||||
pub fn fallback_backend(windows: bool, explicit: bool, cpu: bool, failed: eframe::wgpu::Backends) -> Option<&'static str> {
|
||||
if explicit || cpu {
|
||||
return None;
|
||||
}
|
||||
if windows {
|
||||
let last = WINDOWS_ORDER.iter().rposition(|backend| failed.intersects(*backend));
|
||||
if let Some(next) = last.and_then(|i| WINDOWS_ORDER.get(i + 1)) {
|
||||
return Some(if *next == eframe::wgpu::Backends::VULKAN { "vulkan" } else { "gl" });
|
||||
}
|
||||
}
|
||||
Some("cpu")
|
||||
}
|
||||
|
||||
/// Keep driver diagnostics on one bounded line, including across the finite retry chain.
|
||||
pub fn fallback_note(previous: &str, failed: eframe::wgpu::Backends, next: &str, error: &str) -> String {
|
||||
let previous: String = previous.chars().take(2048).collect();
|
||||
let error: String = error.chars().take(1024).collect();
|
||||
let line = format!("{previous} Graphics fallback: {failed:?} failed ({error}); trying {next}.");
|
||||
line.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(4096).collect()
|
||||
}
|
||||
|
||||
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
|
||||
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||
|
||||
@@ -189,6 +279,29 @@ pub fn has_hardware_adapter(backends: eframe::wgpu::Backends) -> bool {
|
||||
adapters.iter().any(|a| a.get_info().device_type != eframe::wgpu::DeviceType::Cpu)
|
||||
}
|
||||
|
||||
/// The environment variable that switches wgpu's indirect-call validation on or off.
|
||||
pub const INDIRECT_VALIDATION_ENV: &str = "WGPU_VALIDATION_INDIRECT_CALL";
|
||||
|
||||
/// The instance flags for the window. wgpu's indirect-call validation compiles a compute shader
|
||||
/// when the device is created, and on Intel Macs (macOS 12 with Intel Iris graphics, #3024,
|
||||
/// #3080, #4605) the Metal compiler refuses it: the device is lost and the window never opens.
|
||||
/// So on an Intel Mac the validation is left off, unless the user turned it on or off with
|
||||
/// `WGPU_VALIDATION_INDIRECT_CALL` themselves.
|
||||
pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool, env: Option<&str>) -> eframe::wgpu::InstanceFlags {
|
||||
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
|
||||
}
|
||||
|
||||
/// Windows with OpenGL: the window keeps the system title bar (#2063). The window without OS
|
||||
/// decorations keeps its caption style while having no frame, and some OpenGL drivers (older
|
||||
/// Intel HD/UHD) then show the picture a caption's height lower than the window, so it no longer
|
||||
/// lines up with the pointer. Direct3D 12 and Vulkan draw it in place and keep the application bar
|
||||
/// as the title bar. `backends` are the ones wgpu starts: OpenGL alone is decided before the
|
||||
/// window opens.
|
||||
pub fn system_title_bar(windows: bool, backends: eframe::wgpu::Backends) -> bool {
|
||||
use eframe::wgpu::Backends;
|
||||
windows && backends.contains(Backends::GL) && !backends.intersects(Backends::DX12 | Backends::VULKAN)
|
||||
}
|
||||
|
||||
/// The message shown when no window could be opened at all.
|
||||
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
|
||||
@@ -207,6 +320,48 @@ pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn cards() -> Vec<AdapterChoice> {
|
||||
["Integrated", "Discrete small", "Discrete large"].into_iter().map(|name| AdapterChoice { name: name.into(), compatible: true }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_names_environment_override_and_missing_card_fallback() {
|
||||
let cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some("LARGE"), Some("Discrete small")), Some(2));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("removed")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("missing"), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "saved names are exact");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_leaves_selection_to_wgpu() {
|
||||
let mut cards = cards();
|
||||
assert_eq!(choose_adapter(&cards, None, None), None);
|
||||
assert_eq!(choose_adapter(&cards, Some(""), Some("")), None);
|
||||
cards[2].compatible = false;
|
||||
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), None);
|
||||
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
|
||||
assert_eq!(choose_adapter(&[], None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_preserves_the_previous_request_configuration() {
|
||||
use eframe::egui_wgpu::WgpuSetupCreateNew;
|
||||
let previous = WgpuSetupCreateNew::without_display_handle();
|
||||
for preference in [None, Some(String::new())] {
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, None, preference);
|
||||
assert!(setup.native_adapter_selector.is_none(), "Automatic must use the normal wgpu request");
|
||||
assert_eq!(setup.power_preference, previous.power_preference);
|
||||
assert_eq!(setup.instance_descriptor.backends, previous.instance_descriptor.backends);
|
||||
}
|
||||
let mut setup = WgpuSetupCreateNew::without_display_handle();
|
||||
configure_adapter(&mut setup, false, Some("missing".into()), Some("removed".into()));
|
||||
assert!(setup.native_adapter_selector.unwrap()(&[], None).unwrap().is_none(), "missing names must use the normal request too");
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
@@ -214,6 +369,32 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_settings_are_explicit_absolute_and_never_fall_back() {
|
||||
let default = std::env::temp_dir().join("ordinary-settings");
|
||||
assert_eq!(settings_directory(None, Some(default.clone())).unwrap(), Some(default.clone()));
|
||||
let selected = std::env::temp_dir().join("solidcraft-isolated-settings-fixture");
|
||||
assert_eq!(settings_directory(Some(selected.as_os_str()), Some(default.clone())).unwrap(), Some(selected));
|
||||
for raw in ["", "relative/settings", "../ordinary"] {
|
||||
assert!(settings_directory(Some(std::ffi::OsStr::new(raw)), Some(default.clone())).is_err());
|
||||
}
|
||||
let file = std::env::current_exe().unwrap();
|
||||
assert!(settings_directory(Some(file.as_os_str()), Some(default)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intel_macs_start_without_indirect_call_validation() {
|
||||
use eframe::wgpu::InstanceFlags as F;
|
||||
let on = F::VALIDATION | F::VALIDATION_INDIRECT_CALL;
|
||||
assert_eq!(window_instance_flags(on, true, None), F::VALIDATION);
|
||||
assert_eq!(window_instance_flags(on, true, Some(" ")), F::VALIDATION);
|
||||
// The user's own choice stands, on or off.
|
||||
assert_eq!(window_instance_flags(on, true, Some("1")), on);
|
||||
assert_eq!(window_instance_flags(F::VALIDATION, true, Some("0")), F::VALIDATION);
|
||||
// Every other machine keeps what it had.
|
||||
assert_eq!(window_instance_flags(on, false, None), on);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switches_read_from_the_environment() {
|
||||
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
|
||||
@@ -290,6 +471,54 @@ mod tests {
|
||||
assert_eq!(choose_backends(true, false, false, Some(""), probe(&[Backends::GL])), Some(Backends::GL));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_windows_backends_fall_back_in_order_and_stop_after_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12), Some("vulkan"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(true, false, false, B::GL), Some("cpu"));
|
||||
assert_eq!(fallback_backend(true, false, true, B::DX12), None);
|
||||
assert_eq!(fallback_backend(true, false, false, B::DX12 | B::VULKAN), Some("gl"));
|
||||
assert_eq!(fallback_backend(false, false, false, B::VULKAN), Some("cpu"));
|
||||
let note = fallback_note("", B::DX12, "vulkan", "egui_pipeline\n0x80070057");
|
||||
assert!(note.contains("egui_pipeline 0x80070057") && note.contains("vulkan") && !note.contains('\n'));
|
||||
let long = fallback_note(&"x".repeat(5000), B::VULKAN, "gl", &"y".repeat(5000));
|
||||
assert!(long.chars().count() <= 4096 && long.ends_with("trying gl."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_backend_failure_never_switches_api_or_retries_software() {
|
||||
use eframe::wgpu::Backends as B;
|
||||
for windows in [false, true] {
|
||||
for cpu in [false, true] {
|
||||
for backend in [B::DX12, B::VULKAN, B::GL, B::all()] {
|
||||
assert_eq!(fallback_backend(windows, true, cpu, backend), None);
|
||||
let mut probed = false;
|
||||
assert_eq!(
|
||||
choose_backends(windows, true, cpu, Some("vulkan"), |_| {
|
||||
probed = true;
|
||||
false
|
||||
}),
|
||||
None
|
||||
);
|
||||
assert!(!probed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_opengl_on_windows_gets_the_system_title_bar() {
|
||||
use eframe::wgpu::Backends;
|
||||
assert!(system_title_bar(true, Backends::GL));
|
||||
for b in [Backends::DX12, Backends::VULKAN, Backends::DX12 | Backends::GL, Backends::VULKAN | Backends::GL, Backends::all()] {
|
||||
assert!(!system_title_bar(true, b), "{b:?}");
|
||||
}
|
||||
for b in [Backends::GL, Backends::VULKAN, Backends::METAL] {
|
||||
assert!(!system_title_bar(false, b), "{b:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wgpu_backend_the_preference_and_the_software_renderer_override_the_order() {
|
||||
use eframe::wgpu::Backends;
|
||||
|
||||
@@ -25,6 +25,7 @@ assets/
|
||||
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
|
||||
platforms/macos/dmg/ # disk image window: layout, dmgbuild settings, background art
|
||||
archive/icons/macos-v1/
|
||||
source/icon.vectorcraft # earlier editable work, retained
|
||||
# redundant historical exports are retired
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//! Original illustrative opaque finishes; not measured optical or engineering data.
|
||||
//! (name, sRGB color, metal fraction, perceptual roughness). IOR is an illustrative 1.5.
|
||||
//! Separate names preserve the scalar library's appearance and saved assignments.
|
||||
pub const OPAQUE_FINISHES: [(&str, [u8; 3], f64, f64); 6] = [
|
||||
("Steel - Satin PBR", [158, 164, 172], 1.0, 0.4),
|
||||
("Aluminum - Satin PBR", [204, 208, 214], 1.0, 0.3),
|
||||
("Copper - Clean PBR", [204, 122, 80], 1.0, 0.25),
|
||||
("ABS - Molded PBR", [222, 222, 216], 0.0, 0.4),
|
||||
("PLA - Smooth PBR", [110, 160, 222], 0.0, 0.35),
|
||||
("Wood - Oak Uniform PBR Preview", [190, 146, 92], 0.0, 0.65),
|
||||
];
|
||||
@@ -0,0 +1,292 @@
|
||||
//! Original starter visual data, MIT OR Apache-2.0. No external material library.
|
||||
use crate::appearance_pack::{Category, GlassBoundary, GlassRecipe, OpaqueRecipe, TextureAxis, TextureRecipe, VisualPreset};
|
||||
|
||||
const WOOD: &str = "Uniform opaque preview only; grain resources/mapping are not applied";
|
||||
const SOLAR: &str = "Uniform opaque preview only; no cell grid, busbars, electricity or efficiency claim";
|
||||
const REGOLITH: &str = "Uniform opaque preview only; no granular displacement or measured lunar optical data";
|
||||
const OPAQUE: &str = "Illustrative opaque isotropic PBR; no measured alloy, polymer or engineering properties";
|
||||
|
||||
const fn look(color: [u8; 3], metallic: f64, roughness: f64, dielectric_ior: f64) -> OpaqueRecipe {
|
||||
OpaqueRecipe { color, metallic, roughness, dielectric_ior }
|
||||
}
|
||||
const fn maps(id: &'static str, repeat_mm: [f64; 2], grain: bool) -> Option<TextureRecipe> {
|
||||
Some(TextureRecipe { id, repeat_mm, grain_axis: if grain { Some(TextureAxis::V) } else { None } })
|
||||
}
|
||||
const fn glass(transmission_linear_rgb: [f64; 3], ior: f64, reference_path_mm: f64, boundary: GlassBoundary) -> GlassRecipe {
|
||||
GlassRecipe { transmission_linear_rgb, dielectric_ior: ior, reference_path_mm, boundary }
|
||||
}
|
||||
|
||||
pub const STARTER_PACK: [VisualPreset; 40] = [
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.oak-honey",
|
||||
"Wood - Honey Oak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([190, 145, 88], 0.0, 0.64, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.oak-honey.maps-v1", [120.0, 240.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.oak-smoked",
|
||||
"Wood - Smoked Oak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([91, 72, 55], 0.0, 0.68, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.oak-smoked.maps-v1", [120.0, 240.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.walnut-dark",
|
||||
"Wood - Dark Walnut Uniform Preview",
|
||||
Category::Wood,
|
||||
look([99, 66, 43], 0.0, 0.60, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.walnut-dark.maps-v1", [100.0, 220.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.maple-light",
|
||||
"Wood - Light Maple Uniform Preview",
|
||||
Category::Wood,
|
||||
look([221, 192, 141], 0.0, 0.57, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.maple-light.maps-v1", [100.0, 200.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.cherry-warm",
|
||||
"Wood - Warm Cherry Uniform Preview",
|
||||
Category::Wood,
|
||||
look([164, 99, 65], 0.0, 0.58, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.cherry-warm.maps-v1", [110.0, 220.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.ash-pale",
|
||||
"Wood - Pale Ash Uniform Preview",
|
||||
Category::Wood,
|
||||
look([209, 186, 146], 0.0, 0.70, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.ash-pale.maps-v1", [120.0, 260.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.birch-cream",
|
||||
"Wood - Cream Birch Uniform Preview",
|
||||
Category::Wood,
|
||||
look([228, 206, 163], 0.0, 0.63, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.birch-cream.maps-v1", [100.0, 200.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.teak-golden",
|
||||
"Wood - Golden Teak Uniform Preview",
|
||||
Category::Wood,
|
||||
look([167, 122, 66], 0.0, 0.61, 1.5),
|
||||
WOOD,
|
||||
maps("starter.wood.teak-golden.maps-v1", [110.0, 250.0], true),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.carbon-steel-polished",
|
||||
"Metal - Polished Carbon Steel",
|
||||
Category::Metal,
|
||||
look([161, 166, 173], 1.0, 0.16, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.stainless-soft-satin",
|
||||
"Metal - Soft Satin Stainless",
|
||||
Category::Metal,
|
||||
look([187, 193, 201], 1.0, 0.36, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.aluminum-bead-blast",
|
||||
"Metal - Bead Blast Aluminum",
|
||||
Category::Metal,
|
||||
look([195, 201, 210], 1.0, 0.58, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.aluminum-silver-polish",
|
||||
"Metal - Silver Polish Aluminum",
|
||||
Category::Metal,
|
||||
look([218, 224, 232], 1.0, 0.13, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.copper-aged-warm",
|
||||
"Metal - Warm Aged Copper",
|
||||
Category::Metal,
|
||||
look([171, 95, 57], 1.0, 0.48, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.brass-champagne",
|
||||
"Metal - Champagne Satin Brass",
|
||||
Category::Metal,
|
||||
look([205, 173, 102], 1.0, 0.32, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.bronze-dark-cast",
|
||||
"Metal - Dark Cast Bronze",
|
||||
Category::Metal,
|
||||
look([133, 95, 58], 1.0, 0.66, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.titanium-matte",
|
||||
"Metal - Matte Gray Titanium",
|
||||
Category::Metal,
|
||||
look([147, 149, 157], 1.0, 0.53, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.gold-warm",
|
||||
"Metal - Warm Reflective Gold",
|
||||
Category::Metal,
|
||||
look([235, 190, 82], 1.0, 0.18, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.metal.silver-cool",
|
||||
"Metal - Cool Reflective Silver",
|
||||
Category::Metal,
|
||||
look([225, 230, 237], 1.0, 0.12, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.abs-charcoal",
|
||||
"Plastic - Charcoal ABS",
|
||||
Category::Plastic,
|
||||
look([43, 45, 48], 0.0, 0.44, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.abs-warm-white",
|
||||
"Plastic - Warm White ABS",
|
||||
Category::Plastic,
|
||||
look([232, 229, 220], 0.0, 0.40, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque("starter.plastic.pla-coral", "Plastic - Coral PLA", Category::Plastic, look([218, 99, 83], 0.0, 0.43, 1.5), OPAQUE, None),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.petg-cyan-opaque",
|
||||
"Plastic - Opaque Cyan PETG",
|
||||
Category::Plastic,
|
||||
look([36, 148, 167], 0.0, 0.29, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.nylon-ivory",
|
||||
"Plastic - Ivory Nylon",
|
||||
Category::Plastic,
|
||||
look([220, 213, 190], 0.0, 0.62, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.pc-graphite-opaque",
|
||||
"Plastic - Opaque Graphite PC",
|
||||
Category::Plastic,
|
||||
look([70, 76, 84], 0.0, 0.27, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.hdpe-natural-opaque",
|
||||
"Plastic - Opaque Natural HDPE",
|
||||
Category::Plastic,
|
||||
look([221, 222, 207], 0.0, 0.48, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque("starter.plastic.pom-black", "Plastic - Black POM", Category::Plastic, look([29, 31, 35], 0.0, 0.32, 1.5), OPAQUE, None),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.tpu-matte-black",
|
||||
"Plastic - Matte Black TPU",
|
||||
Category::Plastic,
|
||||
look([31, 30, 32], 0.0, 0.83, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.plastic.peek-amber-opaque",
|
||||
"Plastic - Opaque Amber PEEK",
|
||||
Category::Plastic,
|
||||
look([157, 119, 62], 0.0, 0.51, 1.5),
|
||||
OPAQUE,
|
||||
None,
|
||||
),
|
||||
VisualPreset::glass(
|
||||
"starter.glass.clear-soda-lime",
|
||||
"Glass Recipe - Clear Soda Lime",
|
||||
glass([0.92, 0.98, 0.96], 1.52, 3.0, GlassBoundary::Closed),
|
||||
),
|
||||
VisualPreset::glass("starter.glass.low-iron-clear", "Glass Recipe - Low Iron Clear", glass([0.98, 0.99, 1.0], 1.50, 4.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.smoky-neutral", "Glass Recipe - Smoky Neutral", glass([0.35, 0.38, 0.42], 1.52, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.amber-tint", "Glass Recipe - Amber Tint", glass([0.84, 0.38, 0.08], 1.51, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass("starter.glass.cobalt-blue", "Glass Recipe - Cobalt Blue", glass([0.06, 0.14, 0.76], 1.53, 3.0, GlassBoundary::Closed)),
|
||||
VisualPreset::glass(
|
||||
"starter.glass.thin-clear",
|
||||
"Glass Recipe - Thin Clear Sheet",
|
||||
glass([0.95, 0.98, 0.99], 1.50, 2.0, GlassBoundary::Thin { thickness_mm: 2.0 }),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.solar.mono-blue",
|
||||
"Solar Panel - Mono Blue Uniform Preview",
|
||||
Category::Solar,
|
||||
look([20, 41, 85], 0.0, 0.20, 1.5),
|
||||
SOLAR,
|
||||
maps("starter.solar.mono-blue.maps-v1", [160.0, 160.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.solar.thin-film-dark",
|
||||
"Solar Panel - Dark Thin Film Uniform Preview",
|
||||
Category::Solar,
|
||||
look([28, 33, 42], 0.0, 0.28, 1.5),
|
||||
SOLAR,
|
||||
maps("starter.solar.thin-film-dark.maps-v1", [100.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.mare-charcoal",
|
||||
"Regolith - Mare Charcoal Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([56, 56, 55], 0.0, 0.97, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.mare-charcoal.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.highland-pale",
|
||||
"Regolith - Highland Pale Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([116, 113, 106], 0.0, 0.93, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.highland-pale.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.dust-neutral",
|
||||
"Regolith - Neutral Dust Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([86, 85, 81], 0.0, 1.0, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.dust-neutral.maps-v1", [150.0, 150.0], false),
|
||||
),
|
||||
VisualPreset::opaque(
|
||||
"starter.regolith.agglutinate-dark",
|
||||
"Regolith - Dark Agglutinate Uniform Preview",
|
||||
Category::Regolith,
|
||||
look([66, 63, 60], 0.0, 0.89, 1.5),
|
||||
REGOLITH,
|
||||
maps("starter.regolith.agglutinate-dark.maps-v1", [200.0, 200.0], false),
|
||||
),
|
||||
];
|
||||
@@ -78,7 +78,7 @@ Its projected circle uses the same axes; do not arbitrarily flatten the ellipse.
|
||||
|
||||
Only use the stable `source` identifiers below in model SVGs. The renderer maps roles to
|
||||
light/dark theme values. `currentColor` is the background line-art role, not a way
|
||||
to add arbitrary colours. All values and accents come from `design/palette.toml`.
|
||||
to add arbitrary colours. Default display values and semantic accents come from `design/palette.toml`. Theme files may override those display roles and ramps through `icon_palette`; source identifiers and accent assignments stay unchanged.
|
||||
|
||||
| Role | Author helper | Light value | Use |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -96,9 +96,9 @@ The source hexes identify paint roles, not the displayed colour. **Never recolou
|
||||
SVG to apply a workspace accent.** Keep these identifiers and every finished shape
|
||||
unchanged; the runtime and review page resolve them through `design/palette.toml`.
|
||||
|
||||
That file is the single colour authority: edit `light`/`dark` in the relevant
|
||||
That file is the default colour authority: edit `light`/`dark` in the relevant
|
||||
`accents` ramp; `active-3` is its primary stroke and `active-1/2` are shaded faces.
|
||||
Do not edit a role's `source` identifier. `variants` holds colour-vision adjustments.
|
||||
Do not edit a role's `source` identifier. Theme `icon_palette` overrides affect display colours only; colour-vision variants still take precedence. `variants` holds colour-vision adjustments.
|
||||
A role with `use = "active-3"` follows that stroke automatically.
|
||||
|
||||
| Meaning | Accent | Assignment |
|
||||
|
||||
@@ -29,6 +29,7 @@ ground = "c_fix"
|
||||
|
||||
# Same finished drawing, different semantic operation. Colours live only in palette.toml.
|
||||
sketch_break = { master = "break", accent = "sketch" }
|
||||
sketch_cancel = { master = "close", accent = "sketch" }
|
||||
sketch_dxf = { master = "dxf", accent = "sketch" }
|
||||
sketch_move = { master = "move", accent = "sketch" }
|
||||
sketch_scale = { master = "scale", accent = "sketch" }
|
||||
|
||||
@@ -48,6 +48,38 @@ source = "#236ead"
|
||||
light = "#236ead"
|
||||
dark = "#2976b8"
|
||||
|
||||
# Simultaneous component colours for inspection icons; existing workspace ramps stay unchanged.
|
||||
|
||||
[roles.component-orange-1]
|
||||
source = "#f1d2af"
|
||||
light = "#f1d2af"
|
||||
dark = "#ba8f6a"
|
||||
|
||||
[roles.component-orange-2]
|
||||
source = "#c06f00"
|
||||
light = "#c06f00"
|
||||
dark = "#b86f04"
|
||||
|
||||
[roles.component-orange-3]
|
||||
source = "#a45000"
|
||||
light = "#a45000"
|
||||
dark = "#95682d"
|
||||
|
||||
[roles.component-yellow-1]
|
||||
source = "#e5d99b"
|
||||
light = "#e5d99b"
|
||||
dark = "#a9985f"
|
||||
|
||||
[roles.component-yellow-2]
|
||||
source = "#a28200"
|
||||
light = "#a28200"
|
||||
dark = "#9a8100"
|
||||
|
||||
[roles.component-yellow-3]
|
||||
source = "#826700"
|
||||
light = "#826700"
|
||||
dark = "#857100"
|
||||
|
||||
[strokes] # on the 64 grid
|
||||
geometry = 2.55
|
||||
action = 4.25
|
||||
|
||||
@@ -57,6 +57,8 @@ circle_tt false
|
||||
circle_ttt false
|
||||
close false
|
||||
coil false
|
||||
colored_components false
|
||||
colored_components_off false
|
||||
combine false
|
||||
compare false
|
||||
component false
|
||||
@@ -104,6 +106,7 @@ flange false
|
||||
flat false
|
||||
fold false
|
||||
folder false
|
||||
frame_member false
|
||||
gear false
|
||||
graph false
|
||||
ground false
|
||||
@@ -147,6 +150,7 @@ origin false
|
||||
pan false
|
||||
params false
|
||||
paste false
|
||||
path_flange false
|
||||
pattern_circ false
|
||||
pattern_path false
|
||||
pattern_rect false
|
||||
@@ -211,6 +215,7 @@ shield false
|
||||
silhouette false
|
||||
sketch false
|
||||
sketch_break false
|
||||
sketch_cancel false
|
||||
sketch_chamfer false
|
||||
sketch_chamfer_angle false
|
||||
sketch_chamfer_equal false
|
||||
@@ -242,6 +247,7 @@ status_error false
|
||||
status_recovery false
|
||||
style false
|
||||
suppress false
|
||||
surface false
|
||||
surface_blend false
|
||||
surface_boundary false
|
||||
surface_extend false
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Colored components</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#c06f00"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#a45000"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#f1d2af"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#a28200"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#826700"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#e5d99b"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Disable component colors</title>
|
||||
<path d="M9.235 17.706L15.059 20.882L15.059 48.941L9.235 44.926Z" fill="#328bd3"/>
|
||||
<path d="M35.176 9.235L15.059 20.882L15.059 48.941L36.765 35.139Z" fill="#236ead"/>
|
||||
<path d="M29.046 5L35.176 8.176L15.196 19.718L9.066 16.541Z" fill="#b9ddf8"/>
|
||||
<path d="M29.046 5.529L35.176 8.706L35.176 36.235L15.059 49.471L9.235 45.765L9.066 17.071Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.066 17.6L15.059 20.882L34.647 9.235" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.059 20.882L15.059 47.067" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L44.949 51.122L32.222 43.774Z" fill="#328bd3"/>
|
||||
<path d="M54.934 20.173L44.949 25.944L44.949 51.122L54.934 45.352Z" fill="#236ead"/>
|
||||
<path d="M42.059 12.412L54.934 20.173L44.949 25.944L32 17.706Z" fill="#b9ddf8"/>
|
||||
<path d="M41.159 11.766L54.934 20.173L54.934 45.352L44.949 51.122L31.164 42.715L31.164 17.536Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M32.222 18.595L44.949 25.944L54.934 20.173" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44.949 25.944L44.949 51.122" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L31.015 59L18.299 51.652Z" fill="#328bd3"/>
|
||||
<path d="M41.921 40.121L31.015 46.411L31.015 59L41.921 52.711Z" fill="#236ead"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L31.015 46.411L18.299 39.062Z" fill="#b9ddf8"/>
|
||||
<path d="M29.194 32.773L41.921 40.121L41.921 52.711L31.015 59L18.299 51.652L18.299 39.062Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M18.299 39.062L31.015 46.411L41.921 40.121" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.015 46.411L31.015 59" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Frame Member</title>
|
||||
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.25 18.86L17.63 23.7" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="9.25" cy="18.86" r="3" fill="#236ead"/>
|
||||
<path d="M11.88 20.39L40.62 36.98L40.62 50.25L11.88 33.66Z" fill="#328bd3"/>
|
||||
<path d="M23.38 13.75L52.12 30.34L40.62 36.98L11.88 20.39Z" fill="#b9ddf8"/>
|
||||
<path d="M52.12 30.34L40.62 36.98L40.62 50.25L52.12 43.61ZM49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#8e9faf" fill-rule="evenodd"/>
|
||||
<defs><clipPath id="fm_hole"><path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none"/></clipPath></defs>
|
||||
<g clip-path="url(#fm_hole)">
|
||||
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="#236ead"/>
|
||||
<path d="M49.8 34.35L49.8 42.28L42.86 38.27L42.86 30.34Z" fill="#328bd3"/>
|
||||
<path d="M49.8 42.28L42.94 46.24L35.99 42.23L42.86 38.27Z" fill="#b9ddf8"/>
|
||||
</g>
|
||||
<path d="M49.8 34.35L42.94 38.31L42.94 46.24L49.8 42.28Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23.38 13.75L52.12 30.34L52.12 43.61L40.62 50.25L11.88 33.66L11.88 20.39Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M11.88 20.39L40.62 36.98L52.12 30.34" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M40.62 36.98L40.62 50.25" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#ffffff" stroke-width="4.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M41.74 37.62L54.75 45.14" fill="none" stroke="#236ead" stroke-width="2.2" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="54.75" cy="45.14" r="3" fill="#236ead"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Path Flange</title>
|
||||
<path d="M9.5 44L23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#34465b" stroke-width="11.4" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M9.5 44L23.5 44" fill="none" stroke="#b8c3cf" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M23.5 44A6 6 0 0 0 29.5 38L29.5 26A6 6 0 0 1 35.5 20L43.5 20A6 6 0 0 1 47.74 21.76L54.81 28.83" fill="none" stroke="#328bd3" stroke-width="6.3" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 638 B |
@@ -1,4 +1,4 @@
|
||||
//! Constraint, assembly snap and dimension marker artwork.
|
||||
//! Constraint, assembly snap, hole placement and dimension marker artwork.
|
||||
use crate::dialogs_assembly::SnapKind;
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, Pos2, Stroke, vec2};
|
||||
@@ -57,6 +57,39 @@ pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: Sn
|
||||
painter.galley(at, g, t.text);
|
||||
}
|
||||
}
|
||||
/// A place a hole can snap to, before the cursor reaches it: a small hollow mark of its kind
|
||||
/// (a square for a face's centre, a ring for a round edge's centre).
|
||||
pub(crate) fn snap_hint(painter: &egui::Painter, c: Pos2, kind: SnapKind, col: Color32) {
|
||||
let ic = Tokens::get().icons;
|
||||
for stroke in [Stroke::new(3.5, ic.marker_halo), Stroke::new(1.4, col)] {
|
||||
match kind {
|
||||
SnapKind::FaceCenter => {
|
||||
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(8.0, 8.0)), 1.0, stroke, egui::StrokeKind::Middle);
|
||||
}
|
||||
SnapKind::CircleCenter => {
|
||||
painter.circle_stroke(c, 4.5, stroke);
|
||||
}
|
||||
SnapKind::EdgeMid | SnapKind::Vertex => {
|
||||
let r = 5.0;
|
||||
let diamond = vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)];
|
||||
painter.add(egui::Shape::convex_polygon(diamond, Color32::TRANSPARENT, stroke));
|
||||
}
|
||||
}
|
||||
}
|
||||
painter.circle_filled(c, 1.3, col);
|
||||
}
|
||||
|
||||
/// A placed hole's drag handle: a ring round the hole's centre with a cross through it, filled
|
||||
/// while it is hovered or dragged.
|
||||
pub(crate) fn hole_handle(painter: &egui::Painter, c: Pos2, col: Color32, hot: bool) {
|
||||
let ic = Tokens::get().icons;
|
||||
let r = 6.0;
|
||||
painter.circle_filled(c, r + 3.5, ic.marker_halo);
|
||||
painter.circle(c, r, if hot { col } else { col.gamma_multiply(0.35) }, Stroke::new(1.6, col));
|
||||
let ink = Stroke::new(1.3, if hot { ic.highlight } else { col });
|
||||
painter.line_segment([c - vec2(r + 3.0, 0.0), c + vec2(r + 3.0, 0.0)], ink);
|
||||
painter.line_segment([c - vec2(0.0, r + 3.0), c + vec2(0.0, r + 3.0)], ink);
|
||||
}
|
||||
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
"Coincident" => "c_coincident",
|
||||
@@ -75,8 +108,8 @@ pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
|
||||
}
|
||||
pub(crate) fn glyph_letter(name: &str) -> &'static str {
|
||||
match name {
|
||||
"Horizontal" => "H",
|
||||
"Vertical" => "V",
|
||||
"Horizontal" => "-",
|
||||
"Vertical" => "|",
|
||||
_ => "?",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +152,11 @@ pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, h
|
||||
pub(crate) const AXIS_RADIUS: f32 = 15.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 {
|
||||
t.triad[0]
|
||||
t.axis_x
|
||||
} else if d.y != 0.0 {
|
||||
t.triad[1]
|
||||
t.axis_y
|
||||
} else {
|
||||
t.triad[2]
|
||||
t.axis_z
|
||||
};
|
||||
let positive = !name.starts_with('-');
|
||||
let light = t.panel.r() > 128;
|
||||
|
||||
+290
-56
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Vybrat…",
|
||||
"Circular": "Kruhově",
|
||||
"Circumference": "Obvod",
|
||||
"Clockwise": "Ve směru hodinových ručiček",
|
||||
"Close draft": "Zavřít koncept",
|
||||
"Collisions": "Kolize",
|
||||
"Colour": "Barva",
|
||||
@@ -269,7 +270,7 @@
|
||||
"Preview Row": "Náhled řádku",
|
||||
"Preview leaves the current design unchanged.": "Náhled aktuální návrh nemění.",
|
||||
"Profiles/Curves": "Profily/křivky",
|
||||
"Quantity": "Množství",
|
||||
"Quantity": "Počet",
|
||||
"RECENT": "NEDÁVNÉ",
|
||||
"REFERENCE": "REFERENCE",
|
||||
"RULED SURFACE": "PŘÍMKOVÁ PLOCHA",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Hledat příkazy",
|
||||
"Search this library": "Prohledat tuto knihovnu",
|
||||
"Section": "Řez",
|
||||
"Section position": "Poloha průřezu",
|
||||
"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).": "Vybrat jiné: podržte levé tlačítko na modelu (nebo pravé tlačítko › Vybrat jiné…) a zobrazí se vše pod kurzorem; dalším kliknutím na stejné místo vyberete další prvek. Dvojklikem prostředním tlačítkem přejdete na výchozí pohled (Předvolby › Navigace).",
|
||||
"Select bodies (a face picks its body)": "Vyberte tělesa (plocha vybere své těleso)",
|
||||
"Select bodies or faces instead": "Vyberte místo toho tělesa nebo plochy",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Zobrazit kurzor agenta",
|
||||
"Show in folder": "Zobrazit ve složce",
|
||||
"Show the pointer an AI agent moves (#35)": "Zobrazit ukazatel, kterým pohybuje AI agent (#35)",
|
||||
"Show tool at cursor": "Zobrazit nástroj u kurzoru",
|
||||
"Signed axes": "Osy se znaménkem",
|
||||
"Size": "Velikost",
|
||||
"Sketch constraints": "Vazby náčrtu",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se zavřel dříve, než se tyto návrhy uložily. Lze otevřít jejich poslední automatické uložení.",
|
||||
"Specialise": "Specializovat",
|
||||
"Speed": "Rychlost",
|
||||
"Square": "Čtverec",
|
||||
"Stand it up": "Postavit",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postavit: otočení o čtvrt otáčky kolem osy X jako jeden prvek Posunutí, který lze vrátit.",
|
||||
"Stand this part up?": "Postavit tento díl?",
|
||||
"Start angle": "Počáteční úhel",
|
||||
"Start bulge": "Vypouklost začátku",
|
||||
"Start continuity": "Návaznost začátku",
|
||||
"Step": "Krok",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "úhel, výška",
|
||||
"appearance.dielectric_ior": "Index lomu dielektrika",
|
||||
"appearance.metallic": "Kovovost",
|
||||
"appearance.only_available": "Pouze dostupné vzhledy",
|
||||
"appearance.pbr_help": "Tento povrch zobrazíte v renderu nebo v produktovém PNG. Bez lomu a průchodu světla.",
|
||||
"appearance.pbr_opaque": "Neprůhledné PBR",
|
||||
"appearance.roughness": "Drsnost",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Vložit STEP",
|
||||
"cmd.file_save_mesh": "Uložit jako síť",
|
||||
"cmd.file_slicers": "Nainstalované slicery",
|
||||
"cmd.frame_member": "Prvek rámu",
|
||||
"cmd.frame_profile_insert": "Vložit profil",
|
||||
"cmd.frame_profile_list": "Knihovna profilů",
|
||||
"cmd.frame_profile_properties": "Vlastnosti profilu",
|
||||
"cmd.inspect_accessibility": "Analýza přístupnosti",
|
||||
"cmd.inspect_center_of_mass": "Těžiště",
|
||||
"cmd.inspect_clearance": "Vůle",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Nový fyzický materiál",
|
||||
"cmd.material_delete": "Smazat fyzický materiál",
|
||||
"cmd.material_edit": "Upravit fyzický materiál",
|
||||
"cmd.material_inspect": "Zkontrolovat fyzický materiál",
|
||||
"cmd.material_list": "Seznam materiálů",
|
||||
"cmd.material_properties_directional": "Směrový modul pružnosti",
|
||||
"cmd.material_properties_validate": "Ověřit data fyzického materiálu",
|
||||
@@ -615,6 +626,8 @@
|
||||
"cmd.plastic_rules": "Seznam pravidel pro plasty",
|
||||
"cmd.plastic_snap_fit": "Zacvakávací spoj",
|
||||
"cmd.render_backends": "Renderovací jádra",
|
||||
"cmd.render_camera_fit": "Zarámovat objekt renderu",
|
||||
"cmd.render_camera_get": "Prohlédnout kameru renderu",
|
||||
"cmd.render_cancel": "Zrušit render",
|
||||
"cmd.render_export": "Exportovat obrázek renderu",
|
||||
"cmd.render_preflight": "Zkontrolovat požadavek na render",
|
||||
@@ -622,6 +635,7 @@
|
||||
"cmd.render_settings_apply": "Použít kameru renderu",
|
||||
"cmd.render_settings_get": "Nastavení renderu",
|
||||
"cmd.render_settings_set": "Uložit nastavení renderu",
|
||||
"cmd.render_stage_backdrop": "Uložit fyzické pozadí renderu",
|
||||
"cmd.render_start": "Renderovat obrázek",
|
||||
"cmd.render_status": "Průběh renderu",
|
||||
"cmd.select_clear": "Zrušit výběr",
|
||||
@@ -960,6 +974,7 @@
|
||||
"motion.key_here": "Vložit klíčový snímek sem",
|
||||
"motion.key_here_hint": "Přidat v tomto kroku klíčový snímek s aktuální hodnotou kloubu",
|
||||
"motion.no_collisions": "Během studie nedochází ke kolizím",
|
||||
"motion.plot_quantity": "Množství",
|
||||
"motion.step_back": "Krok zpět",
|
||||
"motion.step_forward": "Krok vpřed",
|
||||
"motion.to_end": "Na konec",
|
||||
@@ -985,6 +1000,16 @@
|
||||
"pattern.direction_1": "Směr 1",
|
||||
"pattern.direction_2": "Směr 2",
|
||||
"pattern.flip_direction": "Převrátit směr",
|
||||
"perf.click_for_details": "Klikněte pro podrobnosti: graf časů snímků, histogram a co vykreslila GPU",
|
||||
"perf.draw_calls": "Volání vykreslení",
|
||||
"perf.line_segments": "Úsečky",
|
||||
"perf.low_1": "1% minimum",
|
||||
"perf.renderer": "Vykreslování",
|
||||
"perf.scene_build": "Sestavení scény",
|
||||
"perf.triangles": "Trojúhelníky",
|
||||
"perf.ui_time": "Čas UI",
|
||||
"perf.viewport": "Pohled",
|
||||
"perf.worst": "nejhorší",
|
||||
"pick a hole to seat it, or OK places it at the origin": "vyberte otvor pro usazení, nebo OK umístí do počátku",
|
||||
"pick one or two vertices, edges or faces": "vyberte jeden nebo dva vrcholy, hrany či plochy",
|
||||
"prefs.cpu": "CPU (softwarově)",
|
||||
@@ -997,20 +1022,51 @@
|
||||
"recovery.min_ago": "před {count} min",
|
||||
"recovery.never_saved": "nikdy neuloženo",
|
||||
"render.background": "Pozadí scény",
|
||||
"render.bloom_enable": "Záře ve výstupu",
|
||||
"render.bloom_help": "Kreativní záře z lineárních jasů se třemi normalizovanými průchody. Poloměr v pixelech výstupu, přičtení před sRGB. Nejde o simulaci čočky, objemu ani zlepšení konvergence. Surové EXR se nemění.",
|
||||
"render.bloom_opaque": "Záře vyžaduje neprůhledný výstup. Průhledné PNG se odmítne, neprovede se skryté sloučení.",
|
||||
"render.bloom_radius": "Poloměr záře",
|
||||
"render.bloom_strength": "Síla záře",
|
||||
"render.bloom_threshold": "Lineární práh jasů",
|
||||
"render.cpu_aa_help": "Prostorové vzorky: více vzorků zlepší hrany a prodlouží vykreslení.",
|
||||
"render.cpu_key_rim": "Hlavní a obrysové",
|
||||
"render.cpu_lighting": "Předvolba osvětlení",
|
||||
"render.cpu_neutral": "Neutrální",
|
||||
"render.cpu_opaque_required": "CPU trasování vyžaduje neprůhledné pozadí. Vypněte průhledné pozadí.",
|
||||
"render.cpu_options_pending": "Panel podporuje vzorky hran, předvolby světel, stíny a expozici. Další volby jsou v API.",
|
||||
"render.cpu_primary_samples": "Vzorky hran na pixel",
|
||||
"render.cpu_shadows": "Vrhat stíny",
|
||||
"render.cpu_summary": "Neprůhledný CPU PNG · světla · stíny · bez skla a textur",
|
||||
"render.cpu_warm_cool": "Teplé a studené",
|
||||
"render.exr_pending": "GPU může uchovat surové lineární EXR. Komprese, vícevrstvé obrazy a pomocné průchody zatím nejsou dostupné.",
|
||||
"render.fit": "Přizpůsobit produktu",
|
||||
"render.gpu_depth": "Maximální hloubka cesty",
|
||||
"render.gpu_options_help": "Tyto volby platí pro tuto úlohu. Expozice vychází z uloženého nastavení.",
|
||||
"render.gpu_path_failed": "Trasování cest GPU není dostupné",
|
||||
"render.gpu_path_first_setup": "První příprava na této GPU může trvat několik minut; SolidCraft lze mezitím používat. Při dalších spuštěních se použijí zkompilované shadery, pokud je ovladač uchová.",
|
||||
"render.gpu_path_preparing": "Trasování cest GPU: kompilace shaderů…",
|
||||
"render.gpu_seed": "Náhodné semeno",
|
||||
"render.gpu_unavailable": "Trasování cest GPU není na tomto zařízení připraveno.",
|
||||
"render.image_advanced": "Pokročilé nastavení GPU",
|
||||
"render.image_cancelled": "Render zrušen",
|
||||
"render.image_cancelling": "Rušení",
|
||||
"render.image_completed": "Obrázek připraven k exportu",
|
||||
"render.image_encoding": "Kódování PNG",
|
||||
"render.image_export": "Exportovat PNG",
|
||||
"render.image_export": "Exportovat hotový obrázek",
|
||||
"render.image_exposure": "Expozice",
|
||||
"render.image_failed": "Render selhal",
|
||||
"render.image_height": "Výška",
|
||||
"render.image_help": "Náhledový obrázek z CPU · PNG · přibližná průhlednost",
|
||||
"render.image_host_missing": "Není dostupný vykreslovací hostitel.",
|
||||
"render.image_method": "Metoda obrazu",
|
||||
"render.image_overwrite": "Nahradit existující soubor",
|
||||
"render.image_preparing": "Příprava obrázku",
|
||||
"render.image_progressive": "Progresivní render",
|
||||
"render.image_queued": "Čeká na render",
|
||||
"render.image_reduce": "Zkuste menší obrázek nebo jednodušší scénu.",
|
||||
"render.image_running": "Renderování obrázku",
|
||||
"render.image_samples": "Vzorků na pixel",
|
||||
"render.image_scene_stale": "Před kontrolou nebo spuštěním dokončete modelování nebo obnovte scénu.",
|
||||
"render.image_stale": "Tento obrázek používá scénu zachycenou před pozdějšími změnami.",
|
||||
"render.image_start": "Spustit render",
|
||||
"render.image_supported": "Požadavek na render je podporován; fronta a paměť se znovu ověří při spuštění.",
|
||||
@@ -1020,17 +1076,135 @@
|
||||
"render.image_unknown": "Stav renderu není k dispozici",
|
||||
"render.image_use_largest": "Použít největší velikost v tomto poměru stran:",
|
||||
"render.image_width": "Šířka",
|
||||
"render.material_absorption": "Absorpce R · G · B (na mm)",
|
||||
"render.material_add": "Přidat nebo upravit těleso",
|
||||
"render.material_assignments": "Přiřazení těles",
|
||||
"render.material_binding_enable": "Zahrnout úpravy tohoto tělesa",
|
||||
"render.material_box": "Osově zarovnaný kvádr",
|
||||
"render.material_browse": "Vybrat obrázek…",
|
||||
"render.material_certification": "Uzavřené sklo používá geometrickou vzdálenost. GPU příprava ověřuje topologii, orientaci, překryvy a optická rozhraní; kontrola jen přijímá požadavek.",
|
||||
"render.material_choose_body": "Vybrat těleso scény",
|
||||
"render.material_closed": "Uzavřená objemová síť",
|
||||
"render.material_coat": "Čirý povlak na základním materiálu",
|
||||
"render.material_coat_enable": "Zapnout čirý povlak",
|
||||
"render.material_coat_help": "Ilustrativní dielektrický povlak na stávajícím neprůhledném vzhledu. Nulová váha ponechá základ beze změny.",
|
||||
"render.material_color_map": "Obraz základní barvy (sRGB)",
|
||||
"render.material_custom": "Vlastní ilustrativní optika",
|
||||
"render.material_enable": "Použít tyto úpravy materiálů GPU",
|
||||
"render.material_filter": "Filtrovat tělesa scény…",
|
||||
"render.material_filter_help": "Zobrazuje se nejvýše 128 shod. U velkých scén zpřesněte filtr.",
|
||||
"render.material_glass": "Sklo a optická absorpce",
|
||||
"render.material_glass_enable": "Použít fyzikální sklo na těleso",
|
||||
"render.material_glass_exclusive": "Tento renderer nepodporuje sklo s povlakem nebo mapami na témže tělese.",
|
||||
"render.material_glass_help": "Původní ilustrativní recepty. Vzorky ukazují propustnost, ne render. Absorpce je −ln(lineární propustnosti) / referenční vzdálenost; alfa se nemění.",
|
||||
"render.material_gpu_required": "Aktivní úpravy vyžadují GPU tracing. Před kontrolou nebo spuštěním vyberte GPU nebo úpravy vypněte.",
|
||||
"render.material_ior": "Index lomu (IOR)",
|
||||
"render.material_limits": "Nejvýše 128 nastavení těles, 32 oddělených skel, 64 povlaků a 128 vazeb textur. Shodné mapy sdílejí paměť; limity zařízení a celkové paměti zůstávají aktivní.",
|
||||
"render.material_map_invalid": "Cesty map musí být omezené místní cesty; síla normál musí být konečná mezi 0 a 1.",
|
||||
"render.material_map_required": "Před použitím map vyberte obraz barvy nebo normál.",
|
||||
"render.material_maps": "Rovinné mapy barvy a normál",
|
||||
"render.material_maps_enable": "Zapnout mapy neprůhledného tělesa",
|
||||
"render.material_maps_help": "PNG/JPEG do 1024×1024; celkem 16 MiB kódovaných dat. U a V jsou kolmé vektory opakování ve světových mm (nejvýše 128 opakování). Barva je sRGB, normály OpenGL +Y. Soubory se zachytí při kontrole/spuštění.",
|
||||
"render.material_normal_map": "Obraz normál (OpenGL, +Y)",
|
||||
"render.material_normal_strength": "Síla normál (0–1)",
|
||||
"render.material_origin": "Počátek mapování X · Y · Z (mm)",
|
||||
"render.material_refresh": "Obnovit tělesa scény",
|
||||
"render.material_refresh_needed": "Scéna se změnila. Před přidáním cíle obnovte seznam.",
|
||||
"render.material_remove": "Odstranit přiřazení",
|
||||
"render.material_repeat_u": "Jedno opakování U X · Y · Z (mm)",
|
||||
"render.material_repeat_v": "Jedno opakování V X · Y · Z (mm)",
|
||||
"render.material_revalidate": "Znovu ověřit těleso",
|
||||
"render.material_revalidate_needed": "Přiřazení patří ke starší revizi scény. Před renderem znovu ověřte přesnou identitu tělesa.",
|
||||
"render.material_roughness": "Drsnost povlaku (0,045–1)",
|
||||
"render.material_shadow": "Propouštět přímé stínové paprsky",
|
||||
"render.material_target_missing": "Pořadí a název tělesa již nesouhlasí. Odstraňte přiřazení a znovu výslovně vyberte těleso.",
|
||||
"render.material_thickness": "Tloušťka vrstvy (mm)",
|
||||
"render.material_thin": "Tenká vrstva",
|
||||
"render.material_title": "Materiály GPU obrazu",
|
||||
"render.material_transient": "Dočasné optické úpravy pro tuto kartu. Neukládají se do návrhu, nastavení renderu ani knihovny materiálů.",
|
||||
"render.material_weight": "Váha povlaku (0–1)",
|
||||
"render.method_cpu": "Trasování paprsků CPU",
|
||||
"render.method_gpu": "Trasování cest GPU",
|
||||
"render.method_raster": "CPU rastr",
|
||||
"render.navigation_help": "Kamera se ovládá stejně jako v návrhu (Předvolby › Navigace). Uložit nastavení zaznamená aktuální kameru.",
|
||||
"render.opaque_image_summary": "Náhledové PNG · neprůhledné PBR · smíšená průhlednost",
|
||||
"render.opaque_preview_help": "Neprůhledné PBR a jednoduché barvy; smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.output_format_status": "PNG je přesný dokončený obraz. Volitelné GPU EXR uchovává lineární FLOAT32 radianci před filtry. Export nikdy nenahrazuje výsledek náhledem nebo jinou úlohou.",
|
||||
"render.perspective": "Perspektivní kamera",
|
||||
"render.preview": "Integrovaný náhled",
|
||||
"render.preview_help": "Rastrové stínování a smíšená průhlednost. Lom světla, HDRI a finální render zatím nejsou k dispozici.",
|
||||
"render.progressive_image_summary": "Náhledové PNG · neprůhledné PBR · postupné vzorkování",
|
||||
"render.reload_settings": "Znovu načíst uložené nastavení",
|
||||
"render.restore_camera": "Obnovit uloženou kameru",
|
||||
"render.result_actual_help": "Jeden pixel obrázku na jeden pixel obrazovky.",
|
||||
"render.result_capture": "Zachycená revize",
|
||||
"render.result_current": "Zachycená revize modelu je aktuální.",
|
||||
"render.result_empty": "Zatím není hotový obrázek. Zkontrolujte nastavení a spusťte vykreslování.",
|
||||
"render.result_fit": "Přizpůsobit obrázek",
|
||||
"render.result_job": "Úloha",
|
||||
"render.result_navigation": "Posun a přiblížení používají nastavení navigace. Mezerník + tažení posouvá.",
|
||||
"render.result_open": "Otevřít hotový obrázek na hlavním plátně.",
|
||||
"render.result_preview": "Živý náhled",
|
||||
"render.result_retained": "Zobrazuje se poslední hotový obrázek; novější vykreslení jej zatím nenahradilo.",
|
||||
"render.result_title": "Výsledek vykreslení",
|
||||
"render.return_design": "Zpět k návrhu",
|
||||
"render.ribbon_backgrounds": "Pozadí",
|
||||
"render.ribbon_check": "Kontrola",
|
||||
"render.ribbon_check_help": "Ověřit přesný aktuální návrh renderu. Bez skrytého přepnutí vykreslovače, zmenšení nebo nahrazení zdrojů.",
|
||||
"render.ribbon_edit_lights": "Upravit světla",
|
||||
"render.ribbon_environment_help": "HDR osvětlení, pozadí kamery a fyzické plochy jsou samostatné ovládací prvky s potvrzením nahrazení.",
|
||||
"render.ribbon_finish_dialog": "Nejprve dokončete dialog souboru nebo potvrzení.",
|
||||
"render.ribbon_frame": "Zarámovat",
|
||||
"render.ribbon_hdr_library": "Knihovna HDR",
|
||||
"render.ribbon_job_active": "Render již běží; před spuštěním dalšího jej výslovně zrušte.",
|
||||
"render.ribbon_light_rigs": "Sestavy světel",
|
||||
"render.ribbon_lights_help": "Zkontrolovat světelné sestavy nebo upravit podporovaná světla; aktuální nastavení nahradí až výslovné použití.",
|
||||
"render.ribbon_materials": "Materiály",
|
||||
"render.ribbon_materials_help": "Otevřít inspektor vzhledu a původní povrchy. Optické úpravy nemění skrytě technické vlastnosti.",
|
||||
"render.ribbon_no_active_job": "Není aktivní render ke zrušení.",
|
||||
"render.ribbon_no_result": "Před exportem dokončete render.",
|
||||
"render.ribbon_no_saved_camera": "Před obnovením kamery uložte záběr.",
|
||||
"render.ribbon_saved_camera": "Uložená kamera",
|
||||
"render.ribbon_shots": "Záběry",
|
||||
"render.ribbon_studios": "Studia",
|
||||
"render.ribbon_studios_help": "Procházejte dostupná studia a před použitím zkontrolujte nahrazované prvky. Koncepty zůstávají označené.",
|
||||
"render.ribbon_surface_maps": "Mapy povrchu",
|
||||
"render.save_setup": "Uložit nastavení",
|
||||
"render.setup_name": "Název nastavení",
|
||||
"render.studio_add_area": "Přidat plošné světlo",
|
||||
"render.studio_ambient": "Konstantní zář prostředí",
|
||||
"render.studio_aperture": "Poloměr clony",
|
||||
"render.studio_area": "Plošné světlo",
|
||||
"render.studio_area_help": "Nejvýše čtyři jednostranné obdélníky. Kolmé poloviční hrany U a V určují velikost; U × V míří na osvětlenou stranu. Souřadnice jsou světové milimetry.",
|
||||
"render.studio_center": "Střed X, Y, Z (mm)",
|
||||
"render.studio_denoise": "Potlačit šum v hotovém obrazu",
|
||||
"render.studio_denoise_help": "Závěrečný filtr respektuje hrany; může změkčit detaily. Sklo a ostré zrcadlové povrchy se nefiltrují.",
|
||||
"render.studio_direction": "Směr ke slunci (X, Y, Z)",
|
||||
"render.studio_environment": "Prostředí",
|
||||
"render.studio_environment_bounds": "Intenzita prostředí musí být 0–100 a otočení −360° až 360°.",
|
||||
"render.studio_environment_none": "Bez mapy prostředí",
|
||||
"render.studio_focus": "Osová zaostřovací vzdálenost",
|
||||
"render.studio_half_u": "Poloviční hrana U (mm)",
|
||||
"render.studio_half_v": "Poloviční hrana V (mm)",
|
||||
"render.studio_hdr": "Soubor Radiance HDR",
|
||||
"render.studio_hdr_browse": "Vybrat HDR…",
|
||||
"render.studio_hdr_help": "Radiance .hdr, panorama 2:1, nejvýše 2048 × 1024. EXR není dostupné. Předpoklad barev platí jen při neurčených primárních barvách.",
|
||||
"render.studio_hdr_required": "Vyberte místní soubor .hdr.",
|
||||
"render.studio_hdr_srgb": "Neurčené primární barvy chápat jako lineární sRGB",
|
||||
"render.studio_intensity": "Intenzita prostředí",
|
||||
"render.studio_lens": "Hloubka ostrosti",
|
||||
"render.studio_lens_perspective": "Hloubka ostrosti vyžaduje perspektivní kameru.",
|
||||
"render.studio_lighting": "Studiové osvětlení",
|
||||
"render.studio_lighting_enable": "Použít vlastní studiové osvětlení",
|
||||
"render.studio_radiance": "Lineární RGB zář",
|
||||
"render.studio_remove_area": "Odebrat plošné světlo",
|
||||
"render.studio_rotation": "Otočení prostředí",
|
||||
"render.studio_runtime_help": "Optické úpravy těles jsou v Materiálech GPU obrazu. Ovládání studia a materiálů je dočasné a nemění uložené vzhledy.",
|
||||
"render.studio_sky": "Umělecká obloha",
|
||||
"render.studio_sky_help": "Původní přechod oblohy pro osvětlení; nejde o kalibrovaný model atmosféry.",
|
||||
"render.studio_sun": "Slunce",
|
||||
"render.studio_sun_radius": "Úhlový poloměr",
|
||||
"render.studio_tone_map": "Mapování tónů",
|
||||
"render.unavailable_preview": "Toto nastavení vyžaduje nedostupný profil náhledu.",
|
||||
"render.unsaved": "Neuložené změny náhledu",
|
||||
"render.use_preview": "Použít integrovaný náhled",
|
||||
@@ -1060,6 +1234,7 @@
|
||||
"search.try": "Zadejte název příkazu, prvek v tomto návrhu nebo parametr. Chcete-li spustit příkaz s hodnotami, zadejte jeho id a objekt JSON: {example}",
|
||||
"search.valid": "Platné",
|
||||
"settings.display_quality_note": "Jemnější úrovně kreslí zakřivené plochy a hrany hladčeji, ale síťování trvá déle. Mění se jen zobrazení, nikdy návrh ani exporty.",
|
||||
"settings.frame_rate_details_note": "Přidá graf časů posledních snímků s vykreslenými trojúhelníky, histogram časů snímků a to, co vykreslila GPU. Kliknutím na snímkovou frekvenci v pohledu je zobrazíte nebo skryjete.",
|
||||
"settings.frame_rate_note": "Zobrazuje snímky za sekundu v pravém dolním rohu pohledu.",
|
||||
"settings.measure_axis_distances": "Zobrazit vzdálenosti X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Při otevřeném měření zobrazit kromě celkové vzdálenosti i složky ve směru os.",
|
||||
@@ -1116,12 +1291,115 @@
|
||||
"toolbar.customize_note": "Karta {tab}. Zaškrtnutím příkazu přidáte jeho tlačítko na panel nástrojů. Můžete také přetáhnout řádek z rozbalovací nabídky panelu na panel nástrojů, odstranit tlačítko přetažením mimo panel nástrojů nebo použít připínáček na řádku rozbalovací nabídky.",
|
||||
"toolbar.image": "OBRÁZEK",
|
||||
"toolbar.render": "RENDER",
|
||||
"toolbar.render_appearance": "VZHLED",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "PROSTŘEDÍ",
|
||||
"toolbar.render_lighting": "OSVĚTLENÍ",
|
||||
"toolbar.render_scene": "SCÉNA",
|
||||
"trace.points": "Bodů podél cesty: {count}",
|
||||
"trace.stopped_by_contact": "Končí ve vzorku {sample}: {a} by prošlo skrz {b}",
|
||||
"trace.stopped_by_loop": "Končí ve vzorku {sample}: smyčku přes {joint} už nelze dále uzavřít",
|
||||
"ui.1_degree_of_freedom": "1 stupeň volnosti",
|
||||
"ui.3d_mouse_axes_reassigned": "Osy jsou v Předvolbách přeřazené a to platí i po této kalibraci.",
|
||||
"ui.3d_mouse_button": "Tlačítko {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nic",
|
||||
"ui.3d_mouse_button_settings": "Nastavení 3D myši",
|
||||
"ui.3d_mouse_buttons": "Tlačítka",
|
||||
"ui.3d_mouse_buttons_hint": "Stiskněte tlačítko na 3D myši, ať ho tu najdete, a vyberte, co má dělat.",
|
||||
"ui.3d_mouse_buttons_no_device": "Připojte 3D myš a nastavte, co dělají její tlačítka.",
|
||||
"ui.3d_mouse_buttons_reset": "Obnovit výchozí tlačítka",
|
||||
"ui.3d_mouse_calibrate": "Kalibrovat…",
|
||||
"ui.3d_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrace 3D myši",
|
||||
"ui.3d_mouse_calibrate_wait": "Jednou pohněte 3D myší, aby ji SolidCraft uviděl.",
|
||||
"ui.3d_mouse_calibrated": "Zkalibrováno",
|
||||
"ui.3d_mouse_calibration": "Kalibrace 3D myši",
|
||||
"ui.3d_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
|
||||
"ui.3d_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
|
||||
"ui.3d_mouse_circles_help": "Kruhy ukazují, co zařízení posílá po kalibraci. Zkušební model ukazuje, co s tím udělá pohled po obrácení, rychlosti a přiřazení os.",
|
||||
"ui.3d_mouse_dead_zone": "Mrtvá zóna",
|
||||
"ui.3d_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vidí 3D myš, ale nesmí ji číst. Pravidlo udev najdete v poznámkách k nastavení 3D myši.",
|
||||
"ui.3d_mouse_denied_short": "Nalezeno, ale nelze číst",
|
||||
"ui.3d_mouse_driven_by": "Který kalibrovaný pohyb zařízení ovládá tuto osu. Když vyberete pohyb, který už používá jiná osa, obě se prohodí.",
|
||||
"ui.3d_mouse_driven_by_label": "ovládá",
|
||||
"ui.3d_mouse_driver": "Ovladač 3D myši",
|
||||
"ui.3d_mouse_driver_auto": "Automaticky",
|
||||
"ui.3d_mouse_driver_direct": "Přímo (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Zatím se čte přes spacenavd: zařízení samotné se nepodařilo otevřít.",
|
||||
"ui.3d_mouse_driver_only_direct": "Zařízení se čte přímo; spacenavd neběží",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Čte se přes spacenavd, jiná možnost není",
|
||||
"ui.3d_mouse_forget": "Zapomenout kalibraci",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vlevo ve výřezu",
|
||||
"ui.3d_mouse_ghost_left_window": "Vlevo v okně",
|
||||
"ui.3d_mouse_ghost_position": "Poloha",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vpravo ve výřezu",
|
||||
"ui.3d_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
|
||||
"ui.3d_mouse_kind_cap": "Návrat do středu",
|
||||
"ui.3d_mouse_kind_question": "Jaký typ 3D myši to je?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Hodnota zůstává, dokud ji držíte, jako u zařízení typu „Návrat do středu“.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Hodnota se vrací zpět, jakmile přestanete hýbat, jako u trackballu.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotace X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotace Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotace Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Posun X",
|
||||
"ui.3d_mouse_motion_translation_y": "Posun Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Posun Z",
|
||||
"ui.3d_mouse_navigate": "Navigace pomocí 3D myši",
|
||||
"ui.3d_mouse_none": "3D myš nenalezena",
|
||||
"ui.3d_mouse_not_calibrated": "Nezkalibrováno",
|
||||
"ui.3d_mouse_offer": "Kalibrace 3D myši opraví prohozené nebo obrácené osy a nastaví její rychlost a pocit z ovládání.",
|
||||
"ui.3d_mouse_offer_never": "Už nezobrazovat",
|
||||
"ui.3d_mouse_offer_swap": "Přepnout na Hidraw (přímý ovladač)",
|
||||
"ui.3d_mouse_reading": "Hodnoty",
|
||||
"ui.3d_mouse_reset_axes": "Obnovit přiřazení os",
|
||||
"ui.3d_mouse_reset_view": "Obnovit pohled",
|
||||
"ui.3d_mouse_restore": "Obnovit výchozí nastavení 3D myši",
|
||||
"ui.3d_mouse_restore_ask": "Vrátit nastavení 3D myši na výchozí hodnoty? Kalibrace a přiřazení tlačítek zůstanou.",
|
||||
"ui.3d_mouse_section": "3D myš",
|
||||
"ui.3d_mouse_show_in_view": "Zobrazit 3D myš ve 3D pohledu",
|
||||
"ui.3d_mouse_show_in_view_tip": "Obrázek vaší 3D myši v levém dolním rohu, který se hýbe s ní.",
|
||||
"ui.3d_mouse_skip": "Moje zařízení to neumí",
|
||||
"ui.3d_mouse_skip_tip": "Přeskočí tento pohyb v obou směrech a kalibrace ho vynechá.",
|
||||
"ui.3d_mouse_speed": "Rychlost 3D myši",
|
||||
"ui.3d_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Dejte ruku z kuličky a nechte ji v klidu.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Otočte rolovacím kroužkem o pár cvaknutí po směru hodinových ručiček.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Otočte rolovacím kroužkem o pár cvaknutí proti směru hodinových ručiček.",
|
||||
"ui.3d_mouse_step_roll_away": "Několikrát kutálejte kuličkou od sebe, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_left": "Několikrát kutálejte kuličkou doleva, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_right": "Několikrát kutálejte kuličkou doprava, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_roll_toward": "Několikrát kutálejte kuličkou k sobě, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Několikrát roztočte kuličku po směru hodinových ručiček, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Několikrát roztočte kuličku proti směru hodinových ručiček, pak ji nechte zastavit.",
|
||||
"ui.3d_mouse_step_tilt_away": "Nakloňte vršek hlavice od sebe (okraj nejblíž vám jde nahoru), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Nakloňte vršek hlavice k sobě (okraj nejblíž vám jde dolů), podržte a pusťte.",
|
||||
"ui.3d_mouse_step_touch": "Položte ruku na hlavici, ale netlačte na ni.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.3d_mouse_switch_cap": "Kalibrovat jako návrat do středu",
|
||||
"ui.3d_mouse_switch_trackball": "Kalibrovat jako trackball",
|
||||
"ui.3d_mouse_view_axis": "Osa pohledu",
|
||||
"ui.3d_mouse_view_off": "Vypnuto",
|
||||
"ui.3d_mouse_view_off_tip": "Profil ovladače 3D myši tuto osu vypíná, takže její pohyb s pohledem nehýbe. Když tu vyberete pohyb jiné osy, prohodí se s ní a s pohledem přestane hýbat.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit vlevo / vpravo",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit nahoru / dolů",
|
||||
"ui.3d_mouse_view_pan_sideways": "Posun vlevo / vpravo",
|
||||
"ui.3d_mouse_view_pan_up_down": "Posun nahoru / dolů",
|
||||
"ui.3d_mouse_view_roll": "Natočení",
|
||||
"ui.3d_mouse_view_unmapped": "Osa {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Čeká na pohyb",
|
||||
"ui.3d_mouse_wiggle": "Pokud máte nějakou připojenou, zahýbejte s ní.",
|
||||
"ui.3d_sketch": "3D náčrt",
|
||||
"ui.activate": "Aktivovat",
|
||||
"ui.active_component": "Aktivní komponenta",
|
||||
@@ -1279,6 +1557,7 @@
|
||||
"ui.ai_http_busy": "endpoint je zaneprázdněný nebo překročil limit; zkuste to později",
|
||||
"ui.ai_http_key": "endpoint odmítl klíč: zkontrolujte jej v Předvolby > AI",
|
||||
"ui.ai_http_not_found": "takový endpoint nebo model neexistuje: zkontrolujte základní URL a název modelu",
|
||||
"ui.ai_http_too_large": "konverzace je pro tento endpoint příliš velká; začněte nový chat",
|
||||
"ui.ai_key": "API klíč",
|
||||
"ui.ai_key_hint": "volitelné; uloženo v systémové klíčence",
|
||||
"ui.ai_key_needs_https": "Klíč se odesílá pouze přes HTTPS nebo na tento počítač.",
|
||||
@@ -1306,7 +1585,6 @@
|
||||
"ui.ai_permissions": "Oprávnění",
|
||||
"ui.ai_preset": "Předvolba",
|
||||
"ui.ai_remove": "Odebrat",
|
||||
"ui.ai_request_limit": "zastaveno po 24 požadavcích na jednu zprávu; pro pokračování pošlete další zprávu",
|
||||
"ui.ai_reset": "Obnovit",
|
||||
"ui.ai_settings": "Nastavení AI…",
|
||||
"ui.ai_show_features": "Zobrazit funkce AI",
|
||||
@@ -1351,6 +1629,7 @@
|
||||
"ui.blocked": "Blokováno",
|
||||
"ui.bodies": "Tělesa",
|
||||
"ui.bodies_all_if_none": "Tělesa (všechna, pokud žádné)",
|
||||
"ui.bottom": "Zespodu",
|
||||
"ui.box": "Kvádr",
|
||||
"ui.break": "Přerušit",
|
||||
"ui.browser_bodies_one": "{count} těleso",
|
||||
@@ -1482,6 +1761,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrovat příkazy nebo klávesy",
|
||||
"ui.findInTimeline": "Najít na časové ose",
|
||||
"ui.findInWindow": "Najít v okně",
|
||||
"ui.fine": "Jemná",
|
||||
"ui.finest": "Nejjemnější",
|
||||
"ui.finish_sketch": "Dokončit náčrt",
|
||||
"ui.fit": "Přizpůsobit",
|
||||
"ui.fix_unfix": "Zafixovat/uvolnit",
|
||||
@@ -1489,6 +1770,7 @@
|
||||
"ui.flip": "Převrátit",
|
||||
"ui.format": "Formát",
|
||||
"ui.frame_rate": "Snímková frekvence",
|
||||
"ui.frame_rate_details": "Podrobnosti snímkové frekvence",
|
||||
"ui.front": "Zepředu",
|
||||
"ui.fully_constrained": "Plně zavazbeno",
|
||||
"ui.function": "Funkce",
|
||||
@@ -1570,6 +1852,7 @@
|
||||
"ui.heading_torus": "ANULOID",
|
||||
"ui.heading_utilities": "NÁSTROJE",
|
||||
"ui.heading_utility": "NÁSTROJE",
|
||||
"ui.heading_weldments": "SVAŘENCE",
|
||||
"ui.help": "Nápověda",
|
||||
"ui.hide": "Skrýt",
|
||||
"ui.hideAxes": "Skrýt osy",
|
||||
@@ -1577,6 +1860,8 @@
|
||||
"ui.hideOrigin": "Skrýt počátek",
|
||||
"ui.hidePlanes": "Skrýt roviny",
|
||||
"ui.hideProfile": "Skrýt profil",
|
||||
"ui.hide_origin_by_default": "Ve výchozím stavu skrýt počátek",
|
||||
"ui.hide_origin_note": "Nové návrhy začínají se skrytou složkou Počátek. Návrh si ponechá vlastní nastavení počátku, jakmile ho zobrazíte nebo skryjete.",
|
||||
"ui.history_back": "O krok zpět",
|
||||
"ui.history_below_browser": "Pod prohlížečem",
|
||||
"ui.history_first": "Přejít na začátek",
|
||||
@@ -1706,6 +1991,7 @@
|
||||
"ui.project_face_edges_sketch": "Promítat hrany plochy do nových náčrtů",
|
||||
"ui.properties": "Vlastnosti",
|
||||
"ui.pull_direction": "Směr vyjímání",
|
||||
"ui.quality": "Kvalita",
|
||||
"ui.quantity": "Počet",
|
||||
"ui.quantity_2": "Počet 2",
|
||||
"ui.quit": "Ukončit",
|
||||
@@ -1794,65 +2080,13 @@
|
||||
"ui.smooth_wheel_zoom": "Plynulé přiblížení kolečkem",
|
||||
"ui.smooth_wheel_zoom_tip": "Vypnutím se každý pohyb kolečka projeví okamžitě, bez dobíhání",
|
||||
"ui.snap": "Přichytávání",
|
||||
"ui.space_mouse_axes_reassigned": "Osy jsou v Předvolbách přeřazené a to platí i po této kalibraci.",
|
||||
"ui.space_mouse_calibrate": "Kalibrovat…",
|
||||
"ui.space_mouse_calibrate_tip": "Pohybujte hlavicí podle pokynů v okně, aby se SolidCraft naučil, kterým směrem a jak daleko se pohybuje každá osa této 3D myši. Uloží se pro tuto 3D myš.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrace 3D myši",
|
||||
"ui.space_mouse_calibrated": "Zkalibrováno",
|
||||
"ui.space_mouse_calibration": "Kalibrace 3D myši",
|
||||
"ui.space_mouse_calibration_done": "Hotovo. Uložte ji a použije se vždy, když bude tato 3D myš připojená.",
|
||||
"ui.space_mouse_calibration_no_response": "Jedna osa se skoro nepohnula, takže ji nelze zkalibrovat. Začněte znovu a pokaždé hlavicí pohněte dál.",
|
||||
"ui.space_mouse_dead_zone": "Mrtvá zóna",
|
||||
"ui.space_mouse_dead_zone_tip": "Jak daleko se musí hlavice pohnout, než se pohne pohled, jako podíl jejího plného rozsahu. Zvyšte ji, pokud pohled ujíždí, i když je hlavice v klidu.",
|
||||
"ui.space_mouse_driven_by": "Který kalibrovaný pohyb zařízení ovládá tuto osu. Když vyberete pohyb, který už používá jiná osa, obě se prohodí.",
|
||||
"ui.space_mouse_driven_by_label": "ovládá",
|
||||
"ui.space_mouse_driver": "Ovladač 3D myši",
|
||||
"ui.space_mouse_driver_auto": "Automaticky",
|
||||
"ui.space_mouse_driver_direct": "Přímo (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Zařízení se čte přímo; spacenavd neběží",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Čte se přes spacenavd, jiná možnost není",
|
||||
"ui.space_mouse_forget": "Zapomenout kalibraci",
|
||||
"ui.space_mouse_hint": "Posun a otáčení pohledu současně; náčrty používají jen posun a přiblížení.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotace X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotace Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotace Z",
|
||||
"ui.space_mouse_motion_translation_x": "Posun X",
|
||||
"ui.space_mouse_motion_translation_y": "Posun Y",
|
||||
"ui.space_mouse_motion_translation_z": "Posun Z",
|
||||
"ui.space_mouse_navigate": "Navigace pomocí SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "3D myš nenalezena",
|
||||
"ui.space_mouse_not_calibrated": "Nezkalibrováno",
|
||||
"ui.space_mouse_reading": "Hodnoty",
|
||||
"ui.space_mouse_reset_axes": "Obnovit přiřazení os",
|
||||
"ui.space_mouse_step_lift": "Zvedněte hlavici nahoru, podržte a pusťte.",
|
||||
"ui.space_mouse_step_press": "Zatlačte hlavici dolů, podržte a pusťte.",
|
||||
"ui.space_mouse_step_pull": "Přitáhněte hlavici k sobě, podržte a pusťte.",
|
||||
"ui.space_mouse_step_push": "Odtlačte hlavici od sebe, podržte a pusťte.",
|
||||
"ui.space_mouse_step_rest": "Dejte ruku z hlavice a nechte ji v klidu.",
|
||||
"ui.space_mouse_step_slide_left": "Posuňte hlavici doleva, podržte a pusťte.",
|
||||
"ui.space_mouse_step_slide_right": "Posuňte hlavici doprava, podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_back": "Nakloňte hlavici dozadu (přední hrana nahoru), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_forward": "Nakloňte hlavici dopředu (přední hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_left": "Nakloňte hlavici doleva (levá hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_tilt_right": "Nakloňte hlavici doprava (pravá hrana dolů), podržte a pusťte.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Otočte hlavici po směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Otočte hlavici proti směru hodinových ručiček, podržte a pusťte.",
|
||||
"ui.space_mouse_view_axis": "Osa pohledu",
|
||||
"ui.space_mouse_view_off": "Vypnuto",
|
||||
"ui.space_mouse_view_off_tip": "Profil ovladače 3D myši tuto osu vypíná, takže její pohyb s pohledem nehýbe. Když tu vyberete pohyb jiné osy, prohodí se s ní a s pohledem přestane hýbat.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbit vlevo / vpravo",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbit nahoru / dolů",
|
||||
"ui.space_mouse_view_pan_sideways": "Posun vlevo / vpravo",
|
||||
"ui.space_mouse_view_pan_up_down": "Posun nahoru / dolů",
|
||||
"ui.space_mouse_view_roll": "Natočení",
|
||||
"ui.space_mouse_view_unmapped": "Osa {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Rozteč",
|
||||
"ui.spacing_2": "Rozteč 2",
|
||||
"ui.sphere": "Koule",
|
||||
"ui.spline": "Splajn",
|
||||
"ui.split_body": "Rozdělit těleso",
|
||||
"ui.stand_up_tip": "Když se soubor STEP nebo IGES otevře s osou Z nahoru, nabídnout otočení o čtvrt otáčky kolem X",
|
||||
"ui.standard": "Standardní",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Začít znovu",
|
||||
"ui.start_page": "Úvodní stránka",
|
||||
|
||||
+503
-35
File diff suppressed because it is too large
Load Diff
+263
-57
@@ -85,6 +85,7 @@
|
||||
"Choose…": "Auswählen…",
|
||||
"Circular": "Kreisförmig",
|
||||
"Circumference": "Umfang",
|
||||
"Clockwise": "Im Uhrzeigersinn",
|
||||
"Close draft": "Entwurf schließen",
|
||||
"Collapse": "Einklappen",
|
||||
"Collisions": "Kollisionen",
|
||||
@@ -337,7 +338,7 @@
|
||||
"Profile / Edges": "Profil / Kanten",
|
||||
"Profiles or curves": "Profile oder Kurven",
|
||||
"Profiles/Curves": "Profile/Kurven",
|
||||
"Quantity": "Größe",
|
||||
"Quantity": "Anzahl",
|
||||
"RECENT": "ZULETZT",
|
||||
"REFERENCE": "REFERENZ",
|
||||
"RULED SURFACE": "REGELFLÄCHE",
|
||||
@@ -404,6 +405,7 @@
|
||||
"Search this library": "Diese Bibliothek durchsuchen",
|
||||
"Second plane": "Zweite Ebene",
|
||||
"Section": "Schnitt",
|
||||
"Section position": "Lage des Querschnitts",
|
||||
"Sections (one sketch each)": "Querschnitte (je eine Skizze)",
|
||||
"Select Other": "Andere auswählen",
|
||||
"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).": "Anderes auswählen: Halten Sie die linke Taste ruhig auf dem Modell gedrückt (oder Rechtsklick › Anderes auswählen…), um alles unter dem Cursor aufzulisten; klicken Sie erneut an dieselbe Stelle, um das nächste Element dort auszuwählen. Doppelklick mit der mittleren Taste wechselt zur Startansicht (Einstellungen › Navigation).",
|
||||
@@ -419,6 +421,7 @@
|
||||
"Show Agent Cursor": "Agenten-Cursor anzeigen",
|
||||
"Show in folder": "Im Ordner anzeigen",
|
||||
"Show the pointer an AI agent moves (#35)": "Den Zeiger anzeigen, den ein KI-Agent bewegt (#35)",
|
||||
"Show tool at cursor": "Werkzeug am Cursor anzeigen",
|
||||
"Signed axes": "Vorzeichenbehaftete Achsen",
|
||||
"Size": "Größe",
|
||||
"Sketch constraints": "Skizzenbedingungen",
|
||||
@@ -432,9 +435,11 @@
|
||||
"Specialise": "Spezialisieren",
|
||||
"Speed": "Geschwindigkeit",
|
||||
"Splitting tool": "Trennwerkzeug",
|
||||
"Square": "Quadrat",
|
||||
"Stand it up": "Aufrichten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Aufrichten: eine Vierteldrehung um X, als ein Verschieben-Merkmal, das sich rückgängig machen lässt.",
|
||||
"Stand this part up?": "Dieses Teil aufrichten?",
|
||||
"Start angle": "Startwinkel",
|
||||
"Start bulge": "Startausbauchung",
|
||||
"Start continuity": "Startstetigkeit",
|
||||
"Step": "Schritt",
|
||||
@@ -520,6 +525,7 @@
|
||||
"angle, height": "Winkel, Höhe",
|
||||
"appearance.dielectric_ior": "Brechungsindex des Dielektrikums",
|
||||
"appearance.metallic": "Metallanteil",
|
||||
"appearance.only_available": "Nur verfügbare Erscheinungsbilder",
|
||||
"appearance.pbr_help": "Diese Oberfläche in Render oder einem Produkt-PNG ansehen. Keine Brechung oder Transmission.",
|
||||
"appearance.pbr_opaque": "Opakes PBR",
|
||||
"appearance.roughness": "Rauheit",
|
||||
@@ -625,6 +631,7 @@
|
||||
"cmd.document_metadata": "Konstruktionseigenschaften",
|
||||
"cmd.document_metadata_remove": "Konstruktionseigenschaft entfernen",
|
||||
"cmd.document_metadata_set": "Konstruktionseigenschaften ändern",
|
||||
"cmd.document_revision": "Konstruktionsrevision",
|
||||
"cmd.document_units": "Einheiten der Konstruktion ändern",
|
||||
"cmd.edit_checkpoint": "Prüfpunkt",
|
||||
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
|
||||
@@ -641,6 +648,10 @@
|
||||
"cmd.file_insert_step": "STEP einfügen",
|
||||
"cmd.file_save_mesh": "Als Netz speichern",
|
||||
"cmd.file_slicers": "Installierte Slicer",
|
||||
"cmd.frame_member": "Rahmenelement",
|
||||
"cmd.frame_profile_insert": "Profil einfügen",
|
||||
"cmd.frame_profile_list": "Profilbibliothek",
|
||||
"cmd.frame_profile_properties": "Profileigenschaften",
|
||||
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
|
||||
"cmd.inspect_clearance": "Freiraum",
|
||||
@@ -680,6 +691,7 @@
|
||||
"cmd.material_create": "Neues physikalisches Material",
|
||||
"cmd.material_delete": "Physisches Material löschen",
|
||||
"cmd.material_edit": "Physisches Material bearbeiten",
|
||||
"cmd.material_inspect": "Physikalisches Material prüfen",
|
||||
"cmd.material_list": "Materialien auflisten",
|
||||
"cmd.material_properties_directional": "Richtungsabhängiger Elastizitätsmodul",
|
||||
"cmd.material_properties_validate": "Physikalische Materialdaten prüfen",
|
||||
@@ -726,13 +738,32 @@
|
||||
"cmd.plastic_rules": "Kunststoffregeln auflisten",
|
||||
"cmd.plastic_snap_fit": "Rastverbindung",
|
||||
"cmd.render_backends": "Render-Backends",
|
||||
"cmd.render_camera_fit": "Renderobjekt einpassen",
|
||||
"cmd.render_camera_get": "Renderkamera prüfen",
|
||||
"cmd.render_cancel": "Rendering abbrechen",
|
||||
"cmd.render_export": "PNG exportieren",
|
||||
"cmd.render_hdr_inspect": "HDR-Quelle untersuchen",
|
||||
"cmd.render_library_appearance_apply": "Opakes Bibliotheksaussehen anwenden",
|
||||
"cmd.render_library_describe": "Render-Ressource untersuchen",
|
||||
"cmd.render_library_environment_apply": "Integrierte HDR-Umgebung anwenden",
|
||||
"cmd.render_library_list": "Render-Bibliothek durchsuchen",
|
||||
"cmd.render_library_physical_prepare": "Prepare Physical Reference",
|
||||
"cmd.render_library_studio_apply": "Erstellte Studiolichter anwenden",
|
||||
"cmd.render_library_studio_prepare": "Erstellte Studiolichter vorbereiten",
|
||||
"cmd.render_library_surface_bind": "Save Surface Binding",
|
||||
"cmd.render_library_surface_prepare": "Erstellte Oberflächenmaps vorbereiten",
|
||||
"cmd.render_library_surface_unbind": "Remove Surface Binding",
|
||||
"cmd.render_library_targets": "Ziele für Render-Ressourcen suchen",
|
||||
"cmd.render_preflight": "Renderanfrage prüfen",
|
||||
"cmd.render_result_pin": "Angezeigtes Render-Ergebnis behalten",
|
||||
"cmd.render_scene": "Renderszene prüfen",
|
||||
"cmd.render_settings_apply": "Renderkamera anwenden",
|
||||
"cmd.render_settings_capture_hdr": "Render-Einstellungen mit HDR speichern",
|
||||
"cmd.render_settings_get": "Rendereinstellungen",
|
||||
"cmd.render_settings_set": "Rendereinstellungen speichern",
|
||||
"cmd.render_stage_backdrop": "Physischen Renderhintergrund speichern",
|
||||
"cmd.render_stage_clear": "Remove Render Stage",
|
||||
"cmd.render_stage_ground": "Save Render Ground",
|
||||
"cmd.render_start": "Bild rendern",
|
||||
"cmd.render_status": "Renderfortschritt",
|
||||
"cmd.select_clear": "Auswahl aufheben",
|
||||
@@ -1054,6 +1085,8 @@
|
||||
"manage.material_hint": "Wählen Sie Körper und deren physikalisches Material aus.",
|
||||
"manage.parameters_hint": "Bearbeiten Sie Ausdrücke und Einheiten. Änderungen werden mit der Eingabetaste oder beim Verlassen eines Feldes übernommen.",
|
||||
"manage.rules_hint": "Wählen Sie eine Regel zum Bearbeiten oder erstellen Sie eine neue. Bestätigen Sie, um die Regel zu speichern.",
|
||||
"material.editor.axes_numeric": "1 / 2 / 3",
|
||||
"material.editor.axes_wood": "L / R / T",
|
||||
"measure.at": "Bei",
|
||||
"measure.item": "Element {number}",
|
||||
"messages.ago_h": "vor {n} Std.",
|
||||
@@ -1072,6 +1105,7 @@
|
||||
"motion.key_here": "Schlüsselbild hier setzen",
|
||||
"motion.key_here_hint": "Ein Schlüsselbild bei diesem Schritt mit dem aktuellen Gelenkwert hinzufügen",
|
||||
"motion.no_collisions": "Keine Kollisionen in der Untersuchung",
|
||||
"motion.plot_quantity": "Größe",
|
||||
"motion.step_back": "Schritt zurück",
|
||||
"motion.step_forward": "Schritt vor",
|
||||
"motion.to_end": "Zum Ende",
|
||||
@@ -1084,6 +1118,7 @@
|
||||
"nav.fusion.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, rechte Maustaste dreht",
|
||||
"nav.inventor.help": "Mittlere Maustaste verschiebt, Umschalt+Mitte dreht, Strg+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.openscad.help": "Linksziehen auf leerem Hintergrund dreht, Umschalt+Linksziehen zieht ein Auswahlrechteck auf, Skizzengeometrie wird durch Ziehen bearbeitet; rechtes oder mittleres Ziehen verschiebt, Umschalt+rechtes oder Umschalt+mittleres Ziehen zoomt",
|
||||
"nav.perspective.sketch": "Beim Bearbeiten einer Skizze wird die orthografische Ansicht verwendet. Beenden Sie die Skizze, um die Perspektive umzuschalten.",
|
||||
"nav.solidworks.help": "Mittlere Maustaste dreht, Strg+Mitte verschiebt, Umschalt+Mitte zoomt, rechte Maustaste dreht",
|
||||
"nav.tinkercad.help": "Rechte Maustaste dreht, mittlere oder Umschalt+rechte Maustaste verschiebt",
|
||||
"nav.touchpad.help": "Umschalt gedrückt halten und den Zeiger zum Verschieben bewegen; mit Alt/Option drehen. Keine Maustaste nötig. Vertikales Scrollen zoomt.",
|
||||
@@ -1117,13 +1152,35 @@
|
||||
"recovery.just_now": "gerade eben",
|
||||
"recovery.min_ago": "vor {count} Min.",
|
||||
"recovery.never_saved": "nie gespeichert",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
"render.background": "Szenenhintergrund",
|
||||
"render.bloom_enable": "Darstellungs-Bloom",
|
||||
"render.bloom_help": "Kreativer Ausgabeglanz aus linearen Glanzlichtern mit drei normierten Durchläufen. Radius in Ausgabepixeln; Addition vor sRGB. Keine Linsen-, Volumen- oder Konvergenzsimulation. Das rohe EXR bleibt unverändert.",
|
||||
"render.bloom_opaque": "Bloom benötigt eine deckende Ausgabe. Transparentes PNG wird abgelehnt, nicht still abgeflacht.",
|
||||
"render.bloom_radius": "Bloom-Radius",
|
||||
"render.bloom_strength": "Bloom-Stärke",
|
||||
"render.bloom_threshold": "Linearer Glanzlichtschwellwert",
|
||||
"render.camera_pitch": "Kameraneigung",
|
||||
"render.camera_set": "Render-Kamera einstellen",
|
||||
"render.clear_stage": "Remove saved stage",
|
||||
"render.cpu_budget": "CPU-Ressourcenprofil",
|
||||
"render.cpu_budget_bodies": "Körperobergrenze",
|
||||
"render.cpu_budget_compact": "Kompakt (bisherige Grenzen)",
|
||||
"render.cpu_budget_details": "Ressourcengrenzen",
|
||||
"render.cpu_budget_help": "Größere Grenzen ausdrücklich wählen. Auflösung und Samples werden nie automatisch reduziert.",
|
||||
"render.cpu_budget_memory": "Speicherobergrenze pro Auftrag",
|
||||
"render.cpu_budget_not_guarantee": "Dies sind Zulassungsgrenzen, keine RAM-Reservierung oder Geschwindigkeitsgarantie. Geometrie, Film und Vorschauen teilen das Speicherbudget. Das Rendern bleibt abbrechbar.",
|
||||
"render.cpu_budget_resize": "Der Entwurf überschreitet das CPU-Profil oder sein Kamera-Sample-Budget. Kleinere Abmessungen ausdrücklich bestätigen oder ein größeres Profil wählen.",
|
||||
"render.cpu_budget_studio": "Studio (große Bilder)",
|
||||
"render.cpu_budget_triangles": "Dreiecksobergrenze",
|
||||
"render.cpu_budget_workstation": "Workstation (komplexe Szenen)",
|
||||
"render.exr_pending": "Die GPU kann rohe lineare EXR-Dateien behalten. Kompression, mehrschichtige Bilder und zusätzliche Renderpässe bleiben nicht verfügbar.",
|
||||
"render.fit": "Produkt einpassen",
|
||||
"render.image_cancelled": "Rendering abgebrochen",
|
||||
"render.image_cancelling": "Wird abgebrochen",
|
||||
"render.image_completed": "Bild bereit zum Export",
|
||||
"render.image_encoding": "PNG wird kodiert",
|
||||
"render.image_export": "PNG exportieren",
|
||||
"render.image_export": "Fertiges Bild exportieren",
|
||||
"render.image_failed": "Rendering fehlgeschlagen",
|
||||
"render.image_height": "Höhe",
|
||||
"render.image_help": "CPU-Vorschaubild · PNG · angenäherte Transparenz",
|
||||
@@ -1141,17 +1198,98 @@
|
||||
"render.image_unknown": "Renderstatus nicht verfügbar",
|
||||
"render.image_use_largest": "Größte Größe mit diesem Seitenverhältnis verwenden:",
|
||||
"render.image_width": "Breite",
|
||||
"render.include_rig_cards": "Include passive cards and flags",
|
||||
"render.library_applied": "Ressourcenvariante angewendet. Vollständigen Renderauftrag vor dem Start prüfen.",
|
||||
"render.library_apply_hdr": "Mit dieser HDR-Umgebung speichern",
|
||||
"render.library_apply_maps": "Erstellte GPU-Oberflächenmaps verwenden",
|
||||
"render.library_apply_studio": "Erstellte Studiovariante speichern",
|
||||
"render.library_apply_uniform": "Einheitliches opakes Aussehen anwenden",
|
||||
"render.library_authored_help": "Original erstellte Ressourcen dienen der Darstellung, nicht als gemessene technische Daten. Jede Aktion wendet nur ihre angegebene Variante an.",
|
||||
"render.library_blocked": "Diese Ressource benötigt noch Kalibrierung oder eine externe Sonne und kann noch nicht angewendet werden.",
|
||||
"render.library_center": "Mittelpunkt des Lichtaufbaus",
|
||||
"render.library_emitters_only_ack": "Nur Strahler verwenden; dokumentierte passive Karten auslassen",
|
||||
"render.library_environment_concepts": "Umgebungskonzepte",
|
||||
"render.library_extent": "Objektausdehnung",
|
||||
"render.library_hdr": "Integrierte HDR-Umgebungen",
|
||||
"render.library_hdr_help": "Bettet das originale HDR in die gespeicherte Einstellung ein. Der Längengradursprung wird ausdrücklich umgerechnet; nach dem Speichern ist keine externe Datei nötig.",
|
||||
"render.library_light_concepts": "Beleuchtungskonzepte",
|
||||
"render.library_maps_help": "Built-in maps can be saved with pinned sources and local occurrence mapping. Temporary optical overrides remain a separate mode.",
|
||||
"render.library_material_concepts": "Materialkonzepte",
|
||||
"render.library_next": "Nächste Seite",
|
||||
"render.library_physical_profiles": "Physical reference profiles",
|
||||
"render.library_previous": "Vorherige Seite",
|
||||
"render.library_quality_concepts": "Qualitätsspezifikationen",
|
||||
"render.library_replace_environment": "Vorhandene Umgebung ausdrücklich ersetzen",
|
||||
"render.library_replace_studio": "Replace saved lighting, environment and stage",
|
||||
"render.library_results": "Passende Ressourcen",
|
||||
"render.library_rig_help": "Fit once at an explicit center and physical extent. Complete rigs include passive cards; calibration blockers still refuse. Conservative emitter/card clearance is checked before saving.",
|
||||
"render.library_rigs": "Erstellte Lichtaufbauten",
|
||||
"render.library_save_maps": "Save maps to this setup",
|
||||
"render.library_scene_bounds": "Erfasste Szenengrenzen verwenden",
|
||||
"render.library_scenes": "Szenenspezifikationen",
|
||||
"render.library_search": "Bibliothek durchsuchen",
|
||||
"render.library_selene": "SELENE: opake Oberflächen",
|
||||
"render.library_specification_only": "Nur Spezifikation: keine vollständig ausführbare Voreinstellung. Erforderliche Effekte, Ressourcen und Objektrollen müssen noch implementiert und gebunden werden.",
|
||||
"render.library_surfaces": "Erstellte PBR-Oberflächen",
|
||||
"render.library_target": "Zielkörper",
|
||||
"render.library_title": "Render-Ressourcenbibliothek",
|
||||
"render.library_uniform_ack": "Einheitliche Näherung ohne räumliche oder erweiterte Effekte verwenden",
|
||||
"render.material_color_replace": "Basismap ersetzt Körperfarbe",
|
||||
"render.material_limits": "Höchstens 128 Körperüberschreibungen, 32 getrennte Glaskörper, 64 Beschichtungen und 128 Texturbindungen. Identische Maps teilen Speicher; Geräte- und Gesamtspeichergrenzen bleiben aktiv.",
|
||||
"render.material_metallic_map": "Metallanteil-Map (linear)",
|
||||
"render.material_roughness_map": "Rauheitsmap (linear)",
|
||||
"render.material_v_up": "Textur-V zeigt im Bild nach oben",
|
||||
"render.navigation_help": "Die Kamera bewegt sich wie im Konstruieren (Einstellungen › Navigation). Setup speichern übernimmt die aktuelle Kamera.",
|
||||
"render.opaque_image_summary": "Vorschau-PNG · opakes PBR · Transparenz durch Blending",
|
||||
"render.opaque_preview_help": "Opakes PBR und einfache Farben; Transparenz durch Blending. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.output_format_status": "PNG enthält das exakte fertige Anzeigebild. Optionales GPU-EXR bewahrt rohe lineare FLOAT32-Strahldichte vor Filtern. Export ersetzt nie durch Vorschaucache oder anderen Auftrag.",
|
||||
"render.passive_stage": "Render-only stage",
|
||||
"render.passive_stage_help": "Finite passive panels belong only to rendered images. They do not add CAD bodies or mass. Placement is fixed after applying; intersecting product bounds refuse. CPU does not provide diffuse bounce lighting.",
|
||||
"render.perspective": "Perspektivische Kamera",
|
||||
"render.physical_acknowledge": "I understand this partial reference needs grade, condition and source review",
|
||||
"render.physical_associations": "Physical candidates and roles",
|
||||
"render.physical_copy_plan": "Copy plan with sources and omissions",
|
||||
"render.physical_material_name": "Proposed reference material name",
|
||||
"render.physical_omissions": "Properties not representable in the native card",
|
||||
"render.physical_plan_only": "Read-only plan prepared. No material was created or assigned. Keep its extended profile and omissions when reviewing the native card.",
|
||||
"render.physical_prepare_plan": "Prepare creation plan (does not apply)",
|
||||
"render.physical_separate": "Appearance is not material identification. These are sourced references, not certified effective properties. Existing mass, engineering materials and optics are unchanged.",
|
||||
"render.physical_unknown": "No numerical candidate is established. Grade, formulation or construction must be specified; unknown is not zero.",
|
||||
"render.preview": "Integrierte Vorschau",
|
||||
"render.preview_help": "Rasterdarstellung mit gemischter Transparenz. Brechung, HDRI und finales Rendering sind noch nicht verfügbar.",
|
||||
"render.reload_settings": "Gespeicherte Einstellungen laden",
|
||||
"render.replace_saved_surface": "Replace an existing saved binding for this target",
|
||||
"render.restore_camera": "Gespeicherte Kamera laden",
|
||||
"render.result_reduced_display": "Anzeigevorschau auf die Geräte-/Cachegrenze reduziert. PNG und Export behalten die volle Auflösung; native 100%-Ansicht ist hier nicht verfügbar.",
|
||||
"render.return_design": "Zurück zu Design",
|
||||
"render.ribbon_backgrounds": "Hintergründe",
|
||||
"render.ribbon_check": "Prüfen",
|
||||
"render.ribbon_check_help": "Genau den aktuellen Renderentwurf prüfen. Kein stiller Rendererwechsel, keine Verkleinerung oder Ersetzung.",
|
||||
"render.ribbon_edit_lights": "Licht ändern",
|
||||
"render.ribbon_environment_help": "HDR-Beleuchtung, Kamerahintergründe und physische Bühnen sind getrennte Einstellungen mit ausdrücklicher Ersetzungsbestätigung.",
|
||||
"render.ribbon_finish_dialog": "Zuerst den Datei- oder Bestätigungsdialog abschließen.",
|
||||
"render.ribbon_frame": "Einpassen",
|
||||
"render.ribbon_hdr_library": "HDR-Bibliothek",
|
||||
"render.ribbon_job_active": "Ein Render läuft bereits; vor einem neuen Auftrag ausdrücklich abbrechen.",
|
||||
"render.ribbon_light_rigs": "Lichtaufbauten",
|
||||
"render.ribbon_lights_help": "Lichtaufbauten prüfen oder unterstützte Lichter bearbeiten; erst ausdrücklich Anwenden ersetzt die Einrichtung.",
|
||||
"render.ribbon_materials": "Materialien",
|
||||
"render.ribbon_materials_help": "Gemeinsamen Erscheinungsinspektor und Originaloberflächen öffnen. Optik ändert keine technischen Eigenschaften stillschweigend.",
|
||||
"render.ribbon_no_active_job": "Kein aktiver Render zum Abbrechen.",
|
||||
"render.ribbon_no_result": "Vor dem Export muss ein Render abgeschlossen sein.",
|
||||
"render.ribbon_no_saved_camera": "Vor dem Wiederherstellen eine Aufnahme speichern.",
|
||||
"render.ribbon_saved_camera": "Gespeicherte Kamera",
|
||||
"render.ribbon_shots": "Aufnahmen",
|
||||
"render.ribbon_studios": "Studios",
|
||||
"render.ribbon_studios_help": "Implementierte Studios durchsuchen und Ersetzungen vor dem Anwenden prüfen. Konzepte bleiben gekennzeichnet.",
|
||||
"render.ribbon_surface_maps": "Oberflächen",
|
||||
"render.save_setup": "Setup speichern",
|
||||
"render.saved_maps_runtime_conflict": "Saved surface bindings cannot be mixed with temporary optical overrides or an unsaved HDR. Save the HDR or remove the temporary inputs explicitly.",
|
||||
"render.saved_surface_remove": "Remove saved binding",
|
||||
"render.saved_surfaces": "Saved surface bindings",
|
||||
"render.saved_surfaces_help": "Pinned built-in maps follow the selected occurrence in local millimeters. A replaced or missing target is an error, never an automatic reassignment.",
|
||||
"render.setup_name": "Name des Setups",
|
||||
"render.stage_panel_count": "Saved panels",
|
||||
"render.unavailable_preview": "Dieses Setup verlangt ein nicht verfügbares Vorschauprofil.",
|
||||
"render.unsaved": "Ungespeicherte Vorschauänderungen",
|
||||
"render.use_preview": "Integrierte Vorschau verwenden",
|
||||
@@ -1187,6 +1325,8 @@
|
||||
"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.reference_geometry_through_bodies": "Referenzgeometrie durch Körper hindurch anzeigen",
|
||||
"settings.reference_geometry_through_bodies_help": "Konturen inaktiver Skizzen und Bezugsebenen über schattierten Körpern zeichnen",
|
||||
"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",
|
||||
"settings.supersampling_note": "Zeichnet die Ansicht in höherer Auflösung und rechnet sie herunter: glattere Kanten, Linien und Raster auf Bildschirmen unter 4K. Kostet GPU-Zeit: etwa 2,25-mal so viele Pixel bei 1,5× und 4-mal bei 2×.",
|
||||
@@ -1226,6 +1366,7 @@
|
||||
"thread.designation": "Bezeichnung",
|
||||
"thread.iso_metric": "ISO metrisch",
|
||||
"thread.select_size": "Größe wählen",
|
||||
"timeline.feature_hint": "Doppelklicken zum Bearbeiten · Ziehen zum Neuordnen · Rechtsklicken für weitere Optionen",
|
||||
"timeline.group": "Neuen Merkmalsordner anlegen",
|
||||
"timeline.move_many": "Zeitleistenelemente verschieben",
|
||||
"timeline.move_to_group": "Merkmale in Ordner verschieben",
|
||||
@@ -1237,12 +1378,115 @@
|
||||
"toolbar.customize_note": "Registerkarte {tab}. Markieren Sie einen Befehl, um ihm eine Schaltfläche in der Werkzeugleiste zu geben. Sie können auch eine Zeile aus dem Aufklappmenü eines Panels auf die Werkzeugleiste ziehen, eine Schaltfläche zum Entfernen aus der Werkzeugleiste ziehen oder die Stecknadel einer Menüzeile verwenden.",
|
||||
"toolbar.image": "BILD",
|
||||
"toolbar.render": "RENDERN",
|
||||
"toolbar.render_appearance": "ERSCHEINUNG",
|
||||
"toolbar.render_camera": "KAMERA",
|
||||
"toolbar.render_environment": "UMGEBUNG",
|
||||
"toolbar.render_lighting": "BELEUCHTUNG",
|
||||
"toolbar.render_scene": "SZENE",
|
||||
"trace.points": "{count} Punkte entlang der Bahn",
|
||||
"trace.stopped_by_contact": "Endet bei Probe {sample}: {a} würde {b} durchdringen",
|
||||
"trace.stopped_by_loop": "Endet bei Probe {sample}: die Schleife über {joint} lässt sich nicht weiter schließen",
|
||||
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
|
||||
"ui.3d_mouse_axes_reassigned": "Die Achsen sind in den Einstellungen neu zugeordnet, und das gilt auch nach dieser Kalibrierung.",
|
||||
"ui.3d_mouse_button": "Taste {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nichts",
|
||||
"ui.3d_mouse_button_settings": "3D-Maus-Einstellungen",
|
||||
"ui.3d_mouse_buttons": "Tasten",
|
||||
"ui.3d_mouse_buttons_hint": "Drücken Sie eine Taste an Ihrer 3D-Maus, um sie hier zu finden, und wählen Sie dann, was sie tut.",
|
||||
"ui.3d_mouse_buttons_no_device": "Schließen Sie eine 3D-Maus an, um ihre Tasten zu belegen.",
|
||||
"ui.3d_mouse_buttons_reset": "Standardbelegung wiederherstellen",
|
||||
"ui.3d_mouse_calibrate": "Kalibrieren…",
|
||||
"ui.3d_mouse_calibrate_tip": "Bewegen Sie die Kappe so, wie das Fenster es verlangt. So lernt SolidCraft, in welche Richtung und wie weit jede Achse dieser 3D-Maus geht. Wird für diese 3D-Maus gespeichert.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-Maus kalibrieren",
|
||||
"ui.3d_mouse_calibrate_wait": "Bewegen Sie die 3D-Maus einmal, damit SolidCraft sie erkennt.",
|
||||
"ui.3d_mouse_calibrated": "Kalibriert",
|
||||
"ui.3d_mouse_calibration": "3D-Maus-Kalibrierung",
|
||||
"ui.3d_mouse_calibration_done": "Fertig. Speichern Sie die Kalibrierung, damit sie immer gilt, wenn diese 3D-Maus angeschlossen ist.",
|
||||
"ui.3d_mouse_calibration_no_response": "Eine Achse hat sich kaum bewegt und lässt sich nicht kalibrieren. Beginnen Sie von vorn und bewegen Sie die Kappe jedes Mal weiter.",
|
||||
"ui.3d_mouse_circles_help": "Die Kreise zeigen, was das Gerät nach der Kalibrierung sendet. Das Testmodell zeigt, was die Ansicht daraus macht, nach Umkehren, Geschwindigkeit und Achsenzuordnung.",
|
||||
"ui.3d_mouse_dead_zone": "Totzone",
|
||||
"ui.3d_mouse_dead_zone_tip": "Wie weit sich die Kappe bewegen muss, bevor sich die Ansicht bewegt, als Anteil ihres vollen Wegs. Erhöhen Sie sie, wenn die Ansicht wandert, obwohl die Kappe ruht.",
|
||||
"ui.3d_mouse_denied": "SolidCraft sieht eine 3D-Maus, darf sie aber nicht lesen. Die udev-Regel steht in den Einrichtungshinweisen zur 3D-Maus.",
|
||||
"ui.3d_mouse_denied_short": "Gefunden, aber nicht lesbar",
|
||||
"ui.3d_mouse_driven_by": "Welche kalibrierte Bewegung des Geräts diese Achse steuert. Wählen Sie eine, die schon eine andere Achse nutzt, werden die beiden getauscht.",
|
||||
"ui.3d_mouse_driven_by_label": "gesteuert von",
|
||||
"ui.3d_mouse_driver": "3D-Maus-Treiber",
|
||||
"ui.3d_mouse_driver_auto": "Automatisch",
|
||||
"ui.3d_mouse_driver_direct": "Direkt (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Wird vorerst über spacenavd gelesen: das Gerät selbst ließ sich nicht öffnen.",
|
||||
"ui.3d_mouse_driver_only_direct": "Das Gerät wird direkt gelesen; spacenavd läuft nicht",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lesen über spacenavd, die einzige verfügbare Möglichkeit",
|
||||
"ui.3d_mouse_forget": "Kalibrierung verwerfen",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Ansicht links",
|
||||
"ui.3d_mouse_ghost_left_window": "Fenster links",
|
||||
"ui.3d_mouse_ghost_position": "Position",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Ansicht rechts",
|
||||
"ui.3d_mouse_hint": "Ansicht gleichzeitig verschieben und drehen; Skizzen verwenden nur Schwenken und Zoomen.",
|
||||
"ui.3d_mouse_kind_cap": "Zurück zur Mitte",
|
||||
"ui.3d_mouse_kind_question": "Was für eine 3D-Maus ist es?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Der Wert bleibt stehen, solange Sie halten, wie bei einem Gerät vom Typ „Zurück zur Mitte“.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Der Wert fällt zurück, sobald Sie aufhören, wie bei einem Trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translation X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.3d_mouse_navigate": "Mit der 3D-Maus navigieren",
|
||||
"ui.3d_mouse_none": "Keine 3D-Maus gefunden",
|
||||
"ui.3d_mouse_not_calibrated": "Nicht kalibriert",
|
||||
"ui.3d_mouse_offer": "Die Kalibrierung Ihrer 3D-Maus behebt vertauschte oder umgekehrte Achsen und stellt Geschwindigkeit und Gefühl ein.",
|
||||
"ui.3d_mouse_offer_never": "Nicht mehr anzeigen",
|
||||
"ui.3d_mouse_offer_swap": "Zu Hidraw wechseln (direkter Treiber)",
|
||||
"ui.3d_mouse_reading": "Werte",
|
||||
"ui.3d_mouse_reset_axes": "Achsenzuordnung zurücksetzen",
|
||||
"ui.3d_mouse_reset_view": "Ansicht zurücksetzen",
|
||||
"ui.3d_mouse_restore": "3D-Maus-Standardwerte wiederherstellen",
|
||||
"ui.3d_mouse_restore_ask": "3D-Maus-Einstellungen auf die Standardwerte zurücksetzen? Kalibrierung und Tastenbelegung bleiben.",
|
||||
"ui.3d_mouse_section": "3D-Maus",
|
||||
"ui.3d_mouse_show_in_view": "3D-Maus in der 3D-Ansicht anzeigen",
|
||||
"ui.3d_mouse_show_in_view_tip": "Ein Bild Ihrer 3D-Maus unten links, das sich mit ihr bewegt.",
|
||||
"ui.3d_mouse_skip": "Mein Gerät kann das nicht",
|
||||
"ui.3d_mouse_skip_tip": "Überspringt diese Bewegung in beide Richtungen; die Kalibrierung lässt sie aus.",
|
||||
"ui.3d_mouse_speed": "Geschwindigkeit der 3D-Maus",
|
||||
"ui.3d_mouse_step_lift": "Kappe nach oben ziehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_press": "Kappe nach unten drücken, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_pull": "Kappe zu sich heranziehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_push": "Kappe von sich wegschieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_rest": "Nehmen Sie die Hand von der Kappe und lassen Sie sie ruhen.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Nehmen Sie die Hand von der Kugel und lassen Sie sie ruhen.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Drehen Sie den Scrollring ein paar Rasten im Uhrzeigersinn.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Drehen Sie den Scrollring ein paar Rasten gegen den Uhrzeigersinn.",
|
||||
"ui.3d_mouse_step_roll_away": "Rollen Sie die Kugel ein paarmal von sich weg und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_left": "Rollen Sie die Kugel ein paarmal nach links und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_right": "Rollen Sie die Kugel ein paarmal nach rechts und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_roll_toward": "Rollen Sie die Kugel ein paarmal zu sich hin und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_slide_left": "Kappe nach links schieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_slide_right": "Kappe nach rechts schieben, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Drehen Sie die Kugel ein paarmal im Uhrzeigersinn und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Drehen Sie die Kugel ein paarmal gegen den Uhrzeigersinn und lassen Sie sie dann auslaufen.",
|
||||
"ui.3d_mouse_step_tilt_away": "Oberseite der Kappe von sich weg kippen (die Ihnen nächste Kante geht nach oben), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_left": "Kappe nach links kippen (linke Kante runter), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_right": "Kappe nach rechts kippen (rechte Kante runter), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Oberseite der Kappe zu sich kippen (die Ihnen nächste Kante geht nach unten), halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_touch": "Legen Sie die Hand auf die Kappe, ohne zu drücken.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Kappe im Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Kappe gegen den Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.3d_mouse_switch_cap": "Als Zurück zur Mitte kalibrieren",
|
||||
"ui.3d_mouse_switch_trackball": "Als Trackball kalibrieren",
|
||||
"ui.3d_mouse_view_axis": "Ansichtsachse",
|
||||
"ui.3d_mouse_view_off": "Aus",
|
||||
"ui.3d_mouse_view_off_tip": "Das Profil des 3D-Maus-Treibers schaltet diese Achse ab, daher bewegt ihre Bewegung die Ansicht nicht. Wählen Sie hier die Bewegung einer anderen Achse, werden die beiden getauscht und sie bewegt die Ansicht nicht mehr.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit links / rechts",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit hoch / runter",
|
||||
"ui.3d_mouse_view_pan_sideways": "Schwenken links / rechts",
|
||||
"ui.3d_mouse_view_pan_up_down": "Schwenken hoch / runter",
|
||||
"ui.3d_mouse_view_roll": "Rollen",
|
||||
"ui.3d_mouse_view_unmapped": "Achse {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Warte auf Bewegung",
|
||||
"ui.3d_mouse_wiggle": "Falls eine angeschlossen ist, bewegen Sie sie kurz.",
|
||||
"ui.3d_sketch": "3D-Skizze",
|
||||
"ui.activate": "Aktivieren",
|
||||
"ui.active_component": "Aktive Komponente",
|
||||
@@ -1400,6 +1644,7 @@
|
||||
"ui.ai_http_busy": "der Endpunkt ist ausgelastet oder am Limit; versuchen Sie es später",
|
||||
"ui.ai_http_key": "der Endpunkt hat den Schlüssel abgelehnt: prüfen Sie ihn unter Einstellungen > KI",
|
||||
"ui.ai_http_not_found": "Endpunkt oder Modell nicht gefunden: prüfen Sie Basis-URL und Modellnamen",
|
||||
"ui.ai_http_too_large": "die Unterhaltung ist für diesen Endpunkt zu groß; beginnen Sie einen neuen Chat",
|
||||
"ui.ai_key": "API-Schlüssel",
|
||||
"ui.ai_key_hint": "optional; im Schlüsselbund des Systems gespeichert",
|
||||
"ui.ai_key_needs_https": "Ein Schlüssel wird nur über HTTPS oder an diesen Computer gesendet.",
|
||||
@@ -1427,7 +1672,6 @@
|
||||
"ui.ai_permissions": "Berechtigungen",
|
||||
"ui.ai_preset": "Vorlage",
|
||||
"ui.ai_remove": "Entfernen",
|
||||
"ui.ai_request_limit": "nach 24 Anfragen für eine Nachricht angehalten; senden Sie eine weitere Nachricht, um fortzufahren",
|
||||
"ui.ai_reset": "Zurücksetzen",
|
||||
"ui.ai_settings": "KI-Einstellungen…",
|
||||
"ui.ai_show_features": "KI-Funktionen anzeigen",
|
||||
@@ -1472,6 +1716,7 @@
|
||||
"ui.blocked": "Verdeckt",
|
||||
"ui.bodies": "Körper",
|
||||
"ui.bodies_all_if_none": "Körper (alle, wenn keine)",
|
||||
"ui.bottom": "Unten",
|
||||
"ui.box": "Block",
|
||||
"ui.break": "Unterbrechen",
|
||||
"ui.browser_bodies_one": "{count} Körper",
|
||||
@@ -1606,6 +1851,8 @@
|
||||
"ui.filter_commands_or_keys": "Befehle oder Tasten filtern",
|
||||
"ui.findInTimeline": "In der Zeitleiste suchen",
|
||||
"ui.findInWindow": "Im Fenster suchen",
|
||||
"ui.fine": "Fein",
|
||||
"ui.finest": "Sehr fein",
|
||||
"ui.finish_sketch": "Skizze beenden",
|
||||
"ui.fit": "Einpassen",
|
||||
"ui.fix_unfix": "Fixieren/Lösen",
|
||||
@@ -1694,6 +1941,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "WERKZEUGE",
|
||||
"ui.heading_utility": "HILFSMITTEL",
|
||||
"ui.heading_weldments": "SCHWEISSTEILE",
|
||||
"ui.help": "Hilfe",
|
||||
"ui.hide": "Ausblenden",
|
||||
"ui.hideAxes": "Achsen ausblenden",
|
||||
@@ -1788,6 +2036,7 @@
|
||||
"ui.one_side": "Einseitig",
|
||||
"ui.open": "Öffnen",
|
||||
"ui.open_ends_tip": "Berührt nichts",
|
||||
"ui.open_linked_design": "Verknüpftes Design öffnen",
|
||||
"ui.open_preferences": "Einstellungen öffnen",
|
||||
"ui.open_recent": "Zuletzt verwendet",
|
||||
"ui.operation": "Operation",
|
||||
@@ -1834,6 +2083,7 @@
|
||||
"ui.project_face_edges_sketch": "Flächenkanten in neue Skizzen projizieren",
|
||||
"ui.properties": "Eigenschaften",
|
||||
"ui.pull_direction": "Entformungsrichtung",
|
||||
"ui.quality": "Qualität",
|
||||
"ui.quantity": "Anzahl",
|
||||
"ui.quantity_2": "Anzahl 2",
|
||||
"ui.quit": "Beenden",
|
||||
@@ -1894,6 +2144,12 @@
|
||||
"ui.sent_to_slicer": "An {slicer} gesendet",
|
||||
"ui.sheet_gap": "Spalt",
|
||||
"ui.shell": "Hülle",
|
||||
"ui.shortcut_capture_add_key": "Eine normale Taste hinzufügen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_hint": "Eine Belegung anklicken, dann Modifikatortasten und eine Taste drücken. Weitere Modifikatoren innerhalb einer Sekunde hinzufügen; zum Bestätigen alle Tasten loslassen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_press": "Modifikatortasten und eine Taste drücken. Escape bricht ab.",
|
||||
"ui.shortcut_capture_release": "Zum Bestätigen alle Tasten loslassen. Escape bricht ab.",
|
||||
"ui.shortcut_capture_settling": "Tastenkürzel wird bestätigt… Escape bricht ab.",
|
||||
"ui.shortcut_multiple_keys": "Modifikatortasten und nur eine weitere Taste verwenden. Zum Wiederholen die Belegung anklicken.",
|
||||
"ui.show": "Einblenden",
|
||||
"ui.showAll": "Alle einblenden",
|
||||
"ui.showAllBodies": "Alle Volumenkörper einblenden",
|
||||
@@ -1923,65 +2179,13 @@
|
||||
"ui.smooth_wheel_zoom": "Sanftes Zoomen mit dem Rad",
|
||||
"ui.smooth_wheel_zoom_tip": "Ausschalten, um jedes Radereignis sofort anzuwenden, ohne Nachlauf",
|
||||
"ui.snap": "Einrasten",
|
||||
"ui.space_mouse_axes_reassigned": "Die Achsen sind in den Einstellungen neu zugeordnet, und das gilt auch nach dieser Kalibrierung.",
|
||||
"ui.space_mouse_calibrate": "Kalibrieren…",
|
||||
"ui.space_mouse_calibrate_tip": "Bewegen Sie die Kappe so, wie das Fenster es verlangt. So lernt SolidCraft, in welche Richtung und wie weit jede Achse dieser 3D-Maus geht. Wird für diese 3D-Maus gespeichert.",
|
||||
"ui.space_mouse_calibrate_title": "3D-Maus kalibrieren",
|
||||
"ui.space_mouse_calibrated": "Kalibriert",
|
||||
"ui.space_mouse_calibration": "3D-Maus-Kalibrierung",
|
||||
"ui.space_mouse_calibration_done": "Fertig. Speichern Sie die Kalibrierung, damit sie immer gilt, wenn diese 3D-Maus angeschlossen ist.",
|
||||
"ui.space_mouse_calibration_no_response": "Eine Achse hat sich kaum bewegt und lässt sich nicht kalibrieren. Beginnen Sie von vorn und bewegen Sie die Kappe jedes Mal weiter.",
|
||||
"ui.space_mouse_dead_zone": "Totzone",
|
||||
"ui.space_mouse_dead_zone_tip": "Wie weit sich die Kappe bewegen muss, bevor sich die Ansicht bewegt, als Anteil ihres vollen Wegs. Erhöhen Sie sie, wenn die Ansicht wandert, obwohl die Kappe ruht.",
|
||||
"ui.space_mouse_driven_by": "Welche kalibrierte Bewegung des Geräts diese Achse steuert. Wählen Sie eine, die schon eine andere Achse nutzt, werden die beiden getauscht.",
|
||||
"ui.space_mouse_driven_by_label": "gesteuert von",
|
||||
"ui.space_mouse_driver": "3D-Maus-Treiber",
|
||||
"ui.space_mouse_driver_auto": "Automatisch",
|
||||
"ui.space_mouse_driver_direct": "Direkt (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Das Gerät wird direkt gelesen; spacenavd läuft nicht",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lesen über spacenavd, die einzige verfügbare Möglichkeit",
|
||||
"ui.space_mouse_forget": "Kalibrierung vergessen",
|
||||
"ui.space_mouse_hint": "Ansicht gleichzeitig verschieben und drehen; Skizzen verwenden nur Schwenken und Zoomen.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translation X",
|
||||
"ui.space_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.space_mouse_navigate": "Mit SpaceNavigator / SpaceMouse navigieren",
|
||||
"ui.space_mouse_none": "Keine 3D-Maus gefunden",
|
||||
"ui.space_mouse_not_calibrated": "Nicht kalibriert",
|
||||
"ui.space_mouse_reading": "Werte",
|
||||
"ui.space_mouse_reset_axes": "Achsenzuordnung zurücksetzen",
|
||||
"ui.space_mouse_step_lift": "Kappe nach oben ziehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_press": "Kappe nach unten drücken, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_pull": "Kappe zu sich heranziehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_push": "Kappe von sich wegschieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_rest": "Nehmen Sie die Hand von der Kappe und lassen Sie sie ruhen.",
|
||||
"ui.space_mouse_step_slide_left": "Kappe nach links schieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_slide_right": "Kappe nach rechts schieben, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_back": "Kappe nach hinten kippen (Vorderkante hoch), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_forward": "Kappe nach vorn kippen (Vorderkante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_left": "Kappe nach links kippen (linke Kante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_tilt_right": "Kappe nach rechts kippen (rechte Kante runter), halten, dann loslassen.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Kappe im Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Kappe gegen den Uhrzeigersinn drehen, halten, dann loslassen.",
|
||||
"ui.space_mouse_view_axis": "Ansichtsachse",
|
||||
"ui.space_mouse_view_off": "Aus",
|
||||
"ui.space_mouse_view_off_tip": "Das Profil des 3D-Maus-Treibers schaltet diese Achse ab, daher bewegt ihre Bewegung die Ansicht nicht. Wählen Sie hier die Bewegung einer anderen Achse, werden die beiden getauscht und sie bewegt die Ansicht nicht mehr.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbit links / rechts",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbit hoch / runter",
|
||||
"ui.space_mouse_view_pan_sideways": "Schwenken links / rechts",
|
||||
"ui.space_mouse_view_pan_up_down": "Schwenken hoch / runter",
|
||||
"ui.space_mouse_view_roll": "Rollen",
|
||||
"ui.space_mouse_view_unmapped": "Achse {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Abstand",
|
||||
"ui.spacing_2": "Abstand 2",
|
||||
"ui.sphere": "Sphäre",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Körper trennen",
|
||||
"ui.stand_up_tip": "Nach dem Öffnen einer STEP- oder IGES-Datei mit Z nach oben eine Vierteldrehung um X anbieten",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Von vorn beginnen",
|
||||
"ui.start_page": "Startseite",
|
||||
@@ -2017,7 +2221,7 @@
|
||||
"ui.top": "Oben",
|
||||
"ui.torus": "Torus",
|
||||
"ui.total_angle": "Gesamtwinkel",
|
||||
"ui.trackpad_pan_tip": "Trackpad-Gesten schwenken (aus: sie zoomen wie das Rad); Mausrad zoomt, Daumenrad schwenkt seitlich, Pinch zoomt immer",
|
||||
"ui.trackpad_pan_tip": "Scrollen mit zwei Fingern verschiebt die Ansicht; bei Fusion dreht Umschalt + zwei Finger die Ansicht. Aus: Scrollen zoomt. Mausrad und Pinch-Geste zoomen; das Daumenrad verschiebt seitlich.",
|
||||
"ui.trim": "Kürzen",
|
||||
"ui.twist": "Verdrehung",
|
||||
"ui.two_finger_scroll_pans": "Zwei-Finger-Scrollen schwenkt",
|
||||
@@ -2035,6 +2239,7 @@
|
||||
"ui.up_axis_note": "Welche Achse in den Ansichten nach oben zeigt (Achsenansicht, Home, Ansehen). Designs bleiben unverändert.",
|
||||
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
|
||||
"ui.view_centre": "Ansichtszentrum",
|
||||
"ui.viewport_theme_follow": "Dem Design folgen",
|
||||
"ui.visual_shaded_hidden_edges": "Schattiert mit verdeckten Kanten",
|
||||
"ui.visual_shaded_with_edges": "Schattiert mit Kanten",
|
||||
"ui.visual_style": "Ansichtsstil",
|
||||
@@ -2084,6 +2289,7 @@
|
||||
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
|
||||
"update.prefs_label": "Updates",
|
||||
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
|
||||
"update.prefs_web": "Laden Sie die Seite neu, um die neueste Web-App zu erhalten. Ist das Netzwerk nicht verfügbar oder kann das Update nicht rechtzeitig vollständig heruntergeladen werden, wird die gespeicherte Version geöffnet. Ein laufender Download wird für das nächste Neuladen fortgesetzt.",
|
||||
"update.release_page": "Release-Seite öffnen",
|
||||
"update.restart": "Jetzt neu starten",
|
||||
"update.restart_later": "Später",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"Modeled": "Modelled",
|
||||
"cmd.construct_point_center": "Point At Centre Of Circle/Sphere/Torus",
|
||||
"cmd.inspect_center_of_mass": "Centre of Mass",
|
||||
"cmd.render_aging_describe": "Layered Surface Aging",
|
||||
"cmd.render_effects_describe": "Lens and Creative Effects",
|
||||
"cmd.sketch_arc_center_point": "Centre Point Arc",
|
||||
"cmd.sketch_circle_center": "Centre Diameter Circle",
|
||||
"cmd.sketch_rectangle_center": "Centre Rectangle",
|
||||
@@ -14,8 +16,83 @@
|
||||
"cmd.sketch_slot_center_point": "Centre Point Slot",
|
||||
"cmd.sketch_slot_center_to_center": "Centre to Centre Slot",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"render.aging_add": "Add aging layer",
|
||||
"render.aging_airborne": "Airborne dust / participating medium",
|
||||
"render.aging_airborne_help": "Airborne dust is a separate finite homogeneous scattering volume. It changes transport and raw EXR, not the surface-aging stack. Existing volume/glass/denoising restrictions remain explicit; no atmosphere is added automatically.",
|
||||
"render.aging_all_bodies": "All eligible opaque bodies",
|
||||
"render.aging_chips": "Chipped paint",
|
||||
"render.aging_class_any": "All opaque material classes",
|
||||
"render.aging_class_dielectric": "Nonmetallic surfaces only",
|
||||
"render.aging_class_metal": "Metallic surfaces only",
|
||||
"render.aging_color": "Layer colour · sRGB",
|
||||
"render.aging_coverage": "Coverage",
|
||||
"render.aging_dust": "Deposited dust",
|
||||
"render.aging_empty": "No aging layers: the original surfaces remain unchanged.",
|
||||
"render.aging_fading": "Faded coating",
|
||||
"render.aging_grime": "Grime & staining",
|
||||
"render.aging_help": "Combine up to eight optical layers, in order. Rust and patina are authored appearances, not age/corrosion simulations. Scale is in world millimetres. No geometry, mass or engineering property is changed; glass and passive stages are excluded.",
|
||||
"render.aging_metallic": "Layer metallic fraction",
|
||||
"render.aging_origin": "Field origin · world mm",
|
||||
"render.aging_patina": "Copper-green patina",
|
||||
"render.aging_refresh_targets": "Inspect current bodies",
|
||||
"render.aging_roughness": "Layer roughness",
|
||||
"render.aging_rust": "Oxidized iron / rust",
|
||||
"render.aging_scale": "Pattern scale",
|
||||
"render.aging_scratches": "Fine scratches",
|
||||
"render.aging_target": "Target & coordinates",
|
||||
"render.aging_target_help": "A named target must still match its captured index and name. Global masks are world-anchored; they do not follow moving parts. This is neither cavity detection nor true edge wear. Filter the list to reach additional bodies.",
|
||||
"render.aging_target_search": "Filter body names",
|
||||
"render.aging_upward": "Upward-facing deposition",
|
||||
"render.backdrop_x": "Centre X",
|
||||
"render.backdrop_y": "Centre Y",
|
||||
"render.background_camera_section": "Camera background · PNG presentation",
|
||||
"render.background_completed_frozen": "The displayed result is a frozen completed image. Editing a background does not repaint that result automatically. Use Regrade for camera-only changes, or Render for a new physical stage.",
|
||||
"render.background_hdr_separate": "The current HDR resources illuminate and reflect on surfaces; they are not automatically displayed as a panorama behind the subject. Changing a gradient does not rotate or recolour the lighting.",
|
||||
"render.background_lighting_section": "HDR illumination · separate from camera background",
|
||||
"render.background_no_alpha_override": "A camera background is not added to transparent output silently. Raw-master regrading uses captured coverage and does not reapply denoising; the source EXR remains unchanged.",
|
||||
"render.background_regrade": "Apply to retained raw master",
|
||||
"render.background_stage_occlusion": "This shot includes physical stage geometry. A large floor or wall can fill the frame and hide a camera-only background. Clear or resize the stage deliberately, or change its physical reflectance. Regrading cannot remove captured geometry.",
|
||||
"render.background_workflow": "Choose the background you intend: a camera-only colour/gradient, a physical stage that receives light, or HDR illumination. They are independent. Live Preview is a modelling view; render a new image or regrade a retained master to see camera finishing.",
|
||||
"render.chromatic_offset": "Chromatic edge offset",
|
||||
"render.effects_gpu_only": "These effects require GPU path tracing. Changing the page never switches the renderer automatically.",
|
||||
"render.effects_help": "Combine ordered colour looks and image-space lens effects. Rendering and raw-master regrading use the same saved settings. Raw EXR stays unchanged. These are original artistic controls, not calibrated lens or branded-film emulations.",
|
||||
"render.effects_reset_lens": "Reset lens effects",
|
||||
"render.effects_seed": "Pattern seed",
|
||||
"render.film_grain": "Photographic grain",
|
||||
"render.flare_ghosts": "Ghost rings per source",
|
||||
"render.flare_halo": "Flare halo",
|
||||
"render.flare_streak": "Anamorphic-style streak",
|
||||
"render.flare_strength": "Highlight flare strength",
|
||||
"render.flare_threshold": "Scene-linear flare threshold",
|
||||
"render.layer_down": "Move down",
|
||||
"render.layer_enabled": "Layer enabled",
|
||||
"render.layer_remove": "Remove layer",
|
||||
"render.layer_strength": "Layer strength",
|
||||
"render.layer_up": "Move up",
|
||||
"render.lens_effects_help": "Flare uses up to four separated bright sources in the actual linear image. Ghosts, halo, streaks, chromatic offsets and radial distortion are artistic image-space approximations. Spatial effects need opaque output; outside samples extend the image edge. Grain is deterministic. No physics, EXR or geometry is altered.",
|
||||
"render.lens_effects_title": "Lens-inspired & photographic",
|
||||
"render.look_add": "Add colour look",
|
||||
"render.look_bleach": "Bleach-bypass-inspired",
|
||||
"render.look_cool": "Cool",
|
||||
"render.look_cyanotype": "Cyanotype-inspired",
|
||||
"render.look_faded": "Faded print",
|
||||
"render.look_mono": "Monochrome",
|
||||
"render.look_sepia": "Sepia",
|
||||
"render.look_strength": "Look strength",
|
||||
"render.look_teal_orange": "Teal / orange",
|
||||
"render.look_title": "Creative look stack",
|
||||
"render.look_warm": "Warm",
|
||||
"render.opaque_preview_help": "Opaque PBR and simple colours; blended transparency. Refraction, HDRI and final rendering are not available yet.",
|
||||
"render.page_aging": "Aging & contamination",
|
||||
"render.page_background": "Backgrounds",
|
||||
"render.page_effects": "Lens & finishing effects",
|
||||
"render.radial_distortion": "Radial distortion",
|
||||
"render.ribbon_aging": "Aging",
|
||||
"render.ribbon_effects": "Effects",
|
||||
"render.sharpening": "Detail sharpening",
|
||||
"render.soft_focus": "Soft focus",
|
||||
"sketch_text.align_center": "Centre",
|
||||
"ui.3d_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.center": "Centre",
|
||||
"ui.centerline": "Centreline",
|
||||
"ui.customizeToolbar": "Customise Toolbar…",
|
||||
@@ -24,7 +101,6 @@
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.ribbon_labels": "Show text under ribbon icons",
|
||||
"ui.space_mouse_step_pull": "Pull the cap towards you, hold it, then let go.",
|
||||
"ui.workspace_layout": "Workspace layout",
|
||||
"ui.zoom_to_cursor_tip": "The wheel zooms towards the point under the cursor (off: towards the view's centre)",
|
||||
"update.prefs_flatpak": "Flatpak updates this copy ({version}): run {command} or use your software centre. SolidCraft doesn't check."
|
||||
|
||||
+1046
-57
File diff suppressed because it is too large
Load Diff
+779
-63
File diff suppressed because it is too large
Load Diff
+127
-55
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Choisir…",
|
||||
"Circular": "Circulaire",
|
||||
"Circumference": "Circonférence",
|
||||
"Clockwise": "Horaire",
|
||||
"Close draft": "Fermer le brouillon",
|
||||
"Collisions": "Collisions",
|
||||
"Colour": "Couleur",
|
||||
@@ -269,7 +270,7 @@
|
||||
"Preview Row": "Aperçu de la ligne",
|
||||
"Preview leaves the current design unchanged.": "L'aperçu ne modifie pas la conception actuelle.",
|
||||
"Profiles/Curves": "Profils/Courbes",
|
||||
"Quantity": "Grandeur",
|
||||
"Quantity": "Quantité",
|
||||
"RECENT": "RÉCENTS",
|
||||
"REFERENCE": "RÉFÉRENCE",
|
||||
"RULED SURFACE": "SURFACE RÉGLÉE",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Rechercher des commandes",
|
||||
"Search this library": "Rechercher dans cette bibliothèque",
|
||||
"Section": "Coupe",
|
||||
"Section position": "Position de la section",
|
||||
"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).": "Sélectionner un autre élément : maintenir le bouton gauche immobile sur le modèle (ou clic droit › Sélectionner un autre…) pour lister tout ce qui se trouve sous le curseur ; cliquer de nouveau au même endroit pour sélectionner l'élément suivant. Double-cliquer avec le bouton du milieu pour revenir à l'accueil (Préférences › Navigation).",
|
||||
"Select bodies (a face picks its body)": "Sélectionner des corps (une face sélectionne son corps)",
|
||||
"Select bodies or faces instead": "Sélectionner plutôt des corps ou des faces",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Afficher le curseur de l'agent",
|
||||
"Show in folder": "Afficher dans le dossier",
|
||||
"Show the pointer an AI agent moves (#35)": "Afficher le pointeur déplacé par un agent IA (#35)",
|
||||
"Show tool at cursor": "Afficher l'outil près du curseur",
|
||||
"Signed axes": "Axes signés",
|
||||
"Size": "Taille",
|
||||
"Sketch constraints": "Contraintes d'esquisse",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s'est fermé avant l'enregistrement de ces conceptions. Leur dernier enregistrement automatique peut être ouvert.",
|
||||
"Specialise": "Spécialiser",
|
||||
"Speed": "Vitesse",
|
||||
"Square": "Carré",
|
||||
"Stand it up": "Redresser",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Redresser : un quart de tour autour de X, sous forme d'une fonction Déplacer que vous pouvez annuler.",
|
||||
"Stand this part up?": "Redresser cette pièce ?",
|
||||
"Start angle": "Angle de départ",
|
||||
"Start bulge": "Renflement de début",
|
||||
"Start continuity": "Continuité de début",
|
||||
"Step": "Étape",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "angle, hauteur",
|
||||
"appearance.dielectric_ior": "Indice de réfraction (diélectrique)",
|
||||
"appearance.metallic": "Métallique",
|
||||
"appearance.only_available": "Apparences disponibles uniquement",
|
||||
"appearance.pbr_help": "Voir cette surface dans l'espace Rendu ou dans un PNG du produit. Sans réfraction ni transmission.",
|
||||
"appearance.pbr_opaque": "PBR opaque",
|
||||
"appearance.roughness": "Rugosité",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Insérer un STEP",
|
||||
"cmd.file_save_mesh": "Enregistrer comme maillage",
|
||||
"cmd.file_slicers": "Slicers installés",
|
||||
"cmd.frame_member": "Élément de structure",
|
||||
"cmd.frame_profile_insert": "Insérer un profilé",
|
||||
"cmd.frame_profile_list": "Bibliothèque de profilés",
|
||||
"cmd.frame_profile_properties": "Propriétés du profilé",
|
||||
"cmd.inspect_accessibility": "Analyse d'accessibilité",
|
||||
"cmd.inspect_center_of_mass": "Centre de gravité",
|
||||
"cmd.inspect_clearance": "Jeu",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Nouveau matériau physique",
|
||||
"cmd.material_delete": "Supprimer le matériau physique",
|
||||
"cmd.material_edit": "Modifier le matériau physique",
|
||||
"cmd.material_inspect": "Inspecter le matériau physique",
|
||||
"cmd.material_list": "Lister les matériaux",
|
||||
"cmd.material_properties_directional": "Module d'élasticité directionnel",
|
||||
"cmd.material_properties_validate": "Valider les données du matériau physique",
|
||||
@@ -857,11 +868,17 @@
|
||||
"help.mit": "Licence MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Coupe les arêtes sélectionnées par un biseau plat de la distance indiquée. À utiliser pour casser des arêtes vives ou faciliter l'entrée d'une goupille ou d'une vis.",
|
||||
"help.solid.combine": "Joint des corps, les retire d'un corps cible ou ne garde que le volume qu'ils ont en commun, et peut conserver les corps outils. À utiliser pour fusionner des pièces, tailler une forme avec une autre ou repérer où deux corps se recouvrent.",
|
||||
"help.solid.extrude": "Pousse le profil d'une esquisse en 3D sur une distance, en ajoutant ou en retirant de la matière. À utiliser pour transformer un contour plat en bloc, en bossage ou en poche.",
|
||||
"help.solid.fillet": "Arrondit les arêtes sélectionnées par un raccord circulaire lisse du rayon indiqué. À utiliser pour adoucir des arêtes vives par souci de solidité, de confort ou d'aspect.",
|
||||
"help.solid.hole": "Perce un trou rond du diamètre indiqué en un point choisi d'une face. À utiliser pour les passages de vis, les goupilles et autres ouvertures rondes simples.",
|
||||
"help.solid.loft": "Raccorde en douceur deux profils d'esquisse ou plus, ou un profil et un point, par un solide lissé. À utiliser pour les transitions, entonnoirs, poignées et toute forme dont la section change sur sa longueur.",
|
||||
"help.solid.mirror": "Crée une copie symétrique des corps ou fonctions sélectionnés par rapport à un plan, et peut la fusionner avec l'original. À utiliser pour les pièces symétriques : on modélise une moitié et on obtient les deux.",
|
||||
"help.solid.pattern.circular": "Répète les objets sélectionnés autour d'un axe, selon le nombre et l'angle total indiqués. À utiliser pour les cercles de perçages, rayons, ailettes et dents.",
|
||||
"help.solid.pattern.rectangular": "Répète les objets sélectionnés dans une ou deux directions, selon le nombre et l'espacement indiqués. À utiliser pour les rangées et grilles de trous, nervures, goupilles ou pieds.",
|
||||
"help.solid.revolve": "Fait tourner le profil d'une esquisse autour d'un axe pour obtenir un solide de révolution. À utiliser pour les arbres, boutons, bols et toute pièce ronde autour d'une ligne centrale.",
|
||||
"help.solid.shell": "Évide un solide en laissant des parois de l'épaisseur indiquée, et peut ouvrir les faces choisies. À utiliser pour les boîtes, couvercles et autres pièces à parois minces.",
|
||||
"help.solid.sweep": "Entraîne le profil d'une esquisse le long d'une trajectoire pour obtenir un solide. À utiliser pour les tuyaux, rails, poignées et toute pièce de section constante qui suit une courbe.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"home.back": "Revenir à {name}",
|
||||
"home.crashes": "Rapports de plantage",
|
||||
@@ -966,6 +983,7 @@
|
||||
"motion.key_here": "Placer une image clé ici",
|
||||
"motion.key_here_hint": "Ajouter une image clé à cette étape avec la valeur actuelle de la liaison",
|
||||
"motion.no_collisions": "Aucune collision au cours de l'étude",
|
||||
"motion.plot_quantity": "Grandeur",
|
||||
"motion.step_back": "Étape précédente",
|
||||
"motion.step_forward": "Étape suivante",
|
||||
"motion.to_end": "À la fin",
|
||||
@@ -1072,6 +1090,8 @@
|
||||
"settings.msaa_applies_on_restart": "L'anticrénelage s'appliquera au prochain démarrage de SolidCraft",
|
||||
"settings.msaa_reset": "{setting} : {requested}× n'est pas pris en charge par ce périphérique graphique ; la valeur est désormais {reset}× (pris en charge : {supported}).",
|
||||
"settings.msaa_unsupported": "Non pris en charge par ce périphérique graphique",
|
||||
"settings.reference_geometry_through_bodies": "Afficher la géométrie de référence à travers les corps",
|
||||
"settings.reference_geometry_through_bodies_help": "Afficher les contours des esquisses inactives et les plans de référence par-dessus les corps ombrés",
|
||||
"settings.reset_intro": "Ces réglages ne fonctionnent pas sur cet ordinateur et empêcheraient SolidCraft de démarrer ; ils ont donc été modifiés :",
|
||||
"settings.reset_title": "Des réglages ont été modifiés",
|
||||
"settings.supersampling_note": "Dessine la vue à une résolution plus élevée puis la réduit : arêtes, lignes et grille plus lisses sur les écrans de définition inférieure à la 4K. Cela coûte du temps GPU : environ 2,25× plus de pixels à 1,5× et 4× plus à 2×.",
|
||||
@@ -1127,7 +1147,106 @@
|
||||
"trace.stopped_by_contact": "Se termine à l'échantillon {sample} : {a} traverserait {b}",
|
||||
"trace.stopped_by_loop": "Se termine à l'échantillon {sample} : la boucle passant par {joint} ne peut plus se fermer",
|
||||
"ui.1_degree_of_freedom": "1 degré de liberté",
|
||||
"ui.3d_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
|
||||
"ui.3d_mouse_button": "Bouton {number}",
|
||||
"ui.3d_mouse_button_nothing": "Rien",
|
||||
"ui.3d_mouse_button_settings": "Réglages de la souris 3D",
|
||||
"ui.3d_mouse_buttons": "Boutons",
|
||||
"ui.3d_mouse_buttons_hint": "Appuyez sur un bouton de votre souris 3D pour le trouver ici, puis choisissez ce qu'il fait.",
|
||||
"ui.3d_mouse_buttons_no_device": "Branchez une souris 3D pour choisir ce que font ses boutons.",
|
||||
"ui.3d_mouse_buttons_reset": "Rétablir les boutons par défaut",
|
||||
"ui.3d_mouse_calibrate": "Calibrer…",
|
||||
"ui.3d_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrer la souris 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Bougez la souris 3D une fois pour que SolidCraft la voie.",
|
||||
"ui.3d_mouse_calibrated": "Calibrée",
|
||||
"ui.3d_mouse_calibration": "Calibrage de la souris 3D",
|
||||
"ui.3d_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
|
||||
"ui.3d_mouse_circles_help": "Les cercles montrent ce que l'appareil envoie après calibrage. Le modèle d'essai montre ce que la vue en fait, après inversion, vitesse et affectation des axes.",
|
||||
"ui.3d_mouse_dead_zone": "Zone morte",
|
||||
"ui.3d_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
|
||||
"ui.3d_mouse_denied": "SolidCraft voit une souris 3D mais n'a pas le droit de la lire. La règle udev est dans les notes d'installation de la souris 3D.",
|
||||
"ui.3d_mouse_denied_short": "Trouvée, mais illisible",
|
||||
"ui.3d_mouse_driven_by": "Quel mouvement calibré de l'appareil pilote cet axe. Choisir un mouvement déjà utilisé par un autre axe les échange.",
|
||||
"ui.3d_mouse_driven_by_label": "piloté par",
|
||||
"ui.3d_mouse_driver": "Pilote de la souris 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatique",
|
||||
"ui.3d_mouse_driver_direct": "Direct (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Lecture via spacenavd pour l'instant : l'appareil lui-même n'a pas pu être ouvert.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lecture directe du périphérique ; spacenavd n'est pas lancé",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lecture via spacenavd, seule possibilité disponible",
|
||||
"ui.3d_mouse_forget": "Oublier le calibrage",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vue à gauche",
|
||||
"ui.3d_mouse_ghost_left_window": "Fenêtre à gauche",
|
||||
"ui.3d_mouse_ghost_position": "Position",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vue à droite",
|
||||
"ui.3d_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
|
||||
"ui.3d_mouse_kind_cap": "Retour au centre",
|
||||
"ui.3d_mouse_kind_question": "Quel type de souris 3D est-ce ?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "La valeur reste tant que vous maintenez, comme sur un appareil de type « Retour au centre ».",
|
||||
"ui.3d_mouse_looks_like_trackball": "La valeur retombe dès que vous arrêtez de bouger, comme sur un trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translation X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.3d_mouse_navigate": "Naviguer avec la souris 3D",
|
||||
"ui.3d_mouse_none": "Aucune souris 3D trouvée",
|
||||
"ui.3d_mouse_not_calibrated": "Non calibrée",
|
||||
"ui.3d_mouse_offer": "Calibrer votre souris 3D corrige les axes échangés ou inversés et règle sa vitesse et sa sensation.",
|
||||
"ui.3d_mouse_offer_never": "Ne plus afficher",
|
||||
"ui.3d_mouse_offer_swap": "Passer à Hidraw (pilote direct)",
|
||||
"ui.3d_mouse_reading": "Lecture",
|
||||
"ui.3d_mouse_reset_axes": "Réinitialiser l'attribution des axes",
|
||||
"ui.3d_mouse_reset_view": "Réinitialiser la vue",
|
||||
"ui.3d_mouse_restore": "Rétablir les réglages de la souris 3D",
|
||||
"ui.3d_mouse_restore_ask": "Remettre les réglages de la souris 3D par défaut ? Votre calibrage et l'assignation des boutons sont conservés.",
|
||||
"ui.3d_mouse_section": "Souris 3D",
|
||||
"ui.3d_mouse_show_in_view": "Afficher la souris 3D dans la vue 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Une image de votre souris 3D dans le coin inférieur gauche, qui bouge quand vous la bougez.",
|
||||
"ui.3d_mouse_skip": "Mon appareil ne peut pas le faire",
|
||||
"ui.3d_mouse_skip_tip": "Ignore ce mouvement dans les deux sens ; la calibration le laisse de côté.",
|
||||
"ui.3d_mouse_speed": "Vitesse de la souris 3D",
|
||||
"ui.3d_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Lâchez la boule et laissez-la immobile.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Tournez l'anneau de défilement de quelques crans dans le sens horaire.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Tournez l'anneau de défilement de quelques crans dans le sens antihoraire.",
|
||||
"ui.3d_mouse_step_roll_away": "Faites rouler la boule loin de vous plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_left": "Faites rouler la boule vers la gauche plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_right": "Faites rouler la boule vers la droite plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_roll_toward": "Faites rouler la boule vers vous plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Faites tourner la boule dans le sens horaire plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Faites tourner la boule dans le sens antihoraire plusieurs fois, puis laissez-la s'arrêter.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclinez le haut du capuchon loin de vous (le bord le plus proche monte), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclinez le haut du capuchon vers vous (le bord le plus proche descend), maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_touch": "Posez la main sur le capuchon sans le pousser.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrer comme retour au centre",
|
||||
"ui.3d_mouse_switch_trackball": "Calibrer comme un trackball",
|
||||
"ui.3d_mouse_view_axis": "Axe de la vue",
|
||||
"ui.3d_mouse_view_off": "Désactivé",
|
||||
"ui.3d_mouse_view_off_tip": "Le profil du pilote de la souris 3D désactive cet axe : son mouvement ne déplace pas la vue. Choisir ici le mouvement d'un autre axe les échange, et ce mouvement cesse de déplacer la vue.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbite gauche / droite",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbite haut / bas",
|
||||
"ui.3d_mouse_view_pan_sideways": "Panoramique gauche / droite",
|
||||
"ui.3d_mouse_view_pan_up_down": "Panoramique haut / bas",
|
||||
"ui.3d_mouse_view_roll": "Roulis",
|
||||
"ui.3d_mouse_view_unmapped": "Axe {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "En attente de mouvement",
|
||||
"ui.3d_mouse_wiggle": "Si vous en avez une de branchée, remuez-la.",
|
||||
"ui.3d_sketch": "Esquisse 3D",
|
||||
"ui.activate": "Activer",
|
||||
"ui.active_component": "Composant actif",
|
||||
@@ -1285,6 +1404,7 @@
|
||||
"ui.ai_http_busy": "le point d'accès est occupé ou a dépassé sa limite ; réessayez plus tard",
|
||||
"ui.ai_http_key": "le point d'accès a refusé la clé : vérifiez-la dans Préférences › IA",
|
||||
"ui.ai_http_not_found": "point d'accès ou modèle introuvable : vérifiez l'URL de base et le nom du modèle",
|
||||
"ui.ai_http_too_large": "la conversation est trop volumineuse pour ce point d'accès ; commencez une nouvelle discussion",
|
||||
"ui.ai_key": "Clé d'API",
|
||||
"ui.ai_key_hint": "facultative ; conservée dans le trousseau de clés du système",
|
||||
"ui.ai_key_needs_https": "Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
|
||||
@@ -1312,7 +1432,6 @@
|
||||
"ui.ai_permissions": "Autorisations",
|
||||
"ui.ai_preset": "Préréglage",
|
||||
"ui.ai_remove": "Retirer",
|
||||
"ui.ai_request_limit": "arrêté après 24 requêtes pour un message ; envoyez un autre message pour poursuivre",
|
||||
"ui.ai_reset": "Réinitialiser",
|
||||
"ui.ai_settings": "Réglages IA…",
|
||||
"ui.ai_show_features": "Afficher les fonctionnalités IA",
|
||||
@@ -1357,6 +1476,7 @@
|
||||
"ui.blocked": "Inaccessible",
|
||||
"ui.bodies": "Corps",
|
||||
"ui.bodies_all_if_none": "Corps (tous si aucun)",
|
||||
"ui.bottom": "Dessous",
|
||||
"ui.box": "Boîte",
|
||||
"ui.break": "Fractionner",
|
||||
"ui.browser_bodies_one": "{count} corps",
|
||||
@@ -1488,6 +1608,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrer les commandes ou les touches",
|
||||
"ui.findInTimeline": "Trouver dans la chronologie",
|
||||
"ui.findInWindow": "Trouver dans la fenêtre",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Très fine",
|
||||
"ui.finish_sketch": "Terminer l'esquisse",
|
||||
"ui.fit": "Cadrer",
|
||||
"ui.fix_unfix": "Fixer/Libérer",
|
||||
@@ -1576,6 +1698,7 @@
|
||||
"ui.heading_torus": "TORE",
|
||||
"ui.heading_utilities": "UTILITAIRES",
|
||||
"ui.heading_utility": "UTILITAIRE",
|
||||
"ui.heading_weldments": "MÉCANO-SOUDURE",
|
||||
"ui.help": "Aide",
|
||||
"ui.hide": "Masquer",
|
||||
"ui.hideAxes": "Masquer les axes",
|
||||
@@ -1712,6 +1835,7 @@
|
||||
"ui.project_face_edges_sketch": "Projeter les arêtes de la face dans les nouvelles esquisses",
|
||||
"ui.properties": "Propriétés",
|
||||
"ui.pull_direction": "Direction de démoulage",
|
||||
"ui.quality": "Qualité",
|
||||
"ui.quantity": "Quantité",
|
||||
"ui.quantity_2": "Quantité 2",
|
||||
"ui.quit": "Quitter",
|
||||
@@ -1800,65 +1924,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom fluide à la molette",
|
||||
"ui.smooth_wheel_zoom_tip": "Désactiver pour appliquer chaque cran de molette immédiatement, sans lissage",
|
||||
"ui.snap": "Accrochage",
|
||||
"ui.space_mouse_axes_reassigned": "Les axes sont réattribués dans les Préférences, et cela s'applique toujours en plus de ce calibrage.",
|
||||
"ui.space_mouse_calibrate": "Calibrer…",
|
||||
"ui.space_mouse_calibrate_tip": "Déplacez le capuchon comme la fenêtre le demande, pour que SolidCraft apprenne dans quel sens et jusqu'où va chaque axe de cette souris 3D. Enregistré pour cette souris 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrer la souris 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrée",
|
||||
"ui.space_mouse_calibration": "Calibrage de la souris 3D",
|
||||
"ui.space_mouse_calibration_done": "Terminé. Enregistrez-le pour l'utiliser chaque fois que cette souris 3D est connectée.",
|
||||
"ui.space_mouse_calibration_no_response": "Un axe a à peine bougé, il ne peut donc pas être calibré. Recommencez en déplaçant le capuchon plus loin à chaque fois.",
|
||||
"ui.space_mouse_dead_zone": "Zone morte",
|
||||
"ui.space_mouse_dead_zone_tip": "De combien le capuchon doit bouger avant que la vue ne bouge, en part de sa course complète. Augmentez-la si la vue dérive alors que le capuchon est au repos.",
|
||||
"ui.space_mouse_driven_by": "Quel mouvement calibré de l'appareil pilote cet axe. Choisir un mouvement déjà utilisé par un autre axe les échange.",
|
||||
"ui.space_mouse_driven_by_label": "piloté par",
|
||||
"ui.space_mouse_driver": "Pilote de la souris 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatique",
|
||||
"ui.space_mouse_driver_direct": "Direct (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lecture directe du périphérique ; spacenavd n'est pas lancé",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lecture via spacenavd, seule possibilité disponible",
|
||||
"ui.space_mouse_forget": "Oublier le calibrage",
|
||||
"ui.space_mouse_hint": "Déplace et fait pivoter la vue en même temps ; dans les esquisses, seuls le panoramique et le zoom sont utilisés.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotation X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotation Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotation Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translation X",
|
||||
"ui.space_mouse_motion_translation_y": "Translation Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translation Z",
|
||||
"ui.space_mouse_navigate": "Naviguer avec SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Aucune souris 3D trouvée",
|
||||
"ui.space_mouse_not_calibrated": "Non calibrée",
|
||||
"ui.space_mouse_reading": "Lecture",
|
||||
"ui.space_mouse_reset_axes": "Réinitialiser l'attribution des axes",
|
||||
"ui.space_mouse_step_lift": "Soulevez le capuchon, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_press": "Appuyez sur le capuchon, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_pull": "Tirez le capuchon vers vous, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_push": "Poussez le capuchon loin de vous, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_rest": "Lâchez le capuchon et laissez-le immobile.",
|
||||
"ui.space_mouse_step_slide_left": "Glissez le capuchon vers la gauche, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_slide_right": "Glissez le capuchon vers la droite, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclinez le capuchon vers l'arrière (bord avant vers le haut), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclinez le capuchon vers l'avant (bord avant vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclinez le capuchon vers la gauche (bord gauche vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclinez le capuchon vers la droite (bord droit vers le bas), maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Tournez le capuchon dans le sens horaire, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Tournez le capuchon dans le sens antihoraire, maintenez, puis relâchez.",
|
||||
"ui.space_mouse_view_axis": "Axe de la vue",
|
||||
"ui.space_mouse_view_off": "Désactivé",
|
||||
"ui.space_mouse_view_off_tip": "Le profil du pilote de la souris 3D désactive cet axe : son mouvement ne déplace pas la vue. Choisir ici le mouvement d'un autre axe les échange, et ce mouvement cesse de déplacer la vue.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbite gauche / droite",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbite haut / bas",
|
||||
"ui.space_mouse_view_pan_sideways": "Panoramique gauche / droite",
|
||||
"ui.space_mouse_view_pan_up_down": "Panoramique haut / bas",
|
||||
"ui.space_mouse_view_roll": "Roulis",
|
||||
"ui.space_mouse_view_unmapped": "Axe {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Espacement",
|
||||
"ui.spacing_2": "Espacement 2",
|
||||
"ui.sphere": "Sphère",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Scinder le corps",
|
||||
"ui.stand_up_tip": "Après l'ouverture d'un fichier STEP ou IGES avec Z vers le haut, proposer un quart de tour autour de X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Début",
|
||||
"ui.start_over": "Recommencer",
|
||||
"ui.start_page": "Page d’accueil",
|
||||
|
||||
+118
-54
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Escoller…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circunferencia",
|
||||
"Clockwise": "Horario",
|
||||
"Close draft": "Pechar o borrador",
|
||||
"Collisions": "Colisións",
|
||||
"Colour": "Cor",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Buscar ordes",
|
||||
"Search this library": "Buscar nesta biblioteca",
|
||||
"Section": "Sección",
|
||||
"Section position": "Posición da sección",
|
||||
"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).": "Seleccionar outro: mantén o botón esquerdo quieto sobre o modelo (ou clic dereito › Seleccionar outro…) para listar todo o que hai baixo o cursor; fai clic de novo no mesmo punto para seleccionar o seguinte. Fai dobre clic co botón central para ir ao Inicio (Preferencias › Navegación).",
|
||||
"Select bodies (a face picks its body)": "Selecciona corpos (unha cara escolle o seu corpo)",
|
||||
"Select bodies or faces instead": "Seleccionar corpos ou caras no seu lugar",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Mostrar o cursor do axente",
|
||||
"Show in folder": "Mostrar no cartafol",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostrar o punteiro que move un axente de IA (#35)",
|
||||
"Show tool at cursor": "Mostrar a ferramenta xunto ao cursor",
|
||||
"Signed axes": "Eixes con signo",
|
||||
"Size": "Tamaño",
|
||||
"Sketch constraints": "Restricións do bosquexo",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft pechouse antes de gardar estes deseños. Pódese abrir a súa última gravación automática.",
|
||||
"Specialise": "Especializar",
|
||||
"Speed": "Velocidade",
|
||||
"Square": "Cadrado",
|
||||
"Stand it up": "Poñela de pé",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Poñela de pé: un cuarto de volta arredor de X, como unha operación Mover que podes desfacer.",
|
||||
"Stand this part up?": "Poñer esta peza de pé?",
|
||||
"Start angle": "Ángulo inicial",
|
||||
"Start bulge": "Abaulamento inicial",
|
||||
"Start continuity": "Continuidade inicial",
|
||||
"Step": "Paso",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "ángulo, altura",
|
||||
"appearance.dielectric_ior": "IOR dieléctrico",
|
||||
"appearance.metallic": "Metálico",
|
||||
"appearance.only_available": "Só aparencias dispoñibles",
|
||||
"appearance.pbr_help": "Ve esta superficie en Render ou nun PNG de produto. Sen refracción nin transmisión.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosidade",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Inserir STEP",
|
||||
"cmd.file_save_mesh": "Gardar como malla",
|
||||
"cmd.file_slicers": "Laminadores instalados",
|
||||
"cmd.frame_member": "Membro estrutural",
|
||||
"cmd.frame_profile_insert": "Inserir perfil",
|
||||
"cmd.frame_profile_list": "Biblioteca de perfís",
|
||||
"cmd.frame_profile_properties": "Propiedades do perfil",
|
||||
"cmd.inspect_accessibility": "Análise de accesibilidade",
|
||||
"cmd.inspect_center_of_mass": "Centro de masas",
|
||||
"cmd.inspect_clearance": "Folga",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Material físico novo",
|
||||
"cmd.material_delete": "Eliminar o material físico",
|
||||
"cmd.material_edit": "Editar o material físico",
|
||||
"cmd.material_inspect": "Inspeccionar o material físico",
|
||||
"cmd.material_list": "Listar os materiais",
|
||||
"cmd.material_properties_directional": "Módulo elástico direccional",
|
||||
"cmd.material_properties_validate": "Validar os datos do material físico",
|
||||
@@ -960,6 +971,7 @@
|
||||
"motion.key_here": "Inserir fotograma clave aquí",
|
||||
"motion.key_here_hint": "Engadir un fotograma clave neste paso co valor actual da unión",
|
||||
"motion.no_collisions": "Sen colisións durante o estudo",
|
||||
"motion.plot_quantity": "Cantidade",
|
||||
"motion.step_back": "Paso atrás",
|
||||
"motion.step_forward": "Paso adiante",
|
||||
"motion.to_end": "Ao final",
|
||||
@@ -1121,7 +1133,106 @@
|
||||
"trace.stopped_by_contact": "Remata na mostra {sample}: {a} atravesaría {b}",
|
||||
"trace.stopped_by_loop": "Remata na mostra {sample}: o bucle a través de {joint} non pode pecharse máis",
|
||||
"ui.1_degree_of_freedom": "1 grao de liberdade",
|
||||
"ui.3d_mouse_axes_reassigned": "Os eixes están reasignados en Preferencias, e iso segue aplicándose sobre esta calibración.",
|
||||
"ui.3d_mouse_button": "Botón {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nada",
|
||||
"ui.3d_mouse_button_settings": "Axustes do rato 3D",
|
||||
"ui.3d_mouse_buttons": "Botóns",
|
||||
"ui.3d_mouse_buttons_hint": "Preme un botón do teu rato 3D para atopalo aquí e escolle que fai.",
|
||||
"ui.3d_mouse_buttons_no_device": "Conecta un rato 3D para escoller que fan os seus botóns.",
|
||||
"ui.3d_mouse_buttons_reset": "Restaurar os botóns predeterminados",
|
||||
"ui.3d_mouse_calibrate": "Calibrar…",
|
||||
"ui.3d_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrar o rato 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Move o rato 3D unha vez para que SolidCraft o vexa.",
|
||||
"ui.3d_mouse_calibrated": "Calibrado",
|
||||
"ui.3d_mouse_calibration": "Calibración do rato 3D",
|
||||
"ui.3d_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
|
||||
"ui.3d_mouse_circles_help": "Os círculos mostran o que envía o dispositivo tras a calibración. O modelo de proba mostra o que fai a vista con iso, tras a inversión, a velocidade e a asignación de eixes.",
|
||||
"ui.3d_mouse_dead_zone": "Zona morta",
|
||||
"ui.3d_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ve un rato 3D, pero non ten permiso para lelo. A regra udev está nas notas de configuración do rato 3D.",
|
||||
"ui.3d_mouse_denied_short": "Atopado, pero non se pode ler",
|
||||
"ui.3d_mouse_driven_by": "Que movemento calibrado do dispositivo controla este eixe. Se escolles un que xa usa outro eixe, intercámbianse.",
|
||||
"ui.3d_mouse_driven_by_label": "controlado por",
|
||||
"ui.3d_mouse_driver": "Controlador do rato 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automático",
|
||||
"ui.3d_mouse_driver_direct": "Directo (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Lendo polo de agora a través de spacenavd: non se puido abrir o dispositivo.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lectura directa do dispositivo; spacenavd non está en execución",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lectura mediante spacenavd, a única opción dispoñible",
|
||||
"ui.3d_mouse_forget": "Esquecer a calibración",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vista esquerda",
|
||||
"ui.3d_mouse_ghost_left_window": "Xanela esquerda",
|
||||
"ui.3d_mouse_ghost_position": "Posición",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vista dereita",
|
||||
"ui.3d_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
|
||||
"ui.3d_mouse_kind_cap": "Retorno ao centro",
|
||||
"ui.3d_mouse_kind_question": "Que tipo de rato 3D é?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "A lectura mantense mentres a suxeitas, como nun dispositivo de tipo «Retorno ao centro».",
|
||||
"ui.3d_mouse_looks_like_trackball": "A lectura volve atrás en canto deixas de moverte, como nun trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotación X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotación Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotación Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Translación X",
|
||||
"ui.3d_mouse_motion_translation_y": "Translación Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Translación Z",
|
||||
"ui.3d_mouse_navigate": "Navegar co rato 3D",
|
||||
"ui.3d_mouse_none": "Non se atopou ningún rato 3D",
|
||||
"ui.3d_mouse_not_calibrated": "Sen calibrar",
|
||||
"ui.3d_mouse_offer": "Calibrar o teu rato 3D corrixe os eixes trocados ou invertidos e axusta a súa velocidade e sensación.",
|
||||
"ui.3d_mouse_offer_never": "Non mostrar de novo",
|
||||
"ui.3d_mouse_offer_swap": "Cambiar a Hidraw (controlador directo)",
|
||||
"ui.3d_mouse_reading": "Lectura",
|
||||
"ui.3d_mouse_reset_axes": "Restablecer asignación de eixes",
|
||||
"ui.3d_mouse_reset_view": "Restablecer vista",
|
||||
"ui.3d_mouse_restore": "Restaurar valores do rato 3D",
|
||||
"ui.3d_mouse_restore_ask": "Volver aos valores predeterminados da configuración do rato 3D? A calibración e a asignación de botóns consérvanse.",
|
||||
"ui.3d_mouse_section": "Rato 3D",
|
||||
"ui.3d_mouse_show_in_view": "Mostrar o rato 3D na vista 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Unha imaxe do teu rato 3D na esquina inferior esquerda que se move cando o moves.",
|
||||
"ui.3d_mouse_skip": "O meu dispositivo non fai isto",
|
||||
"ui.3d_mouse_skip_tip": "Omite este movemento nos dous sentidos; a calibración déixao fóra.",
|
||||
"ui.3d_mouse_speed": "Velocidade do rato 3D",
|
||||
"ui.3d_mouse_step_lift": "Levanta o capuchón, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_press": "Preme o capuchón cara abaixo, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_pull": "Tira do capuchón cara a ti, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_push": "Empurra o capuchón lonxe de ti, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_rest": "Solta o capuchón e déixao quieto.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Solta a bóla e déixaa quieta.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Xira o anel de desprazamento uns clics no sentido horario.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Xira o anel de desprazamento uns clics no sentido antihorario.",
|
||||
"ui.3d_mouse_step_roll_away": "Fai rodar a bóla lonxe de ti unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_left": "Fai rodar a bóla á esquerda unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_right": "Fai rodar a bóla á dereita unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_roll_toward": "Fai rodar a bóla cara a ti unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_slide_right": "Despraza o capuchón á dereita, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Xira a bóla no sentido horario unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Xira a bóla no sentido antihorario unhas cantas veces e déixaa parar.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclina a parte de arriba do capuchón lonxe de ti (o bordo máis próximo sobe), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclina a parte de arriba do capuchón cara a ti (o bordo máis próximo baixa), mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_touch": "Apoia a man no capuchón sen empurralo.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantén a posición e despois sóltao.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrar como retorno ao centro",
|
||||
"ui.3d_mouse_switch_trackball": "Calibrar como trackball",
|
||||
"ui.3d_mouse_view_axis": "Eixe da vista",
|
||||
"ui.3d_mouse_view_off": "Desactivado",
|
||||
"ui.3d_mouse_view_off_tip": "O perfil do controlador do rato 3D desactiva este eixe, así que o seu movemento non move a vista. Se escolles aquí o movemento doutro eixe, intercámbianse e ese movemento deixa de mover a vista.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbitar á esquerda / dereita",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbitar arriba / abaixo",
|
||||
"ui.3d_mouse_view_pan_sideways": "Desprazar á esquerda / dereita",
|
||||
"ui.3d_mouse_view_pan_up_down": "Desprazar arriba / abaixo",
|
||||
"ui.3d_mouse_view_roll": "Xirar sobre a vista",
|
||||
"ui.3d_mouse_view_unmapped": "Eixe {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "Agardando movemento",
|
||||
"ui.3d_mouse_wiggle": "Se tes un conectado, móveo un pouco.",
|
||||
"ui.3d_sketch": "Bosquexo 3D",
|
||||
"ui.activate": "Activar",
|
||||
"ui.active_component": "Compoñente activo",
|
||||
@@ -1279,6 +1390,7 @@
|
||||
"ui.ai_http_busy": "o punto de acceso está ocupado ou superou o seu límite; téntao de novo máis tarde",
|
||||
"ui.ai_http_key": "o punto de acceso rexeitou a clave: compróbaa en Preferencias > IA",
|
||||
"ui.ai_http_not_found": "non existe ese punto de acceso ou modelo: comproba o URL base e o nome do modelo",
|
||||
"ui.ai_http_too_large": "a conversa é demasiado grande para este punto de acceso; comeza un chat novo",
|
||||
"ui.ai_key": "Clave da API",
|
||||
"ui.ai_key_hint": "opcional; gárdase no chaveiro do sistema",
|
||||
"ui.ai_key_needs_https": "A clave só se envía por HTTPS ou a este ordenador.",
|
||||
@@ -1306,7 +1418,6 @@
|
||||
"ui.ai_permissions": "Permisos",
|
||||
"ui.ai_preset": "Axuste predefinido",
|
||||
"ui.ai_remove": "Quitar",
|
||||
"ui.ai_request_limit": "detívose tras 24 solicitudes para unha mensaxe; envía outra mensaxe para continuar",
|
||||
"ui.ai_reset": "Restablecer",
|
||||
"ui.ai_settings": "Axustes de IA…",
|
||||
"ui.ai_show_features": "Mostrar as funcións de IA",
|
||||
@@ -1351,6 +1462,7 @@
|
||||
"ui.blocked": "Bloqueado",
|
||||
"ui.bodies": "Corpos",
|
||||
"ui.bodies_all_if_none": "Corpos (todos se non hai ningún)",
|
||||
"ui.bottom": "Inferior",
|
||||
"ui.box": "Caixa",
|
||||
"ui.break": "Romper",
|
||||
"ui.browser_bodies_one": "{count} corpo",
|
||||
@@ -1482,6 +1594,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrar ordes ou teclas",
|
||||
"ui.findInTimeline": "Buscar na liña de tempo",
|
||||
"ui.findInWindow": "Buscar na xanela",
|
||||
"ui.fine": "Fina",
|
||||
"ui.finest": "Moi fina",
|
||||
"ui.finish_sketch": "Rematar o bosquexo",
|
||||
"ui.fit": "Axustar",
|
||||
"ui.fix_unfix": "Fixar/Liberar",
|
||||
@@ -1570,6 +1684,7 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILIDADES",
|
||||
"ui.heading_utility": "UTILIDADE",
|
||||
"ui.heading_weldments": "SOLDADURA",
|
||||
"ui.help": "Axuda",
|
||||
"ui.hide": "Ocultar",
|
||||
"ui.hideAxes": "Ocultar os eixes",
|
||||
@@ -1706,6 +1821,7 @@
|
||||
"ui.project_face_edges_sketch": "Proxectar as arestas da cara en bosquexos novos",
|
||||
"ui.properties": "Propiedades",
|
||||
"ui.pull_direction": "Dirección de extracción",
|
||||
"ui.quality": "Calidade",
|
||||
"ui.quantity": "Cantidade",
|
||||
"ui.quantity_2": "Cantidade 2",
|
||||
"ui.quit": "Saír",
|
||||
@@ -1794,65 +1910,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom suave coa roda",
|
||||
"ui.smooth_wheel_zoom_tip": "Desactívao para aplicar cada movemento da roda de inmediato, sen suavizado",
|
||||
"ui.snap": "Axuste",
|
||||
"ui.space_mouse_axes_reassigned": "Os eixes están reasignados en Preferencias, e iso segue aplicándose sobre esta calibración.",
|
||||
"ui.space_mouse_calibrate": "Calibrar…",
|
||||
"ui.space_mouse_calibrate_tip": "Move o capuchón como pide a xanela para que SolidCraft aprenda cara a onde e ata onde vai cada eixe deste rato 3D. Gárdase para este rato 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrar o rato 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrado",
|
||||
"ui.space_mouse_calibration": "Calibración do rato 3D",
|
||||
"ui.space_mouse_calibration_done": "Feito. Gárdaa para usala sempre que este rato 3D estea conectado.",
|
||||
"ui.space_mouse_calibration_no_response": "Un eixe case non se moveu, así que non se pode calibrar. Volve comezar e move o capuchón máis lonxe cada vez.",
|
||||
"ui.space_mouse_dead_zone": "Zona morta",
|
||||
"ui.space_mouse_dead_zone_tip": "Canto ten que moverse o capuchón antes de que se mova a vista, como parte do seu percorrido completo. Auméntaa se a vista se despraza co capuchón en repouso.",
|
||||
"ui.space_mouse_driven_by": "Que movemento calibrado do dispositivo controla este eixe. Se escolles un que xa usa outro eixe, intercámbianse.",
|
||||
"ui.space_mouse_driven_by_label": "controlado por",
|
||||
"ui.space_mouse_driver": "Controlador do rato 3D",
|
||||
"ui.space_mouse_driver_auto": "Automático",
|
||||
"ui.space_mouse_driver_direct": "Directo (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lectura directa do dispositivo; spacenavd non está en execución",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lectura mediante spacenavd, a única opción dispoñible",
|
||||
"ui.space_mouse_forget": "Esquecer a calibración",
|
||||
"ui.space_mouse_hint": "Move e rota a vista á vez; os bosquexos só usan desprazamento e zoom.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotación X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotación Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotación Z",
|
||||
"ui.space_mouse_motion_translation_x": "Translación X",
|
||||
"ui.space_mouse_motion_translation_y": "Translación Y",
|
||||
"ui.space_mouse_motion_translation_z": "Translación Z",
|
||||
"ui.space_mouse_navigate": "Navegar con SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Non se atopou ningún rato 3D",
|
||||
"ui.space_mouse_not_calibrated": "Sen calibrar",
|
||||
"ui.space_mouse_reading": "Lectura",
|
||||
"ui.space_mouse_reset_axes": "Restablecer asignación de eixes",
|
||||
"ui.space_mouse_step_lift": "Levanta o capuchón, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_press": "Preme o capuchón cara abaixo, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_pull": "Tira do capuchón cara a ti, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_push": "Empurra o capuchón lonxe de ti, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_rest": "Solta o capuchón e déixao quieto.",
|
||||
"ui.space_mouse_step_slide_left": "Despraza o capuchón á esquerda, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_slide_right": "Despraza o capuchón á dereita, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclina o capuchón cara atrás (bordo dianteiro arriba), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclina o capuchón cara adiante (bordo dianteiro abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclina o capuchón á esquerda (bordo esquerdo abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclina o capuchón á dereita (bordo dereito abaixo), mantéño e sóltao.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Xira o capuchón no sentido horario, mantéño e sóltao.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Xira o capuchón no sentido antihorario, mantéño e sóltao.",
|
||||
"ui.space_mouse_view_axis": "Eixe da vista",
|
||||
"ui.space_mouse_view_off": "Desactivado",
|
||||
"ui.space_mouse_view_off_tip": "O perfil do controlador do rato 3D desactiva este eixe, así que o seu movemento non move a vista. Se escolles aquí o movemento doutro eixe, intercámbianse e ese movemento deixa de mover a vista.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbitar á esquerda / dereita",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbitar arriba / abaixo",
|
||||
"ui.space_mouse_view_pan_sideways": "Desprazar á esquerda / dereita",
|
||||
"ui.space_mouse_view_pan_up_down": "Desprazar arriba / abaixo",
|
||||
"ui.space_mouse_view_roll": "Xirar sobre a vista",
|
||||
"ui.space_mouse_view_unmapped": "Eixe {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Espazado",
|
||||
"ui.spacing_2": "Espazado 2",
|
||||
"ui.sphere": "Esfera",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividir o corpo",
|
||||
"ui.stand_up_tip": "Despois de abrir un ficheiro STEP ou IGES con Z cara arriba, ofrecer un cuarto de volta arredor de X",
|
||||
"ui.standard": "Estándar",
|
||||
"ui.start": "Inicio",
|
||||
"ui.start_over": "Volver comezar",
|
||||
"ui.start_page": "Páxina de inicio",
|
||||
|
||||
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Válasszon…",
|
||||
"Circular": "Kör alakú",
|
||||
"Circumference": "Kerület",
|
||||
"Clockwise": "Óramutató járásával megegyező",
|
||||
"Close draft": "Vázlat bezárása",
|
||||
"Collisions": "Ütközések",
|
||||
"Colour": "Szín",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Parancsok keresése",
|
||||
"Search this library": "Keresés ebben a könyvtárban",
|
||||
"Section": "Metszet",
|
||||
"Section position": "Szelvény helyzete",
|
||||
"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).": "Más kijelölése: tartsa nyomva a bal gombot egy helyen a modellen (vagy jobb kattintás › Más kijelölése…) a kurzor alatti elemek listázásához; kattintson újra ugyanarra a helyre a következő elem kijelöléséhez. A középső gomb dupla kattintása a kezdőnézetre vált (Beállítások › Navigáció).",
|
||||
"Select bodies (a face picks its body)": "Testek kijelölése (egy lap a testét jelöli ki)",
|
||||
"Select bodies or faces instead": "Inkább testek vagy lapok kijelölése",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Ügynök kurzorának megjelenítése",
|
||||
"Show in folder": "Megjelenítés a mappában",
|
||||
"Show the pointer an AI agent moves (#35)": "Az AI-ügynök által mozgatott mutató megjelenítése (#35)",
|
||||
"Show tool at cursor": "Eszköz megjelenítése a kurzornál",
|
||||
"Signed axes": "Előjeles tengelyek",
|
||||
"Size": "Méret",
|
||||
"Sketch constraints": "Vázlatkényszerek",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "A SolidCraft bezárult, mielőtt ezek a tervek mentve lettek volna. Az utolsó automatikus mentésük megnyitható.",
|
||||
"Specialise": "Specializálás",
|
||||
"Speed": "Sebesség",
|
||||
"Square": "Négyzet",
|
||||
"Stand it up": "Felállítás",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Felállítás: negyedfordulat az X körül, egyetlen visszavonható Mozgatás elemként.",
|
||||
"Stand this part up?": "Felállítja ezt az alkatrészt?",
|
||||
"Start angle": "Kezdőszög",
|
||||
"Start bulge": "Kezdő ívelés",
|
||||
"Start continuity": "Kezdő folytonosság",
|
||||
"Step": "Lépés",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "szög, magasság",
|
||||
"appearance.dielectric_ior": "Dielektromos törésmutató",
|
||||
"appearance.metallic": "Fémes",
|
||||
"appearance.only_available": "Csak elérhető megjelenések",
|
||||
"appearance.pbr_help": "Tekintse meg ezt a felületet Renderelésben vagy termék-PNG-ben. Nincs fénytörés vagy áteresztés.",
|
||||
"appearance.pbr_opaque": "Átlátszatlan PBR",
|
||||
"appearance.roughness": "Érdesség",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "STEP beszúrása",
|
||||
"cmd.file_save_mesh": "Mentés hálóként",
|
||||
"cmd.file_slicers": "Telepített szeletelők",
|
||||
"cmd.frame_member": "Szerkezeti elem",
|
||||
"cmd.frame_profile_insert": "Profil beszúrása",
|
||||
"cmd.frame_profile_list": "Profilkönyvtár",
|
||||
"cmd.frame_profile_properties": "Profil tulajdonságai",
|
||||
"cmd.inspect_accessibility": "Hozzáférhetőség-elemzés",
|
||||
"cmd.inspect_center_of_mass": "Tömegközéppont",
|
||||
"cmd.inspect_clearance": "Hézag",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Új fizikai anyag",
|
||||
"cmd.material_delete": "Fizikai anyag törlése",
|
||||
"cmd.material_edit": "Fizikai anyag szerkesztése",
|
||||
"cmd.material_inspect": "Fizikai anyag vizsgálata",
|
||||
"cmd.material_list": "Anyagok listázása",
|
||||
"cmd.material_properties_directional": "Irányfüggő rugalmassági modulus",
|
||||
"cmd.material_properties_validate": "Fizikai anyagadatok ellenőrzése",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Kulcs ide",
|
||||
"motion.key_here_hint": "Kulcs hozzáadása ennél a lépésnél a csukló jelenlegi értékével",
|
||||
"motion.no_collisions": "Nincs ütközés a vizsgálat során",
|
||||
"motion.plot_quantity": "Mennyiség",
|
||||
"motion.step_back": "Lépés vissza",
|
||||
"motion.step_forward": "Lépés tovább",
|
||||
"motion.to_end": "A végére",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Vége a(z) {sample}. mintánál: {a} áthatolna ezen: {b}",
|
||||
"trace.stopped_by_loop": "Vége a(z) {sample}. mintánál: a(z) {joint} csuklón átmenő hurok nem zárható tovább",
|
||||
"ui.1_degree_of_freedom": "1 szabadságfok",
|
||||
"ui.3d_mouse_calibrate": "Kalibrálás…",
|
||||
"ui.3d_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-egér kalibrálása",
|
||||
"ui.3d_mouse_calibrate_wait": "Mozdítsa meg egyszer a 3D-egeret, hogy a SolidCraft lássa.",
|
||||
"ui.3d_mouse_calibrated": "Kalibrálva",
|
||||
"ui.3d_mouse_calibration": "3D-egér kalibrálása",
|
||||
"ui.3d_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
|
||||
"ui.3d_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
|
||||
"ui.3d_mouse_circles_help": "A körök azt mutatják, mit küld az eszköz kalibrálás után. A tesztmodell azt mutatja, mit tesz ezzel a nézet a megfordítás, sebesség és tengelykiosztás után.",
|
||||
"ui.3d_mouse_dead_zone": "Holtsáv",
|
||||
"ui.3d_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
|
||||
"ui.3d_mouse_denied": "A SolidCraft látja a 3D-egeret, de nem olvashatja. Az udev-szabályt a 3D-egér beállítási jegyzetei tartalmazzák.",
|
||||
"ui.3d_mouse_denied_short": "Megvan, de nem olvasható",
|
||||
"ui.3d_mouse_driver_fallback": "Egyelőre a spacenavd-n át olvassa: magát az eszközt nem sikerült megnyitni.",
|
||||
"ui.3d_mouse_forget": "Kalibrálás elfelejtése",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Bal nézet",
|
||||
"ui.3d_mouse_ghost_left_window": "Bal ablak",
|
||||
"ui.3d_mouse_ghost_position": "Helyzet",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Jobb nézet",
|
||||
"ui.3d_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
|
||||
"ui.3d_mouse_navigate": "Navigálás a 3D-egérrel",
|
||||
"ui.3d_mouse_none": "Nem található 3D-egér",
|
||||
"ui.3d_mouse_not_calibrated": "Nincs kalibrálva",
|
||||
"ui.3d_mouse_offer": "A 3D-egér kalibrálása kijavítja a felcserélt vagy megfordított tengelyeket, és beállítja a sebességét és az érzetét.",
|
||||
"ui.3d_mouse_offer_never": "Ne jelenjen meg többé",
|
||||
"ui.3d_mouse_offer_swap": "Váltás Hidrawra (közvetlen illesztőprogram)",
|
||||
"ui.3d_mouse_reading": "Beolvasás",
|
||||
"ui.3d_mouse_reset_view": "Nézet alaphelyzetbe állítása",
|
||||
"ui.3d_mouse_restore": "3D-egér alapértékeinek visszaállítása",
|
||||
"ui.3d_mouse_restore_ask": "Visszaállítja a 3D-egér beállításait az alapértékekre? A kalibrálás és a gombkiosztás megmarad.",
|
||||
"ui.3d_mouse_speed": "3D-egér sebessége",
|
||||
"ui.3d_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
|
||||
"ui.3d_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
|
||||
"ui.3d_mouse_waiting": "Mozgásra vár",
|
||||
"ui.3d_mouse_wiggle": "Ha van csatlakoztatva egy, mozgassa meg egy kicsit.",
|
||||
"ui.3d_sketch": "3D vázlat",
|
||||
"ui.activate": "Aktiválás",
|
||||
"ui.active_component": "Aktív komponens",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "a végpont foglalt vagy túllépte a korlátját; próbálja később",
|
||||
"ui.ai_http_key": "a végpont elutasította a kulcsot: ellenőrizze a Beállítások > AI menüben",
|
||||
"ui.ai_http_not_found": "nincs ilyen végpont vagy modell: ellenőrizze az alap-URL-t és a modell nevét",
|
||||
"ui.ai_http_too_large": "a beszélgetés túl nagy ehhez a végponthoz; kezdjen új csevegést",
|
||||
"ui.ai_key": "API-kulcs",
|
||||
"ui.ai_key_hint": "opcionális; a rendszer kulcstartójában tárolódik",
|
||||
"ui.ai_key_needs_https": "A kulcs csak HTTPS-en vagy erre a számítógépre kerül elküldésre.",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Jogosultságok",
|
||||
"ui.ai_preset": "Előbeállítás",
|
||||
"ui.ai_remove": "Eltávolítás",
|
||||
"ui.ai_request_limit": "egy üzenetre eső 24 kérés után leállt; küldjön újabb üzenetet a folytatáshoz",
|
||||
"ui.ai_reset": "Visszaállítás",
|
||||
"ui.ai_settings": "AI-beállítások…",
|
||||
"ui.ai_show_features": "AI-funkciók megjelenítése",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Parancsok vagy billentyűk szűrése",
|
||||
"ui.findInTimeline": "Keresés az idővonalon",
|
||||
"ui.findInWindow": "Keresés az ablakban",
|
||||
"ui.fine": "Finom",
|
||||
"ui.finest": "Legfinomabb",
|
||||
"ui.finish_sketch": "Vázlat befejezése",
|
||||
"ui.fit": "Illesztés",
|
||||
"ui.fix_unfix": "Rögzítés/feloldás",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "TÓRUSZ",
|
||||
"ui.heading_utilities": "SEGÉDESZKÖZÖK",
|
||||
"ui.heading_utility": "SEGÉDESZKÖZ",
|
||||
"ui.heading_weldments": "HEGESZTÉS",
|
||||
"ui.help": "Súgó",
|
||||
"ui.hide": "Elrejtés",
|
||||
"ui.hideAxes": "Tengelyek elrejtése",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Lapélek vetítése új vázlatokba",
|
||||
"ui.properties": "Tulajdonságok",
|
||||
"ui.pull_direction": "Húzási irány",
|
||||
"ui.quality": "Minőség",
|
||||
"ui.quantity": "Mennyiség",
|
||||
"ui.quantity_2": "2. mennyiség",
|
||||
"ui.quit": "Kilépés",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Sima görgőnagyítás",
|
||||
"ui.smooth_wheel_zoom_tip": "Kapcsolja ki az egyes görgőesemények azonnali alkalmazásához, kifutás nélkül",
|
||||
"ui.snap": "Illesztés",
|
||||
"ui.space_mouse_calibrate": "Kalibrálás…",
|
||||
"ui.space_mouse_calibrate_tip": "Mozgassa a kupakot az ablak kérése szerint, hogy a SolidCraft megtanulja, merre és mennyire mozdul a 3D-egér mindegyik tengelye. Ehhez a 3D-egérhez mentve.",
|
||||
"ui.space_mouse_calibrate_title": "3D-egér kalibrálása",
|
||||
"ui.space_mouse_calibrated": "Kalibrálva",
|
||||
"ui.space_mouse_calibration": "3D-egér kalibrálása",
|
||||
"ui.space_mouse_calibration_done": "Kész. Mentse, hogy a 3D-egér csatlakoztatásakor mindig használatos legyen.",
|
||||
"ui.space_mouse_calibration_no_response": "Az egyik tengely alig mozdult, így nem kalibrálható. Kezdje elölről, és minden alkalommal mozgassa a kupakot messzebb.",
|
||||
"ui.space_mouse_dead_zone": "Holtsáv",
|
||||
"ui.space_mouse_dead_zone_tip": "Mennyit kell mozdítani a kupakot, mielőtt a nézet mozdulna, a teljes elmozdulás arányában. Növelje, ha a nézet sodródik, miközben a kupak nyugalomban van.",
|
||||
"ui.space_mouse_forget": "Kalibrálás elfelejtése",
|
||||
"ui.space_mouse_hint": "A nézetet együtt mozgatja és forgatja; a vázlatok csak pásztázást és nagyítást használnak.",
|
||||
"ui.space_mouse_navigate": "Navigálás SpaceNavigator / SpaceMouse eszközzel",
|
||||
"ui.space_mouse_none": "Nem található 3D-egér",
|
||||
"ui.space_mouse_not_calibrated": "Nincs kalibrálva",
|
||||
"ui.space_mouse_reading": "Beolvasás",
|
||||
"ui.space_mouse_step_lift": "Emelje fel a kupakot, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_press": "Nyomja le a kupakot, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_pull": "Húzza a kupakot maga felé, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_push": "Tolja a kupakot magától el, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_rest": "Vegye le a kezét a kupakról, és hagyja nyugodtan.",
|
||||
"ui.space_mouse_step_slide_left": "Csúsztassa a kupakot balra, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_slide_right": "Csúsztassa a kupakot jobbra, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_back": "Billentse hátra a kupakot (az elülső él felfelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_forward": "Billentse előre a kupakot (az elülső él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_left": "Billentse balra a kupakot (a bal él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_tilt_right": "Billentse jobbra a kupakot (a jobb él lefelé), tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Csavarja el a kupakot az óramutató járásával megegyezően, tartsa, majd engedje el.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Csavarja el a kupakot az óramutató járásával ellentétesen, tartsa, majd engedje el.",
|
||||
"ui.spacing": "Térköz",
|
||||
"ui.spacing_2": "2. térköz",
|
||||
"ui.sphere": "Gömb",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Test felosztása",
|
||||
"ui.stand_up_tip": "Miután egy STEP vagy IGES fájl Z felfelé tájolással megnyílik, negyedfordulat felajánlása az X körül",
|
||||
"ui.standard": "Normál",
|
||||
"ui.start": "Kezdés",
|
||||
"ui.start_over": "Újrakezdés",
|
||||
"ui.start_page": "Kezdőoldal",
|
||||
|
||||
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Melingkar",
|
||||
"Circumference": "Keliling",
|
||||
"Clockwise": "Searah jarum jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Tabrakan",
|
||||
"Colour": "Warna",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Cari perintah",
|
||||
"Search this library": "Cari di pustaka ini",
|
||||
"Section": "Penampang",
|
||||
"Section position": "Posisi penampang",
|
||||
"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: tahan tombol kiri diam di model (atau klik kanan › Select Other…) untuk mendaftar semua yang ada di bawah kursor; klik lagi di titik yang sama untuk memilih benda berikutnya. Klik dua kali tombol tengah untuk ke Home (Preferensi › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih bodi (memilih muka berarti memilih bodinya)",
|
||||
"Select bodies or faces instead": "Pilih bodi atau muka saja",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Tampilkan Kursor Agen",
|
||||
"Show in folder": "Tampilkan di folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Tampilkan penunjuk yang digerakkan agen AI (#35)",
|
||||
"Show tool at cursor": "Tampilkan alat di dekat kursor",
|
||||
"Signed axes": "Sumbu bertanda",
|
||||
"Size": "Ukuran",
|
||||
"Sketch constraints": "Batasan sketsa",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft tertutup sebelum desain ini disimpan. Simpan otomatis terakhirnya dapat dibuka.",
|
||||
"Specialise": "Spesialisasikan",
|
||||
"Speed": "Kecepatan",
|
||||
"Square": "Persegi",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: putaran seperempat terhadap X, sebagai satu fitur Move yang dapat dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan part ini?",
|
||||
"Start angle": "Sudut awal",
|
||||
"Start bulge": "Bulge awal",
|
||||
"Start continuity": "Kontinuitas awal",
|
||||
"Step": "Langkah",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "sudut, tinggi",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Metalik",
|
||||
"appearance.only_available": "Hanya tampilan yang tersedia",
|
||||
"appearance.pbr_help": "Lihat permukaan ini di Render atau PNG produk. Tanpa refraksi atau transmisi.",
|
||||
"appearance.pbr_opaque": "PBR Opak",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Sisipkan STEP",
|
||||
"cmd.file_save_mesh": "Simpan Sebagai Mesh",
|
||||
"cmd.file_slicers": "Slicer Terpasang",
|
||||
"cmd.frame_member": "Batang Rangka",
|
||||
"cmd.frame_profile_insert": "Sisipkan Profil",
|
||||
"cmd.frame_profile_list": "Pustaka Profil",
|
||||
"cmd.frame_profile_properties": "Properti Profil",
|
||||
"cmd.inspect_accessibility": "Analisis Aksesibilitas",
|
||||
"cmd.inspect_center_of_mass": "Pusat Massa",
|
||||
"cmd.inspect_clearance": "Kelonggaran",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Material Fisik Baru",
|
||||
"cmd.material_delete": "Hapus Material Fisik",
|
||||
"cmd.material_edit": "Edit Material Fisik",
|
||||
"cmd.material_inspect": "Periksa Material Fisik",
|
||||
"cmd.material_list": "Daftar Material",
|
||||
"cmd.material_properties_directional": "Modulus Elastis Arah",
|
||||
"cmd.material_properties_validate": "Validasi Data Material Fisik",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Key di sini",
|
||||
"motion.key_here_hint": "Tambah key pada langkah ini dengan nilai sambungan saat ini",
|
||||
"motion.no_collisions": "Tidak ada tabrakan sepanjang studi",
|
||||
"motion.plot_quantity": "Jumlah",
|
||||
"motion.step_back": "Mundur satu langkah",
|
||||
"motion.step_forward": "Maju satu langkah",
|
||||
"motion.to_end": "Ke akhir",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Berakhir di sampel {sample}: {a} akan menembus {b}",
|
||||
"trace.stopped_by_loop": "Berakhir di sampel {sample}: loop melalui {joint} tidak dapat menutup lebih jauh",
|
||||
"ui.1_degree_of_freedom": "1 derajat kebebasan",
|
||||
"ui.3d_mouse_calibrate": "Kalibrasi…",
|
||||
"ui.3d_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrasi mouse 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Gerakkan mouse 3D sekali agar SolidCraft dapat melihatnya.",
|
||||
"ui.3d_mouse_calibrated": "Terkalibrasi",
|
||||
"ui.3d_mouse_calibration": "Kalibrasi mouse 3D",
|
||||
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
|
||||
"ui.3d_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
|
||||
"ui.3d_mouse_circles_help": "Lingkaran menunjukkan apa yang dikirim perangkat setelah kalibrasi. Model uji menunjukkan apa yang dilakukan tampilan dengannya, setelah pembalikan, kecepatan, dan penetapan sumbu.",
|
||||
"ui.3d_mouse_dead_zone": "Zona mati",
|
||||
"ui.3d_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
|
||||
"ui.3d_mouse_denied": "SolidCraft melihat mouse 3D tetapi tidak diizinkan membacanya. Lihat catatan penyiapan mouse 3D untuk aturan udev.",
|
||||
"ui.3d_mouse_denied_short": "Ditemukan, tetapi tidak bisa dibaca",
|
||||
"ui.3d_mouse_driver_fallback": "Sementara dibaca lewat spacenavd: perangkatnya sendiri tidak dapat dibuka.",
|
||||
"ui.3d_mouse_forget": "Lupakan kalibrasi",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
|
||||
"ui.3d_mouse_ghost_left_window": "Jendela kiri",
|
||||
"ui.3d_mouse_ghost_position": "Posisi",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
|
||||
"ui.3d_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
|
||||
"ui.3d_mouse_navigate": "Navigasi dengan mouse 3D",
|
||||
"ui.3d_mouse_none": "Mouse 3D tidak ditemukan",
|
||||
"ui.3d_mouse_not_calibrated": "Belum dikalibrasi",
|
||||
"ui.3d_mouse_offer": "Mengkalibrasi mouse 3D memperbaiki sumbu yang tertukar atau terbalik dan mengatur kecepatan serta rasanya.",
|
||||
"ui.3d_mouse_offer_never": "Jangan tampilkan lagi",
|
||||
"ui.3d_mouse_offer_swap": "Beralih ke Hidraw (driver langsung)",
|
||||
"ui.3d_mouse_reading": "Pembacaan",
|
||||
"ui.3d_mouse_reset_view": "Atur ulang tampilan",
|
||||
"ui.3d_mouse_restore": "Pulihkan bawaan mouse 3D",
|
||||
"ui.3d_mouse_restore_ask": "Kembalikan pengaturan mouse 3D ke bawaan? Kalibrasi dan pemetaan tombol tetap ada.",
|
||||
"ui.3d_mouse_speed": "Kecepatan mouse 3D",
|
||||
"ui.3d_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
|
||||
"ui.3d_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.3d_mouse_waiting": "Menunggu gerakan",
|
||||
"ui.3d_mouse_wiggle": "Jika Anda memasang satu, goyangkan sedikit.",
|
||||
"ui.3d_sketch": "Sketsa 3D",
|
||||
"ui.activate": "Aktifkan",
|
||||
"ui.active_component": "Komponen aktif",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "endpoint sibuk atau melebihi batas; coba lagi nanti",
|
||||
"ui.ai_http_key": "endpoint menolak key: periksa di Preferensi > AI",
|
||||
"ui.ai_http_not_found": "endpoint atau model tidak ada: periksa URL dasar dan nama model",
|
||||
"ui.ai_http_too_large": "percakapan terlalu besar untuk endpoint ini; mulai obrolan baru",
|
||||
"ui.ai_key": "Key API",
|
||||
"ui.ai_key_hint": "opsional; disimpan di keychain sistem",
|
||||
"ui.ai_key_needs_https": "Key hanya dikirim lewat HTTPS, atau ke komputer ini.",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Izin",
|
||||
"ui.ai_preset": "Preset",
|
||||
"ui.ai_remove": "Hapus",
|
||||
"ui.ai_request_limit": "dihentikan setelah 24 permintaan untuk satu pesan; kirim pesan lain untuk melanjutkan",
|
||||
"ui.ai_reset": "Reset",
|
||||
"ui.ai_settings": "Pengaturan AI…",
|
||||
"ui.ai_show_features": "Tampilkan fitur AI",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filter perintah atau tombol",
|
||||
"ui.findInTimeline": "Cari di Linimasa",
|
||||
"ui.findInWindow": "Cari di Jendela",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Sangat halus",
|
||||
"ui.finish_sketch": "Selesai Sketsa",
|
||||
"ui.fit": "Pas",
|
||||
"ui.fix_unfix": "Kunci/Lepas Kunci",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITAS",
|
||||
"ui.heading_utility": "UTILITAS",
|
||||
"ui.heading_weldments": "KONSTRUKSI LAS",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyikan",
|
||||
"ui.hideAxes": "Sembunyikan Sumbu",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Proyeksikan tepi muka ke sketsa baru",
|
||||
"ui.properties": "Properti",
|
||||
"ui.pull_direction": "Arah tarik",
|
||||
"ui.quality": "Kualitas",
|
||||
"ui.quantity": "Jumlah",
|
||||
"ui.quantity_2": "Jumlah 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom roda halus",
|
||||
"ui.smooth_wheel_zoom_tip": "Matikan untuk menerapkan setiap peristiwa roda segera, tanpa ekor penghalusan",
|
||||
"ui.snap": "Snap",
|
||||
"ui.space_mouse_calibrate": "Kalibrasi…",
|
||||
"ui.space_mouse_calibrate_tip": "Gerakkan cap sesuai permintaan jendela, agar SolidCraft mempelajari arah dan jarak setiap sumbu mouse 3D ini. Disimpan untuk mouse 3D ini.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrasi mouse 3D",
|
||||
"ui.space_mouse_calibrated": "Terkalibrasi",
|
||||
"ui.space_mouse_calibration": "Kalibrasi mouse 3D",
|
||||
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk dipakai setiap kali mouse 3D ini terhubung.",
|
||||
"ui.space_mouse_calibration_no_response": "Satu sumbu hampir tidak bergerak, sehingga tidak dapat dikalibrasi. Mulai ulang dan gerakkan cap lebih jauh setiap kali.",
|
||||
"ui.space_mouse_dead_zone": "Zona mati",
|
||||
"ui.space_mouse_dead_zone_tip": "Seberapa jauh cap harus bergerak sebelum tampilan bergerak, sebagai bagian dari total jangkauannya. Naikkan jika tampilan bergeser saat cap diam.",
|
||||
"ui.space_mouse_forget": "Lupakan kalibrasi",
|
||||
"ui.space_mouse_hint": "Gerakkan dan putar tampilan bersama; sketsa hanya memakai pan dan zoom.",
|
||||
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Mouse 3D tidak ditemukan",
|
||||
"ui.space_mouse_not_calibrated": "Belum dikalibrasi",
|
||||
"ui.space_mouse_reading": "Pembacaan",
|
||||
"ui.space_mouse_step_lift": "Angkat cap ke atas, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_press": "Tekan cap ke bawah, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_pull": "Tarik cap ke arah Anda, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_push": "Dorong cap menjauh dari Anda, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_rest": "Lepaskan tangan dari cap dan biarkan diam.",
|
||||
"ui.space_mouse_step_slide_left": "Geser cap ke kiri, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_slide_right": "Geser cap ke kanan, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_back": "Miringkan cap ke belakang (tepi depan naik), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Miringkan cap ke depan (tepi depan turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_left": "Miringkan cap ke kiri (tepi kiri turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_tilt_right": "Miringkan cap ke kanan (tepi kanan turun), tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Putar cap searah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Putar cap berlawanan arah jarum jam, tahan, lalu lepaskan.",
|
||||
"ui.spacing": "Jarak antar",
|
||||
"ui.spacing_2": "Jarak antar 2",
|
||||
"ui.sphere": "Bola",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Belah Bodi",
|
||||
"ui.stand_up_tip": "Setelah file STEP atau IGES terbuka dengan Z ke atas, tawarkan putaran seperempat terhadap X",
|
||||
"ui.standard": "Standar",
|
||||
"ui.start": "Mulai",
|
||||
"ui.start_over": "Mulai ulang",
|
||||
"ui.start_page": "Halaman awal",
|
||||
|
||||
+155
-54
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Scegli…",
|
||||
"Circular": "Circolare",
|
||||
"Circumference": "Circonferenza",
|
||||
"Clockwise": "Orario",
|
||||
"Close draft": "Chiudi bozza",
|
||||
"Collisions": "Collisioni",
|
||||
"Colour": "Colore",
|
||||
@@ -163,6 +164,7 @@
|
||||
"From Bodies": "Dai corpi",
|
||||
"From edges": "Spigoli di partenza",
|
||||
"Full Length": "Lunghezza totale",
|
||||
"Full thread depth": "Profondità filetto completa",
|
||||
"Gap": "Gioco",
|
||||
"Grid": "Griglia",
|
||||
"HISTORY": "CRONOLOGIA",
|
||||
@@ -325,6 +327,7 @@
|
||||
"Search commands": "Cerca comandi",
|
||||
"Search this library": "Cerca in questa libreria",
|
||||
"Section": "Sezione",
|
||||
"Section position": "Posizione della sezione",
|
||||
"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).": "Seleziona altro: tieni premuto il pulsante sinistro senza muovere il puntatore sul modello (o fai clic destro › Seleziona altro…) per elencare gli oggetti sotto il cursore; fai di nuovo clic nello stesso punto per selezionare l'oggetto successivo. Fai doppio clic con il pulsante centrale per tornare alla vista iniziale (Preferenze › Navigazione).",
|
||||
"Select bodies (a face picks its body)": "Seleziona corpi (una faccia seleziona il corpo a cui appartiene)",
|
||||
"Select bodies or faces instead": "Seleziona invece corpi o facce",
|
||||
@@ -336,6 +339,7 @@
|
||||
"Show Agent Cursor": "Mostra cursore dell'agente",
|
||||
"Show in folder": "Mostra nella cartella",
|
||||
"Show the pointer an AI agent moves (#35)": "Mostra il puntatore mosso da un agente IA (#35)",
|
||||
"Show tool at cursor": "Mostra lo strumento accanto al cursore",
|
||||
"Signed axes": "Assi con segno",
|
||||
"Size": "Dimensioni",
|
||||
"Sketch constraints": "Vincoli dello schizzo",
|
||||
@@ -348,9 +352,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft si è chiuso prima del salvataggio di questi progetti. È possibile aprire il loro ultimo salvataggio automatico.",
|
||||
"Specialise": "Specializza",
|
||||
"Speed": "Velocità",
|
||||
"Square": "Quadrato",
|
||||
"Stand it up": "Raddrizza",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Raddrizza: un quarto di giro attorno a X, come singola lavorazione Sposta annullabile.",
|
||||
"Stand this part up?": "Raddrizzare questa parte?",
|
||||
"Start angle": "Angolo iniziale",
|
||||
"Start bulge": "Rigonfiamento iniziale",
|
||||
"Start continuity": "Continuità iniziale",
|
||||
"Step": "Passo",
|
||||
@@ -377,6 +383,7 @@
|
||||
"This child is shared. Deleting it removes it from every copy of its parent.": "Questo elemento figlio è condiviso. Eliminandolo verrà rimosso da ogni copia del padre.",
|
||||
"This child is shared. Position changes apply to every copy of its parent.": "Questo elemento figlio è condiviso. Le modifiche alla posizione si applicano a ogni copia del padre.",
|
||||
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Questa faccia non consente un collegamento persistente. Scegli un'altra faccia o usa esplicitamente un piano fisso.",
|
||||
"Thread depth": "Profondità filetto",
|
||||
"To Offset": "Fino a una distanza",
|
||||
"To edges": "Spigoli di arrivo",
|
||||
"Tolerance": "Tolleranza",
|
||||
@@ -415,6 +422,7 @@
|
||||
"angle, height": "angolo, altezza",
|
||||
"appearance.dielectric_ior": "Indice di rifrazione dielettrico",
|
||||
"appearance.metallic": "Metallicità",
|
||||
"appearance.only_available": "Solo aspetti disponibili",
|
||||
"appearance.pbr_help": "Visualizza questa superficie in Rendering o in un PNG del prodotto. Senza rifrazione né trasmissione.",
|
||||
"appearance.pbr_opaque": "PBR opaco",
|
||||
"appearance.roughness": "Rugosità",
|
||||
@@ -533,6 +541,10 @@
|
||||
"cmd.file_insert_step": "Inserisci STEP",
|
||||
"cmd.file_save_mesh": "Salva come mesh",
|
||||
"cmd.file_slicers": "Slicer installati",
|
||||
"cmd.frame_member": "Membro strutturale",
|
||||
"cmd.frame_profile_insert": "Inserisci profilo",
|
||||
"cmd.frame_profile_list": "Libreria profili",
|
||||
"cmd.frame_profile_properties": "Proprietà del profilo",
|
||||
"cmd.inspect_accessibility": "Analisi di accessibilità",
|
||||
"cmd.inspect_center_of_mass": "Centro di massa",
|
||||
"cmd.inspect_clearance": "Distanza libera",
|
||||
@@ -572,6 +584,7 @@
|
||||
"cmd.material_create": "Nuovo materiale fisico",
|
||||
"cmd.material_delete": "Elimina materiale fisico",
|
||||
"cmd.material_edit": "Modifica materiale fisico",
|
||||
"cmd.material_inspect": "Ispeziona materiale fisico",
|
||||
"cmd.material_list": "Elenca materiali",
|
||||
"cmd.material_properties_directional": "Modulo elastico direzionale",
|
||||
"cmd.material_properties_validate": "Convalida dati del materiale fisico",
|
||||
@@ -860,6 +873,26 @@
|
||||
"help.mit": "Licenza MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.third_party": "Licenze di terze parti",
|
||||
"hole_presets.confirm_delete": "Conferma eliminazione",
|
||||
"hole_presets.delete": "Elimina",
|
||||
"hole_presets.delete_prompt": "Eliminare questo preset personale? I fori esistenti restano invariati.",
|
||||
"hole_presets.full": "La raccolta personale contiene già 64 preset.",
|
||||
"hole_presets.invalid_name": "Inserisci un nome breve, senza a capo o caratteri di controllo.",
|
||||
"hole_presets.invalid_parameters": "Completa i parametri del foro con espressioni valide e di lunghezza ragionevole.",
|
||||
"hole_presets.missing": "Il preset selezionato non è più disponibile.",
|
||||
"hole_presets.name": "Nome preset",
|
||||
"hole_presets.name_exists": "Esiste già un preset con questo nome. Usa un nome diverso.",
|
||||
"hole_presets.name_hint": "Un nome univoco",
|
||||
"hole_presets.overwrite": "Salva le modifiche nel preset selezionato",
|
||||
"hole_presets.preset": "Preset",
|
||||
"hole_presets.recall": "Richiama",
|
||||
"hole_presets.rename": "Rinomina",
|
||||
"hole_presets.save": "Salva nuovo",
|
||||
"hole_presets.scope": "Salvati nel tuo profilo per tutti i progetti.",
|
||||
"hole_presets.select": "Seleziona un preset",
|
||||
"hole_presets.title": "Preset personali",
|
||||
"hole_presets.unsupported_options": "Questo preset usa opzioni del filetto che questa versione non può richiamare.",
|
||||
"hole_presets.unsupported_overwrite": "Questo preset contiene opzioni che non possono essere sovrascritte in sicurezza.",
|
||||
"home.back": "Torna a {name}",
|
||||
"home.crashes": "Rapporti di arresto anomalo",
|
||||
"home.design_workspace": "Progettazione",
|
||||
@@ -963,6 +996,7 @@
|
||||
"motion.key_here": "Inserisci fotogramma chiave qui",
|
||||
"motion.key_here_hint": "Aggiungi un fotogramma chiave in questo passo con il valore attuale del giunto",
|
||||
"motion.no_collisions": "Nessuna collisione durante lo studio",
|
||||
"motion.plot_quantity": "Quantità",
|
||||
"motion.step_back": "Passo indietro",
|
||||
"motion.step_forward": "Passo avanti",
|
||||
"motion.to_end": "Alla fine",
|
||||
@@ -988,6 +1022,16 @@
|
||||
"pattern.direction_1": "Direzione 1",
|
||||
"pattern.direction_2": "Direzione 2",
|
||||
"pattern.flip_direction": "Inverti direzione",
|
||||
"perf.click_for_details": "Fai clic per i dettagli: grafico dei tempi, istogramma e ciò che la GPU ha disegnato",
|
||||
"perf.draw_calls": "Chiamate di disegno",
|
||||
"perf.line_segments": "Segmenti",
|
||||
"perf.low_1": "1% più lento",
|
||||
"perf.renderer": "Renderer",
|
||||
"perf.scene_build": "Costruzione scena",
|
||||
"perf.triangles": "Triangoli",
|
||||
"perf.ui_time": "Tempo UI",
|
||||
"perf.viewport": "Vista",
|
||||
"perf.worst": "peggiore",
|
||||
"pick a hole to seat it, or OK places it at the origin": "seleziona un foro in cui inserirlo, oppure premi OK per posizionarlo nell'origine",
|
||||
"pick one or two vertices, edges or faces": "seleziona uno o due vertici, spigoli o facce",
|
||||
"prefs.cpu": "CPU (software)",
|
||||
@@ -1063,6 +1107,7 @@
|
||||
"search.try": "Digita il nome di un comando, una feature di questo progetto o un parametro. Per eseguire un comando con dei valori, digita il suo id e un oggetto JSON: {example}",
|
||||
"search.valid": "Valido",
|
||||
"settings.display_quality_note": "I livelli più fini rendono più uniformi le facce curve e gli spigoli e richiedono più tempo per la mesh. Cambia solo la vista, mai il progetto o le esportazioni.",
|
||||
"settings.frame_rate_details_note": "Aggiunge un grafico dei tempi degli ultimi fotogrammi con i triangoli disegnati sopra, un istogramma dei tempi dei fotogrammi e ciò che la GPU ha disegnato. Fai clic sulla frequenza nella vista per mostrarli o nasconderli.",
|
||||
"settings.frame_rate_note": "Mostra i fotogrammi al secondo nell'angolo inferiore destro della vista.",
|
||||
"settings.measure_axis_distances": "Mostra distanze X/Y/Z",
|
||||
"settings.measure_axis_distances_help": "Mostra le componenti lungo gli assi globali accanto alla distanza totale quando Misura è aperto.",
|
||||
@@ -1106,7 +1151,10 @@
|
||||
"the parts library is not available": "la libreria delle parti non è disponibile",
|
||||
"thread.automatic_size": "Dimensione automatica",
|
||||
"thread.designation": "Designazione",
|
||||
"thread.direction": "Verso del filetto",
|
||||
"thread.iso_metric": "Metrica ISO",
|
||||
"thread.left_hand": "Sinistro",
|
||||
"thread.right_hand": "Destro",
|
||||
"thread.select_size": "Seleziona dimensione",
|
||||
"timeline.group": "Nuova cartella di lavorazioni",
|
||||
"timeline.move_many": "Sposta elementi della sequenza temporale",
|
||||
@@ -1124,7 +1172,106 @@
|
||||
"trace.stopped_by_contact": "Termina al campione {sample}: {a} attraverserebbe {b}",
|
||||
"trace.stopped_by_loop": "Termina al campione {sample}: la catena chiusa che passa per {joint} non può chiudersi ulteriormente",
|
||||
"ui.1_degree_of_freedom": "1 grado di libertà",
|
||||
"ui.3d_mouse_axes_reassigned": "Gli assi sono riassegnati nelle Preferenze, e questo vale anche dopo questa calibrazione.",
|
||||
"ui.3d_mouse_button": "Pulsante {number}",
|
||||
"ui.3d_mouse_button_nothing": "Niente",
|
||||
"ui.3d_mouse_button_settings": "Impostazioni del mouse 3D",
|
||||
"ui.3d_mouse_buttons": "Pulsanti",
|
||||
"ui.3d_mouse_buttons_hint": "Premi un pulsante del tuo mouse 3D per trovarlo qui, poi scegli cosa fa.",
|
||||
"ui.3d_mouse_buttons_no_device": "Collega un mouse 3D per scegliere cosa fanno i suoi pulsanti.",
|
||||
"ui.3d_mouse_buttons_reset": "Ripristina i pulsanti predefiniti",
|
||||
"ui.3d_mouse_calibrate": "Calibra…",
|
||||
"ui.3d_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibra mouse 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Muovi il mouse 3D una volta così SolidCraft lo vede.",
|
||||
"ui.3d_mouse_calibrated": "Calibrato",
|
||||
"ui.3d_mouse_calibration": "Calibrazione del mouse 3D",
|
||||
"ui.3d_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
|
||||
"ui.3d_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
|
||||
"ui.3d_mouse_circles_help": "I cerchi mostrano ciò che il dispositivo invia dopo la calibrazione. Il modello di prova mostra cosa ne fa la vista, dopo inversione, velocità e assegnazione degli assi.",
|
||||
"ui.3d_mouse_dead_zone": "Zona morta",
|
||||
"ui.3d_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D ma non ha il permesso di leggerlo. La regola udev è nelle note di configurazione del mouse 3D.",
|
||||
"ui.3d_mouse_denied_short": "Trovato, ma non leggibile",
|
||||
"ui.3d_mouse_driven_by": "Quale movimento calibrato del dispositivo comanda questo asse. Se ne scegli uno già usato da un altro asse, i due si scambiano.",
|
||||
"ui.3d_mouse_driven_by_label": "comandato da",
|
||||
"ui.3d_mouse_driver": "Driver del mouse 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatico",
|
||||
"ui.3d_mouse_driver_direct": "Diretto (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Per ora legge tramite spacenavd: il dispositivo stesso non si è potuto aprire.",
|
||||
"ui.3d_mouse_driver_only_direct": "Lettura diretta del dispositivo; spacenavd non è in esecuzione",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Lettura tramite spacenavd, l'unica possibilità disponibile",
|
||||
"ui.3d_mouse_forget": "Dimentica calibrazione",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vista a sinistra",
|
||||
"ui.3d_mouse_ghost_left_window": "Finestra a sinistra",
|
||||
"ui.3d_mouse_ghost_position": "Posizione",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vista a destra",
|
||||
"ui.3d_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
|
||||
"ui.3d_mouse_kind_cap": "Ritorno al centro",
|
||||
"ui.3d_mouse_kind_question": "Che tipo di mouse 3D è?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Il valore resta finché tieni, come in un dispositivo di tipo «Ritorno al centro».",
|
||||
"ui.3d_mouse_looks_like_trackball": "Il valore torna indietro appena smetti di muovere, come in una trackball.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Rotazione X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Rotazione Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Rotazione Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Traslazione X",
|
||||
"ui.3d_mouse_motion_translation_y": "Traslazione Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Traslazione Z",
|
||||
"ui.3d_mouse_navigate": "Naviga con il mouse 3D",
|
||||
"ui.3d_mouse_none": "Nessun mouse 3D trovato",
|
||||
"ui.3d_mouse_not_calibrated": "Non calibrato",
|
||||
"ui.3d_mouse_offer": "Calibrare il mouse 3D corregge gli assi scambiati o invertiti e imposta velocità e sensibilità.",
|
||||
"ui.3d_mouse_offer_never": "Non mostrare più",
|
||||
"ui.3d_mouse_offer_swap": "Passa a Hidraw (driver diretto)",
|
||||
"ui.3d_mouse_reading": "Lettura",
|
||||
"ui.3d_mouse_reset_axes": "Ripristina assegnazione assi",
|
||||
"ui.3d_mouse_reset_view": "Ripristina vista",
|
||||
"ui.3d_mouse_restore": "Ripristina impostazioni mouse 3D",
|
||||
"ui.3d_mouse_restore_ask": "Riportare le impostazioni del mouse 3D ai valori predefiniti? Calibrazione e assegnazione dei pulsanti restano.",
|
||||
"ui.3d_mouse_section": "Mouse 3D",
|
||||
"ui.3d_mouse_show_in_view": "Mostra il mouse 3D nella vista 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Un'immagine del tuo mouse 3D nell'angolo in basso a sinistra, che si muove quando lo muovi.",
|
||||
"ui.3d_mouse_skip": "Il mio dispositivo non può farlo",
|
||||
"ui.3d_mouse_skip_tip": "Salta questo movimento in entrambi i versi; la calibrazione lo esclude.",
|
||||
"ui.3d_mouse_speed": "Velocità del mouse 3D",
|
||||
"ui.3d_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Togli la mano dalla sfera e lasciala ferma.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Gira la ghiera di scorrimento di qualche scatto in senso orario.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Gira la ghiera di scorrimento di qualche scatto in senso antiorario.",
|
||||
"ui.3d_mouse_step_roll_away": "Fai rotolare la sfera lontano da te qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_left": "Fai rotolare la sfera verso sinistra qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_right": "Fai rotolare la sfera verso destra qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_roll_toward": "Fai rotolare la sfera verso di te qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Ruota la sfera in senso orario qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Ruota la sfera in senso antiorario qualche volta, poi lasciala fermare.",
|
||||
"ui.3d_mouse_step_tilt_away": "Inclina la parte superiore della manopola lontano da te (il bordo più vicino sale), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Inclina la parte superiore della manopola verso di te (il bordo più vicino scende), tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_touch": "Appoggia la mano sulla manopola senza spingerla.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
|
||||
"ui.3d_mouse_switch_cap": "Calibra come ritorno al centro",
|
||||
"ui.3d_mouse_switch_trackball": "Calibra come trackball",
|
||||
"ui.3d_mouse_view_axis": "Asse della vista",
|
||||
"ui.3d_mouse_view_off": "Disattivato",
|
||||
"ui.3d_mouse_view_off_tip": "Il profilo del driver del mouse 3D disattiva questo asse, quindi il suo movimento non muove la vista. Se qui scegli il movimento di un altro asse, i due si scambiano e quel movimento smette di muovere la vista.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbita sinistra / destra",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbita su / giù",
|
||||
"ui.3d_mouse_view_pan_sideways": "Panoramica sinistra / destra",
|
||||
"ui.3d_mouse_view_pan_up_down": "Panoramica su / giù",
|
||||
"ui.3d_mouse_view_roll": "Rollio",
|
||||
"ui.3d_mouse_view_unmapped": "Asse {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zoom",
|
||||
"ui.3d_mouse_waiting": "In attesa di movimento",
|
||||
"ui.3d_mouse_wiggle": "Se ne hai uno collegato, muovilo un po'.",
|
||||
"ui.3d_sketch": "Schizzo 3D",
|
||||
"ui.activate": "Attiva",
|
||||
"ui.active_component": "Componente attivo",
|
||||
@@ -1282,6 +1429,7 @@
|
||||
"ui.ai_http_busy": "l'endpoint è occupato o ha superato il limite; riprova più tardi",
|
||||
"ui.ai_http_key": "l'endpoint ha rifiutato la chiave: controllala in Preferenze > IA",
|
||||
"ui.ai_http_not_found": "endpoint o modello inesistente: controlla l'URL di base e il nome del modello",
|
||||
"ui.ai_http_too_large": "la conversazione è troppo grande per questo endpoint; inizia una nuova chat",
|
||||
"ui.ai_key": "Chiave API",
|
||||
"ui.ai_key_hint": "facoltativa; conservata nel portachiavi di sistema",
|
||||
"ui.ai_key_needs_https": "La chiave viene inviata solo tramite HTTPS o a questo computer.",
|
||||
@@ -1309,7 +1457,6 @@
|
||||
"ui.ai_permissions": "Autorizzazioni",
|
||||
"ui.ai_preset": "Preimpostazione",
|
||||
"ui.ai_remove": "Rimuovi",
|
||||
"ui.ai_request_limit": "arrestato dopo 24 richieste per un messaggio; invia un altro messaggio per proseguire",
|
||||
"ui.ai_reset": "Reimposta",
|
||||
"ui.ai_settings": "Impostazioni IA…",
|
||||
"ui.ai_show_features": "Mostra funzioni IA",
|
||||
@@ -1354,6 +1501,7 @@
|
||||
"ui.blocked": "Bloccato",
|
||||
"ui.bodies": "Corpi",
|
||||
"ui.bodies_all_if_none": "Corpi (tutti se nessuno)",
|
||||
"ui.bottom": "Inferiore",
|
||||
"ui.box": "Parallelepipedo",
|
||||
"ui.break": "Spezza",
|
||||
"ui.browser_bodies_one": "{count} corpo",
|
||||
@@ -1485,6 +1633,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtra comandi o tasti",
|
||||
"ui.findInTimeline": "Trova nella sequenza temporale",
|
||||
"ui.findInWindow": "Trova nella finestra",
|
||||
"ui.fine": "Fine",
|
||||
"ui.finest": "Finissima",
|
||||
"ui.finish_sketch": "Termina schizzo",
|
||||
"ui.fit": "Adatta",
|
||||
"ui.fix_unfix": "Fissa/Libera",
|
||||
@@ -1492,6 +1642,7 @@
|
||||
"ui.flip": "Inverti",
|
||||
"ui.format": "Formato",
|
||||
"ui.frame_rate": "Frequenza dei fotogrammi",
|
||||
"ui.frame_rate_details": "Dettagli della frequenza dei fotogrammi",
|
||||
"ui.front": "Frontale",
|
||||
"ui.fully_constrained": "Completamente vincolato",
|
||||
"ui.function": "Funzione",
|
||||
@@ -1573,6 +1724,7 @@
|
||||
"ui.heading_torus": "TORO",
|
||||
"ui.heading_utilities": "UTILITÀ",
|
||||
"ui.heading_utility": "UTILITÀ",
|
||||
"ui.heading_weldments": "SALDATURE",
|
||||
"ui.help": "Aiuto",
|
||||
"ui.hide": "Nascondi",
|
||||
"ui.hideAxes": "Nascondi assi",
|
||||
@@ -1709,6 +1861,7 @@
|
||||
"ui.project_face_edges_sketch": "Proietta gli spigoli delle facce nei nuovi schizzi",
|
||||
"ui.properties": "Proprietà",
|
||||
"ui.pull_direction": "Direzione di estrazione",
|
||||
"ui.quality": "Qualità",
|
||||
"ui.quantity": "Quantità",
|
||||
"ui.quantity_2": "Quantità 2",
|
||||
"ui.quit": "Esci",
|
||||
@@ -1797,65 +1950,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom fluido con rotellina",
|
||||
"ui.smooth_wheel_zoom_tip": "Disattiva per applicare immediatamente ogni evento della rotellina, senza coda di interpolazione",
|
||||
"ui.snap": "Aggancio",
|
||||
"ui.space_mouse_axes_reassigned": "Gli assi sono riassegnati nelle Preferenze, e questo vale anche dopo questa calibrazione.",
|
||||
"ui.space_mouse_calibrate": "Calibra…",
|
||||
"ui.space_mouse_calibrate_tip": "Muovi la manopola come chiede la finestra, così SolidCraft impara in che direzione e quanto va ogni asse di questo mouse 3D. Viene salvata per questo mouse 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibra mouse 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrato",
|
||||
"ui.space_mouse_calibration": "Calibrazione del mouse 3D",
|
||||
"ui.space_mouse_calibration_done": "Fatto. Salvala per usarla ogni volta che questo mouse 3D è collegato.",
|
||||
"ui.space_mouse_calibration_no_response": "Un asse si è mosso appena, quindi non si può calibrare. Ricomincia e muovi la manopola più a fondo ogni volta.",
|
||||
"ui.space_mouse_dead_zone": "Zona morta",
|
||||
"ui.space_mouse_dead_zone_tip": "Quanto deve muoversi la manopola prima che si muova la vista, come quota della sua corsa completa. Aumentala se la vista si sposta con la manopola a riposo.",
|
||||
"ui.space_mouse_driven_by": "Quale movimento calibrato del dispositivo comanda questo asse. Se ne scegli uno già usato da un altro asse, i due si scambiano.",
|
||||
"ui.space_mouse_driven_by_label": "comandato da",
|
||||
"ui.space_mouse_driver": "Driver del mouse 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatico",
|
||||
"ui.space_mouse_driver_direct": "Diretto (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Lettura diretta del dispositivo; spacenavd non è in esecuzione",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Lettura tramite spacenavd, l'unica possibilità disponibile",
|
||||
"ui.space_mouse_forget": "Dimentica calibrazione",
|
||||
"ui.space_mouse_hint": "Sposta e ruota la vista insieme; gli schizzi usano solo panoramica e zoom.",
|
||||
"ui.space_mouse_motion_rotation_x": "Rotazione X",
|
||||
"ui.space_mouse_motion_rotation_y": "Rotazione Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Rotazione Z",
|
||||
"ui.space_mouse_motion_translation_x": "Traslazione X",
|
||||
"ui.space_mouse_motion_translation_y": "Traslazione Y",
|
||||
"ui.space_mouse_motion_translation_z": "Traslazione Z",
|
||||
"ui.space_mouse_navigate": "Naviga con SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Nessun mouse 3D trovato",
|
||||
"ui.space_mouse_not_calibrated": "Non calibrato",
|
||||
"ui.space_mouse_reading": "Lettura",
|
||||
"ui.space_mouse_reset_axes": "Ripristina assegnazione assi",
|
||||
"ui.space_mouse_step_lift": "Solleva la manopola, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_press": "Premi la manopola verso il basso, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_pull": "Tira la manopola verso di te, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_push": "Spingi la manopola lontano da te, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_rest": "Togli la mano dalla manopola e lasciala ferma.",
|
||||
"ui.space_mouse_step_slide_left": "Fai scorrere la manopola a sinistra, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_slide_right": "Fai scorrere la manopola a destra, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_back": "Inclina la manopola all'indietro (bordo anteriore in alto), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_forward": "Inclina la manopola in avanti (bordo anteriore in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_left": "Inclina la manopola a sinistra (bordo sinistro in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_tilt_right": "Inclina la manopola a destra (bordo destro in basso), tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Ruota la manopola in senso orario, tienila, poi lasciala.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Ruota la manopola in senso antiorario, tienila, poi lasciala.",
|
||||
"ui.space_mouse_view_axis": "Asse della vista",
|
||||
"ui.space_mouse_view_off": "Disattivato",
|
||||
"ui.space_mouse_view_off_tip": "Il profilo del driver del mouse 3D disattiva questo asse, quindi il suo movimento non muove la vista. Se qui scegli il movimento di un altro asse, i due si scambiano e quel movimento smette di muovere la vista.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Orbita sinistra / destra",
|
||||
"ui.space_mouse_view_orbit_up_down": "Orbita su / giù",
|
||||
"ui.space_mouse_view_pan_sideways": "Panoramica sinistra / destra",
|
||||
"ui.space_mouse_view_pan_up_down": "Panoramica su / giù",
|
||||
"ui.space_mouse_view_roll": "Rollio",
|
||||
"ui.space_mouse_view_unmapped": "Asse {n}",
|
||||
"ui.space_mouse_view_zoom": "Zoom",
|
||||
"ui.spacing": "Spaziatura",
|
||||
"ui.spacing_2": "Spaziatura 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dividi corpo",
|
||||
"ui.stand_up_tip": "Dopo l'apertura di un file STEP o IGES con asse Z verticale, proponi un quarto di giro attorno a X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Inizio",
|
||||
"ui.start_over": "Ricomincia",
|
||||
"ui.start_page": "Pagina iniziale",
|
||||
|
||||
+1047
-58
File diff suppressed because it is too large
Load Diff
+557
-57
File diff suppressed because it is too large
Load Diff
+118
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Pilih…",
|
||||
"Circular": "Bulat",
|
||||
"Circumference": "Lilitan",
|
||||
"Clockwise": "Ikut arah jam",
|
||||
"Close draft": "Tutup draf",
|
||||
"Collisions": "Perlanggaran",
|
||||
"Colour": "Warna",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Cari arahan",
|
||||
"Search this library": "Cari dalam koleksi ini",
|
||||
"Section": "Keratan",
|
||||
"Section position": "Kedudukan keratan",
|
||||
"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).": "Pilih Lain: tahan butang kiri pada model tanpa bergerak (atau klik kanan › Pilih Lain…) untuk senarai objek di bawah kursor. Klik tempat sama untuk objek seterusnya. Klik dua kali butang tengah untuk ke Utama (Tetapan › Navigasi).",
|
||||
"Select bodies (a face picks its body)": "Pilih jasad (klik muka untuk pilih jasadnya)",
|
||||
"Select bodies or faces instead": "Pilih jasad atau muka",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Papar Kursor Ejen",
|
||||
"Show in folder": "Papar dalam folder",
|
||||
"Show the pointer an AI agent moves (#35)": "Papar penuding yang digerakkan oleh ejen AI (#35)",
|
||||
"Show tool at cursor": "Tunjukkan alat di sebelah kursor",
|
||||
"Signed axes": "Paksi bertanda",
|
||||
"Size": "Saiz",
|
||||
"Sketch constraints": "Kekangan lakaran",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft ditutup sebelum reka bentuk disimpan. Buka simpanan automatik terakhirnya.",
|
||||
"Specialise": "Khususkan",
|
||||
"Speed": "Kelajuan",
|
||||
"Square": "Segi empat sama",
|
||||
"Stand it up": "Tegakkan",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Tegakkan: suku putaran pada X, satu ciri Alih yang boleh dibatalkan.",
|
||||
"Stand this part up?": "Tegakkan bahagian ini?",
|
||||
"Start angle": "Sudut mula",
|
||||
"Start bulge": "Bonjolan permulaan",
|
||||
"Start continuity": "Keselanjaran permulaan",
|
||||
"Step": "Langkah",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "sudut, ketinggian",
|
||||
"appearance.dielectric_ior": "IOR Dielektrik",
|
||||
"appearance.metallic": "Sifat Logam",
|
||||
"appearance.only_available": "Rupa yang tersedia sahaja",
|
||||
"appearance.pbr_help": "Lihat dalam Render atau PNG produk. Tiada pembiasan atau penembusan cahaya.",
|
||||
"appearance.pbr_opaque": "PBR Legap",
|
||||
"appearance.roughness": "Kekasaran",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Sisip STEP",
|
||||
"cmd.file_save_mesh": "Simpan sebagai Mesh",
|
||||
"cmd.file_slicers": "Penghiris Terpasang",
|
||||
"cmd.frame_member": "Anggota Rangka",
|
||||
"cmd.frame_profile_insert": "Sisipkan Profil",
|
||||
"cmd.frame_profile_list": "Pustaka Profil",
|
||||
"cmd.frame_profile_properties": "Sifat Profil",
|
||||
"cmd.inspect_accessibility": "Analisis Kebolehcapaian",
|
||||
"cmd.inspect_center_of_mass": "Pusat Jisim",
|
||||
"cmd.inspect_clearance": "Kelegaan",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Bahan Fizikal Baharu",
|
||||
"cmd.material_delete": "Padam Bahan Fizikal",
|
||||
"cmd.material_edit": "Sunting Bahan Fizikal",
|
||||
"cmd.material_inspect": "Periksa Bahan Fizikal",
|
||||
"cmd.material_list": "Senarai Bahan",
|
||||
"cmd.material_properties_directional": "Modulus Keanjalan Berarah",
|
||||
"cmd.material_properties_validate": "Sahkan Data Bahan Fizikal",
|
||||
@@ -960,6 +971,7 @@
|
||||
"motion.key_here": "Kunci di sini",
|
||||
"motion.key_here_hint": "Tambah kekunci pada langkah ini dengan nilai sendi sekarang",
|
||||
"motion.no_collisions": "Tiada perlanggaran sepanjang kajian",
|
||||
"motion.plot_quantity": "Kuantiti",
|
||||
"motion.step_back": "Langkah ke belakang",
|
||||
"motion.step_forward": "Langkah ke hadapan",
|
||||
"motion.to_end": "Ke penghujung",
|
||||
@@ -1121,7 +1133,106 @@
|
||||
"trace.stopped_by_contact": "Berakhir pada sampel {sample}: {a} akan menembusi {b}",
|
||||
"trace.stopped_by_loop": "Berakhir pada sampel {sample}: gelung melalui {joint} tidak lagi dapat ditutup",
|
||||
"ui.1_degree_of_freedom": "1 darjah kebebasan",
|
||||
"ui.3d_mouse_axes_reassigned": "Paksi ditetapkan semula dalam Keutamaan, dan itu masih terpakai di atas penentukuran ini.",
|
||||
"ui.3d_mouse_button": "Butang {number}",
|
||||
"ui.3d_mouse_button_nothing": "Tiada",
|
||||
"ui.3d_mouse_button_settings": "Tetapan tetikus 3D",
|
||||
"ui.3d_mouse_buttons": "Butang",
|
||||
"ui.3d_mouse_buttons_hint": "Tekan butang pada tetikus 3D anda untuk mencarinya di sini, kemudian pilih fungsinya.",
|
||||
"ui.3d_mouse_buttons_no_device": "Sambungkan tetikus 3D untuk menetapkan fungsi butangnya.",
|
||||
"ui.3d_mouse_buttons_reset": "Pulihkan butang lalai",
|
||||
"ui.3d_mouse_calibrate": "Tentukur…",
|
||||
"ui.3d_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
|
||||
"ui.3d_mouse_calibrate_title": "Tentukur tetikus 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Gerakkan tetikus 3D sekali supaya SolidCraft dapat melihatnya.",
|
||||
"ui.3d_mouse_calibrated": "Telah ditentukur",
|
||||
"ui.3d_mouse_calibration": "Penentukuran tetikus 3D",
|
||||
"ui.3d_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
|
||||
"ui.3d_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
|
||||
"ui.3d_mouse_circles_help": "Bulatan menunjukkan apa yang dihantar peranti selepas penentukuran. Model ujian menunjukkan apa yang dilakukan paparan dengannya, selepas songsangan, kelajuan dan penetapan paksi.",
|
||||
"ui.3d_mouse_dead_zone": "Zon mati",
|
||||
"ui.3d_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
|
||||
"ui.3d_mouse_denied": "SolidCraft nampak tetikus 3D tetapi tidak dibenarkan membacanya. Lihat nota persediaan tetikus 3D untuk peraturan udev.",
|
||||
"ui.3d_mouse_denied_short": "Dijumpai, tetapi tidak boleh dibaca",
|
||||
"ui.3d_mouse_driven_by": "Gerakan peranti yang telah ditentukur yang memacu paksi ini. Memilih gerakan yang sudah digunakan paksi lain akan menukar kedua-duanya.",
|
||||
"ui.3d_mouse_driven_by_label": "dipacu oleh",
|
||||
"ui.3d_mouse_driver": "Pemacu tetikus 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatik",
|
||||
"ui.3d_mouse_driver_direct": "Terus (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Buat masa ini dibaca melalui spacenavd: peranti itu sendiri tidak dapat dibuka.",
|
||||
"ui.3d_mouse_driver_only_direct": "Membaca peranti secara terus; spacenavd tidak berjalan",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Membaca melalui spacenavd, satu-satunya cara yang ada",
|
||||
"ui.3d_mouse_forget": "Lupakan penentukuran",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport kiri",
|
||||
"ui.3d_mouse_ghost_left_window": "Tetingkap kiri",
|
||||
"ui.3d_mouse_ghost_position": "Kedudukan",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport kanan",
|
||||
"ui.3d_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
|
||||
"ui.3d_mouse_kind_cap": "Kembali ke tengah",
|
||||
"ui.3d_mouse_kind_question": "Apakah jenis tetikus 3D ini?",
|
||||
"ui.3d_mouse_kind_trackball": "Bola jejak",
|
||||
"ui.3d_mouse_looks_like_cap": "Bacaan kekal selagi anda menahannya, seperti peranti jenis “Kembali ke tengah”.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Bacaan kembali sebaik sahaja anda berhenti bergerak, seperti bola jejak.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Putaran X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Putaran Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Putaran Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Anjakan X",
|
||||
"ui.3d_mouse_motion_translation_y": "Anjakan Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Anjakan Z",
|
||||
"ui.3d_mouse_navigate": "Navigasi dengan tetikus 3D",
|
||||
"ui.3d_mouse_none": "Tiada tetikus 3D ditemui",
|
||||
"ui.3d_mouse_not_calibrated": "Belum ditentukur",
|
||||
"ui.3d_mouse_offer": "Menentukur tetikus 3D membetulkan paksi yang bertukar atau terbalik dan menetapkan kelajuan serta rasanya.",
|
||||
"ui.3d_mouse_offer_never": "Jangan tunjuk lagi",
|
||||
"ui.3d_mouse_offer_swap": "Tukar ke Hidraw (pemacu terus)",
|
||||
"ui.3d_mouse_reading": "Bacaan",
|
||||
"ui.3d_mouse_reset_axes": "Tetapkan semula penetapan paksi",
|
||||
"ui.3d_mouse_reset_view": "Set semula paparan",
|
||||
"ui.3d_mouse_restore": "Pulihkan lalai tetikus 3D",
|
||||
"ui.3d_mouse_restore_ask": "Tetapkan semula tetapan tetikus 3D kepada lalai? Penentukuran dan pemetaan butang kekal.",
|
||||
"ui.3d_mouse_section": "Tetikus 3D",
|
||||
"ui.3d_mouse_show_in_view": "Tunjukkan tetikus 3D dalam paparan 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Gambar tetikus 3D anda di sudut kiri bawah, yang bergerak apabila anda menggerakkannya.",
|
||||
"ui.3d_mouse_skip": "Peranti saya tidak boleh buat ini",
|
||||
"ui.3d_mouse_skip_tip": "Langkau gerakan ini kedua-dua arah; penentukuran tidak menggunakannya.",
|
||||
"ui.3d_mouse_speed": "Kelajuan tetikus 3D",
|
||||
"ui.3d_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Alihkan tangan anda daripada bola dan biarkan ia diam.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Putar gelang tatal ikut arah jam beberapa klik.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Putar gelang tatal lawan arah jam beberapa klik.",
|
||||
"ui.3d_mouse_step_roll_away": "Gulingkan bola menjauhi anda beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_left": "Gulingkan bola ke kiri beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_right": "Gulingkan bola ke kanan beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_roll_toward": "Gulingkan bola ke arah anda beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Pusing bola ikut arah jam beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Pusing bola lawan arah jam beberapa kali, kemudian biarkan ia berhenti.",
|
||||
"ui.3d_mouse_step_tilt_away": "Condongkan bahagian atas penutup menjauhi anda (tepi terdekat naik), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Condongkan bahagian atas penutup ke arah anda (tepi terdekat turun), tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_touch": "Letakkan tangan anda pada penutup tanpa menolaknya.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.3d_mouse_switch_cap": "Tentukur sebagai kembali ke tengah",
|
||||
"ui.3d_mouse_switch_trackball": "Tentukur sebagai bola jejak",
|
||||
"ui.3d_mouse_view_axis": "Paksi paparan",
|
||||
"ui.3d_mouse_view_off": "Mati",
|
||||
"ui.3d_mouse_view_off_tip": "Profil pemacu tetikus 3D mematikan paksi ini, jadi gerakannya tidak menggerakkan paparan. Memilih gerakan paksi lain di sini menukar kedua-duanya, dan gerakan itu tidak lagi menggerakkan paparan.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbit kiri / kanan",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbit atas / bawah",
|
||||
"ui.3d_mouse_view_pan_sideways": "Pan kiri / kanan",
|
||||
"ui.3d_mouse_view_pan_up_down": "Pan atas / bawah",
|
||||
"ui.3d_mouse_view_roll": "Gelek",
|
||||
"ui.3d_mouse_view_unmapped": "Paksi {n}",
|
||||
"ui.3d_mouse_view_zoom": "Zum",
|
||||
"ui.3d_mouse_waiting": "Menunggu pergerakan",
|
||||
"ui.3d_mouse_wiggle": "Jika anda ada yang disambungkan, gerakkannya sedikit.",
|
||||
"ui.3d_sketch": "Lakaran 3D",
|
||||
"ui.activate": "Aktifkan",
|
||||
"ui.active_component": "Komponen aktif",
|
||||
@@ -1279,6 +1390,7 @@
|
||||
"ui.ai_http_busy": "titik akhir sibuk atau telah melebihi had; cuba lagi kemudian",
|
||||
"ui.ai_http_key": "kunci ditolak oleh titik akhir: semak dalam Tetapan > AI",
|
||||
"ui.ai_http_not_found": "titik akhir atau model tidak ditemui: semak URL asas dan nama model",
|
||||
"ui.ai_http_too_large": "perbualan terlalu besar untuk titik akhir ini; mulakan sembang baharu",
|
||||
"ui.ai_key": "Kunci API",
|
||||
"ui.ai_key_hint": "tidak wajib; disimpan dalam rantai kunci sistem",
|
||||
"ui.ai_key_needs_https": "Kunci hanya dihantar melalui HTTPS, atau ke komputer ini.",
|
||||
@@ -1306,7 +1418,6 @@
|
||||
"ui.ai_permissions": "Kebenaran",
|
||||
"ui.ai_preset": "Pratetap",
|
||||
"ui.ai_remove": "Buang",
|
||||
"ui.ai_request_limit": "berhenti selepas 24 permintaan untuk satu mesej; hantar mesej lain untuk meneruskan",
|
||||
"ui.ai_reset": "Set Semula",
|
||||
"ui.ai_settings": "Tetapan AI…",
|
||||
"ui.ai_show_features": "Papar ciri AI",
|
||||
@@ -1351,6 +1462,7 @@
|
||||
"ui.blocked": "Disekat",
|
||||
"ui.bodies": "Jasad",
|
||||
"ui.bodies_all_if_none": "Jasad (semua jika tiada)",
|
||||
"ui.bottom": "Bawah",
|
||||
"ui.box": "Kotak",
|
||||
"ui.break": "Putuskan",
|
||||
"ui.browser_bodies_one": "{count} jasad",
|
||||
@@ -1482,6 +1594,8 @@
|
||||
"ui.filter_commands_or_keys": "Tapis arahan atau kekunci",
|
||||
"ui.findInTimeline": "Cari dalam Garis Masa",
|
||||
"ui.findInWindow": "Cari dalam Tetingkap",
|
||||
"ui.fine": "Halus",
|
||||
"ui.finest": "Paling halus",
|
||||
"ui.finish_sketch": "Tamatkan Lakaran",
|
||||
"ui.fit": "Muatkan",
|
||||
"ui.fix_unfix": "Tetap/Lepas",
|
||||
@@ -1570,6 +1684,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "UTILITI",
|
||||
"ui.heading_utility": "UTILITI",
|
||||
"ui.heading_weldments": "KIMPALAN",
|
||||
"ui.help": "Bantuan",
|
||||
"ui.hide": "Sembunyi",
|
||||
"ui.hideAxes": "Sembunyi Paksi",
|
||||
@@ -1706,6 +1821,7 @@
|
||||
"ui.project_face_edges_sketch": "Unjur tepi muka ke dalam lakaran baharu",
|
||||
"ui.properties": "Sifat",
|
||||
"ui.pull_direction": "Arah tarikan",
|
||||
"ui.quality": "Kualiti",
|
||||
"ui.quantity": "Kuantiti",
|
||||
"ui.quantity_2": "Kuantiti 2",
|
||||
"ui.quit": "Keluar",
|
||||
@@ -1794,40 +1910,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zum roda lancar",
|
||||
"ui.smooth_wheel_zoom_tip": "Matikan untuk mengezum serta-merta bagi setiap putaran roda tetikus, tanpa gerakan susulan",
|
||||
"ui.snap": "Lekap",
|
||||
"ui.space_mouse_calibrate": "Tentukur…",
|
||||
"ui.space_mouse_calibrate_tip": "Gerakkan penutup seperti yang diminta tetingkap, supaya SolidCraft mempelajari arah dan jarak setiap paksi tetikus 3D ini. Disimpan untuk tetikus 3D ini.",
|
||||
"ui.space_mouse_calibrate_title": "Tentukur tetikus 3D",
|
||||
"ui.space_mouse_calibrated": "Telah ditentukur",
|
||||
"ui.space_mouse_calibration": "Penentukuran tetikus 3D",
|
||||
"ui.space_mouse_calibration_done": "Selesai. Simpan untuk digunakan setiap kali tetikus 3D ini disambungkan.",
|
||||
"ui.space_mouse_calibration_no_response": "Satu paksi hampir tidak bergerak, jadi tidak dapat ditentukur. Mulakan semula dan gerakkan penutup lebih jauh setiap kali.",
|
||||
"ui.space_mouse_dead_zone": "Zon mati",
|
||||
"ui.space_mouse_dead_zone_tip": "Sejauh mana penutup perlu bergerak sebelum pandangan bergerak, sebagai bahagian daripada julat penuhnya. Naikkan jika pandangan hanyut semasa penutup dalam keadaan rehat.",
|
||||
"ui.space_mouse_forget": "Lupakan penentukuran",
|
||||
"ui.space_mouse_hint": "Alih dan putar pandangan serentak; dalam lakaran, hanya sorot dan zum digunakan.",
|
||||
"ui.space_mouse_navigate": "Navigasi dengan SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Tiada tetikus 3D ditemui",
|
||||
"ui.space_mouse_not_calibrated": "Belum ditentukur",
|
||||
"ui.space_mouse_reading": "Bacaan",
|
||||
"ui.space_mouse_step_lift": "Angkat penutup ke atas, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_press": "Tekan penutup ke bawah, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_pull": "Tarik penutup ke arah anda, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_push": "Tolak penutup menjauhi anda, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_rest": "Alihkan tangan anda daripada penutup dan biarkan ia diam.",
|
||||
"ui.space_mouse_step_slide_left": "Luncurkan penutup ke kiri, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_slide_right": "Luncurkan penutup ke kanan, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_back": "Condongkan penutup ke belakang (tepi depan naik), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Condongkan penutup ke hadapan (tepi depan turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_left": "Condongkan penutup ke kiri (tepi kiri turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_tilt_right": "Condongkan penutup ke kanan (tepi kanan turun), tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Pusing penutup ikut arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Pusing penutup lawan arah jam, tahan, kemudian lepaskan.",
|
||||
"ui.spacing": "Selang",
|
||||
"ui.spacing_2": "Selang 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Splin",
|
||||
"ui.split_body": "Belah Jasad",
|
||||
"ui.stand_up_tip": "Selepas buka STEP atau IGES dengan Z ke atas, cadangkan suku putaran pada X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Mula",
|
||||
"ui.start_over": "Mula semula",
|
||||
"ui.start_page": "Halaman mula",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Kiezen…",
|
||||
"Circular": "Cirkelvormig",
|
||||
"Circumference": "Omtrek",
|
||||
"Clockwise": "Met de klok mee",
|
||||
"Close draft": "Concept sluiten",
|
||||
"Collisions": "Botsingen",
|
||||
"Colour": "Kleur",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Opdrachten zoeken",
|
||||
"Search this library": "Deze bibliotheek doorzoeken",
|
||||
"Section": "Doorsnede",
|
||||
"Section position": "Positie van de doorsnede",
|
||||
"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).": "Overige selecteren: houd de linkermuisknop stil op het model (of klik met rechts › Overige selecteren…) om alles onder de cursor weer te geven; klik opnieuw op dezelfde plek om het volgende item daar te selecteren. Dubbelklik met de middelste knop om naar Home te gaan (Voorkeuren › Navigatie).",
|
||||
"Select bodies (a face picks its body)": "Lichamen selecteren (een vlak kiest zijn lichaam)",
|
||||
"Select bodies or faces instead": "In plaats daarvan lichamen of vlakken selecteren",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Agentcursor tonen",
|
||||
"Show in folder": "Tonen in map",
|
||||
"Show the pointer an AI agent moves (#35)": "De aanwijzer tonen die een AI-agent verplaatst (#35)",
|
||||
"Show tool at cursor": "Gereedschap bij de cursor tonen",
|
||||
"Signed axes": "Assen met teken",
|
||||
"Size": "Grootte",
|
||||
"Sketch constraints": "Schetsbeperkingen",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft is gesloten voordat deze ontwerpen zijn opgeslagen. Hun laatste automatische opslag kan worden geopend.",
|
||||
"Specialise": "Specialiseren",
|
||||
"Speed": "Snelheid",
|
||||
"Square": "Vierkant",
|
||||
"Stand it up": "Rechtop zetten",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Rechtop zetten: een kwartslag om X, als één Verplaatsen-kenmerk dat u ongedaan kunt maken.",
|
||||
"Stand this part up?": "Dit onderdeel rechtop zetten?",
|
||||
"Start angle": "Beginhoek",
|
||||
"Start bulge": "Beginbolling",
|
||||
"Start continuity": "Begincontinuïteit",
|
||||
"Step": "Stap",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "hoek, hoogte",
|
||||
"appearance.dielectric_ior": "Diëlektrische IOR",
|
||||
"appearance.metallic": "Metaalachtig",
|
||||
"appearance.only_available": "Alleen beschikbare verschijningen",
|
||||
"appearance.pbr_help": "Bekijk dit oppervlak in Render of een product-PNG. Geen breking of transmissie.",
|
||||
"appearance.pbr_opaque": "Ondoorzichtige PBR",
|
||||
"appearance.roughness": "Ruwheid",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "STEP invoegen",
|
||||
"cmd.file_save_mesh": "Opslaan als mesh",
|
||||
"cmd.file_slicers": "Geïnstalleerde slicers",
|
||||
"cmd.frame_member": "Constructiedeel",
|
||||
"cmd.frame_profile_insert": "Profiel invoegen",
|
||||
"cmd.frame_profile_list": "Profielbibliotheek",
|
||||
"cmd.frame_profile_properties": "Profieleigenschappen",
|
||||
"cmd.inspect_accessibility": "Toegankelijkheidsanalyse",
|
||||
"cmd.inspect_center_of_mass": "Zwaartepunt",
|
||||
"cmd.inspect_clearance": "Speling",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Nieuw fysiek materiaal",
|
||||
"cmd.material_delete": "Fysiek materiaal verwijderen",
|
||||
"cmd.material_edit": "Fysiek materiaal bewerken",
|
||||
"cmd.material_inspect": "Fysiek materiaal inspecteren",
|
||||
"cmd.material_list": "Materialen weergeven",
|
||||
"cmd.material_properties_directional": "Richtingsafhankelijke elasticiteitsmodulus",
|
||||
"cmd.material_properties_validate": "Gegevens fysiek materiaal valideren",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Sleutel hier",
|
||||
"motion.key_here_hint": "Voeg een sleutel toe op deze stap met de huidige waarde van de verbinding",
|
||||
"motion.no_collisions": "Geen botsingen in de studie",
|
||||
"motion.plot_quantity": "Aantal",
|
||||
"motion.step_back": "Stap terug",
|
||||
"motion.step_forward": "Stap verder",
|
||||
"motion.to_end": "Naar het einde",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Eindigt bij monster {sample}: {a} zou door {b} heen gaan",
|
||||
"trace.stopped_by_loop": "Eindigt bij monster {sample}: de lus via {joint} kan niet verder sluiten",
|
||||
"ui.1_degree_of_freedom": "1 vrijheidsgraad",
|
||||
"ui.3d_mouse_calibrate": "Kalibreren…",
|
||||
"ui.3d_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
|
||||
"ui.3d_mouse_calibrate_title": "3D-muis kalibreren",
|
||||
"ui.3d_mouse_calibrate_wait": "Beweeg de 3D-muis één keer, zodat SolidCraft hem ziet.",
|
||||
"ui.3d_mouse_calibrated": "Gekalibreerd",
|
||||
"ui.3d_mouse_calibration": "Kalibratie van 3D-muis",
|
||||
"ui.3d_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
|
||||
"ui.3d_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
|
||||
"ui.3d_mouse_circles_help": "De cirkels tonen wat het apparaat na kalibratie stuurt. Het testmodel toont wat de weergave ermee doet, na omkeren, snelheid en asindeling.",
|
||||
"ui.3d_mouse_dead_zone": "Dode zone",
|
||||
"ui.3d_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ziet een 3D-muis maar mag hem niet lezen. Zie de installatienotities voor de 3D-muis voor de udev-regel.",
|
||||
"ui.3d_mouse_denied_short": "Gevonden, maar niet leesbaar",
|
||||
"ui.3d_mouse_driver_fallback": "Voorlopig via spacenavd gelezen: het apparaat zelf kon niet worden geopend.",
|
||||
"ui.3d_mouse_forget": "Kalibratie vergeten",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport links",
|
||||
"ui.3d_mouse_ghost_left_window": "Venster links",
|
||||
"ui.3d_mouse_ghost_position": "Positie",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport rechts",
|
||||
"ui.3d_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
|
||||
"ui.3d_mouse_navigate": "Navigeren met de 3D-muis",
|
||||
"ui.3d_mouse_none": "Geen 3D-muis gevonden",
|
||||
"ui.3d_mouse_not_calibrated": "Niet gekalibreerd",
|
||||
"ui.3d_mouse_offer": "Je 3D-muis kalibreren verhelpt verwisselde of omgekeerde assen en stelt snelheid en gevoel in.",
|
||||
"ui.3d_mouse_offer_never": "Niet meer tonen",
|
||||
"ui.3d_mouse_offer_swap": "Naar Hidraw wisselen (direct)",
|
||||
"ui.3d_mouse_reading": "Uitlezen",
|
||||
"ui.3d_mouse_reset_view": "Weergave resetten",
|
||||
"ui.3d_mouse_restore": "3D-muis standaardwaarden herstellen",
|
||||
"ui.3d_mouse_restore_ask": "De 3D-muisinstellingen terugzetten naar de standaardwaarden? Je kalibratie en knoppentoewijzing blijven staan.",
|
||||
"ui.3d_mouse_speed": "Snelheid van 3D-muis",
|
||||
"ui.3d_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
|
||||
"ui.3d_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
|
||||
"ui.3d_mouse_waiting": "Wacht op beweging",
|
||||
"ui.3d_mouse_wiggle": "Als je er een hebt aangesloten, beweeg hem even.",
|
||||
"ui.3d_sketch": "3D-schets",
|
||||
"ui.activate": "Activeren",
|
||||
"ui.active_component": "Actief component",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "het eindpunt is bezet of boven zijn limiet; probeer het later opnieuw",
|
||||
"ui.ai_http_key": "het eindpunt weigerde de sleutel: controleer deze in Voorkeuren > AI",
|
||||
"ui.ai_http_not_found": "geen eindpunt of model gevonden: controleer de basis-URL en de modelnaam",
|
||||
"ui.ai_http_too_large": "het gesprek is te groot voor dit endpoint; begin een nieuwe chat",
|
||||
"ui.ai_key": "API-sleutel",
|
||||
"ui.ai_key_hint": "optioneel; bewaard in de sleutelhanger van het systeem",
|
||||
"ui.ai_key_needs_https": "Een sleutel wordt alleen via HTTPS of naar deze computer verzonden.",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Machtigingen",
|
||||
"ui.ai_preset": "Voorinstelling",
|
||||
"ui.ai_remove": "Verwijderen",
|
||||
"ui.ai_request_limit": "gestopt na 24 verzoeken voor één bericht; stuur nog een bericht om verder te gaan",
|
||||
"ui.ai_reset": "Herstellen",
|
||||
"ui.ai_settings": "AI-instellingen…",
|
||||
"ui.ai_show_features": "AI-functies tonen",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Opdrachten of toetsen filteren",
|
||||
"ui.findInTimeline": "Zoeken in tijdlijn",
|
||||
"ui.findInWindow": "Zoeken in venster",
|
||||
"ui.fine": "Fijn",
|
||||
"ui.finest": "Zeer fijn",
|
||||
"ui.finish_sketch": "Schets voltooien",
|
||||
"ui.fit": "Passend",
|
||||
"ui.fix_unfix": "Vastzetten/losmaken",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "HULPPROGRAMMA'S",
|
||||
"ui.heading_utility": "HULPPROGRAMMA",
|
||||
"ui.heading_weldments": "LASWERK",
|
||||
"ui.help": "Help",
|
||||
"ui.hide": "Verbergen",
|
||||
"ui.hideAxes": "Assen verbergen",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Vlakranden projecteren in nieuwe schetsen",
|
||||
"ui.properties": "Eigenschappen",
|
||||
"ui.pull_direction": "Trekrichting",
|
||||
"ui.quality": "Kwaliteit",
|
||||
"ui.quantity": "Aantal",
|
||||
"ui.quantity_2": "Aantal 2",
|
||||
"ui.quit": "Afsluiten",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Vloeiend zoomen met wiel",
|
||||
"ui.smooth_wheel_zoom_tip": "Uitschakelen om elke wielgebeurtenis direct toe te passen, zonder uitloop",
|
||||
"ui.snap": "Snap",
|
||||
"ui.space_mouse_calibrate": "Kalibreren…",
|
||||
"ui.space_mouse_calibrate_tip": "Beweeg de dop zoals het venster vraagt, zodat SolidCraft leert welke kant elke as van deze 3D-muis op gaat en hoe ver. Opgeslagen voor deze 3D-muis.",
|
||||
"ui.space_mouse_calibrate_title": "3D-muis kalibreren",
|
||||
"ui.space_mouse_calibrated": "Gekalibreerd",
|
||||
"ui.space_mouse_calibration": "Kalibratie van 3D-muis",
|
||||
"ui.space_mouse_calibration_done": "Gereed. Sla op om het te gebruiken wanneer deze 3D-muis is aangesloten.",
|
||||
"ui.space_mouse_calibration_no_response": "Een as bewoog nauwelijks en kan dus niet worden gekalibreerd. Begin opnieuw en beweeg de dop telkens verder.",
|
||||
"ui.space_mouse_dead_zone": "Dode zone",
|
||||
"ui.space_mouse_dead_zone_tip": "Hoe ver de dop moet bewegen voordat de weergave dat doet, als aandeel van de volledige slag. Verhoog dit als de weergave afdrijft terwijl de dop in rust is.",
|
||||
"ui.space_mouse_forget": "Kalibratie vergeten",
|
||||
"ui.space_mouse_hint": "Verplaats en roteer de weergave samen; schetsen gebruiken alleen pannen en zoomen.",
|
||||
"ui.space_mouse_navigate": "Navigeren met SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Geen 3D-muis gevonden",
|
||||
"ui.space_mouse_not_calibrated": "Niet gekalibreerd",
|
||||
"ui.space_mouse_reading": "Uitlezen",
|
||||
"ui.space_mouse_step_lift": "Til de dop omhoog, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_press": "Druk de dop omlaag, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_pull": "Trek de dop naar u toe, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_push": "Duw de dop van u af, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_rest": "Haal uw hand van de dop en laat hem stilstaan.",
|
||||
"ui.space_mouse_step_slide_left": "Schuif de dop naar links, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_slide_right": "Schuif de dop naar rechts, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_back": "Kantel de dop naar achteren (voorrand omhoog), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_forward": "Kantel de dop naar voren (voorrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_left": "Kantel de dop naar links (linkerrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_tilt_right": "Kantel de dop naar rechts (rechterrand omlaag), houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Draai de dop met de klok mee, houd vast en laat dan los.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Draai de dop tegen de klok in, houd vast en laat dan los.",
|
||||
"ui.spacing": "Tussenruimte",
|
||||
"ui.spacing_2": "Tussenruimte 2",
|
||||
"ui.sphere": "Bol",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Lichaam splitsen",
|
||||
"ui.stand_up_tip": "Zet na het openen van een STEP- of IGES-bestand met Z omhoog een kwartslag om X aan",
|
||||
"ui.standard": "Standaard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Opnieuw beginnen",
|
||||
"ui.start_page": "Startpagina",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Pending UI strings
|
||||
|
||||
A pull request that adds UI strings adds **one new file here** instead of editing
|
||||
`assets/locales/<locale>.json`. Two pull requests then never touch the same file, so they never
|
||||
conflict.
|
||||
|
||||
- Name it after the change, lowercase with dashes: `sketch-trim-hints.json`.
|
||||
- Contents: a map from locale code to `{"message id": "text"}`. Give English; add other locales
|
||||
you can translate (a locale you leave out falls back to English, as before):
|
||||
|
||||
```json
|
||||
{
|
||||
"en": { "sketch.trim.hint": "Click a segment to trim it" },
|
||||
"de": { "sketch.trim.hint": "Segment zum Kürzen anklicken" }
|
||||
}
|
||||
```
|
||||
|
||||
- The id must not already be in that locale's catalog, nor in another pending file; the build and
|
||||
`cargo xtask lists` fail otherwise. To change an existing message, edit the catalog itself.
|
||||
- Don't sort or fold anything. The app, `cargo xtask i18n` and the tests read these files as part of
|
||||
the catalogs, so your strings work in your own pull request.
|
||||
- After a merge, the Generated files workflow runs `cargo xtask fold-pending` on `main`: it adds the
|
||||
messages to the catalogs (sorted, one per line) and deletes the files. Never run it in a pull
|
||||
request.
|
||||
+134
-54
@@ -73,6 +73,7 @@
|
||||
"Choose…": "Wybierz…",
|
||||
"Circular": "Kołowy",
|
||||
"Circumference": "Obwód",
|
||||
"Clockwise": "Zgodnie z ruchem wskazówek zegara",
|
||||
"Close draft": "Zamknij szkic zgłoszenia",
|
||||
"Collisions": "Kolizje",
|
||||
"Colour": "Kolor",
|
||||
@@ -123,6 +124,11 @@
|
||||
"Don't ask again": "Nie pytaj ponownie",
|
||||
"Double-click to rename": "Kliknij dwukrotnie, aby zmienić nazwę",
|
||||
"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": "Dwukrotne kliknięcie środkowym przyciskiem w widoku: Widok główny (kadruje to, co widoczne), Dopasuj (kadruje to, co widoczne, zachowując orientację) lub nic",
|
||||
"Drag to move the boss across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć występ po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to move the rest across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć podparcie po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to move the snap fit across the model. It snaps to sketch points on the face.": "Przeciągnij, aby przesunąć zatrzask po modelu. Przyciąga się do punktów szkicu na ścianie.",
|
||||
"Drag to point the catch in another direction.": "Przeciągnij, aby skierować zaczep w inną stronę.",
|
||||
"Drag to turn the rest in 5° steps. Hold Alt or Ctrl to turn it freely.": "Przeciągnij, aby obrócić podparcie co 5°. Przytrzymaj Alt lub Ctrl, aby obracać swobodnie.",
|
||||
"Drive Joint": "Steruj złączem",
|
||||
"Duration": "Czas trwania",
|
||||
"Edges": "Krawędzie",
|
||||
@@ -339,6 +345,7 @@
|
||||
"Search commands": "Szukaj poleceń",
|
||||
"Search this library": "Przeszukaj tę bibliotekę",
|
||||
"Section": "Przekrój",
|
||||
"Section position": "Położenie przekroju",
|
||||
"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).": "Wybierz inny: przytrzymaj nieruchomo lewy przycisk na modelu (lub prawy przycisk › Wybierz inny…), aby wyświetlić wszystko pod kursorem; kliknij ponownie w tym samym miejscu, aby zaznaczyć kolejny obiekt. Dwukrotne kliknięcie środkowym przyciskiem przechodzi do widoku głównego (Preferencje › Nawigacja).",
|
||||
"Select bodies (a face picks its body)": "Zaznacz bryły (ściana wybiera swoją bryłę)",
|
||||
"Select bodies or faces instead": "Zamiast tego zaznacz bryły lub ściany",
|
||||
@@ -350,6 +357,7 @@
|
||||
"Show Agent Cursor": "Pokaż kursor agenta",
|
||||
"Show in folder": "Pokaż w folderze",
|
||||
"Show the pointer an AI agent moves (#35)": "Pokaż wskaźnik poruszany przez agenta AI (#35)",
|
||||
"Show tool at cursor": "Pokaż narzędzie przy kursorze",
|
||||
"Signed axes": "Osie ze znakiem",
|
||||
"Size": "Rozmiar",
|
||||
"Sketch constraints": "Wiązania szkicu",
|
||||
@@ -362,9 +370,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft zamknął się, zanim te projekty zostały zapisane. Można otworzyć ich ostatni autozapis.",
|
||||
"Specialise": "Uszczegółów",
|
||||
"Speed": "Prędkość",
|
||||
"Square": "Kwadrat",
|
||||
"Stand it up": "Postaw",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Postaw: obrót o ćwierć obrotu wokół osi X, jako jedna funkcja Przesuń, którą można cofnąć.",
|
||||
"Stand this part up?": "Postawić tę część?",
|
||||
"Start angle": "Kąt początkowy",
|
||||
"Start bulge": "Wybrzuszenie początku",
|
||||
"Start continuity": "Ciągłość początku",
|
||||
"Step": "Krok",
|
||||
@@ -460,6 +470,7 @@
|
||||
"angle, height": "kąt, wysokość",
|
||||
"appearance.dielectric_ior": "Współczynnik załamania dielektryka (IOR)",
|
||||
"appearance.metallic": "Metaliczność",
|
||||
"appearance.only_available": "Tylko dostępne wyglądy",
|
||||
"appearance.pbr_help": "Zobacz tę powierzchnię w renderowaniu lub w obrazie PNG produktu. Bez załamania i przepuszczania światła.",
|
||||
"appearance.pbr_opaque": "Nieprzezroczysty PBR",
|
||||
"appearance.roughness": "Chropowatość",
|
||||
@@ -562,6 +573,7 @@
|
||||
"cmd.document_metadata": "Właściwości projektu",
|
||||
"cmd.document_metadata_remove": "Usuń właściwość projektu",
|
||||
"cmd.document_metadata_set": "Zmień właściwości projektu",
|
||||
"cmd.document_revision": "Wersja projektu",
|
||||
"cmd.document_units": "Zmień jednostki projektu",
|
||||
"cmd.edit_checkpoint": "Punkt kontrolny",
|
||||
"cmd.edit_restore_checkpoint": "Przywróć punkt kontrolny",
|
||||
@@ -578,6 +590,10 @@
|
||||
"cmd.file_insert_step": "Wstaw STEP",
|
||||
"cmd.file_save_mesh": "Zapisz jako siatkę",
|
||||
"cmd.file_slicers": "Zainstalowane slicery",
|
||||
"cmd.frame_member": "Element ramy",
|
||||
"cmd.frame_profile_insert": "Wstaw profil",
|
||||
"cmd.frame_profile_list": "Biblioteka profili",
|
||||
"cmd.frame_profile_properties": "Właściwości profilu",
|
||||
"cmd.inspect_accessibility": "Analiza dostępności",
|
||||
"cmd.inspect_center_of_mass": "Środek masy",
|
||||
"cmd.inspect_clearance": "Prześwit",
|
||||
@@ -617,6 +633,7 @@
|
||||
"cmd.material_create": "Nowy materiał fizyczny",
|
||||
"cmd.material_delete": "Usuń materiał fizyczny",
|
||||
"cmd.material_edit": "Edytuj materiał fizyczny",
|
||||
"cmd.material_inspect": "Sprawdź materiał fizyczny",
|
||||
"cmd.material_list": "Lista materiałów",
|
||||
"cmd.material_properties_directional": "Kierunkowy moduł sprężystości",
|
||||
"cmd.material_properties_validate": "Sprawdź dane materiału fizycznego",
|
||||
@@ -1015,6 +1032,7 @@
|
||||
"motion.key_here": "Wstaw klatkę kluczową tutaj",
|
||||
"motion.key_here_hint": "Dodaj klatkę kluczową w tym kroku z bieżącą wartością złącza",
|
||||
"motion.no_collisions": "Brak kolizji w trakcie badania",
|
||||
"motion.plot_quantity": "Liczba",
|
||||
"motion.step_back": "Krok wstecz",
|
||||
"motion.step_forward": "Krok naprzód",
|
||||
"motion.to_end": "Na koniec",
|
||||
@@ -1120,6 +1138,7 @@
|
||||
"prefs.orbit_snap": "Przytrzymaj Shift podczas obracania widoku, aby przyciągnąć do osi.",
|
||||
"prefs.orientation": "Orientacja",
|
||||
"prefs.overall_speed": "Szybkość ogólna",
|
||||
"prefs.placement": "Umieszczanie",
|
||||
"prefs.preview_choices": "Podgląd wybranych opcji",
|
||||
"prefs.quality_default": "domyślnie",
|
||||
"prefs.quality_fastest": "najszybciej",
|
||||
@@ -1245,6 +1264,8 @@
|
||||
"settings.msaa_applies_on_restart": "Wygładzanie krawędzi zadziała po ponownym uruchomieniu SolidCraft",
|
||||
"settings.msaa_reset": "{setting}: {requested}× nie jest obsługiwane przez to urządzenie graficzne; ustawiono {reset}× (obsługiwane: {supported}).",
|
||||
"settings.msaa_unsupported": "Nieobsługiwane przez to urządzenie graficzne",
|
||||
"settings.reference_geometry_through_bodies": "Pokaż geometrię odniesienia przez bryły",
|
||||
"settings.reference_geometry_through_bodies_help": "Rysuj kontury nieaktywnych szkiców i płaszczyzny odniesienia na cieniowanych bryłach",
|
||||
"settings.reset_intro": "Te ustawienia nie działają na tym komputerze i uniemożliwiłyby uruchomienie SolidCraft, więc zostały zmienione:",
|
||||
"settings.reset_title": "Zmieniono ustawienia",
|
||||
"settings.supersampling_note": "Rysuje widok w wyższej rozdzielczości i go zmniejsza: gładsze krawędzie, linie i siatka na ekranach poniżej 4K. Kosztuje czas GPU: ok. 2,25× więcej pikseli przy 1,5× i 4× przy 2×.",
|
||||
@@ -1303,7 +1324,106 @@
|
||||
"trace.stopped_by_contact": "Kończy się na próbce {sample}: {a} przeszedłby przez {b}",
|
||||
"trace.stopped_by_loop": "Kończy się na próbce {sample}: pętla przez {joint} nie może się bardziej zamknąć",
|
||||
"ui.1_degree_of_freedom": "1 stopień swobody",
|
||||
"ui.3d_mouse_axes_reassigned": "Osie są przypisane na nowo w Ustawieniach i to nadal obowiązuje razem z tą kalibracją.",
|
||||
"ui.3d_mouse_button": "Przycisk {number}",
|
||||
"ui.3d_mouse_button_nothing": "Nic",
|
||||
"ui.3d_mouse_button_settings": "Ustawienia myszy 3D",
|
||||
"ui.3d_mouse_buttons": "Przyciski",
|
||||
"ui.3d_mouse_buttons_hint": "Naciśnij przycisk na myszy 3D, aby go tu znaleźć, a potem wybierz, co ma robić.",
|
||||
"ui.3d_mouse_buttons_no_device": "Podłącz mysz 3D, aby ustawić, co robią jej przyciski.",
|
||||
"ui.3d_mouse_buttons_reset": "Przywróć domyślne przyciski",
|
||||
"ui.3d_mouse_calibrate": "Kalibruj…",
|
||||
"ui.3d_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibracja myszy 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Rusz raz myszą 3D, aby SolidCraft ją zobaczył.",
|
||||
"ui.3d_mouse_calibrated": "Skalibrowano",
|
||||
"ui.3d_mouse_calibration": "Kalibracja myszy 3D",
|
||||
"ui.3d_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
|
||||
"ui.3d_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
|
||||
"ui.3d_mouse_circles_help": "Koła pokazują, co urządzenie wysyła po kalibracji. Model testowy pokazuje, co widok z tym robi po odwróceniu, szybkości i przypisaniu osi.",
|
||||
"ui.3d_mouse_dead_zone": "Strefa martwa",
|
||||
"ui.3d_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
|
||||
"ui.3d_mouse_denied": "SolidCraft widzi mysz 3D, ale nie ma pozwolenia, by ją odczytać. Regułę udev opisują uwagi o konfiguracji myszy 3D.",
|
||||
"ui.3d_mouse_denied_short": "Znaleziono, ale nie można odczytać",
|
||||
"ui.3d_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
|
||||
"ui.3d_mouse_driven_by_label": "sterowana przez",
|
||||
"ui.3d_mouse_driver": "Sterownik myszy 3D",
|
||||
"ui.3d_mouse_driver_auto": "Automatyczny",
|
||||
"ui.3d_mouse_driver_direct": "Bezpośredni (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Na razie odczyt przez spacenavd: samego urządzenia nie udało się otworzyć.",
|
||||
"ui.3d_mouse_driver_only_direct": "Urządzenie jest czytane bezpośrednio; spacenavd nie działa",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyna dostępna droga",
|
||||
"ui.3d_mouse_forget": "Zapomnij kalibrację",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Lewy widok",
|
||||
"ui.3d_mouse_ghost_left_window": "Lewe okno",
|
||||
"ui.3d_mouse_ghost_position": "Położenie",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Prawy widok",
|
||||
"ui.3d_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
|
||||
"ui.3d_mouse_kind_cap": "Powrót do środka",
|
||||
"ui.3d_mouse_kind_question": "Jaki to rodzaj myszy 3D?",
|
||||
"ui.3d_mouse_kind_trackball": "Trackball",
|
||||
"ui.3d_mouse_looks_like_cap": "Odczyt zostaje, dopóki trzymasz, jak w urządzeniu typu „Powrót do środka”.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Odczyt wraca, gdy tylko przestajesz ruszać, jak w trackballu.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Obrót X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Obrót Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Obrót Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Przesunięcie X",
|
||||
"ui.3d_mouse_motion_translation_y": "Przesunięcie Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Przesunięcie Z",
|
||||
"ui.3d_mouse_navigate": "Nawiguj za pomocą myszy 3D",
|
||||
"ui.3d_mouse_none": "Nie znaleziono myszy 3D",
|
||||
"ui.3d_mouse_not_calibrated": "Nieskalibrowano",
|
||||
"ui.3d_mouse_offer": "Kalibracja myszy 3D naprawia zamienione lub odwrócone osie oraz ustawia jej szybkość i odczucie.",
|
||||
"ui.3d_mouse_offer_never": "Nie pokazuj więcej",
|
||||
"ui.3d_mouse_offer_swap": "Przełącz na Hidraw (sterownik bezpośredni)",
|
||||
"ui.3d_mouse_reading": "Odczyt",
|
||||
"ui.3d_mouse_reset_axes": "Przywróć przypisanie osi",
|
||||
"ui.3d_mouse_reset_view": "Resetuj widok",
|
||||
"ui.3d_mouse_restore": "Przywróć ustawienia myszy 3D",
|
||||
"ui.3d_mouse_restore_ask": "Przywrócić domyślne ustawienia myszy 3D? Kalibracja i przypisanie przycisków zostają.",
|
||||
"ui.3d_mouse_section": "Mysz 3D",
|
||||
"ui.3d_mouse_show_in_view": "Pokazuj mysz 3D w widoku 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Obraz twojej myszy 3D w lewym dolnym rogu, który porusza się razem z nią.",
|
||||
"ui.3d_mouse_skip": "Moje urządzenie tego nie potrafi",
|
||||
"ui.3d_mouse_skip_tip": "Pomija ten ruch w obu kierunkach; kalibracja go nie uwzględnia.",
|
||||
"ui.3d_mouse_speed": "Czułość myszy 3D",
|
||||
"ui.3d_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Zdejmij dłoń z kulki i nie ruszaj jej.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Obróć pierścień przewijania o kilka kliknięć zgodnie z ruchem wskazówek zegara.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Obróć pierścień przewijania o kilka kliknięć przeciwnie do ruchu wskazówek zegara.",
|
||||
"ui.3d_mouse_step_roll_away": "Potocz kulkę kilka razy od siebie, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_left": "Potocz kulkę kilka razy w lewo, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_right": "Potocz kulkę kilka razy w prawo, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_roll_toward": "Potocz kulkę kilka razy do siebie, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Zakręć kulką kilka razy zgodnie z ruchem wskazówek zegara, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Zakręć kulką kilka razy przeciwnie do ruchu wskazówek zegara, a potem pozwól jej się zatrzymać.",
|
||||
"ui.3d_mouse_step_tilt_away": "Przechyl górę nakładki od siebie (krawędź najbliżej ciebie idzie w górę), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Przechyl górę nakładki do siebie (krawędź najbliżej ciebie idzie w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_touch": "Połóż dłoń na nakładce, nie naciskając jej.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.3d_mouse_switch_cap": "Kalibruj jako powrót do środka",
|
||||
"ui.3d_mouse_switch_trackball": "Kalibruj jako trackball",
|
||||
"ui.3d_mouse_view_axis": "Oś widoku",
|
||||
"ui.3d_mouse_view_off": "Wyłączona",
|
||||
"ui.3d_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tu ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Orbita w lewo / prawo",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Orbita w górę / dół",
|
||||
"ui.3d_mouse_view_pan_sideways": "Przesuwanie w lewo / prawo",
|
||||
"ui.3d_mouse_view_pan_up_down": "Przesuwanie w górę / dół",
|
||||
"ui.3d_mouse_view_roll": "Przechył",
|
||||
"ui.3d_mouse_view_unmapped": "Oś {n}",
|
||||
"ui.3d_mouse_view_zoom": "Powiększenie",
|
||||
"ui.3d_mouse_waiting": "Czekam na ruch",
|
||||
"ui.3d_mouse_wiggle": "Jeśli masz podłączoną, poruszaj nią chwilę.",
|
||||
"ui.3d_sketch": "Szkic 3D",
|
||||
"ui.activate": "Aktywuj",
|
||||
"ui.active_component": "Aktywny komponent",
|
||||
@@ -1463,6 +1583,7 @@
|
||||
"ui.ai_http_busy": "punkt końcowy jest zajęty lub przekroczył limit; spróbuj ponownie później",
|
||||
"ui.ai_http_key": "punkt końcowy odrzucił klucz: sprawdź go w Preferencje > AI",
|
||||
"ui.ai_http_not_found": "nie ma takiego punktu końcowego lub modelu: sprawdź bazowy URL i nazwę modelu",
|
||||
"ui.ai_http_too_large": "rozmowa jest za duża dla tego punktu końcowego; rozpocznij nowy czat",
|
||||
"ui.ai_key": "Klucz API",
|
||||
"ui.ai_key_hint": "opcjonalny; przechowywany w systemowym pęku kluczy",
|
||||
"ui.ai_key_needs_https": "Klucz jest wysyłany tylko przez HTTPS albo do tego komputera.",
|
||||
@@ -1490,7 +1611,6 @@
|
||||
"ui.ai_permissions": "Uprawnienia",
|
||||
"ui.ai_preset": "Ustawienie wstępne",
|
||||
"ui.ai_remove": "Usuń",
|
||||
"ui.ai_request_limit": "zatrzymano po 24 żądaniach dla jednej wiadomości; wyślij kolejną wiadomość, aby kontynuować",
|
||||
"ui.ai_reset": "Resetuj",
|
||||
"ui.ai_settings": "Ustawienia AI…",
|
||||
"ui.ai_show_features": "Pokaż funkcje AI",
|
||||
@@ -1535,6 +1655,7 @@
|
||||
"ui.blocked": "Zablokowane",
|
||||
"ui.bodies": "Bryły",
|
||||
"ui.bodies_all_if_none": "Bryły (wszystkie, jeśli brak)",
|
||||
"ui.bottom": "Dół",
|
||||
"ui.box": "Prostopadłościan",
|
||||
"ui.break": "Podziel",
|
||||
"ui.browser_bodies_one": "{count} bryła",
|
||||
@@ -1666,6 +1787,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtruj polecenia lub klawisze",
|
||||
"ui.findInTimeline": "Znajdź na osi czasu",
|
||||
"ui.findInWindow": "Znajdź w oknie",
|
||||
"ui.fine": "Dokładna",
|
||||
"ui.finest": "Najdokładniejsza",
|
||||
"ui.finish_sketch": "Zakończ szkic",
|
||||
"ui.fit": "Dopasuj",
|
||||
"ui.fix_unfix": "Ustal/zwolnij",
|
||||
@@ -1754,6 +1877,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "NARZĘDZIA",
|
||||
"ui.heading_utility": "NARZĘDZIE",
|
||||
"ui.heading_weldments": "SPAWANIE",
|
||||
"ui.help": "Pomoc",
|
||||
"ui.hide": "Ukryj",
|
||||
"ui.hideAxes": "Ukryj osie",
|
||||
@@ -1833,6 +1957,11 @@
|
||||
"ui.move": "Przesuń",
|
||||
"ui.multiple": "Wiele",
|
||||
"ui.named_views": "Widoki nazwane",
|
||||
"ui.nav_bridge": "Nawiguj programem urządzenia na tym komputerze",
|
||||
"ui.nav_bridge_connected": "Połączono",
|
||||
"ui.nav_bridge_port": "Port",
|
||||
"ui.nav_bridge_tip": "Własny program urządzenia wejściowego 3D (np. sterownik domowej myszy 3D) porusza widokiem przez połączenie lokalne, więc kursor myszy zostaje Twój. SolidCraft łączy się z nim na tym porcie, tylko na tym komputerze.",
|
||||
"ui.nav_bridge_waiting": "Czekam na program urządzenia",
|
||||
"ui.navigation": "Nawigacja",
|
||||
"ui.needs_design": "{command}: najpierw otwórz projekt",
|
||||
"ui.newSketchGroup": "Nowa grupa szkiców",
|
||||
@@ -1876,6 +2005,8 @@
|
||||
"ui.pinToToolbar": "Przypnij do paska narzędzi",
|
||||
"ui.pipe": "Rura",
|
||||
"ui.placement": "Umiejscowienie",
|
||||
"ui.placement_handles": "Uchwyty umieszczania",
|
||||
"ui.placement_handles_tip": "Uchwyty na podglądzie podparcia, występu lub zatrzasku: przeciągnij znacznik, aby przesunąć element po modelu, obróć go pierścieniem",
|
||||
"ui.plane": "Płaszczyzna",
|
||||
"ui.plane_at_angle": "Płaszczyzna pod kątem",
|
||||
"ui.plugins": "Wtyczki",
|
||||
@@ -1897,6 +2028,7 @@
|
||||
"ui.project_face_edges_sketch": "Rzutuj krawędzie ściany do nowych szkiców",
|
||||
"ui.properties": "Właściwości",
|
||||
"ui.pull_direction": "Kierunek wyjmowania",
|
||||
"ui.quality": "Jakość",
|
||||
"ui.quantity": "Liczba",
|
||||
"ui.quantity_2": "Liczba 2",
|
||||
"ui.quit": "Zakończ",
|
||||
@@ -1985,65 +2117,13 @@
|
||||
"ui.smooth_wheel_zoom": "Płynne powiększanie kółkiem",
|
||||
"ui.smooth_wheel_zoom_tip": "Wyłącz, aby każdy ruch kółka działał od razu, bez wygładzania",
|
||||
"ui.snap": "Przyciąganie",
|
||||
"ui.space_mouse_axes_reassigned": "Osie są przypisane inaczej w Preferencjach i to przypisanie nadal działa niezależnie od tej kalibracji.",
|
||||
"ui.space_mouse_calibrate": "Kalibruj…",
|
||||
"ui.space_mouse_calibrate_tip": "Przesuwaj nakładkę zgodnie z poleceniami okna, aby SolidCraft nauczył się kierunku i zakresu każdej osi tej myszy 3D. Zapisywane dla tej myszy 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibracja myszy 3D",
|
||||
"ui.space_mouse_calibrated": "Skalibrowano",
|
||||
"ui.space_mouse_calibration": "Kalibracja myszy 3D",
|
||||
"ui.space_mouse_calibration_done": "Gotowe. Zapisz, aby używać kalibracji po każdym podłączeniu tej myszy 3D.",
|
||||
"ui.space_mouse_calibration_no_response": "Jedna oś prawie się nie poruszyła, więc nie można jej skalibrować. Zacznij od nowa i za każdym razem wychylaj nakładkę mocniej.",
|
||||
"ui.space_mouse_dead_zone": "Strefa martwa",
|
||||
"ui.space_mouse_dead_zone_tip": "O ile trzeba wychylić nakładkę, zanim widok się poruszy, jako część pełnego wychylenia. Zwiększ, jeśli widok dryfuje przy nieruszanej nakładce.",
|
||||
"ui.space_mouse_driven_by": "Który skalibrowany ruch urządzenia steruje tą osią. Wybranie ruchu używanego już przez inną oś zamienia je miejscami.",
|
||||
"ui.space_mouse_driven_by_label": "sterowana przez",
|
||||
"ui.space_mouse_driver": "Sterownik myszy 3D",
|
||||
"ui.space_mouse_driver_auto": "Automatycznie",
|
||||
"ui.space_mouse_driver_direct": "Bezpośrednio (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Bezpośredni odczyt urządzenia; spacenavd nie działa",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Odczyt przez spacenavd, jedyny dostępny sposób",
|
||||
"ui.space_mouse_forget": "Zapomnij kalibrację",
|
||||
"ui.space_mouse_hint": "Przesuwaj i obracaj widok jednocześnie; w szkicach działa tylko przesuwanie i powiększanie.",
|
||||
"ui.space_mouse_motion_rotation_x": "Obrót X",
|
||||
"ui.space_mouse_motion_rotation_y": "Obrót Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Obrót Z",
|
||||
"ui.space_mouse_motion_translation_x": "Przesunięcie X",
|
||||
"ui.space_mouse_motion_translation_y": "Przesunięcie Y",
|
||||
"ui.space_mouse_motion_translation_z": "Przesunięcie Z",
|
||||
"ui.space_mouse_navigate": "Nawiguj za pomocą SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Nie znaleziono myszy 3D",
|
||||
"ui.space_mouse_not_calibrated": "Nieskalibrowano",
|
||||
"ui.space_mouse_reading": "Odczyt",
|
||||
"ui.space_mouse_reset_axes": "Resetuj przypisanie osi",
|
||||
"ui.space_mouse_step_lift": "Unieś nakładkę, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_press": "Wciśnij nakładkę w dół, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_pull": "Pociągnij nakładkę do siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_push": "Popchnij nakładkę od siebie, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_rest": "Zdejmij dłoń z nakładki i nie ruszaj jej.",
|
||||
"ui.space_mouse_step_slide_left": "Przesuń nakładkę w lewo, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_slide_right": "Przesuń nakładkę w prawo, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_back": "Przechyl nakładkę do tyłu (przednia krawędź w górę), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_forward": "Przechyl nakładkę do przodu (przednia krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_left": "Przechyl nakładkę w lewo (lewa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_tilt_right": "Przechyl nakładkę w prawo (prawa krawędź w dół), przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Obróć nakładkę zgodnie z ruchem wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Obróć nakładkę przeciwnie do ruchu wskazówek zegara, przytrzymaj, a następnie puść.",
|
||||
"ui.space_mouse_view_axis": "Oś widoku",
|
||||
"ui.space_mouse_view_off": "Wyłączona",
|
||||
"ui.space_mouse_view_off_tip": "Profil sterownika myszy 3D wyłącza tę oś, więc jej ruch nie porusza widokiem. Wybranie tutaj ruchu innej osi zamienia je miejscami, a ten ruch przestaje poruszać widokiem.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Obracanie w lewo / w prawo",
|
||||
"ui.space_mouse_view_orbit_up_down": "Obracanie w górę / w dół",
|
||||
"ui.space_mouse_view_pan_sideways": "Przesuwanie w lewo / w prawo",
|
||||
"ui.space_mouse_view_pan_up_down": "Przesuwanie w górę / w dół",
|
||||
"ui.space_mouse_view_roll": "Przechylenie",
|
||||
"ui.space_mouse_view_unmapped": "Oś {n}",
|
||||
"ui.space_mouse_view_zoom": "Powiększanie",
|
||||
"ui.spacing": "Odstęp",
|
||||
"ui.spacing_2": "Odstęp 2",
|
||||
"ui.sphere": "Sfera",
|
||||
"ui.spline": "Splajn",
|
||||
"ui.split_body": "Podziel bryłę",
|
||||
"ui.stand_up_tip": "Gdy plik STEP lub IGES otworzy się z osią Z w górę, proponuj obrót o ćwierć obrotu wokół osi X",
|
||||
"ui.standard": "Standardowa",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Zacznij od nowa",
|
||||
"ui.start_page": "Strona startowa",
|
||||
|
||||
+452
-54
File diff suppressed because it is too large
Load Diff
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Alegeți…",
|
||||
"Circular": "Circular",
|
||||
"Circumference": "Circumferință",
|
||||
"Clockwise": "În sens orar",
|
||||
"Close draft": "Închide ciorna",
|
||||
"Collisions": "Coliziuni",
|
||||
"Colour": "Culoare",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Caută comenzi",
|
||||
"Search this library": "Caută în această bibliotecă",
|
||||
"Section": "Secțiune",
|
||||
"Section position": "Poziția secțiunii",
|
||||
"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).": "Selectează altul: țineți apăsat butonul stâng nemișcat pe model (sau clic dreapta › Selectează altul…) pentru a lista tot ce se află sub cursor; faceți clic din nou în același loc pentru a selecta următorul element. Dublu clic pe butonul din mijloc pentru a merge Acasă (Preferințe › Navigare).",
|
||||
"Select bodies (a face picks its body)": "Selectați corpuri (o față alege corpul ei)",
|
||||
"Select bodies or faces instead": "Selectați în schimb corpuri sau fețe",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Afișează cursorul agentului",
|
||||
"Show in folder": "Afișează în dosar",
|
||||
"Show the pointer an AI agent moves (#35)": "Afișează indicatorul pe care îl mută un agent AI (#35)",
|
||||
"Show tool at cursor": "Afișează instrumentul lângă cursor",
|
||||
"Signed axes": "Axe cu semn",
|
||||
"Size": "Dimensiune",
|
||||
"Sketch constraints": "Constrângerile schiței",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft s-a închis înainte ca aceste proiecte să fie salvate. Ultima lor salvare automată poate fi deschisă.",
|
||||
"Specialise": "Specializează",
|
||||
"Speed": "Viteză",
|
||||
"Square": "Pătrat",
|
||||
"Stand it up": "Ridică-l în picioare",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Ridică-l în picioare: un sfert de rotație în jurul lui X, ca o singură operație de mutare ce poate fi anulată.",
|
||||
"Stand this part up?": "Ridicați această piesă în picioare?",
|
||||
"Start angle": "Unghi de început",
|
||||
"Start bulge": "Bombare inițială",
|
||||
"Start continuity": "Continuitate inițială",
|
||||
"Step": "Pas",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "unghi, înălțime",
|
||||
"appearance.dielectric_ior": "IOR dielectric",
|
||||
"appearance.metallic": "Metalic",
|
||||
"appearance.only_available": "Doar aspecte disponibile",
|
||||
"appearance.pbr_help": "Vizualizați această suprafață în Randare sau într-un PNG de produs. Fără refracție sau transmisie.",
|
||||
"appearance.pbr_opaque": "PBR opac",
|
||||
"appearance.roughness": "Rugozitate",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Inserează STEP",
|
||||
"cmd.file_save_mesh": "Salvează ca plasă",
|
||||
"cmd.file_slicers": "Slicere instalate",
|
||||
"cmd.frame_member": "Element de cadru",
|
||||
"cmd.frame_profile_insert": "Inserare profil",
|
||||
"cmd.frame_profile_list": "Bibliotecă de profile",
|
||||
"cmd.frame_profile_properties": "Proprietăți profil",
|
||||
"cmd.inspect_accessibility": "Analiza accesibilității",
|
||||
"cmd.inspect_center_of_mass": "Centru de masă",
|
||||
"cmd.inspect_clearance": "Joc",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Material fizic nou",
|
||||
"cmd.material_delete": "Șterge materialul fizic",
|
||||
"cmd.material_edit": "Editează materialul fizic",
|
||||
"cmd.material_inspect": "Inspectează materialul fizic",
|
||||
"cmd.material_list": "Listează materialele",
|
||||
"cmd.material_properties_directional": "Modul de elasticitate direcțional",
|
||||
"cmd.material_properties_validate": "Validează datele materialului fizic",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Cheie aici",
|
||||
"motion.key_here_hint": "Adaugă o cheie la acest pas cu valoarea curentă a îmbinării",
|
||||
"motion.no_collisions": "Nicio coliziune în studiu",
|
||||
"motion.plot_quantity": "Cantitate",
|
||||
"motion.step_back": "Pas înapoi",
|
||||
"motion.step_forward": "Pas înainte",
|
||||
"motion.to_end": "La sfârșit",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Se termină la eșantionul {sample}: {a} ar trece prin {b}",
|
||||
"trace.stopped_by_loop": "Se termină la eșantionul {sample}: bucla prin {joint} nu se mai poate închide",
|
||||
"ui.1_degree_of_freedom": "1 grad de libertate",
|
||||
"ui.3d_mouse_calibrate": "Calibrează…",
|
||||
"ui.3d_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
|
||||
"ui.3d_mouse_calibrate_title": "Calibrează mouse-ul 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Mișcă o dată mouse-ul 3D ca SolidCraft să îl vadă.",
|
||||
"ui.3d_mouse_calibrated": "Calibrat",
|
||||
"ui.3d_mouse_calibration": "Calibrarea mouse-ului 3D",
|
||||
"ui.3d_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
|
||||
"ui.3d_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
|
||||
"ui.3d_mouse_circles_help": "Cercurile arată ce trimite dispozitivul după calibrare. Modelul de test arată ce face vizualizarea cu acestea, după inversare, viteză și atribuirea axelor.",
|
||||
"ui.3d_mouse_dead_zone": "Zonă moartă",
|
||||
"ui.3d_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
|
||||
"ui.3d_mouse_denied": "SolidCraft vede un mouse 3D, dar nu are voie să îl citească. Regula udev este în notele de configurare a mouse-ului 3D.",
|
||||
"ui.3d_mouse_denied_short": "Găsit, dar nu poate fi citit",
|
||||
"ui.3d_mouse_driver_fallback": "Deocamdată se citește prin spacenavd: dispozitivul în sine nu a putut fi deschis.",
|
||||
"ui.3d_mouse_forget": "Uită calibrarea",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Viewport stânga",
|
||||
"ui.3d_mouse_ghost_left_window": "Fereastră stânga",
|
||||
"ui.3d_mouse_ghost_position": "Poziție",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Viewport dreapta",
|
||||
"ui.3d_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
|
||||
"ui.3d_mouse_navigate": "Navighează cu mouse-ul 3D",
|
||||
"ui.3d_mouse_none": "Niciun mouse 3D găsit",
|
||||
"ui.3d_mouse_not_calibrated": "Necalibrat",
|
||||
"ui.3d_mouse_offer": "Calibrarea mouse-ului 3D corectează axele inversate sau schimbate între ele și îi reglează viteza și senzația.",
|
||||
"ui.3d_mouse_offer_never": "Nu mai afișa",
|
||||
"ui.3d_mouse_offer_swap": "Treci la Hidraw (driver direct)",
|
||||
"ui.3d_mouse_reading": "Valoare citită",
|
||||
"ui.3d_mouse_reset_view": "Resetează vizualizarea",
|
||||
"ui.3d_mouse_restore": "Restaurează implicitele mouse-ului 3D",
|
||||
"ui.3d_mouse_restore_ask": "Revii la valorile implicite ale setărilor mouse-ului 3D? Calibrarea și atribuirea butoanelor rămân.",
|
||||
"ui.3d_mouse_speed": "Viteza mouse-ului 3D",
|
||||
"ui.3d_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
|
||||
"ui.3d_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.3d_mouse_waiting": "Se așteaptă mișcare",
|
||||
"ui.3d_mouse_wiggle": "Dacă ai unul conectat, mișcă-l puțin.",
|
||||
"ui.3d_sketch": "Schiță 3D",
|
||||
"ui.activate": "Activează",
|
||||
"ui.active_component": "Componentă activă",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "endpointul este ocupat sau și-a depășit limita; încercați mai târziu",
|
||||
"ui.ai_http_key": "endpointul a refuzat cheia: verificați-o în Preferințe > AI",
|
||||
"ui.ai_http_not_found": "nu există un astfel de endpoint sau model: verificați URL-ul de bază și numele modelului",
|
||||
"ui.ai_http_too_large": "conversația este prea mare pentru acest endpoint; începeți un chat nou",
|
||||
"ui.ai_key": "Cheie API",
|
||||
"ui.ai_key_hint": "opțional; păstrată în depozitul de chei al sistemului",
|
||||
"ui.ai_key_needs_https": "O cheie se trimite doar prin HTTPS sau către acest calculator.",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Permisiuni",
|
||||
"ui.ai_preset": "Presetare",
|
||||
"ui.ai_remove": "Elimină",
|
||||
"ui.ai_request_limit": "oprit după 24 de cereri pentru un mesaj; trimiteți alt mesaj pentru a continua",
|
||||
"ui.ai_reset": "Resetează",
|
||||
"ui.ai_settings": "Setări AI…",
|
||||
"ui.ai_show_features": "Afișează funcțiile AI",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrează comenzi sau taste",
|
||||
"ui.findInTimeline": "Găsește în cronologie",
|
||||
"ui.findInWindow": "Găsește în fereastră",
|
||||
"ui.fine": "Fină",
|
||||
"ui.finest": "Foarte fină",
|
||||
"ui.finish_sketch": "Finalizează schița",
|
||||
"ui.fit": "Potrivire",
|
||||
"ui.fix_unfix": "Fixează/Defixează",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "TOR",
|
||||
"ui.heading_utilities": "UTILITARE",
|
||||
"ui.heading_utility": "UTILITAR",
|
||||
"ui.heading_weldments": "SUDURĂ",
|
||||
"ui.help": "Ajutor",
|
||||
"ui.hide": "Ascunde",
|
||||
"ui.hideAxes": "Ascunde axele",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Proiectează muchiile feței în schițe noi",
|
||||
"ui.properties": "Proprietăți",
|
||||
"ui.pull_direction": "Direcția de extragere",
|
||||
"ui.quality": "Calitate",
|
||||
"ui.quantity": "Cantitate",
|
||||
"ui.quantity_2": "Cantitatea 2",
|
||||
"ui.quit": "Ieșire",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Zoom lin cu rotița",
|
||||
"ui.smooth_wheel_zoom_tip": "Dezactivați pentru a aplica imediat fiecare eveniment al rotiței, fără coadă de netezire",
|
||||
"ui.snap": "Agățare",
|
||||
"ui.space_mouse_calibrate": "Calibrează…",
|
||||
"ui.space_mouse_calibrate_tip": "Mișcați capacul așa cum cere fereastra, ca SolidCraft să învețe în ce direcție merge fiecare axă a acestui mouse 3D și cât de departe. Salvat pentru acest mouse 3D.",
|
||||
"ui.space_mouse_calibrate_title": "Calibrează mouse-ul 3D",
|
||||
"ui.space_mouse_calibrated": "Calibrat",
|
||||
"ui.space_mouse_calibration": "Calibrarea mouse-ului 3D",
|
||||
"ui.space_mouse_calibration_done": "Gata. Salvați-o pentru a o folosi ori de câte ori acest mouse 3D este conectat.",
|
||||
"ui.space_mouse_calibration_no_response": "O axă abia s-a mișcat, deci nu poate fi calibrată. Reîncepeți și mișcați capacul mai mult de fiecare dată.",
|
||||
"ui.space_mouse_dead_zone": "Zonă moartă",
|
||||
"ui.space_mouse_dead_zone_tip": "Cât trebuie să se miște capacul înainte ca vizualizarea să se miște, ca fracțiune din cursa completă. Măriți-o dacă vizualizarea derivă cât capacul este în repaus.",
|
||||
"ui.space_mouse_forget": "Uită calibrarea",
|
||||
"ui.space_mouse_hint": "Mută și rotește vizualizarea împreună; schițele folosesc doar panoramare și zoom.",
|
||||
"ui.space_mouse_navigate": "Navighează cu SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Niciun mouse 3D găsit",
|
||||
"ui.space_mouse_not_calibrated": "Necalibrat",
|
||||
"ui.space_mouse_reading": "Valoare citită",
|
||||
"ui.space_mouse_step_lift": "Ridicați capacul în sus, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_press": "Apăsați capacul în jos, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_pull": "Trageți capacul spre dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_push": "Împingeți capacul de la dumneavoastră, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_rest": "Luați mâna de pe capac și lăsați-l nemișcat.",
|
||||
"ui.space_mouse_step_slide_left": "Glisați capacul spre stânga, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_slide_right": "Glisați capacul spre dreapta, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_back": "Înclinați capacul spre spate (marginea din față în sus), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_forward": "Înclinați capacul spre față (marginea din față în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_left": "Înclinați capacul spre stânga (marginea stângă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_tilt_right": "Înclinați capacul spre dreapta (marginea dreaptă în jos), țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Răsuciți capacul în sensul acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Răsuciți capacul în sens invers acelor de ceasornic, țineți-l, apoi eliberați-l.",
|
||||
"ui.spacing": "Spațiere",
|
||||
"ui.spacing_2": "Spațierea 2",
|
||||
"ui.sphere": "Sferă",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Împarte corpul",
|
||||
"ui.stand_up_tip": "După ce un fișier STEP sau IGES se deschide cu Z în sus, oferă un sfert de rotație în jurul lui X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Început",
|
||||
"ui.start_over": "Reîncepe",
|
||||
"ui.start_page": "Pagina de start",
|
||||
|
||||
+119
-55
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Выбрать…",
|
||||
"Circular": "Круговой",
|
||||
"Circumference": "Окружность",
|
||||
"Clockwise": "По часовой стрелке",
|
||||
"Close draft": "Закрыть анализ уклонов",
|
||||
"Collisions": "Столкновения",
|
||||
"Colour": "Цвет",
|
||||
@@ -268,7 +269,7 @@
|
||||
"Preview Row": "Строка предпросмотра",
|
||||
"Preview leaves the current design unchanged.": "Предпросмотр не меняет текущий дизайн.",
|
||||
"Profiles/Curves": "Профили/кривые",
|
||||
"Quantity": "Величина",
|
||||
"Quantity": "Количество",
|
||||
"RECENT": "НЕДАВНИЕ",
|
||||
"REFERENCE": "ССЫЛОЧНАЯ ГЕОМЕТРИЯ",
|
||||
"RULED SURFACE": "ЛИНЕЙЧАТАЯ ПОВЕРХНОСТЬ",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Поиск команд",
|
||||
"Search this library": "Поиск в этой библиотеке",
|
||||
"Section": "Сечение",
|
||||
"Section position": "Положение сечения",
|
||||
"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 bodies (a face picks its body)": "Выберите тела (выбор грани выбирает её тело)",
|
||||
"Select bodies or faces instead": "Вместо этого выберите тела или грани",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Показывать курсор агента",
|
||||
"Show in folder": "Показать в папке",
|
||||
"Show the pointer an AI agent moves (#35)": "Показывать указатель, которым двигается ИИ-агент (#35)",
|
||||
"Show tool at cursor": "Показывать инструмент у курсора",
|
||||
"Signed axes": "Знаковые оси",
|
||||
"Size": "Размер",
|
||||
"Sketch constraints": "Ограничения эскиза",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрылся до сохранения этих дизайнов. Можно открыть последнюю автосохранённую версию.",
|
||||
"Specialise": "Специализировать",
|
||||
"Speed": "Скорость",
|
||||
"Square": "Квадрат",
|
||||
"Stand it up": "Поставить вертикально",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Поставить вертикально: четверть оборота вокруг X одной операцией «Переместить», которую можно отменить.",
|
||||
"Stand this part up?": "Поставить эту деталь вертикально?",
|
||||
"Start angle": "Начальный угол",
|
||||
"Start bulge": "Выпуклость в начале",
|
||||
"Start continuity": "Непрерывность в начале",
|
||||
"Step": "Шаг",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "угол, высота",
|
||||
"appearance.dielectric_ior": "Показатель преломления диэлектрика",
|
||||
"appearance.metallic": "Металличность",
|
||||
"appearance.only_available": "Только доступные внешние виды",
|
||||
"appearance.pbr_help": "Просмотр поверхности во вкладке Рендер или на PNG изделия. Без преломления и пропускания света.",
|
||||
"appearance.pbr_opaque": "Непрозрачный PBR",
|
||||
"appearance.roughness": "Шероховатость",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Вставить STEP",
|
||||
"cmd.file_save_mesh": "Сохранить как меш",
|
||||
"cmd.file_slicers": "Установленные слайсеры",
|
||||
"cmd.frame_member": "Элемент рамы",
|
||||
"cmd.frame_profile_insert": "Вставить профиль",
|
||||
"cmd.frame_profile_list": "Библиотека профилей",
|
||||
"cmd.frame_profile_properties": "Свойства профиля",
|
||||
"cmd.inspect_accessibility": "Анализ доступности",
|
||||
"cmd.inspect_center_of_mass": "Центр масс",
|
||||
"cmd.inspect_clearance": "Зазор",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Новый физический материал",
|
||||
"cmd.material_delete": "Удалить физический материал",
|
||||
"cmd.material_edit": "Изменить физический материал",
|
||||
"cmd.material_inspect": "Проверить физический материал",
|
||||
"cmd.material_list": "Список материалов",
|
||||
"cmd.material_properties_directional": "Направленный модуль упругости",
|
||||
"cmd.material_properties_validate": "Проверить данные физического материала",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Вставить ключевой кадр здесь",
|
||||
"motion.key_here_hint": "Добавить ключевой кадр на этом шаге с текущим значением сопряжения",
|
||||
"motion.no_collisions": "За всё исследование столкновений нет",
|
||||
"motion.plot_quantity": "Величина",
|
||||
"motion.step_back": "Шаг назад",
|
||||
"motion.step_forward": "Шаг вперёд",
|
||||
"motion.to_end": "В конец",
|
||||
@@ -1120,7 +1132,106 @@
|
||||
"trace.stopped_by_contact": "Заканчивается на отсчёте {sample}: {a} прошёл бы сквозь {b}",
|
||||
"trace.stopped_by_loop": "Заканчивается на отсчёте {sample}: контур через {joint} дальше не замыкается",
|
||||
"ui.1_degree_of_freedom": "1 степень свободы",
|
||||
"ui.3d_mouse_axes_reassigned": "Оси переназначены в настройках, и это действует и поверх этой калибровки.",
|
||||
"ui.3d_mouse_button": "Кнопка {number}",
|
||||
"ui.3d_mouse_button_nothing": "Ничего",
|
||||
"ui.3d_mouse_button_settings": "Настройки 3D-мыши",
|
||||
"ui.3d_mouse_buttons": "Кнопки",
|
||||
"ui.3d_mouse_buttons_hint": "Нажмите кнопку на 3D-мыши, чтобы найти её здесь, и выберите, что она делает.",
|
||||
"ui.3d_mouse_buttons_no_device": "Подключите 3D-мышь, чтобы назначить её кнопки.",
|
||||
"ui.3d_mouse_buttons_reset": "Вернуть кнопки по умолчанию",
|
||||
"ui.3d_mouse_calibrate": "Калибровать…",
|
||||
"ui.3d_mouse_calibrate_tip": "Двигайте колпачок так, как просит окно, чтобы SolidCraft узнал, куда и насколько далеко движется каждая ось этой 3D-мыши. Сохраняется для этой 3D-мыши.",
|
||||
"ui.3d_mouse_calibrate_title": "Калибровка 3D-мыши",
|
||||
"ui.3d_mouse_calibrate_wait": "Один раз подвигайте 3D-мышь, чтобы SolidCraft её увидел.",
|
||||
"ui.3d_mouse_calibrated": "Откалибрована",
|
||||
"ui.3d_mouse_calibration": "Калибровка 3D-мыши",
|
||||
"ui.3d_mouse_calibration_done": "Готово. Сохраните её, чтобы она применялась всякий раз, когда подключена эта 3D-мышь.",
|
||||
"ui.3d_mouse_calibration_no_response": "Одна ось почти не сдвинулась, поэтому её нельзя откалибровать. Начните заново и каждый раз двигайте колпачок дальше.",
|
||||
"ui.3d_mouse_circles_help": "Круги показывают, что устройство отправляет после калибровки. Тестовая модель показывает, что вид делает с этим после инверсии, скорости и назначения осей.",
|
||||
"ui.3d_mouse_dead_zone": "Мёртвая зона",
|
||||
"ui.3d_mouse_dead_zone_tip": "Насколько нужно сдвинуть колпачок, прежде чем сдвинется вид, в доле от полного хода. Увеличьте, если вид смещается, когда колпачок в покое.",
|
||||
"ui.3d_mouse_denied": "SolidCraft видит 3D-мышь, но не имеет права её читать. Правило udev описано в заметках по настройке 3D-мыши.",
|
||||
"ui.3d_mouse_denied_short": "Найдена, но недоступна для чтения",
|
||||
"ui.3d_mouse_driven_by": "Какое откалиброванное движение устройства управляет этой осью. Если выбрать движение, которое уже использует другая ось, они поменяются местами.",
|
||||
"ui.3d_mouse_driven_by_label": "управляется",
|
||||
"ui.3d_mouse_driver": "Драйвер 3D-мыши",
|
||||
"ui.3d_mouse_driver_auto": "Автоматически",
|
||||
"ui.3d_mouse_driver_direct": "Напрямую (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Пока чтение идёт через spacenavd: само устройство открыть не удалось.",
|
||||
"ui.3d_mouse_driver_only_direct": "Чтение напрямую с устройства; spacenavd не запущен",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Чтение через spacenavd, других вариантов нет",
|
||||
"ui.3d_mouse_forget": "Забыть калибровку",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Область просмотра слева",
|
||||
"ui.3d_mouse_ghost_left_window": "Окно слева",
|
||||
"ui.3d_mouse_ghost_position": "Положение",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Область просмотра справа",
|
||||
"ui.3d_mouse_hint": "Совместное перемещение и вращение вида; в эскизе — только панорама и масштаб.",
|
||||
"ui.3d_mouse_kind_cap": "Возврат в центр",
|
||||
"ui.3d_mouse_kind_question": "Какая у вас 3D-мышь?",
|
||||
"ui.3d_mouse_kind_trackball": "Трекбол",
|
||||
"ui.3d_mouse_looks_like_cap": "Значение держится, пока вы удерживаете, — как у устройства типа «Возврат в центр».",
|
||||
"ui.3d_mouse_looks_like_trackball": "Значение возвращается, как только вы останавливаетесь, — как у трекбола.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Вращение X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Вращение Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Вращение Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Смещение X",
|
||||
"ui.3d_mouse_motion_translation_y": "Смещение Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Смещение Z",
|
||||
"ui.3d_mouse_navigate": "Навигация с помощью 3D-мыши",
|
||||
"ui.3d_mouse_none": "3D-мышь не найдена",
|
||||
"ui.3d_mouse_not_calibrated": "Не откалибрована",
|
||||
"ui.3d_mouse_offer": "Калибровка 3D-мыши исправляет перепутанные или перевёрнутые оси и настраивает скорость и ощущения.",
|
||||
"ui.3d_mouse_offer_never": "Больше не показывать",
|
||||
"ui.3d_mouse_offer_swap": "Переключить на Hidraw (прямой драйвер)",
|
||||
"ui.3d_mouse_reading": "Показания",
|
||||
"ui.3d_mouse_reset_axes": "Сбросить назначение осей",
|
||||
"ui.3d_mouse_reset_view": "Сбросить вид",
|
||||
"ui.3d_mouse_restore": "Восстановить настройки 3D-мыши",
|
||||
"ui.3d_mouse_restore_ask": "Вернуть настройки 3D-мыши к значениям по умолчанию? Калибровка и назначение кнопок останутся.",
|
||||
"ui.3d_mouse_section": "3D-мышь",
|
||||
"ui.3d_mouse_show_in_view": "Показывать 3D-мышь в 3D-виде",
|
||||
"ui.3d_mouse_show_in_view_tip": "Изображение вашей 3D-мыши в левом нижнем углу, которое двигается вместе с ней.",
|
||||
"ui.3d_mouse_skip": "Моё устройство так не умеет",
|
||||
"ui.3d_mouse_skip_tip": "Пропускает это движение в обе стороны; калибровка его не учитывает.",
|
||||
"ui.3d_mouse_speed": "Скорость 3D-мыши",
|
||||
"ui.3d_mouse_step_lift": "Поднимите колпачок вверх, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_press": "Нажмите на колпачок вниз, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_pull": "Потяните колпачок на себя, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_push": "Толкните колпачок от себя, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_rest": "Уберите руку с колпачка и не трогайте его.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Уберите руку с шара и не трогайте его.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Поверните кольцо прокрутки на несколько щелчков по часовой стрелке.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Поверните кольцо прокрутки на несколько щелчков против часовой стрелки.",
|
||||
"ui.3d_mouse_step_roll_away": "Прокатите шар от себя несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_left": "Прокатите шар влево несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_right": "Прокатите шар вправо несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_roll_toward": "Прокатите шар к себе несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_slide_left": "Сдвиньте колпачок влево, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_slide_right": "Сдвиньте колпачок вправо, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Покрутите шар по часовой стрелке несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Покрутите шар против часовой стрелки несколько раз и дайте ему остановиться.",
|
||||
"ui.3d_mouse_step_tilt_away": "Наклоните верх колпачка от себя (ближний край поднимается), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_left": "Наклоните колпачок влево (левый край вниз), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_right": "Наклоните колпачок вправо (правый край вниз), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Наклоните верх колпачка к себе (ближний край опускается), удержите и отпустите.",
|
||||
"ui.3d_mouse_step_touch": "Положите руку на колпачок, не нажимая на него.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Поверните колпачок по часовой стрелке, удержите и отпустите.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Поверните колпачок против часовой стрелки, удержите и отпустите.",
|
||||
"ui.3d_mouse_switch_cap": "Калибровать как возврат в центр",
|
||||
"ui.3d_mouse_switch_trackball": "Калибровать как трекбол",
|
||||
"ui.3d_mouse_view_axis": "Ось вида",
|
||||
"ui.3d_mouse_view_off": "Выкл.",
|
||||
"ui.3d_mouse_view_off_tip": "Профиль драйвера 3D-мыши отключает эту ось, поэтому её движение не двигает вид. Если выбрать здесь движение другой оси, они поменяются местами, и это движение перестанет двигать вид.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Орбита влево / вправо",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Орбита вверх / вниз",
|
||||
"ui.3d_mouse_view_pan_sideways": "Сдвиг влево / вправо",
|
||||
"ui.3d_mouse_view_pan_up_down": "Сдвиг вверх / вниз",
|
||||
"ui.3d_mouse_view_roll": "Крен",
|
||||
"ui.3d_mouse_view_unmapped": "Ось {n}",
|
||||
"ui.3d_mouse_view_zoom": "Масштаб",
|
||||
"ui.3d_mouse_waiting": "Ожидание движения",
|
||||
"ui.3d_mouse_wiggle": "Если 3D-мышь подключена, слегка подвигайте её.",
|
||||
"ui.3d_sketch": "3D-эскиз",
|
||||
"ui.activate": "Активировать",
|
||||
"ui.active_component": "Активный компонент",
|
||||
@@ -1278,6 +1389,7 @@
|
||||
"ui.ai_http_busy": "конечная точка занята или превысила лимит; повторите позже",
|
||||
"ui.ai_http_key": "конечная точка отклонила ключ: проверьте его в Настройки > ИИ",
|
||||
"ui.ai_http_not_found": "нет такой конечной точки или модели: проверьте базовый URL и имя модели",
|
||||
"ui.ai_http_too_large": "разговор слишком велик для этой конечной точки; начните новый чат",
|
||||
"ui.ai_key": "Ключ API",
|
||||
"ui.ai_key_hint": "необязательно; хранится в системной связке ключей",
|
||||
"ui.ai_key_needs_https": "Ключ отправляется только по HTTPS или на этот компьютер.",
|
||||
@@ -1305,7 +1417,6 @@
|
||||
"ui.ai_permissions": "Разрешения",
|
||||
"ui.ai_preset": "Пресет",
|
||||
"ui.ai_remove": "Удалить",
|
||||
"ui.ai_request_limit": "остановлено после 24 запросов на одно сообщение; отправьте ещё одно сообщение, чтобы продолжить",
|
||||
"ui.ai_reset": "Сбросить",
|
||||
"ui.ai_settings": "Настройки ИИ…",
|
||||
"ui.ai_show_features": "Показывать функции ИИ",
|
||||
@@ -1350,6 +1461,7 @@
|
||||
"ui.blocked": "Заблокировано",
|
||||
"ui.bodies": "Тела",
|
||||
"ui.bodies_all_if_none": "Тела (все, если не выбраны)",
|
||||
"ui.bottom": "Снизу",
|
||||
"ui.box": "Бокс",
|
||||
"ui.break": "Разорвать",
|
||||
"ui.browser_bodies_one": "{count} тело",
|
||||
@@ -1481,6 +1593,8 @@
|
||||
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
|
||||
"ui.findInTimeline": "Найти на шкале",
|
||||
"ui.findInWindow": "Найти в окне",
|
||||
"ui.fine": "Высокое",
|
||||
"ui.finest": "Максимальное",
|
||||
"ui.finish_sketch": "Завершить эскиз",
|
||||
"ui.fit": "Вписать",
|
||||
"ui.fix_unfix": "Зафиксировать/Освободить",
|
||||
@@ -1569,6 +1683,7 @@
|
||||
"ui.heading_torus": "ТОР",
|
||||
"ui.heading_utilities": "УТИЛИТЫ",
|
||||
"ui.heading_utility": "УТИЛИТА",
|
||||
"ui.heading_weldments": "СВАРКА",
|
||||
"ui.help": "Справка",
|
||||
"ui.hide": "Скрыть",
|
||||
"ui.hideAxes": "Скрыть оси",
|
||||
@@ -1705,6 +1820,7 @@
|
||||
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
|
||||
"ui.properties": "Свойства",
|
||||
"ui.pull_direction": "Направление вытягивания",
|
||||
"ui.quality": "Качество",
|
||||
"ui.quantity": "Количество",
|
||||
"ui.quantity_2": "Количество 2",
|
||||
"ui.quit": "Выход",
|
||||
@@ -1793,65 +1909,13 @@
|
||||
"ui.smooth_wheel_zoom": "Плавное масштабирование колесом",
|
||||
"ui.smooth_wheel_zoom_tip": "Отключите, чтобы каждое движение колеса применялось сразу, без сглаживания",
|
||||
"ui.snap": "Привязка",
|
||||
"ui.space_mouse_axes_reassigned": "Оси переназначены в настройках, и это действует и поверх этой калибровки.",
|
||||
"ui.space_mouse_calibrate": "Калибровать…",
|
||||
"ui.space_mouse_calibrate_tip": "Двигайте колпачок так, как просит окно, чтобы SolidCraft узнал, куда и насколько далеко движется каждая ось этой 3D-мыши. Сохраняется для этой 3D-мыши.",
|
||||
"ui.space_mouse_calibrate_title": "Калибровка 3D-мыши",
|
||||
"ui.space_mouse_calibrated": "Откалибрована",
|
||||
"ui.space_mouse_calibration": "Калибровка 3D-мыши",
|
||||
"ui.space_mouse_calibration_done": "Готово. Сохраните её, чтобы она применялась всякий раз, когда подключена эта 3D-мышь.",
|
||||
"ui.space_mouse_calibration_no_response": "Одна ось почти не сдвинулась, поэтому её нельзя откалибровать. Начните заново и каждый раз двигайте колпачок дальше.",
|
||||
"ui.space_mouse_dead_zone": "Мёртвая зона",
|
||||
"ui.space_mouse_dead_zone_tip": "Насколько нужно сдвинуть колпачок, прежде чем сдвинется вид, в доле от полного хода. Увеличьте, если вид смещается, когда колпачок в покое.",
|
||||
"ui.space_mouse_driven_by": "Какое откалиброванное движение устройства управляет этой осью. Если выбрать движение, которое уже использует другая ось, они поменяются местами.",
|
||||
"ui.space_mouse_driven_by_label": "управляется",
|
||||
"ui.space_mouse_driver": "Драйвер 3D-мыши",
|
||||
"ui.space_mouse_driver_auto": "Автоматически",
|
||||
"ui.space_mouse_driver_direct": "Напрямую (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Чтение напрямую с устройства; spacenavd не запущен",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Чтение через spacenavd, других вариантов нет",
|
||||
"ui.space_mouse_forget": "Забыть калибровку",
|
||||
"ui.space_mouse_hint": "Совместное перемещение и вращение вида; в эскизе — только панорама и масштаб.",
|
||||
"ui.space_mouse_motion_rotation_x": "Вращение X",
|
||||
"ui.space_mouse_motion_rotation_y": "Вращение Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Вращение Z",
|
||||
"ui.space_mouse_motion_translation_x": "Смещение X",
|
||||
"ui.space_mouse_motion_translation_y": "Смещение Y",
|
||||
"ui.space_mouse_motion_translation_z": "Смещение Z",
|
||||
"ui.space_mouse_navigate": "Навигация через SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "3D-мышь не найдена",
|
||||
"ui.space_mouse_not_calibrated": "Не откалибрована",
|
||||
"ui.space_mouse_reading": "Показания",
|
||||
"ui.space_mouse_reset_axes": "Сбросить назначение осей",
|
||||
"ui.space_mouse_step_lift": "Поднимите колпачок вверх, удержите и отпустите.",
|
||||
"ui.space_mouse_step_press": "Нажмите на колпачок вниз, удержите и отпустите.",
|
||||
"ui.space_mouse_step_pull": "Потяните колпачок на себя, удержите и отпустите.",
|
||||
"ui.space_mouse_step_push": "Толкните колпачок от себя, удержите и отпустите.",
|
||||
"ui.space_mouse_step_rest": "Уберите руку с колпачка и не трогайте его.",
|
||||
"ui.space_mouse_step_slide_left": "Сдвиньте колпачок влево, удержите и отпустите.",
|
||||
"ui.space_mouse_step_slide_right": "Сдвиньте колпачок вправо, удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_back": "Наклоните колпачок назад (передний край вверх), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_forward": "Наклоните колпачок вперёд (передний край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_left": "Наклоните колпачок влево (левый край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_tilt_right": "Наклоните колпачок вправо (правый край вниз), удержите и отпустите.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Поверните колпачок по часовой стрелке, удержите и отпустите.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Поверните колпачок против часовой стрелки, удержите и отпустите.",
|
||||
"ui.space_mouse_view_axis": "Ось вида",
|
||||
"ui.space_mouse_view_off": "Выкл.",
|
||||
"ui.space_mouse_view_off_tip": "Профиль драйвера 3D-мыши отключает эту ось, поэтому её движение не двигает вид. Если выбрать здесь движение другой оси, они поменяются местами, и это движение перестанет двигать вид.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Орбита влево / вправо",
|
||||
"ui.space_mouse_view_orbit_up_down": "Орбита вверх / вниз",
|
||||
"ui.space_mouse_view_pan_sideways": "Сдвиг влево / вправо",
|
||||
"ui.space_mouse_view_pan_up_down": "Сдвиг вверх / вниз",
|
||||
"ui.space_mouse_view_roll": "Крен",
|
||||
"ui.space_mouse_view_unmapped": "Ось {n}",
|
||||
"ui.space_mouse_view_zoom": "Масштаб",
|
||||
"ui.spacing": "Интервал",
|
||||
"ui.spacing_2": "Интервал 2",
|
||||
"ui.sphere": "Сфера",
|
||||
"ui.spline": "Сплайн",
|
||||
"ui.split_body": "Разделить тело",
|
||||
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
|
||||
"ui.standard": "Стандартное",
|
||||
"ui.start": "Старт",
|
||||
"ui.start_over": "Начать заново",
|
||||
"ui.start_page": "Начальная страница",
|
||||
|
||||
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Välj…",
|
||||
"Circular": "Cirkulär",
|
||||
"Circumference": "Omkrets",
|
||||
"Clockwise": "Medurs",
|
||||
"Close draft": "Stäng utkast",
|
||||
"Collisions": "Kollisioner",
|
||||
"Colour": "Färg",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Sök kommandon",
|
||||
"Search this library": "Sök i detta bibliotek",
|
||||
"Section": "Sektion",
|
||||
"Section position": "Profilens läge",
|
||||
"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).": "Markera annan: håll vänsterknappen stilla på modellen (eller högerklicka › Markera annan…) för att lista allt under markören; klicka igen på samma plats för att markera nästa objekt där. Dubbelklicka med mittenknappen för att gå till Hem (Inställningar › Navigering).",
|
||||
"Select bodies (a face picks its body)": "Markera kroppar (en yta väljer sin kropp)",
|
||||
"Select bodies or faces instead": "Markera kroppar eller ytor istället",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Visa agentmarkör",
|
||||
"Show in folder": "Visa i mapp",
|
||||
"Show the pointer an AI agent moves (#35)": "Visa pekaren som en AI-agent flyttar (#35)",
|
||||
"Show tool at cursor": "Visa verktyget vid markören",
|
||||
"Signed axes": "Axlar med tecken",
|
||||
"Size": "Storlek",
|
||||
"Sketch constraints": "Skissvillkor",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft stängdes innan dessa konstruktioner sparades. Deras senaste autospar kan öppnas.",
|
||||
"Specialise": "Specialisera",
|
||||
"Speed": "Hastighet",
|
||||
"Square": "Kvadrat",
|
||||
"Stand it up": "Res upp den",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Res upp den: ett kvartsvarv runt X, som en enda Flytta-funktion som du kan ångra.",
|
||||
"Stand this part up?": "Resa upp denna del?",
|
||||
"Start angle": "Startvinkel",
|
||||
"Start bulge": "Startbukt",
|
||||
"Start continuity": "Startkontinuitet",
|
||||
"Step": "Steg",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "vinkel, höjd",
|
||||
"appearance.dielectric_ior": "Dielektriskt IOR",
|
||||
"appearance.metallic": "Metallisk",
|
||||
"appearance.only_available": "Endast tillgängliga utseenden",
|
||||
"appearance.pbr_help": "Visa den här ytan i Rendering eller en produkt-PNG. Ingen refraktion eller transmission.",
|
||||
"appearance.pbr_opaque": "Ogenomskinlig PBR",
|
||||
"appearance.roughness": "Skrovlighet",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "Infoga STEP",
|
||||
"cmd.file_save_mesh": "Spara som mesh",
|
||||
"cmd.file_slicers": "Installerade slicers",
|
||||
"cmd.frame_member": "Ramdel",
|
||||
"cmd.frame_profile_insert": "Infoga profil",
|
||||
"cmd.frame_profile_list": "Profilbibliotek",
|
||||
"cmd.frame_profile_properties": "Profilegenskaper",
|
||||
"cmd.inspect_accessibility": "Tillgänglighetsanalys",
|
||||
"cmd.inspect_center_of_mass": "Masscentrum",
|
||||
"cmd.inspect_clearance": "Frigång",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Nytt fysiskt material",
|
||||
"cmd.material_delete": "Ta bort fysiskt material",
|
||||
"cmd.material_edit": "Redigera fysiskt material",
|
||||
"cmd.material_inspect": "Granska fysiskt material",
|
||||
"cmd.material_list": "Lista material",
|
||||
"cmd.material_properties_directional": "Riktningsberoende elasticitetsmodul",
|
||||
"cmd.material_properties_validate": "Validera fysiska materialdata",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Nyckel här",
|
||||
"motion.key_here_hint": "Lägg till en nyckel vid detta steg med ledens nuvarande värde",
|
||||
"motion.no_collisions": "Inga kollisioner under studien",
|
||||
"motion.plot_quantity": "Antal",
|
||||
"motion.step_back": "Steg bakåt",
|
||||
"motion.step_forward": "Steg framåt",
|
||||
"motion.to_end": "Till slutet",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "Slutar vid prov {sample}: {a} skulle gå igenom {b}",
|
||||
"trace.stopped_by_loop": "Slutar vid prov {sample}: slingan genom {joint} kan inte stängas längre",
|
||||
"ui.1_degree_of_freedom": "1 frihetsgrad",
|
||||
"ui.3d_mouse_calibrate": "Kalibrera…",
|
||||
"ui.3d_mouse_calibrate_tip": "Flytta knappen som fönstret ber om, så att SolidCraft lär sig åt vilket håll och hur långt varje axel på denna 3D-mus går. Sparas för denna 3D-mus.",
|
||||
"ui.3d_mouse_calibrate_title": "Kalibrera 3D-mus",
|
||||
"ui.3d_mouse_calibrate_wait": "Rör 3D-musen en gång så att SolidCraft ser den.",
|
||||
"ui.3d_mouse_calibrated": "Kalibrerad",
|
||||
"ui.3d_mouse_calibration": "Kalibrering av 3D-mus",
|
||||
"ui.3d_mouse_calibration_done": "Klart. Spara den för att använda den när denna 3D-mus är ansluten.",
|
||||
"ui.3d_mouse_calibration_no_response": "En axel rörde sig knappt, så den kan inte kalibreras. Börja om och flytta knappen längre varje gång.",
|
||||
"ui.3d_mouse_circles_help": "Cirklarna visar vad enheten skickar efter kalibrering. Testmodellen visar vad vyn gör med det, efter inversion, hastighet och axeltilldelning.",
|
||||
"ui.3d_mouse_dead_zone": "Dödzon",
|
||||
"ui.3d_mouse_dead_zone_tip": "Hur långt knappen måste röra sig innan vyn gör det, som andel av dess fulla rörelse. Höj den om vyn driver medan knappen vilar.",
|
||||
"ui.3d_mouse_denied": "SolidCraft ser en 3D-mus men får inte läsa den. Se installationsanteckningarna för 3D-musen om udev-regeln.",
|
||||
"ui.3d_mouse_denied_short": "Hittad, men går inte att läsa",
|
||||
"ui.3d_mouse_driver_fallback": "Läser tills vidare via spacenavd: själva enheten kunde inte öppnas.",
|
||||
"ui.3d_mouse_forget": "Glöm kalibrering",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Vyn vänster",
|
||||
"ui.3d_mouse_ghost_left_window": "Fönster vänster",
|
||||
"ui.3d_mouse_ghost_position": "Placering",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Vyn höger",
|
||||
"ui.3d_mouse_hint": "Flytta och rotera vyn tillsammans; skisser använder endast panorering och zoom.",
|
||||
"ui.3d_mouse_navigate": "Navigera med 3D-musen",
|
||||
"ui.3d_mouse_none": "Ingen 3D-mus hittades",
|
||||
"ui.3d_mouse_not_calibrated": "Inte kalibrerad",
|
||||
"ui.3d_mouse_offer": "Att kalibrera din 3D-mus rättar förväxlade eller omvända axlar och ställer in hastighet och känsla.",
|
||||
"ui.3d_mouse_offer_never": "Visa inte igen",
|
||||
"ui.3d_mouse_offer_swap": "Byt till Hidraw (direkt drivrutin)",
|
||||
"ui.3d_mouse_reading": "Avläsning",
|
||||
"ui.3d_mouse_reset_view": "Återställ vyn",
|
||||
"ui.3d_mouse_restore": "Återställ 3D-musens standardvärden",
|
||||
"ui.3d_mouse_restore_ask": "Vill du återställa 3D-musens inställningar till standard? Din kalibrering och knapptilldelning behålls.",
|
||||
"ui.3d_mouse_speed": "3D-musens hastighet",
|
||||
"ui.3d_mouse_step_lift": "Lyft knappen uppåt, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_press": "Tryck ned knappen, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_pull": "Dra knappen mot dig, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_push": "Skjut knappen bort från dig, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_rest": "Ta bort handen från knappen och låt den vara stilla.",
|
||||
"ui.3d_mouse_step_slide_left": "Skjut knappen åt vänster, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_slide_right": "Skjut knappen åt höger, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_tilt_left": "Luta knappen åt vänster (vänsterkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_tilt_right": "Luta knappen åt höger (högerkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Vrid knappen medurs, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Vrid knappen moturs, håll den och släpp sedan.",
|
||||
"ui.3d_mouse_waiting": "Väntar på rörelse",
|
||||
"ui.3d_mouse_wiggle": "Om du har en ansluten, rör lite på den.",
|
||||
"ui.3d_sketch": "3D-skiss",
|
||||
"ui.activate": "Aktivera",
|
||||
"ui.active_component": "Aktiv komponent",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "slutpunkten är upptagen eller över sin gräns; försök igen senare",
|
||||
"ui.ai_http_key": "slutpunkten nekade nyckeln: kontrollera den under Inställningar > AI",
|
||||
"ui.ai_http_not_found": "ingen sådan slutpunkt eller modell: kontrollera bas-URL:en och modellnamnet",
|
||||
"ui.ai_http_too_large": "konversationen är för stor för den här slutpunkten; starta en ny chatt",
|
||||
"ui.ai_key": "API-nyckel",
|
||||
"ui.ai_key_hint": "valfritt; lagras i systemets nyckelring",
|
||||
"ui.ai_key_needs_https": "En nyckel skickas endast via HTTPS, eller till den här datorn.",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "Behörigheter",
|
||||
"ui.ai_preset": "Förinställning",
|
||||
"ui.ai_remove": "Ta bort",
|
||||
"ui.ai_request_limit": "stoppades efter 24 förfrågningar för ett meddelande; skicka ett nytt meddelande för att fortsätta",
|
||||
"ui.ai_reset": "Återställ",
|
||||
"ui.ai_settings": "AI-inställningar…",
|
||||
"ui.ai_show_features": "Visa AI-funktioner",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "Filtrera kommandon eller tangenter",
|
||||
"ui.findInTimeline": "Sök i tidslinjen",
|
||||
"ui.findInWindow": "Sök i fönstret",
|
||||
"ui.fine": "Fin",
|
||||
"ui.finest": "Finast",
|
||||
"ui.finish_sketch": "Avsluta skiss",
|
||||
"ui.fit": "Anpassa",
|
||||
"ui.fix_unfix": "Fäst/Lös",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "VERKTYG",
|
||||
"ui.heading_utility": "VERKTYG",
|
||||
"ui.heading_weldments": "SVETSNING",
|
||||
"ui.help": "Hjälp",
|
||||
"ui.hide": "Dölj",
|
||||
"ui.hideAxes": "Dölj axlar",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "Projicera ytkanter i nya skisser",
|
||||
"ui.properties": "Egenskaper",
|
||||
"ui.pull_direction": "Dragriktning",
|
||||
"ui.quality": "Kvalitet",
|
||||
"ui.quantity": "Antal",
|
||||
"ui.quantity_2": "Antal 2",
|
||||
"ui.quit": "Avsluta",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "Jämn hjulzoom",
|
||||
"ui.smooth_wheel_zoom_tip": "Stäng av för att tillämpa varje hjulhändelse direkt, utan utjämningsfördröjning",
|
||||
"ui.snap": "Snappa",
|
||||
"ui.space_mouse_calibrate": "Kalibrera…",
|
||||
"ui.space_mouse_calibrate_tip": "Flytta knappen som fönstret ber om, så att SolidCraft lär sig åt vilket håll och hur långt varje axel på denna 3D-mus går. Sparas för denna 3D-mus.",
|
||||
"ui.space_mouse_calibrate_title": "Kalibrera 3D-mus",
|
||||
"ui.space_mouse_calibrated": "Kalibrerad",
|
||||
"ui.space_mouse_calibration": "Kalibrering av 3D-mus",
|
||||
"ui.space_mouse_calibration_done": "Klart. Spara den för att använda den när denna 3D-mus är ansluten.",
|
||||
"ui.space_mouse_calibration_no_response": "En axel rörde sig knappt, så den kan inte kalibreras. Börja om och flytta knappen längre varje gång.",
|
||||
"ui.space_mouse_dead_zone": "Dödzon",
|
||||
"ui.space_mouse_dead_zone_tip": "Hur långt knappen måste röra sig innan vyn gör det, som andel av dess fulla rörelse. Höj den om vyn driver medan knappen vilar.",
|
||||
"ui.space_mouse_forget": "Glöm kalibrering",
|
||||
"ui.space_mouse_hint": "Flytta och rotera vyn tillsammans; skisser använder endast panorering och zoom.",
|
||||
"ui.space_mouse_navigate": "Navigera med SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Ingen 3D-mus hittades",
|
||||
"ui.space_mouse_not_calibrated": "Inte kalibrerad",
|
||||
"ui.space_mouse_reading": "Avläsning",
|
||||
"ui.space_mouse_step_lift": "Lyft knappen uppåt, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_press": "Tryck ned knappen, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_pull": "Dra knappen mot dig, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_push": "Skjut knappen bort från dig, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_rest": "Ta bort handen från knappen och låt den vara stilla.",
|
||||
"ui.space_mouse_step_slide_left": "Skjut knappen åt vänster, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_slide_right": "Skjut knappen åt höger, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_back": "Luta knappen bakåt (framkanten uppåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_forward": "Luta knappen framåt (framkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_left": "Luta knappen åt vänster (vänsterkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_tilt_right": "Luta knappen åt höger (högerkanten nedåt), håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Vrid knappen medurs, håll den och släpp sedan.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Vrid knappen moturs, håll den och släpp sedan.",
|
||||
"ui.spacing": "Avstånd",
|
||||
"ui.spacing_2": "Avstånd 2",
|
||||
"ui.sphere": "Sfär",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Dela kropp",
|
||||
"ui.stand_up_tip": "När en STEP- eller IGES-fil öppnas med Z uppåt, erbjud ett kvartsvarv runt X",
|
||||
"ui.standard": "Standard",
|
||||
"ui.start": "Start",
|
||||
"ui.start_over": "Börja om",
|
||||
"ui.start_page": "Startsida",
|
||||
|
||||
+119
-55
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Seç…",
|
||||
"Circular": "Dairesel",
|
||||
"Circumference": "Çevre",
|
||||
"Clockwise": "Saat yönünde",
|
||||
"Close draft": "Taslağı kapat",
|
||||
"Collisions": "Çarpışmalar",
|
||||
"Colour": "Renk",
|
||||
@@ -268,7 +269,7 @@
|
||||
"Preview Row": "Satırı önizle",
|
||||
"Preview leaves the current design unchanged.": "Önizleme geçerli tasarımı değiştirmez.",
|
||||
"Profiles/Curves": "Profiller/Eğriler",
|
||||
"Quantity": "Büyüklük",
|
||||
"Quantity": "Adet",
|
||||
"RECENT": "SON KULLANILANLAR",
|
||||
"REFERENCE": "REFERANS",
|
||||
"RULED SURFACE": "REGLE YÜZEY",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "Komutlarda ara",
|
||||
"Search this library": "Bu kitaplıkta ara",
|
||||
"Section": "Kesit",
|
||||
"Section position": "Kesit konumu",
|
||||
"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).": "Diğerini seç: İmlecin altındaki her şeyi listelemek için sol tuşu model üzerinde hareket ettirmeden basılı tutun (veya sağ tık › Diğerini seç…); oradaki bir sonraki öğeyi seçmek için aynı noktaya tekrar tıklayın. Başlangıç görünümüne dönmek için orta tuşla çift tıklayın (Ayarlar › Gezinme).",
|
||||
"Select bodies (a face picks its body)": "Gövdeleri seçin (bir yüz kendi gövdesini seçer)",
|
||||
"Select bodies or faces instead": "Bunun yerine gövde veya yüz seçin",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "Ajan imlecini göster",
|
||||
"Show in folder": "Klasörde göster",
|
||||
"Show the pointer an AI agent moves (#35)": "Bir AI ajanının hareket ettirdiği imleci göster (#35)",
|
||||
"Show tool at cursor": "Aracı imlecin yanında göster",
|
||||
"Signed axes": "İşaretli eksenler",
|
||||
"Size": "Boyut",
|
||||
"Sketch constraints": "Eskiz kısıtlamaları",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft bu tasarımlar kaydedilmeden önce kapandı. Son otomatik kayıtları açılabilir.",
|
||||
"Specialise": "Özelleştir",
|
||||
"Speed": "Hız",
|
||||
"Square": "Kare",
|
||||
"Stand it up": "Dik çevir",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Dik çevir: X etrafında çeyrek tur; geri alınabilen tek bir Taşı özelliği olarak eklenir.",
|
||||
"Stand this part up?": "Bu parça dik çevrilsin mi?",
|
||||
"Start angle": "Başlangıç açısı",
|
||||
"Start bulge": "Başlangıç şişkinliği",
|
||||
"Start continuity": "Başlangıç sürekliliği",
|
||||
"Step": "Adım",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "açı, yükseklik",
|
||||
"appearance.dielectric_ior": "Dielektrik kırılma indisi",
|
||||
"appearance.metallic": "Metaliklik",
|
||||
"appearance.only_available": "Yalnızca kullanılabilir görünümler",
|
||||
"appearance.pbr_help": "Bu yüzeyi Render'da veya bir ürün PNG'sinde görüntüleyin. Kırılma veya geçirgenlik yok.",
|
||||
"appearance.pbr_opaque": "Opak PBR",
|
||||
"appearance.roughness": "Pürüzlülük",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "STEP ekle",
|
||||
"cmd.file_save_mesh": "Ağ olarak kaydet",
|
||||
"cmd.file_slicers": "Yüklü dilimleyiciler",
|
||||
"cmd.frame_member": "Çerçeve Elemanı",
|
||||
"cmd.frame_profile_insert": "Profil Ekle",
|
||||
"cmd.frame_profile_list": "Profil Kitaplığı",
|
||||
"cmd.frame_profile_properties": "Profil Özellikleri",
|
||||
"cmd.inspect_accessibility": "Erişilebilirlik analizi",
|
||||
"cmd.inspect_center_of_mass": "Kütle merkezi",
|
||||
"cmd.inspect_clearance": "Açıklık",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "Yeni fiziksel malzeme",
|
||||
"cmd.material_delete": "Fiziksel malzemeyi sil",
|
||||
"cmd.material_edit": "Fiziksel malzemeyi düzenle",
|
||||
"cmd.material_inspect": "Fiziksel malzemeyi incele",
|
||||
"cmd.material_list": "Malzemeleri listele",
|
||||
"cmd.material_properties_directional": "Yöne bağlı elastisite modülü",
|
||||
"cmd.material_properties_validate": "Fiziksel malzeme verilerini kontrol et",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "Buraya anahtar kare ekle",
|
||||
"motion.key_here_hint": "Bu adıma bağlantının mevcut değeriyle bir anahtar kare ekle",
|
||||
"motion.no_collisions": "Etüt boyunca çarpışma yok",
|
||||
"motion.plot_quantity": "Büyüklük",
|
||||
"motion.step_back": "Bir adım geri",
|
||||
"motion.step_forward": "Bir adım ileri",
|
||||
"motion.to_end": "Sona git",
|
||||
@@ -1120,7 +1132,106 @@
|
||||
"trace.stopped_by_contact": "Örnek {sample} noktasında biter: {a}, {b} içinden geçecek",
|
||||
"trace.stopped_by_loop": "Örnek {sample} noktasında biter: {joint} üzerinden geçen döngü daha fazla kapanamaz",
|
||||
"ui.1_degree_of_freedom": "1 serbestlik derecesi",
|
||||
"ui.3d_mouse_axes_reassigned": "Eksenler Tercihler'de yeniden atanmış; bu, bu kalibrasyonun üstüne de uygulanır.",
|
||||
"ui.3d_mouse_button": "Düğme {number}",
|
||||
"ui.3d_mouse_button_nothing": "Hiçbir şey",
|
||||
"ui.3d_mouse_button_settings": "3B fare ayarları",
|
||||
"ui.3d_mouse_buttons": "Düğmeler",
|
||||
"ui.3d_mouse_buttons_hint": "Burada bulmak için 3B farenizdeki bir düğmeye basın, sonra ne yapacağını seçin.",
|
||||
"ui.3d_mouse_buttons_no_device": "Düğmelerinin ne yapacağını seçmek için bir 3B fare bağlayın.",
|
||||
"ui.3d_mouse_buttons_reset": "Varsayılan düğmelere dön",
|
||||
"ui.3d_mouse_calibrate": "Kalibre et…",
|
||||
"ui.3d_mouse_calibrate_tip": "Pencerenin istediği gibi başlığı hareket ettirin; SolidCraft bu 3B farenin her ekseninin hangi yöne ve ne kadar gittiğini öğrensin. Bu 3B fare için kaydedilir.",
|
||||
"ui.3d_mouse_calibrate_title": "3B fareyi kalibre et",
|
||||
"ui.3d_mouse_calibrate_wait": "SolidCraft'ın görebilmesi için 3D fareyi bir kez hareket ettirin.",
|
||||
"ui.3d_mouse_calibrated": "Kalibre edildi",
|
||||
"ui.3d_mouse_calibration": "3B fare kalibrasyonu",
|
||||
"ui.3d_mouse_calibration_done": "Bitti. Bu 3B fare her bağlandığında kullanılması için kaydedin.",
|
||||
"ui.3d_mouse_calibration_no_response": "Bir eksen neredeyse hiç hareket etmedi, bu yüzden kalibre edilemiyor. Baştan başlayın ve başlığı her seferinde daha fazla hareket ettirin.",
|
||||
"ui.3d_mouse_circles_help": "Daireler, aygıtın kalibrasyondan sonra gönderdiğini gösterir. Test modeli, görünümün bunu ters çevirme, hız ve eksen atamasından sonra ne yaptığını gösterir.",
|
||||
"ui.3d_mouse_dead_zone": "Ölü bölge",
|
||||
"ui.3d_mouse_dead_zone_tip": "Görünüm hareket etmeden önce başlığın ne kadar hareket etmesi gerektiği, tam hareket aralığının oranı olarak. Başlık dururken görünüm kayıyorsa artırın.",
|
||||
"ui.3d_mouse_denied": "SolidCraft bir 3D fare görüyor ama onu okumasına izin verilmiyor. udev kuralı için 3D fare kurulum notlarına bakın.",
|
||||
"ui.3d_mouse_denied_short": "Bulundu, ancak okunamıyor",
|
||||
"ui.3d_mouse_driven_by": "Bu ekseni cihazın hangi kalibre edilmiş hareketinin yönettiği. Başka bir eksenin zaten kullandığını seçerseniz ikisi yer değiştirir.",
|
||||
"ui.3d_mouse_driven_by_label": "kumanda eden",
|
||||
"ui.3d_mouse_driver": "3B fare sürücüsü",
|
||||
"ui.3d_mouse_driver_auto": "Otomatik",
|
||||
"ui.3d_mouse_driver_direct": "Doğrudan (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Şimdilik spacenavd üzerinden okunuyor: aygıtın kendisi açılamadı.",
|
||||
"ui.3d_mouse_driver_only_direct": "Aygıt doğrudan okunuyor; spacenavd çalışmıyor",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "spacenavd üzerinden okunuyor, tek seçenek bu",
|
||||
"ui.3d_mouse_forget": "Kalibrasyonu unut",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Sol görünüm",
|
||||
"ui.3d_mouse_ghost_left_window": "Sol pencere",
|
||||
"ui.3d_mouse_ghost_position": "Konum",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Sağ görünüm",
|
||||
"ui.3d_mouse_hint": "Görünümü aynı anda kaydırın ve döndürün; eskizlerde yalnızca kaydırma ve yakınlaştırma kullanılır.",
|
||||
"ui.3d_mouse_kind_cap": "Merkeze dönüş",
|
||||
"ui.3d_mouse_kind_question": "Ne tür bir 3B fare?",
|
||||
"ui.3d_mouse_kind_trackball": "İztopu",
|
||||
"ui.3d_mouse_looks_like_cap": "Tuttuğunuz sürece değer yerinde kalıyor, “Merkeze dönüş” türü bir aygıttaki gibi.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Hareketi bırakır bırakmaz değer geri dönüyor, iztopunda olduğu gibi.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Dönme X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Dönme Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Dönme Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Öteleme X",
|
||||
"ui.3d_mouse_motion_translation_y": "Öteleme Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Öteleme Z",
|
||||
"ui.3d_mouse_navigate": "3D fare ile gezin",
|
||||
"ui.3d_mouse_none": "3B fare bulunamadı",
|
||||
"ui.3d_mouse_not_calibrated": "Kalibre edilmedi",
|
||||
"ui.3d_mouse_offer": "3D farenizi kalibre etmek, yer değiştirmiş veya ters dönmüş eksenleri düzeltir; hızını ve hissini ayarlar.",
|
||||
"ui.3d_mouse_offer_never": "Bir daha gösterme",
|
||||
"ui.3d_mouse_offer_swap": "Hidraw'a geç (doğrudan sürücü)",
|
||||
"ui.3d_mouse_reading": "Okunan değer",
|
||||
"ui.3d_mouse_reset_axes": "Eksen atamasını sıfırla",
|
||||
"ui.3d_mouse_reset_view": "Görünümü sıfırla",
|
||||
"ui.3d_mouse_restore": "3D fare varsayılanlarını geri yükle",
|
||||
"ui.3d_mouse_restore_ask": "3D fare ayarları varsayılanlara döndürülsün mü? Kalibrasyon ve düğme eşlemesi korunur.",
|
||||
"ui.3d_mouse_section": "3D fare",
|
||||
"ui.3d_mouse_show_in_view": "3B fareyi 3B görünümde göster",
|
||||
"ui.3d_mouse_show_in_view_tip": "Sol alt köşede, siz hareket ettirdikçe hareket eden bir 3B fare resmi.",
|
||||
"ui.3d_mouse_skip": "Cihazım bunu yapamıyor",
|
||||
"ui.3d_mouse_skip_tip": "Bu hareketi iki yönde de atlar; kalibrasyon onu dışarıda bırakır.",
|
||||
"ui.3d_mouse_speed": "3D fare hızı",
|
||||
"ui.3d_mouse_step_lift": "Başlığı yukarı kaldırın, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_press": "Başlığı aşağı bastırın, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_pull": "Başlığı kendinize doğru çekin, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_push": "Başlığı sizden uzağa itin, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_rest": "Elinizi başlıktan çekin ve hareketsiz bırakın.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Elinizi toptan çekin ve hareketsiz bırakın.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Kaydırma halkasını saat yönünde birkaç tık çevirin.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Kaydırma halkasını saat yönünün tersine birkaç tık çevirin.",
|
||||
"ui.3d_mouse_step_roll_away": "Topu birkaç kez sizden uzağa yuvarlayın, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_roll_left": "Topu birkaç kez sola yuvarlayın, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_roll_right": "Topu birkaç kez sağa yuvarlayın, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_roll_toward": "Topu birkaç kez kendinize doğru yuvarlayın, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_slide_left": "Başlığı sola kaydırın, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_slide_right": "Başlığı sağa kaydırın, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Topu birkaç kez saat yönünde döndürün, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Topu birkaç kez saat yönünün tersine döndürün, sonra durmasını bekleyin.",
|
||||
"ui.3d_mouse_step_tilt_away": "Başlığın üstünü sizden uzağa eğin (size en yakın kenar yukarı kalkar), tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_tilt_left": "Başlığı sola eğin (sol kenar aşağı), tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_tilt_right": "Başlığı sağa eğin (sağ kenar aşağı), tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Başlığın üstünü kendinize doğru eğin (size en yakın kenar aşağı iner), tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_touch": "Elinizi başlığa itmeden koyun.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Başlığı saat yönünde çevirin, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Başlığı saat yönünün tersine çevirin, tutun, sonra bırakın.",
|
||||
"ui.3d_mouse_switch_cap": "Merkeze dönüş olarak kalibre et",
|
||||
"ui.3d_mouse_switch_trackball": "İztopu olarak kalibre et",
|
||||
"ui.3d_mouse_view_axis": "Görünüm ekseni",
|
||||
"ui.3d_mouse_view_off": "Kapalı",
|
||||
"ui.3d_mouse_view_off_tip": "3D fare sürücüsünün profili bu ekseni kapatıyor, bu yüzden hareketi görünümü oynatmaz. Burada başka bir eksenin hareketini seçerseniz ikisi yer değiştirir ve o hareket görünümü oynatmayı bırakır.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Sola / sağa yörünge",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Yukarı / aşağı yörünge",
|
||||
"ui.3d_mouse_view_pan_sideways": "Sola / sağa kaydır",
|
||||
"ui.3d_mouse_view_pan_up_down": "Yukarı / aşağı kaydır",
|
||||
"ui.3d_mouse_view_roll": "Yuvarlanma",
|
||||
"ui.3d_mouse_view_unmapped": "Eksen {n}",
|
||||
"ui.3d_mouse_view_zoom": "Yakınlaştır",
|
||||
"ui.3d_mouse_waiting": "Hareket bekleniyor",
|
||||
"ui.3d_mouse_wiggle": "Takılı bir tane varsa biraz oynatın.",
|
||||
"ui.3d_sketch": "3D eskiz",
|
||||
"ui.activate": "Etkinleştir",
|
||||
"ui.active_component": "Etkin bileşen",
|
||||
@@ -1278,6 +1389,7 @@
|
||||
"ui.ai_http_busy": "uç nokta meşgul veya sınırda; daha sonra tekrar deneyin",
|
||||
"ui.ai_http_key": "uç nokta anahtarı reddetti: Ayarlar > AI altında kontrol edin",
|
||||
"ui.ai_http_not_found": "Uç nokta veya model bulunamadı: temel URL'yi ve model adını kontrol edin",
|
||||
"ui.ai_http_too_large": "konuşma bu uç nokta için çok büyük; yeni bir sohbet başlatın",
|
||||
"ui.ai_key": "API anahtarı",
|
||||
"ui.ai_key_hint": "isteğe bağlı; sistem anahtar zincirinde saklanır",
|
||||
"ui.ai_key_needs_https": "Anahtar yalnızca HTTPS üzerinden veya bu bilgisayara gönderilir.",
|
||||
@@ -1305,7 +1417,6 @@
|
||||
"ui.ai_permissions": "İzinler",
|
||||
"ui.ai_preset": "Şablon",
|
||||
"ui.ai_remove": "Kaldır",
|
||||
"ui.ai_request_limit": "bir mesaj için 24 istekten sonra durduruldu; devam etmek için başka bir mesaj gönderin",
|
||||
"ui.ai_reset": "Sıfırla",
|
||||
"ui.ai_settings": "AI ayarları…",
|
||||
"ui.ai_show_features": "AI özelliklerini göster",
|
||||
@@ -1350,6 +1461,7 @@
|
||||
"ui.blocked": "Engellenmiş",
|
||||
"ui.bodies": "Gövdeler",
|
||||
"ui.bodies_all_if_none": "Gövdeler (yoksa tümü)",
|
||||
"ui.bottom": "Alt",
|
||||
"ui.box": "Kutu",
|
||||
"ui.break": "Kır",
|
||||
"ui.browser_bodies_one": "{count} gövde",
|
||||
@@ -1481,6 +1593,8 @@
|
||||
"ui.filter_commands_or_keys": "Komutları veya tuşları filtrele",
|
||||
"ui.findInTimeline": "Zaman çizelgesinde bul",
|
||||
"ui.findInWindow": "Pencerede bul",
|
||||
"ui.fine": "İnce",
|
||||
"ui.finest": "En ince",
|
||||
"ui.finish_sketch": "Eskizi bitir",
|
||||
"ui.fit": "Sığdır",
|
||||
"ui.fix_unfix": "Sabitle/Çöz",
|
||||
@@ -1569,6 +1683,7 @@
|
||||
"ui.heading_torus": "TORUS",
|
||||
"ui.heading_utilities": "ARAÇLAR",
|
||||
"ui.heading_utility": "YARDIMCI ARAÇLAR",
|
||||
"ui.heading_weldments": "KAYNAK",
|
||||
"ui.help": "Yardım",
|
||||
"ui.hide": "Gizle",
|
||||
"ui.hideAxes": "Eksenleri gizle",
|
||||
@@ -1705,6 +1820,7 @@
|
||||
"ui.project_face_edges_sketch": "Yüz kenarlarını yeni eskizlere yansıt",
|
||||
"ui.properties": "Özellikler",
|
||||
"ui.pull_direction": "Kalıptan çıkarma yönü",
|
||||
"ui.quality": "Kalite",
|
||||
"ui.quantity": "Adet",
|
||||
"ui.quantity_2": "Adet 2",
|
||||
"ui.quit": "Çıkış",
|
||||
@@ -1793,65 +1909,13 @@
|
||||
"ui.smooth_wheel_zoom": "Yumuşak tekerlek yakınlaştırması",
|
||||
"ui.smooth_wheel_zoom_tip": "Her tekerlek hareketini yumuşatma olmadan hemen uygulamak için kapatın",
|
||||
"ui.snap": "Yakala",
|
||||
"ui.space_mouse_axes_reassigned": "Eksenler Tercihler'de yeniden atanmış; bu, bu kalibrasyonun üstüne de uygulanır.",
|
||||
"ui.space_mouse_calibrate": "Kalibre et…",
|
||||
"ui.space_mouse_calibrate_tip": "Pencerenin istediği gibi başlığı hareket ettirin; SolidCraft bu 3B farenin her ekseninin hangi yöne ve ne kadar gittiğini öğrensin. Bu 3B fare için kaydedilir.",
|
||||
"ui.space_mouse_calibrate_title": "3B fareyi kalibre et",
|
||||
"ui.space_mouse_calibrated": "Kalibre edildi",
|
||||
"ui.space_mouse_calibration": "3B fare kalibrasyonu",
|
||||
"ui.space_mouse_calibration_done": "Bitti. Bu 3B fare her bağlandığında kullanılması için kaydedin.",
|
||||
"ui.space_mouse_calibration_no_response": "Bir eksen neredeyse hiç hareket etmedi, bu yüzden kalibre edilemiyor. Baştan başlayın ve başlığı her seferinde daha fazla hareket ettirin.",
|
||||
"ui.space_mouse_dead_zone": "Ölü bölge",
|
||||
"ui.space_mouse_dead_zone_tip": "Görünüm hareket etmeden önce başlığın ne kadar hareket etmesi gerektiği, tam hareket aralığının oranı olarak. Başlık dururken görünüm kayıyorsa artırın.",
|
||||
"ui.space_mouse_driven_by": "Bu ekseni cihazın hangi kalibre edilmiş hareketinin yönettiği. Başka bir eksenin zaten kullandığını seçerseniz ikisi yer değiştirir.",
|
||||
"ui.space_mouse_driven_by_label": "kumanda eden",
|
||||
"ui.space_mouse_driver": "3B fare sürücüsü",
|
||||
"ui.space_mouse_driver_auto": "Otomatik",
|
||||
"ui.space_mouse_driver_direct": "Doğrudan (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Aygıt doğrudan okunuyor; spacenavd çalışmıyor",
|
||||
"ui.space_mouse_driver_only_spacenavd": "spacenavd üzerinden okunuyor, tek seçenek bu",
|
||||
"ui.space_mouse_forget": "Kalibrasyonu unut",
|
||||
"ui.space_mouse_hint": "Görünümü aynı anda kaydırın ve döndürün; eskizlerde yalnızca kaydırma ve yakınlaştırma kullanılır.",
|
||||
"ui.space_mouse_motion_rotation_x": "Dönme X",
|
||||
"ui.space_mouse_motion_rotation_y": "Dönme Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Dönme Z",
|
||||
"ui.space_mouse_motion_translation_x": "Öteleme X",
|
||||
"ui.space_mouse_motion_translation_y": "Öteleme Y",
|
||||
"ui.space_mouse_motion_translation_z": "Öteleme Z",
|
||||
"ui.space_mouse_navigate": "SpaceNavigator / SpaceMouse ile gezin",
|
||||
"ui.space_mouse_none": "3B fare bulunamadı",
|
||||
"ui.space_mouse_not_calibrated": "Kalibre edilmedi",
|
||||
"ui.space_mouse_reading": "Okunan değer",
|
||||
"ui.space_mouse_reset_axes": "Eksen atamasını sıfırla",
|
||||
"ui.space_mouse_step_lift": "Başlığı yukarı kaldırın, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_press": "Başlığı aşağı bastırın, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_pull": "Başlığı kendinize doğru çekin, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_push": "Başlığı sizden uzağa itin, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_rest": "Elinizi başlıktan çekin ve hareketsiz bırakın.",
|
||||
"ui.space_mouse_step_slide_left": "Başlığı sola kaydırın, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_slide_right": "Başlığı sağa kaydırın, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_tilt_back": "Başlığı geriye eğin (ön kenar yukarı), tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_tilt_forward": "Başlığı öne eğin (ön kenar aşağı), tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_tilt_left": "Başlığı sola eğin (sol kenar aşağı), tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_tilt_right": "Başlığı sağa eğin (sağ kenar aşağı), tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Başlığı saat yönünde çevirin, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Başlığı saat yönünün tersine çevirin, tutun, sonra bırakın.",
|
||||
"ui.space_mouse_view_axis": "Görünüm ekseni",
|
||||
"ui.space_mouse_view_off": "Kapalı",
|
||||
"ui.space_mouse_view_off_tip": "3D fare sürücüsünün profili bu ekseni kapatıyor, bu yüzden hareketi görünümü oynatmaz. Burada başka bir eksenin hareketini seçerseniz ikisi yer değiştirir ve o hareket görünümü oynatmayı bırakır.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Sola / sağa yörünge",
|
||||
"ui.space_mouse_view_orbit_up_down": "Yukarı / aşağı yörünge",
|
||||
"ui.space_mouse_view_pan_sideways": "Sola / sağa kaydır",
|
||||
"ui.space_mouse_view_pan_up_down": "Yukarı / aşağı kaydır",
|
||||
"ui.space_mouse_view_roll": "Yuvarlanma",
|
||||
"ui.space_mouse_view_unmapped": "Eksen {n}",
|
||||
"ui.space_mouse_view_zoom": "Yakınlaştır",
|
||||
"ui.spacing": "Aralık",
|
||||
"ui.spacing_2": "Aralık 2",
|
||||
"ui.sphere": "Küre",
|
||||
"ui.spline": "Spline",
|
||||
"ui.split_body": "Gövdeyi böl",
|
||||
"ui.stand_up_tip": "Bir STEP veya IGES dosyası Z yukarı olarak açıldığında X etrafında çeyrek tur döndürmeyi öner",
|
||||
"ui.standard": "Standart",
|
||||
"ui.start": "Başlangıç",
|
||||
"ui.start_over": "Baştan başla",
|
||||
"ui.start_page": "Başlangıç sayfası",
|
||||
|
||||
+422
-56
@@ -14,11 +14,15 @@
|
||||
"Active": "Активна",
|
||||
"Active rule": "Активне правило",
|
||||
"Add": "Додати",
|
||||
"Add Key": "Додати клавішу",
|
||||
"Add Column": "Додати стовпець",
|
||||
"Add Key": "Додати ключовий кадр",
|
||||
"Add Mid Radius": "Додати середній радіус",
|
||||
"Add Row": "Додати рядок",
|
||||
"Add a tool": "Додати інструмент",
|
||||
"Add condition": "Додати умову",
|
||||
"Add or edit rule": "Додати або змінити правило",
|
||||
"Add to New Folder": "Додати до нової теки",
|
||||
"Advanced": "Додатково",
|
||||
"Against": "Проти",
|
||||
"Alignment": "Вирівнювання",
|
||||
"All Sketch Points": "Усі точки ескізу",
|
||||
@@ -28,9 +32,11 @@
|
||||
"Also deletes": "Також видаляє",
|
||||
"Angle 2": "Кут 2",
|
||||
"Animate": "Анімувати",
|
||||
"App/system information and command settings are included. Review private details before sharing.": "Додано відомості про програму й систему та налаштування команд. Перегляньте приватні дані перед поширенням.",
|
||||
"Appearance name": "Назва вигляду",
|
||||
"Applied Radius": "Застосований радіус",
|
||||
"Apply To": "Застосувати до",
|
||||
"Apply library appearance": "Застосувати вигляд із бібліотеки",
|
||||
"Apply to selection": "Застосувати до виділення",
|
||||
"Apply your appearance first. Exports assigned appearances and materials created in this design.": "Спочатку застосуйте вигляд. Експортує призначені вигляди та матеріали, створені в цьому проєкті.",
|
||||
"Arc length": "Довжина дуги",
|
||||
@@ -41,13 +47,18 @@
|
||||
"BLEND SURFACE": "ПОВЕРХНЯ ПЕРЕХОДУ",
|
||||
"BOUNDARY SURFACE": "ПОВЕРХНЯ ЗА МЕЖАМИ",
|
||||
"BROWSER": "БРАУЗЕР",
|
||||
"Bend Edges": "Ребра згину",
|
||||
"Bend Line Position": "Положення лінії згину",
|
||||
"Bend Position": "Положення згину",
|
||||
"Bend Radius": "Радіус згину",
|
||||
"Bend Table": "Таблиця згинів",
|
||||
"Bends": "Згини",
|
||||
"Bodies": "Тіла",
|
||||
"Bodies (optional)": "Тіла (необов'язково)",
|
||||
"Body": "Тіло",
|
||||
"Body (last if none)": "Тіло (останнє, якщо не вибрано)",
|
||||
"Body to split": "Тіло для розділення",
|
||||
"Body, or faces to take out": "Тіло або грані для вилучення",
|
||||
"Boundary": "Межа",
|
||||
"Bounding box": "Обмежувальна рамка",
|
||||
"CHECK": "ПЕРЕВІРКА",
|
||||
@@ -59,7 +70,9 @@
|
||||
"CSV or JSON: updates existing parameters, adds new ones": "CSV або JSON: оновлює наявні параметри, додає нові",
|
||||
"Capture All Positions": "Зафіксувати всі положення",
|
||||
"Capture Pos.": "Зафікс. полож.",
|
||||
"Catch Depth": "Глибина зачеплення",
|
||||
"Catch Direction": "Напрямок зачеплення",
|
||||
"Catch Length": "Довжина зачеплення",
|
||||
"Cells": "Комірки",
|
||||
"Center X": "Центр X",
|
||||
"Center Y": "Центр Y",
|
||||
@@ -68,22 +81,31 @@
|
||||
"Chamfer Type": "Тип фаски",
|
||||
"Check collisions": "Перевіряти зіткнення",
|
||||
"Checked": "Перевірено",
|
||||
"Choose Variable radius or Spline in Type.": "Виберіть у полі «Тип» значення «Variable radius» або «Spline».",
|
||||
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Виберіть нову площину. Геометрія та розміри ескізу залишаються незмінними у своїх двовимірних координатах.",
|
||||
"Choose…": "Вибрати…",
|
||||
"Circular": "Круговий",
|
||||
"Circumference": "Довжина кола",
|
||||
"Clockwise": "За годинниковою стрілкою",
|
||||
"Close draft": "Закрити чернетку",
|
||||
"Collapse": "Згорнути",
|
||||
"Collisions": "Зіткнення",
|
||||
"Colour": "Колір",
|
||||
"Command": "Команда",
|
||||
"Comment": "Коментар",
|
||||
"Component 1": "Компонент 1",
|
||||
"Component 2": "Компонент 2",
|
||||
"Component1": "Компонент1",
|
||||
"Components": "Компоненти",
|
||||
"Config Table": "Табл. конфігурацій",
|
||||
"Configuration": "Конфігурація",
|
||||
"Configuration rules": "Правила конфігурації",
|
||||
"Contact": "Контакт",
|
||||
"Contact Sets": "Набори контактів",
|
||||
"Contact sets": "Набори контактів",
|
||||
"Continue on GitHub": "Продовжити на GitHub",
|
||||
"Continuity": "Неперервність",
|
||||
"Control Radius": "Керівний радіус",
|
||||
"Copy Error Message": "Копіювати повідомлення про помилку",
|
||||
"Copy Warning Message": "Копіювати попереджувальне повідомлення",
|
||||
"Copy all licences": "Копіювати всі ліцензії",
|
||||
@@ -93,8 +115,11 @@
|
||||
"Corner Relief": "Полегшення кута",
|
||||
"Corner Relief Shape": "Форма полегшення кута",
|
||||
"Counterbore depth": "Глибина циліндричного розширення",
|
||||
"Counterbore Ø": "Ø циліндричного розширення",
|
||||
"Countersink angle": "Кут зенкування",
|
||||
"Countersink Ø": "Ø зенкування",
|
||||
"Create Issue — review draft": "Створити повідомлення про проблему — перегляд чернетки",
|
||||
"Create Issue…": "Створити повідомлення про проблему…",
|
||||
"Cross curves": "Поперечні криві",
|
||||
"Cube": "Куб",
|
||||
"Current orthographic view (1:1 mm)": "Поточний ортографічний вид (1:1 mm)",
|
||||
@@ -102,18 +127,28 @@
|
||||
"Curv. Comb": "Гребінь кривини",
|
||||
"Curvature (G2)": "Кривина (G2)",
|
||||
"Curvature change (G3)": "Зміна кривини (G3)",
|
||||
"Custom": "Власний",
|
||||
"Customize Toolbar": "Налаштувати панель інструментів",
|
||||
"Customize…": "Налаштувати…",
|
||||
"Cylindrical face": "Циліндрична грань",
|
||||
"Delete": "Видалити",
|
||||
"Delete Selected Features…": "Видалити вибрані операції…",
|
||||
"Delete Selected Folders": "Видалити вибрані теки",
|
||||
"Delete all": "Видалити все",
|
||||
"Delete rule": "Видалити правило",
|
||||
"Delete the row": "Видалити рядок",
|
||||
"Delete the set": "Видалити набір",
|
||||
"Density": "Густина",
|
||||
"Dependents": "Залежні",
|
||||
"Describe the problem you saw": "Опишіть проблему, яку ви побачили",
|
||||
"Describe what happened before continuing.": "Опишіть, що сталося, перш ніж продовжити.",
|
||||
"Designs you open or save appear here.": "Проєкти, які ви відкриваєте чи зберігаєте, з'являються тут.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Визначається об'єктом призначення. Виберіть «Кут», щоб редагувати кутову протяжність.",
|
||||
"Diameter": "Діаметр",
|
||||
"Direction": "Напрямок",
|
||||
"Discard": "Відхилити",
|
||||
"Distance": "Відстань",
|
||||
"Distance 1": "Відстань 1",
|
||||
"Do not export hidden edges": "Не експортувати приховані ребра",
|
||||
"Don't ask again": "Більше не питати",
|
||||
"Double-click to rename": "Двічі клацніть, щоб перейменувати",
|
||||
@@ -125,17 +160,22 @@
|
||||
"Edit Profile Sketch": "Редагувати ескіз профілю",
|
||||
"Enable rule": "Увімкнути правило",
|
||||
"Enabled": "Увімкнено",
|
||||
"End Radius": "Кінцевий радіус",
|
||||
"End bulge": "Роздуття кінця",
|
||||
"End continuity": "Неперервність кінця",
|
||||
"Expand": "Розгорнути",
|
||||
"Expected result (optional)": "Очікуваний результат (необов'язково)",
|
||||
"Explode": "Рознести",
|
||||
"Explode Text": "Розібрати текст",
|
||||
"Export Animation…": "Експортувати анімацію…",
|
||||
"Export Bend Table…": "Експортувати таблицю згинів…",
|
||||
"Export CSV…": "Експорт CSV…",
|
||||
"Export Current View as DXF": "Експортувати поточний вид як DXF",
|
||||
"Export Current View as DXF…": "Експортувати поточний вид як DXF…",
|
||||
"Export DXF": "Експорт DXF",
|
||||
"Export DXF…": "Експорт DXF…",
|
||||
"Export JSON…": "Експорт JSON…",
|
||||
"Export design library...": "Експортувати бібліотеку проєкту...",
|
||||
"Export hidden edges as dashed lines": "Експортувати приховані ребра як штрихові лінії",
|
||||
"Expression": "Вираз",
|
||||
"Extend to meet": "Подовжити до перетину",
|
||||
@@ -143,31 +183,41 @@
|
||||
"FILES": "ФАЙЛИ",
|
||||
"FLAT": "РОЗГОРТКА",
|
||||
"Face": "Грань",
|
||||
"Face Appearance": "Вигляд грані",
|
||||
"Face attachment lost": "Втрачено прив'язку до грані",
|
||||
"Faces": "Грані",
|
||||
"Faces / edges / vertices": "Грані / ребра / вершини",
|
||||
"Faces to split": "Грані для розділення",
|
||||
"Fastener": "Кріплення",
|
||||
"Favorites": "Обране",
|
||||
"Favourite": "Обране",
|
||||
"Feature": "Операція",
|
||||
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Файли з багатьох CAD-програм мають напрямок Y вгору, тому тут вони відкриваються лежачи на спині (Z вгору).",
|
||||
"Filter": "Фільтр",
|
||||
"Filter…": "Фільтр…",
|
||||
"Find in Browser": "Знайти в браузері",
|
||||
"Finish": "Оздоблення",
|
||||
"First plane": "Перша площина",
|
||||
"Fit and G2 corners require Constant radius.": "Для підгонки та кутів G2 потрібен тип «Constant radius».",
|
||||
"Fixed Face": "Фіксована грань",
|
||||
"Flat Pattern": "Розгортка",
|
||||
"Flat size": "Розмір розгортки",
|
||||
"Follow camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
|
||||
"Follow the camera (Look At sketches)": "Стежити за камерою (ескізи наведення)",
|
||||
"Font": "Шрифт",
|
||||
"From": "Від",
|
||||
"From Bodies": "З тіл",
|
||||
"From edges": "Початкові ребра",
|
||||
"Full Length": "Повна довжина",
|
||||
"G2 corners require a constant radius and Exact fit. G2 corner construction is not supported yet.": "Кути G2 потребують постійного радіуса та підгонки «Exact». Побудова кутів G2 ще не підтримується.",
|
||||
"Gap": "Зазор",
|
||||
"Grid": "Сітка",
|
||||
"HISTORY": "ІСТОРІЯ",
|
||||
"Height": "Висота",
|
||||
"Hidden edges": "Приховані ребра",
|
||||
"History layout": "Розташування історії",
|
||||
"History names": "Назви в історії",
|
||||
"Hole (optional)": "Отвір (необов'язково)",
|
||||
"Holes": "Отвори",
|
||||
"Holes (edges on them)": "Отвори (ребра на них)",
|
||||
"Home view": "Головний вид",
|
||||
@@ -178,14 +228,20 @@
|
||||
"In place (surfaces)": "На місці (поверхні)",
|
||||
"In plane": "У площині",
|
||||
"Inside": "Усередину",
|
||||
"Instant": "Миттєво",
|
||||
"Interface font": "Шрифт інтерфейсу",
|
||||
"Interferes": "Перетинається",
|
||||
"Invert": "Інвертувати",
|
||||
"Isocurve": "Ізокрива",
|
||||
"Issue title": "Заголовок повідомлення",
|
||||
"Items": "Елементи",
|
||||
"JOINTS": "З’ЄДНАННЯ",
|
||||
"Joint Origin1": "Початок з'єднання1",
|
||||
"Keep (points on it)": "Залишити (точки на ньому)",
|
||||
"Keep Tools": "Зберегти інструменти",
|
||||
"Keep of the other surface": "Залишити від іншої поверхні",
|
||||
"Key a joint's value at this step": "Вставити ключовий кадр зі значенням з'єднання на цьому кроці",
|
||||
"Keys": "Ключові кадри",
|
||||
"LIBRARY": "БІБЛІОТЕКА",
|
||||
"LINKS": "ПОСИЛАННЯ",
|
||||
"Later": "Пізніше",
|
||||
@@ -196,9 +252,14 @@
|
||||
"Letter Spacing": "Міжлітерний інтервал",
|
||||
"Library": "Бібліотека",
|
||||
"Library file": "Файл бібліотеки",
|
||||
"Limits": "Межі",
|
||||
"Line Spacing": "Міжрядковий інтервал",
|
||||
"Line Type": "Тип лінії",
|
||||
"Listing the installed fonts…": "Пошук встановлених шрифтів…",
|
||||
"Load library": "Завантажити бібліотеку",
|
||||
"Local library": "Локальна бібліотека",
|
||||
"Loop": "Контур",
|
||||
"M5 or M5x0.5": "M5 або M5x0.5",
|
||||
"MANAGE": "КЕРУВАННЯ",
|
||||
"MATERIALS": "МАТЕРІАЛИ",
|
||||
"MODEL": "МОДЕЛЬ",
|
||||
@@ -209,14 +270,20 @@
|
||||
"Matched restrictions check the final values.": "Обмеження, умови яких виконано, перевіряють кінцеві значення.",
|
||||
"Material": "Матеріал",
|
||||
"Max": "Макс.",
|
||||
"Maximum": "Максимум",
|
||||
"Mesh body": "Тіло сітки",
|
||||
"Mid Radius": "Середній радіус",
|
||||
"Middle double-click": "Подвійне клацання середньою кнопкою",
|
||||
"Min": "Мін.",
|
||||
"Minimum": "Мінімум",
|
||||
"Mirror Plane": "Площина дзеркала",
|
||||
"Mitre": "Скіс",
|
||||
"Model Parameters": "Параметри моделі",
|
||||
"Modeled": "Змодельовано",
|
||||
"More toolbar panels": "Більше панелей інструментів",
|
||||
"Motion": "Рух",
|
||||
"Mouse gestures": "Жести мишею",
|
||||
"Mouse gestures only work with the SolidWorks mouse mode (the wheel orbits, Ctrl + wheel pans).": "Жести мишею працюють лише в режимі миші SolidWorks (колесо обертає, Ctrl + колесо панорамує).",
|
||||
"Move / Copy": "Перемістити / копіювати",
|
||||
"Move Type": "Тип переміщення",
|
||||
"Move down": "Перемістити вниз",
|
||||
@@ -234,15 +301,22 @@
|
||||
"No matching appearance presets.": "Немає відповідних наборів вигляду.",
|
||||
"No matching appearances": "Немає відповідних виглядів",
|
||||
"No visible bodies or sketches to export.": "Немає видимих тіл або ескізів для експорту.",
|
||||
"Nominal diameter": "Номінальний діаметр",
|
||||
"None": "Немає",
|
||||
"Not available yet": "Ще недоступно",
|
||||
"Notch": "Виріз",
|
||||
"OUTPUT": "ВИВЕДЕННЯ",
|
||||
"Of": "Ступінь вільності",
|
||||
"On your computer": "На цьому комп'ютері",
|
||||
"Opacity": "Непрозорість",
|
||||
"Open GitHub draft": "Відкрити чернетку GitHub",
|
||||
"Open library...": "Відкрити бібліотеку...",
|
||||
"Open local libraries in the desktop app. You can export this design's library here.": "Відкривайте локальні бібліотеки в настільному застосунку. Тут можна експортувати бібліотеку цього проєкту.",
|
||||
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Параметричний 3D-CAD з відкритим вихідним кодом на Rust. Ліцензовано за MIT OR Apache-2.0.",
|
||||
"Opens your browser and copies the report text for you to paste into GitHub.": "Відкриває браузер і копіює текст звіту, щоб ви вставили його на GitHub.",
|
||||
"Opens your browser. Review and submit your report on GitHub.": "Відкриває браузер. Перегляньте й надішліть звіт на GitHub.",
|
||||
"Operator": "Оператор",
|
||||
"Orbit (right drag)": "Обертання (перетягування правою кнопкою)",
|
||||
"Other side": "Інший бік",
|
||||
"Other surface (mutual trim)": "Інша поверхня (взаємне обрізання)",
|
||||
"Outside": "Назовні",
|
||||
@@ -257,8 +331,11 @@
|
||||
"Pick the tool bodies": "Виберіть тіла інструмента",
|
||||
"Picked": "Вибрано",
|
||||
"Picked point": "Вибрана точка",
|
||||
"Pitch": "Крок",
|
||||
"Pivot": "Точка обертання",
|
||||
"Place at the origin instead": "Натомість розмістити в початку координат",
|
||||
"Plastic Rules": "Правила пластику",
|
||||
"Play the joint's motion in the view": "Відтворити рух з'єднання у виді",
|
||||
"Playback": "Відтворення",
|
||||
"Plot": "Графік",
|
||||
"Position": "Положення",
|
||||
@@ -266,14 +343,18 @@
|
||||
"Position (G0)": "Положення (G0)",
|
||||
"Position along the edge, 0 at the start end, 1 at the other": "Положення вздовж ребра: 0 на початку, 1 на іншому кінці",
|
||||
"Preferences › Design turns the offer back on": "Налаштування › Дизайн повертає пропозицію",
|
||||
"Preview": "Попередній перегляд",
|
||||
"Preview Row": "Рядок перегляду",
|
||||
"Preview leaves the current design unchanged.": "Попередній перегляд не змінює поточний проєкт.",
|
||||
"Profile / Edges": "Профіль / ребра",
|
||||
"Profiles or curves": "Профілі або криві",
|
||||
"Profiles/Curves": "Профілі/криві",
|
||||
"Quantity": "Величина",
|
||||
"Quantity": "Кількість",
|
||||
"RECENT": "НЕДАВНІ",
|
||||
"REFERENCE": "ДОПОМІЖНА ГЕОМЕТРІЯ",
|
||||
"RULED SURFACE": "ЛІНІЙЧАСТА ПОВЕРХНЯ",
|
||||
"RULES": "ПРАВИЛА",
|
||||
"Radius": "Радіус",
|
||||
"Radius Fit": "Підгонка за радіусом",
|
||||
"Ratio": "Співвідношення",
|
||||
"Reattach selected face": "Повторно приєднати вибрану грань",
|
||||
@@ -282,6 +363,8 @@
|
||||
"Rectangular": "Прямокутний",
|
||||
"Reference face": "Опорна грань",
|
||||
"Remove Tools": "Прибрати інструменти",
|
||||
"Remove condition": "Прибрати умову",
|
||||
"Remove from Folder": "Прибрати з теки",
|
||||
"Remove from list": "Прибрати зі списку",
|
||||
"Remove override": "Прибрати перевизначення",
|
||||
"Remove private design details or paths before sharing.": "Перед публікацією приберіть приватні дані проєкту або шляхи.",
|
||||
@@ -289,12 +372,18 @@
|
||||
"Remove the column": "Прибрати стовпець",
|
||||
"Remove the track": "Прибрати доріжку",
|
||||
"Remove this point": "Прибрати цю точку",
|
||||
"Rename Feature": "Перейменувати операцію",
|
||||
"Rename Folder": "Перейменувати теку",
|
||||
"Replace existing library file": "Замінити наявний файл бібліотеки",
|
||||
"Report a bug — review a screenshot and open a GitHub draft": "Повідомити про помилку — переглянути знімок екрана й відкрити чернетку GitHub",
|
||||
"Report text copied. Paste it into GitHub and submit your report there.": "Текст звіту скопійовано. Вставте його на GitHub і надішліть звіт там.",
|
||||
"Reports are public. A GitHub account is required.": "Звіти загальнодоступні. Потрібен обліковий запис GitHub.",
|
||||
"Reset Toolbar": "Скинути панель інструментів",
|
||||
"Resize Part": "Змінити розмір деталі",
|
||||
"Restore View": "Відновити вид",
|
||||
"Restore outer boundary": "Відновити зовнішню межу",
|
||||
"Restore the inherited component or physical material appearance": "Відновити успадкований вигляд компонента або фізичного матеріалу",
|
||||
"Restore this studio's tools": "Відновити інструменти цієї студії",
|
||||
"Result": "Результат",
|
||||
"Retake screenshot": "Повторити знімок екрана",
|
||||
"Reverse": "Обернути",
|
||||
@@ -304,7 +393,14 @@
|
||||
"Revert Pos.": "Скасув. полож.",
|
||||
"Revert Position": "Повернути положення",
|
||||
"Review the report and screenshot before opening GitHub.": "Перегляньте звіт і знімок екрана перед відкриттям GitHub.",
|
||||
"Review your report on GitHub and click Submit new issue to publish it.": "Перегляньте звіт на GitHub і натисніть Submit new issue, щоб опублікувати його.",
|
||||
"Rib Length": "Довжина ребра",
|
||||
"Rib Offset": "Зсув ребра",
|
||||
"Rib Thickness": "Товщина ребра",
|
||||
"Ribs": "Ребра",
|
||||
"Rim Edge": "Крайове ребро",
|
||||
"Ring of tools on the right button": "Кільце інструментів на правій кнопці",
|
||||
"Roll Back Here": "Відкотити сюди",
|
||||
"Roll History Marker Here": "Перемістити маркер історії сюди",
|
||||
"Round": "Круглий",
|
||||
"Rule": "Правило",
|
||||
@@ -314,25 +410,34 @@
|
||||
"SURFACE QUALITY": "ЯКІСТЬ ПОВЕРХНІ",
|
||||
"Save Rule": "Зберегти правило",
|
||||
"Save View": "Зберегти вид",
|
||||
"Save screenshot…": "Зберегти знімок екрана…",
|
||||
"Save the study as an animated GIF, seen as the viewport shows it": "Зберегти дослідження як анімований GIF, як його показує вікно перегляду",
|
||||
"Saved preview. Geometry is being checked before editing.": "Збережений перегляд. Геометрію перевіряють перед редагуванням.",
|
||||
"Scale factor": "Коефіцієнт масштабу",
|
||||
"Scene": "Сцена",
|
||||
"Screenshot (optional)": "Знімок екрана (необов'язково)",
|
||||
"Search": "Пошук",
|
||||
"Search commands": "Пошук команд",
|
||||
"Search this library": "Пошук у цій бібліотеці",
|
||||
"Second plane": "Друга площина",
|
||||
"Section": "Переріз",
|
||||
"Section position": "Положення перерізу",
|
||||
"Sections (one sketch each)": "Перерізи (по одному ескізу)",
|
||||
"Select Other": "Вибрати інше",
|
||||
"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 bodies (a face picks its body)": "Виділяти тіла (грань виділяє своє тіло)",
|
||||
"Select bodies or faces instead": "Натомість виділіть тіла або грані",
|
||||
"Select edges": "Виберіть ребра",
|
||||
"Selection filter": "Фільтр виділення",
|
||||
"Semantic faces share their appearance across instances.": "Семантичні грані спільно використовують свій вигляд між екземплярами.",
|
||||
"Send your report on GitHub": "Надіслати звіт на GitHub",
|
||||
"Shaded": "Затінений",
|
||||
"Shape": "Форма",
|
||||
"Sheet Rules": "Правила листа",
|
||||
"Show Agent Cursor": "Показати курсор агента",
|
||||
"Show in folder": "Показати в теці",
|
||||
"Show the pointer an AI agent moves (#35)": "Показувати вказівник, яким рухає ШІ-агент (#35)",
|
||||
"Show tool at cursor": "Показувати інструмент біля курсора",
|
||||
"Signed axes": "Підписані осі",
|
||||
"Size": "Розмір",
|
||||
"Sketch constraints": "Обмеження ескізу",
|
||||
@@ -345,24 +450,37 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрився до того, як ці проєкти було збережено. Можна відкрити їхнє останнє автозбереження.",
|
||||
"Specialise": "Спеціалізувати",
|
||||
"Speed": "Швидкість",
|
||||
"Splitting tool": "Інструмент розділення",
|
||||
"Square": "Квадрат",
|
||||
"Stand it up": "Вирівняти",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Вирівняти: поворот на чверть оберту навколо X як одна операція переміщення, яку можна скасувати.",
|
||||
"Stand this part up?": "Вирівняти цю деталь?",
|
||||
"Start angle": "Початковий кут",
|
||||
"Start bulge": "Роздуття початку",
|
||||
"Start continuity": "Неперервність початку",
|
||||
"Step": "Крок",
|
||||
"Steps": "Кроки",
|
||||
"Steps to reproduce (optional)": "Кроки відтворення (необов'язково)",
|
||||
"Stitch the kept pieces": "Зшити залишені частини",
|
||||
"Stitch to both bodies": "Зшити з обома тілами",
|
||||
"Studio": "Студія",
|
||||
"Study": "Дослідження",
|
||||
"Style": "Стиль",
|
||||
"Summary": "Короткий опис",
|
||||
"Suppress": "Придушити",
|
||||
"Suppressed": "Придушено",
|
||||
"Surface": "Поверхня",
|
||||
"Surface, or faces to turn over": "Поверхня або грані, щоб обернути нормаль",
|
||||
"Swap Ends": "Поміняти кінці місцями",
|
||||
"TRIM": "ОБРІЗАННЯ",
|
||||
"Tangent (G1)": "Дотик (G1)",
|
||||
"Tangent Chain": "Дотичний ланцюг",
|
||||
"Tangent chain": "Дотичний ланцюг",
|
||||
"Tap drill diameter": "Діаметр свердла під різьбу",
|
||||
"Target": "Цільова грань або площина",
|
||||
"Target Body": "Цільове тіло",
|
||||
"Tear": "Розрив",
|
||||
"Technical details": "Технічні подробиці",
|
||||
"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": "Потім",
|
||||
@@ -374,24 +492,32 @@
|
||||
"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.": "Ця грань не може забезпечити надійну прив'язку. Виберіть іншу грань або явно використайте фіксовану площину.",
|
||||
"This image is not attached to GitHub yet. Copy or save it, then attach it on GitHub.": "Це зображення ще не додано до GitHub. Скопіюйте або збережіть його, а потім додайте на GitHub.",
|
||||
"To": "До",
|
||||
"To Offset": "До зсуву",
|
||||
"To edges": "Кінцеві ребра",
|
||||
"Tolerance": "Допуск",
|
||||
"Too close": "Занадто близько",
|
||||
"Tool Bodies": "Тіла інструмента",
|
||||
"Tools": "Інструменти",
|
||||
"Tools, clockwise from the top": "Інструменти за годинниковою стрілкою згори",
|
||||
"Trimming tool (plane, face or curves)": "Інструмент обрізання (площина, грань або криві)",
|
||||
"Turn off section analysis before exporting the view.": "Вимкніть аналіз перерізу перед експортом виду.",
|
||||
"Type": "Тип",
|
||||
"UNTRIM": "ЗНЯТИ ОБРІЗАННЯ",
|
||||
"Unit": "Одиниця",
|
||||
"Unsuppress": "Скасувати придушення",
|
||||
"Updating the preview": "Оновлення попереднього перегляду",
|
||||
"Use active author placement": "Використати активне вихідне розміщення",
|
||||
"Use fixed plane": "Використати фіксовану площину",
|
||||
"Use saved fixed plane": "Використати збережену фіксовану площину",
|
||||
"Use selected face as fixed plane": "Використати вибрану грань як фіксовану площину",
|
||||
"Use the SolidWorks mouse mode": "Використовувати режим миші SolidWorks",
|
||||
"User Parameters": "Параметри користувача",
|
||||
"VIEW": "ВИД",
|
||||
"VIEWS": "ВИГЛЯДИ",
|
||||
"Value": "Значення",
|
||||
"Variable Radius": "Змінний радіус",
|
||||
"Velocity": "Швидкість",
|
||||
"Vertices": "Вершини",
|
||||
"View navigator": "Навігатор виду",
|
||||
@@ -399,19 +525,54 @@
|
||||
"Wall": "Стінка",
|
||||
"Wall Location": "Розташування стінки",
|
||||
"Wall Thickness": "Товщина стінки",
|
||||
"What did you do just before the problem? A short list is enough": "Що ви робили безпосередньо перед проблемою? Короткого переліку достатньо",
|
||||
"What did you expect to see?": "Що ви очікували побачити?",
|
||||
"What happened?": "Що сталося?",
|
||||
"When": "Коли",
|
||||
"Width": "Ширина",
|
||||
"Will fail": "Зазнає невдачі",
|
||||
"With gestures on, the right button no longer orbits the view: hold it and move to open the ring, then touch a tool's zone. A plain right-click still opens the menu.": "Коли жести ввімкнено, права кнопка більше не обертає вид: утримуйте її та рухайте, щоб відкрити кільце, а потім наведіть на зону інструмента. Звичайне клацання правою кнопкою так само відкриває меню.",
|
||||
"X": "X",
|
||||
"X Angle": "Кут X",
|
||||
"X angle": "Кут X",
|
||||
"X distance": "Відстань X",
|
||||
"Y": "Y",
|
||||
"Y Angle": "Кут Y",
|
||||
"Y angle": "Кут Y",
|
||||
"Y distance": "Відстань Y",
|
||||
"Z": "Z",
|
||||
"Z Angle": "Кут Z",
|
||||
"Z Distance": "Відстань Z",
|
||||
"Z angle": "Кут Z",
|
||||
"Z distance": "Відстань Z",
|
||||
"Zebra": "Зебра",
|
||||
"ai.prefs.availability": "Керування асистентом",
|
||||
"ai.prefs.background_commands": "Виконувати команди агента у фоновому режимі",
|
||||
"ai.prefs.follow_camera": "Повертати камеру до ескізів",
|
||||
"ai.prefs.follow_camera_off": "Автоматичне стеження камери вимкнено.",
|
||||
"ai.prefs.follow_camera_on": "Камера повертається до ескізів, які створює агент.",
|
||||
"ai.prefs.intro": "Агенти, дозволи та те, як показується їхня робота.",
|
||||
"ai.prefs.make_default": "Використовувати за замовчуванням",
|
||||
"ai.prefs.no_agents": "Агентів ще немає. Додайте агента або оновіть список, щоб знайти встановлених.",
|
||||
"ai.prefs.no_saved_approvals": "Немає збережених дозволів.",
|
||||
"ai.prefs.override_all_warning": "Цей агент може зберігати, перезаписувати, експортувати або замінювати проєкт без запиту.",
|
||||
"ai.prefs.permission_all": "Дозволяти все",
|
||||
"ai.prefs.permission_all_summary": "Доступні інструменти SolidCraft працюють без запиту.",
|
||||
"ai.prefs.permission_ask": "Запитувати перед змінами",
|
||||
"ai.prefs.permission_ask_summary": "Підтверджувати кожну зміну проєкту та дію з файлами.",
|
||||
"ai.prefs.permission_edits": "Дозволяти редагування моделі",
|
||||
"ai.prefs.permission_edits_summary": "Редагування моделі виконується; для дій з файлами й руйнівних дій усе ще запитують дозвіл.",
|
||||
"ai.prefs.permission_scope": "Лише для інструментів SolidCraft. Оболонка агента та редагування файлів підкоряються його власним дозволам.",
|
||||
"ai.prefs.pointer_preview_hidden": "Вказівник приховано",
|
||||
"ai.prefs.pointer_preview_speed": "Вказівник агента · {speed}",
|
||||
"ai.prefs.pointer_speed": "Швидкість руху",
|
||||
"ai.prefs.privacy_note": "Розмову отримує лише вибраний вами агент або кінцева точка. Ключі кінцевих точок зберігаються в системному сховищі ключів.",
|
||||
"ai.prefs.show_pointer": "Показати вказівник агента",
|
||||
"ai.prefs.turn_limit_hint": "Максимальна кількість викликів інструментів, що змінюють проєкт, які Асистент може зробити за одне повідомлення.",
|
||||
"angle, height": "кут, висота",
|
||||
"appearance.dielectric_ior": "Діелектричний IOR",
|
||||
"appearance.metallic": "Металевість",
|
||||
"appearance.only_available": "Лише доступні зовнішні вигляди",
|
||||
"appearance.pbr_help": "Перегляньте цю поверхню в режимі рендерингу або як PNG-зображення виробу. Без заломлення чи пропускання.",
|
||||
"appearance.pbr_opaque": "Непрозорий PBR",
|
||||
"appearance.roughness": "Шорсткість",
|
||||
@@ -530,6 +691,10 @@
|
||||
"cmd.file_insert_step": "Вставити STEP",
|
||||
"cmd.file_save_mesh": "Зберегти як сітку",
|
||||
"cmd.file_slicers": "Встановлені слайсери",
|
||||
"cmd.frame_member": "Елемент рами",
|
||||
"cmd.frame_profile_insert": "Вставити профіль",
|
||||
"cmd.frame_profile_list": "Бібліотека профілів",
|
||||
"cmd.frame_profile_properties": "Властивості профілю",
|
||||
"cmd.inspect_accessibility": "Аналіз доступності",
|
||||
"cmd.inspect_center_of_mass": "Центр мас",
|
||||
"cmd.inspect_clearance": "Зазор",
|
||||
@@ -569,6 +734,7 @@
|
||||
"cmd.material_create": "Новий фізичний матеріал",
|
||||
"cmd.material_delete": "Видалити фізичний матеріал",
|
||||
"cmd.material_edit": "Редагувати фізичний матеріал",
|
||||
"cmd.material_inspect": "Перевірити фізичний матеріал",
|
||||
"cmd.material_list": "Список матеріалів",
|
||||
"cmd.material_properties_directional": "Спрямований модуль пружності",
|
||||
"cmd.material_properties_validate": "Перевірити дані фізичного матеріалу",
|
||||
@@ -759,6 +925,7 @@
|
||||
"cmd.view_graphics_preset": "Набір графіки",
|
||||
"cmd.view_list": "Список іменованих видів",
|
||||
"cmd.view_navigate": "Навігація видом",
|
||||
"cmd.view_normal_to": "Перпендикулярно до виділення",
|
||||
"cmd.view_rename": "Перейменувати іменований вид",
|
||||
"cmd.view_save": "Новий іменований вид",
|
||||
"color.copy": "Клацніть, щоб скопіювати значення кольору",
|
||||
@@ -856,6 +1023,12 @@
|
||||
"help.licences": "Ліцензії",
|
||||
"help.mit": "Ліцензія MIT",
|
||||
"help.notice": "NOTICE",
|
||||
"help.solid.chamfer": "Зрізає вибрані ребра плоским скосом на заданій відстані. Використовуйте, щоб притупити гострі ребра або полегшити вставляння штифта чи гвинта.",
|
||||
"help.solid.extrude": "Витягує профіль ескізу в 3D на задану відстань, додаючи або видаляючи матеріал. Використовуйте, щоб перетворити плоский контур на брусок, бобишку або кишеню.",
|
||||
"help.solid.fillet": "Закруглює вибрані ребра плавним круговим переходом заданого радіуса. Використовуйте, щоб пом'якшити гострі ребра для міцності, зручності чи вигляду.",
|
||||
"help.solid.hole": "Просвердлює круглий отвір заданого діаметра в обраній точці на грані. Використовуйте для отворів під гвинти, штифтів та інших простих круглих отворів.",
|
||||
"help.solid.revolve": "Обертає профіль ескізу навколо осі, утворюючи кругле тіло. Використовуйте для валів, ручок, чаш та будь-яких деталей, круглих навколо осьової лінії.",
|
||||
"help.solid.shell": "Робить тіло порожнистим, залишаючи стінки заданої товщини, і може відкривати вибрані грані. Використовуйте для коробок, кришок та інших тонкостінних деталей.",
|
||||
"help.third_party": "Ліцензії сторонніх компонентів",
|
||||
"home.back": "Назад до {name}",
|
||||
"home.crashes": "Звіти про збої",
|
||||
@@ -960,11 +1133,21 @@
|
||||
"motion.key_here": "Вставити ключовий кадр тут",
|
||||
"motion.key_here_hint": "Додати ключовий кадр на цьому кроці з поточним значенням з’єднання",
|
||||
"motion.no_collisions": "За все дослідження зіткнень немає",
|
||||
"motion.plot_quantity": "Величина",
|
||||
"motion.step_back": "Крок назад",
|
||||
"motion.step_forward": "Крок уперед",
|
||||
"motion.to_end": "У кінець",
|
||||
"motion.to_start": "На початок",
|
||||
"motion.track_name": "Доріжка {number}",
|
||||
"nav.alias.help": "Shift+Alt+ліва обертає, Shift+Alt+середня панорамує, Shift+Alt+права масштабує; середня також панорамує, права обертає",
|
||||
"nav.blender.help": "Середня обертає, Shift+середня панорамує, Ctrl+середня масштабує; права не рухає камеру",
|
||||
"nav.free_orbit.help": "Миша та 3D-миша обертають через обидва полюси й зберігають крен. Прив'язування до осей через Shift вимкнено.",
|
||||
"nav.free_orbit.label": "Вільне обертання",
|
||||
"nav.fusion.help": "Середня панорамує, Shift+середня обертає, права обертає",
|
||||
"nav.inventor.help": "Середня панорамує, Shift+середня обертає, Ctrl+середня масштабує, права обертає",
|
||||
"nav.openscad.help": "Ліва обертає по порожньому місцю, Shift+ліва виділяє рамкою, геометрію ескізу перетягують для редагування; права або середня панорамує, Shift+права або Shift+середня масштабує",
|
||||
"nav.solidworks.help": "Середня обертає, Ctrl+середня панорамує, Shift+середня масштабує, права обертає",
|
||||
"nav.tinkercad.help": "Права обертає, середня або Shift+права панорамує",
|
||||
"nav.touchpad.help": "Утримуйте Shift і рухайте вказівник, щоб панорамувати; утримуйте Alt/Option і рухайте, щоб обертати. Кнопка миші не потрібна. Вертикальна прокрутка масштабує.",
|
||||
"nav.touchpad.label": "Трекпад",
|
||||
"nav.touchpad.orbit": "Обертання (утримуйте Alt/Option і рухайте; без кнопки миші)",
|
||||
@@ -987,8 +1170,130 @@
|
||||
"pattern.flip_direction": "Змінити напрямок",
|
||||
"pick a hole to seat it, or OK places it at the origin": "виберіть отвір для посадки, або OK розмістить його в початку координат",
|
||||
"pick one or two vertices, edges or faces": "виберіть одну-дві вершини, ребра або грані",
|
||||
"prefs.add_item": "Додати елемент",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option також дає вільне перетягування стрілок маніпулятора",
|
||||
"prefs.anti_aliasing_note": "Згладжує нерівні краї.",
|
||||
"prefs.applies_on_restart": "Застосовується після перезапуску",
|
||||
"prefs.assistant": "Асистент",
|
||||
"prefs.axes": "Осі",
|
||||
"prefs.axis": "Вісь",
|
||||
"prefs.bottom": "Низ",
|
||||
"prefs.browser": "Браузер",
|
||||
"prefs.built_in": "Вбудована",
|
||||
"prefs.button_left": "Перетягування лівою кнопкою",
|
||||
"prefs.button_middle": "Перетягування середньою кнопкою",
|
||||
"prefs.button_right": "Перетягування правою кнопкою",
|
||||
"prefs.choose_font": "Вибрати файл шрифту…",
|
||||
"prefs.click": "Клацання",
|
||||
"prefs.contact_shading": "М'яке контактне затінення",
|
||||
"prefs.contact_shading_note": "М'яке затінення там, де деталі зближуються.",
|
||||
"prefs.context_menu": "Контекстне меню",
|
||||
"prefs.context_menu_ring": "Контекстне меню · утримуйте для кругового меню",
|
||||
"prefs.cpu": "ЦП (програмний)",
|
||||
"prefs.crash_reports": "Відкрити звіти про збої…",
|
||||
"prefs.crash_reports_note": "Звіти зберігаються на цьому комп'ютері разом із проєктом і командами, що відтворюють проблему.",
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Криві поверхні",
|
||||
"prefs.curves": "Криві поверхні",
|
||||
"prefs.dead_zone_note": "Заштрихована смуга — це мертва зона: рух у її межах ігнорується.",
|
||||
"prefs.default": "типово",
|
||||
"prefs.design": "Типові параметри проєкту",
|
||||
"prefs.design_note": "З чого починаються нові проєкти. Відкриті проєкти зберігають власні параметри.",
|
||||
"prefs.devices": "3D-миша",
|
||||
"prefs.display": "Відображення",
|
||||
"prefs.edges": "Ребра та затінення",
|
||||
"prefs.extend": "Розширення",
|
||||
"prefs.extra_navigation": "Ctrl+перетягування лівою на порожньому місці обертає; утримуйте Space, щоб панорамувати.",
|
||||
"prefs.follows_os": "Як у системі",
|
||||
"prefs.font": "Шрифт",
|
||||
"prefs.font_fallback": "Символи, яких немає у вибраному шрифті, беруться з вбудованого, тож жоден напис не показує порожній квадрат.",
|
||||
"prefs.font_sample": "Ґедзь, що їв фінік, з'їв і юшку · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "Не вдалося прочитати вибраний шрифт.",
|
||||
"prefs.general": "Загальні",
|
||||
"prefs.general_note": "Мова, збереження та оновлення.",
|
||||
"prefs.gesture_orbit": "Обертання",
|
||||
"prefs.gesture_pan": "Панорамування",
|
||||
"prefs.gesture_zoom": "Масштаб",
|
||||
"prefs.graphics": "Графіка",
|
||||
"prefs.graphics_note": "Обміняйте швидкість на плавність. Кожен варіант показує свій ефект на зразку.",
|
||||
"prefs.graphics_presets_note": "Набори задають наведені нижче параметри разом.",
|
||||
"prefs.history": "Історія",
|
||||
"prefs.hold_stop_snapping": "Утримуйте, щоб вимкнути прив'язування",
|
||||
"prefs.icons_only": "Лише піктограми",
|
||||
"prefs.imported_file": "Імпортований файл",
|
||||
"prefs.input": "Введення",
|
||||
"prefs.interface": "Інтерфейс",
|
||||
"prefs.interface_language": "Мова інтерфейсу",
|
||||
"prefs.interface_note": "Як виглядає SolidCraft і де розташовані його частини.",
|
||||
"prefs.interval_minutes": "{n} хв",
|
||||
"prefs.labels": "Підписи",
|
||||
"prefs.layout": "Компонування",
|
||||
"prefs.layout_note": "«Компактне» прикріплює панелі до країв вікна. Прозорість браузера доступна в режимі «Плаваюче». Сторону Асистента можна змінити на сторінці «Асистент».",
|
||||
"prefs.left": "Ліворуч",
|
||||
"prefs.left_click": "Клацання лівою",
|
||||
"prefs.live_input": "Поточне введення",
|
||||
"prefs.live_preview": "Попередній перегляд у реальному часі",
|
||||
"prefs.minutes": "хв",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Миша й трекпад",
|
||||
"prefs.mouse_heading": "Миша",
|
||||
"prefs.mouse_mapping": "Призначення кнопок миші",
|
||||
"prefs.navigation": "Навігація",
|
||||
"prefs.navigation_note": "Як миша, трекпад і 3D-миша рухають вид.",
|
||||
"prefs.not_on_gpu": "не на цьому GPU",
|
||||
"prefs.on": "Увімкнено",
|
||||
"prefs.opaque": "Непрозорий",
|
||||
"prefs.orbit_snap": "Утримуйте Shift під час обертання, щоб прив'язатися до осі.",
|
||||
"prefs.orientation": "Орієнтація",
|
||||
"prefs.overall_speed": "Загальна швидкість",
|
||||
"prefs.preview_choices": "Перегляд ваших виборів",
|
||||
"prefs.quality_default": "типово",
|
||||
"prefs.quality_fastest": "найшвидше",
|
||||
"prefs.quality_slowest": "найповільніше",
|
||||
"prefs.quality_smoother": "плавніше",
|
||||
"prefs.reports_saved": "Збережено: {n}",
|
||||
"prefs.right_click": "Клацання правою",
|
||||
"prefs.saving": "Автозбереження й відновлення",
|
||||
"prefs.scale_note": "Також Ctrl + / Ctrl − у вікні. Кнопки мають 28 px за 100%.",
|
||||
"prefs.scale_text": "Масштаб і текст",
|
||||
"prefs.select": "Вибрати",
|
||||
"prefs.select_one_item": "Вибрати один елемент",
|
||||
"prefs.selecting": "Вибір",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift лише додає; Ctrl / Cmd перемикає",
|
||||
"prefs.shift_add_note": "Увімкнено: Shift додає, Ctrl / Cmd перемикає. Вимкнено (типово): обидва перемикають.",
|
||||
"prefs.shortcuts": "Комбінації клавіш",
|
||||
"prefs.sketch_selection": "Ескіз і вибір",
|
||||
"prefs.sketch_selection_heading": "Ескіз і вибір",
|
||||
"prefs.sketch_selection_note": "Як ескізи прив'язуються й як клацання вибирають.",
|
||||
"prefs.sketching": "Ескізування",
|
||||
"prefs.snap_drawing_note": "Під час малювання або перетягування точки ескізу.",
|
||||
"prefs.source": "Джерело",
|
||||
"prefs.speed": "Швидкість",
|
||||
"prefs.stand_up_note": "Коли файл STEP або IGES відкрито з віссю Z угору, пропонувати поворот на чверть оберту навколо X.",
|
||||
"prefs.status_done": "Готово",
|
||||
"prefs.status_error": "Помилка",
|
||||
"prefs.status_selected": "Вибрано",
|
||||
"prefs.status_warning": "Попередження",
|
||||
"prefs.supersampling_brief": "Малює вид у більшому розмірі й зменшує його: дуже чітко, але потребує більше ресурсів.",
|
||||
"prefs.tab_accent_note": "Акцентний колір відповідає активній вкладці",
|
||||
"prefs.theme_file": "файл теми",
|
||||
"prefs.timeline_half_hour": "−30 хв",
|
||||
"prefs.timeline_now": "зараз",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
"prefs.toggle_item": "Додати або прибрати елемент",
|
||||
"prefs.toolbar": "Панель інструментів",
|
||||
"prefs.translucent": "Напівпрозорий",
|
||||
"prefs.under_browser": "Під браузером",
|
||||
"prefs.under_cursor": "Під курсором",
|
||||
"prefs.units_precision": "Одиниці й точність",
|
||||
"prefs.updates": "Оновлення",
|
||||
"prefs.view": "Поведінка виду",
|
||||
"prefs.viewport": "Вікно перегляду",
|
||||
"prefs.wheel": "Колесо",
|
||||
"prefs.wheel_trackpad": "Колесо й трекпад",
|
||||
"prefs.workspace": "Робоча область",
|
||||
"prefs.zoom_toward_cursor": "Масштаб до курсора",
|
||||
"properties.title": "Властивості: {name}",
|
||||
"recovery.days_ago": "{count} дн. тому",
|
||||
"recovery.detail": "{file} · елементів: {features} · автозбережено {when}",
|
||||
@@ -1121,7 +1426,106 @@
|
||||
"trace.stopped_by_contact": "Закінчується на відліку {sample}: {a} пройшов би крізь {b}",
|
||||
"trace.stopped_by_loop": "Закінчується на відліку {sample}: контур через {joint} далі не замикається",
|
||||
"ui.1_degree_of_freedom": "1 ступінь вільності",
|
||||
"ui.3d_mouse_axes_reassigned": "Осі перепризначено в налаштуваннях, і це діє й поверх цього калібрування.",
|
||||
"ui.3d_mouse_button": "Кнопка {number}",
|
||||
"ui.3d_mouse_button_nothing": "Нічого",
|
||||
"ui.3d_mouse_button_settings": "Налаштування 3D-миші",
|
||||
"ui.3d_mouse_buttons": "Кнопки",
|
||||
"ui.3d_mouse_buttons_hint": "Натисніть кнопку на 3D-миші, щоб знайти її тут, і виберіть, що вона робить.",
|
||||
"ui.3d_mouse_buttons_no_device": "Під'єднайте 3D-мишу, щоб призначити її кнопки.",
|
||||
"ui.3d_mouse_buttons_reset": "Повернути типові кнопки",
|
||||
"ui.3d_mouse_calibrate": "Калібрувати…",
|
||||
"ui.3d_mouse_calibrate_tip": "Рухайте ковпачок так, як просить вікно, щоб SolidCraft дізнався, куди й наскільки далеко рухається кожна вісь цієї 3D-миші. Зберігається для цієї 3D-миші.",
|
||||
"ui.3d_mouse_calibrate_title": "Калібрування 3D-миші",
|
||||
"ui.3d_mouse_calibrate_wait": "Один раз порухайте 3D-мишу, щоб SolidCraft її побачив.",
|
||||
"ui.3d_mouse_calibrated": "Відкалібровано",
|
||||
"ui.3d_mouse_calibration": "Калібрування 3D-миші",
|
||||
"ui.3d_mouse_calibration_done": "Готово. Збережіть його, щоб воно застосовувалося щоразу, коли підключено цю 3D-мишу.",
|
||||
"ui.3d_mouse_calibration_no_response": "Одна вісь майже не зрушила, тому її не можна відкалібрувати. Почніть спочатку й щоразу рухайте ковпачок далі.",
|
||||
"ui.3d_mouse_circles_help": "Кола показують, що пристрій надсилає після калібрування. Тестова модель показує, що вигляд робить із цим після інверсії, швидкості та призначення осей.",
|
||||
"ui.3d_mouse_dead_zone": "Мертва зона",
|
||||
"ui.3d_mouse_dead_zone_tip": "Наскільки треба зсунути ковпачок, перш ніж зрушить вигляд, у частці від повного ходу. Збільште, якщо вигляд зміщується, коли ковпачок у спокої.",
|
||||
"ui.3d_mouse_denied": "SolidCraft бачить 3D-мишу, але не має права її читати. Правило udev описано в нотатках із налаштування 3D-миші.",
|
||||
"ui.3d_mouse_denied_short": "Знайдено, але не читається",
|
||||
"ui.3d_mouse_driven_by": "Який відкалібрований рух пристрою керує цією віссю. Якщо вибрати рух, який уже використовує інша вісь, вони поміняються місцями.",
|
||||
"ui.3d_mouse_driven_by_label": "керується",
|
||||
"ui.3d_mouse_driver": "Драйвер 3D-миші",
|
||||
"ui.3d_mouse_driver_auto": "Автоматично",
|
||||
"ui.3d_mouse_driver_direct": "Напряму (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Наразі читання йде через spacenavd: сам пристрій відкрити не вдалося.",
|
||||
"ui.3d_mouse_driver_only_direct": "Читання напряму з пристрою; spacenavd не запущено",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Читання через spacenavd, інших варіантів немає",
|
||||
"ui.3d_mouse_forget": "Забути калібрування",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Ліворуч у вікні перегляду",
|
||||
"ui.3d_mouse_ghost_left_window": "Ліворуч у вікні",
|
||||
"ui.3d_mouse_ghost_position": "Розташування",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Праворуч у вікні перегляду",
|
||||
"ui.3d_mouse_hint": "Переміщуйте й обертайте вид разом; в ескізах доступні лише панорамування та масштаб.",
|
||||
"ui.3d_mouse_kind_cap": "Повернення до центру",
|
||||
"ui.3d_mouse_kind_question": "Яка у вас 3D-миша?",
|
||||
"ui.3d_mouse_kind_trackball": "Трекбол",
|
||||
"ui.3d_mouse_looks_like_cap": "Значення тримається, поки ви утримуєте, — як у пристрою типу «Повернення до центру».",
|
||||
"ui.3d_mouse_looks_like_trackball": "Значення повертається, щойно ви зупиняєтеся, — як у трекбола.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Обертання X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Обертання Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Обертання Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Зміщення X",
|
||||
"ui.3d_mouse_motion_translation_y": "Зміщення Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Зміщення Z",
|
||||
"ui.3d_mouse_navigate": "Навігація за допомогою 3D-миші",
|
||||
"ui.3d_mouse_none": "3D-мишу не знайдено",
|
||||
"ui.3d_mouse_not_calibrated": "Не відкалібровано",
|
||||
"ui.3d_mouse_offer": "Калібрування 3D-миші виправляє переплутані або обернені осі та налаштовує швидкість і відчуття.",
|
||||
"ui.3d_mouse_offer_never": "Більше не показувати",
|
||||
"ui.3d_mouse_offer_swap": "Перемкнути на Hidraw (прямий драйвер)",
|
||||
"ui.3d_mouse_reading": "Показники",
|
||||
"ui.3d_mouse_reset_axes": "Скинути призначення осей",
|
||||
"ui.3d_mouse_reset_view": "Скинути вигляд",
|
||||
"ui.3d_mouse_restore": "Відновити налаштування 3D-миші",
|
||||
"ui.3d_mouse_restore_ask": "Повернути налаштування 3D-миші до типових? Калібрування і призначення кнопок залишаться.",
|
||||
"ui.3d_mouse_section": "3D-миша",
|
||||
"ui.3d_mouse_show_in_view": "Показувати 3D-мишу в 3D-вигляді",
|
||||
"ui.3d_mouse_show_in_view_tip": "Зображення вашої 3D-миші в лівому нижньому куті, що рухається разом із нею.",
|
||||
"ui.3d_mouse_skip": "Мій пристрій так не вміє",
|
||||
"ui.3d_mouse_skip_tip": "Пропускає цей рух в обидва боки; калібрування його не враховує.",
|
||||
"ui.3d_mouse_speed": "Швидкість 3D-миші",
|
||||
"ui.3d_mouse_step_lift": "Підніміть ковпачок угору, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_press": "Натисніть ковпачок униз, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_pull": "Потягніть ковпачок до себе, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_push": "Штовхніть ковпачок від себе, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_rest": "Приберіть руку з ковпачка й не торкайтеся його.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Приберіть руку з кулі й не торкайтеся її.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Поверніть кільце прокручування на кілька клацань за годинниковою стрілкою.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Поверніть кільце прокручування на кілька клацань проти годинникової стрілки.",
|
||||
"ui.3d_mouse_step_roll_away": "Прокотіть кулю від себе кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_roll_left": "Прокотіть кулю ліворуч кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_roll_right": "Прокотіть кулю праворуч кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_roll_toward": "Прокотіть кулю до себе кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_slide_left": "Зсуньте ковпачок ліворуч, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_slide_right": "Зсуньте ковпачок праворуч, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Покрутіть кулю за годинниковою стрілкою кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Покрутіть кулю проти годинникової стрілки кілька разів і дайте їй зупинитися.",
|
||||
"ui.3d_mouse_step_tilt_away": "Нахиліть верх ковпачка від себе (ближній край піднімається), утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_tilt_left": "Нахиліть ковпачок ліворуч (лівий край униз), утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_tilt_right": "Нахиліть ковпачок праворуч (правий край униз), утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Нахиліть верх ковпачка до себе (ближній край опускається), утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_touch": "Покладіть руку на ковпачок, не натискаючи на нього.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Поверніть ковпачок за годинниковою стрілкою, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Поверніть ковпачок проти годинникової стрілки, утримайте й відпустіть.",
|
||||
"ui.3d_mouse_switch_cap": "Калібрувати як повернення до центру",
|
||||
"ui.3d_mouse_switch_trackball": "Калібрувати як трекбол",
|
||||
"ui.3d_mouse_view_axis": "Вісь вигляду",
|
||||
"ui.3d_mouse_view_off": "Вимк.",
|
||||
"ui.3d_mouse_view_off_tip": "Профіль драйвера 3D-миші вимикає цю вісь, тож її рух не рухає вигляд. Якщо вибрати тут рух іншої осі, вони поміняються місцями, і цей рух перестане рухати вигляд.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Орбіта ліворуч / праворуч",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Орбіта вгору / вниз",
|
||||
"ui.3d_mouse_view_pan_sideways": "Зсув ліворуч / праворуч",
|
||||
"ui.3d_mouse_view_pan_up_down": "Зсув угору / вниз",
|
||||
"ui.3d_mouse_view_roll": "Крен",
|
||||
"ui.3d_mouse_view_unmapped": "Вісь {n}",
|
||||
"ui.3d_mouse_view_zoom": "Масштаб",
|
||||
"ui.3d_mouse_waiting": "Очікування руху",
|
||||
"ui.3d_mouse_wiggle": "Якщо 3D-миша підключена, трохи порухайте нею.",
|
||||
"ui.3d_sketch": "3D-ескіз",
|
||||
"ui.activate": "Активувати",
|
||||
"ui.active_component": "Активний компонент",
|
||||
@@ -1251,6 +1655,8 @@
|
||||
"ui.agent_chat_waiting": "Очікує на вашу відповідь",
|
||||
"ui.agent_cursor": "Курсор агента",
|
||||
"ui.agent_cursor_speed": "Швидкість курсора агента",
|
||||
"ui.agent_off_thread": "Підтримувати чуйність вікна, поки агенти виконують команди",
|
||||
"ui.agent_off_thread_note": "Повільні команди агента (з панелі ШІ або клієнта MCP) обчислюються паралельно з вікном: ви можете далі обертати й оглядати вид і зупинити команду з індикатора у виді. Вимкнено: кожна виконується в потоці вікна, і вікно чекає на неї.",
|
||||
"ui.ai_add": "Додати",
|
||||
"ui.ai_add_agent": "Додати агента…",
|
||||
"ui.ai_add_agent_title": "Додати агента",
|
||||
@@ -1279,6 +1685,7 @@
|
||||
"ui.ai_http_busy": "кінцева точка зайнята або перевищила ліміт; спробуйте пізніше",
|
||||
"ui.ai_http_key": "кінцева точка відхилила ключ: перевірте його в Налаштування > ШІ",
|
||||
"ui.ai_http_not_found": "немає такої кінцевої точки чи моделі: перевірте базову URL-адресу й назву моделі",
|
||||
"ui.ai_http_too_large": "розмова завелика для цієї кінцевої точки; почніть новий чат",
|
||||
"ui.ai_key": "Ключ API",
|
||||
"ui.ai_key_hint": "необов'язково; зберігається в системному сховищі ключів",
|
||||
"ui.ai_key_needs_https": "Ключ надсилається лише через HTTPS або на цей комп'ютер.",
|
||||
@@ -1306,7 +1713,6 @@
|
||||
"ui.ai_permissions": "Дозволи",
|
||||
"ui.ai_preset": "Набір",
|
||||
"ui.ai_remove": "Видалити",
|
||||
"ui.ai_request_limit": "зупинено після 24 запитів на одне повідомлення; надішліть інше повідомлення, щоб продовжити",
|
||||
"ui.ai_reset": "Скинути",
|
||||
"ui.ai_settings": "Налаштування ШІ…",
|
||||
"ui.ai_show_features": "Показувати функції ШІ",
|
||||
@@ -1351,6 +1757,7 @@
|
||||
"ui.blocked": "Заблоковані",
|
||||
"ui.bodies": "Тіла",
|
||||
"ui.bodies_all_if_none": "Тіла (усі, якщо не вибрано)",
|
||||
"ui.bottom": "Знизу",
|
||||
"ui.box": "Куб",
|
||||
"ui.break": "Розірвати",
|
||||
"ui.browser_bodies_one": "{count} тіло",
|
||||
@@ -1482,6 +1889,8 @@
|
||||
"ui.filter_commands_or_keys": "Фільтрувати команди або клавіші",
|
||||
"ui.findInTimeline": "Знайти в часовому ланцюжку",
|
||||
"ui.findInWindow": "Знайти у вікні",
|
||||
"ui.fine": "Висока",
|
||||
"ui.finest": "Максимальна",
|
||||
"ui.finish_sketch": "Завершити ескіз",
|
||||
"ui.fit": "Вписати",
|
||||
"ui.fix_unfix": "Закріпити/розкріпити",
|
||||
@@ -1570,6 +1979,7 @@
|
||||
"ui.heading_torus": "ТОР",
|
||||
"ui.heading_utilities": "УТИЛІТИ",
|
||||
"ui.heading_utility": "УТИЛІТА",
|
||||
"ui.heading_weldments": "ЗВАРЮВАННЯ",
|
||||
"ui.help": "Довідка",
|
||||
"ui.hide": "Приховати",
|
||||
"ui.hideAxes": "Приховати осі",
|
||||
@@ -1579,10 +1989,17 @@
|
||||
"ui.hideProfile": "Приховати профіль",
|
||||
"ui.history_back": "На один крок назад",
|
||||
"ui.history_below_browser": "Під браузером",
|
||||
"ui.history_edge_hint": "двічі клацніть край",
|
||||
"ui.history_first": "Перейти на початок",
|
||||
"ui.history_forward": "На один крок уперед",
|
||||
"ui.history_hide_names": "Приховати назви",
|
||||
"ui.history_last": "Перейти в кінець",
|
||||
"ui.history_marker_tip": "Маркер історії: перетягніть, щоб відкотити модель назад; нові елементи додаються тут",
|
||||
"ui.history_names": "Назви",
|
||||
"ui.history_play": "Відтворити до кінця",
|
||||
"ui.history_show_names": "Показати назви",
|
||||
"ui.history_title": "Історія",
|
||||
"ui.history_toggle_names": "Показувати або приховувати назви елементів",
|
||||
"ui.hole": "Отвір",
|
||||
"ui.hole_tap_type": "Тип різьби отвору",
|
||||
"ui.hole_type": "Тип отвору",
|
||||
@@ -1706,6 +2123,7 @@
|
||||
"ui.project_face_edges_sketch": "Проєктувати ребра граней у нові ескізи",
|
||||
"ui.properties": "Властивості",
|
||||
"ui.pull_direction": "Напрямок витягування",
|
||||
"ui.quality": "Якість",
|
||||
"ui.quantity": "Кількість",
|
||||
"ui.quantity_2": "Кількість 2",
|
||||
"ui.quit": "Вихід",
|
||||
@@ -1794,65 +2212,13 @@
|
||||
"ui.smooth_wheel_zoom": "Плавне масштабування колесом",
|
||||
"ui.smooth_wheel_zoom_tip": "Вимкніть, щоб застосовувати кожну подію колеса негайно, без плавного завершення",
|
||||
"ui.snap": "Прив'язка",
|
||||
"ui.space_mouse_axes_reassigned": "Осі перепризначено в налаштуваннях, і це діє й поверх цього калібрування.",
|
||||
"ui.space_mouse_calibrate": "Калібрувати…",
|
||||
"ui.space_mouse_calibrate_tip": "Рухайте ковпачок так, як просить вікно, щоб SolidCraft дізнався, куди й наскільки далеко рухається кожна вісь цієї 3D-миші. Зберігається для цієї 3D-миші.",
|
||||
"ui.space_mouse_calibrate_title": "Калібрування 3D-миші",
|
||||
"ui.space_mouse_calibrated": "Відкалібровано",
|
||||
"ui.space_mouse_calibration": "Калібрування 3D-миші",
|
||||
"ui.space_mouse_calibration_done": "Готово. Збережіть його, щоб воно застосовувалося щоразу, коли підключено цю 3D-мишу.",
|
||||
"ui.space_mouse_calibration_no_response": "Одна вісь майже не зрушила, тому її не можна відкалібрувати. Почніть спочатку й щоразу рухайте ковпачок далі.",
|
||||
"ui.space_mouse_dead_zone": "Мертва зона",
|
||||
"ui.space_mouse_dead_zone_tip": "Наскільки треба зсунути ковпачок, перш ніж зрушить вигляд, у частці від повного ходу. Збільште, якщо вигляд зміщується, коли ковпачок у спокої.",
|
||||
"ui.space_mouse_driven_by": "Який відкалібрований рух пристрою керує цією віссю. Якщо вибрати рух, який уже використовує інша вісь, вони поміняються місцями.",
|
||||
"ui.space_mouse_driven_by_label": "керується",
|
||||
"ui.space_mouse_driver": "Драйвер 3D-миші",
|
||||
"ui.space_mouse_driver_auto": "Автоматично",
|
||||
"ui.space_mouse_driver_direct": "Напряму (hidraw)",
|
||||
"ui.space_mouse_driver_only_direct": "Читання напряму з пристрою; spacenavd не запущено",
|
||||
"ui.space_mouse_driver_only_spacenavd": "Читання через spacenavd, інших варіантів немає",
|
||||
"ui.space_mouse_forget": "Забути калібрування",
|
||||
"ui.space_mouse_hint": "Переміщуйте й обертайте вид разом; в ескізах доступні лише панорамування та масштаб.",
|
||||
"ui.space_mouse_motion_rotation_x": "Обертання X",
|
||||
"ui.space_mouse_motion_rotation_y": "Обертання Y",
|
||||
"ui.space_mouse_motion_rotation_z": "Обертання Z",
|
||||
"ui.space_mouse_motion_translation_x": "Зміщення X",
|
||||
"ui.space_mouse_motion_translation_y": "Зміщення Y",
|
||||
"ui.space_mouse_motion_translation_z": "Зміщення Z",
|
||||
"ui.space_mouse_navigate": "Навігація за допомогою SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "3D-мишу не знайдено",
|
||||
"ui.space_mouse_not_calibrated": "Не відкалібровано",
|
||||
"ui.space_mouse_reading": "Показники",
|
||||
"ui.space_mouse_reset_axes": "Скинути призначення осей",
|
||||
"ui.space_mouse_step_lift": "Підніміть ковпачок угору, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_press": "Натисніть ковпачок униз, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_pull": "Потягніть ковпачок до себе, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_push": "Штовхніть ковпачок від себе, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_rest": "Приберіть руку з ковпачка й не торкайтеся його.",
|
||||
"ui.space_mouse_step_slide_left": "Зсуньте ковпачок ліворуч, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_slide_right": "Зсуньте ковпачок праворуч, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_tilt_back": "Нахиліть ковпачок назад (передній край угору), утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_tilt_forward": "Нахиліть ковпачок уперед (передній край униз), утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_tilt_left": "Нахиліть ковпачок ліворуч (лівий край униз), утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_tilt_right": "Нахиліть ковпачок праворуч (правий край униз), утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Поверніть ковпачок за годинниковою стрілкою, утримайте й відпустіть.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Поверніть ковпачок проти годинникової стрілки, утримайте й відпустіть.",
|
||||
"ui.space_mouse_view_axis": "Вісь вигляду",
|
||||
"ui.space_mouse_view_off": "Вимк.",
|
||||
"ui.space_mouse_view_off_tip": "Профіль драйвера 3D-миші вимикає цю вісь, тож її рух не рухає вигляд. Якщо вибрати тут рух іншої осі, вони поміняються місцями, і цей рух перестане рухати вигляд.",
|
||||
"ui.space_mouse_view_orbit_sideways": "Орбіта ліворуч / праворуч",
|
||||
"ui.space_mouse_view_orbit_up_down": "Орбіта вгору / вниз",
|
||||
"ui.space_mouse_view_pan_sideways": "Зсув ліворуч / праворуч",
|
||||
"ui.space_mouse_view_pan_up_down": "Зсув угору / вниз",
|
||||
"ui.space_mouse_view_roll": "Крен",
|
||||
"ui.space_mouse_view_unmapped": "Вісь {n}",
|
||||
"ui.space_mouse_view_zoom": "Масштаб",
|
||||
"ui.spacing": "Крок",
|
||||
"ui.spacing_2": "Крок 2",
|
||||
"ui.sphere": "Куля",
|
||||
"ui.spline": "Сплайн",
|
||||
"ui.split_body": "Розділити тіло",
|
||||
"ui.stand_up_tip": "Після відкриття файлу STEP або IGES з напрямком Z вгору пропонувати поворот на чверть оберту навколо X",
|
||||
"ui.standard": "Стандартна",
|
||||
"ui.start": "Початок",
|
||||
"ui.start_over": "Почати спочатку",
|
||||
"ui.start_page": "Початкова сторінка",
|
||||
|
||||
+118
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "Chọn…",
|
||||
"Circular": "Tròn",
|
||||
"Circumference": "Chu vi",
|
||||
"Clockwise": "Theo chiều kim đồng hồ",
|
||||
"Close draft": "Đóng bản nháp",
|
||||
"Collisions": "Va chạm",
|
||||
"Colour": "Màu sắc",
|
||||
@@ -322,6 +323,7 @@
|
||||
"Search commands": "Tìm kiếm lệnh",
|
||||
"Search this library": "Tìm trong thư viện này",
|
||||
"Section": "Mặt cắt",
|
||||
"Section position": "Vị trí mặt cắt",
|
||||
"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).": "Chọn đối tượng khác: giữ nút chuột trái đứng yên trên mô hình (hoặc nhấp chuột phải › Chọn đối tượng khác…) để liệt kê mọi đối tượng dưới con trỏ; nhấp lại vào cùng vị trí đó để chọn đối tượng kế tiếp. Nhấp đúp chuột giữa để về Góc nhìn chuẩn (Tùy chọn cài đặt › Điều hướng).",
|
||||
"Select bodies (a face picks its body)": "Chọn thân khối (chọn một mặt sẽ chọn cả thân khối)",
|
||||
"Select bodies or faces instead": "Chọn thân khối hoặc bề mặt thay thế",
|
||||
@@ -333,6 +335,7 @@
|
||||
"Show Agent Cursor": "Hiển thị con trỏ của AI Agent",
|
||||
"Show in folder": "Hiển thị trong thư mục",
|
||||
"Show the pointer an AI agent moves (#35)": "Hiển thị con trỏ do AI Agent điều khiển (#35)",
|
||||
"Show tool at cursor": "Hiển thị công cụ cạnh con trỏ",
|
||||
"Signed axes": "Các trục có dấu (+/-)",
|
||||
"Size": "Kích thước",
|
||||
"Sketch constraints": "Ràng buộc phác thảo",
|
||||
@@ -345,9 +348,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft đã đóng trước khi các thiết kế này được lưu. Bạn có thể mở bản tự động lưu gần nhất của chúng.",
|
||||
"Specialise": "Chuyên biệt hóa",
|
||||
"Speed": "Tốc độ",
|
||||
"Square": "Hình vuông",
|
||||
"Stand it up": "Dựng đứng đối tượng",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Dựng đứng đối tượng: xoay 90° quanh trục X, dưới dạng một tính năng Di chuyển có thể hoàn tác.",
|
||||
"Stand this part up?": "Dựng đứng chi tiết này lên?",
|
||||
"Start angle": "Góc bắt đầu",
|
||||
"Start bulge": "Độ phình đầu",
|
||||
"Start continuity": "Độ liên tục đầu",
|
||||
"Step": "Bước",
|
||||
@@ -412,6 +417,7 @@
|
||||
"angle, height": "góc, chiều cao",
|
||||
"appearance.dielectric_ior": "Chỉ số khúc xạ điện môi (IOR)",
|
||||
"appearance.metallic": "Độ kim loại",
|
||||
"appearance.only_available": "Chỉ các kiểu bề ngoài khả dụng",
|
||||
"appearance.pbr_help": "Xem bề mặt này trong chế độ Kết xuất hoặc ảnh PNG sản phẩm. Không có khúc xạ hay truyền sáng.",
|
||||
"appearance.pbr_opaque": "PBR không trong suốt",
|
||||
"appearance.roughness": "Độ nhám bề mặt",
|
||||
@@ -530,6 +536,10 @@
|
||||
"cmd.file_insert_step": "Chèn tệp STEP",
|
||||
"cmd.file_save_mesh": "Lưu dưới dạng lưới đa giác",
|
||||
"cmd.file_slicers": "Phần mềm cắt lớp đã cài đặt",
|
||||
"cmd.frame_member": "Thanh khung",
|
||||
"cmd.frame_profile_insert": "Chèn tiết diện",
|
||||
"cmd.frame_profile_list": "Thư viện tiết diện",
|
||||
"cmd.frame_profile_properties": "Thuộc tính tiết diện",
|
||||
"cmd.inspect_accessibility": "Phân tích khả năng gia công tiếp cận",
|
||||
"cmd.inspect_center_of_mass": "Trọng tâm",
|
||||
"cmd.inspect_clearance": "Khoảng cách an toàn",
|
||||
@@ -569,6 +579,7 @@
|
||||
"cmd.material_create": "Tạo vật liệu vật lý mới",
|
||||
"cmd.material_delete": "Xóa vật liệu vật lý",
|
||||
"cmd.material_edit": "Chỉnh sửa vật liệu vật lý",
|
||||
"cmd.material_inspect": "Kiểm tra vật liệu vật lý",
|
||||
"cmd.material_list": "Danh sách vật liệu",
|
||||
"cmd.material_properties_directional": "Mô đun đàn hồi theo hướng",
|
||||
"cmd.material_properties_validate": "Xác thực dữ liệu thuộc tính vật liệu",
|
||||
@@ -960,6 +971,7 @@
|
||||
"motion.key_here": "Đặt khóa tại đây",
|
||||
"motion.key_here_hint": "Thêm một khóa tại bước này với giá trị hiện tại của khớp nối",
|
||||
"motion.no_collisions": "Không có va chạm trong suốt quá trình khảo sát",
|
||||
"motion.plot_quantity": "Số lượng",
|
||||
"motion.step_back": "Lùi một bước",
|
||||
"motion.step_forward": "Tiến một bước",
|
||||
"motion.to_end": "Đến cuối",
|
||||
@@ -1121,7 +1133,106 @@
|
||||
"trace.stopped_by_contact": "Kết thúc tại mẫu {sample}: {a} sẽ xuyên qua {b}",
|
||||
"trace.stopped_by_loop": "Kết thúc tại mẫu {sample}: vòng lặp qua {joint} không thể khép kín thêm được nữa",
|
||||
"ui.1_degree_of_freedom": "1 bậc tự do",
|
||||
"ui.3d_mouse_axes_reassigned": "Các trục được gán lại trong Tùy chọn, và điều đó vẫn áp dụng cùng với hiệu chuẩn này.",
|
||||
"ui.3d_mouse_button": "Nút {number}",
|
||||
"ui.3d_mouse_button_nothing": "Không có",
|
||||
"ui.3d_mouse_button_settings": "Cài đặt chuột 3D",
|
||||
"ui.3d_mouse_buttons": "Nút",
|
||||
"ui.3d_mouse_buttons_hint": "Nhấn một nút trên chuột 3D để tìm nó ở đây, rồi chọn chức năng.",
|
||||
"ui.3d_mouse_buttons_no_device": "Kết nối chuột 3D để chọn chức năng cho các nút của nó.",
|
||||
"ui.3d_mouse_buttons_reset": "Khôi phục nút mặc định",
|
||||
"ui.3d_mouse_calibrate": "Hiệu chuẩn…",
|
||||
"ui.3d_mouse_calibrate_tip": "Di chuyển núm điều khiển theo yêu cầu của cửa sổ để SolidCraft biết mỗi trục của chuột 3D này đi theo hướng nào và xa bao nhiêu. Được lưu cho chuột 3D này.",
|
||||
"ui.3d_mouse_calibrate_title": "Hiệu chuẩn chuột 3D",
|
||||
"ui.3d_mouse_calibrate_wait": "Hãy di chuyển chuột 3D một lần để SolidCraft nhận ra nó.",
|
||||
"ui.3d_mouse_calibrated": "Đã hiệu chuẩn",
|
||||
"ui.3d_mouse_calibration": "Hiệu chuẩn chuột 3D",
|
||||
"ui.3d_mouse_calibration_done": "Xong. Hãy lưu để dùng mỗi khi chuột 3D này được kết nối.",
|
||||
"ui.3d_mouse_calibration_no_response": "Một trục hầu như không di chuyển nên không thể hiệu chuẩn. Hãy bắt đầu lại và di chuyển núm điều khiển xa hơn mỗi lần.",
|
||||
"ui.3d_mouse_circles_help": "Các vòng tròn cho thấy thiết bị gửi gì sau khi hiệu chuẩn. Mô hình thử cho thấy khung nhìn làm gì với nó, sau đảo chiều, tốc độ và gán trục.",
|
||||
"ui.3d_mouse_dead_zone": "Vùng chết",
|
||||
"ui.3d_mouse_dead_zone_tip": "Núm điều khiển phải di chuyển bao xa thì khung nhìn mới di chuyển, tính theo tỷ lệ hành trình tối đa. Hãy tăng giá trị này nếu khung nhìn bị trôi khi núm đang đứng yên.",
|
||||
"ui.3d_mouse_denied": "SolidCraft thấy một chuột 3D nhưng không được phép đọc nó. Xem ghi chú thiết lập chuột 3D để biết quy tắc udev.",
|
||||
"ui.3d_mouse_denied_short": "Đã thấy, nhưng không đọc được",
|
||||
"ui.3d_mouse_driven_by": "Chuyển động đã hiệu chuẩn nào của thiết bị điều khiển trục này. Chọn chuyển động mà trục khác đang dùng sẽ hoán đổi hai trục.",
|
||||
"ui.3d_mouse_driven_by_label": "điều khiển bởi",
|
||||
"ui.3d_mouse_driver": "Trình điều khiển chuột 3D",
|
||||
"ui.3d_mouse_driver_auto": "Tự động",
|
||||
"ui.3d_mouse_driver_direct": "Trực tiếp (hidraw)",
|
||||
"ui.3d_mouse_driver_fallback": "Tạm thời đọc qua spacenavd: không mở được chính thiết bị.",
|
||||
"ui.3d_mouse_driver_only_direct": "Đang đọc thiết bị trực tiếp; spacenavd không chạy",
|
||||
"ui.3d_mouse_driver_only_spacenavd": "Đang đọc qua spacenavd, cách duy nhất có sẵn",
|
||||
"ui.3d_mouse_forget": "Xóa hiệu chuẩn",
|
||||
"ui.3d_mouse_ghost_left_viewport": "Khung nhìn bên trái",
|
||||
"ui.3d_mouse_ghost_left_window": "Cửa sổ bên trái",
|
||||
"ui.3d_mouse_ghost_position": "Vị trí",
|
||||
"ui.3d_mouse_ghost_right_viewport": "Khung nhìn bên phải",
|
||||
"ui.3d_mouse_hint": "Di chuyển và xoay khung nhìn đồng thời; môi trường phác thảo chỉ sử dụng dời khung nhìn và thu phóng.",
|
||||
"ui.3d_mouse_kind_cap": "Trở về giữa",
|
||||
"ui.3d_mouse_kind_question": "Đây là loại chuột 3D nào?",
|
||||
"ui.3d_mouse_kind_trackball": "Bi xoay",
|
||||
"ui.3d_mouse_looks_like_cap": "Giá trị giữ nguyên khi bạn giữ, giống thiết bị kiểu “Trở về giữa”.",
|
||||
"ui.3d_mouse_looks_like_trackball": "Giá trị trở về ngay khi bạn ngừng di chuyển, giống bi xoay.",
|
||||
"ui.3d_mouse_motion_rotation_x": "Xoay X",
|
||||
"ui.3d_mouse_motion_rotation_y": "Xoay Y",
|
||||
"ui.3d_mouse_motion_rotation_z": "Xoay Z",
|
||||
"ui.3d_mouse_motion_translation_x": "Tịnh tiến X",
|
||||
"ui.3d_mouse_motion_translation_y": "Tịnh tiến Y",
|
||||
"ui.3d_mouse_motion_translation_z": "Tịnh tiến Z",
|
||||
"ui.3d_mouse_navigate": "Điều hướng bằng chuột 3D",
|
||||
"ui.3d_mouse_none": "Không tìm thấy chuột 3D",
|
||||
"ui.3d_mouse_not_calibrated": "Chưa hiệu chuẩn",
|
||||
"ui.3d_mouse_offer": "Hiệu chuẩn chuột 3D sẽ sửa các trục bị hoán đổi hoặc đảo ngược và chỉnh tốc độ cũng như cảm giác.",
|
||||
"ui.3d_mouse_offer_never": "Không hiện lại",
|
||||
"ui.3d_mouse_offer_swap": "Chuyển sang Hidraw (điều khiển trực tiếp)",
|
||||
"ui.3d_mouse_reading": "Giá trị đọc",
|
||||
"ui.3d_mouse_reset_axes": "Đặt lại phép gán trục",
|
||||
"ui.3d_mouse_reset_view": "Đặt lại khung nhìn",
|
||||
"ui.3d_mouse_restore": "Khôi phục mặc định chuột 3D",
|
||||
"ui.3d_mouse_restore_ask": "Đặt cài đặt chuột 3D về mặc định? Hiệu chuẩn và gán nút được giữ nguyên.",
|
||||
"ui.3d_mouse_section": "Chuột 3D",
|
||||
"ui.3d_mouse_show_in_view": "Hiện chuột 3D trong khung nhìn 3D",
|
||||
"ui.3d_mouse_show_in_view_tip": "Hình chuột 3D của bạn ở góc dưới bên trái, chuyển động theo bạn.",
|
||||
"ui.3d_mouse_skip": "Thiết bị của tôi không làm được điều này",
|
||||
"ui.3d_mouse_skip_tip": "Bỏ qua chuyển động này theo cả hai chiều; việc hiệu chuẩn sẽ không dùng nó.",
|
||||
"ui.3d_mouse_speed": "Tốc độ chuột 3D",
|
||||
"ui.3d_mouse_step_lift": "Nâng núm điều khiển lên, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_press": "Nhấn núm điều khiển xuống, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_pull": "Kéo núm điều khiển về phía bạn, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_push": "Đẩy núm điều khiển ra xa bạn, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_rest": "Bỏ tay khỏi núm điều khiển và để yên.",
|
||||
"ui.3d_mouse_step_rest_trackball": "Bỏ tay khỏi viên bi và để yên.",
|
||||
"ui.3d_mouse_step_ring_clockwise": "Xoay vòng cuộn theo chiều kim đồng hồ vài nấc.",
|
||||
"ui.3d_mouse_step_ring_counterclockwise": "Xoay vòng cuộn ngược chiều kim đồng hồ vài nấc.",
|
||||
"ui.3d_mouse_step_roll_away": "Lăn viên bi ra xa bạn vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_roll_left": "Lăn viên bi sang trái vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_roll_right": "Lăn viên bi sang phải vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_roll_toward": "Lăn viên bi về phía bạn vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_slide_left": "Trượt núm điều khiển sang trái, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_slide_right": "Trượt núm điều khiển sang phải, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_spin_clockwise": "Xoay viên bi theo chiều kim đồng hồ vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_spin_counterclockwise": "Xoay viên bi ngược chiều kim đồng hồ vài lần, rồi để nó dừng lại.",
|
||||
"ui.3d_mouse_step_tilt_away": "Nghiêng phần trên núm điều khiển ra xa bạn (cạnh gần bạn nhất nâng lên), giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_tilt_left": "Nghiêng núm điều khiển sang trái (cạnh trái hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_tilt_right": "Nghiêng núm điều khiển sang phải (cạnh phải hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_tilt_toward": "Nghiêng phần trên núm điều khiển về phía bạn (cạnh gần bạn nhất hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_touch": "Đặt tay lên núm điều khiển nhưng không đẩy.",
|
||||
"ui.3d_mouse_step_twist_clockwise": "Xoay núm điều khiển theo chiều kim đồng hồ, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Xoay núm điều khiển ngược chiều kim đồng hồ, giữ nguyên, rồi thả ra.",
|
||||
"ui.3d_mouse_switch_cap": "Hiệu chuẩn theo kiểu trở về giữa",
|
||||
"ui.3d_mouse_switch_trackball": "Hiệu chuẩn như bi xoay",
|
||||
"ui.3d_mouse_view_axis": "Trục khung nhìn",
|
||||
"ui.3d_mouse_view_off": "Tắt",
|
||||
"ui.3d_mouse_view_off_tip": "Hồ sơ của trình điều khiển chuột 3D tắt trục này, nên chuyển động của nó không làm khung nhìn di chuyển. Chọn chuyển động của trục khác ở đây sẽ hoán đổi hai trục, và chuyển động đó không còn di chuyển khung nhìn.",
|
||||
"ui.3d_mouse_view_orbit_sideways": "Xoay quanh trái / phải",
|
||||
"ui.3d_mouse_view_orbit_up_down": "Xoay quanh lên / xuống",
|
||||
"ui.3d_mouse_view_pan_sideways": "Kéo trái / phải",
|
||||
"ui.3d_mouse_view_pan_up_down": "Kéo lên / xuống",
|
||||
"ui.3d_mouse_view_roll": "Lăn",
|
||||
"ui.3d_mouse_view_unmapped": "Trục {n}",
|
||||
"ui.3d_mouse_view_zoom": "Thu phóng",
|
||||
"ui.3d_mouse_waiting": "Đang chờ chuyển động",
|
||||
"ui.3d_mouse_wiggle": "Nếu bạn đã cắm một chiếc, hãy lắc nhẹ nó.",
|
||||
"ui.3d_sketch": "3D Phác thảo",
|
||||
"ui.activate": "Kích hoạt",
|
||||
"ui.active_component": "Cụm chi tiết đang kích hoạt",
|
||||
@@ -1279,6 +1390,7 @@
|
||||
"ui.ai_http_busy": "điểm cuối đang bận hoặc vượt quá giới hạn; vui lòng thử lại sau",
|
||||
"ui.ai_http_key": "điểm cuối từ chối khóa API: hãy kiểm tra trong Tùy chọn cài đặt > AI",
|
||||
"ui.ai_http_not_found": "không tìm thấy điểm cuối hoặc mô hình: hãy kiểm tra URL cơ sở và tên mô hình",
|
||||
"ui.ai_http_too_large": "cuộc trò chuyện quá lớn đối với điểm cuối này; hãy bắt đầu cuộc trò chuyện mới",
|
||||
"ui.ai_key": "Khóa API",
|
||||
"ui.ai_key_hint": "tùy chọn; lưu trữ bảo mật trong chuỗi khóa hệ thống",
|
||||
"ui.ai_key_needs_https": "Khóa chỉ được gửi qua HTTPS, hoặc trực tiếp trên máy tính này.",
|
||||
@@ -1306,7 +1418,6 @@
|
||||
"ui.ai_permissions": "Quyền hạn",
|
||||
"ui.ai_preset": "Mẫu cấu hình sẵn",
|
||||
"ui.ai_remove": "Xóa",
|
||||
"ui.ai_request_limit": "đã dừng sau 24 yêu cầu cho một tin nhắn; gửi tin nhắn khác để tiếp tục",
|
||||
"ui.ai_reset": "Đặt lại",
|
||||
"ui.ai_settings": "AI cài đặt…",
|
||||
"ui.ai_show_features": "Hiển thị các tính năng AI",
|
||||
@@ -1351,6 +1462,7 @@
|
||||
"ui.blocked": "Bị chặn",
|
||||
"ui.bodies": "Các thân khối",
|
||||
"ui.bodies_all_if_none": "Thân khối (tất cả nếu không chọn)",
|
||||
"ui.bottom": "Dưới",
|
||||
"ui.box": "Khối hộp",
|
||||
"ui.break": "Ngắt",
|
||||
"ui.browser_bodies_one": "{count} thân khối",
|
||||
@@ -1482,6 +1594,8 @@
|
||||
"ui.filter_commands_or_keys": "Lọc lệnh hoặc phím tắt",
|
||||
"ui.findInTimeline": "Tìm trong lịch sử thiết kế",
|
||||
"ui.findInWindow": "Tìm trong cửa sổ",
|
||||
"ui.fine": "Mịn",
|
||||
"ui.finest": "Mịn nhất",
|
||||
"ui.finish_sketch": "Hoàn tất phác thảo",
|
||||
"ui.fit": "Vừa vặn (Fit)",
|
||||
"ui.fix_unfix": "Cố định/Uncố định",
|
||||
@@ -1570,6 +1684,7 @@
|
||||
"ui.heading_torus": "HÌNH XUYẾN",
|
||||
"ui.heading_utilities": "TIỆN ÍCH",
|
||||
"ui.heading_utility": "TIỆN ÍCH",
|
||||
"ui.heading_weldments": "KẾT CẤU HÀN",
|
||||
"ui.help": "Trợ giúp",
|
||||
"ui.hide": "Ẩn",
|
||||
"ui.hideAxes": "Ẩn Các trục",
|
||||
@@ -1706,6 +1821,7 @@
|
||||
"ui.project_face_edges_sketch": "Chiếu các cạnh bề mặt vào phác thảo mới",
|
||||
"ui.properties": "Thuộc tính cơ lý",
|
||||
"ui.pull_direction": "Hướng kéo",
|
||||
"ui.quality": "Chất lượng",
|
||||
"ui.quantity": "Số lượng",
|
||||
"ui.quantity_2": "Số lượng 2",
|
||||
"ui.quit": "Thoát ứng dụng",
|
||||
@@ -1794,40 +1910,13 @@
|
||||
"ui.smooth_wheel_zoom": "Thu phóng mượt bằng con lăn",
|
||||
"ui.smooth_wheel_zoom_tip": "Tắt tùy chọn này để áp dụng từng sự kiện con lăn ngay lập tức mà không có hiệu ứng làm mượt chuyển động",
|
||||
"ui.snap": "Bắt điểm",
|
||||
"ui.space_mouse_calibrate": "Hiệu chuẩn…",
|
||||
"ui.space_mouse_calibrate_tip": "Di chuyển núm điều khiển theo yêu cầu của cửa sổ để SolidCraft biết mỗi trục của chuột 3D này đi theo hướng nào và xa bao nhiêu. Được lưu cho chuột 3D này.",
|
||||
"ui.space_mouse_calibrate_title": "Hiệu chuẩn chuột 3D",
|
||||
"ui.space_mouse_calibrated": "Đã hiệu chuẩn",
|
||||
"ui.space_mouse_calibration": "Hiệu chuẩn chuột 3D",
|
||||
"ui.space_mouse_calibration_done": "Xong. Hãy lưu để dùng mỗi khi chuột 3D này được kết nối.",
|
||||
"ui.space_mouse_calibration_no_response": "Một trục hầu như không di chuyển nên không thể hiệu chuẩn. Hãy bắt đầu lại và di chuyển núm điều khiển xa hơn mỗi lần.",
|
||||
"ui.space_mouse_dead_zone": "Vùng chết",
|
||||
"ui.space_mouse_dead_zone_tip": "Núm điều khiển phải di chuyển bao xa thì góc nhìn mới di chuyển, tính theo tỷ lệ hành trình tối đa. Hãy tăng giá trị này nếu góc nhìn bị trôi khi núm đang đứng yên.",
|
||||
"ui.space_mouse_forget": "Xóa hiệu chuẩn",
|
||||
"ui.space_mouse_hint": "Di chuyển và xoay khung nhìn đồng thời; môi trường phác thảo chỉ sử dụng dời góc nhìn và thu phóng.",
|
||||
"ui.space_mouse_navigate": "Điều hướng bằng SpaceNavigator / SpaceMouse",
|
||||
"ui.space_mouse_none": "Không tìm thấy chuột 3D",
|
||||
"ui.space_mouse_not_calibrated": "Chưa hiệu chuẩn",
|
||||
"ui.space_mouse_reading": "Giá trị đọc",
|
||||
"ui.space_mouse_step_lift": "Nâng núm điều khiển lên, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_press": "Nhấn núm điều khiển xuống, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_pull": "Kéo núm điều khiển về phía bạn, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_push": "Đẩy núm điều khiển ra xa bạn, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_rest": "Bỏ tay khỏi núm điều khiển và để yên.",
|
||||
"ui.space_mouse_step_slide_left": "Trượt núm điều khiển sang trái, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_slide_right": "Trượt núm điều khiển sang phải, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_tilt_back": "Nghiêng núm điều khiển về sau (cạnh trước nâng lên), giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_tilt_forward": "Nghiêng núm điều khiển về trước (cạnh trước hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_tilt_left": "Nghiêng núm điều khiển sang trái (cạnh trái hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_tilt_right": "Nghiêng núm điều khiển sang phải (cạnh phải hạ xuống), giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_twist_clockwise": "Xoay núm điều khiển theo chiều kim đồng hồ, giữ nguyên, rồi thả ra.",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "Xoay núm điều khiển ngược chiều kim đồng hồ, giữ nguyên, rồi thả ra.",
|
||||
"ui.spacing": "Khoảng cách",
|
||||
"ui.spacing_2": "Khoảng cách 2",
|
||||
"ui.sphere": "Hình cầu",
|
||||
"ui.spline": "Đường cong Spline",
|
||||
"ui.split_body": "Chia tách khối",
|
||||
"ui.stand_up_tip": "Sau khi mở tệp STEP hoặc IGES với trục Z hướng lên, đề xuất xoay 90° quanh trục X",
|
||||
"ui.standard": "Tiêu chuẩn",
|
||||
"ui.start": "Bắt đầu",
|
||||
"ui.start_over": "Bắt đầu lại",
|
||||
"ui.start_page": "Trang bắt đầu",
|
||||
|
||||
+61
-29
@@ -72,6 +72,7 @@
|
||||
"Choose…": "選擇…",
|
||||
"Circular": "環形",
|
||||
"Circumference": "周長",
|
||||
"Clockwise": "順時針",
|
||||
"Close draft": "關閉草稿",
|
||||
"Collisions": "碰撞",
|
||||
"Colour": "顏色",
|
||||
@@ -321,6 +322,7 @@
|
||||
"Search commands": "搜尋命令",
|
||||
"Search this library": "搜尋此零件庫",
|
||||
"Section": "剖面",
|
||||
"Section position": "截面位置",
|
||||
"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 bodies (a face picks its body)": "選擇實體(點擊面即選擇其所屬實體)",
|
||||
"Select bodies or faces instead": "改為選擇實體或面",
|
||||
@@ -332,6 +334,7 @@
|
||||
"Show Agent Cursor": "顯示代理游標",
|
||||
"Show in folder": "在資料夾中顯示",
|
||||
"Show the pointer an AI agent moves (#35)": "顯示 AI 代理移動的指標(#35)",
|
||||
"Show tool at cursor": "在游標旁顯示工具",
|
||||
"Signed axes": "帶符號軸",
|
||||
"Size": "大小",
|
||||
"Sketch constraints": "草圖拘束條件",
|
||||
@@ -344,9 +347,11 @@
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft 在這些設計儲存前已關閉。可以開啟它們的上次自動儲存。",
|
||||
"Specialise": "專用操作",
|
||||
"Speed": "速度",
|
||||
"Square": "正方形",
|
||||
"Stand it up": "將其立起",
|
||||
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "將其立起:繞 X 軸旋轉四分之一週,作為一個可復原的移動特徵。",
|
||||
"Stand this part up?": "立起此零件?",
|
||||
"Start angle": "起始角度",
|
||||
"Start bulge": "起始鼓度",
|
||||
"Start continuity": "起始連續性",
|
||||
"Step": "步驟",
|
||||
@@ -411,6 +416,7 @@
|
||||
"angle, height": "角度、高度",
|
||||
"appearance.dielectric_ior": "介電折射率",
|
||||
"appearance.metallic": "金屬度",
|
||||
"appearance.only_available": "僅顯示可用外觀",
|
||||
"appearance.pbr_help": "在彩現工作區或產品 PNG 中檢視此表面。不支援折射或透射。",
|
||||
"appearance.pbr_opaque": "不透明 PBR",
|
||||
"appearance.roughness": "粗糙度",
|
||||
@@ -529,6 +535,10 @@
|
||||
"cmd.file_insert_step": "插入 STEP",
|
||||
"cmd.file_save_mesh": "另存為網格",
|
||||
"cmd.file_slicers": "已安裝的切片軟體",
|
||||
"cmd.frame_member": "框架構件",
|
||||
"cmd.frame_profile_insert": "插入型材",
|
||||
"cmd.frame_profile_list": "型材庫",
|
||||
"cmd.frame_profile_properties": "型材屬性",
|
||||
"cmd.inspect_accessibility": "可達性分析",
|
||||
"cmd.inspect_center_of_mass": "質心",
|
||||
"cmd.inspect_clearance": "間隙",
|
||||
@@ -568,6 +578,7 @@
|
||||
"cmd.material_create": "新建物理材質",
|
||||
"cmd.material_delete": "刪除物理材質",
|
||||
"cmd.material_edit": "編輯物理材質",
|
||||
"cmd.material_inspect": "檢查物理材質",
|
||||
"cmd.material_list": "列出材質",
|
||||
"cmd.material_properties_directional": "方向彈性模量",
|
||||
"cmd.material_properties_validate": "驗證物理材質資料",
|
||||
@@ -959,6 +970,7 @@
|
||||
"motion.key_here": "在此建立關鍵影格",
|
||||
"motion.key_here_hint": "在此步驟新增關鍵影格,使用接頭目前的值",
|
||||
"motion.no_collisions": "整個研究中沒有碰撞",
|
||||
"motion.plot_quantity": "數量",
|
||||
"motion.step_back": "上一步",
|
||||
"motion.step_forward": "下一步",
|
||||
"motion.to_end": "到結尾",
|
||||
@@ -1120,7 +1132,50 @@
|
||||
"trace.stopped_by_contact": "在取樣 {sample} 處結束:{a} 會穿過 {b}",
|
||||
"trace.stopped_by_loop": "在取樣 {sample} 處結束:經過 {joint} 的環路無法再閉合",
|
||||
"ui.1_degree_of_freedom": "1 個自由度",
|
||||
"ui.3d_mouse_calibrate": "校準…",
|
||||
"ui.3d_mouse_calibrate_tip": "按視窗提示移動搖桿帽,SolidCraft 會記住這個 3D 滑鼠每個軸的方向和行程。校準結果按此 3D 滑鼠儲存。",
|
||||
"ui.3d_mouse_calibrate_title": "校準 3D 滑鼠",
|
||||
"ui.3d_mouse_calibrate_wait": "請移動一次 3D 滑鼠,讓 SolidCraft 能夠辨識它。",
|
||||
"ui.3d_mouse_calibrated": "已校準",
|
||||
"ui.3d_mouse_calibration": "3D 滑鼠校準",
|
||||
"ui.3d_mouse_calibration_done": "完成。儲存後,每次連線此 3D 滑鼠都會使用它。",
|
||||
"ui.3d_mouse_calibration_no_response": "有一個軸幾乎沒有移動,因此無法校準。請重新開始,並且每次把搖桿帽移得更遠。",
|
||||
"ui.3d_mouse_circles_help": "圓圈顯示裝置在校準後送出的內容。測試模型顯示檢視在反向、速度與軸指定之後如何處理它。",
|
||||
"ui.3d_mouse_dead_zone": "死區",
|
||||
"ui.3d_mouse_dead_zone_tip": "檢視開始移動前搖桿帽需要移動的距離,以其全行程的比例表示。如果搖桿帽靜止時檢視仍在漂移,請調大。",
|
||||
"ui.3d_mouse_denied": "SolidCraft 看得到 3D 滑鼠,但沒有權限讀取。udev 規則請見 3D 滑鼠的設定說明。",
|
||||
"ui.3d_mouse_denied_short": "已找到,但無法讀取",
|
||||
"ui.3d_mouse_driver_fallback": "目前改透過 spacenavd 讀取:無法開啟裝置本身。",
|
||||
"ui.3d_mouse_forget": "清除校準",
|
||||
"ui.3d_mouse_ghost_left_viewport": "視埠左側",
|
||||
"ui.3d_mouse_ghost_left_window": "視窗左側",
|
||||
"ui.3d_mouse_ghost_position": "位置",
|
||||
"ui.3d_mouse_ghost_right_viewport": "視埠右側",
|
||||
"ui.3d_mouse_hint": "同時移動和旋轉檢視;草圖中僅支援平移和縮放。",
|
||||
"ui.3d_mouse_navigate": "使用 3D 滑鼠導覽",
|
||||
"ui.3d_mouse_none": "未找到 3D 滑鼠",
|
||||
"ui.3d_mouse_not_calibrated": "未校準",
|
||||
"ui.3d_mouse_offer": "校準 3D 滑鼠可以修正互換或反向的軸,並調整它的速度和手感。",
|
||||
"ui.3d_mouse_offer_never": "不再顯示",
|
||||
"ui.3d_mouse_offer_swap": "切換到 Hidraw(直接驅動)",
|
||||
"ui.3d_mouse_reading": "讀值",
|
||||
"ui.3d_mouse_reset_view": "重設檢視",
|
||||
"ui.3d_mouse_restore": "還原 3D 滑鼠預設值",
|
||||
"ui.3d_mouse_restore_ask": "要將 3D 滑鼠設定還原為預設值嗎?校準和按鈕對應會保留。",
|
||||
"ui.3d_mouse_speed": "3D 滑鼠速度",
|
||||
"ui.3d_mouse_step_lift": "向上提起搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_press": "向下按壓搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_pull": "把搖桿帽拉向自己,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_push": "把搖桿帽推離自己,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_rest": "請把手從搖桿帽上移開,讓它保持靜止。",
|
||||
"ui.3d_mouse_step_slide_left": "向左推動搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_slide_right": "向右推動搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_tilt_left": "向左傾斜搖桿帽(左緣向下),保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_tilt_right": "向右傾斜搖桿帽(右緣向下),保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_twist_clockwise": "順時針扭轉搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "逆時針扭轉搖桿帽,保持住,然後鬆開。",
|
||||
"ui.3d_mouse_waiting": "等待移動",
|
||||
"ui.3d_mouse_wiggle": "如果已插上,請輕輕晃動一下。",
|
||||
"ui.3d_sketch": "3D 草圖",
|
||||
"ui.activate": "啟用",
|
||||
"ui.active_component": "作用中元件",
|
||||
@@ -1278,6 +1333,7 @@
|
||||
"ui.ai_http_busy": "端點繁忙或已達上限,請稍後再試",
|
||||
"ui.ai_http_key": "端點拒絕了金鑰:請在「偏好設定 > AI」中檢查",
|
||||
"ui.ai_http_not_found": "找不到該端點或模型:請檢查基礎 URL 和模型名稱",
|
||||
"ui.ai_http_too_large": "對話對此端點來說過大;請開始新的聊天",
|
||||
"ui.ai_key": "API 金鑰",
|
||||
"ui.ai_key_hint": "可選;儲存在系統鑰匙圈中",
|
||||
"ui.ai_key_needs_https": "金鑰只會透過 HTTPS 傳送,或傳送到本機。",
|
||||
@@ -1305,7 +1361,6 @@
|
||||
"ui.ai_permissions": "權限",
|
||||
"ui.ai_preset": "預設",
|
||||
"ui.ai_remove": "移除",
|
||||
"ui.ai_request_limit": "每條訊息 24 次請求後已停止;再傳送一條訊息即可繼續",
|
||||
"ui.ai_reset": "重設",
|
||||
"ui.ai_settings": "AI 設定…",
|
||||
"ui.ai_show_features": "顯示 AI 功能",
|
||||
@@ -1481,6 +1536,8 @@
|
||||
"ui.filter_commands_or_keys": "篩選命令或按鍵",
|
||||
"ui.findInTimeline": "在時間軸中查詢",
|
||||
"ui.findInWindow": "在視窗中查詢",
|
||||
"ui.fine": "精細",
|
||||
"ui.finest": "最精細",
|
||||
"ui.finish_sketch": "完成草圖",
|
||||
"ui.fit": "適合",
|
||||
"ui.fix_unfix": "固定/取消固定",
|
||||
@@ -1569,6 +1626,7 @@
|
||||
"ui.heading_torus": "圓環體",
|
||||
"ui.heading_utilities": "工具",
|
||||
"ui.heading_utility": "實用工具",
|
||||
"ui.heading_weldments": "焊件",
|
||||
"ui.help": "說明",
|
||||
"ui.hide": "隱藏",
|
||||
"ui.hideAxes": "隱藏軸",
|
||||
@@ -1705,6 +1763,7 @@
|
||||
"ui.project_face_edges_sketch": "將面的邊投影到新草圖",
|
||||
"ui.properties": "屬性",
|
||||
"ui.pull_direction": "拔模方向",
|
||||
"ui.quality": "品質",
|
||||
"ui.quantity": "數量",
|
||||
"ui.quantity_2": "數量 2",
|
||||
"ui.quit": "結束",
|
||||
@@ -1793,40 +1852,13 @@
|
||||
"ui.smooth_wheel_zoom": "平滑滾輪縮放",
|
||||
"ui.smooth_wheel_zoom_tip": "關閉後每個滾輪事件立即生效,不做平滑延遲",
|
||||
"ui.snap": "鎖點",
|
||||
"ui.space_mouse_calibrate": "校準…",
|
||||
"ui.space_mouse_calibrate_tip": "按視窗提示移動搖桿帽,SolidCraft 會記住這個 3D 滑鼠每個軸的方向和行程。校準結果按此 3D 滑鼠儲存。",
|
||||
"ui.space_mouse_calibrate_title": "校準 3D 滑鼠",
|
||||
"ui.space_mouse_calibrated": "已校準",
|
||||
"ui.space_mouse_calibration": "3D 滑鼠校準",
|
||||
"ui.space_mouse_calibration_done": "完成。儲存後,每次連線此 3D 滑鼠都會使用它。",
|
||||
"ui.space_mouse_calibration_no_response": "有一個軸幾乎沒有移動,因此無法校準。請重新開始,並且每次把搖桿帽移得更遠。",
|
||||
"ui.space_mouse_dead_zone": "死區",
|
||||
"ui.space_mouse_dead_zone_tip": "檢視開始移動前搖桿帽需要移動的距離,以其全行程的比例表示。如果搖桿帽靜止時檢視仍在漂移,請調大。",
|
||||
"ui.space_mouse_forget": "清除校準",
|
||||
"ui.space_mouse_hint": "同時移動和旋轉檢視;草圖中僅支援平移和縮放。",
|
||||
"ui.space_mouse_navigate": "使用 SpaceNavigator / SpaceMouse 導覽",
|
||||
"ui.space_mouse_none": "未找到 3D 滑鼠",
|
||||
"ui.space_mouse_not_calibrated": "未校準",
|
||||
"ui.space_mouse_reading": "讀值",
|
||||
"ui.space_mouse_step_lift": "向上提起搖桿帽,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_press": "向下按壓搖桿帽,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_pull": "把搖桿帽拉向自己,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_push": "把搖桿帽推離自己,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_rest": "請把手從搖桿帽上移開,讓它保持靜止。",
|
||||
"ui.space_mouse_step_slide_left": "向左推動搖桿帽,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_slide_right": "向右推動搖桿帽,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_tilt_back": "向後傾斜搖桿帽(前緣向上),保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_tilt_forward": "向前傾斜搖桿帽(前緣向下),保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_tilt_left": "向左傾斜搖桿帽(左緣向下),保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_tilt_right": "向右傾斜搖桿帽(右緣向下),保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_twist_clockwise": "順時針扭轉搖桿帽,保持住,然後鬆開。",
|
||||
"ui.space_mouse_step_twist_counterclockwise": "逆時針扭轉搖桿帽,保持住,然後鬆開。",
|
||||
"ui.spacing": "間距",
|
||||
"ui.spacing_2": "間距 2",
|
||||
"ui.sphere": "球體",
|
||||
"ui.spline": "雲形線",
|
||||
"ui.split_body": "分割實體",
|
||||
"ui.stand_up_tip": "當 Z 朝上的 STEP 或 IGES 檔案開啟後,提示繞 X 軸旋轉四分之一週",
|
||||
"ui.standard": "標準",
|
||||
"ui.start": "開始",
|
||||
"ui.start_over": "重新開始",
|
||||
"ui.start_page": "起始頁面",
|
||||
|
||||
+1047
-58
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Packs the boat for the 3D mouse test model in Preferences into assets/models/test_boat.bin, from a
|
||||
binary STL of the low-poly boat (about 850 triangles). Needs numpy.
|
||||
|
||||
python3 assets/models/make_test_boat.py SOURCE.stl assets/models/test_boat.bin
|
||||
|
||||
No reduction happens here: the vertices are welded, and each welded vertex is split by the angle
|
||||
between the faces that meet there (more than 40 degrees apart is a crease), so smooth surfaces share
|
||||
a vertex normal and edges stay sharp. The model is centred and scaled into a unit box, then
|
||||
quantised. File layout, little-endian: u32 vertex count, u32 triangle count, f32 centre (x, y, z),
|
||||
f32 half size of the longest side; then per vertex its position as i16 x 3 (32767 is the half size)
|
||||
and its normal as i8 x 3 (127 is a unit); then per triangle three u16 vertex indices. The model
|
||||
sits with z up.
|
||||
"""
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
CREASE_DEGREES = 40.0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
source, target = sys.argv[1], sys.argv[2]
|
||||
data = open(source, "rb").read()
|
||||
count = struct.unpack_from("<I", data, 80)[0]
|
||||
record = np.dtype([("n", "<f4", 3), ("v", "<f4", (3, 3)), ("attr", "<u2")])
|
||||
tris = np.frombuffer(data, dtype=record, count=count, offset=84)["v"].astype(np.float64)
|
||||
points, inverse = np.unique(np.round(tris.reshape(-1, 3), 3), axis=0, return_inverse=True)
|
||||
faces = inverse.reshape(-1, 3)
|
||||
keep = (faces[:, 0] != faces[:, 1]) & (faces[:, 1] != faces[:, 2]) & (faces[:, 0] != faces[:, 2])
|
||||
faces = faces[keep]
|
||||
a, b, c = points[faces[:, 0]], points[faces[:, 1]], points[faces[:, 2]]
|
||||
cross = np.cross(b - a, c - a)
|
||||
area = np.linalg.norm(cross, axis=1)
|
||||
ok = area > 1e-9
|
||||
faces, cross, area = faces[ok], cross[ok], area[ok]
|
||||
face_normal = cross / area[:, None]
|
||||
# Split each welded vertex into one vertex per group of faces that agree (within the crease angle).
|
||||
cos_limit = np.cos(np.radians(CREASE_DEGREES))
|
||||
corners = {}
|
||||
for f, tri in enumerate(faces):
|
||||
for k in range(3):
|
||||
corners.setdefault(int(tri[k]), []).append((f, k))
|
||||
out_points, out_normals, index = [], [], np.zeros(faces.shape, dtype=np.int64)
|
||||
for v, incident in corners.items():
|
||||
groups = [] # each: [summed normal, [(face, corner)]]
|
||||
for f, k in incident:
|
||||
n = face_normal[f]
|
||||
for g in groups:
|
||||
mean = g[0] / np.linalg.norm(g[0])
|
||||
if mean @ n > cos_limit:
|
||||
g[0] = g[0] + n * area[f]
|
||||
g[1].append((f, k))
|
||||
break
|
||||
else:
|
||||
groups.append([n * area[f], [(f, k)]])
|
||||
for total, members in groups:
|
||||
length = np.linalg.norm(total)
|
||||
out_normals.append(total / length if length > 0 else np.array([0.0, 0.0, 1.0]))
|
||||
out_points.append(points[v])
|
||||
for f, k in members:
|
||||
index[f, k] = len(out_points) - 1
|
||||
out_points, out_normals = np.array(out_points), np.array(out_normals)
|
||||
low, high = out_points.min(axis=0), out_points.max(axis=0)
|
||||
centre, half = (low + high) / 2.0, float((high - low).max() / 2.0)
|
||||
positions = np.clip(np.round((out_points - centre) / half * 32767.0), -32767, 32767).astype("<i2")
|
||||
normals = np.clip(np.round(out_normals * 127.0), -127, 127).astype("i1")
|
||||
assert len(out_points) <= 65535
|
||||
with open(target, "wb") as out:
|
||||
out.write(struct.pack("<II3ff", len(out_points), len(index), *centre.tolist(), half))
|
||||
for p, n in zip(positions, normals):
|
||||
out.write(p.tobytes() + n.tobytes())
|
||||
out.write(index.astype("<u2").tobytes())
|
||||
print(f"{len(out_points)} vertices, {len(index)} triangles, centre {centre}, half size {half}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"window": [540, 360],
|
||||
"title_bar": 32,
|
||||
"icon_size": 148,
|
||||
"text_size": 12,
|
||||
"app": [132, 176],
|
||||
"applications": [408, 176]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Draw the SolidCraft disk image window background (background.tiff).
|
||||
|
||||
A blueprint sheet: deep blue with a drafting grid, a double border with ticks, a circle round each
|
||||
icon, dash-dot centre lines, a leader arrow from the app to Applications and a small title block.
|
||||
Every line stops short of text. The window geometry comes from layout.json, shared with
|
||||
settings.py, so the art and the icons agree.
|
||||
|
||||
Authoring tool only (assets/AGENTS.md): run it after editing, commit the TIFF. Packaging never
|
||||
runs it. Needs Python 3.11+, Pillow and macOS `tiffutil`:
|
||||
|
||||
python3 -m venv target/dmg-art && target/dmg-art/bin/pip install pillow
|
||||
target/dmg-art/bin/python assets/platforms/macos/dmg/make_background.py [--preview DIR]
|
||||
|
||||
All coordinates are points; the TIFF holds a 1x and a 144 dpi 2x representation.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
FONTS = HERE.parents[2] / "fonts"
|
||||
LAYOUT = json.loads((HERE / "layout.json").read_text())
|
||||
W, H = LAYOUT["window"]
|
||||
ICON = LAYOUT["icon_size"]
|
||||
AX, CY = LAYOUT["app"]
|
||||
BX, _ = LAYOUT["applications"]
|
||||
SS = 6 # drawing scale: 3x the 2x output, reduced afterwards
|
||||
|
||||
PAPER_TOP, PAPER_BOTTOM = (28, 84, 158), (18, 58, 118)
|
||||
LINE = (226, 238, 252)
|
||||
PALE = (190, 215, 245)
|
||||
# Finder draws the icon names itself, black in Light and white in Dark appearance, and the art
|
||||
# cannot change that: each name sits on a mid-tone plate that both colours read on.
|
||||
PLATE = (92, 132, 196)
|
||||
PLATE_W, PLATE_H, PLATE_GAP = 116, 22, 8
|
||||
|
||||
|
||||
def px(v):
|
||||
return int(round(v * SS))
|
||||
|
||||
|
||||
def sans(size, weight):
|
||||
# IBMPlexSans-Variable axes: wght, wdth
|
||||
f = ImageFont.truetype(str(FONTS / "ibm-plex-sans/IBMPlexSans-Variable.ttf"), px(size))
|
||||
f.set_variation_by_axes([weight, 100])
|
||||
return f
|
||||
|
||||
|
||||
def mono(size):
|
||||
return ImageFont.truetype(str(FONTS / "ibm-plex-mono/IBMPlexMono-Regular.ttf"), px(size))
|
||||
|
||||
|
||||
def text(d, xy, s, f, fill, anchor="la", tracking=0.0):
|
||||
"""Draw text with letter spacing (points); anchor is the usual Pillow one for the whole run."""
|
||||
x, y = xy
|
||||
if tracking == 0:
|
||||
d.text((px(x), px(y)), s, font=f, fill=fill, anchor=anchor)
|
||||
return
|
||||
widths = [f.getlength(c) / SS for c in s]
|
||||
total = sum(widths) + tracking * (len(s) - 1)
|
||||
if anchor[0] == "m":
|
||||
x -= total / 2
|
||||
elif anchor[0] == "r":
|
||||
x -= total
|
||||
for c, w in zip(s, widths):
|
||||
d.text((px(x), px(y)), c, font=f, fill=fill, anchor="l" + anchor[1])
|
||||
x += w + tracking
|
||||
|
||||
|
||||
def dashdot(d, p0, p1, fill, width=1.0, pattern=(14, 3, 2, 3)):
|
||||
"""A centre line: long dash, gap, short dash, gap. Horizontal or vertical."""
|
||||
(x0, y0), (x1, y1) = p0, p1
|
||||
horiz = y0 == y1
|
||||
a, b = (x0, x1) if horiz else (y0, y1)
|
||||
pos, k = a, 0
|
||||
while pos < b:
|
||||
seg = min(pattern[k % 4], b - pos)
|
||||
if k % 2 == 0:
|
||||
q, r = ((pos, y0), (pos + seg, y0)) if horiz else ((x0, pos), (x0, pos + seg))
|
||||
d.line([(px(q[0]), px(q[1])), (px(r[0]), px(r[1]))], fill=fill, width=px(width))
|
||||
pos += seg
|
||||
k += 1
|
||||
|
||||
|
||||
def draw_sheet():
|
||||
img = Image.new("RGB", (px(W), px(H)))
|
||||
d = ImageDraw.Draw(img)
|
||||
for y in range(px(H)):
|
||||
t = y / px(H)
|
||||
d.line([(0, y), (px(W), y)], fill=tuple(round(a + (b - a) * t) for a, b in zip(PAPER_TOP, PAPER_BOTTOM)))
|
||||
|
||||
# Grid on its own layer so a soft radial mask can fade it towards the edges.
|
||||
grid = Image.new("RGBA", img.size, (0, 0, 0, 0))
|
||||
g = ImageDraw.Draw(grid)
|
||||
for i in range(0, W + 1, 12):
|
||||
g.line([(px(i), 0), (px(i), px(H))], fill=(*PALE, 95 if i % 60 == 0 else 40), width=px(0.6))
|
||||
for j in range(0, H + 1, 12):
|
||||
g.line([(0, px(j)), (px(W), px(j))], fill=(*PALE, 95 if j % 60 == 0 else 40), width=px(0.6))
|
||||
mask = Image.radial_gradient("L").resize(img.size).point(lambda v: max(0, 255 - int(v * 0.7)))
|
||||
grid.putalpha(Image.composite(grid.getchannel("A"), Image.new("L", img.size, 0), mask))
|
||||
img = Image.alpha_composite(img.convert("RGBA"), grid)
|
||||
d = ImageDraw.Draw(img)
|
||||
|
||||
# Double border with a tick every 60 pt.
|
||||
d.rectangle([px(10), px(10), px(W - 10), px(H - 10)], outline=(*LINE, 255), width=px(1.8))
|
||||
d.rectangle([px(15), px(15), px(W - 15), px(H - 15)], outline=(*LINE, 110), width=px(0.6))
|
||||
for t in range(60, W, 60):
|
||||
d.line([(px(t), px(10)), (px(t), px(15))], fill=(*LINE, 255), width=px(0.8))
|
||||
d.line([(px(t), px(H - 15)), (px(t), px(H - 10))], fill=(*LINE, 255), width=px(0.8))
|
||||
|
||||
# Title, top left. The left icon's centre line sits to its right and starts below it.
|
||||
text(d, (30, 24), "SolidCraft", sans(20, 600), (*LINE, 255))
|
||||
text(d, (31, 49), "PARAMETRIC 3D CAD", mono(7), (*PALE, 255), tracking=0.9)
|
||||
|
||||
# A circle round each icon, with centre lines that stop short of circles, plates and text.
|
||||
r = ICON / 2 + 6
|
||||
for cx in (AX, BX):
|
||||
d.ellipse([px(cx - r), px(CY - r), px(cx + r), px(CY + r)], outline=(*LINE, 170), width=px(1.1))
|
||||
cl = (*LINE, 120)
|
||||
dashdot(d, (30, CY), (AX - r - 10, CY), cl)
|
||||
dashdot(d, (BX + r + 10, CY), (W - 30, CY), cl)
|
||||
for cx in (AX, BX):
|
||||
dashdot(d, (cx, 66), (cx, CY - r - 6), cl)
|
||||
dashdot(d, (cx, CY + r + 34), (cx, H - 42), cl)
|
||||
|
||||
# Leader from the app to Applications: origin dot, line, arrowhead, a label above.
|
||||
x0, x1, y = AX + r + 8, BX - r - 8, CY + 7
|
||||
d.line([(px(x0 + 4), px(y)), (px(x1 - 10), px(y))], fill=(*LINE, 255), width=px(1.8))
|
||||
d.ellipse([px(x0 - 3), px(y - 3), px(x0 + 3), px(y + 3)], fill=(*LINE, 255))
|
||||
d.polygon([(px(x1), px(y)), (px(x1 - 11), px(y - 4.4)), (px(x1 - 11), px(y + 4.4))], fill=(*LINE, 255))
|
||||
text(d, ((x0 + x1) / 2, CY - 6), "DRAG TO INSTALL", mono(8.5), (*LINE, 255), "ms", tracking=1.4)
|
||||
|
||||
# Title block, bottom right: two cells.
|
||||
bx1, by1 = W - 16, H - 16
|
||||
d.rectangle([px(bx1 - 196), px(by1 - 18), px(bx1), px(by1)], outline=(*LINE, 255), width=px(1))
|
||||
d.line([(px(bx1 - 98), px(by1 - 18)), (px(bx1 - 98), px(by1))], fill=(*LINE, 255), width=px(1))
|
||||
text(d, (bx1 - 147, by1 - 8.5), "SHEET 1/1", mono(7), (*LINE, 255), "mm", tracking=1)
|
||||
text(d, (bx1 - 49, by1 - 8.5), "SCALE 1:1", mono(7), (*LINE, 255), "mm", tracking=1)
|
||||
|
||||
# Name plates where Finder draws the icon names.
|
||||
py = CY + ICON / 2 + PLATE_GAP
|
||||
for cx in (AX, BX):
|
||||
d.rounded_rectangle(
|
||||
[px(cx - PLATE_W / 2), px(py), px(cx + PLATE_W / 2), px(py + PLATE_H)], radius=px(6), fill=(*PLATE, 255)
|
||||
)
|
||||
|
||||
return img.convert("RGB")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--preview", help="also write the 1x and 2x PNGs to this directory")
|
||||
ap.add_argument("--out", default=str(HERE / "background.tiff"))
|
||||
args = ap.parse_args()
|
||||
|
||||
big = draw_sheet()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
one, two = Path(tmp, "bg.png"), Path(tmp, "bg@2x.png")
|
||||
big.resize((W, H), Image.LANCZOS).save(one, dpi=(72, 72))
|
||||
big.resize((W * 2, H * 2), Image.LANCZOS).save(two, dpi=(144, 144))
|
||||
# A multi-representation TIFF: Finder picks the 2x one on Retina screens.
|
||||
raw = Path(tmp, "bg.tiff")
|
||||
subprocess.run(["tiffutil", "-cat", str(one), str(two), "-out", str(raw)], check=True)
|
||||
subprocess.run(["tiffutil", "-lzw", str(raw), "-out", args.out], check=True)
|
||||
if args.preview:
|
||||
out = Path(args.preview)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for p in (one, two):
|
||||
(out / p.name).write_bytes(p.read_bytes())
|
||||
print("wrote", args.out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
"""dmgbuild settings for the SolidCraft disk image (packaging/macos/package.sh).
|
||||
|
||||
Passed with -D here=<this folder> -D app=<SolidCraft.app> -D readme=<README.txt>
|
||||
-D icns=<icon.icns>. dmgbuild writes the Finder layout (window, background, icon positions) straight into the image, so the build
|
||||
needs no Finder session and no AppleScript.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
here = defines["here"] # noqa: F821 (this folder; settings files have no __file__)
|
||||
layout = json.load(open(os.path.join(here, "layout.json")))
|
||||
width, height = layout["window"]
|
||||
|
||||
format = "UDZO"
|
||||
filesystem = "HFS+"
|
||||
# `dmgbuild -D` values arrive in `defines`.
|
||||
files = [defines["app"], defines["readme"]] # noqa: F821
|
||||
symlinks = {"Applications": "/Applications"}
|
||||
# The read-me stays on the disk for people who look for it, out of the window's calm layout.
|
||||
hide = ["README.txt"]
|
||||
icon = defines["icns"] # noqa: F821
|
||||
|
||||
background = os.path.join(here, "background.tiff")
|
||||
show_status_bar = False
|
||||
show_tab_view = False
|
||||
show_toolbar = False
|
||||
show_pathbar = False
|
||||
show_sidebar = False
|
||||
default_view = "icon-view"
|
||||
arrange_by = None
|
||||
# The rectangle is the whole window: the title bar (32 pt on macOS 27) sits above the content area,
|
||||
# which must be exactly the size of the background.
|
||||
window_rect = ((260, 260), (width, height + layout["title_bar"]))
|
||||
icon_size = layout["icon_size"]
|
||||
text_size = layout["text_size"]
|
||||
label_pos = "bottom"
|
||||
icon_locations = {
|
||||
os.path.basename(defines["app"]): tuple(layout["app"]), # noqa: F821
|
||||
"Applications": tuple(layout["applications"]),
|
||||
"README.txt": (width // 2, height + 200),
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Per-machine install with a setup wizard: Welcome, install folder (Program Files\SolidCraft by
|
||||
default, remembered for upgrades), Ready, progress, and a Finish page with a ticked "Launch
|
||||
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli, the licence files (including
|
||||
SolidCraft" box. Installs a Start Menu shortcut, solidcraft-cli (and adds its folder to the system PATH), the licence files (including
|
||||
THIRD-PARTY-LICENSES.txt), makes SolidCraft the owner of `.solidcraft` (and of the pre-rename `.solvecraft`), and offers it under
|
||||
"Open with" for STEP, IGES, 3MF and STL. `msiexec /qn` installs silently.
|
||||
Adapted from PhotoCraft's (storytold/photocraft, Apache-2.0; see ATTRIBUTION.md).
|
||||
@@ -122,6 +122,9 @@
|
||||
|
||||
<Component Id="SolidcraftCli">
|
||||
<File Id="SolidcraftCliExe" Source="$(var.BinDir)\solidcraft-cli.exe" KeyPath="yes" />
|
||||
<!-- The install folder on the system PATH, so MCP clients and shells find `solidcraft-cli` by name;
|
||||
removed again on uninstall. -->
|
||||
<Environment Id="SolidcraftPath" Name="PATH" Value="[INSTALLFOLDER]" Action="set" Part="last" System="yes" Permanent="no" />
|
||||
</Component>
|
||||
<!-- Compatibility shim (docs/compatibility.md): the old CLI name, forwarding to solidcraft-cli. -->
|
||||
<Component Id="SolidcraftCliAlias">
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
# Recipe and generator receipts hash canonical bytes on every platform.
|
||||
*.json text eol=lf
|
||||
*.py text eol=lf
|
||||
*.md text eol=lf
|
||||
*.png binary
|
||||
*.hdr binary
|
||||
@@ -0,0 +1,14 @@
|
||||
# Runtime render catalog exports
|
||||
|
||||
These JSON files are necessary runtime exports of the editable authoring catalogs in
|
||||
`docs/render-materials/`. Do not edit both copies. `python -B tools/render-library/generate.py`
|
||||
copies the original bytes and updates the Rust lookup; `--check` verifies exact equality.
|
||||
The application includes these asset exports, never files from the documentation tree.
|
||||
|
||||
The 192 texture maps and 24 HDR files are separate from this metadata. They are included
|
||||
only with the engine's `bundled-render-library` feature. Desktop builds enable it by
|
||||
default. Standalone CLI and web builds do not; use their same-named feature to opt in.
|
||||
Discovery and uniform appearances remain available without the image pack. Requests
|
||||
requiring missing bytes fail explicitly before document mutation; no network is used.
|
||||
GPU/UI test targets opt in deliberately. Exercise the engine's no-pack negative tests
|
||||
separately from its feature-enabled resource tests.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user