mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
1226 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 165e2d5bd5 | |||
| cbab92b0e7 | |||
| 4bdffef061 | |||
| 2aa7e3e51c | |||
| 7a7fa9a1db | |||
| 2b921df189 | |||
| 38815796a3 | |||
| 55a5f86e2e | |||
| a1c8702854 | |||
| 25fba581bd | |||
| 61f07fb294 | |||
| 47c685d471 | |||
| c7c28515ce | |||
| 01abf8af17 | |||
| ac3e6e0105 | |||
| d9310b2389 | |||
| 440fdaed13 | |||
| ce5fbf40ab | |||
| 64010a1d36 | |||
| 2baa3890bd | |||
| 3131b933c7 | |||
| c97092e385 | |||
| 44ec12cb69 | |||
| d2882754fa | |||
| 955232fbea | |||
| ae45cc6623 | |||
| a3f06a4f94 | |||
| 954e10d6f0 | |||
| 53cd9ea5aa | |||
| f3f8752153 | |||
| 29614b5e0d | |||
| e1d3d5009b | |||
| d170403841 | |||
| 8a730c6273 | |||
| 881899bee1 | |||
| 0b6991f1a9 | |||
| 9f6c340d15 | |||
| 90b3c87de3 | |||
| a59c14678f | |||
| 147b4049cf | |||
| b4a3a9baae | |||
| cc3e954f45 | |||
| 185c480319 | |||
| c5ccb9201d | |||
| 53211ab6ff | |||
| 49d9ec2be7 | |||
| 39807359f0 | |||
| ea772a0438 | |||
| 054392abbe | |||
| 28e983e191 | |||
| 841b355df1 | |||
| e831d681d3 | |||
| 30e272c44a | |||
| 28b7ae007d | |||
| 59ffd38019 | |||
| 331a8526ef | |||
| 5e54681c80 | |||
| 7bdaec8f4d | |||
| 990867cd80 | |||
| b818d7043d | |||
| f582b1f39e | |||
| 6be5be7f0c | |||
| e4cae82274 | |||
| 0d8eebbea6 | |||
| 1721cb0a4c | |||
| 3742b1a008 | |||
| 3bf29b9787 | |||
| 10bc1afec2 | |||
| 861fb8ad56 | |||
| 78b95a71b3 | |||
| f7d66d2b5b | |||
| ce6ad21106 | |||
| 71bb2c58a4 | |||
| da9960e366 | |||
| 7b8174cb88 | |||
| ef8e4d4195 | |||
| 703509bf89 | |||
| 60cb0c7342 | |||
| 71d34b9b15 | |||
| 01b5dceba1 | |||
| 887607ebb3 | |||
| ae6ced6580 | |||
| 60488a0837 | |||
| 21c5be2b25 | |||
| a9f8ccfa01 | |||
| 780c1163f9 | |||
| fa1eb57e3b | |||
| 6602ab823f | |||
| ccdb470e01 | |||
| a54d0517f7 | |||
| 7015c296ea | |||
| b73eb6c243 | |||
| 38cc3507db | |||
| 90e30126ea | |||
| e3bbd8258a | |||
| 857b13b64f | |||
| 0f8b971ef0 | |||
| 9432cdfee8 | |||
| 4a680d17ba | |||
| dadefec702 | |||
| ca4796921f | |||
| 686056e4de | |||
| d7ad69e64f | |||
| a0ec7dbdf0 | |||
| 00f7e97578 | |||
| 89f1234f28 | |||
| 6b1eb5796e | |||
| 927a5c3733 | |||
| c7547eb3a8 | |||
| 6cee7a91b9 | |||
| 15e7615ce3 | |||
| 1b99e756a9 | |||
| 70b2466965 | |||
| ec1f78c6fa | |||
| 01b592e0c8 | |||
| f6e932f61a | |||
| 8ab33bb80c | |||
| e33b8cfb07 | |||
| 768dc253dd | |||
| 1d98b59883 | |||
| abea79d8ab | |||
| c4808e4715 | |||
| 6649cf4cfe | |||
| 9b431fea16 | |||
| f484457c3b | |||
| 2339510e41 | |||
| a9fa61ce77 | |||
| ac9d4e8a1d | |||
| 39163148d9 | |||
| 3ca311f922 | |||
| b978eeddcd | |||
| 2ccca96929 | |||
| 8ac4c03602 | |||
| 063f481ea0 | |||
| a025d2de9b | |||
| 3c806394b0 | |||
| 3ddd73728b | |||
| 8ec3dbc201 | |||
| ef939b52eb | |||
| c1612e9234 | |||
| 544de55baa | |||
| 961be2269c | |||
| 29a6bbdb43 | |||
| a169ad4194 | |||
| d907078434 | |||
| 073bbc3f00 | |||
| 9e5e143ac3 | |||
| 0dd8d9e5ec | |||
| 9fdf153054 | |||
| 5336007c2b | |||
| 21fc384e40 | |||
| 5d6a80ff35 | |||
| 9636188a0f | |||
| ec39a8ebb4 | |||
| 5f2de93fc6 | |||
| 450cc8b389 | |||
| 3c12c36bef | |||
| 6e67ae34b0 | |||
| 1cbebe21bf | |||
| 25a19cdefe | |||
| 7099e80059 | |||
| 12ebc80e15 | |||
| a89ea233ee | |||
| b470d46ffc | |||
| 6e98bd474e | |||
| ad23882080 | |||
| 63fb818f33 | |||
| 96509412e9 | |||
| f2ffd8d233 | |||
| 2b08e0c6a7 | |||
| a239ff50c9 | |||
| 1885fa739b | |||
| cacfbbc578 | |||
| 2ae13ba9cf | |||
| e91f5d73bd | |||
| da501178aa | |||
| 3231f049d0 | |||
| a22ea76ab6 | |||
| a735620aa8 | |||
| 059119b345 | |||
| e016c40f43 | |||
| 1fb9dadcd9 | |||
| 91a19548d3 | |||
| a277839236 | |||
| b5d931ff4c | |||
| d7a17de3d9 | |||
| a33619fd2b | |||
| e4e3c8609c | |||
| 325917f2ae | |||
| 5ad2503503 | |||
| f43198988d | |||
| 6f73bd95c4 | |||
| 9d7544929e | |||
| 3125a79827 | |||
| eb0cecee31 | |||
| 27951f40c3 | |||
| 9e3cf05c4f | |||
| 6459c9fcf0 | |||
| 5e7f3651fb | |||
| da586bc048 | |||
| 3a5b38355c | |||
| fb1d5f79ad | |||
| 062a7b8288 | |||
| 5bbfa5ea6c | |||
| 75c46dad7b | |||
| af54b09bb1 | |||
| dcbca523a0 | |||
| e9b08f4af4 | |||
| 6a7275a5af | |||
| 666356dba0 | |||
| 3fe2b3386f | |||
| afe06a1a3d | |||
| eaf34d2528 | |||
| 2b9261050f | |||
| eac1425661 | |||
| 88dbda42a7 | |||
| 9e30ff5878 | |||
| 4c4c768d57 | |||
| 83fe1f874f | |||
| 0734616104 | |||
| d68d86dae9 | |||
| 7d86a8cfd3 | |||
| cea07cbc66 | |||
| 0b90731096 | |||
| 0aa6f15b82 | |||
| afa4f72f7d | |||
| cfc46448a8 | |||
| 58c64ca4d4 | |||
| 5c7e50cb67 | |||
| b6aabb88ff | |||
| a1dc849e8d | |||
| 96a8820f56 | |||
| 446590dca1 | |||
| 303236e524 | |||
| 97336ccfe7 | |||
| e1d4cb433c | |||
| 0ad74f2577 | |||
| 835e261593 | |||
| 84c3b6356c | |||
| d730a273dc | |||
| 89cb651084 | |||
| 9efcb461f8 | |||
| 958ab867cd | |||
| cffa6e1580 | |||
| ce296a1ad5 | |||
| 0515dee9ce | |||
| 0478f7e401 | |||
| 52f0b52b45 | |||
| 8f691ac257 | |||
| 1ae93218a7 | |||
| 3017641b41 | |||
| 17b9a8e67f | |||
| 90c09eaaf9 | |||
| 50dfff716b | |||
| 47402bfa8d | |||
| 2b85a39c9c | |||
| 251573a5a4 | |||
| bdce595fb3 | |||
| 862807ffc9 | |||
| 6f368a5b77 | |||
| a0ed5a9923 | |||
| 38ae728538 | |||
| 147d1cfadd | |||
| d135e1659c | |||
| 3ccf04d4b2 | |||
| 2014e60ae6 | |||
| c454c05f88 | |||
| 1cc90036fb | |||
| 176c22acaa | |||
| 2ab5dedfc1 | |||
| d0fbd1f60a | |||
| e77c55549f | |||
| e7bb038795 | |||
| a5a561caae | |||
| 462de5b9d2 | |||
| be425c6102 | |||
| bc4d35b097 | |||
| 78c5b8de42 | |||
| 9738861d61 | |||
| 0cf4da606c | |||
| 074cc758ee | |||
| 47e8b96c2f | |||
| 7b3d84fb33 | |||
| 59a40aabc0 | |||
| b557f22f97 | |||
| 7240165c31 | |||
| 29acaf8642 | |||
| 36bdf4ea3c | |||
| ddd65cc4bb | |||
| 2a410c2d76 | |||
| 01b43999a5 | |||
| bcb5efcaa4 | |||
| 74ee047d3f | |||
| ab6e6941c7 | |||
| a93d7b207c | |||
| 2b65164159 | |||
| fbb00dc7b2 | |||
| 7702316039 | |||
| bb0b9a1d2f | |||
| 74eda80b5d | |||
| ea63eec845 | |||
| 64a55392d8 | |||
| 3a6920d10f | |||
| 228b311a6f | |||
| 16f00d4cf9 | |||
| 9fa3c85c68 | |||
| cda0277c3e | |||
| f233f000e8 | |||
| fcb0a925e2 | |||
| 71624cf7fa | |||
| ce72abacf9 | |||
| f25a8e6abc | |||
| 3f0e9b87fa | |||
| 24b91bd453 | |||
| 22f12e80e4 | |||
| 1b02f38732 | |||
| 7239c95858 | |||
| f90b6116d5 | |||
| 38b9ff223b | |||
| d96d547f6b | |||
| 05a8911f83 | |||
| 1581d85cba | |||
| ad54bdf8eb | |||
| 7bafc99390 | |||
| e537a63988 | |||
| 984a5de001 | |||
| a799318b0f | |||
| f989b3309f | |||
| 8771b734e4 | |||
| ef081519d5 | |||
| 2a3d29766e | |||
| 058f17cc37 | |||
| 6475418169 | |||
| 1a76f80bae | |||
| f7ceba9cca | |||
| 249ca9267a | |||
| 119168cb3e | |||
| 2eb81a79b5 | |||
| b503340b06 | |||
| 63123e9237 | |||
| c843d1fb28 | |||
| b1a28df0ba | |||
| 4b6bbe9d72 | |||
| 7032e6c760 | |||
| 483e21ab1b | |||
| 9f37003dfa | |||
| 4c44126a4d | |||
| 5c1aad8a09 | |||
| 9d01f6e005 | |||
| 3b31dc77d8 | |||
| 127373a950 | |||
| 632c63f034 | |||
| 541910add2 | |||
| 6cc1acc72a | |||
| 64fe82b6c1 | |||
| fafa575c9f | |||
| 7e701aabc1 | |||
| 9475e0e1a4 | |||
| a8be28563c | |||
| a69a7c6ffe | |||
| 7366fe0ecc | |||
| 60d79e7ea2 | |||
| 653bc00be4 | |||
| 915dd9a83f | |||
| 3ddd8c7179 | |||
| b99d4ddde9 | |||
| 425417b36a | |||
| b7a6967ef2 | |||
| 68105938e2 | |||
| 11e283ba77 | |||
| c25c2f3a21 | |||
| e0582668bb | |||
| df04e7742f | |||
| 56b77b405d | |||
| e4a67d1e30 | |||
| ed9599d1e4 | |||
| 80fb02f0b8 | |||
| a5c8fa7be7 | |||
| beb35029a4 | |||
| 6a3d422a83 | |||
| 4c3d1484c5 | |||
| e0c1b8adbd | |||
| 604515cfb3 | |||
| 57d789c0aa | |||
| 4e9656ea9a | |||
| 07160d9366 | |||
| c111b5f2fb | |||
| 4594d81f7a | |||
| 2e3448f348 | |||
| e5c6e3ef1e | |||
| 57df47b763 | |||
| 42145719a0 | |||
| 64421a2cb9 | |||
| 711e41233f | |||
| 8fa2488a00 | |||
| cf0508e5c8 | |||
| bace1a1228 | |||
| bda8b6b2e8 | |||
| 52765304cd | |||
| ec526d2b3d | |||
| 3b3d93f333 | |||
| 05f30c1674 | |||
| bba2fc1400 | |||
| 538d7fcb43 | |||
| 8be56316f6 | |||
| eda51873aa | |||
| d4144ddf9b | |||
| 8f7a8f693c | |||
| c39f44e190 | |||
| 5b8d8416e9 | |||
| 1f2356fa7b | |||
| e29e32f197 | |||
| 76e009ab10 | |||
| 441d67f9ff | |||
| 6541a4e980 | |||
| 85197c1224 | |||
| 95788bb730 | |||
| ed66adac5c | |||
| e88295ceba | |||
| 91ca23359e | |||
| aeca117045 | |||
| 31326e9898 | |||
| a9e9f8b82b | |||
| 41700f928e | |||
| 8558b37e44 | |||
| aa244bcee4 | |||
| eaf3acc297 | |||
| c392791f2c | |||
| 0fb2e9b06b | |||
| b410c577e0 | |||
| d95b8208b3 | |||
| 99f8cff454 | |||
| 3292cba4ad | |||
| 6fa023b3ab | |||
| 510378d965 | |||
| c8a9159ac5 | |||
| fca86a414e | |||
| b76fcae341 | |||
| e64202417f | |||
| 6a4b5c90c8 | |||
| 2e981d66e8 | |||
| ef0914ff94 | |||
| 0476154973 | |||
| 5e265089c7 | |||
| 905c02afc2 | |||
| 31a0c99a2c | |||
| ce3b0d4086 | |||
| e1b4ebfe2f | |||
| d782d45cce | |||
| 9a44641970 | |||
| 2b1ca94e2d | |||
| f30fce0fc1 | |||
| f4b74fcb1b | |||
| 0cb1031f2c | |||
| 59efc86826 | |||
| 24ce89e0fc | |||
| a476e645b0 | |||
| 3385d1c570 | |||
| f0acea0992 | |||
| 6047c0d5a1 | |||
| 34c378c063 | |||
| dc6e25250c | |||
| 2e22ff8035 | |||
| cb9b50b11e | |||
| a22aeaaf93 | |||
| d4eeedbf95 | |||
| c512c8e86a | |||
| 1bcc1125f0 | |||
| 7c013333db | |||
| 7e0f8a9a4c | |||
| 18e200f262 | |||
| bd0f0f3471 | |||
| e39d741862 | |||
| ea9ea7bba8 | |||
| 07fa5e4f1f | |||
| 2a51f42495 | |||
| 8f259d5b83 | |||
| a6b9d5a5b2 | |||
| 18e4b9ea9f | |||
| 7cccb2b52e | |||
| 7d7a1ecfa5 | |||
| 1202a79e25 | |||
| fbafdfdcb6 | |||
| 2744063631 | |||
| 8e53d5050e | |||
| fde56d3c8b | |||
| 81a6a3dcde | |||
| 3eb76a8b3b | |||
| 60de59d5a9 | |||
| ad0b5c9d05 | |||
| ff08eb0043 | |||
| 6bfd53a55a | |||
| 709cb62fe8 | |||
| ee4786cd8f | |||
| 6a4fef151f | |||
| d3574dd493 | |||
| d2a3430147 | |||
| 598556bca9 | |||
| 80341ec13b | |||
| 7be5d93e51 | |||
| 5bb3b6717f | |||
| 5b6e5815a3 | |||
| 376c313214 | |||
| ab665ee3b3 | |||
| f0d47e1d26 | |||
| b6078cbe68 | |||
| a2365228ab | |||
| e2a7a45c44 | |||
| fa21db7df8 | |||
| 4fda59af80 | |||
| 776dfc9c18 | |||
| 6583c28231 | |||
| 6b24775413 | |||
| c4f9269409 | |||
| 9b31d29443 | |||
| e16647a63f | |||
| 5281734a22 | |||
| b5fe8776b5 | |||
| 8986a670e5 | |||
| c528964363 | |||
| 251c20bef0 | |||
| 3c0d4e120e | |||
| bfd6aa9b7d | |||
| 44d7c1fb6f | |||
| db696ec24b | |||
| a69c5770a4 | |||
| da23eb1377 | |||
| 5d9e4d5f95 | |||
| 75b3fb678c | |||
| 827713cc2f | |||
| 4cf104ea2c | |||
| 4fd8f8475c | |||
| 63ebc56842 | |||
| 732436fd59 | |||
| 9af354a6db | |||
| 8b35e23551 | |||
| b78df205d1 | |||
| c35be9d20e | |||
| cc313cc3fa | |||
| 66327c27aa | |||
| a98d055597 | |||
| 2eff99979a | |||
| e2186acfd7 | |||
| af35c52805 | |||
| 8702e71372 | |||
| 9e1b41f90b | |||
| 890eb79452 | |||
| d4d8be7f00 | |||
| 1c70f2c708 | |||
| ef7bd814ee | |||
| c8ea4168b1 | |||
| b9105a394e | |||
| f60f71d878 | |||
| 133de31ec9 | |||
| 59b9345750 | |||
| 67013acc28 | |||
| 9fdf380286 | |||
| 4c1dee13f3 | |||
| affe37bcc3 | |||
| 9d8ab72da7 | |||
| 160662ffec | |||
| b5550d01c1 | |||
| fd5d0e6738 | |||
| 0067dcab28 | |||
| 2f6ca7e46b | |||
| c2f66107cf | |||
| ec5f4b8740 | |||
| 91aefd5abc | |||
| f1c2fc8804 | |||
| d7cba9e96d | |||
| 849bb392d2 | |||
| 24134d2ed0 | |||
| cfd04c33fe | |||
| 3b77567932 | |||
| 810a9558bd | |||
| d7ae568790 | |||
| 43a549d967 | |||
| 7228e218bf | |||
| 67b491671f | |||
| 60b9e9d188 | |||
| ca005237a1 | |||
| 213e05da5f | |||
| 251d18588a | |||
| 97f48a9f63 | |||
| 72a265e4fc | |||
| 3877ef08b9 | |||
| cce337f83f | |||
| 5304294107 | |||
| b8eb8d262c | |||
| 28cbca72c5 | |||
| 9d18034e13 | |||
| d61ec60480 | |||
| c4538c53e1 | |||
| b6b0d80332 | |||
| 42166bd6b7 | |||
| 5bd5be9543 | |||
| 2f09c90542 | |||
| 8311ae5c5d | |||
| 4973638b56 | |||
| 8a61069fbb | |||
| 55bea620c7 | |||
| c755e067f5 | |||
| f73479ddf2 | |||
| 772815b4c0 | |||
| 6d89de1f5a | |||
| 6375529b44 | |||
| bdd7605d47 | |||
| ffbaf292f0 | |||
| 2b4f07a0e8 | |||
| 6f88c0a0a7 | |||
| 4b1556d102 | |||
| 0fe5bf114c | |||
| 526cdfd247 | |||
| 3db45df8be | |||
| 7f9ddb31a8 | |||
| 045cfed6a7 | |||
| 5f60d78658 | |||
| 39e9916cbc | |||
| 646eb97eaf | |||
| 2cf8df2ef6 | |||
| 0096e694dd | |||
| c29bd18190 | |||
| 8e9e270027 | |||
| 1b927421f6 | |||
| 8d22b3a41a | |||
| edcf02c0d4 | |||
| b85d5a665c | |||
| a232df7751 | |||
| 4757c3cee6 | |||
| 736f334301 | |||
| dfe4701d22 | |||
| e721004f62 | |||
| 88d87729d4 | |||
| c7597ea66f | |||
| f3389c3af7 | |||
| a0a52f6657 | |||
| cc9787f80e | |||
| c324acdf29 | |||
| 8e80d98610 | |||
| f8a8114843 | |||
| 98bfe1af9c | |||
| ffbf9836cc | |||
| 806adb6c46 | |||
| 250ed2f266 | |||
| 56f07a5598 | |||
| dfdad35d08 | |||
| 1cef0d83df | |||
| a99c57ffc7 | |||
| b6cd0d495e | |||
| cdffdcbbef | |||
| 4f05716f69 | |||
| f0567d6a48 | |||
| 6e9ffe83d2 | |||
| 2647ec51fc | |||
| 23587433e2 | |||
| 0414de7b98 | |||
| 295bd0e165 | |||
| 0b34d3590d | |||
| aedbf0b1d4 | |||
| a13e0e0ceb | |||
| 6f05c19c42 | |||
| db0c25522e | |||
| e6c999bbcd | |||
| 180a725c0e | |||
| 4ba6a9744b | |||
| dd2376923f | |||
| ae998906ff | |||
| 634f0a2308 | |||
| 306b78ce16 | |||
| 678da425da | |||
| fc499441e2 | |||
| 1cf5f83924 | |||
| 6a465abee9 | |||
| 89a3e89d62 | |||
| 58c0fc396f | |||
| 8c597703f8 | |||
| 0952686c1b | |||
| 64b506dccf | |||
| 61195235d8 | |||
| 6241decaf9 | |||
| 3288adf3dd | |||
| 5b53a1354a | |||
| 579dddaea0 | |||
| 9052b69e81 | |||
| 2c5d875ab7 | |||
| 04efc68578 | |||
| 0e137b947f | |||
| 97a24941d2 | |||
| c2a19bf375 | |||
| e40d7aaee7 | |||
| 2b18b68307 | |||
| b9ae8814d4 | |||
| f3a4c62240 | |||
| dbf38d84ef | |||
| f913a2a459 | |||
| ae9ff0aa71 | |||
| 0b206f2ed5 | |||
| 3b4fcd25c0 | |||
| 7d0c7e383e | |||
| 8ac8ff66b7 | |||
| 5fa7a8497c | |||
| bc93a653f6 | |||
| 33fdadcc40 | |||
| f504c82216 | |||
| df18823d0a | |||
| 47985ce485 | |||
| 0396b2d00a | |||
| b1c132bc9a | |||
| 71b3a70254 | |||
| bcdecd19f4 | |||
| 0b8a0e08a0 | |||
| 201617ef88 | |||
| 3b3f860adc | |||
| f0d5666a74 | |||
| f1c2f67d63 | |||
| 32900556e0 | |||
| bf14bf3678 | |||
| f0a5fc5016 | |||
| ad1652683b | |||
| 0d778e074b | |||
| 0b2d51d301 | |||
| 99cd20ea20 | |||
| bf7979d4a7 | |||
| 5439bfc0b5 | |||
| c2ea096abb | |||
| 085853315d | |||
| 332f96c26a | |||
| 56e752c182 | |||
| 014ccf5ff4 | |||
| ab309f2219 | |||
| 6c9afc6f7e | |||
| aff8106205 | |||
| 143a6cb987 | |||
| d263bf1e90 | |||
| 3f0139b4d9 | |||
| 8ced905843 | |||
| 21340ba019 | |||
| 8b25fa0ae4 | |||
| c0b0369aa2 | |||
| 44a9f9ee42 | |||
| f331a0740b | |||
| 12636b67be | |||
| 9f5ac92faa | |||
| 6e39efd4ac | |||
| 3c40edd12a | |||
| 81b620d7aa | |||
| 9af2ae9a0f | |||
| 1754318df7 | |||
| f273195144 | |||
| 247a77e1b1 | |||
| ce4212a33a | |||
| 522bd95254 | |||
| b0b1e6832e | |||
| 3cb9aa07b5 | |||
| 60f6836ab7 | |||
| 43f0438fbb | |||
| ded10b52da | |||
| 4bc5bdc791 | |||
| 1baaebd3ef | |||
| 97e467f2d1 | |||
| 665ff84495 | |||
| 07b3b92703 | |||
| ddf56e1246 | |||
| 0ddbe87935 | |||
| 19dc8b6881 | |||
| 3e2e229dd1 | |||
| c8d9a4bfdd | |||
| 6c764dce06 | |||
| 655e771455 | |||
| 7a761d9436 | |||
| 15f74b7981 | |||
| 74405f5da0 | |||
| 9b607f6f1f | |||
| 96181d3c41 | |||
| 47bec0a072 | |||
| 4ffc6a9017 | |||
| ff3000e10b | |||
| faf08ec1bb | |||
| 2c5a94cd1d | |||
| e58e85e3d6 | |||
| 4a0519e464 | |||
| 103b86f8f4 | |||
| b9237538e4 | |||
| b36140f6fd | |||
| e19d1d1051 | |||
| 9be549e6a1 | |||
| eeaddb56c1 | |||
| 7044a44425 | |||
| 02b8c88cf8 | |||
| f80b3ebc1f | |||
| db9897fda7 | |||
| f1ac8d3361 | |||
| 8ecfeef245 | |||
| f0a2c3f99d | |||
| 54c3b1e90e | |||
| 819461c908 | |||
| fdd103fc2a | |||
| f4dcd28de7 | |||
| e790c6428d | |||
| f43214a52f | |||
| fc3062884f | |||
| 8f4378db60 | |||
| 3afa2e874e | |||
| 5c69b88375 | |||
| e6d1893f1c | |||
| eda02a5ee7 | |||
| 1bffc3cc78 | |||
| 7df316ccce | |||
| 0710897b63 | |||
| 27d240d58c | |||
| 39e6e333c0 | |||
| 3ff9e634bc | |||
| 1db07a82f8 | |||
| a322451356 | |||
| 88c7c71761 | |||
| 20dde1a67a | |||
| 27e5a5ea8d | |||
| 54706bbb36 | |||
| d76cee7ce7 | |||
| dbec20dbbe | |||
| 1e9e80b7a7 | |||
| 5cd36d55e1 | |||
| e4ebb8c820 | |||
| 2cc1577a9d | |||
| 86390de541 | |||
| 9acdd5ec66 | |||
| d0a8be5939 | |||
| 12c2310d5e | |||
| aa1557009c | |||
| 2b2229ab31 | |||
| d3dc45681a | |||
| 98ec65d94d | |||
| cae382900a | |||
| 3348b60b54 | |||
| 68ea0f940f | |||
| fadca8440f | |||
| cd5c13e78d | |||
| 06e4d75e9c | |||
| 27a0383e1c | |||
| 7623e37e7b | |||
| 43a8e92a12 | |||
| 470cd4d761 | |||
| 35a31aa3cb | |||
| 93f8ba1a8d | |||
| 22c92d8eff | |||
| 45ee28379e | |||
| 121439114c | |||
| b10cd4b8e0 | |||
| 2aec675349 | |||
| 992b243cb6 | |||
| ca8fc27e8b | |||
| f6f2f0bc38 | |||
| 709045380e | |||
| c5a31b96dc | |||
| 295ec703fb | |||
| 80fb1768a8 | |||
| 1237203137 | |||
| 7de16a06ee | |||
| bb0d3800de | |||
| f226557ab1 | |||
| 88aaf50bf1 | |||
| d64f60bf93 | |||
| 4401e46c89 | |||
| f253d56895 | |||
| ce68658422 | |||
| c4e727871d | |||
| f0a52c36b5 | |||
| e909f3e013 | |||
| 824615e119 | |||
| 524b25b8ba | |||
| 97f5208c2c | |||
| e89b8528cd | |||
| aa161ee924 | |||
| 763f0632d1 | |||
| 99c879f9b2 | |||
| 3d5e874943 | |||
| 0106365e64 | |||
| aaecfda600 | |||
| ce8981f167 | |||
| 615e810f28 | |||
| 74bca47120 | |||
| c3670a7054 | |||
| 8ac38056c1 | |||
| b8b8d6b6a2 | |||
| aba58faeff | |||
| 74a97586a8 | |||
| d20efa730b | |||
| ae52945298 | |||
| be583dbfd7 | |||
| 5cab3f00b7 | |||
| d2bb84d6fb | |||
| 7919eaf44c | |||
| 7067258444 | |||
| dc64e105b9 | |||
| e794c6b6e4 | |||
| f3b8ae5497 | |||
| 207cb82818 | |||
| 21582b66e9 | |||
| e71d2e2844 | |||
| 48498d477a | |||
| 32ca22cf6d | |||
| 0448d3126f | |||
| 6fc8706eea | |||
| 091357ea58 | |||
| 2f7f0c679c | |||
| bc4741b05a | |||
| a93baff0be | |||
| aba3f2e1e7 | |||
| 0d3e1eac82 | |||
| 0cae7f871c | |||
| 522a8adf91 | |||
| 70beda47b1 | |||
| 4cd4ebfa50 | |||
| 0fa9c52b30 | |||
| 2480af1907 | |||
| 6537aaa0d0 | |||
| 18ac5bfd45 | |||
| 2b559115a7 | |||
| 77f1bd1c1d | |||
| 57c0e6b802 | |||
| 44e7ddfc9f | |||
| dca61e72be | |||
| 01aaf872b0 | |||
| 40426d19bd | |||
| 20aedb330d | |||
| 6d5a12789e | |||
| 14ae2c6996 | |||
| e8d0bd9c20 | |||
| 08156eb6d0 | |||
| 74cb9ca753 | |||
| 7314e9440c | |||
| 089dec39d4 | |||
| 159f9f40b3 | |||
| 789b20294e | |||
| 65b02ff7ab | |||
| 53f4d607e2 | |||
| 5a2e050f71 | |||
| 7c40ff2805 | |||
| 3099117942 | |||
| 7c14ae2ce2 | |||
| fb6a38693e | |||
| ab4f92674b | |||
| a48570cd40 | |||
| fceb07b797 | |||
| ffb0e6650b | |||
| 894b02e02f | |||
| 57921d7fcd | |||
| 40f16c36bb | |||
| 01be42ef41 | |||
| e98cd0a0e2 | |||
| cc2de29abb | |||
| 4531ab6182 | |||
| 64dfa3ca11 | |||
| d1b64fb2ff | |||
| cf8ad08ec3 | |||
| 0968a2a42d | |||
| 34c5bc74a5 | |||
| 706eafbb17 | |||
| 68263a8e02 | |||
| 57b4ce6453 | |||
| 335278e3c7 | |||
| 3dc5d6d475 | |||
| ae7a464148 | |||
| c025baddd4 | |||
| e7aadf8f40 | |||
| 176202b238 | |||
| 5a4e482e9c | |||
| 88736dc9e4 | |||
| d6dacbd6d8 | |||
| dc8b5489fc | |||
| 27b8046c6f | |||
| 5238a30bef | |||
| 9ae514b41f | |||
| 90ef5af541 | |||
| 242d8d9a7d | |||
| 6fcd6713f5 | |||
| d94ed7adb9 | |||
| 062054482b | |||
| cc425a6e5c | |||
| 5047403bb1 | |||
| 71a81514ee | |||
| 2931f71d78 | |||
| fb292f17b4 | |||
| cffbe6a72b | |||
| db2552f078 | |||
| 8f28952005 | |||
| cbd7b07767 | |||
| 7c481d281f | |||
| 2425f8b453 | |||
| a5fc58bfed | |||
| 2c2f2e5140 | |||
| 8e1005c82d | |||
| 4c922a19a9 | |||
| cc085dcd52 | |||
| e5bca0495d | |||
| a68a6ca981 | |||
| 7fe4469fc4 | |||
| 8554b2a0e4 | |||
| c7ead5658d | |||
| a889da6000 | |||
| fd0022ee15 | |||
| 30fbf80cc4 | |||
| 06c2c6087b | |||
| 9767f95c31 | |||
| 51f0a9df43 | |||
| cf0ff5159d | |||
| b11e66e606 | |||
| c2a1e7524e | |||
| c1b2eb638a | |||
| 0a108aac38 | |||
| 7b19f68368 | |||
| 9a508571fd | |||
| 69a2c57cfe | |||
| 0de308b21d | |||
| b1c82e0582 | |||
| 8b3eedd0aa | |||
| 9aedc5faed | |||
| 227e4b7794 | |||
| 59e1da2bd8 | |||
| bfae336ce1 | |||
| b6dcdd163e | |||
| 3557e3c563 | |||
| 0a014857ab | |||
| 542715de62 | |||
| 6354e472ed | |||
| e2723501ab | |||
| 82f1404518 | |||
| 2ed50ed7df | |||
| a59ec94b02 | |||
| 95a242cb67 | |||
| 460f7fa0f7 | |||
| 8291eff436 | |||
| 8a9315e7f7 | |||
| 37c118042c | |||
| 50ca3c5a61 | |||
| 816e43c5d0 | |||
| 209ef1f6c0 | |||
| 7ddfd0a389 | |||
| f6dd5c8c2e | |||
| 3696bf259e | |||
| 33ffde4d5a | |||
| 540aa0da1b | |||
| 13bb81b847 | |||
| 00659d4d12 | |||
| 58dd3a66cf | |||
| e45d7a3873 | |||
| 68c1c18e73 | |||
| 4b94403dd6 | |||
| b9e0543ef5 | |||
| fc66604b07 | |||
| 2c3f760f05 | |||
| 9f7f510843 | |||
| 2e39a45145 | |||
| 6d9703c97c | |||
| 18618bb72c | |||
| 13df957b01 | |||
| 2c4a5b6872 | |||
| ab9c676627 | |||
| 966af74030 | |||
| 585a6b4efe | |||
| 186b145be8 | |||
| ce74cebdc2 | |||
| fde43b5317 | |||
| 89457d7917 | |||
| 11be845827 | |||
| 739c9d8f5b | |||
| 5fded8864a | |||
| a8038a81df | |||
| b885929d8a | |||
| 60244c2dcd | |||
| 0d502d2634 | |||
| 22d1ca264a | |||
| 73e7636790 | |||
| 894022904f | |||
| 73713b0de2 | |||
| a51c09e063 | |||
| be01004e30 | |||
| 12dd1a17cc | |||
| 38ef4e83cc | |||
| 4a589aca8c | |||
| c302f6cbb3 | |||
| 9f8e44b216 | |||
| b1dc94da99 | |||
| bc9ab68c20 | |||
| 3e564dffb0 | |||
| 6ada9b193e | |||
| ba83c9c6fd | |||
| a1b9d92d22 | |||
| dedcd516e5 | |||
| 16f7253218 | |||
| ab69757e9b | |||
| 60fbf550b6 | |||
| 79e81613fa | |||
| cba1f888b6 | |||
| e4a9870353 | |||
| c34789dcf6 | |||
| 3092bf60b7 | |||
| 4708184430 | |||
| 54cd0d89d3 | |||
| e92a411dd7 | |||
| 6a7581deaa | |||
| e8f47435a1 | |||
| 9aa0c22097 | |||
| 4db0e94082 | |||
| d4173f612a | |||
| c30f621b98 | |||
| 52f1f0273e | |||
| 3e069a389a | |||
| ab7d133c40 | |||
| b8533c79a7 | |||
| ec7077e326 | |||
| efddcdfeeb | |||
| 699bfc12e0 | |||
| ce8c6063bd | |||
| 4eff79e57e | |||
| 52453f50fb | |||
| 0bd4531c7c | |||
| f4e2f38a27 | |||
| d62301ad01 | |||
| 782457f960 | |||
| 623679bf9d | |||
| ba418eb40d | |||
| e63bcc3808 | |||
| 24df37a653 | |||
| 3f20ea1358 | |||
| 249139ece8 | |||
| cf2b37d9c9 | |||
| 6fd5b65c1d | |||
| 043d213ce1 | |||
| a90020efb4 | |||
| eaa434e518 | |||
| 2c4da62525 | |||
| 84aa2c5b1b | |||
| 532027f7a6 | |||
| 7622329499 | |||
| f9f8f21918 | |||
| 723b84a044 | |||
| b2c79e44d8 | |||
| b5b2701591 | |||
| fa6243a283 | |||
| 5f743c46b7 | |||
| f4e6dbd1d0 | |||
| ffc289d7d8 | |||
| ac27a36f76 | |||
| da75341655 | |||
| 244f8d81cf | |||
| ae52813f2b | |||
| 8b6604dc63 | |||
| 47c5f081d4 | |||
| 157b53cb34 | |||
| 452da45fac | |||
| 924ed1b57d | |||
| 11aa1db4a4 | |||
| 0f92dda7eb | |||
| 2493b2f8c4 | |||
| 96aae9c316 | |||
| 9134b3c73e | |||
| 11de8516a1 | |||
| 68033b4357 | |||
| ed5bed8f00 | |||
| dcab94fcd7 | |||
| b86ae1b6b4 | |||
| 1fc1497af3 | |||
| ba8cca8fd7 | |||
| aa3ca73268 | |||
| be90b0ec6d | |||
| 57efe54fbd | |||
| b82fd0b769 | |||
| b5853aec6c | |||
| 3c66261621 | |||
| 5f8a58956f | |||
| ad39519794 | |||
| 98c7b65864 | |||
| 5675da8ee9 | |||
| b77d6f2df9 | |||
| 24e1e79cde | |||
| eaf1fcf436 | |||
| 5cdff8906e | |||
| dd6ab191aa | |||
| bf818a3fc0 | |||
| 69ac6ffabd | |||
| 7710849823 | |||
| 90820291dc | |||
| e1e7b4fceb | |||
| 0b25fe582c | |||
| 06bdfc1c13 | |||
| 9f47145da7 | |||
| 73056666a6 | |||
| 6d72c526bf | |||
| 059d9654c1 | |||
| d747b2f970 | |||
| 8b023a06be | |||
| 906cb69d6f | |||
| 82983c2e63 | |||
| 9e3d118f54 | |||
| 03e3bee095 | |||
| 9f1b47d689 | |||
| bdca59e7a0 | |||
| e931576723 | |||
| 539f52cc0a | |||
| 7808c5f286 | |||
| 39118d67a9 | |||
| f63a15bc68 | |||
| b1315a3de9 | |||
| 517471d0d3 | |||
| 4c4dbe4121 | |||
| e0fb4ef757 | |||
| c72062a96a | |||
| 61f8fbcbfe | |||
| 0e2b0345eb | |||
| d05b24fb5e | |||
| af6528b420 | |||
| a7ede4efab | |||
| 7f7358f918 | |||
| b9758637c8 | |||
| 709191218c | |||
| cddc4db061 | |||
| 453b257a7f | |||
| 6224363182 | |||
| 4b72c3e257 | |||
| b96d239c16 | |||
| 0b9d8600b5 | |||
| b3cf2ff2af | |||
| 1db9bf54f0 | |||
| 3faf15f08c | |||
| 9e3dc7848b |
+3
-8
@@ -1,14 +1,9 @@
|
||||
# cargo-mutants (docs/mutation-testing.md). Mutants are judged by the black-box suites that will
|
||||
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test)
|
||||
# and the pull-request sample of the robustness goldens (every 8th seed, as ci.yml). The
|
||||
# kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
|
||||
# outlive the kernel backend: the reference tests (tests/oracle, through the oracle_cases test).
|
||||
# The kernel's own unit tests are left out on purpose: a mutant only they catch is a behaviour the
|
||||
# backend-neutral suites would miss when the kernel is swapped.
|
||||
profile = "mutants"
|
||||
additional_cargo_test_args = [
|
||||
"--test", "oracle_cases", "--lib", "--", "--include-ignored",
|
||||
"--test", "oracle_cases", "--", "--include-ignored",
|
||||
"oracle_suite",
|
||||
"robustness_goldens::seed_00", "robustness_goldens::seed_08", "robustness_goldens::seed_16",
|
||||
"robustness_goldens::seed_24", "robustness_goldens::seed_32", "robustness_goldens::seed_40",
|
||||
"robustness_goldens::seed_48", "robustness_goldens::seed_56", "robustness_goldens::seed_64",
|
||||
"robustness_goldens::seed_72", "robustness_goldens::seed_80", "robustness_goldens::seed_88",
|
||||
]
|
||||
|
||||
@@ -12,7 +12,6 @@ not (
|
||||
(package(solidcraft-engine) & (
|
||||
test(=tests::hostile_params_never_panic)
|
||||
| test(/^tests::robustness::/)
|
||||
| test(/^tests::robustness_goldens::seed_/)
|
||||
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
|
||||
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
|
||||
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
|
||||
@@ -44,13 +43,12 @@ 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.
|
||||
# core while the others sit idle. Medians from the CI test timings (the junit artifacts), slowest
|
||||
# first; a test over ~100 s belongs here.
|
||||
[[profile.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::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)
|
||||
@@ -92,14 +90,13 @@ 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))"
|
||||
filter = "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::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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!-- Title: which part of the app changes and what a user now sees or can do, clear to someone who hasn't seen your chat, the issue or the code. -->
|
||||
|
||||
## Change
|
||||
|
||||
Describe the problem and resulting behavior. Link the issue when applicable.
|
||||
Start with what this is, in plain words someone new to the project would understand: what it does and who it's for. Don't assume anyone knows the name of your tool, project or feature. Then describe the problem and resulting behavior. Link the issue when applicable.
|
||||
|
||||
Changes a default: no
|
||||
|
||||
@@ -14,6 +16,8 @@ What did you run, and what were the results? Include expected and measured value
|
||||
|
||||
## Screenshots of the behavior (when observable)
|
||||
|
||||
Put the picture or video here in the description. A file in the diff doesn't count: a maintainer has to be able to see what this is without opening the changed files.
|
||||
|
||||
For observable CAD, UI or model behavior, include actual descriptive screenshots or a short recording from a small reproduction. This includes kernel, geometry, assembly and backend effects that can be shown in the app. Coding agents should run the app and capture the evidence themselves, rather than asking a human for evidence they can produce. Show the actual failure/result, preferably before and after, with brief steps, expected/actual behavior and annotations where helpful.
|
||||
|
||||
Flowcharts and Mermaid diagrams cannot substitute for evidence of visible behavior. Use diagrams for architectural reasoning when actual screenshots cannot illustrate the point. Changes with no useful observable behavior may omit screenshot/video evidence. Exclude private designs and sensitive details; attach media or link to an accessible recording rather than committing generated review artifacts.
|
||||
|
||||
+50
-53
@@ -33,6 +33,8 @@
|
||||
# `ci` job still runs and reports the commit's real result (see the `ci` job): the merge queue
|
||||
# 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.
|
||||
# The release gate (release-gate.yml) calls this workflow (workflow_call) as a Run workflow with
|
||||
# cross-compilation on.
|
||||
# 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 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);
|
||||
@@ -50,10 +52,14 @@ on:
|
||||
# only (other labels are skipped, see the top of this file).
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review, labeled]
|
||||
# The kernel-smlib branch (ADR 0002) runs no CI for now (owner, 2026-10-21). Drop this
|
||||
# line before the branch merges back into main.
|
||||
branches-ignore: [kernel-smlib]
|
||||
# The merge queue: each queued group runs the full suite before it lands on main.
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main]
|
||||
# develop: the SMLib kernel's trunk until it merges into main (its PRs land there).
|
||||
branches: [main, develop]
|
||||
schedule:
|
||||
- cron: "17 6 * * *"
|
||||
workflow_dispatch:
|
||||
@@ -62,6 +68,14 @@ on:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
# The release gate (release-gate.yml): the event stays the caller's (workflow_dispatch), so a
|
||||
# call runs exactly what Run workflow runs.
|
||||
workflow_call:
|
||||
inputs:
|
||||
cross_compile:
|
||||
description: "Also check WebAssembly and RISC-V (before a release)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -117,7 +131,6 @@ jobs:
|
||||
outputs:
|
||||
native: ${{ steps.paths.outputs.native }}
|
||||
kernel: ${{ steps.paths.outputs.kernel }}
|
||||
goldens: ${{ steps.paths.outputs.goldens }}
|
||||
windows: ${{ steps.paths.outputs.windows }}
|
||||
# The GPU path tests run (see the rule where it is computed). The job's `if` and the `ci` job's check both read this, so they cannot disagree.
|
||||
gpu: ${{ steps.paths.outputs.gpu }}
|
||||
@@ -156,7 +169,6 @@ jobs:
|
||||
run: |
|
||||
native=false
|
||||
kernel=false
|
||||
goldens=false
|
||||
windows=false
|
||||
gpu_paths=false
|
||||
deps=false
|
||||
@@ -238,12 +250,12 @@ jobs:
|
||||
if [ "${tested:-0}" -gt 0 ]; then
|
||||
echo "The merge queue already tested $PUSH_SHA: repository checks only."
|
||||
elif tested=$(curl -fsS -H "Authorization: Bearer $GH_TOKEN" -H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=main&status=success&per_page=1" \
|
||||
"https://api.github.com/repos/$REPO/actions/workflows/ci.yml/runs?event=push&branch=$GITHUB_REF_NAME&status=success&per_page=1" \
|
||||
| grep -o '"head_sha": *"[0-9a-f]*"' | head -n 1 | grep -o '[0-9a-f]\{40\}') \
|
||||
&& [ -n "$tested" ] && git merge-base --is-ancestor "$tested" "$PUSH_SHA" \
|
||||
&& git diff --no-renames --name-only -z "$tested" "$PUSH_SHA" > "$RUNNER_TEMP/ci-paths" \
|
||||
&& select_packages "$RUNNER_TEMP/ci-paths" && [ "$full" != true ] && [ -z "$packages" ]; then
|
||||
# Nothing a build reads changed since the last push to main whose run passed (the
|
||||
# Nothing a build reads changed since the last push to this branch whose run passed (the
|
||||
# push before may never have run: a newer push cancels a waiting one).
|
||||
echo "$tested..$PUSH_SHA changes no code: repository checks only."
|
||||
else
|
||||
@@ -282,13 +294,6 @@ jobs:
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
|
||||
while IFS= read -r -d '' path; do
|
||||
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
|
||||
# What a pull request re-checks the goldens for: the kernel, truck and the goldens
|
||||
# themselves (a re-record is checked before it is queued). The goldens are frozen, so
|
||||
# on any other change they would only re-check unchanged results; the merge queue,
|
||||
# main and the nightly run every seed before anything lands.
|
||||
case "$path" in
|
||||
crates/kernel/*|vendor/truck-*|tests/robustness/*) goldens=true ;;
|
||||
esac
|
||||
done < "$RUNNER_TEMP/ci-paths"
|
||||
# Computed as for a merge group, then turned off below (the fast lane).
|
||||
windows_paths "$RUNNER_TEMP/ci-paths" "$BASE_SHA...$HEAD_SHA"
|
||||
@@ -300,10 +305,6 @@ jobs:
|
||||
# selected means no code changed.
|
||||
select_packages "$RUNNER_TEMP/ci-paths"
|
||||
if [ "$full" = true ] || [ -n "$packages" ]; then native=true; fi
|
||||
# The goldens are solidcraft-engine tests: its tests must be in the archive.
|
||||
if [ "$goldens" = true ] && [ "$full" != true ]; then
|
||||
case " $packages " in *" solidcraft-engine "*) ;; *) packages="$packages solidcraft-engine" ;; esac
|
||||
fi
|
||||
fi
|
||||
# The reference tests replay recipes through solidcraft-cli: needed when it, or anything
|
||||
# it depends on, is selected.
|
||||
@@ -318,13 +319,12 @@ jobs:
|
||||
doctests=$native
|
||||
if [ "$EVENT_NAME" = pull_request ]; then windows=false; oracle=false; doctests=false; fi
|
||||
# Until a maintainer adds approved-for-ci, a pull request gets a quick compile instead of
|
||||
# Clippy, the tests, the doc tests and the goldens.
|
||||
# Clippy, the tests and the doc tests.
|
||||
quick=false
|
||||
if [ "$EVENT_NAME" = pull_request ] && [ "$APPROVED" != true ]; then
|
||||
quick=$native
|
||||
native=false
|
||||
oracle=false
|
||||
goldens=false
|
||||
fi
|
||||
# A push that only merges main into the change (Merge branch 'main', Update branch) when
|
||||
# the change itself already passed: nothing new to test here, and main tests the merged
|
||||
@@ -337,7 +337,6 @@ jobs:
|
||||
if [ -n "$carried" ]; then
|
||||
echo "Only a merge of main into a change that passed in $carried: repository checks only."
|
||||
native=false
|
||||
goldens=false
|
||||
fi
|
||||
fi
|
||||
# The GPU path tests (their own job, about 56 runner-minutes) are for render changes only:
|
||||
@@ -363,7 +362,6 @@ jobs:
|
||||
echo "native=$native"
|
||||
echo "gpu=$gpu"
|
||||
echo "kernel=$kernel"
|
||||
echo "goldens=$goldens"
|
||||
echo "windows=$windows"
|
||||
echo "oracle=$oracle"
|
||||
echo "doctests=$doctests"
|
||||
@@ -389,7 +387,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: repo-checks
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Format
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Asset attribution
|
||||
@@ -409,6 +407,10 @@ jobs:
|
||||
# New UI text must reach every complete catalog; report it here, before review (#8542).
|
||||
- name: Translations in the complete catalogs
|
||||
run: cargo xtask i18n --check
|
||||
- name: Model decisions use exact geometry, not meshes
|
||||
run: |
|
||||
cargo xtask mesh-decisions
|
||||
cargo test -p xtask mesh_decisions --locked
|
||||
- name: Agent router and skills cite real files
|
||||
run: |
|
||||
cargo xtask skills
|
||||
@@ -439,7 +441,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: quick-compile
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Cargo check
|
||||
run: cargo check --workspace --all-targets --locked
|
||||
|
||||
@@ -458,7 +460,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-clippy
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Clippy
|
||||
env:
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
@@ -501,7 +503,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -572,7 +574,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -583,13 +585,8 @@ jobs:
|
||||
- name: Run tests
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
GOLDENS: ${{ needs.changes.outputs.goldens }}
|
||||
FULL: ${{ needs.changes.outputs.full }}
|
||||
PACKAGES: ${{ needs.changes.outputs.packages }}
|
||||
# The robustness golden seeds a pull request skips: each takes one to twenty minutes under
|
||||
# CI load, together about four fifths of the goldens' time. Pushes to main, the merge
|
||||
# queue and the nightly run every seed.
|
||||
SLOW_SEEDS: 81|45|20|24|95|02|57|40|18|48|32|46|85|61|77|34|14
|
||||
run: |
|
||||
# Pull requests skip the heavy tests (default-filter in .config/nextest.toml); `default()`
|
||||
# below is that filter, so a pull request still runs only what it ran before.
|
||||
@@ -603,29 +600,23 @@ jobs:
|
||||
# The boolean property tests draw new random inputs every run; they run only in the
|
||||
# nightly property-tests job, so a known kernel bug cannot turn main red at random.
|
||||
base='not (package(solidcraft-kernel) & binary(boolean_props))'
|
||||
elif [ "$GOLDENS" = true ]; then
|
||||
# A pull request that can change a robustness golden (tests/robustness/goldens.json)
|
||||
# also checks every seed but the slow ones, in the same run so they share its cores.
|
||||
base="(default() & not (binary(action_matrix) & test(=action_matrix_sample))) | (package(solidcraft-engine) & test(/^tests::robustness_goldens::/) & not test(/^tests::robustness_goldens::seed_($SLOW_SEEDS)\$/))"
|
||||
fi
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
# Pull requests test the affected packages (scripts/ci/affected.py).
|
||||
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.
|
||||
# A failed test does not fail the step by itself: the tests known to fail today
|
||||
# (tests/KNOWN_FAILING_TESTS) may fail, any other fails it, and so does a listed test
|
||||
# that passes. nextest's exit code 100 is "tests failed"; any other is its own error
|
||||
# (a build error, a bad filter) and fails the step at once.
|
||||
status=0
|
||||
cargo nextest run "${scope[@]}" --cargo-profile ci --locked --profile ci --no-tests=pass \
|
||||
--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
|
||||
echo "::error::A robustness golden changed. If the new result is intended, re-record it in this pull request and say why:"
|
||||
echo "::error::ROBUSTNESS_GOLDENS_BLESS=\"why it changed\" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens:: (see docs/robustness.md)"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
--ignore-default-filter -E "($base) & not (package(=solidcraft-gpu) & test(/^path::/))" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
@@ -699,7 +690,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: native-gpu
|
||||
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 1 }}
|
||||
save-if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && matrix.shard == 1 }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -714,8 +705,14 @@ jobs:
|
||||
# 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.
|
||||
# Graded against tests/KNOWN_FAILING_TESTS, as in Native Linux tests.
|
||||
junit=${CARGO_TARGET_DIR:-target}/nextest/ci/junit.xml
|
||||
rm -f "$junit"
|
||||
status=0
|
||||
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"
|
||||
--ignore-default-filter -E 'test(/^path::/)' --partition "count:$SHARD/4" || status=$?
|
||||
if [ "$status" -ne 0 ] && [ "$status" -ne 100 ]; then exit "$status"; fi
|
||||
cargo xtask known-failing --junit "$junit"
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
@@ -849,7 +846,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: doc-tests
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -907,7 +904,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: oracle
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
@@ -946,7 +943,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-fxc
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
# One test at a time: WARP's shader JIT crashes now and then (a null read in
|
||||
# d3d10warp.dll's JIT optimizer, about 1 run in 60) when several devices compile compute
|
||||
# pipelines at once, which the parallel tests do. Minidumps showed it in WARP itself, not
|
||||
@@ -970,7 +967,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: windows-startup
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Backend order and probing on Windows
|
||||
run: cargo test -p solidcraft --profile ci --locked -- startup::tests native_crash::tests native_frame::tests
|
||||
- name: Build the app
|
||||
@@ -1107,7 +1104,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: wasm
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- run: cargo xtask wasm
|
||||
|
||||
riscv64-gates:
|
||||
@@ -1129,7 +1126,7 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: riscv64
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Install cross-compilation dependencies
|
||||
run: |
|
||||
# Ubuntu's regular archive serves amd64; RISC-V packages live in ports.
|
||||
@@ -1145,7 +1142,7 @@ jobs:
|
||||
EOF
|
||||
sudo dpkg --add-architecture riscv64
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
sudo apt-get install -y gcc-riscv64-linux-gnu g++-riscv64-linux-gnu libasound2-dev:riscv64 libxkbcommon-dev:riscv64 \
|
||||
libwayland-dev:riscv64 libx11-dev:riscv64 libxrandr-dev:riscv64 libxi-dev:riscv64 libgl1-mesa-dev:riscv64
|
||||
- name: Cargo check on RISC-V
|
||||
env:
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
# The fillet rig as a regression gate: a pull request that touches fillet code (the paths below)
|
||||
# runs every case in tests/kernel-cases/fillets/ through one release build of
|
||||
# solidcraft-cli, and fails on a regression (a case that fails and
|
||||
# tests/kernel-cases/fillets/KNOWN_FAILING does not list) or on a listed case that now passes
|
||||
# (it is fixed: remove its line, so the list only shrinks). See tests/kernel-cases/README.md.
|
||||
#
|
||||
# GitHub filters a workflow by whole files, so the fillet parts of crates/doc/src/eval*.rs and
|
||||
# crates/engine/src/cmd/features.rs select it whenever those files change. Pushes to main and
|
||||
# develop that touch the same paths run it too and save the compiler cache (a Blacksmith sticky
|
||||
# disk) that pull requests then mount read-only.
|
||||
name: Fillet rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/src/blend*.rs"
|
||||
- "crates/kernel/src/fillet*.rs"
|
||||
- "crates/kernel/src/*corner*.rs"
|
||||
- "crates/kernel/src/continuity*.rs"
|
||||
- "crates/smlib/**"
|
||||
- "crates/doc/src/eval*.rs"
|
||||
- "crates/engine/src/cmd/features.rs"
|
||||
- "tests/kernel-cases/fillets/**"
|
||||
# The rig itself: the grading, the runner and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- ".github/workflows/fillet-rig.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: fillet-rig-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
fillets:
|
||||
name: Fillet rig
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only main and develop commit the cache; pull requests and forks mount a clone and never
|
||||
# write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-fillet-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: fillet-rig
|
||||
save-if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release)
|
||||
run: cargo build --release --locked -p solidcraft-cli
|
||||
- name: Run the fillet cases against the known-failing list
|
||||
run: >-
|
||||
cargo run -q --locked -p xtask -- kernel-cases --area fillets
|
||||
--cli target/release/solidcraft-cli --out "$RUNNER_TEMP/fillet-rig"
|
||||
--known-failing tests/kernel-cases/fillets/KNOWN_FAILING
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: fillet-rig-results
|
||||
path: ${{ runner.temp }}/fillet-rig/results.json
|
||||
if-no-files-found: ignore
|
||||
@@ -1,6 +1,7 @@
|
||||
# While a new geometry kernel is evaluated, new issues, pull requests and discussions about kernel
|
||||
# work are labelled `kernel-paused` for a maintainer to answer (CONTRIBUTING.md, "Kernel work is
|
||||
# paused"). The logic is in scripts/kernel-paused/ (tested in ci.yml).
|
||||
# While a new geometry kernel is evaluated, new issues and discussions about kernel work, and new
|
||||
# pull requests that change the kernel directories, are labelled `kernel-paused` for a maintainer to
|
||||
# answer (CONTRIBUTING.md, "Kernel work is paused"). The logic is in scripts/kernel-paused/ (tested
|
||||
# in ci.yml).
|
||||
name: Kernel paused
|
||||
|
||||
on:
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
# The kernel rig and the complexity ladder as a regression gate. A plan job maps a pull request's
|
||||
# changed files to the rig areas they can affect (scripts/ci/kernel_rig_plan.py): an area runs
|
||||
# when a file its tests/kernel-cases/<area>/PATHS lists changes, or one of its cases; every area
|
||||
# runs when the shared core in tests/kernel-cases/PATHS-ALL changes, and when the solidcore pin in
|
||||
# the root Cargo.toml and Cargo.lock moves (the plan reads their diff); the ladder runs whenever any
|
||||
# area does. One job then builds solidcraft-cli once (release), checks that every
|
||||
# registry command has a case or an exemption (tests/kernel-cases/EXEMPT), and hands the CLI to a
|
||||
# matrix of shards as an artifact, one per selected area plus the ladder, in parallel, so the
|
||||
# wall time is the build plus the slowest shard. See tests/kernel-cases/README.md.
|
||||
#
|
||||
# - An area fails on a regression (a case that fails and its tests/kernel-cases/<area>/KNOWN_FAILING
|
||||
# does not list) or on a listed case that passes (it is fixed: remove its line in the same pull
|
||||
# request, so the list only shrinks).
|
||||
# - The ladder fails on any case whose status or counts differ from
|
||||
# tests/kernel-cases/ladder/golden.json; a right change is re-recorded (`--record "why"`).
|
||||
#
|
||||
# The trigger paths are whole folders that hold every area's PATHS (a self-test,
|
||||
# scripts/ci/test_kernel_rig_plan.py, checks they do), so a pull request that touches none of
|
||||
# them (the UI, docs, packaging) starts no runner at all, and a new area needs no edit here: the
|
||||
# plan finds the areas from the folders. Pushes to main and develop on the same paths, merge groups
|
||||
# (once the queue is on), the release gate (release-gate.yml, workflow_call) and a run by hand run
|
||||
# every area; those pushes also commit the compiler cache (a Blacksmith sticky disk) that every
|
||||
# other run mounts read-only.
|
||||
name: Kernel rig
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
push:
|
||||
branches: [main, develop]
|
||||
# The same paths as pull_request.
|
||||
paths:
|
||||
- "crates/kernel/**"
|
||||
- "crates/geom/**"
|
||||
- "crates/smlib/**"
|
||||
- "crates/step/**"
|
||||
- "crates/io/src/**"
|
||||
- "crates/doc/src/**"
|
||||
- "crates/engine/src/**"
|
||||
- "tests/kernel-cases/**"
|
||||
# The solidcore pin (the plan runs every area only when its lines change).
|
||||
- "Cargo.toml"
|
||||
- "Cargo.lock"
|
||||
# The rig itself: the grading, the runner, the selection and this workflow.
|
||||
- "apps/solidcraft-cli/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_cases.rs"
|
||||
- "xtask/src/kernel_ladder.rs"
|
||||
- "scripts/ci/kernel_rig_plan.py"
|
||||
- "scripts/ci/test_kernel_rig_plan.py"
|
||||
- ".github/workflows/kernel-rig.yml"
|
||||
merge_group:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
# A new push to a pull request replaces its old run.
|
||||
group: kernel-rig-${{ github.event_name }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
plan:
|
||||
name: Select the rig areas
|
||||
# Draft pull requests run no CI, as in ci.yml.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.draft
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
# The shard matrix (JSON list of areas and the ladder; [] when nothing is selected).
|
||||
shards: ${{ steps.plan.outputs.shards }}
|
||||
# Whether the build job, and with it the coverage check, runs.
|
||||
build: ${{ steps.plan.outputs.build }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
# A pull request's diff needs its base's history.
|
||||
fetch-depth: ${{ github.event_name == 'pull_request' && '0' || '1' }}
|
||||
- name: Selection self-test
|
||||
run: python3 -I scripts/ci/test_kernel_rig_plan.py
|
||||
- id: plan
|
||||
name: Map the changed files to areas
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Pull requests run the areas their diff reaches; pushes, merge groups, the release
|
||||
# gate and a run by hand run every area.
|
||||
if [ "$EVENT_NAME" = pull_request ]; then
|
||||
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/rig-paths"
|
||||
git diff --no-renames "$BASE_SHA...$HEAD_SHA" -- Cargo.toml Cargo.lock > "$RUNNER_TEMP/rig-pin.diff"
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --paths "$RUNNER_TEMP/rig-paths" --pin-diff "$RUNNER_TEMP/rig-pin.diff" \
|
||||
--github-output "$GITHUB_OUTPUT"
|
||||
else
|
||||
python3 -I scripts/ci/kernel_rig_plan.py --all --github-output "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build the CLI
|
||||
needs: plan
|
||||
if: needs.plan.outputs.build == 'true'
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: sccache
|
||||
SCCACHE_DIR: /home/runner/.cache/sccache
|
||||
SCCACHE_CACHE_SIZE: 10G
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Size parallel compiles to the job's memory
|
||||
run: scripts/ci/build-jobs.sh
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
# Only pushes to main and develop commit the cache; pull requests, merge groups and forks
|
||||
# mount a clone and never write back.
|
||||
- name: Mount the compiler cache
|
||||
uses: useblacksmith/stickydisk@3f6be1451e3cad893b778bc93495c80452a5da26 # v1.7.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
key: ${{ github.repository }}-sccache-kernel-rig
|
||||
path: /home/runner/.cache/sccache
|
||||
commit: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && 'on-change' || 'false' }}
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||
with:
|
||||
key: kernel-rig
|
||||
save-if: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') }}
|
||||
- name: Set up compiler cache
|
||||
uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
|
||||
with:
|
||||
version: v0.16.0
|
||||
- name: Build the CLI (release) and the rig's runner
|
||||
run: |
|
||||
cargo build --release --locked -p solidcraft-cli
|
||||
cargo build --locked -p xtask
|
||||
# A tar keeps the executable bits, which an artifact drops. xtask finds the repository from
|
||||
# the path it was built in (CARGO_MANIFEST_DIR), which is the same checkout path in every
|
||||
# job of this workflow.
|
||||
# Every registry command is used by a case or listed in tests/kernel-cases/EXEMPT.
|
||||
- name: Every command has a case
|
||||
run: target/debug/xtask kernel-cases --coverage --cli target/release/solidcraft-cli
|
||||
# A coverage-only run (the registry or the cases changed, no area selected) runs no shard.
|
||||
- name: Pack the binaries
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
run: tar -cf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target release/solidcraft-cli debug/xtask
|
||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}/kernel-rig-bin.tar
|
||||
retention-days: 1
|
||||
- name: Compiler cache statistics
|
||||
if: always()
|
||||
run: sccache --show-stats
|
||||
|
||||
shard:
|
||||
name: Kernel rig (${{ matrix.shard }})
|
||||
needs: [plan, build]
|
||||
if: needs.plan.outputs.shards != '[]'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# The areas of tests/kernel-cases/ the plan selected, and the ladder. On the build box
|
||||
# (aarch64, 4 cases at a time) the slowest areas took about 1 minute (booleans, 212 s of
|
||||
# cases with one of 84 s; threads, 161 s) and the ladder, one case at a time, 92 s: no area
|
||||
# needs splitting.
|
||||
shard: ${{ fromJSON(needs.plan.outputs.shards) }}
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
||||
with:
|
||||
name: kernel-rig-bin
|
||||
path: ${{ runner.temp }}
|
||||
- name: Unpack the binaries
|
||||
run: mkdir -p target && tar -xf "$RUNNER_TEMP/kernel-rig-bin.tar" -C target
|
||||
- name: Run the cases against the known-failing list (the ladder against its golden)
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
run: |
|
||||
if [ "$SHARD" = ladder ]; then
|
||||
set -- --ladder --gate
|
||||
else
|
||||
set -- --area "$SHARD" --known-failing "tests/kernel-cases/$SHARD/KNOWN_FAILING"
|
||||
fi
|
||||
target/debug/xtask kernel-cases "$@" --cli target/release/solidcraft-cli --out "$RUNNER_TEMP/kernel-rig"
|
||||
- name: Keep the results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: kernel-rig-results-${{ matrix.shard }}
|
||||
path: ${{ runner.temp }}/kernel-rig/**/results.json
|
||||
if-no-files-found: ignore
|
||||
|
||||
# One check for the whole gate, for branch protection and the merge queue.
|
||||
kernel-rig:
|
||||
name: kernel-rig
|
||||
needs: [plan, build, shard]
|
||||
if: always() && (github.event_name != 'pull_request' || !github.event.pull_request.draft)
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
# A skipped build or shard counts as passed only when the plan selected none.
|
||||
- name: Every selected job passed
|
||||
env:
|
||||
PLAN: ${{ needs.plan.result }}
|
||||
WANT_BUILD: ${{ needs.plan.outputs.build }}
|
||||
WANT_SHARDS: ${{ needs.plan.outputs.shards }}
|
||||
BUILD: ${{ needs.build.result }}
|
||||
SHARDS: ${{ needs.shard.result }}
|
||||
run: |
|
||||
echo "plan=$PLAN build=$BUILD shards=$SHARDS selected=$WANT_SHARDS"
|
||||
test "$PLAN" = success
|
||||
if [ "$WANT_BUILD" = true ]; then test "$BUILD" = success; else test "$BUILD" = skipped; fi
|
||||
if [ "$WANT_SHARDS" != '[]' ]; then test "$SHARDS" = success; else test "$SHARDS" = skipped; fi
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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).
|
||||
# Three quick checks on every pull request (CONTRIBUTING.md, "Sending a change"): no second open PR
|
||||
# for an issue a lower-numbered one already closes, the "Changes a default:" line is filled in, and
|
||||
# a PR adding to the showcase shows a picture. None is a required status check. The logic is in scripts/pr-checks/pr-checks.js (tested in ci.yml).
|
||||
name: PR checks
|
||||
|
||||
on:
|
||||
@@ -48,6 +48,21 @@ jobs:
|
||||
const r = await checks.checkDefaults({ github, context, core });
|
||||
core.info(JSON.stringify(r));
|
||||
|
||||
picture:
|
||||
name: Picture 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.checkPicture({ 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:
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# The release gate: everything, in full and in parallel, on one branch's head, before that branch
|
||||
# replaces main as a release (develop, rebased onto main). Run it by hand:
|
||||
# gh workflow run release-gate.yml --ref develop
|
||||
# It calls the workflows that own each check instead of copying their jobs:
|
||||
# - ci.yml as Run workflow with cross-compilation: every native suite in full (not scoped), the
|
||||
# reference tests, the doc tests, the GPU path tests, the action matrix (all 16 shards) and its
|
||||
# determinism check, the property tests, the repository checks (web-offline among them),
|
||||
# WebAssembly and RISC-V, Windows D3D12 FXC and Windows app startup;
|
||||
# - kernel-rig.yml: every rig area against its KNOWN_FAILING and the ladder against its golden;
|
||||
# - release.yml as a dry run (draft false, packages_only): the Linux, Flatpak, Snap, FreeBSD,
|
||||
# macOS, Windows and web packages with their smoke tests and package checks, unsigned (no
|
||||
# secrets are passed, so no signing secret or SignPath quota is used) and without the release
|
||||
# job (checksums, update signing, publishing).
|
||||
# The gate passes only when all three pass. Before main is replaced, tag the old main
|
||||
# (main-before-develop-<date>) and push the tag.
|
||||
name: Release gate
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-gate-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
name: CI
|
||||
uses: ./.github/workflows/ci.yml
|
||||
permissions:
|
||||
contents: read
|
||||
# Select checks reads this commit's other runs.
|
||||
actions: read
|
||||
with:
|
||||
cross_compile: true
|
||||
|
||||
kernel-rig:
|
||||
name: Kernel rig
|
||||
uses: ./.github/workflows/kernel-rig.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
packages:
|
||||
name: Packages
|
||||
uses: ./.github/workflows/release.yml
|
||||
# No secrets: the signing steps skip without them. GitHub checks a called workflow's job
|
||||
# permissions before the run, skipped jobs included, so the release job's contents: write must
|
||||
# be granted here; packages_only skips that job, and no step that runs writes to the
|
||||
# repository. actions: read is the Windows job's.
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
with:
|
||||
draft: false
|
||||
packages_only: true
|
||||
|
||||
release-gate:
|
||||
name: release-gate
|
||||
needs: [ci, kernel-rig, packages]
|
||||
if: always()
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Every part passed
|
||||
env:
|
||||
CI: ${{ needs.ci.result }}
|
||||
KERNEL_RIG: ${{ needs.kernel-rig.result }}
|
||||
PACKAGES: ${{ needs.packages.result }}
|
||||
run: |
|
||||
echo "ci=$CI kernel-rig=$KERNEL_RIG packages=$PACKAGES"
|
||||
test "$CI" = success
|
||||
test "$KERNEL_RIG" = success
|
||||
test "$PACKAGES" = success
|
||||
@@ -21,6 +21,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.
|
||||
# The release gate (release-gate.yml) calls it as such a dry run, without secrets (unsigned) and
|
||||
# without the release job (packages_only).
|
||||
# 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
|
||||
@@ -41,6 +43,25 @@ on:
|
||||
description: "SignPath signing policy for the Windows files (empty: the repository variable, else test-signing)"
|
||||
type: string
|
||||
default: ""
|
||||
# The release gate (release-gate.yml): a dry run (draft false) of every package and its checks.
|
||||
# The event stays the caller's (workflow_dispatch), so a call runs as a dispatched dry run does.
|
||||
# The gate passes no secrets, so nothing is signed (the signing steps skip without their
|
||||
# secrets, as on a fork), and packages_only skips the release job (checksums, update signing,
|
||||
# publishing).
|
||||
workflow_call:
|
||||
inputs:
|
||||
packages_only:
|
||||
description: "Build and check every package only: no release job"
|
||||
type: boolean
|
||||
default: false
|
||||
draft:
|
||||
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
|
||||
@@ -213,6 +234,8 @@ jobs:
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# makepkg only repackages the tarball's prebuilt binaries, so both architectures build here.
|
||||
runs-on: ubuntu-24.04
|
||||
# A pre-release or nightly without an Arch package is still worth having; a stable release isn't.
|
||||
continue-on-error: ${{ contains(needs.version.outputs.version, '-') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -229,10 +252,29 @@ jobs:
|
||||
# makepkg in an archlinux:base-devel container. The package is named after the tarball's
|
||||
# .desktop file (io.tangencylabs.solidcraft) and written as
|
||||
# dist/release/solidcraft-<version>-linux-<arch>.pkg.tar.zst.
|
||||
# An Arch version can't hold a hyphen, and the action reads the version from the tarball's name
|
||||
# (a hyphenated one comes out as 0.0.0): 0.40.0-rc.1 goes in as 0.40.0rc.1, which pacman sorts
|
||||
# before 0.40.0, and the package keeps the release's name.
|
||||
- id: pkgver
|
||||
name: Arch version
|
||||
run: |
|
||||
pkgver="${VERSION//-/}"
|
||||
echo "pkgver=$pkgver" >>"$GITHUB_OUTPUT"
|
||||
if [ "$pkgver" != "$VERSION" ]; then
|
||||
cp "dist/linux/solidcraft-$VERSION-linux-$ARCH.tar.gz" "dist/linux/solidcraft-$pkgver-linux-$ARCH.tar.gz"
|
||||
fi
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
- uses: storytold/craft-common-actions/actions/arch-package@fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 # main
|
||||
with:
|
||||
tarball: dist/linux/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.tar.gz
|
||||
tarball: dist/linux/solidcraft-${{ steps.pkgver.outputs.pkgver }}-linux-${{ matrix.arch }}.tar.gz
|
||||
arch: ${{ matrix.arch }}
|
||||
- name: The package under the release's version
|
||||
if: steps.pkgver.outputs.pkgver != env.VERSION
|
||||
env:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
PKGVER: ${{ steps.pkgver.outputs.pkgver }}
|
||||
run: sudo mv "dist/release/solidcraft-$PKGVER-linux-$ARCH.pkg.tar.zst" "dist/release/solidcraft-$VERSION-linux-$ARCH.pkg.tar.zst"
|
||||
- name: Check the package
|
||||
env:
|
||||
PKG: dist/release/solidcraft-${{ env.VERSION }}-linux-${{ matrix.arch }}.pkg.tar.zst
|
||||
@@ -350,7 +392,7 @@ jobs:
|
||||
name: macOS build ${{ matrix.arch }}
|
||||
needs: version
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -384,7 +426,7 @@ jobs:
|
||||
name: macOS universal
|
||||
needs: [version, macos-build]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
env:
|
||||
SOLIDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||
SOLIDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }}
|
||||
@@ -612,7 +654,8 @@ jobs:
|
||||
release:
|
||||
name: GitHub Release or dry-run artifacts
|
||||
needs: [version, linux, flatpak, arch, snap, freebsd, macos, windows, web]
|
||||
if: needs.version.outputs.build == 'true'
|
||||
# Not for the release gate (packages_only): it builds and checks the packages, nothing more.
|
||||
if: needs.version.outputs.build == 'true' && !inputs.packages_only
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -10,6 +10,7 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- 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`.
|
||||
- Model decisions (what an operation does, which bodies it touches, which face or edge a point or name means, depths, nearness) use exact geometry; a mesh is for display, export, previews and mesh bodies (`xtask/mesh-decisions.txt` only shrinks): `cargo xtask mesh-decisions`.
|
||||
- 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`.
|
||||
@@ -22,6 +23,7 @@ Work: ROADMAP.md, "Where help is wanted most" in CONTRIBUTING.md, issues labelle
|
||||
- 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`.
|
||||
- A PR's title and first sentence make sense to someone who never saw your chat or the issue: `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.
|
||||
|
||||
|
||||
+159
-9
@@ -84,6 +84,8 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| docs/book/src/generated/tool-preview.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/tool-preview.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
|
||||
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-sc/CraftUI-SC.otf | Adobe / Google Inc. (Noto Sans SC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/SC Regular, glyph-subset for assets/locales/zh.json and renamed Craft UI SC (OFL reserved-name); licence text in assets/fonts/craft-ui-sc/OFL.txt; web UI Simplified Chinese | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-tc/CraftUI-TC.otf | Adobe / Google Inc. (Noto Sans TC); SolidCraft subset | https://github.com/notofonts/noto-cjk Sans/SubsetOTF/TC Regular, glyph-subset for assets/locales/zh-TW.json and renamed Craft UI TC (OFL reserved-name); licence text in assets/fonts/craft-ui-tc/OFL.txt; web UI Traditional Chinese | SIL OFL 1.1 |
|
||||
| tests/step-conformance/cone_point.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/cone_point_vertex.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/cylinder.step | step-conformance contributors | generated from formulas by https://github.com/dan335/step-conformance (corpus/generated, 96f30bb); import regression fixture | CC0-1.0 |
|
||||
@@ -101,6 +103,22 @@ 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/kernel-cases/import/1721-closed-bspline-tube-face/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#1721 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/2043-outer-bound-opposing-surface-sense/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#2043 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/258-cone-point-vertex/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point_vertex.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/258-cone-to-point/input.step | step-conformance contributors | copy of tests/step-conformance/cone_point.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/258-tiny-pin/input.step | step-conformance contributors | copy of tests/step-conformance/tiny_pin.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#258 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3098-bspline-edges-over-tiny-knot-span/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3098 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3208-extruded-ellipse-face-over-parameter-seam/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3208 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3352-vertex-off-its-edge-curve/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3352 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3730-void-oriented-shell-false/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3730 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/3880-cylinder-seam-edge/input.step | step-conformance contributors | copy of tests/step-conformance/cylinder_seam.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3880-nurbs-hemisphere-dome/input.step | step-conformance contributors | copy of tests/step-conformance/dome_nurbs.step (generated from formulas by https://github.com/dan335/step-conformance, corpus/generated, 96f30bb); kernel regression case for tangency-labs/solidcraft#3880 | CC0-1.0 |
|
||||
| tests/kernel-cases/import/3963-closed-spline-edge-vertex-past-start/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#3963 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4005-cylinder-written-as-near-zero-cone/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4005 | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4006-plane-written-as-cone-of-nearly-right-angle/input.step | SolidCraft contributors | synthetic STEP file written by hand from ISO 10303-21 for the kernel regression case of tangency-labs/solidcraft#4006 | MIT OR Apache-2.0 |
|
||||
| tests/ssi-corpus/cases/*.json.gz, tests/ssi-corpus/LICENSE-MIT, tests/ssi-corpus/LICENSE-APACHE | ssi-corpus contributors (jyviko) | 119 surface/surface intersection cases with exact ground truth from https://github.com/jyviko/ssi-corpus (cases/data, b04c8a6), gzip-compressed, unchanged; SSI scorer fixtures (tests/ssi-corpus/README.md) | MIT OR Apache-2.0 |
|
||||
| tests/ssi-corpus/cases/y23_*.json.gz | Yang, Jia and Yan (paper authors: published surfaces, figures and topology); ssi-corpus contributors (reconstructions and truth) | 25 cases built from Yang, Jia & Yan 2023, "Topology Guaranteed B-Spline Surface/Surface Intersection", ACM Transactions on Graphics 42(6), Article 211; each cites the paper in its `source` field | CC-BY 4.0 (paper); MIT OR Apache-2.0 (case files) |
|
||||
| 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 |
|
||||
@@ -125,6 +143,8 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| 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/craft-ui-sc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-SC.otf | SIL OFL 1.1 |
|
||||
| assets/fonts/craft-ui-tc/OFL.txt | Adobe / Google Inc. (Noto CJK); SolidCraft header | Accompanying OFL for CraftUI-TC.otf | SIL OFL 1.1 |
|
||||
| assets/fonts/ibm-plex-mono/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/ibm-plex-sans/OFL.txt | IBM Corp. | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
|
||||
@@ -185,11 +205,7 @@ Editable drawing code, native projects and shipped exports are collected under `
|
||||
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-dark-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light-hard.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/gruvbox-light-soft.json | SolidCraft contributors | original SolidCraft theme file mapping SolidCraft's UI colours to the Gruvbox palette's colour values (Gruvbox by Pavel Pertsev, https://github.com/morhetz/gruvbox, MIT) | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/solidcraft-95.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/platinum-9.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
| assets/styles/ui/themes/cupertino.json | SolidCraft contributors | original SolidCraft theme file | MIT OR Apache-2.0 |
|
||||
@@ -628,6 +644,134 @@ Original SolidCraft drawings, Open for owner review; no runtime assignments.
|
||||
| docs/book/src/generated/equation-parabola.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-parabola.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-spiral.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-spiral.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| docs/book/src/generated/equation-wave.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/equation-wave.json`; gitignored) | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/print_face.svg | SolidCraft contributors | two-tone modelling icon master (64 grid), original SolidCraft artwork | MIT OR Apache-2.0 |
|
||||
| assets/locales/fi.json | SolidCraft contributors | Original Finnish translation of SolidCraft's English catalog, AI-assisted (Claude Code) clean-room from the English and not yet reviewed line by line by a native speaker; no commercial CAD text used | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/model/plane_axis_point.svg | SolidCraft contributors | original icon master (64 grid) | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-2x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-2x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-4x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-4x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-8x.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-8x.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-off.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/aa-off.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/agent-pointer-speed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/agent-pointer-speed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/ao-on.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/ao-on.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/assembly-move.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/assembly-move.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-card.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-card.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-column.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/browser-column.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-bends.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-bends.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-draft.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-draft.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-failed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-failed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-fixed.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-fixed.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-projected.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/colour-vision-projected.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/direct-interaction.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/direct-interaction.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/frame-rate.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/frame-rate.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-bottom.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-bottom.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-in-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-in-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-left.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-left.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-under-browser.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/history-under-browser.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/layout-schematic.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/layout-schematic.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/measure-axis-distances.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/measure-axis-distances.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/mouse-mapping.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/mouse-mapping.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-axes.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/navigator-cube.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-centre.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-origin.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orbit-origin.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-y-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-y-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-z-up.dark.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/orientation-z-up.light.svg | SolidCraft contributors | original SolidCraft artwork: the Orientation figure copied from the app and extracted from the Preferences redesign board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/quick-measure.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/quick-measure.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-bodies.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-bodies.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-faces-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/select-faces-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-conflict.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-conflict.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-keep.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-keep.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-previous.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-previous.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-unless-turned.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-exit-unless-turned.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-guides.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-guides.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-hide-glyphs.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-hide-glyphs.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-state-icons.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/sketch-state-icons.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/style-shaded-edges.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/style-shaded-edges.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-coarse.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-coarse.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-fine.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-fine.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-normal.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-normal.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-ultra.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tess-ultra.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-dark.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-dark.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-gruvbox.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-gruvbox.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-imported.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-imported.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-light.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-light.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-dark-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-dark-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-light-half.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/theme-system-light-half.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-cursor.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-cursor.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-extrude.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-extrude.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-hole.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-hole.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-placement.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/tool-placement.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/vertex-markers.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/vertex-markers.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-angles.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-angles.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-grid.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-grid.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-perspective.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-perspective.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-shadow.dark.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| docs/design/preferences/figures/view-shadow.light.svg | SolidCraft contributors | original SolidCraft artwork drawn for the Preferences redesign (tracker #9863), extracted from the design board as a standalone SVG | MIT OR Apache-2.0 |
|
||||
| `assets/locales/pending/sketch-offset-instances.json` | SolidCraft contributors | Original offset instance-count label and translations | MIT OR Apache-2.0 |
|
||||
| assets/icons/ui/design/svg/flat/settings_profile.svg | SolidCraft contributors | single-colour application/navigation icon master (64 grid), original analytic head and shoulder geometry for settings profiles | MIT OR Apache-2.0 |
|
||||
| tests/kernel-cases/import/4561-convert-box-mesh-with-a-zero-area-triangle/box-degenerate.stl | SolidCraft contributors | synthetic ASCII STL written by hand: a box with one zero-area triangle, for the kernel regression case of tangency-labs/solidcraft#4561 | MIT OR Apache-2.0 |
|
||||
|
||||
## Code adapted from sibling projects
|
||||
|
||||
@@ -667,6 +811,15 @@ in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
|
||||
The glTF test files are built by the tests themselves (crates/io/src/gltf_in_tests.rs,
|
||||
crates/engine/src/tests/gltf_import.rs); no third-party models are committed.
|
||||
|
||||
## Geometry kernel crates
|
||||
|
||||
| Crate | Used for | Licence |
|
||||
|---|---|---|
|
||||
| solidcore (https://github.com/tangency-labs/solidcore, pinned by rev in Cargo.toml) | the Rust port of NVIDIA's SMLib: the B-rep kernel behind crates/smlib | Apache-2.0 (its NOTICE credits NVIDIA's USD BRep) |
|
||||
|
||||
Linked as a crate; no code from it is copied into SolidCraft. Its licence text and NOTICE go
|
||||
into THIRD-PARTY-LICENSES.txt when `main` regenerates it.
|
||||
|
||||
## FreeCAD and Rhino import library
|
||||
|
||||
FreeCAD `.FCStd` and Rhino `.3dm` files are read with [cadmpeg](https://github.com/cadmpeg/cadmpeg)
|
||||
@@ -682,16 +835,13 @@ copied into SolidCraft.
|
||||
| SolidCraft file | Original contribution | Licence |
|
||||
|---|---|---|
|
||||
| crates/ui-egui/src/viewport.rs (`nearest_quarter_turn`, `axis_view_orbit`) | N3MI-DG's [PR #101](https://github.com/bherbruck/solidcraft/pull/101): [minimal TOP/BOTTOM turns](https://github.com/bherbruck/solidcraft/commit/c28e780f52490784ef5886f094b9d3f6d10c3e3f) and [drag-to-orbit](https://github.com/bherbruck/solidcraft/commit/f30b30be6ecc4e2e6544a851243137c01a354934), adapted to the axis gizmo | MIT OR Apache-2.0 |
|
||||
| crates/smlib/src/core/offset.rs (`draft`: the tilted planes and `SmBrep::LocalOperation`, the tail of the capi's `draft_faces`) | ablasik's `draft` in `crates/smlib/src/ffi/rs_ops2.rs` on [ablasik/solidcraft `app/solidcore-backend-on-develop`](https://github.com/ablasik/solidcraft/blob/9c7e8e31565e1823f0591b58e2967351c32af13f/crates/smlib/src/ffi/rs_ops2.rs), adapted to `core::offset`'s errors and checks | MIT OR Apache-2.0 |
|
||||
|
||||
## Vendored third-party code
|
||||
|
||||
| Directory | Source | Licence | Changes |
|
||||
|---|---|---|---|
|
||||
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
|
||||
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
|
||||
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro; corner choice at vertices several intersection curves end on; reuse of an operand's meshes (vendor/README.md) |
|
||||
| vendor/truck-geometry | truck-geometry 0.5.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | intersection curves divided from their neighbours' surface parameters; surface second derivatives (vendor/README.md) |
|
||||
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
|
||||
| vendor/accesskit_macos | accesskit_macos 0.26.3, https://github.com/AccessKit/accesskit (The AccessKit contributors) | MIT OR Apache-2.0 | the subclassing adapter and its tests from 0.27.2: dropping the adapter keeps AppKit's key-value observers (vendor/README.md) |
|
||||
|
||||
## Vendored renderer compatibility fix
|
||||
|
||||
|
||||
+31
-12
@@ -19,12 +19,13 @@ Questions, ideas or want to say what you're working on? Join the [Discord](https
|
||||
|
||||
## Kernel work is paused
|
||||
|
||||
We're evaluating a new geometry kernel. Until that's decided, please don't open issues, pull
|
||||
requests or discussions about kernel work: booleans, fillets, chamfers, shell, offsets, sweeps and
|
||||
lofts, threads and holes, STEP/IGES import and export, tessellation, or anything else that changes
|
||||
`crates/kernel/`. The pinned issue on the tracker explains it. New ones get the `kernel-paused`
|
||||
label (`.github/workflows/kernel-paused.yml`) and a maintainer answers them. Crashes and data loss
|
||||
are still taken as issues, and everything outside the kernel carries on as normal.
|
||||
We're moving to a new geometry kernel. Until that's done, please don't open pull requests that
|
||||
change `crates/kernel/`, `crates/geom/` or `vendor/truck*`, or issues and discussions about kernel
|
||||
work: booleans, fillets, chamfers, shell, offsets, sweeps and lofts, threads and holes, STEP/IGES
|
||||
import and export, or tessellation. The pinned issue on the tracker explains it. New ones get the
|
||||
`kernel-paused` label (`.github/workflows/kernel-paused.yml`) and a maintainer answers them. Crashes
|
||||
and data loss are still taken as issues. Pull requests outside those directories carry on as normal,
|
||||
including the dialogs and tools for these features.
|
||||
|
||||
## Get it running (about 10 minutes on a fast machine)
|
||||
|
||||
@@ -160,9 +161,11 @@ the most complete description of the house style.
|
||||
| UI scenarios | `crates/ui-egui/tests/scenarios/*.json` | part of `cargo test` |
|
||||
| Reference tests | `cargo xtask oracle` | `tests/oracle/` (see below) |
|
||||
|
||||
A geometry change that changes the robustness goldens re-records them in the same pull request:
|
||||
`ROBUSTNESS_GOLDENS_BLESS="why it changed" cargo test --profile ci -p solidcraft-engine --lib robustness_goldens::`
|
||||
(see [docs/robustness.md](docs/robustness.md)); CI runs every seed on pull requests that touch geometry.
|
||||
The reference for what the kernel builds is the kernel-cases ladder (`tests/kernel-cases/ladder/`,
|
||||
`cargo xtask kernel-cases --ladder`, [docs/kernel-ladder.md](docs/kernel-ladder.md)).
|
||||
The naming corpus checks persistent names: every name a corpus design stores still finds a face or
|
||||
an edge, and two builds name its faces and edges alike
|
||||
(`cargo test --profile ci -p solidcraft-cli --test naming_corpus`).
|
||||
[docs/mutation-testing.md](docs/mutation-testing.md) shows how to find kernel behaviour these
|
||||
black-box suites miss.
|
||||
|
||||
@@ -221,8 +224,7 @@ Pull requests skip a few heavy tests (one to twelve minutes each), which run in
|
||||
Check that validation after merging. They are listed in
|
||||
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
|
||||
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
|
||||
the seeds of `tests::robustness_goldens` (`seed_00` … `seed_95`; pull requests that change the kernel, truck or the goldens run all but the slowest, `SLOW_SEEDS` in `ci.yml`),
|
||||
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
|
||||
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
|
||||
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
|
||||
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`, the `atomic_commands` contract tests
|
||||
@@ -242,7 +244,8 @@ the surface itself). Expected values are either worked out in closed form
|
||||
(`tools/oracle-analytic/`) or computed by building the same recipe in FreeCAD, run as a separate
|
||||
program (`tools/freecad-oracle/`); each file says which. The generated
|
||||
[reference test report](docs/oracle.md) has the current case count and results. The reference-test
|
||||
CI job runs `cargo xtask oracle` during full validation; pull-request runs skip it. A case that
|
||||
CI job runs `cargo xtask oracle` during full validation, and on pull requests that reach
|
||||
`solidcraft-cli`. A case that
|
||||
fails because of a known bug names its issue in `expected.json` (`"known_failure": "#123"`);
|
||||
never change an expected value to match SolidCraft's output.
|
||||
|
||||
@@ -338,6 +341,15 @@ and a fresh CI run:
|
||||
PRs in groups by area, so PRs that touch the same code meet a conflict at most once.
|
||||
4. Reference the issue (`Fixes #12`). In the PR, say what you tested and how. For geometry work, include the measured numbers (and
|
||||
the expected ones).
|
||||
5. Write the PR title and first sentence for someone who hasn't seen your chat, the issue or the
|
||||
code. Maintainers pick what to review from the title, and it becomes the commit on `main`. Say
|
||||
which part of the app changes and what a user now sees or can do, and name things rather than
|
||||
leaning on "there", "it" or "this".
|
||||
- Unclear: `Send to Slicer: pick one print face per body there, and show print faces only while it is open`
|
||||
- Clear: `3D printing: choose the face each body rests on in the Send to Slicer window, not the face menu`
|
||||
|
||||
Use plain, literal English in PR text: one idea per sentence, active voice, and one word for one
|
||||
thing. This helps everyone, including readers whose first language isn't English.
|
||||
|
||||
Commenting `/claim` assigns you, adds the `in progress` label, and replies with its automatic cleanup
|
||||
deadline. It frees a claim with no linked PR after 72 hours, or a claim with a PR after 14 days
|
||||
@@ -346,6 +358,13 @@ without pushes. These automatic cleanup deadlines are separate from the daily pr
|
||||
`/unclaim` releases the bot claim early (only you or a maintainer can). Issues a maintainer has
|
||||
taken are never freed automatically.
|
||||
|
||||
**A PR has to explain itself.** Start the description with what it is, in plain words someone new
|
||||
to the project would understand: what it does and who it's for. Don't assume anyone knows the name
|
||||
of your tool, project or feature. If there's anything to look at, show it in the description: a
|
||||
picture or video has to be visible there without opening the changed files. "The GIF is the change"
|
||||
doesn't count. A PR that doesn't do this waits until it does. For a PR that adds to
|
||||
`docs/showcase.md` or `docs/screenshots/`, the Picture check fails until the description shows one.
|
||||
|
||||
Help reviewers see what the words mean. For observable CAD, UI or model behavior, please
|
||||
include actual descriptive screenshots or a short recording in the issue or PR. This evidence
|
||||
is required for changes or bugs that can be demonstrated in the app, including kernel,
|
||||
|
||||
Generated
+106
-329
@@ -60,8 +60,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "accesskit_macos"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076"
|
||||
dependencies = [
|
||||
"accesskit",
|
||||
"accesskit_consumer 0.38.0",
|
||||
@@ -188,15 +186,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f2a05fd1bd10b2527e20a2cd32d8873d115b8b39fe219ee25f42a8aca6ba278"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
@@ -217,12 +206,6 @@ dependencies = [
|
||||
"x11rb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "array-macro"
|
||||
version = "2.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "220a2c618ab466efe41d0eace94dfeff1c35e3aa47891bdb95e1c0fefffd3c99"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -535,15 +518,6 @@ dependencies = [
|
||||
"piper",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "branches"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f11502672c5570f77f6bdf573332483f8475bab6a7fda00f1fae8ddb5a6245c0"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
@@ -791,17 +765,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cgmath"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a98d30140e3296250832bbaaff83b27dcd6fa3cc70fb6f1f3e5c9c0023b5317"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"num-traits",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chacha20"
|
||||
version = "0.10.2"
|
||||
@@ -824,19 +787,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -907,12 +857,6 @@ version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1090,19 +1034,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -1249,7 +1180,7 @@ dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"emath",
|
||||
"epaint",
|
||||
"itertools 0.15.0",
|
||||
"itertools",
|
||||
"log",
|
||||
"nohash-hasher",
|
||||
"profiling",
|
||||
@@ -1667,10 +1598,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1985,30 +1914,6 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.3.0"
|
||||
@@ -2168,15 +2073,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.15.0"
|
||||
@@ -2271,17 +2167,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "katexit"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccfb0b7ce7938f84a5ecbdca5d0a991e46bc9d6d078934ad5e92c5270fe547db"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyboard-types"
|
||||
version = "0.8.3"
|
||||
@@ -2432,15 +2317,6 @@ version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "matext4cgmath"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cgmath",
|
||||
"katexit",
|
||||
"num-complex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
@@ -3316,30 +3192,6 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
@@ -3512,15 +3364,6 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rclite"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e09665c494a9de2bd230e1aedc0c2fb20481a51f28f7db84d0c9bcfe2fe537b2"
|
||||
dependencies = [
|
||||
"branches",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "read-fonts"
|
||||
version = "0.41.0"
|
||||
@@ -4208,9 +4051,27 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore"
|
||||
version = "1.0.1"
|
||||
source = "git+https://github.com/tangency-labs/solidcore?rev=8ddce901671f1cebd9214a31d21895d725e886c8#8ddce901671f1cebd9214a31d21895d725e886c8"
|
||||
dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcore-record"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/tangency-labs/solidcore?rev=b21eb123ee989bd708726080b5d2298a1f2573b5#b21eb123ee989bd708726080b5d2298a1f2573b5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4242,27 +4103,30 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"winit",
|
||||
"x11rb",
|
||||
"zbus 5.19.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-mcp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4271,15 +4135,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcore-record",
|
||||
"solidcraft-derive",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4288,7 +4154,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4296,10 +4162,12 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_path_to_error",
|
||||
"solidcore-record",
|
||||
"solidcraft-doc",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-render",
|
||||
"solidcraft-sketch",
|
||||
"thiserror 2.0.21",
|
||||
@@ -4309,16 +4177,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
@@ -4327,6 +4196,7 @@ dependencies = [
|
||||
"robust",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-io",
|
||||
@@ -4338,7 +4208,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4353,45 +4223,56 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcore-record",
|
||||
"solidcraft-doc",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-kernel",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-testkit",
|
||||
"thiserror 2.0.21",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
"rayon",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
"solidcraft-smlib",
|
||||
"solidcraft-step",
|
||||
"spade",
|
||||
"thiserror 2.0.21",
|
||||
"truck-meshalgo",
|
||||
"truck-modeling",
|
||||
"truck-shapeops",
|
||||
"truck-stepio",
|
||||
"truck-topology",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-profile"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4399,33 +4280,62 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"web-time",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"harfrust",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"thiserror 2.0.21",
|
||||
"ttf-parser",
|
||||
"unicode-bidi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-smlib"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"log",
|
||||
"serde_json",
|
||||
"solidcore",
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-step"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"solidcore-record",
|
||||
"solidcraft-geom",
|
||||
"solidcraft-profile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-testkit"
|
||||
version = "0.40.0-rc.1"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4440,10 +4350,12 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"solidcore-record",
|
||||
"solidcraft-engine",
|
||||
"solidcraft-gpu",
|
||||
"solidcraft-space-mouse",
|
||||
"syn 2.0.119",
|
||||
"sys-locale",
|
||||
"tiny-skia",
|
||||
"toml",
|
||||
"ttf-parser",
|
||||
@@ -4455,7 +4367,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4516,16 +4428,6 @@ version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
@@ -4559,6 +4461,18 @@ dependencies = [
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
@@ -4566,7 +4480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -4763,144 +4677,6 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-base"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c279de9e92e5dc20a188deb0bb9a4bd421a6a185e57e03e19025e84a36c5a05"
|
||||
dependencies = [
|
||||
"cgmath",
|
||||
"matext4cgmath",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-derivers"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421ff1c5a303aed64e295d9b30e8424ee7a2fa251617a59ae4acfd078496009f"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-geometry"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-geotrait"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a31fe98d5a0589ad20576260e214af23dea21a8d125da81ad3bbcb666f411f"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"rand 0.8.8",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-derivers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-meshalgo"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"array-macro",
|
||||
"derive_more",
|
||||
"itertools 0.13.0",
|
||||
"rayon",
|
||||
"rustc-hash 2.1.3",
|
||||
"spade",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-modeling"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8082c2dfac8c2732a014d81807fb5d24dbac0d72ee4de2a705b7fbc05fa05b18"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-polymesh"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8f22def310af4a89c37beb4839bbf702b302d2c112ea29553759f0e16622fbf"
|
||||
dependencies = [
|
||||
"array-macro",
|
||||
"bytemuck",
|
||||
"itertools 0.13.0",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-shapeops"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"derive_more",
|
||||
"itertools 0.13.0",
|
||||
"rustc-hash 2.1.3",
|
||||
"truck-base",
|
||||
"truck-geometry",
|
||||
"truck-geotrait",
|
||||
"truck-meshalgo",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-stepio"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"truck-geometry",
|
||||
"truck-modeling",
|
||||
"truck-polymesh",
|
||||
"truck-topology",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "truck-topology"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf62a1610405c5e39ab5eb6948f326fc1641689734a6b8207a80792133fe0e78"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"rclite",
|
||||
"rustc-hash 2.1.3",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"truck-base",
|
||||
"truck-geotrait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ttf-parser"
|
||||
version = "0.25.1"
|
||||
@@ -5998,9 +5774,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
+21
-19
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.27.1"
|
||||
version = "0.40.0-rc.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
@@ -16,8 +16,11 @@ repository = "https://github.com/bherbruck/solidcraft"
|
||||
robust = "1.2"
|
||||
# internal
|
||||
solidcraft-geom = { path = "crates/geom" }
|
||||
solidcraft-profile = { path = "crates/profile" }
|
||||
solidcraft-sketch = { path = "crates/sketch" }
|
||||
solidcraft-kernel = { path = "crates/kernel" }
|
||||
solidcraft-smlib = { path = "crates/smlib" }
|
||||
solidcraft-step = { path = "crates/step" }
|
||||
solidcraft-render = { path = "crates/render" }
|
||||
solidcraft-gpu = { path = "crates/gpu" }
|
||||
solidcraft-doc = { path = "crates/doc" }
|
||||
@@ -28,6 +31,16 @@ solidcraft-space-mouse = { path = "crates/space-mouse" }
|
||||
solidcraft-mcp = { path = "crates/mcp" }
|
||||
solidcraft-derive = { path = "crates/derive" }
|
||||
solidcraft-3dx-client = { path = "crates/threedx-client" }
|
||||
# The Rust port of NVIDIA's SMLib (Apache-2.0), the B-rep kernel behind crates/smlib. Pinned;
|
||||
# move the rev on its own commit. For a
|
||||
# local checkout, add `[patch."https://github.com/tangency-labs/solidcore"] solidcore = { path =
|
||||
# "../solidcore/crates/solidcore" }` to a `.cargo/config.toml` outside the repository.
|
||||
solidcore = { git = "https://github.com/tangency-labs/solidcore", rev = "8ddce901671f1cebd9214a31d21895d725e886c8" }
|
||||
# Geometry as data (solidcore's layers §1, "Record"): the B-rep record, its structural validation
|
||||
# and the native body file. Pinned like solidcore; for a local checkout, patch it as above.
|
||||
solidcore-record = { git = "https://github.com/tangency-labs/solidcore", rev = "b21eb123ee989bd708726080b5d2298a1f2573b5" }
|
||||
# test tooling: a dev-dependency only (xtask/src/layers.rs)
|
||||
solidcraft-testkit = { path = "crates/testkit" }
|
||||
|
||||
# foundation
|
||||
thiserror = "2"
|
||||
@@ -45,14 +58,7 @@ sha2 = "0.10"
|
||||
moxcms = { version = "0.8.1", default-features = false, features = ["extended_range"] }
|
||||
semver = "1"
|
||||
|
||||
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
|
||||
truck-modeling = "0.6"
|
||||
truck-shapeops = { version = "0.4", default-features = false }
|
||||
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
|
||||
truck-stepio = { version = "0.3", default-features = false }
|
||||
truck-topology = "0.6"
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
|
||||
# tree through truck-meshalgo.
|
||||
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0).
|
||||
spade = { version = "2.15", default-features = false, features = ["std"] }
|
||||
rayon = "1.12"
|
||||
|
||||
@@ -125,9 +131,7 @@ opt-level = 2
|
||||
opt-level = 1
|
||||
|
||||
# Tests and lints (`cargo xtask ci`, GitHub CI): quick to build, quick enough to run. Light
|
||||
# optimisation, line tables only (file:line in backtraces), no LTO. Debug assertions are off because
|
||||
# truck's debug-only checks reject valid geometry (seam edges, closed edges); that was the only
|
||||
# reason the tests used to build with --release, which cost most of CI's time.
|
||||
# optimisation, line tables only (file:line in backtraces), no LTO, no debug assertions.
|
||||
[profile.ci]
|
||||
inherits = "dev"
|
||||
opt-level = 1
|
||||
@@ -150,14 +154,9 @@ codegen-units = 16
|
||||
# binary). See docs/crash-reports.md.
|
||||
debug = "line-tables-only"
|
||||
|
||||
# truck-shapeops with SolidCraft fixes (Apache-2.0, see vendor/truck-shapeops and ATTRIBUTION.md).
|
||||
[patch.crates-io]
|
||||
truck-geometry = { path = "vendor/truck-geometry" }
|
||||
truck-shapeops = { path = "vendor/truck-shapeops" }
|
||||
truck-stepio = { path = "vendor/truck-stepio" }
|
||||
truck-meshalgo = { path = "vendor/truck-meshalgo" }
|
||||
matext4cgmath = { path = "vendor/matext4cgmath" }
|
||||
|
||||
# Vendored crates with SolidCraft fixes (see vendor/README.md and ATTRIBUTION.md).
|
||||
[patch.crates-io]
|
||||
# Keep OpenGL pipelines, resolve textures and CAD callbacks at the same safe sample count.
|
||||
egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
|
||||
@@ -165,6 +164,9 @@ egui-wgpu = { path = "vendor/egui-wgpu" }
|
||||
# winit 0.30 never binds and eframe 0.36 still requires winit 0.30.
|
||||
winit = { path = "vendor/winit" }
|
||||
|
||||
# macOS: dropping the accessibility adapter leaves AppKit's key-value observers intact (#7535).
|
||||
accesskit_macos = { path = "vendor/accesskit_macos" }
|
||||
|
||||
# Mutation testing (cargo-mutants, docs/mutation-testing.md): the ci profile with incremental
|
||||
# builds, since every mutant rebuilds the crates above the one it changes.
|
||||
[profile.mutants]
|
||||
|
||||
+3
-1
@@ -662,7 +662,9 @@ completed drawings publish atomically. The browser still exports synchronously.
|
||||
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.
|
||||
sampling. A fold crosses the sheet's own outer edges the same way, slanted or on any curve,
|
||||
with the outline exact in the flat pattern and its DXF, and flanges and other folds still go
|
||||
on the panels.
|
||||
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
|
||||
|
||||
+40
-12
@@ -3,7 +3,7 @@
|
||||
## Supported versions
|
||||
|
||||
SolidCraft is young and moves quickly, so only the latest release gets security fixes. Please
|
||||
update to the [latest release](https://github.com/bherbruck/solidcraft/releases/latest) before
|
||||
update to the [latest release](https://github.com/tangency-labs/solidcraft/releases/latest) before
|
||||
reporting, and check whether the problem is still on `main`.
|
||||
|
||||
| Version | Supported |
|
||||
@@ -15,18 +15,46 @@ reporting, and check whether the problem is still on `main`.
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please **don't open a public issue** for a security problem. Report it privately through GitHub:
|
||||
open the repository's [Security tab](https://github.com/bherbruck/solidcraft/security) and choose
|
||||
open the repository's [Security tab](https://github.com/tangency-labs/solidcraft/security) and choose
|
||||
**Report a vulnerability**
|
||||
([direct link](https://github.com/bherbruck/solidcraft/security/advisories/new)).
|
||||
([direct link](https://github.com/tangency-labs/solidcraft/security/advisories/new)).
|
||||
|
||||
Please include what you found, how to reproduce it (a design file, command or MCP request), the
|
||||
version and platform, and the impact you expect. I'll acknowledge the report as soon as I can,
|
||||
keep you updated in the advisory, and credit you when the fix is published unless you'd rather
|
||||
stay anonymous.
|
||||
Please include what you found, the steps you ran to reproduce it (a design file, command or MCP
|
||||
request), the version and platform, and what an attacker gains. I'll acknowledge the report as soon
|
||||
as I can, keep you updated in the advisory, and credit you when the fix is published unless you'd
|
||||
rather stay anonymous.
|
||||
|
||||
Areas where reports are especially useful:
|
||||
## What counts as a security issue
|
||||
|
||||
- opening untrusted design, STEP, STL or 3MF files (crashes, excessive memory or CPU, path tricks)
|
||||
- the MCP server and the control API (file access outside the allowed folders, command injection)
|
||||
- the web app (anything reaching beyond the page's own storage)
|
||||
- release packages and the update paths (AppImage, Flatpak, MSI, deb, rpm, snap)
|
||||
Advisories are for problems where someone gets something they shouldn't, without already having
|
||||
access to your computer or your account. For example:
|
||||
|
||||
- opening a design, STEP, STL or 3MF file from someone else runs code, or reads or overwrites files
|
||||
it shouldn't
|
||||
- a website or another computer can reach or control SolidCraft
|
||||
- an agent gets past a prompt that promised to ask you first
|
||||
- the web app reaches beyond the page's own storage
|
||||
- a release package or an update can be tampered with
|
||||
- an API key or token gets sent somewhere it shouldn't go
|
||||
|
||||
## What to report as a normal bug
|
||||
|
||||
Please [open an issue](https://github.com/tangency-labs/solidcraft/issues/new) for these. They're
|
||||
still worth fixing, but they aren't security issues:
|
||||
|
||||
- anything that needs a program already running on your computer, or the control token. A program
|
||||
running as you can already do what you can.
|
||||
- MCP's allowed folders. They guard against accidents, they aren't a security boundary, and an agent
|
||||
with a shell of its own can already reach those files. Getting around them isn't a vulnerability.
|
||||
- crashes, hangs, or heavy memory or CPU use, unless they also lead to something in the list above
|
||||
- anything that only happens in an unusual setup you'd have to choose on purpose
|
||||
|
||||
## Before you send it
|
||||
|
||||
- **Run it.** Reproduce the problem on the latest release. If you only read the code, say so, and
|
||||
expect it to wait.
|
||||
- **Check it yourself.** If a scanner or an AI agent found it, read the code and confirm it first.
|
||||
These tools tend to overrate what they find.
|
||||
- **Say what's gained.** Name what the attacker needs and what they get. I set the final severity.
|
||||
|
||||
Reports that don't fit will be closed with a short note, or moved to the issue tracker.
|
||||
|
||||
+421
-1167
File diff suppressed because it is too large
Load Diff
@@ -20,11 +20,15 @@ name = "solvecraft-cli"
|
||||
path = "src/alias.rs"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu"]
|
||||
default = ["raytrace-cpu", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# The profiler, compiled in so any build can say where its time goes (`--profile`,
|
||||
# SOLIDCRAFT_PROFILE=1); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcore-record = { workspace = true }
|
||||
solidcraft-engine = { workspace = true }
|
||||
solidcraft-gpu = { workspace = true }
|
||||
solidcraft-mcp = { workspace = true }
|
||||
@@ -34,3 +38,5 @@ serde_json = { workspace = true }
|
||||
[dev-dependencies]
|
||||
# Snapshot tests decode the pictures to compare them with a tolerance.
|
||||
image = { workspace = true }
|
||||
# Tests measure meshes at their own tolerances.
|
||||
solidcraft-kernel = { workspace = true }
|
||||
|
||||
@@ -184,7 +184,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
|
||||
run.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().warm_meshes(|| true);
|
||||
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
|
||||
shown.push(t.elapsed().as_secs_f64() * 1000.0);
|
||||
if run.len() == 100 || i + 1 == commands.len() {
|
||||
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
|
||||
@@ -198,7 +198,7 @@ pub fn run(args: &[String]) -> Result<(), String> {
|
||||
shown.clear();
|
||||
}
|
||||
}
|
||||
let world = s.world_state();
|
||||
let world = s.world_state().map_err(|e| e.to_string())?;
|
||||
println!(
|
||||
"{}",
|
||||
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
|
||||
@@ -236,7 +236,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()));
|
||||
let world = s.world_state();
|
||||
let world = s.world_state().unwrap();
|
||||
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
|
||||
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
|
||||
}
|
||||
|
||||
@@ -40,8 +40,6 @@ pub fn one(args: &[String]) -> Result<(), String> {
|
||||
"steps": run.steps.iter().map(|s| s.kind).collect::<Vec<_>>(),
|
||||
"unsupported": run.unsupported,
|
||||
"rejected": run.rejected,
|
||||
"boolean_fallbacks": run.boolean_fallbacks,
|
||||
"boolean_reports_truncated": run.boolean_reports_truncated,
|
||||
"panics": PANICS.swap(0, std::sync::atomic::Ordering::SeqCst),
|
||||
});
|
||||
if run.outcome.is_bug() {
|
||||
@@ -191,23 +189,9 @@ fn summarise(results: &[Value], took: Duration) -> String {
|
||||
let mut boolean: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut at_step: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut unsupported: BTreeMap<String, usize> = BTreeMap::new();
|
||||
let mut fallbacks: BTreeMap<String, [u64; 2]> = BTreeMap::new();
|
||||
let mut truncated = [0usize; 2];
|
||||
let mut steps = 0;
|
||||
let (mut dp, mut bp) = (0, 0);
|
||||
for v in results {
|
||||
for (i, group) in ["design", "boolean"].into_iter().enumerate() {
|
||||
if v[group]["boolean_reports_truncated"].as_bool().unwrap_or(false) {
|
||||
truncated[i] += 1;
|
||||
}
|
||||
if let Some(counts) = v[group]["boolean_fallbacks"].as_object() {
|
||||
for (strategy, count) in counts {
|
||||
if let Some(total) = fallbacks.entry(strategy.clone()).or_default().get_mut(i) {
|
||||
*total = total.saturating_add(count.as_u64().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dp += usize::from(v["design"]["panics"].as_u64().unwrap_or(0) > 0);
|
||||
bp += usize::from(v["boolean_panics"].as_u64().unwrap_or(0) > 0);
|
||||
let k = if v.get("timeout").is_some() {
|
||||
@@ -246,11 +230,6 @@ fn summarise(results: &[Value], took: Duration) -> String {
|
||||
for (k, c) in &boolean {
|
||||
s += &format!("- {k}: {}\n", pct(*c));
|
||||
}
|
||||
s += "\nSuccessful Boolean retries (events, including nested operations):\n";
|
||||
for (strategy, [designs, checks]) in &fallbacks {
|
||||
s += &format!("- {strategy}: designs {designs}, boolean checks {checks}\n");
|
||||
}
|
||||
s += &format!("- Capped reports: designs {}, boolean checks {}\n", truncated[0], truncated[1]);
|
||||
s
|
||||
}
|
||||
|
||||
@@ -259,15 +238,13 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn report_counts_retry_events_separately_and_marks_incomplete_logs() {
|
||||
fn report_counts_outcomes_and_timeouts() {
|
||||
let results = [
|
||||
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":2}},"boolean":{"outcome":"ok","boolean_fallbacks":{"shifted_operands":3},"boolean_reports_truncated":true}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"},"boolean_fallbacks":{"shifted_operands":1}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"design":{"outcome":{"kind":"ok"}},"boolean":{"outcome":"ok"}}),
|
||||
json!({"timeout":90}),
|
||||
];
|
||||
let text = summarise(&results, Duration::ZERO);
|
||||
assert!(text.contains("shifted_operands: designs 3, boolean checks 3"), "{text}");
|
||||
assert!(text.contains("Capped reports: designs 0, boolean checks 1"), "{text}");
|
||||
assert!(text.contains("timeout: 1 (33.3 %)"), "{text}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+109
-23
@@ -10,12 +10,13 @@
|
||||
//! 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]
|
||||
//! the standard Assistant model test (#4851) on a running app
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes]
|
||||
//! solidcraft-cli snapshot <script|design> [--in design|part.step] --out shot.png [--width W] [--height H] [--view iso|front|top|…|NAME | --camera JSON] [--no-grid] [--no-axes] [--quality LEVEL]
|
||||
//! [--body NAME:opacity=0.3,color=#rrggbb]... [--tool NAME]... [--section z=27]
|
||||
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
|
||||
//! 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 kernel-cases [--json] [--dumps=DIR] [--validation=MODE] <case-dir>... the kernel regression rig (tests/kernel-cases)
|
||||
//! solidcraft-cli mcp [--headless] [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
|
||||
//! [--param-check off|warn|strict]
|
||||
//! MCP server on stdio (docs/mcp.md)
|
||||
@@ -33,9 +34,11 @@ mod bench_agent;
|
||||
mod bench_assistant;
|
||||
mod crashes;
|
||||
mod fuzz;
|
||||
mod kernel_cases;
|
||||
mod logger;
|
||||
mod oracle;
|
||||
mod preview;
|
||||
mod profiling;
|
||||
mod recipe;
|
||||
mod render;
|
||||
mod spatial;
|
||||
@@ -58,19 +61,19 @@ struct Command {
|
||||
}
|
||||
|
||||
/// `run`'s lines in the usage.
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--json-out FILE] [--allow-errors]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n";
|
||||
const USAGE_RUN: &str = " solidcraft-cli run <script.json|design.sc|part.step> [--in design.sc|part.step] [--out FILE]... [--save FILE] [--quiet] [--results] [--timings] [--profile] [--profile-out trace.json] [--json-out FILE] [--allow-errors] [--validation each_step|each_feature|off]\n [--param-check off|warn|strict]\n solidcraft-cli run --in <design.sc|part.step> [--out FILE]...\n (a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)\n (--validation: when SMLib's full check runs on the features computed (diagnostics.validation), default each_feature)\n (--param-check checks each step against its command's contract: warn reports on stderr, strict stops the run)\n (--timings prints {timings: [{index, command, ms}], total_ms} for a script, with --results its results too;\n --profile adds each command's profile: {boolean_ms, tessellate_ms, other_ms}, the kernel's share)\n (--profile also records where the time goes, SMLib's calls, kernel operations, features, commands and import stages,\n and prints the table to stderr: count, total, self time and longest per span; --profile-out FILE writes the spans\n as a Chrome trace for chrome://tracing or ui.perfetto.dev; SOLIDCRAFT_PROFILE=1 does the same for any command)\n";
|
||||
/// `eval`'s lines in the usage.
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n";
|
||||
const USAGE_EVAL: &str = " solidcraft-cli eval <script.json|design.sc|part.step> [--in design.sc|part.step] [--json-out FILE] [--profile] [--profile-out trace.json] (alias: inspect)\n solidcraft-cli eval --in <script.json|design.sc|part.step> [--json-out FILE]\n (with both inputs, runs the positional script on --in before measuring; does not automatically save the modified design)\n (--json-out FILE writes the JSON that run (with --results, else the measurement) and eval print to FILE instead of stdout,\n as plain UTF-8: a shell's > can re-encode it, e.g. Windows PowerShell adds a byte-order mark)\n (--profile and --profile-out as for run)\n";
|
||||
/// `script`'s lines in the usage.
|
||||
const USAGE_SCRIPT: &str = " solidcraft-cli script <design.sc|script.json> [--out script.json]\n (the design's history as a command script that eval and run replay)\n";
|
||||
/// `bench-open`'s lines in the usage.
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--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";
|
||||
const USAGE_BENCH_OPEN: &str = " solidcraft-cli bench-open <design.sc | part.step> [--parse-only] [--save OUT.sc] [--display LEVEL [--edit NAME EXPR]] [--ops script.json] [--profile] [--profile-out trace.json]\n (--profile and --profile-out as for run)\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.sc] [--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.sc|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.sc|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";
|
||||
const USAGE_SNAPSHOT: &str = " solidcraft-cli snapshot <script|design> [--in design.sc|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] [--quality coarse|normal|fine|ultra]\n solidcraft-cli snapshot --in <design.sc|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 --quality: the display quality the bodies are meshed at, as the app's setting (default normal))\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.
|
||||
@@ -81,6 +84,8 @@ const USAGE_COMMANDS: &str = " solidcraft-cli commands\n";
|
||||
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";
|
||||
/// `kernel-cases`'s lines in the usage.
|
||||
const USAGE_KERNEL_CASES: &str = " solidcraft-cli kernel-cases <case-dir>... [--json] [--dumps=DIR] [--validation=each_step|each_feature|off]\n (the kernel regression rig, tests/kernel-cases, on SMLib; --dumps: each case's\n failing SMLib calls leave their inputs for replay in DIR/<area>-<case>; --validation: when SMLib's\n full check runs (diagnostics.validation), default each_feature)\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";
|
||||
@@ -118,6 +123,7 @@ const COMMANDS: &[Command] = &[
|
||||
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: "kernel-cases", usage: USAGE_KERNEL_CASES, 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 },
|
||||
@@ -133,6 +139,19 @@ const COMMANDS: &[Command] = &[
|
||||
fn main() -> ExitCode {
|
||||
let mut args: Vec<String> = std::env::args().skip(1).collect();
|
||||
logger::init(&mut args);
|
||||
// `--profile` / `--profile-out` (run, eval, bench-open) or SOLIDCRAFT_PROFILE (any command):
|
||||
// record from here, and print the table when the command ends.
|
||||
let profiling = match args.first().map(String::as_str) {
|
||||
Some("run" | "eval" | "inspect" | "bench-open") => profiling::start(&args[1..]),
|
||||
_ => profiling::start(&[]),
|
||||
};
|
||||
let profiling = match profiling {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
}
|
||||
};
|
||||
if let Err(e) = max_file_size(&mut args) {
|
||||
eprintln!("error: {e}");
|
||||
return ExitCode::FAILURE;
|
||||
@@ -170,9 +189,14 @@ fn main() -> ExitCode {
|
||||
recipe::to_script(&v).map(|s| println!("{}", pretty(&s)))
|
||||
}),
|
||||
Some("oracle") => oracle::run(&args[1..]),
|
||||
Some("kernel-cases") => kernel_cases::run(&args[1..]),
|
||||
Some("fuzz") => fuzz::run(&args[1..]),
|
||||
Some("fuzz-one") => fuzz::one(&args[1..]),
|
||||
Some("mcp") => cmd_mcp(&args[1..]),
|
||||
Some("mcp") => {
|
||||
let result = cmd_mcp(&args[1..]);
|
||||
solidcraft_mcp::cleanup_scratch_dir();
|
||||
result
|
||||
}
|
||||
Some("crashes") => crashes::run(&args[1..]),
|
||||
Some("icon") => cmd_icon(&args[1..]),
|
||||
Some("licences" | "licenses") => {
|
||||
@@ -202,6 +226,11 @@ fn main() -> ExitCode {
|
||||
}
|
||||
_ => Err(usage()),
|
||||
};
|
||||
// The table also when the command failed: where it spent its time before failing.
|
||||
let r = match (r, profiling.map(profiling::Profiling::finish)) {
|
||||
(Ok(()), Some(Err(e))) => Err(e),
|
||||
(r, _) => r,
|
||||
};
|
||||
match r {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
@@ -282,8 +311,15 @@ pub fn load(path: &str) -> Result<Session, String> {
|
||||
/// `load`, with scripts and recipes run at the parameter-check level `check`, which the
|
||||
/// session keeps.
|
||||
fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
|
||||
load_with(path, check, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// `load_checked`, the features computed under `validation` (`diagnostics.validation`), which
|
||||
/// the session keeps.
|
||||
fn load_with(path: &str, check: solidcraft_engine::schema::ParamCheck, validation: solidcraft_engine::kernel::Validation) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
s.param_check = check;
|
||||
s.set_validation(validation);
|
||||
if solidcraft_engine::io::is_step_path(path) || solidcraft_engine::io::is_mesh_path(path) || solidcraft_engine::io::is_point_cloud_path(path) {
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
@@ -291,7 +327,7 @@ fn load_checked(path: &str, check: solidcraft_engine::schema::ParamCheck) -> Res
|
||||
let bytes = solidcraft_engine::io::limits::read(path, solidcraft_engine::io::limits::Kind::Design).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::opened_at(doc, None, Some(path));
|
||||
let mut session = Session::opened_at_validating(doc, None, Some(path), validation);
|
||||
session.param_check = check;
|
||||
return Ok(session);
|
||||
}
|
||||
@@ -355,6 +391,16 @@ fn positional(args: &[String]) -> Option<&String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Is `path` a command script (or a recipe), not a design: JSON that is no saved design.
|
||||
fn is_script(path: &str) -> bool {
|
||||
let Ok(bytes) = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES) else { return false };
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
return false;
|
||||
}
|
||||
serde_json::from_slice::<Value>(&bytes)
|
||||
.is_ok_and(|v| !v.get("format").and_then(Value::as_str).is_some_and(|f| solidcraft_engine::doc::format::version_of(f).is_some()))
|
||||
}
|
||||
|
||||
/// `--json-out FILE`: where the JSON a command prints goes instead of stdout.
|
||||
fn json_out(args: &[String]) -> Result<Option<&str>, String> {
|
||||
if !args.iter().any(|a| a == "--json-out") {
|
||||
@@ -391,24 +437,31 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
known_args(
|
||||
"run",
|
||||
args,
|
||||
&["--in", "--out", "--save", "--json-out", "--param-check"],
|
||||
&["--in", "--out", "--save", "--json-out", "--param-check", "--profile-out", "--validation"],
|
||||
&["--quiet", "--results", "--timings", "--profile", "--allow-errors"],
|
||||
true,
|
||||
)?;
|
||||
let validation = validation_flag("run", args)?;
|
||||
let base = flag(args, "--in");
|
||||
let path = positional(args);
|
||||
if path.is_none() && base.is_none() {
|
||||
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`.
|
||||
let profile = args.iter().any(|a| a == "--profile");
|
||||
// `--profile` records the profiler's spans (the table goes to stderr, `profiling.rs`); for a
|
||||
// script it also times each command and splits out its kernel time, implying `--timings`.
|
||||
let script = path.is_some_and(|p| is_script(p));
|
||||
let profile = script && args.iter().any(|a| a == "--profile" || a == "--profile-out");
|
||||
let timings = profile || args.iter().any(|a| a == "--timings");
|
||||
let (mut s, results) = if want_results || timings {
|
||||
// Collect every command's result or time (scripts only), then process requested files too.
|
||||
let mut s = match base {
|
||||
Some(b) => load_checked(b, check)?,
|
||||
None => Session::default(),
|
||||
Some(b) => load_with(b, check, validation)?,
|
||||
None => {
|
||||
let mut s = Session::default();
|
||||
s.set_validation(validation);
|
||||
s
|
||||
}
|
||||
};
|
||||
s.param_check = check;
|
||||
let needs = if want_results { "--results needs a script" } else { "--timings needs a script" };
|
||||
@@ -425,7 +478,7 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
};
|
||||
(s, Some(out))
|
||||
} else {
|
||||
(open(base, path, check)?, None)
|
||||
(open_validating(base, path, check, validation)?, None)
|
||||
};
|
||||
// A feature that fails leaves the bodies as they were before it (a parameter change can
|
||||
// break a feature without failing the command that made it): writing them would hand out
|
||||
@@ -452,18 +505,38 @@ fn cmd_run(args: &[String]) -> Result<(), String> {
|
||||
/// The session `run`, `eval` and `snapshot` start from: `--in` (a design or STEP file) with the
|
||||
/// positional script run on top of it, or either one alone, with scripts run at `check`.
|
||||
fn open(base: Option<&str>, path: Option<&String>, check: solidcraft_engine::schema::ParamCheck) -> Result<Session, String> {
|
||||
open_validating(base, path, check, solidcraft_engine::kernel::Validation::default())
|
||||
}
|
||||
|
||||
/// [`open`], the features computed under `validation`.
|
||||
fn open_validating(
|
||||
base: Option<&str>,
|
||||
path: Option<&String>,
|
||||
check: solidcraft_engine::schema::ParamCheck,
|
||||
validation: solidcraft_engine::kernel::Validation,
|
||||
) -> Result<Session, String> {
|
||||
let load = |p: &str| load_with(p, check, validation);
|
||||
match (base, path) {
|
||||
(Some(b), Some(p)) => {
|
||||
let mut s = load_checked(b, check)?;
|
||||
let mut s = load(b)?;
|
||||
run_script_warning(&mut s, p, &read_json(p)?)?;
|
||||
Ok(s)
|
||||
}
|
||||
(Some(b), None) => load_checked(b, check),
|
||||
(None, Some(p)) => load_checked(p, check),
|
||||
(Some(b), None) => load(b),
|
||||
(None, Some(p)) => load(p),
|
||||
(None, None) => Err(usage()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `--validation MODE` (`diagnostics.validation`): the default, each_feature, when not given.
|
||||
fn validation_flag(cmd: &str, args: &[String]) -> Result<solidcraft_engine::kernel::Validation, String> {
|
||||
match flag(args, "--validation") {
|
||||
Some(m) => solidcraft_engine::kernel::Validation::parse(m)
|
||||
.ok_or_else(|| format!("{cmd}: --validation must be each_step, each_feature or off, not `{m}`")),
|
||||
None => Ok(solidcraft_engine::kernel::Validation::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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. `positionals` says whether the
|
||||
/// command takes a word of its own (a file, a seed): `fuzz` takes none, and would start a run.
|
||||
@@ -501,7 +574,7 @@ fn timeline_ok(s: &Session) -> Result<(), String> {
|
||||
|
||||
fn cmd_eval(args: &[String]) -> Result<(), String> {
|
||||
let json_out = json_out(args)?;
|
||||
known_args("eval", args, &["--in", "--json-out"], &[], true)?;
|
||||
known_args("eval", args, &["--in", "--json-out", "--profile-out"], &["--profile"], true)?;
|
||||
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
|
||||
let m = s.execute("inspect.measure", &json!({})).map_err(|e| e.to_string())?;
|
||||
let d = s.execute("document.inspect", &json!({})).map_err(|e| e.to_string())?;
|
||||
@@ -583,7 +656,7 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
known_args(
|
||||
"snapshot",
|
||||
args,
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section"],
|
||||
&["--in", "--out", "--width", "--height", "--view", "--camera", "--body", "--tool", "--section", "--quality"],
|
||||
&["--no-grid", "--no-axes"],
|
||||
true,
|
||||
)?;
|
||||
@@ -596,6 +669,16 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
Some(x) => x.parse().ok().filter(|n| (16..=8192).contains(n)).ok_or_else(|| format!("{name} must be 16 to 8192 pixels, not `{x}`")),
|
||||
};
|
||||
let (w, h) = (size("--width", 1280)?, size("--height", 800)?);
|
||||
// The display quality the bodies are meshed at, as the app's Display Quality setting: set
|
||||
// before the design opens, so every display plan is made at it.
|
||||
if let Some(q) = flag(args, "--quality") {
|
||||
use solidcraft_engine::doc::display_quality::{DisplayQuality, set_current};
|
||||
let level = DisplayQuality::parse(q).ok_or_else(|| {
|
||||
let ids: Vec<&str> = DisplayQuality::ALL.iter().map(|q| q.id()).collect();
|
||||
format!("--quality must be one of {}, not `{q}`", ids.join(", "))
|
||||
})?;
|
||||
set_current(level);
|
||||
}
|
||||
let mut s = open(flag(args, "--in"), positional(args), solidcraft_engine::schema::ParamCheck::Off)?;
|
||||
// How the bodies are drawn and where the model is cut, through the commands the app's
|
||||
// Appearance dialog and Section Analysis run (`preview`).
|
||||
@@ -615,9 +698,12 @@ fn cmd_snapshot(args: &[String]) -> Result<(), String> {
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
let cam = s.camera.unwrap_or_else(|| solidcraft_engine::view::home_camera(&s));
|
||||
let cam = match s.camera {
|
||||
Some(cam) => cam,
|
||||
None => solidcraft_engine::view::home_camera(&s).map_err(|e| e.to_string())?,
|
||||
};
|
||||
let guides = Guides { grid: !args.iter().any(|a| a == "--no-grid"), axes: !args.iter().any(|a| a == "--no-axes") };
|
||||
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides);
|
||||
let scene = solidcraft_engine::view::scene_with_guides(&s, &cam, s.section, guides).map_err(|e| e.to_string())?;
|
||||
let png = solidcraft_engine::capture::png(&scene, &cam, w, h)?;
|
||||
std::fs::write(out, &png.bytes).map_err(|e| format!("{out}: {e}"))?;
|
||||
let mut reply = json!({"path": out, "width": w, "height": h, "bytes": png.bytes.len(), "renderer": png.renderer});
|
||||
@@ -846,7 +932,7 @@ fn bench_ops(mut s: Session, script: &str) -> Result<Session, String> {
|
||||
let r = s.execute(id, p).map_err(|e| format!("{id}: {e}"))?;
|
||||
let ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let t = std::time::Instant::now();
|
||||
s.world_state().warm_meshes(|| true);
|
||||
s.world_state().map_err(|e| e.to_string())?.warm_meshes(|| true);
|
||||
let view_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
println!("{}", json!({"step": "op", "command": id, "ms": ms, "view_ms": view_ms}));
|
||||
Ok(r)
|
||||
@@ -902,7 +988,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let st = s.world_state();
|
||||
let st = s.world_state().map_err(|e| e.to_string())?;
|
||||
let place_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
// As the desktop app prepares a design it opened.
|
||||
st.warm_meshes(|| true);
|
||||
@@ -972,7 +1058,7 @@ pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
|
||||
let t = std::time::Instant::now();
|
||||
s.execute("parameters.change", &json!({"name": name, "expression": expr})).map_err(|e| e.to_string())?;
|
||||
let eval_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let st = s.world_state();
|
||||
let st = s.world_state().map_err(|e| e.to_string())?;
|
||||
let t = std::time::Instant::now();
|
||||
st.warm_meshes(|| true);
|
||||
let mesh_ms = t.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#![allow(clippy::needless_range_loop)]
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
use solidcore_record::Vec3;
|
||||
|
||||
use crate::spatial::{self, Tri};
|
||||
use crate::{read_json, recipe};
|
||||
@@ -13,28 +13,28 @@ use crate::{read_json, recipe};
|
||||
/// Default relative tolerance for volume and area (our curved-face tessellation is within ~2e-4).
|
||||
pub const REL_TOL: f64 = 1e-3;
|
||||
/// Default tolerance for centre of mass and bounding box, as a fraction of the bbox diagonal.
|
||||
const ABS_FRAC: f64 = 1e-3;
|
||||
pub(crate) const ABS_FRAC: f64 = 1e-3;
|
||||
/// Chord tolerance used to triangulate our part for the surface-distance check (mm).
|
||||
const MESH_TOL: f64 = 0.01;
|
||||
/// Relative tolerance on the principal second moments of the two triangulations.
|
||||
const INERTIA_REL: f64 = 2e-3;
|
||||
|
||||
/// What SolidCraft built: totals over every body.
|
||||
struct Got {
|
||||
body_count: usize,
|
||||
volume: f64,
|
||||
area: f64,
|
||||
com: [f64; 3],
|
||||
bbox: ([f64; 3], [f64; 3]),
|
||||
faces: u64,
|
||||
pub(crate) struct Got {
|
||||
pub(crate) body_count: usize,
|
||||
pub(crate) volume: f64,
|
||||
pub(crate) area: f64,
|
||||
pub(crate) com: [f64; 3],
|
||||
pub(crate) bbox: ([f64; 3], [f64; 3]),
|
||||
pub(crate) faces: u64,
|
||||
}
|
||||
|
||||
fn v3(v: &Value) -> Option<[f64; 3]> {
|
||||
pub(crate) fn v3(v: &Value) -> Option<[f64; 3]> {
|
||||
let a = v.as_array()?;
|
||||
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?])
|
||||
}
|
||||
|
||||
fn totals(m: &Value) -> Got {
|
||||
pub(crate) fn totals(m: &Value) -> Got {
|
||||
let bodies = m["bodies"].as_array().cloned().unwrap_or_default();
|
||||
let (mut volume, mut area, mut faces) = (0.0, 0.0, 0);
|
||||
let mut moment = [0.0; 3];
|
||||
@@ -57,21 +57,21 @@ fn totals(m: &Value) -> Got {
|
||||
Got { body_count: bodies.len(), volume, area, com, bbox: (lo, hi), faces }
|
||||
}
|
||||
|
||||
struct Checks {
|
||||
list: Vec<Value>,
|
||||
pass: bool,
|
||||
pub(crate) struct Checks {
|
||||
pub(crate) list: Vec<Value>,
|
||||
pub(crate) pass: bool,
|
||||
}
|
||||
|
||||
impl Checks {
|
||||
fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
|
||||
pub(crate) fn push(&mut self, what: &str, got: Value, want: Value, ok: bool) {
|
||||
self.pass &= ok;
|
||||
self.list.push(json!({"check": what, "got": got, "want": want, "ok": ok}));
|
||||
}
|
||||
fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
|
||||
pub(crate) fn rel(&mut self, what: &str, g: f64, w: Option<f64>, rel: f64) {
|
||||
let ok = w.is_some_and(|w| g.is_finite() && (g - w).abs() <= rel * w.abs().max(1.0));
|
||||
self.push(what, json!(g), json!(w), ok);
|
||||
}
|
||||
fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
|
||||
pub(crate) fn vec(&mut self, what: &str, g: [f64; 3], w: Option<[f64; 3]>, tol: f64) {
|
||||
let ok = w.is_some_and(|w| g.iter().zip(w).all(|(g, w)| g.is_finite() && (g - w).abs() <= tol));
|
||||
self.push(what, json!(g), json!(w), ok);
|
||||
}
|
||||
@@ -186,8 +186,8 @@ fn step_export_check(s: &solidcraft_engine::Session, volume: f64) -> (bool, Stri
|
||||
/// Our part as a triangle soup (curved faces refined onto the surface).
|
||||
fn our_mesh(s: &solidcraft_engine::Session) -> Result<Vec<Tri>, String> {
|
||||
let mut ours: Vec<Tri> = Vec::new();
|
||||
for b in &s.world_state().bodies {
|
||||
let m = b.body.tessellate_with(MESH_TOL, true).map_err(|e| e.to_string())?;
|
||||
for b in &s.world_state().map_err(|e| e.to_string())?.bodies {
|
||||
let m = b.body.mesh(&solidcraft_engine::kernel::MeshTolerance::chord(MESH_TOL)).map_err(|e| e.to_string())?;
|
||||
let p = |i: u32| m.positions.get(i as usize).map(|v: &Vec3| [v.x, v.y, v.z]);
|
||||
for t in &m.triangles {
|
||||
if let (Some(a), Some(b), Some(c)) = (p(t[0]), p(t[1]), p(t[2])) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//! `--profile` and `--profile-out FILE` for `run`, `eval` and `bench-open` (and
|
||||
//! `SOLIDCRAFT_PROFILE=1` for any command): record where the time goes, the kernel's calls to the
|
||||
//! commands, print the table to stderr when the command ends (stdout stays the command's JSON)
|
||||
//! and write the spans as a Chrome trace for chrome://tracing or Perfetto.
|
||||
|
||||
use solidcraft_engine::profile;
|
||||
|
||||
/// Rows the table prints.
|
||||
const TOP: usize = 30;
|
||||
|
||||
/// A recording that prints its table when finished.
|
||||
pub struct Profiling {
|
||||
trace: Option<String>,
|
||||
}
|
||||
|
||||
/// Start recording when `args` (after the command) ask for it, or `SOLIDCRAFT_PROFILE` does.
|
||||
pub fn start(args: &[String]) -> Result<Option<Profiling>, String> {
|
||||
let trace = match args.iter().position(|a| a == "--profile-out") {
|
||||
Some(i) => match args.get(i + 1) {
|
||||
Some(f) if !f.starts_with("--") => Some(f.clone()),
|
||||
_ => return Err("--profile-out needs a FILE".into()),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let asked = trace.is_some() || args.iter().any(|a| a == "--profile");
|
||||
if !asked && !solidcraft_engine::timing::profile_from_env() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !profile::COMPILED {
|
||||
return Err("this solidcraft-cli was built without the profiler (its `profile` feature)".into());
|
||||
}
|
||||
profile::reset();
|
||||
profile::set_enabled(true);
|
||||
Ok(Some(Profiling { trace }))
|
||||
}
|
||||
|
||||
impl Profiling {
|
||||
/// Stop recording, print the table on stderr and write the trace.
|
||||
pub fn finish(self) -> Result<(), String> {
|
||||
profile::set_enabled(false);
|
||||
eprint!("{}", profile::report().table(TOP));
|
||||
if let Some(path) = self.trace {
|
||||
let mut bytes = Vec::new();
|
||||
profile::write_chrome_trace(&mut bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
eprintln!("profile: trace written to {path} (open it in chrome://tracing or ui.perfetto.dev)");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
//! Each design runs in a child process (this test binary, [`action_matrix_worker`]) so an action
|
||||
//! that hangs or aborts is recorded as such and the rest of the design still runs.
|
||||
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -28,10 +29,10 @@ use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
use solidcraft_engine::doc::{FaceAttachmentStatus, ModelBody, naming};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
/// One in this many actions runs on pull requests (by a stable hash of its key).
|
||||
const PR_SAMPLE: u64 = 40;
|
||||
@@ -196,7 +197,7 @@ fn round3(x: f64) -> f64 {
|
||||
|
||||
/// Every action on every entity of the design's final bodies, in a fixed order.
|
||||
fn actions(s: &Session) -> Vec<Action> {
|
||||
let st = s.world_state();
|
||||
let st = s.world_state().unwrap();
|
||||
let placed = s.placed_document();
|
||||
let paths: BTreeMap<String, Vec<u64>> =
|
||||
placed.body_frames_with_source(&s.model.state()).into_iter().map(|(world, _, _, path, _)| (world, path)).collect();
|
||||
@@ -1338,7 +1339,9 @@ fn a_fillet_or_chamfer_on_one_edge_ending_at_a_cylinder() {
|
||||
fn a_round_on_a_knuckle_cap_circle() {
|
||||
for (cmd, size, removed) in [("solid.fillet", "radius", (2.4, 2.8)), ("solid.chamfer", "distance", (5.6, 6.3))] {
|
||||
let mut s = load(&root().join("tests/oracle/63-hinge-knuckle/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate(0.001).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.001)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let before = volume(&s);
|
||||
s.execute(cmd, &json!({"edges": [[40.0, 29.837_722, 6.449_490]], size: 0.728})).unwrap_or_else(|e| panic!("{cmd}: {e}"));
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{cmd}: {kind}: {e}"));
|
||||
@@ -1358,7 +1361,9 @@ fn a_round_on_a_knuckle_cap_circle() {
|
||||
#[test]
|
||||
fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
let mut s = load(&root().join("examples/regressions/loft_cut_beside_round_hole.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.005, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.005)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.133, "position": [357.22, -150.993_333, 730.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1375,7 +1380,9 @@ fn a_hair_thin_hole_on_a_ledge_beside_a_loft_cut() {
|
||||
#[test]
|
||||
fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
let mut s = load(&root().join("apps/solidcraft-cli/tests/corpus/modern-plastic.solidcraft")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 0.8, "position": [63.069_841_5, 28.333_333, 4.0]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1390,7 +1397,9 @@ fn a_small_hole_through_the_wall_of_a_plastic_rest() {
|
||||
#[test]
|
||||
fn a_hole_square_to_a_45_degree_flange() {
|
||||
let mut s = load(&root().join("tests/oracle/70-sm-flange-45/recipe.json")).unwrap();
|
||||
let volume = |s: &Session| s.model.state().bodies.iter().map(|b| b.body.tessellate_with(0.002, true).unwrap().measure().volume).sum::<f64>();
|
||||
let volume = |s: &Session| {
|
||||
s.model.state().bodies.iter().map(|b| b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.002)).unwrap().measure().volume).sum::<f64>()
|
||||
};
|
||||
let before = volume(&s);
|
||||
s.execute("solid.hole", &json!({"diameter": 3.448, "position": [66.666_667, -4.511_845, 4.511_845]})).unwrap();
|
||||
check(&s, 0, &BTreeMap::new()).unwrap_or_else(|(kind, e)| panic!("{kind}: {e}"));
|
||||
@@ -1648,7 +1657,7 @@ impl TipConstruction {
|
||||
other => panic!("expected an axis, got {other:?}"),
|
||||
}
|
||||
}
|
||||
fn plane(&self) -> solidcraft_engine::geom::Plane {
|
||||
fn plane(&self) -> solidcore_record::Plane {
|
||||
match &self.0 {
|
||||
solidcraft_engine::doc::construct::ConstructGeom::Plane(p) => *p,
|
||||
other => panic!("expected a plane, got {other:?}"),
|
||||
@@ -1728,7 +1737,7 @@ fn the_ribbed_lid_builds_the_same_body_every_time() {
|
||||
st.bodies
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let mesh = b.body.tessellate(0.1).unwrap();
|
||||
let mesh = b.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.1)).unwrap();
|
||||
let bits = mesh.positions.iter().fold(0u64, |acc, p| {
|
||||
acc.rotate_left(7).wrapping_add(p.x.to_bits() ^ p.y.to_bits().rotate_left(21) ^ p.z.to_bits().rotate_left(42))
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use serde_json::Value;
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::{Session, io, kernel};
|
||||
|
||||
fn assert_two_through_holes(session: &Session, stage: &str) {
|
||||
@@ -26,8 +26,8 @@ fn assert_two_through_holes(session: &Session, stage: &str) {
|
||||
let expected = 2.0 * PI * 4.0_f64.powi(2) * 6.0;
|
||||
assert!((removed - expected).abs() < expected * 1e-3, "{stage}: the two holes remove {removed} mm³, expected {expected} mm³");
|
||||
|
||||
let before_mesh = uncut.tessellate(0.01).unwrap();
|
||||
let after_mesh = bracket.tessellate(0.01).unwrap();
|
||||
let before_mesh = uncut.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let after_mesh = bracket.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
let down = Vec3::new(0.0, 0.0, -1.0);
|
||||
for y in [-15.0, -45.0] {
|
||||
let center = Vec3::new(25.0, y, 1.0);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Design file corpus
|
||||
|
||||
Designs saved by older SolidCraft builds. `tests/design_corpus.rs` opens each one with the
|
||||
current build and checks that it evaluates as it did when saved (the `.expected.json` next to
|
||||
it is `solidcraft-cli eval` of that file by the build that saved it: bodies, volumes, areas,
|
||||
world bounding boxes), that it is upgraded to the current file format, and that damaged and
|
||||
hostile variants of it never panic.
|
||||
current build and checks that it is upgraded to the current file format, that it evaluates
|
||||
(a feature may fail only by asking for a reference it lost to be picked again; what it rebuilds
|
||||
to is today's), and that damaged and hostile variants of it never panic. The `.expected.json`
|
||||
next to each is `solidcraft-cli eval` of that file by the build that saved it (bodies, volumes,
|
||||
areas, world bounding boxes), kept as a record of what that build made; no test compares with
|
||||
it.
|
||||
|
||||
| File | Saved by | Era |
|
||||
|---|---|---|
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 0.131943,
|
||||
@@ -104,10 +100,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 1.0812709999999999,
|
||||
@@ -118,10 +110,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.450002,
|
||||
@@ -132,10 +120,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 5.191514000000001,
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 1.178957,
|
||||
@@ -68,10 +64,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 83.24342399999999,
|
||||
@@ -82,10 +74,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.150889,
|
||||
@@ -96,10 +84,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 94.465481,
|
||||
@@ -110,10 +94,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 5,
|
||||
"ms": 50.426736,
|
||||
@@ -124,10 +104,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 6,
|
||||
"ms": 40.383005,
|
||||
@@ -138,10 +114,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 7,
|
||||
"ms": 59.041461000000005,
|
||||
@@ -152,18 +124,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [
|
||||
{
|
||||
"face_shift_mm": 0.0,
|
||||
"rotation_rad": 0.0,
|
||||
"strategy": "planar_fallback",
|
||||
"tolerance_mm": 0.0,
|
||||
"translation_mm": 0.0
|
||||
}
|
||||
],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 8,
|
||||
"ms": 562.5477970000001,
|
||||
@@ -171,13 +131,9 @@
|
||||
"rolled_back": false,
|
||||
"suppressed": false,
|
||||
"type": "BoxFeature",
|
||||
"warning": "Boolean retry: exact planar fallback (no displacement)"
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 9,
|
||||
"ms": 89.56076,
|
||||
@@ -188,10 +144,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 10,
|
||||
"ms": 65.330634,
|
||||
@@ -202,10 +154,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 11,
|
||||
"ms": 97.229403,
|
||||
|
||||
@@ -99,10 +99,6 @@
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 1,
|
||||
"ms": 0.204585,
|
||||
@@ -113,10 +109,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 2,
|
||||
"ms": 2.318662,
|
||||
@@ -127,10 +119,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 3,
|
||||
"ms": 0.109196,
|
||||
@@ -141,10 +129,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 4,
|
||||
"ms": 0.338859,
|
||||
@@ -155,10 +139,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 5,
|
||||
"ms": 0.312259,
|
||||
@@ -169,10 +149,6 @@
|
||||
"warning": null
|
||||
},
|
||||
{
|
||||
"boolean_diagnostics": {
|
||||
"events": [],
|
||||
"truncated": false
|
||||
},
|
||||
"error": null,
|
||||
"id": 6,
|
||||
"ms": 0.23144699999999999,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open and evaluate as
|
||||
//! they did when they were saved; damaged and hostile design files never panic.
|
||||
//! Designs saved by older SolidCraft builds (tests/corpus, see its README) open, are upgraded to
|
||||
//! the current format and evaluate (what they rebuild to today is today's, not what the build
|
||||
//! that saved them made); damaged and hostile design files never panic.
|
||||
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
@@ -21,67 +23,24 @@ fn open(path: &Path) -> Session {
|
||||
s
|
||||
}
|
||||
|
||||
/// Differences that are deliberate behaviour changes made before the file format had versions
|
||||
/// (no migration can tell those files apart): (file, check, why).
|
||||
const KNOWN: [(&str, &str, &str); 2] = [
|
||||
(
|
||||
"m0-revolve-primitives",
|
||||
"Body1 bbox",
|
||||
"partial revolves turn toward the sketch normal since ac43365 (as most parametric CAD tools do); M0 turned them the other way",
|
||||
),
|
||||
("components-arm", "body_count", "Measure counted only the root component's bodies then; it measures every placed body now"),
|
||||
];
|
||||
|
||||
fn known(file: &str, check: &str) -> bool {
|
||||
KNOWN.iter().any(|(f, c, _)| *f == file && *c == check)
|
||||
}
|
||||
|
||||
/// Volumes and areas then came from coarser tessellation (and curved booleans and fillets have
|
||||
/// become exact since): the same part within this relative tolerance (the oracle's).
|
||||
const REL: f64 = 1e-3;
|
||||
|
||||
fn close(a: f64, b: f64, rel: f64) -> bool {
|
||||
(a - b).abs() <= rel * b.abs().max(1.0)
|
||||
}
|
||||
|
||||
/// Every corpus design opens, is upgraded to the current format, evaluates, and saves and reads
|
||||
/// back the same. A feature may fail only by asking for a reference it lost to be picked again
|
||||
/// (a name saved before format 14 finds nothing today).
|
||||
#[test]
|
||||
fn old_designs_open_and_evaluate_as_saved() {
|
||||
fn old_designs_open_upgrade_and_evaluate() {
|
||||
let mut problems = Vec::new();
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let want: Value = serde_json::from_slice(&std::fs::read(p.with_extension("expected.json")).unwrap()).unwrap();
|
||||
let mut s = open(&p);
|
||||
for r in &s.model.results {
|
||||
if let Some(e) = &r.error {
|
||||
if let Some(e) = r.error.as_deref().filter(|e| solidcraft_engine::doc::reselect_parts(e).is_none()) {
|
||||
problems.push(format!("{name}: feature {}: {e}", r.id));
|
||||
}
|
||||
}
|
||||
assert_eq!(s.doc.format, solidcraft_engine::doc::format::format_string(solidcraft_engine::doc::format::FORMAT), "{name}: upgraded");
|
||||
let got = s.execute("inspect.measure", &json!({})).unwrap();
|
||||
if got["body_count"] != want["body_count"] && !known(&name, "body_count") {
|
||||
problems.push(format!("{name}: {} bodies, saved with {}", got["body_count"], want["body_count"]));
|
||||
}
|
||||
for w in want["bodies"].as_array().into_iter().flatten() {
|
||||
let bname = w["name"].as_str().unwrap_or_default();
|
||||
let Some(g) = got["bodies"].as_array().into_iter().flatten().find(|g| g["name"] == w["name"]) else {
|
||||
problems.push(format!("{name}: body {bname} missing"));
|
||||
continue;
|
||||
};
|
||||
for k in ["volume_mm3", "area_mm2"] {
|
||||
let (a, b) = (g[k].as_f64().unwrap_or(f64::NAN), w[k].as_f64().unwrap_or(f64::NAN));
|
||||
if !close(a, b, REL) {
|
||||
problems.push(format!("{name}: {bname} {k} {a} (saved {b})"));
|
||||
}
|
||||
}
|
||||
if known(&name, &format!("{bname} bbox")) {
|
||||
continue;
|
||||
}
|
||||
for (end, i) in [("min", 0), ("min", 1), ("min", 2), ("max", 0), ("max", 1), ("max", 2)] {
|
||||
let (a, b) = (g["bbox"][end][i].as_f64().unwrap_or(f64::NAN), w["bbox"][end][i].as_f64().unwrap_or(f64::NAN));
|
||||
if (a - b).abs() > 1e-3 * b.abs().max(1.0) {
|
||||
problems.push(format!("{name}: {bname} bbox {end}[{i}] {a} (saved {b})"));
|
||||
}
|
||||
}
|
||||
if got["body_count"].as_u64().unwrap_or(0) == 0 {
|
||||
problems.push(format!("{name}: no bodies"));
|
||||
}
|
||||
// Saved again, it reads back the same.
|
||||
let again = solidcraft_engine::doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
@@ -94,40 +53,48 @@ fn old_designs_open_and_evaluate_as_saved() {
|
||||
}
|
||||
|
||||
/// Names saved before format 14 were worked out from geometry, pieces of a split face numbered by
|
||||
/// where their centres lay (`F2:top#2`). They keep naming the faces they named: marked as such,
|
||||
/// they are found as the build that saved them found them, without new notes.
|
||||
/// where their centres lay (`F2:top#2`). They open marked as such and find no face or edge today:
|
||||
/// each feature holding one fails, asking for its reference to be picked again, and the rest of
|
||||
/// the design builds.
|
||||
#[test]
|
||||
fn names_saved_before_exact_history_find_the_faces_they_named() {
|
||||
fn names_saved_before_exact_history_ask_for_their_faces_again() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/naming-split-pieces.solidcraft");
|
||||
let s = open(&path);
|
||||
let feature = |name: &str| s.doc.features.iter().find(|f| f.name == name).unwrap_or_else(|| panic!("{name}"));
|
||||
assert_eq!(feature("Round").edge_names, vec!["?F2:side:l2|F2:top#2".to_string()]);
|
||||
assert_eq!(feature("Bevel").edge_names, vec!["?F2:side:l4|F2:top#1".to_string()]);
|
||||
assert_eq!(feature("Raise").face_names, vec!["?F2:top#1".to_string()]);
|
||||
for r in &s.model.results {
|
||||
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||
let asked = |name: &str| {
|
||||
let r = s.model.result(feature(name).id).unwrap_or_else(|| panic!("{name}"));
|
||||
r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts).map(|(f, reference)| (f.to_string(), reference.to_string()))
|
||||
};
|
||||
assert_eq!(asked("Round"), Some(("Round".into(), "?F2:side:l2|F2:top#2".into())));
|
||||
assert_eq!(asked("Bevel"), Some(("Bevel".into(), "?F2:side:l4|F2:top#1".into())));
|
||||
assert_eq!(asked("Raise"), Some(("Raise".into(), "?F2:top#1".into())));
|
||||
for r in s.model.results.iter().filter(|r| !["Round", "Bevel", "Raise"].contains(&r.name.as_str())) {
|
||||
assert!(r.error.is_none(), "{}: {:?}", r.name, r.error);
|
||||
}
|
||||
// The raised piece is the left one, as saved (the stored point is on it).
|
||||
let state = s.model.state();
|
||||
let solid = |p: [f64; 3]| state.bodies.iter().any(|b| b.body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2])));
|
||||
assert!(solid([7.0, 15.0, 11.0]) && !solid([32.0, 15.0, 11.0]));
|
||||
assert!(!s.model.state().bodies.is_empty());
|
||||
}
|
||||
|
||||
/// Body names saved before format 14 name the pieces of a split body in the kernel's shell order.
|
||||
/// The features after the split that refer to a piece by its body name still find that piece:
|
||||
/// the far one stays `Body1` (chamfered), the near one `Body1 (1)` (filleted, coloured).
|
||||
/// Body names saved before format 14 named the pieces of a split body in the kernel's shell
|
||||
/// order: such a design opens with every feature marked `shell_piece_order` and evaluates, each
|
||||
/// body name it stores naming a piece. (Which piece takes which name is not promised to be as it
|
||||
/// was saved.) Its fillet and chamfer hold edge names saved then, which find nothing today: they
|
||||
/// ask for their edges to be picked again.
|
||||
#[test]
|
||||
fn body_names_saved_before_exact_history_keep_their_pieces() {
|
||||
fn body_names_saved_before_exact_history_open_marked_and_evaluate() {
|
||||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/split-body-pieces.solidcraft");
|
||||
let s = open(&path);
|
||||
assert!(s.doc.features.iter().all(|f| f.shell_piece_order), "every saved feature keeps the shell order");
|
||||
for r in &s.model.results {
|
||||
assert!(r.error.is_none() && r.warning.is_none(), "{}: {:?} {:?}", r.name, r.error, r.warning);
|
||||
let asked = r.error.as_deref().and_then(solidcraft_engine::doc::reselect_parts);
|
||||
assert!(r.error.is_none() || asked.is_some_and(|(_, reference)| reference.starts_with('?')), "{}: {:?}", r.name, r.error);
|
||||
}
|
||||
let state = s.model.state();
|
||||
let piece = |name: &str| state.bodies.iter().find(|b| b.name == name).unwrap_or_else(|| panic!("{name}"));
|
||||
let solid = |name: &str, p: [f64; 3]| piece(name).body.contains(solidcraft_engine::geom::Vec3::new(p[0], p[1], p[2]));
|
||||
assert!(solid("Body1", [20.0, 24.0, 5.0]) && solid("Body1 (1)", [20.0, 6.0, 5.0]));
|
||||
for name in ["Body1", "Body1 (1)"] {
|
||||
assert!(state.bodies.iter().any(|b| b.name == name), "{name}");
|
||||
}
|
||||
assert!(s.doc.appearances.bodies.contains_key("Body1 (1)"));
|
||||
}
|
||||
|
||||
@@ -173,6 +140,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
for p in corpus() {
|
||||
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
let mut s = open(&p);
|
||||
let failed_on_open: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
let state = s.model.state();
|
||||
let placed = s.placed_document();
|
||||
let mut picks = Vec::new();
|
||||
@@ -180,7 +148,7 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let Some(body) = state.bodies.iter().find(|b| b.definition_name() == definition && placed.model_body_component(b) == component) else {
|
||||
continue;
|
||||
};
|
||||
let mesh = body.body.tessellate(0.01).unwrap();
|
||||
let mesh = body.body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
for index in 0..body.body.face_count() {
|
||||
if body.body.planar_face(index).is_none() {
|
||||
continue;
|
||||
@@ -224,7 +192,8 @@ fn flat_face_attachments() -> Vec<(String, String, usize, Option<(u32, bool)>, O
|
||||
let known = known_without_attachment(&name, &body, normal, distance);
|
||||
out.push((name.clone(), body, index, known, problem));
|
||||
}
|
||||
assert!(s.model.results.iter().all(|r| r.error.is_none()), "{name}: left with errors after undo");
|
||||
let failed: Vec<u64> = s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect();
|
||||
assert_eq!(failed, failed_on_open, "{name}: undo leaves the features that failed on opening, and only those, failed");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -263,3 +263,110 @@ mod setup {
|
||||
assert!(json_of(&pkg.join("claude_desktop_config.json"))["mcpServers"]["solidcraft"].is_object());
|
||||
}
|
||||
}
|
||||
|
||||
/// Scratch files belong to the CLI process; exercise real startup, requests and shutdown.
|
||||
mod scratch_cleanup {
|
||||
use super::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
fn command(case: &str) -> (PathBuf, Command) {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-mcp-lifecycle-{case}-{}", std::process::id()));
|
||||
std::fs::create_dir(&dir).unwrap();
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
command
|
||||
.args(["mcp", "--headless"])
|
||||
.env("TMPDIR", &dir)
|
||||
.env("TMP", &dir)
|
||||
.env("TEMP", &dir)
|
||||
.env("SOLIDCRAFT_RECOVERY_DIR", dir.join("recovery"))
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
(dir, command)
|
||||
}
|
||||
|
||||
fn scratch_folders(dir: &Path) -> Vec<PathBuf> {
|
||||
std::fs::read_dir(dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.filter(|path| path.file_name().unwrap().to_string_lossy().starts_with("solidcraft-mcp-"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn initialize() -> Value {
|
||||
json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18"}})
|
||||
}
|
||||
|
||||
fn run(mut command: Command, request: Value) -> std::process::Output {
|
||||
let mut child = command.spawn().unwrap();
|
||||
writeln!(child.stdin.take().unwrap(), "{request}").unwrap();
|
||||
child.wait_with_output().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof_removes_only_the_process_scratch_folder() {
|
||||
let (dir, command) = command("eof");
|
||||
let unrelated = dir.join("solidcraft-mcp-unrelated");
|
||||
std::fs::create_dir(&unrelated).unwrap();
|
||||
let out = run(command, initialize());
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||
assert!(reply.get("result").is_some(), "{reply}");
|
||||
assert_eq!(scratch_folders(&dir), vec![unrelated]);
|
||||
assert!(dir.join("recovery").is_dir(), "cleanup must leave the recovery folder alone");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn startup_error_removes_the_process_scratch_folder() {
|
||||
let (dir, mut command) = command("startup-error");
|
||||
let out = command.args(["--param-check", "invalid"]).stdin(Stdio::null()).output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(!out.status.success());
|
||||
assert!(stderr.contains("--param-check wants off, warn or strict"), "{stderr}");
|
||||
assert!(scratch_folders(&dir).is_empty(), "startup error left a scratch folder in {dir:?}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shutdown_preserves_nonempty_scratch_folder() {
|
||||
let (dir, mut command) = command("nonempty");
|
||||
let mut child = command.spawn().unwrap();
|
||||
let mut input = child.stdin.take().unwrap();
|
||||
writeln!(input, "{}", initialize()).unwrap();
|
||||
let mut reply = String::new();
|
||||
BufReader::new(child.stdout.take().unwrap()).read_line(&mut reply).unwrap();
|
||||
let reply: Value = serde_json::from_str(&reply).unwrap();
|
||||
assert!(reply.get("result").is_some(), "{reply}");
|
||||
let scratch = scratch_folders(&dir);
|
||||
assert_eq!(scratch.len(), 1);
|
||||
let saved = scratch[0].join("keep.txt");
|
||||
std::fs::write(&saved, "saved output").unwrap();
|
||||
drop(input);
|
||||
let out = child.wait_with_output().unwrap();
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
assert_eq!(std::fs::read_to_string(saved).unwrap(), "saved output");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_screenshot_scratch_folder_is_removed_without_a_path_scope() {
|
||||
let (dir, mut command) = command("screenshot");
|
||||
command.arg("--allow-any-path");
|
||||
// A headless server cannot capture a window. The request still allocates a scratch
|
||||
// path before the backend refuses it, without relying on a GPU or installed renderer.
|
||||
let out = run(
|
||||
command,
|
||||
json!({
|
||||
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
|
||||
"params": {"name": "screenshot", "arguments": {"source": "window"}},
|
||||
}),
|
||||
);
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let reply: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||
assert_eq!(reply["result"]["isError"], true, "{reply}");
|
||||
assert!(reply["result"]["content"][0]["text"].as_str().unwrap().contains("the window gave no picture"), "{reply}");
|
||||
assert!(scratch_folders(&dir).is_empty(), "failed screenshot left a scratch folder in {dir:?}");
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ fn model(name: &str, s: &Session) -> Value {
|
||||
.iter()
|
||||
.map(|r| json!({"id": r.id, "name": r.name, "error": r.error, "warning": r.warning, "skipped": r.skipped, "state": state(name, &r.state)}))
|
||||
.collect();
|
||||
json!({"results": results, "world": state(name, &s.world_state())})
|
||||
json!({"results": results, "world": state(name, &s.world_state().unwrap())})
|
||||
}
|
||||
|
||||
/// The same edit on both: a parameter's value when the design has one, else a new body.
|
||||
@@ -289,6 +289,60 @@ fn undoing_an_edit_after_opening_reuses_the_saved_results() {
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
/// A saved model holds the results of the body check mode it was evaluated under: opened under
|
||||
/// the same mode it is taken as it is, under another the design is evaluated again.
|
||||
#[test]
|
||||
fn a_model_saved_under_another_check_mode_is_evaluated_again() {
|
||||
let plate = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Sample Plate").unwrap();
|
||||
let mut built = Session::default();
|
||||
for c in (plate.script)()["commands"].as_array().unwrap() {
|
||||
built.execute(c["command"].as_str().unwrap(), &c["params"]).unwrap();
|
||||
}
|
||||
let path = temp("mode");
|
||||
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let same = open(&path);
|
||||
assert_eq!(same.model.validation(), built.model.validation());
|
||||
assert_eq!(same.model.recomputed, 0, "taken from the file");
|
||||
let mut other = Session::default();
|
||||
other.set_validation(solidcraft_engine::kernel::Validation::EachStep);
|
||||
assert_ne!(other.validation(), built.model.validation());
|
||||
other.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
assert_eq!(other.validation(), solidcraft_engine::kernel::Validation::EachStep);
|
||||
assert!(other.model.recomputed > 0, "evaluated again under its own mode");
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
/// A body made by rounds keeps, in the saved model, the rounds and the body they were made on: a
|
||||
/// later round at the corner where they meet blends them again with it whether the design was
|
||||
/// opened from its model or evaluated anew. Two edges of a 20 mm box rounded r2 (a mitre at the
|
||||
/// corner (20, 0, 20)), saved; opened, the upright edge there rounded r3: the corner all three
|
||||
/// rounded at once make (SMLib's setback corner, kernel `three_rounds_at_a_corner`), 15 faces and
|
||||
/// 7928.142563938 mm³, the same body as the design evaluated anew.
|
||||
#[test]
|
||||
fn a_later_round_after_opening_blends_the_saved_rounds_again() {
|
||||
let mut built = Session::default();
|
||||
built.execute("solid.box", &json!({"length": 20, "width": 20, "height": 20, "body_name": "Box"})).unwrap();
|
||||
built.execute("solid.fillet", &json!({"edges": [[20, 10, 20], [10, 0, 20]], "radius": 2})).unwrap();
|
||||
let path = temp("later-round");
|
||||
built.execute("file.save_as", &json!({"path": path.to_string_lossy()})).unwrap();
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let mut cached = open(&path);
|
||||
assert_eq!(cached.model.last_recomputed, 0, "opened without evaluating");
|
||||
let mut rebuilt = Session::new(solidcraft_engine::io::read_design(&bytes).unwrap());
|
||||
for s in [&mut cached, &mut rebuilt] {
|
||||
s.execute("solid.fillet", &json!({"edges": [[20, 0, 10]], "radius": 3})).unwrap();
|
||||
let st = s.model.state();
|
||||
let b = &st.body("Box").unwrap().body;
|
||||
let v = solidcraft_engine::kernel::measure(b).unwrap().volume;
|
||||
assert_eq!(b.face_count(), 15);
|
||||
assert!((v - 7928.142563938).abs() < 1e-6, "{v}");
|
||||
}
|
||||
assert_eq!(model("later round", &cached), model("later round", &rebuilt));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let _ = std::fs::remove_file(format!("{}.bak", path.display()));
|
||||
}
|
||||
|
||||
/// A design saved before body names were pinned (#6403) pins them as it opens, as its first
|
||||
/// command would, and opening it is no change. That command pinned them as part of itself, so
|
||||
/// undoing it went back to the unpinned design, which no evaluated result matched: the first
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
//! Every design of the corpus loads, two builds of it name its faces and edges alike, and a
|
||||
//! name it stores that no longer finds its face or edge fails its feature. The stored names are
|
||||
//! the fillet and chamfer edges, the faces a feature picks, the face a sketch sits on and the
|
||||
//! sheet metal edges; one that finds nothing (`gone`, a sketch's face `lost`) must leave its
|
||||
//! feature failed (it asks for the reference to be picked again, never takes another face or
|
||||
//! edge), and a design that no longer loads fails the test. A build's names are each face's and
|
||||
//! edge's name with where it is (its area and centre, its length and middle); the second build
|
||||
//! must give the same, and its stored names must find the same faces and edges.
|
||||
//!
|
||||
//! Face and edge indices are left out: they are the kernel's order, which a rebuild need not
|
||||
//! keep (a fillet's or a boolean's faces come out in another order from one build to the next).
|
||||
//! How many faces a body has and what each is called is the kernel's business, not this test's.
|
||||
//!
|
||||
//! The corpus is the action matrix's (the old design files in `tests/corpus`, the examples, the
|
||||
//! demo and typical designs in `tests/action_matrix/designs`, the reference recipes), the sample
|
||||
//! designs, the engine's design fixtures and the older designs kept by the hole matrix, taper
|
||||
//! and projection tests.
|
||||
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::{FaceAttachmentStatus, Feature, FeatureKind, ModelBody, ModelState, PlaneRef, naming};
|
||||
|
||||
fn root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
|
||||
}
|
||||
|
||||
/// Where a design comes from.
|
||||
enum Source {
|
||||
File(PathBuf),
|
||||
Sample(fn() -> Value),
|
||||
}
|
||||
|
||||
/// Every design of the corpus: (`<group>/<design>`, where it comes from).
|
||||
fn designs() -> Vec<(String, Source)> {
|
||||
let root = root();
|
||||
let mut out = Vec::new();
|
||||
let mut add = |prefix: &str, dir: PathBuf, ext: &str| {
|
||||
let Ok(rd) = std::fs::read_dir(&dir) else { return };
|
||||
let mut v: Vec<PathBuf> = rd.flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == ext)).collect();
|
||||
v.sort();
|
||||
for p in v {
|
||||
let stem = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
|
||||
out.push((format!("{prefix}/{stem}"), Source::File(p)));
|
||||
}
|
||||
};
|
||||
add("corpus", root.join("apps/solidcraft-cli/tests/corpus"), "solidcraft");
|
||||
add("examples", root.join("examples"), "json");
|
||||
add("examples", root.join("examples/acceptance"), "json");
|
||||
add("regressions", root.join("examples/regressions"), "json");
|
||||
add("demo", root.join("examples/demo"), "json");
|
||||
add("typical", root.join("tests/action_matrix/designs"), "json");
|
||||
add("demo", root.join("tests/action_matrix/designs"), "solidcraft");
|
||||
add("fixtures", root.join("crates/engine/src/tests/fixtures"), "solidcraft");
|
||||
add("hole_matrix", root.join("tests/hole_matrix/old-0.9"), "json");
|
||||
add("taper", root.join("tests/taper/before-1014"), "json");
|
||||
add("projection", root.join("tests/projection/before-1508"), "json");
|
||||
let mut cases: Vec<PathBuf> =
|
||||
std::fs::read_dir(root.join("tests/oracle")).unwrap().flatten().map(|e| e.path()).filter(|p| p.join("recipe.json").is_file()).collect();
|
||||
cases.sort();
|
||||
for c in cases {
|
||||
out.push((format!("oracle/{}", c.file_name().unwrap_or_default().to_string_lossy()), Source::File(c.join("recipe.json"))));
|
||||
}
|
||||
for sample in solidcraft_engine::sample::samples() {
|
||||
let stem: String = sample.name.to_lowercase().split_whitespace().collect::<Vec<_>>().join("-");
|
||||
out.push((format!("samples/{stem}"), Source::Sample(sample.script)));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Build a design: a design file (also a design saved as JSON) is opened, a script is run.
|
||||
fn load(source: &Source) -> Result<Session, String> {
|
||||
let mut s = Session::default();
|
||||
let script = match source {
|
||||
Source::Sample(script) => script(),
|
||||
Source::File(path) if path.extension().is_some_and(|x| x == "solidcraft") => {
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
Source::File(path) if path.file_name().is_some_and(|n| n == "recipe.json") => {
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_solidcraft-cli")).arg("recipe").arg(path).output().map_err(|e| e.to_string())?;
|
||||
if !out.status.success() {
|
||||
return Err(format!("recipe: {}", String::from_utf8_lossy(&out.stderr).trim()));
|
||||
}
|
||||
serde_json::from_slice(&out.stdout).map_err(|e| e.to_string())?
|
||||
}
|
||||
Source::File(path) => {
|
||||
let v: Value = serde_json::from_slice(&std::fs::read(path).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
|
||||
if v.get("features").is_some() {
|
||||
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
match v.get("steps").and_then(Value::as_array) {
|
||||
// The demo's steps: an MCP `execute` each.
|
||||
Some(steps) => {
|
||||
json!({"commands": steps.iter().map(|st| json!({"command": st["command"], "params": st["params"]})).collect::<Vec<_>>()})
|
||||
}
|
||||
None => v,
|
||||
}
|
||||
}
|
||||
};
|
||||
s.run_script(&script).map_err(|e| e.to_string())?;
|
||||
if s.active_sketch.is_some() {
|
||||
let _ = s.execute("sketch.finish", &json!({}));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// What decides a name beyond its text: `[geometric]` when it was worked out from geometry, not
|
||||
/// exact history.
|
||||
fn flags(list: &[(bool, &str)]) -> String {
|
||||
let set: Vec<&str> = list.iter().filter(|(on, _)| *on).map(|(_, f)| *f).collect();
|
||||
if set.is_empty() { String::new() } else { format!(" [{}]", set.join(", ")) }
|
||||
}
|
||||
|
||||
/// Where each named face and edge of a body is, by name, sorted: a face's area and centre, an
|
||||
/// edge's length and middle, from the body's mesh. Two builds that name their faces alike give
|
||||
/// the same list.
|
||||
fn placed_names(b: &ModelBody) -> Vec<(String, [f64; 4])> {
|
||||
if b.body.is_mesh() {
|
||||
return Vec::new();
|
||||
}
|
||||
let m = b.mesh();
|
||||
let faces = naming::face_names(b);
|
||||
let mut sums = vec![[0.0f64; 4]; faces.len()];
|
||||
for (t, f) in m.triangles.iter().zip(&m.tri_face) {
|
||||
let [p, q, r] = t.map(|i| m.positions.get(i as usize).copied().unwrap_or(Vec3::ZERO));
|
||||
let area = (q - p).cross(r - p).len() / 2.0;
|
||||
let c = (p + q + r) / 3.0;
|
||||
if let Some(s) = sums.get_mut(*f as usize) {
|
||||
*s = [s[0] + area, s[1] + area * c.x, s[2] + area * c.y, s[3] + area * c.z];
|
||||
}
|
||||
}
|
||||
let mut out: Vec<(String, [f64; 4])> = faces
|
||||
.iter()
|
||||
.zip(&sums)
|
||||
.map(|(n, s)| {
|
||||
let a = s[0].max(1e-300);
|
||||
(format!("{} face {n}", b.name), [s[0], s[1] / a, s[2] / a, s[3] / a])
|
||||
})
|
||||
.collect();
|
||||
for (n, line) in naming::edge_names(b).iter().zip(&m.edges) {
|
||||
let length: f64 = line.windows(2).map(|w| w[1].dist(w[0])).sum();
|
||||
let mid = line.iter().fold(Vec3::ZERO, |acc, p| acc + *p) / (line.len().max(1) as f64);
|
||||
out.push((format!("{} edge {n}", b.name), [length, mid.x, mid.y, mid.z]));
|
||||
}
|
||||
out.sort_by(|x, y| x.0.cmp(&y.0).then(x.1.partial_cmp(&y.1).unwrap_or(std::cmp::Ordering::Equal)));
|
||||
out
|
||||
}
|
||||
|
||||
fn face_label(st: &ModelState, body: usize, face: usize) -> String {
|
||||
match st.bodies.get(body) {
|
||||
Some(b) => format!("{} face {}", b.name, naming::face_names(b).get(face).map(String::as_str).unwrap_or("?")),
|
||||
None => format!("body #{body} face #{face}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Every name feature `f` stores, with what it finds in the model before it, as a feature
|
||||
/// rebuilding finds it, and whether it finds anything.
|
||||
fn record_refs(out: &mut Vec<(String, bool)>, s: &Session, f: &Feature) {
|
||||
let doc = &*s.doc;
|
||||
let who = format!("ref {} ({})", f.name, f.id);
|
||||
let before = s.model.state_before(f.id);
|
||||
// Faces a feature picks by point and stores by name (shell, draft, hole, …).
|
||||
let explicit_emboss = matches!(&f.kind, FeatureKind::Emboss { face: Some(_), .. });
|
||||
if f.face_names.iter().any(|n| !n.is_empty()) {
|
||||
let fst = solidcraft_engine::doc::face_point_state(doc, &before, &f.kind, f.component).unwrap();
|
||||
for (k, (p, n)) in f.kind.face_points().into_iter().zip(&f.face_names).enumerate() {
|
||||
if n.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let (found, ok) = if explicit_emboss {
|
||||
("not checked (a cylindrical emboss finds its face on its own body)".to_string(), true)
|
||||
} else {
|
||||
match naming::locate_face_on_body(&fst, f.kind.face_body(), n, p) {
|
||||
Some(hit) => (format!("{}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])), true),
|
||||
None => ("gone".into(), false),
|
||||
}
|
||||
};
|
||||
out.push((format!("{who} face {k} `{n}` -> {found}"), ok));
|
||||
}
|
||||
}
|
||||
// Edges a feature picks by point and stores by name.
|
||||
if f.edge_names.iter().any(|n| !n.is_empty()) {
|
||||
let edge = |out: &mut Vec<(String, bool)>, b: &ModelBody, k: usize, n: &str, p: Vec3| {
|
||||
let (found, ok) = match naming::locate_edge(b, n, p) {
|
||||
Ok(hit) => (
|
||||
format!(
|
||||
"{} edge {}{}",
|
||||
b.name,
|
||||
naming::edge_names(b).get(hit.edge).map(String::as_str).unwrap_or("?"),
|
||||
flags(&[(!hit.verified, "geometric")])
|
||||
),
|
||||
true,
|
||||
),
|
||||
Err(e) => (format!("gone ({e})"), false),
|
||||
};
|
||||
out.push((format!("{who} edge {k} `{n}` -> {found}"), ok));
|
||||
};
|
||||
let named = |k: usize| f.edge_names.get(k).filter(|n| !n.is_empty());
|
||||
match &f.kind {
|
||||
FeatureKind::Fillet { edges, body, .. } | FeatureKind::Chamfer { edges, body, .. } => {
|
||||
let pts: Vec<Vec3> = solidcraft_engine::doc::plain(edges);
|
||||
match solidcraft_engine::doc::blend_target(&before, body, &pts).ok().and_then(|i| before.bodies.get(i)) {
|
||||
Some(b) => {
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
if let Some(n) = named(k) {
|
||||
edge(out, b, k, n, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => out.push((format!("{who} edges: no body to round"), false)),
|
||||
}
|
||||
}
|
||||
FeatureKind::SheetConvert { body, bends, .. } => match before.body(body) {
|
||||
Some(b) => {
|
||||
for (k, p) in solidcraft_engine::doc::plain::<Vec3, _>(bends).iter().enumerate() {
|
||||
if let Some(n) = named(k) {
|
||||
edge(out, b, k, n, *p);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => out.push((format!("{who} bends: body `{body}` gone"), false)),
|
||||
},
|
||||
// Sheet metal edges are named by the sheet's panels, and found on its sheets.
|
||||
FeatureKind::SheetFlange { .. } | FeatureKind::SheetHem { .. } => {
|
||||
let pts: Vec<Vec3> = match &f.kind {
|
||||
FeatureKind::SheetFlange { edges, .. } => edges.iter().map(|e| e.point.0).collect(),
|
||||
FeatureKind::SheetHem { edges, .. } => solidcraft_engine::doc::plain(edges),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for (k, p) in pts.iter().enumerate() {
|
||||
let Some(n) = named(k) else { continue };
|
||||
let on: Vec<&str> = before.sheets.iter().filter(|sh| sh.edge_by_name(n, *p).is_some()).map(|sh| sh.body.as_str()).collect();
|
||||
let found = if on.is_empty() { "gone".to_string() } else { format!("sheet edge on {}", on.join(", ")) };
|
||||
out.push((format!("{who} sheet edge {k} `{n}` -> {found}"), !on.is_empty()));
|
||||
}
|
||||
}
|
||||
other => {
|
||||
for (k, n) in f.edge_names.iter().enumerate().filter(|(_, n)| !n.is_empty()) {
|
||||
out.push((format!("{who} edge {k} `{n}` -> not checked ({})", other.type_name()), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The face a sketch sits on.
|
||||
if let FeatureKind::Sketch { plane: PlaneRef::Face { name, anchor, at, .. }, .. } = &f.kind {
|
||||
let mut ok = true;
|
||||
let status = match doc.sketch_face_attachment(f.id, &s.model) {
|
||||
FaceAttachmentStatus::Healthy { target, .. } => {
|
||||
let faces = match before.body(&target.body) {
|
||||
Some(b) => match doc.anchored_face_indices(&before, b, &target) {
|
||||
Ok((indices, note)) => {
|
||||
let names = naming::face_names(b);
|
||||
let mut list: Vec<&str> = indices.iter().map(|i| names.get(*i).map(String::as_str).unwrap_or("?")).collect();
|
||||
list.sort();
|
||||
format!("{} faces {}{}", b.name, list.join(", "), flags(&[(note.is_some(), "changed cap")]))
|
||||
}
|
||||
Err(e) => {
|
||||
ok = false;
|
||||
format!("no faces ({e})")
|
||||
}
|
||||
},
|
||||
None => {
|
||||
ok = false;
|
||||
format!("body `{}` gone", target.body)
|
||||
}
|
||||
};
|
||||
format!("healthy: {faces}")
|
||||
}
|
||||
FaceAttachmentStatus::Lost { message, .. } => {
|
||||
ok = false;
|
||||
format!("lost: {message}")
|
||||
}
|
||||
FaceAttachmentStatus::Legacy { .. } => match name {
|
||||
// The face its name finds, as the sketch finds it.
|
||||
Some(n) => {
|
||||
let fst = solidcraft_engine::doc::state_in_frame(doc, &before, f.component).unwrap();
|
||||
match naming::locate_face(&fst, n, *at) {
|
||||
Some(hit) => format!("by name: {}{}", face_label(&fst, hit.body, hit.face), flags(&[(!hit.verified, "geometric")])),
|
||||
None => {
|
||||
ok = false;
|
||||
"by name: gone".into()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => "by its pick".into(),
|
||||
},
|
||||
FaceAttachmentStatus::Sheet { .. } => "sheet face".into(),
|
||||
FaceAttachmentStatus::Fixed { .. } => "fixed".into(),
|
||||
};
|
||||
let stored = match (anchor, name) {
|
||||
(Some(a), _) => {
|
||||
let kind =
|
||||
a.target.certificate.as_ref().and_then(|c| serde_json::to_value(c).ok()).and_then(|v| v["kind"].as_str().map(String::from));
|
||||
format!("`{}` on {} ({})", a.target.name, a.target.body, kind.unwrap_or_else(|| "no certificate".into()))
|
||||
}
|
||||
(None, Some(n)) => format!("`{n}`"),
|
||||
(None, None) => "by its pick".into(),
|
||||
};
|
||||
out.push((format!("{who} plane {stored} -> {status}"), ok));
|
||||
}
|
||||
}
|
||||
|
||||
/// A build of a design: where its faces and edges are by name, and what every name it stores
|
||||
/// finds.
|
||||
struct Build {
|
||||
names: Vec<(String, [f64; 4])>,
|
||||
refs: Vec<(String, bool)>,
|
||||
}
|
||||
|
||||
fn build(s: &Session) -> Build {
|
||||
let names = s.model.state().bodies.iter().flat_map(placed_names).collect();
|
||||
let mut refs = Vec::new();
|
||||
for f in s.doc.features.iter().filter(|f| !f.suppressed) {
|
||||
let from = refs.len();
|
||||
record_refs(&mut refs, s, f);
|
||||
// A name that finds nothing is sound when its feature fails: it asks for the reference
|
||||
// to be picked again.
|
||||
if s.model.result(f.id).is_some_and(|r| r.error.is_some()) {
|
||||
refs.iter_mut().skip(from).for_each(|(_, ok)| *ok = true);
|
||||
}
|
||||
}
|
||||
Build { names, refs }
|
||||
}
|
||||
|
||||
/// Where two builds name their faces and edges apart (at most `cap` of them): a name only one
|
||||
/// has, or a face or edge of that name somewhere else (1e-6 of its size, to the mesh's rounding).
|
||||
fn differences(a: &Build, b: &Build, cap: usize) -> Vec<String> {
|
||||
let mut out = Vec::new();
|
||||
if a.names.len() != b.names.len() {
|
||||
out.push(format!("{} named faces and edges, then {}", a.names.len(), b.names.len()));
|
||||
}
|
||||
for ((n, x), (m, y)) in a.names.iter().zip(&b.names) {
|
||||
let scale = x.iter().chain(y).fold(1.0f64, |s, v| s.max(v.abs()));
|
||||
if n != m || x.iter().zip(y).any(|(p, q)| (p - q).abs() > 1e-6 * scale) {
|
||||
out.push(format!("{n} at {x:?}, then {m} at {y:?}"));
|
||||
}
|
||||
}
|
||||
for ((r, _), (q, _)) in a.refs.iter().zip(&b.refs) {
|
||||
if r != q {
|
||||
out.push(format!("{r}, then {q}"));
|
||||
}
|
||||
}
|
||||
if a.refs.len() != b.refs.len() {
|
||||
out.push(format!("{} stored names, then {}", a.refs.len(), b.refs.len()));
|
||||
}
|
||||
out.truncate(cap);
|
||||
out
|
||||
}
|
||||
|
||||
/// Designs whose names differ from one build to the next today: (design, why). Their
|
||||
/// differences are printed, not failed on, until the cause is fixed; a stored name that finds
|
||||
/// nothing still fails.
|
||||
const UNSTABLE: [(&str, &str); 1] = [(
|
||||
"regressions/closed_sweep_torus_cut",
|
||||
"the halves of each swept wall lie level along every axis, so which is `#1` and which `#2` follows the order the kernel gives the faces in, which changes from one build to the next",
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn corpus_designs_name_alike_every_build_and_fail_where_a_reference_is_gone() {
|
||||
let designs = designs();
|
||||
let next = std::sync::atomic::AtomicUsize::new(0);
|
||||
let results = std::sync::Mutex::new(Vec::new());
|
||||
let workers = std::thread::available_parallelism().map_or(4, |n| n.get()).min(designs.len().max(1));
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..workers {
|
||||
scope.spawn(|| {
|
||||
while let Some((name, source)) = designs.get(next.fetch_add(1, std::sync::atomic::Ordering::Relaxed)) {
|
||||
let got = load(source).and_then(|first| Ok((build(&first), build(&load(source)?))));
|
||||
results.lock().unwrap_or_else(|e| e.into_inner()).push((name.clone(), got));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
let mut results = results.into_inner().unwrap_or_else(|e| e.into_inner());
|
||||
results.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
let mut problems = Vec::new();
|
||||
for (name, got) in &results {
|
||||
let (first, second) = match got {
|
||||
Ok(builds) => builds,
|
||||
Err(e) => {
|
||||
problems.push(format!("{name}: does not load: {e}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let lost: Vec<&str> = first.refs.iter().filter(|(_, ok)| !ok).map(|(r, _)| r.as_str()).collect();
|
||||
if !lost.is_empty() {
|
||||
problems.push(format!("{name}: stored names that find nothing, in features that build:\n {}", lost.join("\n ")));
|
||||
}
|
||||
let apart = differences(first, second, 40);
|
||||
if !apart.is_empty() {
|
||||
match UNSTABLE.iter().find(|(design, _)| design == name) {
|
||||
Some((_, why)) => eprintln!("{name} (not checked: {why}):\n {}", apart.join("\n ")),
|
||||
None => problems.push(format!("{name}: two builds name it apart (first, then second):\n {}", apart.join("\n "))),
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(problems.is_empty(), "{} of {} design(s) of the corpus:\n{}", problems.len(), results.len(), problems.join("\n"));
|
||||
}
|
||||
@@ -9,6 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
|
||||
fn root() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
|
||||
@@ -31,7 +32,7 @@ struct Topology {
|
||||
|
||||
/// Every solid body's topology, by name.
|
||||
fn topology(s: &Session) -> BTreeMap<String, Topology> {
|
||||
let st = s.world_state();
|
||||
let st = s.world_state().unwrap();
|
||||
st.bodies
|
||||
.iter()
|
||||
.filter(|b| !b.body.is_mesh())
|
||||
@@ -142,16 +143,14 @@ fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
|
||||
/// face count after each feature, and each outer side (the four walls and the bottom) one face
|
||||
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
|
||||
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
|
||||
/// wall at 24) into strips along the openings' tops and bottoms.
|
||||
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): after
|
||||
/// each feature, each outer side (the four walls and the bottom) is one face and the case is as
|
||||
/// tall as its height, and a height changed in Parameters rebuilds the same topology as one
|
||||
/// built at that height: (a) built at height 24, (b) built at 24 and changed to 32, (c) built at
|
||||
/// 32. The port cut-outs and the floor vents used to split the long wall (or the end wall at 24)
|
||||
/// into strips along the openings' tops and bottoms.
|
||||
#[test]
|
||||
fn pi_case_faces_per_step_at_both_heights() {
|
||||
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
|
||||
const FACES: [(usize, usize); 13] =
|
||||
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
|
||||
fn pi_case_sides_stay_whole_at_both_heights() {
|
||||
let dir = scratch_dir("pi");
|
||||
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.sc")).unwrap()).unwrap();
|
||||
let at = |height: &str, file: &str| {
|
||||
@@ -174,12 +173,13 @@ fn pi_case_faces_per_step_at_both_heights() {
|
||||
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
|
||||
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
|
||||
let mut steps = Vec::new();
|
||||
for (k, want) in FACES {
|
||||
// Sketch1 (1) is no body; the box (2) .. RectangularPattern2 (14).
|
||||
for k in 2..=14 {
|
||||
let mut t = s.scratch();
|
||||
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
|
||||
let st = t.world_state();
|
||||
let st = t.world_state().unwrap();
|
||||
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
|
||||
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
|
||||
assert!(body.body.validity().is_empty(), "{case}: after feature {k}: {:?}", body.body.validity());
|
||||
// Each outer side: the faces in its plane.
|
||||
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
|
||||
let on = (0..body.body.face_count())
|
||||
|
||||
@@ -229,7 +229,9 @@ fn known_pending_concepts_and_released_files_round_trip_with_inert_metadata() {
|
||||
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);
|
||||
// #4907: `run --save` maps to Save As, and a borrowed design name
|
||||
// follows the destination file's stem instead of keeping the old name.
|
||||
assert_eq!(reopened.name, format!("resaved-{index}"));
|
||||
assert_eq!(reopened.features, document.features);
|
||||
assert_eq!(std::fs::read(&source).unwrap(), bytes);
|
||||
}
|
||||
|
||||
@@ -65,7 +65,10 @@ fn allow_errors_writes_anyway_and_a_saved_design_keeps_failing() {
|
||||
let output = cli().arg("run").arg(dir.broken_script()).arg("--allow-errors").arg("--out").arg(&step).arg("--save").arg(&saved).output().unwrap();
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
let measured: Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(measured["total"]["volume_mm3"], json!(4000.0), "the box without its fillet");
|
||||
// SMLib integrates the volume (a relative accuracy of 1e-4 asked for; a box comes out within
|
||||
// a few units in the last place), so the box's 4000 mm³ is checked to 1e-9, not to the bit.
|
||||
let volume = measured["total"]["volume_mm3"].as_f64().unwrap();
|
||||
assert!((volume - 4000.0).abs() < 4000.0 * 1e-9, "the box without its fillet: {volume}");
|
||||
assert!(std::fs::metadata(&step).unwrap().len() > 0);
|
||||
// The saved design still holds the failing feature: running it fails too, `eval` reports it.
|
||||
let again = dir.0.join("again.step");
|
||||
|
||||
@@ -123,3 +123,42 @@ fn a_design_naming_a_sketch_and_its_extrude_alike_replays_from_its_script() {
|
||||
assert_eq!(names, ["Block", "Block"], "{first:#}");
|
||||
assert_eq!(second["commands"], first["commands"], "the replayed design's script has the same steps");
|
||||
}
|
||||
|
||||
/// #10230: script export must disclose the print-face state that replay leaves out.
|
||||
#[test]
|
||||
fn script_export_warns_about_omitted_print_faces_on_stderr() {
|
||||
let dir = std::env::temp_dir().join(format!("solidcraft-print-face-script-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let saved = dir.join("block.sc");
|
||||
let exported = dir.join("block.json");
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 20, "width": 10, "height": 5, "body_name": "Block"})).unwrap();
|
||||
s.execute("body.print_face", &json!({"body": "Block", "point": [10, 5, 0]})).unwrap();
|
||||
s.execute("file.save", &json!({"path": saved})).unwrap();
|
||||
let native = std::fs::read(&saved).unwrap();
|
||||
|
||||
for to_file in [false, true] {
|
||||
let mut cmd = std::process::Command::new(env!("CARGO_BIN_EXE_solidcraft-cli"));
|
||||
cmd.arg("script").arg(&saved);
|
||||
if to_file {
|
||||
cmd.arg("--out").arg(&exported);
|
||||
}
|
||||
let out = cmd.output().unwrap();
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(out.status.success(), "{stderr}");
|
||||
assert!(stderr.contains("note: the script leaves out print-face assignments (body.print_face)"), "{stderr}");
|
||||
let script = if to_file {
|
||||
assert!(out.stdout.is_empty(), "--out must keep stdout empty");
|
||||
std::fs::read(&exported).unwrap()
|
||||
} else {
|
||||
out.stdout
|
||||
};
|
||||
let script: Value = serde_json::from_slice(&script).unwrap();
|
||||
let mut replayed = Session::default();
|
||||
replayed.run_script(&script).unwrap();
|
||||
assert!(replayed.doc.print_faces.is_empty());
|
||||
assert_eq!(replayed.doc.features, s.doc.features);
|
||||
assert_eq!(std::fs::read(&saved).unwrap(), native, "export must not change the source design");
|
||||
}
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
|
||||
@@ -171,9 +171,9 @@ fn a_saved_view_is_its_camera_and_a_standard_view_is_unchanged() {
|
||||
let top = dir.snapshot(&script, "top.png", &["--view", "top"]);
|
||||
let mut s = Session::default();
|
||||
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s);
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
|
||||
cam.set_view(solidcraft_engine::render::StandardView::Top);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
|
||||
assert_same(&top, &solidcraft_engine::capture::png(&scene, &cam, SIZE.0, SIZE.1).unwrap().bytes, "the home camera turned to the top");
|
||||
assert_same(&top, &dir.mcp(&script, "mcp-top.png", json!({"view": "top"})), "the MCP top view");
|
||||
assert_differs(&top, &saved, "top and the saved view");
|
||||
@@ -288,7 +288,7 @@ fn a_snapshot_shows_the_part_where_the_camera_puts_it() {
|
||||
let vp = c.view_proj(f64::from(SIZE.0) / f64::from(SIZE.1), 1000.0);
|
||||
let (mut elo, mut ehi) = ([f64::MAX; 2], [f64::MIN; 2]);
|
||||
for k in 0..8 {
|
||||
let p = solidcraft_engine::geom::Vec3::new(
|
||||
let p = solidcore_record::Vec3::new(
|
||||
corner[0] + size[0] * f64::from(k & 1),
|
||||
corner[1] + size[1] * f64::from((k >> 1) & 1),
|
||||
corner[2] + size[2] * f64::from((k >> 2) & 1),
|
||||
@@ -335,9 +335,9 @@ fn a_forced_cpu_snapshot_is_the_cpu_rasterizers_and_says_so() {
|
||||
assert!(stderr.contains("CPU rasterizer"), "{stderr}");
|
||||
let mut s = Session::default();
|
||||
s.run_script(&serde_json::from_slice(&std::fs::read(&script).unwrap()).unwrap()).unwrap();
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s);
|
||||
let mut cam = solidcraft_engine::view::home_camera(&s).unwrap();
|
||||
cam.set_view(solidcraft_engine::render::StandardView::Top);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam);
|
||||
let scene = solidcraft_engine::view::scene(&s, &cam).unwrap();
|
||||
let cpu = solidcraft_engine::render::render_png(&scene, &cam, SIZE.0 as usize, SIZE.1 as usize).unwrap();
|
||||
assert_eq!(std::fs::read(dir.0.join("cpu.png")).unwrap(), cpu);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
|
||||
//! as many pieces, and a sketch on such a face projected every piece.
|
||||
|
||||
use solidcraft_engine::geom::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use serde_json::json;
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
|
||||
fn design() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.sc")
|
||||
@@ -48,11 +49,11 @@ fn key(v: Vec3) -> [i64; 3] {
|
||||
fn straight_edges_are_one_edge_after_many_cuts() {
|
||||
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
|
||||
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
|
||||
let st = s.world_state();
|
||||
let st = s.world_state().unwrap();
|
||||
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
|
||||
assert_eq!(bodies.len(), 1, "{what}: one body");
|
||||
let body = &bodies[0].body;
|
||||
let mesh = body.tessellate(0.01).unwrap();
|
||||
let mesh = body.mesh(&solidcraft_kernel::MeshTolerance::chord(0.01)).unwrap();
|
||||
// Every edge is drawn as one polyline; a straight one as one segment.
|
||||
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
|
||||
for (i, e) in mesh.edges.iter().enumerate() {
|
||||
|
||||
@@ -14,8 +14,11 @@ name = "solidcraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
[dependencies]
|
||||
solidcraft-engine = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Links the browser build with an 8 MiB stack: solidcore's kernel algorithms (subdivision,
|
||||
//! intersection, filleting) recurse legitimately, and wasm has no guard page to catch an
|
||||
//! overflow (solidcore's README recommends 8 MiB).
|
||||
|
||||
fn main() {
|
||||
if std::env::var("CARGO_CFG_TARGET_ARCH").is_ok_and(|a| a == "wasm32") {
|
||||
println!("cargo:rustc-link-arg=-zstack-size=8388608");
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@
|
||||
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/CraftUI-SC.otf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-sc/OFL.txt" data-target-path="fonts/craft-ui-sc" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/CraftUI-TC.otf" data-target-path="fonts" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/fonts/craft-ui-tc/OFL.txt" data-target-path="fonts/craft-ui-tc" />
|
||||
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
|
||||
<link data-trunk rel="copy-file" href="../../assets/styles/web/loading-logo.svg" />
|
||||
<!-- Installable as an app (manifest.webmanifest); the icons are the Linux exports of the app mark. -->
|
||||
|
||||
@@ -15,6 +15,9 @@ type Reply = Result<(String, Vec<u8>), String>;
|
||||
pub struct Download {
|
||||
reply: Rc<RefCell<Option<Reply>>>,
|
||||
pending: Option<(String, Vec<u8>)>,
|
||||
/// The downloaded design being opened (a few features a frame, #10198): the answer to come,
|
||||
/// the name it takes and the file it was read from.
|
||||
opening: Option<(std::sync::mpsc::Receiver<serde_json::Value>, String, String)>,
|
||||
}
|
||||
|
||||
impl Download {
|
||||
@@ -33,13 +36,14 @@ impl Download {
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, app: &mut SolveApp, ctx: &egui::Context) {
|
||||
self.opened(app);
|
||||
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() {
|
||||
if self.pending.is_none() || self.opening.is_some() || app.file_picker_busy() {
|
||||
return;
|
||||
}
|
||||
if solidcraft_ui_egui::documents::any_dirty(app) {
|
||||
@@ -71,18 +75,33 @@ impl Download {
|
||||
}
|
||||
}
|
||||
let Some((name, bytes)) = self.pending.take() else { return };
|
||||
// The extension selects the design loader even when the URL has no extension.
|
||||
let path = &format!("/linked/design.{}", solidcraft_engine::io::DESIGN_EXT);
|
||||
vfs::insert(path, bytes);
|
||||
app.open_path(path);
|
||||
if app.session.path.as_deref() == Some(path.as_str()) {
|
||||
// The extension selects the design loader even when the URL has no extension. Named
|
||||
// after the link, as the progress card shows the file's name.
|
||||
let file: String = name.chars().map(|c| if matches!(c, '/' | '\\') { '_' } else { c }).collect();
|
||||
let path = format!("/linked/{}.{}", if file.is_empty() { "design" } else { &file }, solidcraft_engine::io::DESIGN_EXT);
|
||||
vfs::insert(&path, bytes);
|
||||
// Opened as any design is, with its progress and Cancel; finished in `opened`.
|
||||
self.opening = Some((app.open_path_answered(&path), name, path));
|
||||
self.opened(app);
|
||||
}
|
||||
|
||||
/// The downloaded design is in: it is an unsaved copy under the link's name.
|
||||
fn opened(&mut self, app: &mut SolveApp) {
|
||||
let Some((rx, _, _)) = &self.opening else { return };
|
||||
let (ok, cancelled) = match rx.try_recv() {
|
||||
Ok(reply) => (reply["ok"].as_bool() == Some(true), reply["error"].as_str().is_some_and(|e| e.contains("cancelled"))),
|
||||
Err(std::sync::mpsc::TryRecvError::Empty) => return,
|
||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => (false, false),
|
||||
};
|
||||
let Some((_, name, path)) = self.opening.take() else { return };
|
||||
if ok && app.session.path.as_deref() == Some(path.as_str()) {
|
||||
app.session.path = None;
|
||||
app.session.doc_mut().name = name;
|
||||
app.session.mark_unsaved();
|
||||
} else {
|
||||
} else if !ok && !cancelled {
|
||||
app.set_status("Could not open design link: the downloaded file is not a valid SolidCraft design", true);
|
||||
}
|
||||
let _ = vfs::remove(path);
|
||||
let _ = vfs::remove(&path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -273,8 +273,10 @@ mod web {
|
||||
}
|
||||
|
||||
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
|
||||
const CHINESE_SC_FONT_PATH: &str = "./fonts/CraftUI-SC.otf";
|
||||
const CHINESE_TC_FONT_PATH: &str = "./fonts/CraftUI-TC.otf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<(String, Vec<u8>), String>>>>;
|
||||
|
||||
struct Shell {
|
||||
app: SolveApp,
|
||||
@@ -292,11 +294,17 @@ mod web {
|
||||
rename_focus: bool,
|
||||
/// The start page's recent designs were filled from the stored ones.
|
||||
recents_synced: bool,
|
||||
/// On-demand CJK font: fetched only when a CJK language is selected.
|
||||
/// On-demand script fonts: fetched only when a CJK language is selected.
|
||||
font: solidcraft_ui_egui::fonts::CjkFontLoader,
|
||||
font_result: FontLoadResult,
|
||||
/// The fetched CJK font's bytes: kept so a language change can re-order the fallbacks.
|
||||
/// The fetched Japanese font's bytes: kept so a language change can re-order the fallbacks.
|
||||
cjk_bytes: Option<Vec<u8>>,
|
||||
/// The fetched Chinese font's bytes (Simplified or Traditional for this session).
|
||||
chinese_bytes: Option<Vec<u8>>,
|
||||
/// Which Chinese face `chinese_bytes` is: `"zh"` or `"zh-TW"`.
|
||||
chinese_kind: Option<&'static str>,
|
||||
/// Kind requested by the in-flight fetch (`"ja"`, `"zh"`, or `"zh-TW"`).
|
||||
font_fetch_kind: Option<&'static str>,
|
||||
/// The language the fonts were last set up for (a CJK UI leads with the CJK face).
|
||||
font_language: String,
|
||||
space_mouse: SpaceMouse,
|
||||
@@ -367,7 +375,8 @@ mod web {
|
||||
// Designs opened from the computer's disk (the files window): open them.
|
||||
let opened: Vec<_> = self.files.opened.borrow_mut().drain(..).collect();
|
||||
for path in opened {
|
||||
self.app.open_path(&path);
|
||||
// A few features a frame, with progress and Cancel (#10198).
|
||||
self.app.open_path_async(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
// Recent designs whose file is gone are dropped from the list.
|
||||
@@ -408,7 +417,7 @@ mod web {
|
||||
path
|
||||
};
|
||||
match intent {
|
||||
Intent::Open => self.app.open_path(&path),
|
||||
Intent::Open => self.app.open_path_async(&path),
|
||||
Intent::Insert | Intent::Theme => self.app.drop_path(&path),
|
||||
}
|
||||
self.files.panel.set(false);
|
||||
@@ -572,7 +581,7 @@ mod web {
|
||||
let path = format!("{}{name}", files::OPFS);
|
||||
match what {
|
||||
"open" => {
|
||||
self.app.open_path(&path);
|
||||
self.app.open_path_async(&path);
|
||||
self.files.panel.set(false);
|
||||
}
|
||||
"insert" => {
|
||||
@@ -674,65 +683,102 @@ mod web {
|
||||
files::autosave(&self.files, None);
|
||||
}
|
||||
|
||||
/// Fetch and install the CJK font when a CJK language is selected. If that fails, this
|
||||
/// Whether the shell already has the on-demand face the live language needs.
|
||||
fn script_font_ready(&self) -> bool {
|
||||
match solidcraft_ui_egui::fonts::web_script_font_kind(solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language)) {
|
||||
Some("zh") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh"),
|
||||
Some("zh-TW") => self.chinese_bytes.is_some() && self.chinese_kind == Some("zh-TW"),
|
||||
Some("ja") => self.cjk_bytes.is_some(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch and install a script font when a CJK language is selected. If that fails, this
|
||||
/// session falls back to English (no tofu); the saved language preference is kept.
|
||||
fn tick_font(&mut self, ctx: &egui::Context) {
|
||||
let done = self.font_result.borrow_mut().take();
|
||||
if let Some(res) = done {
|
||||
let kind = self.font_fetch_kind.take();
|
||||
let installed = match res {
|
||||
Ok(bytes) => {
|
||||
self.cjk_bytes = Some(bytes);
|
||||
Ok((path, bytes)) => {
|
||||
match kind {
|
||||
Some("zh") => {
|
||||
self.chinese_bytes = Some(bytes);
|
||||
self.chinese_kind = Some("zh");
|
||||
}
|
||||
Some("zh-TW") => {
|
||||
self.chinese_bytes = Some(bytes);
|
||||
self.chinese_kind = Some("zh-TW");
|
||||
}
|
||||
_ => {
|
||||
self.cjk_bytes = Some(bytes);
|
||||
}
|
||||
}
|
||||
log::info!("loaded web script font from {path}");
|
||||
true
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("failed to fetch CJK font from {JAPANESE_FONT_PATH}: {err}");
|
||||
let path = match kind {
|
||||
Some("zh") => CHINESE_SC_FONT_PATH,
|
||||
Some("zh-TW") => CHINESE_TC_FONT_PATH,
|
||||
_ => JAPANESE_FONT_PATH,
|
||||
};
|
||||
log::warn!("failed to fetch script font from {path}: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
if installed {
|
||||
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
|
||||
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
|
||||
log::warn!("CJK font unavailable; showing English for this session");
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if !installed && solidcraft_ui_egui::fonts::needs_cjk_font(effective) {
|
||||
log::warn!("script font unavailable; showing English for this session");
|
||||
}
|
||||
self.font.finished(installed, &mut self.app.preferences.language);
|
||||
self.apply_fonts(ctx);
|
||||
ctx.request_repaint();
|
||||
}
|
||||
if self.font.tick(&mut self.app.preferences.language) {
|
||||
self.fetch_font(ctx);
|
||||
let ready = self.script_font_ready();
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if self.font.tick(&mut self.app.preferences.language, ready) {
|
||||
self.fetch_font(ctx, solidcraft_ui_egui::fonts::web_script_font_kind(effective).unwrap_or("ja"));
|
||||
}
|
||||
// The CJK face leads only a CJK UI: its Cyrillic glyphs are full-width, so switching
|
||||
// to Ukrainian (or English) must put the Latin face back in front.
|
||||
if self.app.preferences.language != self.font_language {
|
||||
if solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language) != self.font_language {
|
||||
self.apply_fonts(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set up the UI fonts for the live language, keeping the fetched CJK font as a fallback
|
||||
/// (it leads only for a CJK UI, see [`solidcraft_ui_egui::fonts::install_cjk_font_for`]).
|
||||
/// Set up the UI fonts for the live language, keeping fetched script fonts as fallbacks
|
||||
/// (Chinese leads a Chinese UI; see [`solidcraft_ui_egui::fonts::install_web_script_fonts`]).
|
||||
fn apply_fonts(&mut self, ctx: &egui::Context) {
|
||||
match self.cjk_bytes.clone() {
|
||||
Some(bytes) => {
|
||||
solidcraft_ui_egui::fonts::install_cjk_font_for(ctx, bytes, &self.app.preferences.language);
|
||||
}
|
||||
None => solidcraft_ui_egui::fonts::setup_fonts(ctx),
|
||||
let effective = solidcraft_ui_egui::i18n::effective_language(&self.app.preferences.language);
|
||||
if self.cjk_bytes.is_none() && self.chinese_bytes.is_none() {
|
||||
solidcraft_ui_egui::fonts::setup_fonts(ctx);
|
||||
} else {
|
||||
solidcraft_ui_egui::fonts::install_web_script_fonts(ctx, self.cjk_bytes.clone(), self.chinese_bytes.clone(), effective);
|
||||
}
|
||||
self.font_language.clone_from(&self.app.preferences.language);
|
||||
self.font_language = effective.to_owned();
|
||||
}
|
||||
|
||||
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
|
||||
/// fetch the font, so the text does not stay tofu (#720).
|
||||
/// CJK text on screen with no Japanese font yet (an English UI showing a Japanese name):
|
||||
/// fetch the Japanese font, so the text does not stay tofu (#720).
|
||||
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
|
||||
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
|
||||
self.fetch_font(ctx);
|
||||
self.fetch_font(ctx, "ja");
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_font(&self, ctx: &egui::Context) {
|
||||
fn fetch_font(&mut self, ctx: &egui::Context, kind: &'static str) {
|
||||
let path = match kind {
|
||||
"zh" => CHINESE_SC_FONT_PATH,
|
||||
"zh-TW" => CHINESE_TC_FONT_PATH,
|
||||
_ => JAPANESE_FONT_PATH,
|
||||
};
|
||||
self.font_fetch_kind = Some(kind);
|
||||
let slot = self.font_result.clone();
|
||||
let ctx = ctx.clone();
|
||||
let path = path.to_owned();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
|
||||
let res = fetch_font_bytes(&path).await.map(|bytes| (path, bytes));
|
||||
*slot.borrow_mut() = Some(res);
|
||||
ctx.request_repaint();
|
||||
});
|
||||
@@ -962,6 +1008,9 @@ mod web {
|
||||
font: Default::default(),
|
||||
font_result: Rc::new(std::cell::RefCell::new(None)),
|
||||
cjk_bytes: None,
|
||||
chinese_bytes: None,
|
||||
chinese_kind: None,
|
||||
font_fetch_kind: None,
|
||||
font_language,
|
||||
space_mouse: SpaceMouse::default(),
|
||||
wheel_gestures: solidcraft_ui_egui::WebWheelGestures::default(),
|
||||
|
||||
@@ -8,9 +8,12 @@ publish = false
|
||||
default-run = "solidcraft"
|
||||
|
||||
[features]
|
||||
default = ["raytrace-cpu", "bundled-render-library"]
|
||||
default = ["raytrace-cpu", "bundled-render-library", "profile"]
|
||||
bundled-render-library = ["solidcraft-engine/bundled-render-library"]
|
||||
raytrace-cpu = ["solidcraft-engine/raytrace-cpu"]
|
||||
# The profiler, compiled in so any build can say where its time goes (the Profile operations
|
||||
# preference, SOLIDCRAFT_PROFILE=1, diagnostics.profile); it records nothing until asked.
|
||||
profile = ["solidcraft-engine/profile"]
|
||||
|
||||
# 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), src/native_crash_macos.rs
|
||||
@@ -104,5 +107,10 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
|
||||
raw-window-handle = "0.6"
|
||||
x11rb = { version = "0.13", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# The desktop's dark or light preference through the XDG Desktop Portal (src/desktop_theme.rs,
|
||||
# #7944); already in the tree through eframe's accessibility support (accesskit_unix).
|
||||
zbus = { version = "5", default-features = false, features = ["async-io", "blocking-api"] }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3.0.11"
|
||||
|
||||
@@ -22,6 +22,10 @@ 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);
|
||||
/// How long a new connection has to show the token. A client connects when it has a request to
|
||||
/// send, so this is plenty; one that doesn't send it loses the connection, and its seat among
|
||||
/// [`MAX_CONNECTIONS`] goes to someone else.
|
||||
const TOKEN_WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
static OPEN: AtomicUsize = AtomicUsize::new(0);
|
||||
/// The token files this launch wrote (its control port, the agent chat's endpoint) and the
|
||||
@@ -225,23 +229,53 @@ fn unauthorized() -> Value {
|
||||
/// Refuse the connection: the reply, then the end of what we send, then what the client had
|
||||
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
|
||||
/// the connection, and a reset can reach the client before it has read the refusal.
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
|
||||
fn refuse(out: &mut TcpStream, reader: &mut BufReader<ClientInput>) {
|
||||
use std::io::Read;
|
||||
let _ = send(out, &unauthorized());
|
||||
let _ = out.shutdown(std::net::Shutdown::Write);
|
||||
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
|
||||
reader.get_mut().deadline = Some(Instant::now() + Duration::from_millis(500));
|
||||
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
|
||||
}
|
||||
|
||||
/// What a client sends. Until it shows the token, all of its reads share one deadline, so neither
|
||||
/// blank lines nor a line sent a byte at a time buy it more time. After that it may stay idle as
|
||||
/// long as it likes.
|
||||
struct ClientInput {
|
||||
socket: TcpStream,
|
||||
deadline: Option<Instant>,
|
||||
}
|
||||
|
||||
impl ClientInput {
|
||||
/// No deadline any more: the client showed the token.
|
||||
fn admit(&mut self) -> std::io::Result<()> {
|
||||
self.deadline = None;
|
||||
self.socket.set_read_timeout(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::io::Read for ClientInput {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if let Some(deadline) = self.deadline {
|
||||
let left = deadline.saturating_duration_since(Instant::now());
|
||||
if left.is_zero() {
|
||||
return Err(std::io::ErrorKind::TimedOut.into());
|
||||
}
|
||||
self.socket.set_read_timeout(Some(left))?;
|
||||
}
|
||||
self.socket.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// How long a request waits for the app's reply: most requests, and a batch (or every request,
|
||||
/// on a patient connection).
|
||||
/// on a patient connection); and how long a new connection has to show the token.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Waits {
|
||||
short: Duration,
|
||||
long: Duration,
|
||||
token: Duration,
|
||||
}
|
||||
|
||||
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600) };
|
||||
const WAITS: Waits = Waits { short: Duration::from_secs(60), long: Duration::from_secs(3600), token: TOKEN_WAIT };
|
||||
|
||||
/// A request that timed out but is still running in the app (#1618): its id and method, and where
|
||||
/// its reply will land.
|
||||
@@ -413,11 +447,37 @@ impl Lines {
|
||||
}
|
||||
}
|
||||
|
||||
/// A subscribed connection's seat on the watch: it leaves exactly once, whichever thread sees
|
||||
/// the connection's end first — the connection's own thread at the end of the stream, or the
|
||||
/// events thread at the first write a closed client refuses. The seat used to wait for the
|
||||
/// events thread alone, and that thread only leaves its wait when the design moves, so the
|
||||
/// count could outlive the connection by a second or more (#8315).
|
||||
struct Seat(Mutex<Option<Subscriber>>);
|
||||
|
||||
impl Seat {
|
||||
fn new(counted: Subscriber) -> Self {
|
||||
Seat(Mutex::new(Some(counted)))
|
||||
}
|
||||
|
||||
/// Leave the watch now: the seat goes once, whoever gets here first.
|
||||
fn release(&self) {
|
||||
let counted = self.0.lock().unwrap_or_else(PoisonError::into_inner).take();
|
||||
drop(counted);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Seat {
|
||||
fn drop(&mut self) {
|
||||
self.release();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
seat: Arc<Seat>,
|
||||
}
|
||||
|
||||
impl Subscription {
|
||||
@@ -433,15 +493,17 @@ impl Subscription {
|
||||
) -> 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 seat = Arc::new(Seat::new(counted));
|
||||
let first = lines.turn.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
let (l, s) = (lines.clone(), stop.clone());
|
||||
let (l, s, watch_seat) = (lines.clone(), stop.clone(), Arc::clone(&seat));
|
||||
// The connection's seat on the watch rides the events thread: it leaves when the thread
|
||||
// does, so the next failed write to a closed client retires it even while the connection's
|
||||
// own thread is slow to see the end of the stream (#7226).
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, counted)).ok()?;
|
||||
// own thread is slow to see the end of the stream (#7226). The connection's own thread
|
||||
// leaves it too, so a closed connection stops counting at once (#8315).
|
||||
std::thread::Builder::new().stack_size(64 * 1024).spawn(move || send_events(design, &l, &s, seen, watch_seat)).ok()?;
|
||||
let sent = send(&mut &lines.socket, reply);
|
||||
drop(first);
|
||||
Some((Subscription { lines, stop }, sent))
|
||||
Some((Subscription { lines, stop, seat }, sent))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +516,9 @@ fn design_of(reply: &Value) -> Option<(String, u64)> {
|
||||
impl Drop for Subscription {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
// The seat on the watch goes now: the connection is over, and nothing counts it any more
|
||||
// (#8315).
|
||||
self.seat.release();
|
||||
// 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);
|
||||
@@ -461,10 +526,10 @@ impl Drop for Subscription {
|
||||
}
|
||||
|
||||
/// The `design.changed` lines of a subscribed connection, each time the design differs from the
|
||||
/// last one it was told of (`seen`), until `stop`. Owns the connection's seat on the watch
|
||||
/// (`counted`), so the seat leaves exactly when this thread does: on `stop`, or on the first
|
||||
/// write a closed client refuses (#7226).
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), _counted: Subscriber) {
|
||||
/// last one it was told of (`seen`), until `stop`. Holds the connection's seat on the watch
|
||||
/// (`seat`), which it leaves when the thread ends: on `stop`, or on the first write a closed
|
||||
/// client refuses (#7226).
|
||||
fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen: (String, u64), seat: Arc<Seat>) {
|
||||
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() {
|
||||
@@ -473,6 +538,9 @@ fn send_events(design: &DesignWatch, lines: &Lines, stop: &AtomicBool, mut seen:
|
||||
seen = now;
|
||||
std::thread::sleep(EVENT_GAP);
|
||||
}
|
||||
// The client is gone: it is not counted on the watch any more, whatever the connection's own
|
||||
// thread still has to notice (#8315).
|
||||
seat.release();
|
||||
}
|
||||
|
||||
/// What this app's control channel does beyond the methods every version has, for a client to
|
||||
@@ -509,7 +577,7 @@ fn serve_with(
|
||||
let connection = running.connections.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
let mut out = stream;
|
||||
let mut reader = BufReader::new(read);
|
||||
let mut reader = BufReader::new(ClientInput { socket: read, deadline: Some(Instant::now() + waits.token) });
|
||||
let mut bad = 0usize;
|
||||
let mut authenticated = false;
|
||||
let mut pending: Option<Pending> = None;
|
||||
@@ -520,6 +588,10 @@ fn serve_with(
|
||||
let line = match read_line(&mut reader, MAX_LINE) {
|
||||
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
|
||||
Ok(None) => break,
|
||||
// Closed without a word: a client that connected early and only now sends its first
|
||||
// request would read a line here as the reply to it. It finds the connection closed
|
||||
// and opens another.
|
||||
Err(e) if !authenticated && matches!(e.kind(), std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock) => break,
|
||||
Err(e) => {
|
||||
let _ = answer(&mut out, events.as_ref(), &json!({"ok": false, "error": e.to_string()}));
|
||||
break;
|
||||
@@ -531,7 +603,12 @@ fn serve_with(
|
||||
// The first request must carry the token; later ones on the same connection may.
|
||||
if !authenticated {
|
||||
match token_of(&line) {
|
||||
Some(t) if control_auth::tokens_match(&t, token) => authenticated = true,
|
||||
Some(t) if control_auth::tokens_match(&t, token) => {
|
||||
if reader.get_mut().admit().is_err() {
|
||||
break;
|
||||
}
|
||||
authenticated = true;
|
||||
}
|
||||
_ => {
|
||||
refuse(&mut out, &mut reader);
|
||||
break;
|
||||
@@ -641,6 +718,80 @@ fn serve_with(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A connection that doesn't show the token in time is closed without a word, however it fills
|
||||
/// the time. One that does keeps working when it idles for longer than that.
|
||||
#[test]
|
||||
fn the_token_has_a_time_limit_and_an_authenticated_client_may_idle() {
|
||||
use std::io::Read;
|
||||
let token = "0123456789abcdef0123456789abcdef";
|
||||
let limit = Duration::from_millis(500);
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
std::thread::spawn(move || {
|
||||
for req in rx {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
static RUNNING: Running = Running::new();
|
||||
let waits = Waits { short: Duration::from_secs(10), long: Duration::from_secs(10), token: limit };
|
||||
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, &DESIGN_WATCH));
|
||||
}
|
||||
});
|
||||
let open = || {
|
||||
let s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(20))).unwrap();
|
||||
s
|
||||
};
|
||||
// Sends `drip` every 50 ms until the server closes; what it read, and when it closed.
|
||||
let closed_after = |drip: &[u8]| {
|
||||
let mut s = open();
|
||||
let start = Instant::now();
|
||||
s.set_read_timeout(Some(Duration::from_millis(50))).unwrap();
|
||||
let mut got = Vec::new();
|
||||
loop {
|
||||
let mut buf = [0u8; 256];
|
||||
match s.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(n) => got.extend_from_slice(buf.get(..n).unwrap()),
|
||||
Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => {
|
||||
assert!(start.elapsed() < Duration::from_secs(20), "still open");
|
||||
let _ = s.write_all(drip);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
(got, start.elapsed())
|
||||
};
|
||||
for drip in [&b""[..], b"\n", b"\r\n \n", b" ", b"{"] {
|
||||
let (got, took) = closed_after(drip);
|
||||
assert!(got.is_empty(), "{drip:?} was answered: {}", String::from_utf8_lossy(&got));
|
||||
assert!(took >= limit.saturating_sub(Duration::from_millis(50)), "{drip:?} closed after {took:?}");
|
||||
}
|
||||
|
||||
let mut s = open();
|
||||
let mut lines = BufReader::new(s.try_clone().unwrap());
|
||||
let mut ask = |line: Value| -> Value {
|
||||
writeln!(s, "{line}").unwrap();
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
serde_json::from_str(&l).unwrap()
|
||||
};
|
||||
assert_eq!(ask(json!({"id": 1, "method": "ui.view", "token": token}))["ok"], true);
|
||||
std::thread::sleep(limit * 3);
|
||||
assert_eq!(ask(json!({"id": 2, "method": "ui.view"}))["ok"], true, "idle for longer than the limit");
|
||||
|
||||
// A wrong token is still answered, at once.
|
||||
let mut wrong = open();
|
||||
writeln!(wrong, "{}", json!({"id": 1, "method": "ui.view", "token": "nope"})).unwrap();
|
||||
let mut l = String::new();
|
||||
BufReader::new(wrong).read_line(&mut l).unwrap();
|
||||
assert!(l.contains("unauthorized"), "{l}");
|
||||
}
|
||||
|
||||
/// #1618: a request that outlasts its wait says it is still running; the connection keeps its
|
||||
/// reply for `control.pending`, and refuses other requests as `busy` until it lands, so none
|
||||
/// is queued to run unseen behind it.
|
||||
@@ -664,7 +815,7 @@ mod tests {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100) };
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_millis(100), token: TOKEN_WAIT };
|
||||
static RUNNING: Running = Running::new();
|
||||
std::thread::spawn(move || {
|
||||
if let Some(stream) = listener.incoming().flatten().next() {
|
||||
@@ -730,7 +881,7 @@ mod tests {
|
||||
let _ = req.reply.send(json!({"ok": true, "result": req.method}));
|
||||
}
|
||||
});
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60) };
|
||||
let waits = Waits { short: Duration::from_millis(100), long: Duration::from_secs(60), token: TOKEN_WAIT };
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
@@ -905,6 +1056,58 @@ mod tests {
|
||||
assert!(!WATCH.subscribed());
|
||||
}
|
||||
|
||||
/// #8315: a closed connection leaves the watch at once. The seat of a subscribed connection
|
||||
/// goes when the connection's own thread sees the end of the stream, not only when the
|
||||
/// events thread notices: that thread waits for the design to move, so a connection that
|
||||
/// closed while the design stood still kept the watch counting it for a second or more.
|
||||
#[test]
|
||||
fn a_closed_connection_leaves_the_watch_at_once() {
|
||||
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 names the design once and then leaves it alone: nothing moves it, so the
|
||||
// events thread has nothing to write and only the end of the connection can retire it.
|
||||
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}}));
|
||||
} 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 s = TcpStream::connect(addr).unwrap();
|
||||
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
|
||||
let mut out = s.try_clone().unwrap();
|
||||
let mut lines = BufReader::new(s);
|
||||
writeln!(out, "{}", json!({"id": 1, "method": "control.subscribe", "token": token})).unwrap();
|
||||
let mut l = String::new();
|
||||
lines.read_line(&mut l).unwrap();
|
||||
let reply: Value = serde_json::from_str(&l).unwrap();
|
||||
assert!(reply["ok"] == true, "{reply}");
|
||||
assert!(WATCH.subscribed(), "the connection is subscribed");
|
||||
// Let the events thread settle into its wait: the design stands still, so it has nothing
|
||||
// to write, and only the end of the connection can retire it.
|
||||
std::thread::sleep(Duration::from_millis(200));
|
||||
// The client goes away without asking anything else.
|
||||
drop((out, lines));
|
||||
let deadline = Instant::now() + Duration::from_millis(400);
|
||||
while WATCH.subscribed() && Instant::now() < deadline {
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert!(!WATCH.subscribed(), "a closed connection is not counted on the watch");
|
||||
}
|
||||
|
||||
/// 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]
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Linux: the desktop's dark or light preference, for Theme: System (#7944).
|
||||
//!
|
||||
//! The window toolkit does not pass it on everywhere: on GNOME (Wayland, or X11 through XWayland)
|
||||
//! the window never learns it, nor when it changes, so the app kept its colours. This reads the
|
||||
//! freedesktop setting through the XDG Desktop Portal's Settings interface on the session bus
|
||||
//! (`org.freedesktop.appearance color-scheme`, then GNOME's `color-scheme` and `gtk-theme`), the
|
||||
//! same setting other applications follow, and inside a Flatpak too. It runs on a thread of its
|
||||
//! own: the first reading, then the portal's `SettingChanged` signal; where the signal can't be
|
||||
//! had, a reading every few seconds. Without a portal it asks `gsettings` instead, on the same
|
||||
//! thread. The window reads the last answer with an atomic load
|
||||
//! (`solidcraft_ui_egui::Services::desktop_dark`), and the decision from the raw values is
|
||||
//! `solidcraft_ui_egui::prefs::desktop_prefers_dark`.
|
||||
|
||||
use std::io::Read;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const UNKNOWN: u8 = 0;
|
||||
const DARK: u8 = 1;
|
||||
const LIGHT: u8 = 2;
|
||||
|
||||
/// How often to read again where the portal's change signal is not available.
|
||||
const POLL: Duration = Duration::from_secs(5);
|
||||
/// How long one `gsettings` reading may take before it is given up.
|
||||
const GSETTINGS_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
const PORTAL: &str = "org.freedesktop.portal.Desktop";
|
||||
const PORTAL_PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
const SETTINGS: &str = "org.freedesktop.portal.Settings";
|
||||
const APPEARANCE: &str = "org.freedesktop.appearance";
|
||||
const GNOME: &str = "org.gnome.desktop.interface";
|
||||
|
||||
/// Start following the desktop's preference; the closure answers the latest reading (`None`
|
||||
/// until there is one, or when the desktop says nothing).
|
||||
pub fn start(ctx: &egui::Context) -> Box<dyn Fn() -> Option<bool>> {
|
||||
let state = Arc::new(AtomicU8::new(UNKNOWN));
|
||||
let (thread_state, ctx) = (state.clone(), ctx.clone());
|
||||
if let Err(e) = std::thread::Builder::new().name("desktop-theme".into()).spawn(move || follow(&thread_state, &ctx)) {
|
||||
log::warn!("desktop theme: no thread to follow it: {e}");
|
||||
}
|
||||
Box::new(move || match state.load(Ordering::Relaxed) {
|
||||
DARK => Some(true),
|
||||
LIGHT => Some(false),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keep `state` on the desktop's preference, asking for a frame whenever it changes.
|
||||
fn follow(state: &AtomicU8, ctx: &egui::Context) {
|
||||
let publish = |dark: Option<bool>| {
|
||||
let code = match dark {
|
||||
Some(true) => DARK,
|
||||
Some(false) => LIGHT,
|
||||
None => UNKNOWN,
|
||||
};
|
||||
if state.swap(code, Ordering::Relaxed) != code {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
};
|
||||
let portal = match portal() {
|
||||
Ok(p) => Some(p),
|
||||
Err(e) => {
|
||||
log::info!("desktop theme: no settings portal ({e}); asking gsettings");
|
||||
None
|
||||
}
|
||||
};
|
||||
// The portal first; gsettings where there is none or it says nothing (its backend missing).
|
||||
let read = |portal: Option<&zbus::blocking::Proxy<'static>>| portal.and_then(read_portal).or_else(read_gsettings);
|
||||
let from_portal = portal.as_ref().and_then(read_portal);
|
||||
let first = from_portal.or_else(read_gsettings);
|
||||
publish(first);
|
||||
// Wait for the portal's signal only when the portal answers: else the reading is gsettings',
|
||||
// which only a new reading notices.
|
||||
if let Some(p) = portal.as_ref().filter(|_| from_portal.is_some()) {
|
||||
match p.receive_signal("SettingChanged") {
|
||||
Ok(changes) => {
|
||||
for message in changes {
|
||||
if relevant(&message) {
|
||||
publish(read(Some(p)));
|
||||
}
|
||||
}
|
||||
log::info!("desktop theme: the portal's change signal ended; reading every few seconds");
|
||||
}
|
||||
Err(e) => log::info!("desktop theme: no change signal ({e}); reading every few seconds"),
|
||||
}
|
||||
} else if portal.is_none() && first.is_none() && !gsettings_available() {
|
||||
// Neither the portal nor gsettings: nothing to follow; the window toolkit's theme stays.
|
||||
return;
|
||||
}
|
||||
loop {
|
||||
std::thread::sleep(POLL);
|
||||
publish(read(portal.as_ref()));
|
||||
}
|
||||
}
|
||||
|
||||
/// The portal's Settings interface on the session bus.
|
||||
fn portal() -> zbus::Result<zbus::blocking::Proxy<'static>> {
|
||||
let connection = zbus::blocking::Connection::session()?;
|
||||
zbus::blocking::Proxy::new(&connection, PORTAL, PORTAL_PATH, SETTINGS)
|
||||
}
|
||||
|
||||
/// One reading through the portal.
|
||||
fn read_portal(portal: &zbus::blocking::Proxy<'static>) -> Option<bool> {
|
||||
let appearance = read_setting(portal, APPEARANCE, "color-scheme").and_then(|v| as_u32(&v));
|
||||
// The GNOME settings only matter when the freedesktop one has no preference.
|
||||
if matches!(appearance, Some(1 | 2)) {
|
||||
return solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, None, None);
|
||||
}
|
||||
let gnome = read_setting(portal, GNOME, "color-scheme").and_then(|v| as_string(&v));
|
||||
let gtk_theme = read_setting(portal, GNOME, "gtk-theme").and_then(|v| as_string(&v));
|
||||
solidcraft_ui_egui::prefs::desktop_prefers_dark(appearance, gnome.as_deref(), gtk_theme.as_deref())
|
||||
}
|
||||
|
||||
/// A setting through `ReadOne` (portal version 2), else the older `Read`.
|
||||
fn read_setting(portal: &zbus::blocking::Proxy<'static>, namespace: &str, key: &str) -> Option<OwnedValue> {
|
||||
let one: zbus::Result<OwnedValue> = portal.call("ReadOne", &(namespace, key));
|
||||
one.or_else(|_| -> zbus::Result<OwnedValue> { portal.call("Read", &(namespace, key)) }).ok()
|
||||
}
|
||||
|
||||
/// Whether a `SettingChanged` signal is about a setting the preference is read from.
|
||||
fn relevant(message: &zbus::Message) -> bool {
|
||||
let body = message.body();
|
||||
let Ok((namespace, key, _)) = body.deserialize::<(String, String, OwnedValue)>() else { return false };
|
||||
matches!((namespace.as_str(), key.as_str()), (APPEARANCE, "color-scheme") | (GNOME, "color-scheme" | "gtk-theme"))
|
||||
}
|
||||
|
||||
/// The value inside the variants `Read` wraps it in (a portal answers one or two deep).
|
||||
fn unwrap_variants<'v, 'a>(value: &'v Value<'a>) -> &'v Value<'a> {
|
||||
let mut value = value;
|
||||
for _ in 0..4 {
|
||||
match value {
|
||||
Value::Value(inner) => value = &**inner,
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
value
|
||||
}
|
||||
|
||||
fn as_u32(value: &Value<'_>) -> Option<u32> {
|
||||
match unwrap_variants(value) {
|
||||
Value::U32(n) => Some(*n),
|
||||
Value::I32(n) => u32::try_from(*n).ok(),
|
||||
Value::U8(n) => Some(u32::from(*n)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_string(value: &Value<'_>) -> Option<String> {
|
||||
match unwrap_variants(value) {
|
||||
Value::Str(s) => Some(s.as_str().to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Without a portal: GNOME's settings through `gsettings` (`None` when it is not there).
|
||||
fn read_gsettings() -> Option<bool> {
|
||||
let gnome = gsettings("color-scheme");
|
||||
let gtk_theme = gsettings("gtk-theme");
|
||||
solidcraft_ui_egui::prefs::desktop_prefers_dark(None, gnome.as_deref(), gtk_theme.as_deref())
|
||||
}
|
||||
|
||||
fn gsettings_available() -> bool {
|
||||
gsettings("gtk-theme").is_some()
|
||||
}
|
||||
|
||||
/// `gsettings get org.gnome.desktop.interface <key>`, its quoted answer, or `None` when the
|
||||
/// program is missing, fails or does not answer in time.
|
||||
fn gsettings(key: &str) -> Option<String> {
|
||||
let mut child = std::process::Command::new("gsettings")
|
||||
.args(["get", GNOME, key])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.ok()?;
|
||||
let started = std::time::Instant::now();
|
||||
loop {
|
||||
match child.try_wait() {
|
||||
Ok(Some(status)) if status.success() => break,
|
||||
Ok(None) if started.elapsed() < GSETTINGS_TIMEOUT => std::thread::sleep(Duration::from_millis(20)),
|
||||
_ => {
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out = String::new();
|
||||
child.stdout.take()?.take(4096).read_to_string(&mut out).ok()?;
|
||||
let value = out.trim().trim_matches('\'').to_string();
|
||||
(!value.is_empty()).then_some(value)
|
||||
}
|
||||
@@ -5,12 +5,25 @@
|
||||
//! for recovery as a forced quit does, and starts again ([`restart`]), and the new start says what
|
||||
//! happened ([`note`]). A device lost again soon after such a restart starts the next one on the
|
||||
//! software renderer, which doesn't depend on the graphics driver.
|
||||
//!
|
||||
//! The old process ends without tearing down its GPU objects ([`end`]): on a lost NVIDIA device
|
||||
//! destroying the window's swap chain busy-waits in the driver and never returns (#9044).
|
||||
|
||||
use crate::startup;
|
||||
|
||||
/// Set on the start after a lost device: when the device was lost (seconds since 1970).
|
||||
pub const LOST_ENV: &str = "SOLIDCRAFT_DEVICE_LOST";
|
||||
|
||||
/// Set on the start after a lost device: why it was lost, as wgpu said it. The old process's
|
||||
/// session journal, which recorded it, is gone by then.
|
||||
pub const REASON_ENV: &str = "SOLIDCRAFT_DEVICE_LOST_REASON";
|
||||
|
||||
/// The longest reason passed on ([`REASON_ENV`]) and shown, in characters.
|
||||
const REASON_CHARS: usize = 300;
|
||||
|
||||
/// How long the process may take to end ([`end`]) before it is aborted.
|
||||
const END_GRACE: std::time::Duration = std::time::Duration::from_secs(3);
|
||||
|
||||
/// A loss this recent (seconds) counts for the start after it: its note, and a second loss
|
||||
/// within it starts on the software renderer. Inherited by later starts, an older stamp is
|
||||
/// ignored.
|
||||
@@ -32,27 +45,54 @@ pub fn restart_on_software(after_recent_loss: bool, cpu_renderer: bool) -> bool
|
||||
after_recent_loss || cpu_renderer
|
||||
}
|
||||
|
||||
/// `reason` on one line, at most [`REASON_CHARS`] long; None when there is nothing to say.
|
||||
pub fn reason(reason: Option<&str>) -> Option<String> {
|
||||
let line = reason?.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
(!line.is_empty()).then(|| line.chars().take(REASON_CHARS).collect())
|
||||
}
|
||||
|
||||
/// What the start after a lost device tells the user, `software` when it runs on the software
|
||||
/// renderer.
|
||||
pub fn note(software: bool) -> String {
|
||||
/// renderer, with what the driver reported ([`REASON_ENV`]).
|
||||
pub fn note(software: bool, reason: Option<&str>) -> String {
|
||||
let mut note = String::from(
|
||||
"The graphics device was reset (the graphics driver stopped responding), so SolidCraft closed and started again. Unsaved changes were kept for recovery.",
|
||||
);
|
||||
if software {
|
||||
note.push_str(" This start uses software rendering, which doesn't depend on the graphics driver.");
|
||||
}
|
||||
if let Some(reason) = self::reason(reason) {
|
||||
note.push_str(&format!(" The graphics driver reported: {reason}"));
|
||||
}
|
||||
note
|
||||
}
|
||||
|
||||
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at`, on the software
|
||||
/// renderer when `software`.
|
||||
pub fn restart(software: bool, lost_at: u64) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
/// The command that starts `exe` again with `args`, the loss stamped at `lost_at` and its
|
||||
/// `reason`, on the software renderer when `software`.
|
||||
fn command(
|
||||
exe: std::path::PathBuf,
|
||||
args: impl IntoIterator<Item = std::ffi::OsString>,
|
||||
software: bool,
|
||||
lost_at: u64,
|
||||
reason: &str,
|
||||
) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(exe);
|
||||
command.args(std::env::args_os().skip(1)).env(LOST_ENV, lost_at.to_string());
|
||||
command.args(args).env(LOST_ENV, lost_at.to_string());
|
||||
match self::reason(Some(reason)) {
|
||||
Some(reason) => command.env(REASON_ENV, reason),
|
||||
// Not one inherited from an earlier loss.
|
||||
None => command.env_remove(REASON_ENV),
|
||||
};
|
||||
if software {
|
||||
command.env(startup::RENDERER_ENV, "cpu");
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
/// Start SolidCraft again with the same arguments, the loss stamped at `lost_at` and its `reason`,
|
||||
/// on the software renderer when `software`.
|
||||
pub fn restart(software: bool, lost_at: u64, reason: &str) {
|
||||
let Ok(exe) = std::env::current_exe() else { return };
|
||||
let mut command = command(exe, std::env::args_os().skip(1), software, lost_at, reason);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
@@ -64,6 +104,23 @@ pub fn restart(software: bool, lost_at: u64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// End the process now, without tearing down what is left on the lost device: the window's swap
|
||||
/// chain, the viewport's buffers and pipelines. On NVIDIA's Vulkan driver destroying the swap
|
||||
/// chain of a lost device busy-waits forever, which left the old window frozen at 100% of a core
|
||||
/// beside the new one (#9044). Should the exit itself get stuck in a library's exit handler, the
|
||||
/// process is aborted after [`END_GRACE`].
|
||||
pub fn end() -> ! {
|
||||
let watchdog = std::thread::Builder::new().name("solidcraft-exit".into()).spawn(|| {
|
||||
std::thread::sleep(END_GRACE);
|
||||
eprintln!("SolidCraft graphics: still closing after the device was lost; aborting");
|
||||
std::process::abort();
|
||||
});
|
||||
if let Err(e) = watchdog {
|
||||
log::warn!("graphics: no exit watchdog: {e}");
|
||||
}
|
||||
std::process::exit(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// Only a recent, well-formed stamp counts: a later start that inherited it says nothing.
|
||||
@@ -84,8 +141,34 @@ mod tests {
|
||||
assert!(!super::restart_on_software(false, false));
|
||||
assert!(super::restart_on_software(true, false));
|
||||
assert!(super::restart_on_software(false, true));
|
||||
assert!(super::note(true).contains("software rendering"));
|
||||
assert!(!super::note(false).contains("software rendering"));
|
||||
assert!(super::note(false).contains("kept for recovery"));
|
||||
assert!(super::note(true, None).contains("software rendering"));
|
||||
assert!(!super::note(false, None).contains("software rendering"));
|
||||
assert!(super::note(false, None).contains("kept for recovery"));
|
||||
}
|
||||
|
||||
/// The start after a loss is told why (#9044): the old process's journal held the reason, and
|
||||
/// it is deleted as that process closes.
|
||||
#[test]
|
||||
fn the_next_start_is_told_why_the_device_was_lost() {
|
||||
let exe = std::path::PathBuf::from("solidcraft");
|
||||
let args = ["--control".into(), "7979".into()];
|
||||
let command = super::command(exe.clone(), args, true, 1_000, "Lost: Device lost\n (vkQueueSubmit)");
|
||||
let env = |command: &std::process::Command, name: &str| {
|
||||
command.get_envs().find(|(k, _)| *k == name).map(|(_, v)| v.map(|v| v.to_string_lossy().into_owned()))
|
||||
};
|
||||
assert_eq!(env(&command, super::LOST_ENV), Some(Some("1000".into())));
|
||||
assert_eq!(env(&command, super::REASON_ENV), Some(Some("Lost: Device lost (vkQueueSubmit)".into())), "on one line");
|
||||
assert_eq!(env(&command, crate::startup::RENDERER_ENV), Some(Some("cpu".into())));
|
||||
assert_eq!(command.get_args().collect::<Vec<_>>(), ["--control", "7979"]);
|
||||
// No reason: none inherited from an earlier loss either.
|
||||
let command = super::command(exe, Vec::new(), false, 1_000, " ");
|
||||
assert_eq!(env(&command, super::REASON_ENV), Some(None));
|
||||
assert_eq!(env(&command, crate::startup::RENDERER_ENV), None);
|
||||
|
||||
let note = super::note(false, Some("Lost: Device lost"));
|
||||
assert!(note.ends_with("The graphics driver reported: Lost: Device lost"), "{note}");
|
||||
assert!(!super::note(false, Some(" \n ")).contains("reported"));
|
||||
let long = "x".repeat(1_000);
|
||||
assert_eq!(super::reason(Some(&long)).map(|r| r.chars().count()), Some(300));
|
||||
}
|
||||
}
|
||||
|
||||
+313
-10
@@ -22,6 +22,8 @@
|
||||
mod app_icon;
|
||||
mod come_forward;
|
||||
mod control_server;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod desktop_theme;
|
||||
mod device_lost;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod native_crash;
|
||||
@@ -97,6 +99,86 @@ struct App {
|
||||
nav_bridge: nav_bridge::NavBridge,
|
||||
/// Where the next file dialog opens: it follows the open design (#7756).
|
||||
dialog_folder: DialogFolder,
|
||||
/// Settings go to `app.ron` in the frame they change (#8275).
|
||||
prefs_writer: PrefsWriter,
|
||||
}
|
||||
|
||||
/// Settings written to the settings file soon after they change (#8275). eframe saves them
|
||||
/// only every 30 s and when the window closes normally, so a change was lost whenever the app
|
||||
/// did not get that far: ended from the Task Manager, stopped by a sign-out or shutdown, or
|
||||
/// crashed or hung in the graphics driver on the way out.
|
||||
///
|
||||
/// Some settings change continuously (a panel's width while its edge is dragged), so the file
|
||||
/// is written at most once per [`PrefsWriter::INTERVAL`], and a change made in between is written
|
||||
/// when that interval ends.
|
||||
#[derive(Default)]
|
||||
struct PrefsWriter {
|
||||
/// The settings written (or read at start) last.
|
||||
written: Option<String>,
|
||||
/// The previous frame had input that can change settings: what it changed shows in this
|
||||
/// frame's settings.
|
||||
input_before: bool,
|
||||
/// When the settings were last written by this writer.
|
||||
last_write: Option<std::time::Instant>,
|
||||
/// A change waits until then to be written.
|
||||
pending_until: Option<std::time::Instant>,
|
||||
}
|
||||
|
||||
impl PrefsWriter {
|
||||
/// The least time between two writes of the settings file.
|
||||
const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
|
||||
|
||||
/// The settings as they were read at start: they are on disk already.
|
||||
fn read(prefs: String) -> Self {
|
||||
Self { written: Some(prefs), ..Self::default() }
|
||||
}
|
||||
|
||||
/// Whether this frame's settings are worth comparing: input that can change them came in this
|
||||
/// frame or the one before, or a waiting change's time has come. Pointer motion alone isn't
|
||||
/// such input; eframe's own save still catches anything else.
|
||||
fn due(&mut self, input: bool, now: std::time::Instant) -> bool {
|
||||
let due = input || self.input_before || self.pending_until.is_some_and(|at| now >= at);
|
||||
self.input_before = input;
|
||||
due
|
||||
}
|
||||
|
||||
/// Write `prefs` and flush them to disk if they differ from the settings written last and the
|
||||
/// last write is at least [`Self::INTERVAL`] old. Returns how long a change waits otherwise:
|
||||
/// the caller asks for a frame then, so an idle app still writes it.
|
||||
fn store(&mut self, storage: &mut dyn eframe::Storage, prefs: String, now: std::time::Instant) -> Option<std::time::Duration> {
|
||||
if self.written.as_deref() == Some(prefs.as_str()) {
|
||||
self.pending_until = None;
|
||||
return None;
|
||||
}
|
||||
if let Some(wait) = self.last_write.map(|at| Self::INTERVAL.saturating_sub(now.saturating_duration_since(at))).filter(|w| !w.is_zero()) {
|
||||
self.pending_until = Some(now + wait);
|
||||
return Some(wait);
|
||||
}
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
|
||||
storage.flush();
|
||||
self.written = Some(prefs);
|
||||
self.last_write = Some(now);
|
||||
self.pending_until = None;
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Input that can change a setting: a click, a key, typed or pasted text, a wheel or a pinch.
|
||||
/// Pointer motion, which comes every frame while the mouse moves, cannot by itself.
|
||||
fn changes_settings(event: &egui::Event) -> bool {
|
||||
matches!(
|
||||
event,
|
||||
egui::Event::PointerButton { .. }
|
||||
| egui::Event::Key { .. }
|
||||
| egui::Event::Text(_)
|
||||
| egui::Event::Paste(_)
|
||||
| egui::Event::Cut
|
||||
| egui::Event::Ime(_)
|
||||
| egui::Event::MouseWheel { .. }
|
||||
| egui::Event::Zoom(_)
|
||||
| egui::Event::Rotate(_)
|
||||
| egui::Event::AccessKitActionRequest(_)
|
||||
)
|
||||
}
|
||||
|
||||
impl eframe::App for App {
|
||||
@@ -184,11 +266,21 @@ impl eframe::App for App {
|
||||
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) {
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
content_ui(&mut self.solve, ui);
|
||||
// After the frame's UI, so a setting changed in it (Preferences' OK) is written now.
|
||||
let now = std::time::Instant::now();
|
||||
if self.prefs_writer.due(ui.input(|i| i.events.iter().any(changes_settings)), now)
|
||||
&& let Some(storage) = frame.storage_mut()
|
||||
&& let Some(wait) = self.prefs_writer.store(storage, self.solve.prefs(), now)
|
||||
{
|
||||
ui.ctx().request_repaint_after(wait);
|
||||
}
|
||||
}
|
||||
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, self.solve.prefs());
|
||||
let prefs = self.solve.prefs();
|
||||
storage.set_string(solidcraft_ui_egui::prefs::STORAGE_KEY, prefs.clone());
|
||||
self.prefs_writer.written = Some(prefs);
|
||||
}
|
||||
fn on_exit(&mut self) {
|
||||
// The agent chat's agent and everything it started end with the app.
|
||||
@@ -202,13 +294,29 @@ impl eframe::App for App {
|
||||
}
|
||||
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`).
|
||||
// Before a restart (`Drop for App`) 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);
|
||||
// The next start (after a lost device, or into an update) begins once the app is dropped
|
||||
// (`Drop for App`), when the settings are on disk.
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
/// eframe drops its settings storage first, which waits for `app.ron` to be written, then the
|
||||
/// app, then the window and its GPU surface. So the next start begins here: started from
|
||||
/// `on_exit`, it read `app.ron` while the old process was still writing it, found it
|
||||
/// unreadable and moved it aside, resetting every setting (#9044).
|
||||
fn drop(&mut self) {
|
||||
if let Some(gpu) = &self.gpu
|
||||
&& let Some(lost_at) = gpu.lost_at
|
||||
{
|
||||
device_lost::restart(gpu.restart_on_software, lost_at, &gpu.reason);
|
||||
// The window's GPU surface is still to be dropped, and on a lost device that may
|
||||
// never return (#9044).
|
||||
device_lost::end();
|
||||
}
|
||||
// An update asked to restart into the new version or to run its installer.
|
||||
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve) {
|
||||
@@ -236,6 +344,8 @@ struct LostWatch {
|
||||
restart_on_software: bool,
|
||||
/// When the loss was seen: the app is closing to start again.
|
||||
lost_at: Option<u64>,
|
||||
/// Why, as wgpu said it: passed to the next start ([`device_lost::REASON_ENV`]).
|
||||
reason: String,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -249,6 +359,7 @@ impl App {
|
||||
eprintln!("SolidCraft graphics: the device was lost ({reason}); starting again");
|
||||
solidcraft_engine::crash::phase(&format!("graphics device lost: {reason}"));
|
||||
gpu.lost_at = Some(device_lost::now_secs());
|
||||
gpu.reason = reason;
|
||||
self.solve.quit_requested = true;
|
||||
}
|
||||
}
|
||||
@@ -422,6 +533,12 @@ fn services(cc: &eframe::CreationContext<'_>, folder: &DialogFolder) -> Services
|
||||
graphics_adapters: Vec::new(),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
window_system: window_system_now(),
|
||||
// Linux: the desktop's dark or light setting, which the window is not told on every
|
||||
// desktop (#7944). Windows and macOS: the window's own theme.
|
||||
#[cfg(target_os = "linux")]
|
||||
desktop_dark: Some(desktop_theme::start(&cc.egui_ctx)),
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
desktop_dark: None,
|
||||
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.
|
||||
@@ -701,6 +818,24 @@ fn msaa() -> Option<u16> {
|
||||
/// (`solidcraft_ui_egui::titlebar`). macOS keeps its traffic lights over the integrated bar.
|
||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||
|
||||
/// Linux: the window manager's own decorations (server-side, or the compositor's frame) can be
|
||||
/// asked for instead of the application bar ([`titlebar_choice`]). Elsewhere always `true`.
|
||||
fn app_titlebar_wanted() -> bool {
|
||||
if !cfg!(all(unix, not(target_os = "macos"))) {
|
||||
return true;
|
||||
}
|
||||
let setting = graphics().and_then(|v| v.get("titlebar").and_then(serde_json::Value::as_str).map(str::to_string));
|
||||
titlebar_choice(solidcraft_engine::geom::env::var("TITLEBAR").as_deref(), setting.as_deref())
|
||||
}
|
||||
|
||||
/// Linux: whether the application bar is the title bar. `SOLIDCRAFT_TITLEBAR` (else
|
||||
/// `graphics.json`'s `titlebar`) set to `system` gives the window the desktop's own title bar and
|
||||
/// buttons; `app` (or anything else, or nothing) keeps the application bar as the title bar.
|
||||
fn titlebar_choice(env: Option<&str>, setting: Option<&str>) -> bool {
|
||||
let choice = env.map(str::trim).filter(|v| !v.is_empty()).or(setting.map(str::trim));
|
||||
!choice.is_some_and(|v| v.eq_ignore_ascii_case("system"))
|
||||
}
|
||||
|
||||
const DEPTH_BITS: u8 = 24;
|
||||
/// A 32-bit float depth buffer, drawn with reversed depth (1 near, 0 far) so its precision follows
|
||||
/// the distance from the eye (#1434). Only where every adapter offers it with a stencil
|
||||
@@ -750,6 +885,18 @@ fn main() -> eframe::Result {
|
||||
#[cfg(target_os = "macos")]
|
||||
objc2_foundation::NSProcessInfo::processInfo().setProcessName(objc2_foundation::ns_string!("SolidCraft"));
|
||||
mark("main");
|
||||
// On OCLP-patched Macs, wgpu picks a Metal Shading Language standard that the system's
|
||||
// compiler refuses (`invalid value 'metal3.2'`), because the patched OS reports a newer
|
||||
// macOS version than the GPU's Metal compiler supports. Re-launching with
|
||||
// SYSTEM_VERSION_COMPAT=1 makes the OS report the real (older) version so wgpu selects
|
||||
// a standard the compiler accepts. The variable must be set before the process starts, so
|
||||
// we detect OCLP here and re-exec immediately, before any macOS framework call (#3080, #4605).
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Err(e) = startup::relaunch_with_version_compat() {
|
||||
log::warn!("startup: OCLP check failed (continuing normally): {e}");
|
||||
}
|
||||
// SOLIDCRAFT_PROFILE=1: the profiler records from the start (diagnostics.profile reads it).
|
||||
solidcraft_engine::timing::profile_from_env();
|
||||
// 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() {
|
||||
@@ -821,7 +968,11 @@ fn main() -> eframe::Result {
|
||||
// The launch before this one closed because its graphics device was lost (`App::watch_device`).
|
||||
let after_loss = device_lost::recent(std::env::var(device_lost::LOST_ENV).ok().as_deref(), device_lost::now_secs());
|
||||
if after_loss {
|
||||
notes.push(device_lost::note(cpu_renderer));
|
||||
let reason = std::env::var(device_lost::REASON_ENV).ok();
|
||||
if let Some(reason) = device_lost::reason(reason.as_deref()) {
|
||||
log::warn!("graphics: the last launch lost its device ({reason})");
|
||||
}
|
||||
notes.push(device_lost::note(cpu_renderer, reason.as_deref()));
|
||||
}
|
||||
if safe_mode {
|
||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||
@@ -857,7 +1008,7 @@ fn main() -> eframe::Result {
|
||||
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);
|
||||
let custom_titlebar = CUSTOM_TITLEBAR && app_titlebar_wanted() && !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 {
|
||||
@@ -961,6 +1112,7 @@ fn main() -> eframe::Result {
|
||||
lost: rs.device_lost.clone(),
|
||||
restart_on_software: device_lost::restart_on_software(after_loss, cpu_renderer),
|
||||
lost_at: None,
|
||||
reason: String::new(),
|
||||
});
|
||||
if !safe_mode {
|
||||
app.preferences.graphics_adapter =
|
||||
@@ -976,6 +1128,12 @@ fn main() -> eframe::Result {
|
||||
})
|
||||
.collect();
|
||||
let info = rs.adapter.get_info();
|
||||
if !app.preferences.graphics_adapter.is_empty() && !info.name.eq_ignore_ascii_case(app.preferences.graphics_adapter.as_str()) {
|
||||
eprintln!(
|
||||
"SolidCraft graphics: preferred adapter {:?} was not used; using {:?} instead",
|
||||
app.preferences.graphics_adapter, info.name
|
||||
);
|
||||
}
|
||||
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)
|
||||
@@ -1080,6 +1238,8 @@ fn main() -> eframe::Result {
|
||||
mark("app created");
|
||||
solidcraft_engine::crash::phase("first frame");
|
||||
Ok(Box::new(App {
|
||||
// Before `app` moves: the settings as read, already on disk.
|
||||
prefs_writer: PrefsWriter::read(app.prefs()),
|
||||
solve: app,
|
||||
starting,
|
||||
frames: 0,
|
||||
@@ -1103,12 +1263,13 @@ fn main() -> eframe::Result {
|
||||
}
|
||||
|
||||
/// egui-wgpu is about to start a step of getting the graphics device and the window's surface up:
|
||||
/// the crash journal keeps it, and the adapter once it is picked.
|
||||
/// the crash journal keeps it until the app runs (a window opened later goes through the same
|
||||
/// steps), and the adapter once it is picked.
|
||||
fn startup_step(step: &'static str, detail: &str) {
|
||||
if step == eframe::egui_wgpu::STEP_ADAPTER_CHOSEN {
|
||||
solidcraft_engine::crash::set_environment(&format!("Renderer: {detail}"));
|
||||
}
|
||||
solidcraft_engine::crash::phase(step);
|
||||
solidcraft_engine::crash::startup_step(step);
|
||||
}
|
||||
|
||||
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe). The
|
||||
@@ -1270,6 +1431,148 @@ mod backend_tests {
|
||||
assert!(!prefer_x11(true, true, Some("GNOME"), over(Some("wayland"))));
|
||||
assert!(prefer_x11(true, true, Some("niri"), over(Some("x11"))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_desktops_title_bar_only_when_asked_for() {
|
||||
use super::titlebar_choice;
|
||||
// Nothing set: the application bar is the title bar, as before.
|
||||
assert!(titlebar_choice(None, None));
|
||||
// graphics.json's `titlebar`.
|
||||
assert!(!titlebar_choice(None, Some("system")));
|
||||
assert!(titlebar_choice(None, Some("app")));
|
||||
// SOLIDCRAFT_TITLEBAR wins over the file; empty counts as unset; case and spaces don't matter.
|
||||
assert!(!titlebar_choice(Some(" System "), Some("app")));
|
||||
assert!(titlebar_choice(Some("app"), Some("system")));
|
||||
assert!(!titlebar_choice(Some(""), Some("system")));
|
||||
// Anything unknown keeps the application bar.
|
||||
assert!(titlebar_choice(Some("ssd"), None));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod prefs_writer_tests {
|
||||
use super::PrefsWriter;
|
||||
use solidcraft_ui_egui::prefs::{NavigatorStyle, STORAGE_KEY};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// eframe's settings file: what was flushed to disk is what the next start reads.
|
||||
#[derive(Default)]
|
||||
struct Disk {
|
||||
pending: HashMap<String, String>,
|
||||
flushed: HashMap<String, String>,
|
||||
flushes: usize,
|
||||
}
|
||||
|
||||
impl eframe::Storage for Disk {
|
||||
fn get_string(&self, key: &str) -> Option<String> {
|
||||
self.pending.get(key).cloned()
|
||||
}
|
||||
fn set_string(&mut self, key: &str, value: String) {
|
||||
self.pending.insert(key.into(), value);
|
||||
}
|
||||
fn remove_string(&mut self, key: &str) {
|
||||
self.pending.remove(key);
|
||||
}
|
||||
fn flush(&mut self) {
|
||||
self.flushed = self.pending.clone();
|
||||
self.flushes += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn new_app() -> solidcraft_ui_egui::SolveApp {
|
||||
solidcraft_ui_egui::SolveApp::new(solidcraft_engine::Session::default(), solidcraft_ui_egui::Services::default())
|
||||
}
|
||||
|
||||
/// #8275: Preferences' View navigator and Display quality, changed and confirmed, are on disk
|
||||
/// in that frame, so the next start has them even when this one never closes normally (ended
|
||||
/// from the Task Manager, a sign-out, a hang or crash in the graphics driver on the way out).
|
||||
#[test]
|
||||
fn changed_settings_reach_the_disk_in_the_frame_they_change() {
|
||||
let mut app = new_app();
|
||||
let mut disk = Disk::default();
|
||||
let mut writer = PrefsWriter::read(app.prefs());
|
||||
let now = Instant::now();
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
assert_eq!(disk.flushes, 0, "the settings read at start are not written again");
|
||||
app.preferences.navigator_style = NavigatorStyle::Cube;
|
||||
app.preferences.display_quality = "fine".into();
|
||||
assert!(writer.due(true, now), "a frame with input is checked");
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now), None);
|
||||
assert_eq!(disk.flushes, 1, "flushed at once, not left for eframe's next save");
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), now + Duration::from_secs(5)), None);
|
||||
assert_eq!(disk.flushes, 1, "unchanged settings are not written again");
|
||||
|
||||
// The next start reads them.
|
||||
let mut next = new_app();
|
||||
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
|
||||
next.load_prefs(&stored);
|
||||
assert_eq!(next.preferences.navigator_style, NavigatorStyle::Cube);
|
||||
assert_eq!(next.preferences.display_quality, "fine");
|
||||
assert!(disk.flushed.contains_key(STORAGE_KEY));
|
||||
}
|
||||
|
||||
/// A frame without input is checked only right after one with input (whose clicks the UI
|
||||
/// handled in that frame); after that, idle frames cost nothing. Pointer motion, which comes
|
||||
/// every frame while the mouse moves, doesn't count as input.
|
||||
#[test]
|
||||
fn frames_are_checked_with_input_and_once_after_it() {
|
||||
let now = Instant::now();
|
||||
let mut writer = PrefsWriter::default();
|
||||
assert!(!writer.due(false, now));
|
||||
assert!(writer.due(true, now));
|
||||
assert!(writer.due(false, now), "the frame after input");
|
||||
assert!(!writer.due(false, now));
|
||||
|
||||
let click = egui::Event::PointerButton {
|
||||
pos: egui::Pos2::ZERO,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed: false,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
};
|
||||
assert!(super::changes_settings(&click));
|
||||
assert!(super::changes_settings(&egui::Event::Text("5".into())));
|
||||
assert!(!super::changes_settings(&egui::Event::PointerMoved(egui::Pos2::ZERO)));
|
||||
assert!(!super::changes_settings(&egui::Event::MouseMoved(egui::Vec2::X)));
|
||||
}
|
||||
|
||||
/// A setting that changes in every frame (a panel's width while its edge is dragged) doesn't
|
||||
/// write the file in every frame: at most once a second, and the last change is on disk at
|
||||
/// most a second after it was made, with a frame asked for then so an idle app writes it.
|
||||
#[test]
|
||||
fn a_continuous_change_is_written_once_a_second_and_its_end_soon_after() {
|
||||
let mut app = new_app();
|
||||
let mut disk = Disk::default();
|
||||
let mut writer = PrefsWriter::read(app.prefs());
|
||||
let start = Instant::now();
|
||||
let frame = Duration::from_millis(16);
|
||||
let mut asked = None;
|
||||
// 1.5 s of dragging at 60 frames a second, with input in every frame.
|
||||
for n in 0..94u32 {
|
||||
let now = start + frame * n;
|
||||
app.preferences.browser_width = 200.0 + n as f32;
|
||||
assert!(writer.due(true, now));
|
||||
asked = writer.store(&mut disk, app.prefs(), now);
|
||||
}
|
||||
assert_eq!(disk.flushes, 2, "the first change at once, then one a second later");
|
||||
let wait = asked.expect("the last width waits, and a frame is asked for when it can be written");
|
||||
assert!(wait <= PrefsWriter::INTERVAL);
|
||||
|
||||
// The drag ends: no more input. Frames before the wait is over don't even look.
|
||||
let end = start + frame * 93;
|
||||
assert!(writer.due(false, end + frame), "the frame right after input");
|
||||
assert!(!writer.due(false, end + frame * 2), "an idle frame before the wait is over");
|
||||
// The frame asked for writes the last width.
|
||||
let then = end + wait;
|
||||
assert!(writer.due(false, then));
|
||||
assert_eq!(writer.store(&mut disk, app.prefs(), then), None);
|
||||
assert_eq!(disk.flushes, 3);
|
||||
let mut next = new_app();
|
||||
let stored = solidcraft_ui_egui::prefs::stored(|k| disk.flushed.get(k).cloned()).expect("settings on disk");
|
||||
next.load_prefs(&stored);
|
||||
assert_eq!(next.preferences.browser_width, 293.0, "the width the drag ended at");
|
||||
assert!(!writer.due(false, then + frame), "nothing waits any more");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -101,6 +101,7 @@ impl NativeMenu {
|
||||
])?;
|
||||
for (action, label) in [
|
||||
("docs", "Documentation"),
|
||||
("updates", app.tr("update.check_for_updates")),
|
||||
("shortcuts", "Keyboard Shortcuts"),
|
||||
("messages", "Messages\u{2026}"),
|
||||
("report", "Report an Issue\u{2026}"),
|
||||
|
||||
+111
-10
@@ -137,8 +137,11 @@ pub struct AdapterChoice {
|
||||
pub compatible: bool,
|
||||
}
|
||||
|
||||
/// Choose only surface-compatible adapters. Names saved in preferences are exact; the
|
||||
/// environment override follows wgpu's case-insensitive substring convention.
|
||||
/// Choose an adapter by name. The environment override follows wgpu's case-insensitive
|
||||
/// substring convention among surface-compatible adapters. A saved preference prefers an
|
||||
/// exact (then case-insensitive) name among compatible adapters, and if the named card is
|
||||
/// only listed as incompatible with the surface still picks it (#8341: otherwise the
|
||||
/// preference is kept in the dialog while wgpu silently uses another GPU).
|
||||
/// 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.
|
||||
@@ -149,12 +152,14 @@ pub fn choose_adapter(adapters: &[AdapterChoice], environment: Option<&str>, pre
|
||||
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
|
||||
let name = preference.filter(|s| !s.is_empty())?;
|
||||
let find = |pred: &dyn Fn(&AdapterChoice) -> bool, only_compatible: bool| {
|
||||
adapters.iter().enumerate().find(|(_, a)| (!only_compatible || a.compatible) && pred(a)).map(|(i, _)| i)
|
||||
};
|
||||
find(&|a| a.name == name, true)
|
||||
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), true))
|
||||
.or_else(|| find(&|a| a.name == name, false))
|
||||
.or_else(|| find(&|a| a.name.eq_ignore_ascii_case(name), false))
|
||||
}
|
||||
|
||||
/// Leave Automatic on wgpu's normal request path; named choices may defer to it too.
|
||||
@@ -292,6 +297,71 @@ pub fn window_instance_flags(flags: eframe::wgpu::InstanceFlags, intel_mac: bool
|
||||
if intel_mac && env.map(str::trim).is_none_or(str::is_empty) { flags - eframe::wgpu::InstanceFlags::VALIDATION_INDIRECT_CALL } else { flags }
|
||||
}
|
||||
|
||||
/// The environment variable that makes the macOS compatibility shim report an older OS
|
||||
/// version to wgpu's Metal backend. On OCLP-patched machines the Metal compiler accepts
|
||||
/// only up to the MSL standard of the original GPU, not the one the patched OS reports,
|
||||
/// so wgpu picks a standard that the compiler refuses (`invalid value 'metal3.2'`).
|
||||
/// Setting this variable before the process starts causes the OS to report the older
|
||||
/// version and wgpu selects a supported MSL standard (#3080, #4605).
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const SYSTEM_VERSION_COMPAT_ENV: &str = "SYSTEM_VERSION_COMPAT";
|
||||
|
||||
/// The NVRAM variable that OpenCore Legacy Patcher writes on every OCLP-booted system.
|
||||
/// Its presence is the primary signal that `SYSTEM_VERSION_COMPAT=1` is needed.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub const OCLP_NVRAM_KEY: &str = "4D1FDA02-38C7-4A6A-9CC6-4BCCA8B30102:OCLP-Version";
|
||||
|
||||
/// `true` if the OCLP NVRAM key is present on this system.
|
||||
///
|
||||
/// Runs `nvram <key>` and treats exit 0 as present. Any error (missing binary,
|
||||
/// permission denied, key absent) returns `false` — this is a best-effort probe.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn oclp_nvram_present() -> bool {
|
||||
std::process::Command::new("nvram").arg(OCLP_NVRAM_KEY).output().map(|o| o.status.success()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Whether to re-launch with `SYSTEM_VERSION_COMPAT=1`: it is not set yet and OCLP shows through
|
||||
/// the NVRAM key. The probe runs only when it is needed, so a re-launched process (variable
|
||||
/// set) never starts `nvram`.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn compat_wanted(compat_set: bool, nvram: impl FnOnce() -> bool) -> bool {
|
||||
!compat_set && nvram()
|
||||
}
|
||||
|
||||
/// If this process is running under OCLP and `SYSTEM_VERSION_COMPAT` has not been set,
|
||||
/// re-launch the current binary with `SYSTEM_VERSION_COMPAT=1` added to the environment,
|
||||
/// then exit with the child's status code.
|
||||
///
|
||||
/// `SYSTEM_VERSION_COMPAT=1` must be present **before** the process starts: setting it
|
||||
/// with `std::env::set_var` inside `main` is too late because the macOS frameworks have
|
||||
/// already queried the OS version by then. Re-launching is the only safe approach that
|
||||
/// works without `unsafe` (a true `execve` replacement would require it).
|
||||
///
|
||||
/// Returns `Ok(false)` immediately when:
|
||||
/// - `SYSTEM_VERSION_COMPAT` is already set (re-launched process, no loop), or
|
||||
/// - the OCLP NVRAM key is absent.
|
||||
///
|
||||
/// Returns `Err` only when `std::env::current_exe()` fails or the child process cannot
|
||||
/// be spawned; the caller should log the error and continue normally.
|
||||
///
|
||||
/// Never returns `Ok(true)`: a successful re-launch exits the current process.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn relaunch_with_version_compat() -> Result<bool, String> {
|
||||
// A set variable means this is the re-launched process: no loop.
|
||||
if !compat_wanted(std::env::var_os(SYSTEM_VERSION_COMPAT_ENV).is_some(), oclp_nvram_present) {
|
||||
return Ok(false);
|
||||
}
|
||||
log::info!("startup: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
eprintln!("SolidCraft: OCLP detected; re-launching with {}=1", SYSTEM_VERSION_COMPAT_ENV);
|
||||
let exe = std::env::current_exe().map_err(|e| format!("could not find own executable: {e}"))?;
|
||||
let status = std::process::Command::new(&exe)
|
||||
.args(std::env::args_os().skip(1))
|
||||
.env(SYSTEM_VERSION_COMPAT_ENV, "1")
|
||||
.status()
|
||||
.map_err(|e| format!("could not re-launch with {}: {e}", SYSTEM_VERSION_COMPAT_ENV))?;
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -351,7 +421,8 @@ mod tests {
|
||||
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");
|
||||
assert_eq!(choose_adapter(&cards, None, Some("discrete small")), Some(1), "preference matches without regard to case");
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete")), None, "a preference is the full name, not a substring");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -360,11 +431,20 @@ mod tests {
|
||||
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);
|
||||
// Environment still needs a compatible adapter; a preference keeps the named card (#8341).
|
||||
assert_eq!(choose_adapter(&cards, Some("large"), Some("Discrete large")), Some(2));
|
||||
assert_eq!(choose_adapter(&cards, Some("small"), None), Some(1));
|
||||
assert_eq!(choose_adapter(&[], None, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preference_keeps_a_named_card_even_when_surface_says_incompatible() {
|
||||
let mut cards = cards();
|
||||
cards[1].compatible = false;
|
||||
assert_eq!(choose_adapter(&cards, None, Some("Discrete small")), Some(1));
|
||||
assert_eq!(choose_adapter(&cards, None, Some("discrete SMALL")), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_adapter_preserves_the_previous_request_configuration() {
|
||||
use eframe::egui_wgpu::WgpuSetupCreateNew;
|
||||
@@ -414,6 +494,27 @@ mod tests {
|
||||
assert_eq!(window_instance_flags(on, false, None), on);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn oclp_relaunch_needs_the_signal_and_no_compat_variable() {
|
||||
use super::compat_wanted;
|
||||
let probed = std::cell::Cell::new(false);
|
||||
let probe = |found: bool| {
|
||||
let probed = &probed;
|
||||
move || {
|
||||
probed.set(true);
|
||||
found
|
||||
}
|
||||
};
|
||||
// The re-launched process has the variable: no probe, no second re-launch.
|
||||
assert!(!compat_wanted(true, probe(true)));
|
||||
assert!(!probed.get());
|
||||
// The OCLP NVRAM key is the signal.
|
||||
assert!(compat_wanted(false, probe(true)));
|
||||
// No signal: an ordinary Mac starts as it is.
|
||||
assert!(!compat_wanted(false, probe(false)));
|
||||
}
|
||||
|
||||
#[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")));
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
|
||||
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
|
||||
Subset for SolidCraft web UI (glyphs used by the zh locale); renamed Craft UI SC (no Reserved Font Name).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,96 @@
|
||||
Copyright 2014-2021 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'.
|
||||
Copyright 2014-2021 Google Inc. with Reserved Font Name 'Noto'.
|
||||
Subset for SolidCraft web UI (glyphs used by the zh-TW locale); renamed Craft UI TC (no Reserved Font Name).
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -181,6 +181,7 @@ pipe false
|
||||
plane false
|
||||
plane_along_path false
|
||||
plane_angle false
|
||||
plane_axis_point false
|
||||
plane_midplane false
|
||||
plane_perpendicular false
|
||||
plane_tangent false
|
||||
@@ -201,6 +202,7 @@ polygon false
|
||||
polygon_circumscribed false
|
||||
polygon_edge false
|
||||
presspull false
|
||||
print_face false
|
||||
project false
|
||||
project_surface false
|
||||
rack_pinion false
|
||||
@@ -235,6 +237,7 @@ settings_monitor false
|
||||
settings_mouse false
|
||||
settings_palette false
|
||||
settings_plug false
|
||||
settings_profile false
|
||||
sheet false
|
||||
sheet_rules false
|
||||
shell false
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Settings profile</title>
|
||||
<circle cx="32" cy="19" r="8" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 53C12 44.7157 20.9543 38 32 38C43.0457 38 52 44.7157 52 53" fill="none" stroke="currentColor" stroke-width="5.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 413 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Plane through axis and point</title>
|
||||
<path d="M26 15L56 32.32L38 42.71L8 25.39Z" fill="#b9ddf8" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round" stroke-dasharray="0.1 5"/>
|
||||
<path d="M15 18.4L53 40.34" fill="none" stroke="currentColor" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<circle cx="22" cy="29" r="3.1" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
@@ -0,0 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<title>Print Face</title>
|
||||
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="#b8c3cf"/>
|
||||
<path d="M9.72 26.17L4.5 20.95L4.5 40.85L9.72 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="#328bd3"/>
|
||||
<path d="M18.77 23.15L9.72 26.17L9.72 46.07L18.77 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="#e2e8ef"/>
|
||||
<path d="M13.55 17.93L18.77 23.15L9.72 26.17L4.5 20.95Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="#b8c3cf"/>
|
||||
<path d="M50.45 37.54L38.27 25.35L38.27 33.88L50.45 46.07Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="#8e9faf"/>
|
||||
<path d="M59.5 34.53L50.45 37.54L50.45 46.07L59.5 43.05Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="#e2e8ef"/>
|
||||
<path d="M47.31 22.34L59.5 34.53L50.45 37.54L38.27 25.35Z" fill="none" stroke="#34465b" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M50.45 46.07L38.27 33.88" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M59.5 43.05L50.45 46.07" fill="none" stroke="#236ead" stroke-width="2.55" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M24.17 34.2L28.31 34.2" fill="none" stroke="#236ead" stroke-width="4.25" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<path d="M35.31 34.2L27.31 38.7L27.31 29.7Z" fill="#236ead"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -1,7 +1,8 @@
|
||||
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
|
||||
use crate::theme::Tokens;
|
||||
use egui::{Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
|
||||
use solidcraft_engine::geom::{Ring, Vec3};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcraft_engine::geom::Ring;
|
||||
pub(crate) struct Facet {
|
||||
pub(crate) direction: Vec3,
|
||||
pub(crate) points: Ring<Pos2>,
|
||||
|
||||
+727
-17
File diff suppressed because it is too large
Load Diff
+501
-3
@@ -60,6 +60,7 @@
|
||||
"BLEND SURFACE": "PRELAZNA POVRŠ",
|
||||
"BOUNDARY SURFACE": "GRANIČNA POVRŠ",
|
||||
"BROWSER": "STABLO MODELA",
|
||||
"Back to the original order": "Nazad na izvorni redoslijed",
|
||||
"Ball": "Kuglasti",
|
||||
"Bar": "Šipka",
|
||||
"Base Center": "Centar osnove",
|
||||
@@ -595,6 +596,8 @@
|
||||
"Snap to face and edge centres": "Privlači na centre lica i ivica",
|
||||
"SolidCraft cannot submit reports directly. Submit on GitHub; no token is needed in the app.": "SolidCraft ne može slati izvještaje direktno. Pošaljite ih na GitHubu; u aplikaciji nije potreban token.",
|
||||
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft se zatvorio prije nego što su ovi dizajni sačuvani. Može se otvoriti njihovo posljednje automatsko čuvanje.",
|
||||
"Sort A to Z": "Sortiraj od A do Ž",
|
||||
"Sort Z to A": "Sortiraj od Ž do A",
|
||||
"Spacing 2 must be a non-zero number": "Razmak 2 mora biti broj različit od nule",
|
||||
"Spacing must be a non-zero number": "Razmak mora biti broj različit od nule",
|
||||
"Specialise": "Specijalizuj",
|
||||
@@ -775,6 +778,8 @@
|
||||
"at the face": "na licu",
|
||||
"bom.group_children": "Poddijelovi u redu svog dijela",
|
||||
"browser.body_name": "Tijelo{number}",
|
||||
"browser.calibrate_click_a": "Kliknite sliku da zadate tačku A ili upišite njene koordinate.",
|
||||
"browser.calibrate_click_b": "Kliknite sliku da zadate tačku B ili upišite njene koordinate.",
|
||||
"browser.calibrate_title": "Kalibriši: {name}",
|
||||
"browser.capture_many": "Pomjerene komponente bez zabilježenog položaja: {count}.",
|
||||
"browser.capture_note": "Zabilježi zadržava novi položaj; Vrati ga vraća nazad.",
|
||||
@@ -788,6 +793,13 @@
|
||||
"browser.sketch_attachment_lost": "{name} (pričvršćenje izgubljeno)",
|
||||
"browser.sketch_editing": "{name} (uređuje se)",
|
||||
"browser.sketch_name": "Skica{number}",
|
||||
"browser.sketches_to_parts_already_in_part": "već u svom dijelu",
|
||||
"browser.sketches_to_parts_design_body": "na tijelu samog dizajna",
|
||||
"browser.sketches_to_parts_moved": "Skice premještene u dio na kojem se nalaze: {moved}",
|
||||
"browser.sketches_to_parts_no_exact_attachment": "na licu bez tačnog pričvršćenja",
|
||||
"browser.sketches_to_parts_not_on_face": "ne na licu dijela",
|
||||
"browser.sketches_to_parts_part_missing": "njihov dio više ne postoji",
|
||||
"browser.sketches_to_parts_result": "Skice premještene u dio na kojem se nalaze: {moved}; ostale na mjestu: {stayed} ({why})",
|
||||
"browser.units": "Jedinice: {units}",
|
||||
"cam.flat": "Ravno lice",
|
||||
"cam.follower": "Pratilac",
|
||||
@@ -817,6 +829,8 @@
|
||||
"cmd.appearance_pick_up": "Preuzmi izgled",
|
||||
"cmd.appearance_replace": "Zamijeni izgled",
|
||||
"cmd.assembly_bom": "Sastavnica",
|
||||
"cmd.assembly_mass_properties": "Svojstva mase",
|
||||
"cmd.body.print_face": "Površina za štampu",
|
||||
"cmd.body_rename": "Preimenuj tijelo",
|
||||
"cmd.browser_collapse_all": "Skupi sve",
|
||||
"cmd.browser_expand_all": "Proširi sve",
|
||||
@@ -853,6 +867,7 @@
|
||||
"cmd.component_move_bodies": "Premjesti tijela u komponentu",
|
||||
"cmd.component_move_features": "Premjesti operacije u komponentu",
|
||||
"cmd.component_move_sketches": "Premjesti skice u komponentu",
|
||||
"cmd.component_move_sketches_to_parts": "Premjesti skice u njihove dijelove",
|
||||
"cmd.component_paste_from_design": "Zalijepi iz drugog dizajna",
|
||||
"cmd.component_paste_new": "Zalijepi kao novo",
|
||||
"cmd.component_properties": "Fizička svojstva komponente",
|
||||
@@ -1188,6 +1203,7 @@
|
||||
"cmd.sketch_spline_control_point_5": "Splajn kontrolnih tačaka (stepen 5)",
|
||||
"cmd.sketch_spline_fit_point": "Splajn kroz tačke",
|
||||
"cmd.sketch_spline_insert_point": "Dodaj tačku splajna",
|
||||
"cmd.sketch_spline_remove_point": "Ukloni tačku splajna",
|
||||
"cmd.sketch_spline_reverse": "Obrni smjer splajna",
|
||||
"cmd.sketch_spun_profile": "Rotirani profil",
|
||||
"cmd.sketch_toggle_diameter": "Prebaci poluprečnik / prečnik",
|
||||
@@ -1241,6 +1257,7 @@
|
||||
"cmd.view.render_browser": "Stablo renderovanja",
|
||||
"cmd.view.render_dock": "Premjesti panel renderovanja",
|
||||
"cmd.view.render_focus": "Prikaz fokusa renderovanja",
|
||||
"cmd.view.status_details": "Detalji statusa prozora prikaza",
|
||||
"cmd.view.status_items": "Stavke statusa prozora prikaza",
|
||||
"cmd.view_ambient_occlusion": "Ambijentalna okluzija",
|
||||
"cmd.view_angle_readout": "Uglovi prikaza",
|
||||
@@ -1279,8 +1296,14 @@
|
||||
"color.hex_hint": "#rrggbb",
|
||||
"color.hue": "Nijansa",
|
||||
"color.linear": "Prikaz vrijednosti boje u linearnom prostoru 0-1",
|
||||
"color.recent": "Nedavne boje",
|
||||
"color.recent_help": "Ovdje se pojavljuju boje koje izaberete u bilo kojem polju boje",
|
||||
"color.saturation": "Zasićenost (procenat)",
|
||||
"color.saturation_value": "Zasićenost i vrijednost",
|
||||
"color.selected": "Izabrana boja",
|
||||
"color.step_down": "Smanji za korak",
|
||||
"color.step_up": "Povećaj za korak",
|
||||
"color.value": "Vrijednost, svjetlina (procenat)",
|
||||
"config.column_filter_empty": "Nema odgovarajućih kolona",
|
||||
"contact.components_count": "Komponente: {count}",
|
||||
"contact.enabled_label": "omogućeno",
|
||||
@@ -1327,6 +1350,7 @@
|
||||
"dialog.dimension_name": "Kota: {name}",
|
||||
"dialog.edit_title": "UREDI {title}",
|
||||
"dialog.no_shown_body": "Nema prikazanog tijela za izmjenu: prikažite jedno ili izaberite Novo tijelo",
|
||||
"dialog.pattern_sketch_not_object": "Izaberite tijela ili operacije: skica nije ni jedno ni drugo. Ponovite njene krive pomoću alata Niz ili Preslikaj u samoj skici ili je prvo ekstrudirajte.",
|
||||
"dialog.select": "↖ Izaberi",
|
||||
"dialog.selected_count": "↖ Izabrano: {count}",
|
||||
"dialog.selection_guidance": "Izaberite ulaz da ga promijenite. Kliknite izabrane objekte da ih uklonite; Ctrl/Cmd-klik ih također uključuje i isključuje. Pogledajte pregled, a zatim pritisnite U redu.",
|
||||
@@ -1384,26 +1408,243 @@
|
||||
"from the rule": "iz pravila",
|
||||
"help.apache": "Apache licenca 2.0",
|
||||
"help.appearance.assign": "Daje izabranim tijelima, licima ili komponentama boju ili izgled materijala, bez promjene njihove težine. Koristite ga da razlikujete dijelove ili pregledate završnu obradu.",
|
||||
"help.assembly.bom": "Navodi dijelove dizajna ili komponente s njihovim količinama, po broju dijela, a popis može sačuvati kao CSV ili JSON. Koristite je kao popis dijelova za naručivanje i montažu.",
|
||||
"help.attributions": "Atribucije (ATTRIBUTION.md)",
|
||||
"help.build": "Verzija {version} · commit {commit} · izgrađeno {date}",
|
||||
"help.canvas.decal": "Postavlja sliku na ravno lice modela, poput naljepnice. Koristite je da prikažete etiketu, logotip ili štampani uzorak na dijelu.",
|
||||
"help.canvas.edit": "Mijenja platno: njegov položaj, veličinu, ugao, neprozirnost, preslikavanje, vidljivost ili naziv. Koristite ga da poravnate referentnu sliku sa skicom kojom je precrtavate.",
|
||||
"help.canvas.insert": "Postavlja PNG ili JPEG sliku na ravan ili lice kao prozirnu referencu. Koristite je da u skici precrtate fotografiju ili crtež, uz sliku skaliranu na poznatu veličinu.",
|
||||
"help.component.break_link": "Odvaja povezanu komponentu od njene datoteke dizajna, a njena tijela ostavlja kakva jesu. Koristite ga kada dio više ne treba pratiti izmjene izvorne datoteke.",
|
||||
"help.component.capture_position": "Zadržava položaje dijelova koje ste ručno pomjerili, tako da to postaju njihovi stvarni položaji. Koristite ga nakon što ručno namjestite dijelove koje nijedan spoj ne drži.",
|
||||
"help.component.create": "Dodaje novu, praznu komponentu u dizajn, unutar aktivne komponente osim ako izaberete drugog roditelja. Koristite je da novi dio sklopa dobije vlastito mjesto za tijela i skice.",
|
||||
"help.component.finish_edit": "Čuva dio koji uređujete na mjestu, vraća se u njegov sklop i ažurira sklop prema njemu. Upotrijebite kada završite izmjene povezanog dijela.",
|
||||
"help.component.from_bodies": "Pretvara svako izabrano tijelo u zasebnu komponentu, nazvanu po tijelu. Koristite ga kada dizajn izgrađen od odvojenih tijela treba postati sklop čiji se dijelovi mogu spajati i pomjerati.",
|
||||
"help.component.insert_design": "Umeće drugi sačuvani dizajn kao komponentu povezanu s njegovom datotekom, s njegovim punim tijelima postavljenim onako kako su tamo. Koristite ga da ponovo upotrijebite dio u sklopu i kasnije izmjene preuzmete pomoću Ažuriraj veze.",
|
||||
"help.component.properties": "Prikazuje zapreminu, masu, centar mase i momente inercije komponente, prema materijalima njenih tijela. Koristite ga da provjerite težinu i uravnoteženost sklopa.",
|
||||
"help.component.revert_position": "Vraća dijelove koje ste ručno pomjerili tamo gdje su bili, odbacujući pomjeranja koja nisu zadržana. Koristite ga da poništite probna pomjeranja u sklopu.",
|
||||
"help.component.update_links": "Ponovo čita povezane datoteke dizajna i zamjenjuje tijela tamo gdje su se promijenila. Koristite ga nakon uređivanja dijela koji ovaj sklop koristi.",
|
||||
"help.config.configure": "Dodaje parametre, operacije, materijale ili izglede kao kolone tabele konfiguracija. Koristite ga da napravite varijante jednog dizajna, npr. veličine nekog dijela.",
|
||||
"help.config.preview": "Provjerava može li se red tabele konfiguracija ponovo izgraditi, bez promjene dizajna ili njegove vremenske trake. Koristite ga prije prelaska na varijantu u koju niste sigurni.",
|
||||
"help.config.rules": "Prikazuje pravila tabele konfiguracija, njene redove i koji je red aktivan. Koristite ga da vidite kako su postavljene varijante dizajna.",
|
||||
"help.config.table": "Prikazuje tabelu konfiguracija: jedan red po varijanti, jednu kolonu po parametru ili operaciji i da li dizajn još odgovara aktivnom redu. Koristite je da pregledate varijante dizajna.",
|
||||
"help.connect_note": "Ponovo pokrenite svaki asistent da učita SolidCraft server. Svaka datoteka je sigurnosno kopirana kao .bak prije promjene.",
|
||||
"help.connect_title": "Poveži AI asistente",
|
||||
"help.construct.axis.edge": "Pravi pomoćnu osu duž ravne ivice ili linije skice. Koristite je kao osu za rotaciju, kružni niz ili spoj.",
|
||||
"help.construct.axis.normal_to_face": "Pravi pomoćnu osu kroz tačku, okomitu na lice. Koristite je kao osu koja stoji pravo iz površine.",
|
||||
"help.construct.axis.through_cylinder": "Pravi pomoćnu osu kroz centar cilindričnog, konusnog ili torusnog lica, ili kružne ivice. Koristite je da operacije okrećete ili ih postavljate u niz oko okruglog dijela.",
|
||||
"help.construct.axis.two_planes": "Pravi pomoćnu osu gdje se sastaju dvije ravni ili ravna lica. Koristite je kada je osa koja vam treba linija između dva lica.",
|
||||
"help.construct.axis.two_points": "Pravi pomoćnu osu kroz dvije tačke, vrhove ili tačke skice. Koristite je za osu koja prolazi između dva poznata položaja.",
|
||||
"help.construct.plane.along_path": "Pravi pomoćnu ravan okomitu na ivicu ili krivu, na nekom položaju duž nje. Koristite je da skicirate profil za povlačenje ili presjek putanje.",
|
||||
"help.construct.plane.at_angle": "Pravi pomoćnu ravan zakrenutu za ugao u odnosu na ravan ili lice, oko ose ili ivice. Koristite je za nagnute skice, npr. kutni nosač.",
|
||||
"help.construct.plane.midplane": "Pravi pomoćnu ravan na pola puta između dvije ravni ili ravna lica, ili koja ih raspolavlja tamo gdje se sastaju pod uglom. Koristite je kao ravan simetrije za preslikavanje.",
|
||||
"help.construct.plane.offset": "Pravi pomoćnu ravan paralelnu s ravni ili ravnim licem, na određenoj udaljenosti ili kroz tačku. Koristite je da skicirate na zadatoj visini iznad lica.",
|
||||
"help.construct.plane.perpendicular": "Pravi pomoćnu ravan koja sadrži liniju ili ivicu i okomita je na ravan ili lice. Koristite je da skicirate uspravno na modelu duž ivice.",
|
||||
"help.construct.plane.tangent": "Pravi pomoćnu ravan koja u jednoj tački dodiruje cilindar, konus, sferu ili torus. Koristite je da skicirate na boku okruglog dijela.",
|
||||
"help.construct.plane.three_points": "Pravi pomoćnu ravan kroz tri tačke. Koristite je za ravan određenu položajima na modelu, npr. s tri ugla.",
|
||||
"help.construct.plane.two_edges": "Pravi pomoćnu ravan kroz dvije ravne ivice ili linije koje leže u jednoj ravni. Koristite je za ravan kroz dio, npr. kroz dvije suprotne ivice.",
|
||||
"help.construct.point.along_path": "Pravi pomoćnu tačku na nekom položaju duž ivice ili krive. Koristite je kao referencu negdje duž putanje.",
|
||||
"help.construct.point.center": "Pravi pomoćnu tačku u centru kružne ivice, kruga ili luka skice, sfere ili torusa. Koristite je kao referencu za rupe, ose i spojeve.",
|
||||
"help.construct.point.edge_plane": "Pravi pomoćnu tačku gdje ivica ili linija prolazi kroz ravan ili ravno lice. Koristite je da označite gdje putanja siječe ravan.",
|
||||
"help.construct.point.three_planes": "Pravi pomoćnu tačku gdje se sastaju tri ravni ili ravna lica. Koristite je kao referencu ugla, čak i tamo gdje model nema vrh.",
|
||||
"help.construct.point.two_edges": "Pravi pomoćnu tačku gdje se dvije ivice ili linije sastaju ili su najbliže. Koristite je kao referencu u virtualnom uglu.",
|
||||
"help.construct.point.vertex": "Pravi pomoćnu tačku na vrhu ili tački skice. Koristite je kao referencu koja ostaje kada se skica ili tijelo oko nje sakrije.",
|
||||
"help.contact.check": "Navodi parove dijelova u skupovima kontakata koji se u trenutnom položaju međusobno presijecaju. Koristite ga da pronađete sudare prije nego što pokrenete spojeve ili studiju kretanja.",
|
||||
"help.contact.create": "Grupiše dva ili više dijelova u skup kontakata čija se tijela sprečavaju da prolaze jedno kroz drugo dok se spojevi kreću. Koristite ga za dijelove koji treba da se zaustave kad se dodirnu, npr. poluga uz svoj graničnik.",
|
||||
"help.contact.disable_all": "Isključuje kontakt, pa se pokretani spojevi i studije kretanja više ne zaustavljaju tamo gdje se dijelovi dodiruju. Koristite ga da ponovo slobodno pomjerate dijelove ili da veliki sklop ostane brz u radu.",
|
||||
"help.contact.edit": "Mijenja skup kontakata: dijelove koje sadrži, njegov naziv ili da li je potisnut; može i izbrisati skup. Koristite ga da podesite koji dijelovi ne smiju prolaziti jedan kroz drugi.",
|
||||
"help.contact.enable_all": "Stavlja svaki dio sklopa u jedan skup kontakata, pa nijedna dva dijela ne mogu prolaziti jedan kroz drugi dok se spojevi kreću. Koristite ga za brzu provjeru cijelog mehanizma.",
|
||||
"help.contact.enable_sets": "Uključuje kontakt za skupove kontakata koje ste definisali, pa se pokretani spojevi i studije kretanja zaustavljaju tamo gdje bi njihovi dijelovi prošli jedan kroz drugi. Koristite ga da vratite svoje skupove nakon isključivanja kontakta.",
|
||||
"help.contact.list": "Navodi skupove kontakata dizajna, dijelove u svakom od njih i da li je kontakt uključen. Koristite ga da provjerite kako je kontakt postavljen prije nego što pokrenete sklop.",
|
||||
"help.document.inspect": "Prikazuje pregled dizajna: njegove komponente, tijela, skice, operacije i parametre. Koristite ga da se snađete u dizajnu koji niste sami napravili.",
|
||||
"help.document.units": "Mijenja jedinice u kojima dizajn prikazuje dužine, npr. mm ili inče. Geometrija zadržava veličinu; mijenjaju se samo prikaz i brojevi bez jedinice.",
|
||||
"help.edit.checkpoint": "Pamti dizajn, aktivnu skicu i historiju poništavanja onakve kakvi su sada. Koristite ga prije nego što isprobate nešto rizično od čega ćete se možda htjeti vratiti.",
|
||||
"help.edit.restore_checkpoint": "Vraća se na kontrolnu tačku, uključujući historiju poništavanja. Koristite ga da jednim korakom odbacite sve urađeno od kontrolne tačke.",
|
||||
"help.explode.create": "Pravi rastavljeni prikaz koji odguruje dijelove od centra sklopa ili prema pomjeranjima koja zadate. Koristite ga da pokažete kako se sklop sastavlja, za uputstva ili prezentacije.",
|
||||
"help.face.delete": "Uklanja izabrana lica i pušta da susjedna lica zatvore otvor, ili ostavlja otvor otvorenim kao površ. Koristite ga da uklonite zaobljenu ivicu, rupu ili ispupčenje koje smeta.",
|
||||
"help.file.insert_mesh": "Dodaje 3MF, STL, OBJ, PLY ili glTF datoteku u dizajn kao mrežna tijela u novoj komponenti. Koristite ga da pored svog dizajna kao referencu postavite preuzeti ili skenirani model.",
|
||||
"help.file.insert_step": "Dodaje STEP, IGES, FreeCAD ili Rhino datoteku u dizajn kao puna tijela u novoj komponenti; dijelovi sklopa postaju ugniježđene komponente. Koristite ga da uvezete modele od dobavljača ili iz drugih CAD programa.",
|
||||
"help.file.save_mesh": "Čuva tijela kao STL, 3MF ili OBJ mrežu, s glatkoćom zakrivljenih lica koju izaberete. Koristite ga da pripremite dijelove za 3D štampu ili za programe koji čitaju mreže.",
|
||||
"help.file.send_to_slicer": "Čuva izabrana tijela kao 3MF datoteku i otvara je u slajseru koji izaberete. Koristite ga da od modela do štampe dođete jednim korakom.",
|
||||
"help.font_details": "Interfejs koristi ugrađene fontove biblioteke egui (Ubuntu Light i Hack, pod licencama Ubuntu Font Licence i MIT/Bitstream Vera; Noto Emoji i font emoji ikona, pod licencama OFL i MIT). Tekst skice koristi Liberation Sans (SIL OFL 1.1). Neobavezni paket fontova Crafting Apps, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1), nije uključen u izdanja SolidCrafta.",
|
||||
"help.fonts": "Fontovi",
|
||||
"help.frame.member": "Postavlja profil, npr. cijev ili aluminijski ekstruzioni profil, duž linija skice konstrukcije, po jedan konstrukcioni profil na liniju. Koristite ga za izradu okvira, regala i zavarenih konstrukcija.",
|
||||
"help.inspect.accessibility": "Boji lica do kojih alat može doći iz jednog smjera i ona do kojih ne može. Koristite ga da provjerite dio za obradu ili izradu u kalupu s te strane.",
|
||||
"help.inspect.center_of_mass": "Prikazuje centar mase tijela, svakog posebno i svih zajedno, uz ujednačenu gustoću. Koristite ga da provjerite ravnotežu i na koju stranu će se dio prevrnuti.",
|
||||
"help.inspect.colored_components": "Daje svakoj komponenti vlastitu privremenu boju, bez promjene izgleda. Koristite ga da na prvi pogled razlikujete dijelove sklopa.",
|
||||
"help.inspect.compare": "Upoređuje dva tijela i pokazuje gdje jedno ima materijala kojeg drugo nema, uz zapreminu i odstupanje. Koristite ga da provjerite izmijenjen ili uvezen dio u odnosu na original.",
|
||||
"help.inspect.continuity": "Provjerava koliko glatko se lica sastaju duž svojih ivica: otvorene krajeve, pregibe ili skokove zakrivljenosti, i boji spojeve. Koristite ga da pronađete gdje se površi ne spajaju čisto.",
|
||||
"help.inspect.curvature_comb": "Crta češljeve duž kriva, ivica ili lica čiji zupci pokazuju koliko se jako savijaju. Koristite ga da uočite ravna mjesta, pregibe i neujednačenu zakrivljenost.",
|
||||
"help.inspect.curvature_map": "Boji lica prema tome koliko su zakrivljena, crveno gdje je poluprečnik mali. Koristite ga da pronađete mjesta oštre zakrivljenosti i procijenite koliko je površ glatka.",
|
||||
"help.inspect.draft": "Boji lica prema uglu nagiba u odnosu na smjer vađenja iz kalupa i označava ona s premalim nagibom. Koristite ga da provjerite dio prije brizganja ili livenja.",
|
||||
"help.inspect.environment_map": "Prikazuje model sa sjajnom završnom obradom koja reflektuje poput ogledala. Koristite ga da procijenite tok površi prema načinu na koji se refleksije protežu preko njih.",
|
||||
"help.inspect.interference": "Pronalazi tijela koja se preklapaju i prikazuje svako preklapanje s njegovom zapreminom. Koristite ga da u sklopu provjerite ima li dijelova koji se sudaraju.",
|
||||
"help.inspect.isocurve": "Crta izokrive lica (izoparametarske krive), mrežu na kojoj je površ izgrađena. Koristite ga da vidite kako površ teče i gdje se zgušnjava.",
|
||||
"help.inspect.measure": "Mjeri dužine, udaljenosti, uglove, površine i poluprečnike između onoga što izaberete, ili zapreminu i masu tijela. Koristite ga da provjerite veličine i zazore.",
|
||||
"help.inspect.minimum_radius": "Pronalazi najmanji poluprečnik zakrivljenosti kriva, ivica ili lica i gdje se nalazi. Koristite ga da provjerite može li alat ili mlaznica pratiti oblik.",
|
||||
"help.inspect.section": "Otvara prikaz presjekom duž ravni i šrafira presječena lica, bez promjene modela. Koristite ga da pogledate unutrašnjost dijelova i sklopova.",
|
||||
"help.inspect.zebra": "Prikazuje crno-bijele pruge reflektovane na licima modela. Koristite ga da procijenite glatkoću: pruge koje se prekidaju ili lome na ivici pokazuju loš prelaz.",
|
||||
"help.joint.as_built": "Spaja dva dijela tamo gdje već jesu, bez pomjeranja ijednog od njih, i određuje kako se još smiju kretati. Koristite ga za dijelove koje ste već postavili u konačni položaj.",
|
||||
"help.joint.cam": "Povezuje brijeg koji se okreće s pratiocem, pa pratilac klizi ili se ljulja dok obris brijega prolazi ispod njega. Koristite ga za brijegove, ekscentre i pogone ventila.",
|
||||
"help.joint.create": "Spaja dva dijela u ishodištima spoja koja izaberete i pomjera drugi dio na mjesto; vrsta spoja određuje kako se još smiju kretati. Koristite ga da napravite sklop čiji se dijelovi kreću kao pravi mehanizam.",
|
||||
"help.joint.drive": "Zadaje ugao ili udaljenost spoja i s njim pomjera dijelove, u okviru njegovih granica; povezani spojevi i zatvorene petlje ga prate. Koristite ga da provjerite kreće li se mehanizam kako je zamišljeno.",
|
||||
"help.joint.gear": "Povezuje dva rotaciona spoja prijenosnim omjerom, pa pokretanje jednog okreće drugi. Koristite ga za čeone i prstenaste zupčanike, s omjerom iz podionih poluprečnika ili broja zubaca.",
|
||||
"help.joint.limits": "Zadaje koliko daleko se spoj smije okrenuti ili pomjeriti, kao minimum i maksimum, ili uklanja te granice. Koristite ga da šarku, klizač ili polugu zadržite unutar njihovog stvarnog raspona kretanja.",
|
||||
"help.joint.motion_link": "Veže dva spoja omjerom i pomakom, pa pokretanje jednog pomjera drugi. Koristite ga za čvrst odnos između spojeva koji Zupčanik, Zupčasta letva i zupčanik ili Vijak ne pokrivaju.",
|
||||
"help.joint.origin": "Postavlja imenovano ishodište spoja na dio: u centar lica ili okruglog kraja, sredinu ivice, ugao ili tačku. Koristite ga da pripremite gdje se spojevi vezuju; ishodište izabrano na licu prati ga kada dio promijeni veličinu.",
|
||||
"help.joint.rack_pinion": "Povezuje mali zupčanik koji se okreće sa zupčastom letvom koja klizi, pa se letva pomjera za dužinu podione kružnice koju mali zupčanik prevali. Koristite ga za pogone sa zupčastom letvom, upravljanje i linearne stolove.",
|
||||
"help.joint.reach": "Pomjera tačku na dijelu prema cilju mijenjajući spojeve u njegovom lancu, u okviru njihovih granica. Koristite ga da namjestite mehanizam prema tome gdje treba biti njegov kraj, npr. vrh robotske ruke.",
|
||||
"help.joint.rigid_group": "Zaključava izabrane dijelove zajedno onakve kakvi jesu, pa se kreću kao jedan. Koristite ga za dijelove koji zajedno pripadaju i između kojih nema kretanja.",
|
||||
"help.joint.screw": "Povezuje okretanje i klizanje korakom, pa se svakim okretajem napreduje za tu udaljenost. Koristite ga za navojna vretena, vijke i navojne osovine.",
|
||||
"help.library.apply": "Primjenjuje materijal ili izgled iz lokalne datoteke biblioteke na tijela, lica ili komponente. Koristite ga da ponovo upotrijebite materijale koje ste sačuvali iz drugog dizajna.",
|
||||
"help.library.export": "Čuva materijale i izglede dizajna u datoteku biblioteke. Koristite ga da ih dijelite ili ponovo koristite u drugim dizajnima.",
|
||||
"help.library.inspect": "Provjerava datoteku biblioteke i navodi njene materijale i izglede, bez promjene dizajna. Koristite ga da vidite šta biblioteka sadrži prije nego što je primijenite.",
|
||||
"help.licences": "Licence",
|
||||
"help.material.assign": "Određuje od čega su izabrana tijela napravljena, npr. čelik, aluminij ili ABS. Koristite ga da masa i težina odgovaraju stvarnom dijelu.",
|
||||
"help.material.properties.validate": "Provjerava karticu podataka materijala na usklađenost brojeva i vrijednosti koje nedostaju. Koristite ga prije nego što se oslonite na podatke za masu ili krutost.",
|
||||
"help.mesh.convert": "Ponovo gradi mrežno tijelo, npr. STL, kao uredivo puno tijelo s ravnim i cilindričnim licima. Koristite ga da izmijenite uvezene mehaničke dijelove kao modelirano tijelo.",
|
||||
"help.mit": "MIT licenca",
|
||||
"help.motion.study": "Zadaje vrijednosti spojeva u koracima na vremenskoj traci i reprodukuje ih kao animaciju. Koristite ga da posmatrate kako se mehanizam kreće kroz niz pokreta i usput provjerite zazore.",
|
||||
"help.motion.trace": "Prati putanju kojom ide tačka na dijelu dok se reprodukuje studija kretanja ili se pokreće spoj i, kada potvrdite, zadržava je kao krivu 3D skice. Koristite ga da vidite dokle mehanizam dosiže ili da nastavite graditi na njegovoj putanji.",
|
||||
"help.notice": "NOTICE",
|
||||
"help.occurrence.ground": "Učvršćuje dio na mjestu, ili ga ponovo oslobađa, tako da spojevi pomjeraju ostale dijelove oko njega. Koristite ga za okvir ili osnovu na kojoj je izgrađen ostatak sklopa.",
|
||||
"help.occurrence.move": "Pomjera dio sklopa samo duž stepeni slobode koje dozvoljavaju njegovi spojevi. Izaberite lice dijela, a zatim prevucite dostupnu strelicu ili luk.",
|
||||
"help.parameters.change": "Otvara tabelu imenovanih vrijednosti i izraza koji upravljaju kotama dizajna. Koristite je da promijenite mjeru na jednom mjestu, a sve operacije koje je koriste je slijede.",
|
||||
"help.parameters.export": "Čuva parametre dizajna kao CSV ili JSON. Koristite ga da ih uredite u proračunskoj tabeli ili prenesete u drugi dizajn.",
|
||||
"help.parameters.graph": "Prikazuje koji parametri i operacije koriste koje druge, te sve petlje ili greške među njima. Koristite ga da vidite šta sve zahvata promjena nekog parametra.",
|
||||
"help.parameters.import": "Učitava parametre iz CSV ili JSON u dizajn, sve ili nijedan. Koristite ga da unesete vrijednosti iz proračunske tabele ili drugog dizajna.",
|
||||
"help.parameters.move": "Pomjera korisnički parametar ispred ili iza drugog na popisu. Koristite ga da povezane parametre držite zajedno.",
|
||||
"help.parts.fastener": "Dodaje standardni vijak, zavrtanj, maticu, podlošku, zatik ili ležaj u izabranoj veličini. Postavljen na rupu, naliježe u rupu pomoću spoja, pa spojne elemente ne morate sami modelirati.",
|
||||
"help.parts.insert": "Umeće standardni dio u porodici, veličini i dužini koje izaberete. Postavljen na rub ili zid rupe, naliježe na rupu krutim spojem.",
|
||||
"help.parts.library": "Otvara standardne dijelove: vijke, zavrtnjeve, matice, podloške, bezglave vijke, cilindrične zatike i ležajeve u standardnim veličinama. Koristite ga da dodate standardni dio umjesto da ga sami modelirate.",
|
||||
"help.parts.resize": "Mijenja veličinu ili dužinu umetnutog standardnog dijela i ponovo ga gradi; njegovi spojevi ga drže na mjestu. Koristite ga kada se vijak pokaže predugim ili rupa traži drugu veličinu.",
|
||||
"help.plastic.assign_rule": "Dodjeljuje pravilo za plastiku tijelima, pa njihove plastične operacije preuzimaju njegov nagib i zazor kao podrazumijevane vrijednosti. Koristite ga da jednom podesite brizgani dio za sve njegove operacije.",
|
||||
"help.plastic.boss": "Dodaje ispust za vijak na licu: stub s neobaveznom rupom, nagibom, zaobljenjem u korijenu i potpornim rebrima. Koristite ga za tačke za vijke u brizganim ili štampanim kućištima.",
|
||||
"help.plastic.lip": "Dodaje usnu ili odgovarajući utor duž ruba izdubljenog tijela. Koristite ga da se dvije polovine kućišta međusobno pozicioniraju i čvrsto zatvore.",
|
||||
"help.plastic.manage_rules": "Pravi i uređuje pravila za plastiku: materijal, debljinu zida, poluprečnike, zazor i nagib. Koristite ih da operacije brizganog dijela ostanu usklađene.",
|
||||
"help.plastic.rest": "Dodaje oslonac na licu: okruglu ili pravougaonu podlogu, punu ili šuplju, s nagibom. Koristite ga kao potporu ili odstojnik u brizganom kućištu.",
|
||||
"help.plastic.snap_fit": "Dodaje kuku škljocajućeg spoja na licu, s njenom dužinom, debljinom i zapinjačem. Koristite ga da dijelove spojite zaklapanjem bez vijaka.",
|
||||
"help.pointcloud.insert": "Umetnite PLY, XYZ ili PTS skeniranje kao oblak tačaka uz model. Koristite ga kao referencu pri izradi modela iz izmjerenih podataka.",
|
||||
"help.sheet.convert": "Pretvara ploču ili izdubljeno tijelo ujednačene debljine u tijelo od lima, sa savijanjima na ivicama koje izaberete. Koristite ga da postojeći dio učinite razvijivim.",
|
||||
"help.sheet.export_bend_table": "Čuva tabelu savijanja tijela od lima kao CSV ili JSON: uglove, poluprečnike i položaje savijanja u razvijenom obliku. Koristite ga za presu za savijanje ili radionicu.",
|
||||
"help.sheet.export_dxf": "Čuva razvijeni oblik kao DXF, s obrisima, linijama savijanja i neobaveznim napomenama o savijanju. Koristite ga za rezanje laserom, plazmom ili vodenim mlazom.",
|
||||
"help.sheet.flange": "Dodaje prirubnicu: osnovnu ploču iz skice, zid duž otvorene konture skice ili savijeni zid na ivicama lima. Koristite je za izgradnju dijelova od lima.",
|
||||
"help.sheet.flat_pattern": "Pravi razvijeni oblik tijela od lima, s njegovom veličinom, izrezima i linijama savijanja. Koristite ga da provjerite pripremak prije rezanja.",
|
||||
"help.sheet.fold": "Savija lim duž linije skice za zadati ugao i poluprečnik. Koristite ga da presavijete ravan lim nakon što u njemu izrežete operacije.",
|
||||
"help.sheet.hem": "Presavija ivicu lima natrag preko sebe. Koristite ga za sigurne, krute ivice bez oštrih srha.",
|
||||
"help.sheet.manage_rules": "Pravi i uređuje pravila za lim: debljinu, poluprečnik savijanja, K-faktor, rasterećenja i razmake. Koristite ih da se uskladite s materijalom i alatima svoje radionice.",
|
||||
"help.sheet.path_flange": "Dodaje lanac prirubnica duž ivice lima, prateći otvoreni lanac linija skice. Koristite ga za profile poput U ili Z oblika u jednom koraku.",
|
||||
"help.sheet.perforate": "Dodaje polje rupa preko cijelog ravnog lima, u izmaknutom ili kvadratnom rasporedu. Koristite ga za otvore za ventilaciju, rešetke i lakše ploče.",
|
||||
"help.sheet.refold": "Ponovo presavija razvijeno tijelo od lima. Koristite ga nakon dodavanja operacija na ravan lim.",
|
||||
"help.sheet.unfold": "Razvija sva savijanja tijela od lima. Koristite ga da dodate rupe ili rezove koji moraju prelaziti preko savijanja, a zatim ga ponovo presavijte.",
|
||||
"help.sketch.arc.center_point": "Crta kružni luk oko tačke centra, od početne do završne tačke. Koristite ga kada je centar luka poznat, npr. poluprečnik oko rupe ili osovine.",
|
||||
"help.sketch.arc.tangent": "Crta luk koji počinje na kraju linije ili luka i nastavlja se tangentno na njega. Koristite ga da lanac nastavite glatkim prelazom umjesto ugla.",
|
||||
"help.sketch.arc.three_point": "Crta luk od početne do završne tačke kroz treću tačku. Koristite ga kada luk mora proći kroz zadate tačke, a ne oko poznatog centra.",
|
||||
"help.sketch.arc3d": "Crta tačan kružni luk u prostoru kroz tri tačke, uz uključenu 3D skicu. Koristite ga za zavoje putanja i vodilica koji izlaze iz ravni skice.",
|
||||
"help.sketch.auto_constrain": "Dodaje ograničenja koja skica već gotovo ima, npr. horizontalno, paralelno ili podudarno, a zatim je kotira dok ne bude potpuno definisana. Koristite ga da grubo nacrtanu skicu zaključate u jednom koraku.",
|
||||
"help.sketch.auto_constrain_from_datum": "Radi kao Automatska ograničenja, ali položaje mjeri od referentne tačke koju izaberete umjesto od ishodišta. Koristite ga kada je dio raspoređen od jedne referentne tačke, npr. ugla ili rupe.",
|
||||
"help.sketch.blend_curve": "Spaja krajeve dviju kriva glatkim splajnom koji je tangentan na obje, ili i kontinuiran po zakrivljenosti ako tako izaberete. Koristite ga da premostite razmak između kriva bez pregiba.",
|
||||
"help.sketch.break": "Dijeli krivu na dijelove tamo gdje je presijecaju druge krive, ili na tačkama koje postavite na nju. Koristite ga kada dio krive treba vlastito ograničenje ili ga treba zasebno izbrisati.",
|
||||
"help.sketch.cancel": "Napušta skicu i odbacuje sve što je promijenjeno otkako je napravljena ili otvorena za uređivanje. Pomoću Poništi možete vratiti promjene.",
|
||||
"help.sketch.centerline": "Prebacuje linije između običnih linija i središnjih linija, osa koje profili izostavljaju. Središnju liniju koristite kao osu za rotaciju, preslikavanje ili ograničenje simetrije.",
|
||||
"help.sketch.chamfer.distance_angle": "Skošava ugao između dvije linije, zadat udaljenošću duž prve linije i uglom. Koristite ga kada crtež daje skošenje kao dužinu i ugao, npr. 2 mm × 30°.",
|
||||
"help.sketch.chamfer.equal_distance": "Skošava ugao između dvije linije za istu udaljenost unazad duž obje. Koristite ga za uobičajeno skošenje od 45° na obrisu.",
|
||||
"help.sketch.chamfer.two_distance": "Skošava ugao između dvije linije, zadat posebnom udaljenošću duž svake linije. Koristite ga za skošenja koja nisu 45°, zadana kao dvije dužine.",
|
||||
"help.sketch.circle.center": "Crta krug iz njegovog centra i prečnika. Koristite ga za rupe, ispuste i druge okrugle oblike čiji je centar poznat.",
|
||||
"help.sketch.circle.three_point": "Crta krug kroz tri tačke. Koristite ga kada krug mora proći kroz zadate tačke, npr. postojeće geometrije.",
|
||||
"help.sketch.circle.three_tangent": "Crta krug koji dodiruje tri elementa: linije, krugove ili lukove. Koristite ga da krug uklopite u prostor između triju kriva, npr. upisani krug trougla.",
|
||||
"help.sketch.circle.two_point": "Crta krug iz dva kraja njegovog prečnika. Koristite ga kada su poznate tačke na rubu kruga, a ne njegov centar.",
|
||||
"help.sketch.circle.two_tangent": "Crta krug zadate veličine koji dodiruje dva elementa: linije, krugove ili lukove. Koristite ga za zaobljenje stalnog poluprečnika između dvije krive.",
|
||||
"help.sketch.conic": "Crta konusnu krivu između dvije krajnje tačke, oblikovanu tjemenom tačkom i vrijednošću rho. Koristite je za glatke prelaze čiju punoću želite podesiti, od ravnog do gotovo oštrog ugla.",
|
||||
"help.sketch.constrainer": "Primjenjuje ograničenje koje jedna ili dvije izabrane krive već gotovo imaju, npr. horizontalno, paralelno, tangentno ili jednako. Koristite ga da uredite ručno nacrtanu skicu, jedan odnos po jedan.",
|
||||
"help.sketch.constraint.coincident": "Čini da tačka leži na drugoj tački, krivoj ili osi skice. Koristite ga da zatvorite otvore u profilu ili da tačku postavite na liniju.",
|
||||
"help.sketch.constraint.collinear": "Čini da dvije linije leže na jednoj pravoj. Koristite ga da poravnate prekinute ivice, npr. obje strane zareza.",
|
||||
"help.sketch.constraint.concentric": "Daje dva kruga ili luka isti centar. Koristite ga za prstenove, cilindrična upuštenja i osovine s ramenima.",
|
||||
"help.sketch.constraint.curvature": "Čini da dvije krive koje se sastaju u krajnjoj tački nastave istom zakrivljenošću (G2). Koristite ga tamo gdje tangentni spoj nije dovoljno gladak, npr. na vidljivim površima.",
|
||||
"help.sketch.constraint.equal": "Izjednačava dužine linija, ili poluprečnike krugova i lukova. Koristite ga da jedna kota upravlja s više istovjetnih oblika.",
|
||||
"help.sketch.constraint.fix": "Fiksira tačke ili krive tamo gdje jesu, ili ih ponovo oslobađa. Koristite ga za geometriju koja se ne smije pomjerati kada povlačite ili rješavate skicu.",
|
||||
"help.sketch.constraint.horizontal_vertical": "Čini liniju, ili liniju između dvije tačke, horizontalnom ili vertikalnom, zavisno od toga što je bliže. Koristite ga da obris poravnate s osama skice.",
|
||||
"help.sketch.constraint.midpoint": "Postavlja tačku na sredinu linije, ili spaja središnje tačke dviju linija. Koristite ga da geometriju centrirate na ivici.",
|
||||
"help.sketch.constraint.parallel": "Čini dvije linije paralelnima. Koristite ga za stranice koje moraju ostati paralelne kada se promijene druge kote.",
|
||||
"help.sketch.constraint.perpendicular": "Čini da se dvije linije sastaju pod pravim uglom. Koristite ga za prave uglove između linija koje nisu na osama skice.",
|
||||
"help.sketch.constraint.polygon": "Čini zatvoreni lanac linija pravilnim poligonom: jednake stranice, a svaki ugao na jednom krugu. Koristite ga da ručno nacrtan poligon učinite pravilnim i takvim ga zadržite.",
|
||||
"help.sketch.constraint.symmetry": "Čini da dvije tačke ili krive budu međusobno preslikane preko linije ili oko tačke. Koristite ga da profil ostane simetričan kada promijenite jednu stranu.",
|
||||
"help.sketch.constraint.tangent": "Čini da se dvije krive dodiruju tangentno, npr. linija i luk. Koristite ga za glatke spojeve bez pregiba.",
|
||||
"help.sketch.construction": "Prebacuje krive između obične geometrije i pomoćne geometrije, koju profili izostavljaju. Pomoćne krive koristite kao pomagala za raspored, reference i kote.",
|
||||
"help.sketch.construction_line": "Napravite beskonačnu pomoćnu pravu kroz dvije tačke. Koristite je da rasporedite skicu bez razdvajanja profila.",
|
||||
"help.sketch.construction_line.angle": "Napravite beskonačnu pomoćnu pravu pod uglom od ose X skice. Izaberite tačku i upišite ugao ili izaberite smjer.",
|
||||
"help.sketch.construction_line.bisector": "Izaberite dvije prave linije skice koje se sijeku, pa jednu od dvije simetrale ugla. Rezultat prati izvorne linije.",
|
||||
"help.sketch.construction_line.horizontal": "Napravite beskonačnu horizontalnu pomoćnu pravu kroz tačku. Njeno ograničenje horizontalnosti ostaje na snazi pri kasnijim izmjenama.",
|
||||
"help.sketch.construction_line.parallel": "Izaberite pravu liniju skice, pa postavite paralelnu beskonačnu pomoćnu pravu. Desnim klikom izaberite drugu referencu.",
|
||||
"help.sketch.construction_line.perpendicular": "Izaberite pravu liniju skice, pa postavite okomitu beskonačnu pomoćnu pravu. Desnim klikom izaberite drugu referencu.",
|
||||
"help.sketch.construction_line.tangent_at": "Napravite tangentu u tački na krugu ili kružnom luku, ili u krajnjoj tački otvorenog splajna ili konike. Izaberite krivu, pa tačku dodira.",
|
||||
"help.sketch.construction_line.tangent_external": "Izaberite krug ili kružni luk, pa vanjsku tačku i rješenje tangente. Pregledi prikazuju samo dodirne tačke na nacrtanom luku.",
|
||||
"help.sketch.construction_line.tangent_two": "Izaberite dva kruga ili kružna luka, pa u pregledu izaberite zajedničku tangentu. Dodirne tačke moraju ležati na nacrtanim krivama.",
|
||||
"help.sketch.construction_line.vertical": "Napravite beskonačnu vertikalnu pomoćnu pravu kroz tačku. Njeno ograničenje vertikalnosti ostaje na snazi pri kasnijim izmjenama.",
|
||||
"help.sketch.copy": "Kopira izabrane krive i tačke skice, zajedno s ograničenjima i kotama među njima. Zalijepite ih u ovu skicu ili u drugu.",
|
||||
"help.sketch.create": "Počinje novu 2D skicu na ravni ili na ravnom licu modela. Svako ekstrudiranje, rotiranje i povlačenje počinje skicom.",
|
||||
"help.sketch.cut": "Kopira izabranu geometriju skice i briše je iz skice, u jednom koraku poništavanja. Zalijepite je drugdje u ovoj skici ili u drugoj.",
|
||||
"help.sketch.dimension": "Dodaje pogonsku kotu: dužinu, udaljenost, ugao, poluprečnik ili prečnik, zavisno od onoga što izaberete. Upišite vrijednost ili naziv parametra, a skica ih prati.",
|
||||
"help.sketch.edit": "Otvara postojeću skicu za uređivanje. Operacije izgrađene na njoj se ažuriraju kada završite skicu.",
|
||||
"help.sketch.ellipse": "Crta elipsu iz njenog centra, kraja velike ose i tačke na maloj strani. Koristite je za ovalne otvore, brijegove i druge izdužene okrugle oblike.",
|
||||
"help.sketch.equation_curve": "Crta krivu iz formula: x(t) i y(t) ili y = f(x), u zadanom rasponu. Formule mogu koristiti parametre i kriva ih prati: koristite je za sinusoidu, spiralu ili evolventu zupca zupčanika.",
|
||||
"help.sketch.extend": "Produžava liniju ili luk do najbliže krive na putu, ili izdužuje dvije krive dok se ne sastanu. Koristite ga da zatvorite otvore u profilu nakon odsijecanja ili uređivanja.",
|
||||
"help.sketch.fillet": "Zaobljuje ugao gdje se dvije linije ili lukovi sastaju lukom zadatog poluprečnika. Više uglova zaobljenih zajedno dijeli jedan poluprečnik.",
|
||||
"help.sketch.finish": "Napušta skicu i vraća se na model. Njeni zatvoreni profili tada su spremni za Ekstrudiraj, Rotiraj i druge operacije.",
|
||||
"help.sketch.finish_auto_constrain": "Pokreće Automatska ograničenja na skici, a zatim je završava. Koristite ga da skicu ostavite potpuno definisanu u jednom koraku.",
|
||||
"help.sketch.fit_curves_to_section": "Siječe mrežno ili puno tijelo ravni skice i uklapa linije i lukove u presjek. Koristite ga da skenirani ili uvezeni dio ponovo izgradite kao uredivu skicu.",
|
||||
"help.sketch.include_3d": "Unosi ivice modela, vrhove i krive drugih skica u ovu skicu kao povezane reference, spljoštavajući one izvan njene ravni na nju. Koristite ga da gradite na geometriji koja ne leži u ravni skice.",
|
||||
"help.sketch.infinite": "Prebacuje linije između običnih linija i beskonačnih pomoćnih pravih koje se protežu preko cijelog prikaza. Koristite ih kao reference za raspored koje nikada ne završe prekratko.",
|
||||
"help.sketch.insert_dxf": "Učitava krive DXF crteža u skicu, skalirane, zakrenute i postavljene gdje zadate. Koristite ga da gradite na 2D crtežu iz drugog programa, npr. na obrisu za rezanje laserom.",
|
||||
"help.sketch.insert_svg": "Učitava putanje SVG crteža u skicu, skalirane, zakrenute i postavljene gdje zadate. Koristite ga da logotip ili vektorski obris pretvorite u krive koje možete ekstrudirati.",
|
||||
"help.sketch.inspect": "Otvara izvještaj o otvorenoj skici: koliko je stepeni slobode preostalo i je li potpuno ograničena. Koristite ga da saznate šta se još pomjera prije nego što završite skicu.",
|
||||
"help.sketch.intersect": "Dodaje krive ili tačke gdje tijela, lica, ivice ili druge skice sijeku ravan skice. Ostaju povezane i prate model kada se promijeni.",
|
||||
"help.sketch.intersection_curve": "Pravi 3D krive tamo gdje se dva tijela ili lica sastaju, povezane s njima. Koristite ih kao putanje ili vodilice koje prate model.",
|
||||
"help.sketch.isoparametric_curve": "Pravi 3D krivu u smjeru U ili V lica kroz tačku koju izaberete, povezanu s licem. Koristite je kao putanju ili liniju razdvajanja koja prati površ.",
|
||||
"help.sketch.line": "Crta povezane linije tačku po tačku, kao otvoreni lanac ili zatvoren obris. To je osnovni alat za većinu profila.",
|
||||
"help.sketch.line.midpoint": "Crta liniju od njene središnje tačke do jednog kraja, a drugi kraj se preslikava. Koristite je za linije centrirane na tački, npr. simetričnu ivicu.",
|
||||
"help.sketch.line3d": "Crta povezane prave linije između tačaka u prostoru, uz uključenu 3D skicu. Koristite ga za putanje i okvire koji izlaze iz ravni skice.",
|
||||
"help.sketch.mirror": "Preslikava krive i tačke skice preko linije ili ose i dodaje ograničenja simetrije. Koristite ga da nacrtate pola simetričnog profila i dovršite ga.",
|
||||
"help.sketch.move": "Pomjera ili rotira krive i tačke skice, ili njihovu kopiju. Koristite ga da promijenite položaj geometrije uz zadržavanje njenog oblika i ograničenja.",
|
||||
"help.sketch.offset": "Pravi kopiju lanca kriva na zadatoj udaljenosti, s jedne strane ili s obje. Koristite ga za zidove, obrise i zazore; udaljenost ostaje kota koju možete uređivati.",
|
||||
"help.sketch.paste": "Lijepi geometriju skice koju ste kopirali ili izrezali u aktivnu skicu. Postavite je na tačku ili je pomjerite za odmak.",
|
||||
"help.sketch.pattern.circular": "Kopira geometriju skice oko tačke centra, preko punog kruga ili zadatog ugla. Koristite ga za kružne rasporede vijaka, paoke i zupce.",
|
||||
"help.sketch.pattern.rectangular": "Kopira geometriju skice u redovima i kolonama na zadatim razmacima. Koristite ga za mreže rupa, žlijebova ili otvora za ventilaciju.",
|
||||
"help.sketch.point": "Postavlja tačku skice. Koristite je kao referencu za kote, ograničenja, centre rupa ili kasnije operacije.",
|
||||
"help.sketch.point3d": "Postavlja tačku u prostoru, uz uključenu 3D skicu. Koristite je kao referencu za 3D krive i putanje.",
|
||||
"help.sketch.polygon.circumscribed": "Crta pravilan poligon oko kruga, zadat od centra do sredine stranice. Koristite ga kada je poznata širina ključa, npr. za šestougaonu maticu.",
|
||||
"help.sketch.polygon.edge": "Crta pravilan poligon iz dva kraja jedne stranice. Koristite ga kada su poznate dužina i položaj stranice.",
|
||||
"help.sketch.polygon.inscribed": "Crta pravilan poligon unutar kruga, zadat od centra do ugla. Koristite ga kada je poznata mjera preko uglova.",
|
||||
"help.sketch.project": "Unosi ivice modela, lica, vrhove i krive drugih skica u ovu skicu, projektovane na njenu ravan. Ostaju povezani i prate model kada se promijeni.",
|
||||
"help.sketch.project_to_surface": "Projektuje krive drugih skica duž normale ove skice na lice, kao povezane 3D krive. Koristite ga da logotip, liniju razdvajanja ili putanju postavite na zakrivljenu površ.",
|
||||
"help.sketch.rectangle.center": "Crta pravougaonik iz njegovog centra i jednog ugla. Koristite ga za obrise centrirane na tački, npr. na ishodištu.",
|
||||
"help.sketch.rectangle.rounded": "Crta pravougaonik sa sva četiri ugla zaobljena istim poluprečnikom. Koristite ga za ploče, poklopce i otvore sa zaobljenim uglovima.",
|
||||
"help.sketch.rectangle.three_point": "Crta pravougaonik iz jedne ivice i tačke na suprotnoj strani. Koristite ga za pravougaonike pod uglom u odnosu na ose skice.",
|
||||
"help.sketch.rectangle.two_point": "Crta pravougaonik iz dva suprotna ugla. Koristite ga za obrise ploča, blokova i džepova.",
|
||||
"help.sketch.scale": "Skalira krive i tačke skice faktorom oko bazne tačke. Koristite ga da promijenite veličinu dijela skice bez ponovnog crtanja.",
|
||||
"help.sketch.slot.arc_center": "Crta lučni žlijeb zadate širine duž luka određenog centrom, početkom i krajem. Koristite ga za žlijebove za podešavanje oko osovine.",
|
||||
"help.sketch.slot.arc_three_point": "Crta lučni žlijeb zadate širine duž luka kroz tri tačke. Koristite ga kada putanja žlijeba mora proći kroz zadate tačke.",
|
||||
"help.sketch.slot.center_point": "Crta ravni žlijeb zadate širine od njegovog centra do centra jednog krajnjeg luka. Koristite ga za žlijebove centrirane na tački.",
|
||||
"help.sketch.slot.center_to_center": "Crta ravni žlijeb zadate širine između centara njegova dva krajnja luka. Koristite ga za žlijebove kotirane od centra do centra, npr. žlijebove za podešavanje vijaka.",
|
||||
"help.sketch.slot.overall": "Crta ravni žlijeb zadate širine od jednog vanjskog kraja do drugog. Koristite ga kada je bitna ukupna dužina žlijeba.",
|
||||
"help.sketch.spline.control_point": "Crta kubni splajn oblikovan kontrolnim tačkama kroz koje ne prolazi. Koristite ga za glatke, tečne krive koje oblikujete pomjeranjem tačaka.",
|
||||
"help.sketch.spline.control_point_5": "Crta splajn petog stepena oblikovan kontrolnim tačkama. Koristite ga tamo gdje se krive moraju spajati glađom zakrivljenošću, npr. na vidljivim dizajnerskim površima.",
|
||||
"help.sketch.spline.fit_point": "Crta glatki splajn koji prolazi kroz svaku tačku koju postavite, otvoren ili zatvoren. Koristite ga da oblik provučete kroz poznate tačke.",
|
||||
"help.sketch.spline3d": "Crta glatku krivu u prostoru kroz tačke, uz uključenu 3D skicu. Koristite ga za putanje povlačenja i vođenje kablova koji izlaze iz ravni skice.",
|
||||
"help.sketch.spun_profile": "Pravi u ravni skice obris koji tijelo opisuje kada se okreće oko ose. Koristite ga da dobijete siluetu tokarenog dijela za rotiranje ili crtež.",
|
||||
"help.sketch.text": "Upisuje tekst u skicu izabranim fontom, visinom i uglom; njegove riječi mogu pratiti parametre. Ekstrudirajte ga ili ga napravite reljefom za etikete i natpise.",
|
||||
"help.sketch.transform": "Skalira, okreće i pomjera geometriju skice oko tačke centra u jednom koraku. Koristite ga da istovremeno promijenite položaj i veličinu izbora.",
|
||||
"help.sketch.trim": "Uklanja dio krive između najbližih presjeka oko mjesta na koje kliknete. Koristite ga da preklapajuće linije očistite u zatvoren profil.",
|
||||
"help.solid.align": "Pomjera izabrana tijela tako da se tačka, lice ili ravan na njima poklopi s tačkom, licem ili ravni negdje drugdje. Koristite ga da postavite dio uz drugi bez mjerenja odmaka.",
|
||||
"help.solid.boundary_fill": "Pravi puna tijela od područja zatvorenih preklapajućim alatnim tijelima, popunjavajući samo područja koja izaberete. Koristite ga da oblik izgradite iz prostora koji nekoliko tijela zajedno ograničava.",
|
||||
"help.solid.bounding_solid": "Pravi kutiju koja obuhvata izabrana tijela, s neobaveznom marginom. Koristite ga da vidite ukupnu veličinu dijela ili kao pripremak iz kojeg se dio obrađuje.",
|
||||
"help.solid.box": "Pravi kutiju iz dužine, širine i visine, iz tačke ugla ili centriranu na tački. Koristite je za brz blok, privremeni oblik ili tijelo u koje se urezuju operacije.",
|
||||
"help.solid.chamfer": "Odsijeca izabrane ivice ravnim skošenjem zadate udaljenosti. Koristite ga da ublažite oštre ivice ili olakšate uvođenje zatika ili vijka.",
|
||||
"help.solid.coil": "Pravi zavojno tijelo, npr. oprugu, iz prečnika, broja zavoja, visine ili koraka i okruglog ili kvadratnog presjeka. Koristite je za opruge, zavojnice i zavojne grebene.",
|
||||
"help.solid.combine": "Spaja tijela, oduzima ih od ciljnog tijela ili zadržava samo zajedničku zapreminu, a može zadržati i alatna tijela. Koristite ga da spojite dijelove, da jednim oblikom odsiječete drugi ili da pronađete gdje se dva tijela preklapaju.",
|
||||
"help.solid.cone": "Pravi konus iz kruga osnove i visine, ili zarubljeni konus kada i vrh ima poluprečnik. Koristite ga za suženja, lijevke, konusna upuštenja i šiljke.",
|
||||
"help.solid.cylinder": "Pravi cilindar iz poluprečnika ili prečnika i visine, postavljen na tačku duž ose. Koristite ga za osovine, zatike, ispuste i okrugle pripremke.",
|
||||
"help.solid.draft": "Naginje izabrana lica za ugao koji zadate, mjereno od neutralne ravni. Koristite ga na livenim ili brizganim dijelovima kako bi izašli iz kalupa.",
|
||||
"help.solid.emboss": "Izdiže profile skice, npr. tekst ili logotip, iz lica, ili ih udubljuje u njega. Koristite ga za natpise, etikete i oznake na ravnim ili okruglim licima.",
|
||||
"help.solid.extrude": "Izvlači profil skice u 3D duž zadate udaljenosti, dodajući ili uklanjajući materijal. Koristite ga da ravni obris pretvorite u blok, ispust ili džep.",
|
||||
"help.solid.fillet": "Zaobljuje izabrane ivice glatkim kružnim prelazom zadatog poluprečnika. Koristite ga da omekšate oštre ivice radi čvrstoće, udobnosti ili izgleda.",
|
||||
"help.solid.hole": "Buši okruglu rupu zadatog prečnika na izabranoj tački lica. Koristite je za prolaze vijaka, zatike i druge jednostavne okrugle otvore.",
|
||||
@@ -1412,20 +1653,60 @@
|
||||
"help.solid.move": "Pomjera ili rotira izabrana tijela, ili njihovu kopiju, za udaljenost i ugao koje zadate. Koristite ga da premjestite tijelo ili napravite drugo na drugom mjestu.",
|
||||
"help.solid.offset_face": "Gura izabrana lica unutra ili van za udaljenost koju zadate, a okolna lica ih prate. Koristite ga da podebljate zid, produbite džep ili povisite ispupčenje bez uređivanja skice.",
|
||||
"help.solid.pattern.circular": "Ponavlja izabrane objekte oko ose, za zadati broj i ukupni ugao. Koristite ga za krugove vijaka, žbice, rebra hladnjaka i zupce.",
|
||||
"help.solid.pattern.path": "Kopira operacije ili tijela duž putanje skice, prema broju i razmaku. Koristite ga za rupe duž zakrivljene ivice ili dijelove duž vodilice.",
|
||||
"help.solid.pattern.rectangular": "Ponavlja izabrane objekte u jednom ili dva smjera sa zadatim brojem i razmakom. Koristite ga za redove i mreže rupa, rebara, zatika ili nožica.",
|
||||
"help.solid.pipe": "Pravi okruglu cijev duž putanje od kriva skice, punu ili šuplju sa zadatom debljinom zida. Koristite je za cijevi, žice, kablove i šine.",
|
||||
"help.solid.press_pull": "Dodaje ili uklanja materijal dok povlačite lice, ili zaobljava ivice dok ih povlačite. Koristite ga za brze izmjene direktno na modelu.",
|
||||
"help.solid.remove": "Uklanja izabrana tijela iz dizajna od ove tačke vremenske trake nadalje; raniji koraci ih zadržavaju. Koristite ga da odbacite pomoćno tijelo, npr. alat za rezanje koji vam više ne treba.",
|
||||
"help.solid.replace_face": "Premješta izabrana ravna lica na ravan ili na paralelno lice, a tijelo raste ili se smanjuje da ga dosegne. Koristite ga da poravnate lice s drugim dijelom ili pomoćnom ravni.",
|
||||
"help.solid.revolve": "Okreće profil skice oko ose i pravi okruglo tijelo. Koristite ga za vratila, dugmad, posude i svaki dio koji je okrugao oko središnje linije.",
|
||||
"help.solid.rib": "Dodaje tanak zid duž otvorene krive skice, popunjen do sljedećih lica tijela. Koristite ga da ojačate zidove i uglove, npr. u brizganim ili štampanim dijelovima.",
|
||||
"help.solid.scale": "Povećava ili smanjuje izabrana tijela za faktor, jednako u svim smjerovima ili posebno po osi. Koristite ga da promijenite veličinu uvezenog oblika ili uračunate skupljanje.",
|
||||
"help.solid.shell": "Izdubljuje tijelo i ostavlja zidove zadate debljine, a može otvoriti lica koja izaberete. Koristite ga za kutije, poklopce i druge tankozidne dijelove.",
|
||||
"help.solid.sphere": "Pravi sferu iz poluprečnika ili prečnika oko tačke centra. Koristite je za kugle, ručke i zaobljene krajeve.",
|
||||
"help.solid.split_body": "Reže tijelo na zasebna tijela ravni, licem drugog tijela ili linijom skice. Koristite ga za dijelove koji se zasebno štampaju, obrađuju ili boje.",
|
||||
"help.solid.split_face": "Dijeli izabrana lica po ravni, licu drugog tijela ili krivama skice, bez rezanja tijela. Koristite ga da dijelu lica date vlastiti nagib, boju ili odmak.",
|
||||
"help.solid.sweep": "Povlači profil skice duž putanje i pravi tijelo. Koristite ga za cijevi, šine, ručke i svaki dio konstantnog presjeka koji prati krivu.",
|
||||
"help.solid.thread": "Dodaje ISO metrički navoj na cilindrično lice, kao kozmetički navoj ili urezan u tijelo. Koristite ga za vijke, navojne rupe i štampane dijelove s navojem.",
|
||||
"help.solid.torus": "Pravi prsten iz velikog poluprečnika oko njegove ose i malog poluprečnika za okrugli presjek. Koristite ga za O-prstenove, ručke i okrugle perle.",
|
||||
"help.solid.web": "Dodaje tanak zid duž svake otvorene krive skice, svaki popunjen do sljedećih lica tijela. Koristite ga za mrežu potpornih zidova unutar kućišta i posuda.",
|
||||
"help.surface.blend": "Pravi površ koja premošćuje ivice dvaju tijela, tangentnu ili kontinuiranu po zakrivljenosti s oba. Koristite je da dvije površi glatko spojite preko razmaka.",
|
||||
"help.surface.boundary": "Pravi površ koja ispunjava petlju od dvije do četiri krive skice, ili mrežu kriva. Koristite je za slobodne panele određene svojim ivicama.",
|
||||
"help.surface.extend": "Produžava površ preko njenih ivica, za neku udaljenost ili do ravni ili lica. Koristite ga da se površi preklope prije nego što ih odsiječete ili sašijete.",
|
||||
"help.surface.extrude": "Izvlači krive ili profile skice na zadatu udaljenost u površ, samo zidove, bez poklopaca. Koristite ga za tanke zidove i za alate za rezanje ili razdvajanje.",
|
||||
"help.surface.fair": "Izglađuje unutrašnjost slobodnih lica dok njihove ivice ostaju na mjestu. Koristite ga da izravnate izbočine i valovite refleksije u površi.",
|
||||
"help.surface.loft": "Pravi površ koja redom prolazi kroz dva ili više presjeka, bez poklopaca. Koristite je za ljuske i prelaze između različitih oblika.",
|
||||
"help.surface.match": "Ponovo gradi ivicu površi tako da se sastaje s ivicom drugog tijela, uz podudarnu tangentu ili zakrivljenost. Koristite ga da glatko zatvorite šav između dvije površi.",
|
||||
"help.surface.offset": "Pravi kopiju lica na udaljenosti duž njihovih normala, kao novo tijelo površi. Koristite ga za omotače, zazore ili drugu stranu zida.",
|
||||
"help.surface.patch": "Ispunjava područje skice ili zatvorenu petlju ivica licem, ravnim ili glatko zakrivljenim. Koristite ga da zatvorite rupe u tijelu površi.",
|
||||
"help.surface.reverse_normal": "Okreće lica tijela površi, pa njihova vanjska strana gleda na drugu stranu. Koristite ga prije podebljanja ili šivenja kada lica gledaju prema unutra.",
|
||||
"help.surface.revolve": "Okreće krive ili profile skice oko ose u površ, bez poklopaca. Koristite ga za okrugle ljuske poput abažura, zdjela i mlaznica.",
|
||||
"help.surface.ruled": "Pravi traku pravih linija iz lanca ivica, duž normale lica, tangente ili pod uglom. Koristite ga za zidove s nagibom, odvodne površi i površi razdvajanja.",
|
||||
"help.surface.stitch": "Šije tijela površi zajedno duž njihovih zajedničkih ivica, u puno tijelo kada se zatvore. Koristite ga da skup površi pretvorite u jedno tijelo.",
|
||||
"help.surface.sweep": "Pomjera krive ili profile skice duž putanje u površ, bez poklopaca. Koristite ga za žljebove, lajsne i druge tanke oblike koji prate putanju.",
|
||||
"help.surface.thicken": "Daje tijelu površi debljinu i pretvara ga u puno tijelo. Koristite ga da od površi napravite dijelove pogodne za štampu ili obradu.",
|
||||
"help.surface.trim": "Siječe površ ravni, drugom površi ili krivama skice i zadržava stranu koju izaberete. Koristite ga da površi oblikujete prije šivenja.",
|
||||
"help.surface.unstitch": "Razdvaja lica tijela u zasebna tijela površi; puno tijelo se raspada na površi. Koristite ga da preradite pojedinačna lica tijela.",
|
||||
"help.surface.untrim": "Uklanja rupe iz lica, ili vraća njegovu vanjsku granicu na prirodne granice površi. Koristite ga da nakon odsijecanja vratite cijelu površ.",
|
||||
"help.third_party": "Licence trećih strana",
|
||||
"help.timeline.compute_all": "Ponovo gradi svaku operaciju na vremenskoj traci od početka. Koristite ga da osvježite model i vidite koje operacije ne uspijevaju.",
|
||||
"help.timeline.delete": "Briše izabrane operacije s vremenske trake; brisanje skice briše i operacije napravljene iz nje. Koristite ga da uklonite korak koji više ne želite.",
|
||||
"help.timeline.dependents": "Navodi operacije koje bi bile izbrisane zajedno s nekom operacijom i one koje bi bez nje otkazale. Koristite ga prije brisanja ili mijenjanja operacije.",
|
||||
"help.timeline.play": "Reprodukuje preostale operacije jednu po jednu, s pauzom od pola sekunde nakon svake ponovne izgradnje. Pokrenite ovu komandu ponovo ili pritisnite Escape da zaustavite; Idi na kraj skače odmah.",
|
||||
"help.view.apply": "Okreće prikaz u standardni smjer, npr. sprijeda, odozgo ili izometrijski, ili u sačuvani imenovani prikaz. Koristite ga da model posmatrate iz utvrđenog ugla.",
|
||||
"help.view.delete": "Briše sačuvani imenovani prikaz. Model se ne mijenja.",
|
||||
"help.view.fit": "Zumira i pomjera prikaz tako da cijeli model, ili izbor, ispuni prikaz. Koristite ga kada model nestane iz vidokruga.",
|
||||
"help.view.grid": "Prikazuje ili sakriva mrežu u prozoru prikaza. Koristite je kao vodič za veličinu i orijentaciju tokom modeliranja.",
|
||||
"help.view.ground_shadow": "Prikazuje ili sakriva sjenu modela na podlozi. Koristite je da lakše procijenite visinu i položaj.",
|
||||
"help.view.home": "Vraća prikaz na početni smjer i uklapa model. Koristite ga da se brzo vratite na poznat prikaz.",
|
||||
"help.view.list": "Navodi imenovane prikaze sačuvane u dizajnu. Koristite ga da pronađete prikaz koji ste ranije sačuvali.",
|
||||
"help.view.navigate": "Okreće, pomjera ili zumira prikaz u koracima, npr. četvrtinu okreta ulijevo. Koristite ga da kameru pomjerate precizno umjesto povlačenjem.",
|
||||
"help.view.navigation_bar": "Prikazuje ili sakriva navigacijsku traku na dnu prozora prikaza. Sakrijte je za više prostora, prikažite je da alatima prikaza pristupate mišem.",
|
||||
"help.view.rename": "Daje sačuvanom imenovanom prikazu novi naziv. Sam prikaz ostaje isti.",
|
||||
"help.view.save": "Čuva trenutni položaj kamere kao imenovani prikaz u dizajnu. Koristite ga da se kasnije vratite na isti ugao, npr. za snimke ekrana.",
|
||||
"help.view.spin": "Neprekidno orbitira prikaz oko modela do sljedećeg klika ili tipke. Koristite ga da model predstavite sa svih strana.",
|
||||
"help.view.tool": "Zaključava alat kamere, orbitiranje, pomjeranje ili zumiranje, tako da povlačenje lijevim dugmetom miša pomjera prikaz dok ga ne isključite. Koristite ga na touchpadu ili bez srednjeg dugmeta miša.",
|
||||
"help.view.visual_style": "Mijenja način crtanja modela: osjenčeno sa ili bez ivica, žičani model ili sa skrivenim ivicama. Koristite žičani model da vidite unutra, a osjenčeno za čist izgled.",
|
||||
"history.sketch_under_constrained": "Nedovoljno ograničeno (stepeni slobode: {count})",
|
||||
"history.sketch_under_constrained_one": "Nedovoljno ograničeno ({count} stepen slobode)",
|
||||
"hole_presets.confirm_delete": "Potvrdi brisanje",
|
||||
@@ -1602,6 +1883,10 @@
|
||||
"material.selection.mixed": "Različiti materijali",
|
||||
"measure.at": "Na",
|
||||
"measure.item": "Stavka {number}",
|
||||
"measure.quick_copied": "Mjerenje kopirano",
|
||||
"measure.quick_copy": "Kliknite da kopirate mjerenje",
|
||||
"measure.quick_edges": "Ivice: {count}",
|
||||
"measure.quick_faces": "Lica: {count}",
|
||||
"messages.ago_h": "prije {n} h",
|
||||
"messages.ago_min": "prije {n} min",
|
||||
"messages.ago_s": "prije {n} s",
|
||||
@@ -1635,6 +1920,7 @@
|
||||
"nav.hfss.help": "Alt+lijevo povlačenje pomjera prikaz, Shift+Alt+lijevo zumira, Ctrl+lijevo povlačenje na praznom prostoru orbitira",
|
||||
"nav.inventor.help": "Povlačenje srednjim dugmetom pomjera, Shift+srednje orbitira, Ctrl+srednje zumira, povlačenje desnim orbitira",
|
||||
"nav.maya.help": "Alt+lijevo povlačenje orbitira, Alt+srednje pomjera, Alt+desno zumira; srednje povlačenje također pomjera",
|
||||
"nav.nx.help": "Povlačenje srednjim dugmetom orbitira, Shift+srednje pomjera (u NX-u srednje + desno), Ctrl+srednje zumira (u NX-u srednje + lijevo); povlačenje desnim dugmetom ne dira kameru",
|
||||
"nav.openscad.help": "Povlačenje lijevim dugmetom po praznom prostoru orbitira, Shift+povlačenje lijevim bira okvirom, povlačenje geometrije skice je uređuje; povlačenje desnim ili srednjim dugmetom pomjera, Shift+desno ili Shift+srednje zumira",
|
||||
"nav.perspective.sketch": "Uređivanje skice koristi ortografski prikaz. Završite skicu da prebacite na perspektivu.",
|
||||
"nav.right_drag_orbits.help": "Isključeno: povlačenje desnim dugmetom ne dira kameru, a desno dugme samo otvara kontekstni meni. Izbor mapiranja miša SolidWorks to isključuje.",
|
||||
@@ -1754,6 +2040,11 @@
|
||||
"not a move": "nije pomjeranje",
|
||||
"not a number": "nije broj",
|
||||
"open a sketch first": "prvo otvorite skicu",
|
||||
"parameters.apply_live": "Primijeni svaku promjenu kad napustite njeno polje",
|
||||
"parameters.apply_live_tip": "Isključeno: promjene čekaju na Primijeni ili U redu, a zatim se dizajn jednom ponovo gradi za sve njih. Otkaži ili Escape ih odbacuje.",
|
||||
"parameters.held": "Promjene na čekanju za Primijeni: {count}",
|
||||
"parameters.held_hint": "Uređujte izraze i jedinice. Promjene čekaju na Primijeni ili U redu.",
|
||||
"parameters.held_one": "1 promjena na čekanju za Primijeni",
|
||||
"parts.automatic_name": "automatski",
|
||||
"parts.dimension.across_flats": "Širina ključa",
|
||||
"parts.dimension.bore": "Provrt",
|
||||
@@ -1776,6 +2067,8 @@
|
||||
"parts.family.set_screw": "Bezglavi vijak s imbusom, ravni vrh",
|
||||
"parts.family.socket_head_cap_screw": "Vijak s cilindričnom glavom i imbusom",
|
||||
"parts.family.washer": "Obična podloška",
|
||||
"parts.holes": "Rupe (neobavezno)",
|
||||
"parts.placement_hint": "izaberite jednu ili više rupa da u svaku postavite dio, ili ga U redu postavlja u ishodište",
|
||||
"pattern.alignment_2": "Poravnanje 2",
|
||||
"pattern.alignment_hint": "Broj uključuje original. Centar ravnomjerno raspoređuje kopije; Obrni bira dodatnu stranu za parne ukupne brojeve.",
|
||||
"pattern.direction_1": "Smjer 1",
|
||||
@@ -1828,6 +2121,8 @@
|
||||
"prefs.bottom_centre": "Dolje u sredini",
|
||||
"prefs.bottom_right": "Dolje desno",
|
||||
"prefs.browser": "Stablo modela",
|
||||
"prefs.browser_eye_left": "Oko pored strelice za skupljanje",
|
||||
"prefs.browser_eye_left_note": "Redovi stabla modela prikazuju oko za prikazivanje/skrivanje pored strelice za skupljanje, kao ranije, umjesto u koloni na desnom kraju redova.",
|
||||
"prefs.browser_reveal_on_hover": "Prikaži pri prelasku mišem",
|
||||
"prefs.browser_show_note": "Prikazano drži stablo modela na ekranu. Prikaži pri prelasku mišem drži ga skrivenim dok pokazivač ne pređe preko njegovog mjesta ili tankog jezička na njegovom lijevom rubu, a ostaje otvoreno dok je pokazivač u njemu ili dok je otvoren neki meni iz njega.",
|
||||
"prefs.browser_shown": "Prikazano",
|
||||
@@ -1847,6 +2142,8 @@
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "Zakrivljene površi",
|
||||
"prefs.curves": "Zakrivljene površi",
|
||||
"prefs.dark_parts_shading": "Zadrži oblik tamnih dijelova",
|
||||
"prefs.dark_parts_shading_help": "Sjenči prikaz u linearnom svjetlu s mekim dopunskim svjetlom neba, tako da tamna lica zadrže gradijent umjesto da pocrne",
|
||||
"prefs.dead_zone_note": "Šrafirana traka je mrtva zona: kretanje unutar nje se zanemaruje.",
|
||||
"prefs.default": "podrazumijevano",
|
||||
"prefs.design": "Podrazumijevane postavke dizajna",
|
||||
@@ -1861,6 +2158,8 @@
|
||||
"prefs.edges": "Ivice i sjenčenje",
|
||||
"prefs.extend": "Produži",
|
||||
"prefs.extra_navigation": "Ctrl + povlačenje lijevim dugmetom po praznom prostoru orbitira; držite razmaknicu za pomjeranje.",
|
||||
"prefs.extrude_view_isometric": "Okreni prikaz u izometrijski kada se otvori Ekstrudiraj",
|
||||
"prefs.extrude_view_return": "Vrati se na prethodni prikaz kada se Ekstrudiraj zatvori",
|
||||
"prefs.follows_os": "Prati operativni sistem",
|
||||
"prefs.font": "Font",
|
||||
"prefs.font_fallback": "Znakovi koji nedostaju izabranom fontu prikazuju se ugrađenim fontom, tako da nijedna oznaka ne prikazuje prazan kvadrat.",
|
||||
@@ -1876,8 +2175,11 @@
|
||||
"prefs.graphics_card_auto": "Automatski",
|
||||
"prefs.graphics_card_missing": "Sačuvana grafička kartica nije dostupna; koristi se Automatski.",
|
||||
"prefs.graphics_card_restart": "Stupa na snagu nakon ponovnog pokretanja SolidCrafta. WGPU_ADAPTER_NAME ima prednost nad ovim izborom.",
|
||||
"prefs.graphics_card_unused": "Sačuvana grafička kartica nije korištena za ovaj prozor; kartica koja se koristi prikazana je u Pomoć › O programu i u statusnoj traci. Pokušajte s opcijom Automatski ili postavite WGPU_ADAPTER_NAME na jedinstveni dio naziva kartice.",
|
||||
"prefs.graphics_note": "Žrtvujte brzinu za glatkoću. Svaki izbor pokazuje svoj učinak na primjeru.",
|
||||
"prefs.graphics_presets_note": "Unaprijed postavljene vrijednosti podešavaju sve izbore ispod odjednom.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "Sakrij simbole ograničenja kada je prikaz umanjen",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "Simboli ograničenja nestaju iz prikaza kada je skica toliko umanjena da bi je zatrpali. Isključeno: ostaju fiksne veličine na ekranu pri svakom zumiranju.",
|
||||
"prefs.history": "Vremenska traka",
|
||||
"prefs.history_origin_bodies": "Prikaži ishodište i tijela na vrhu",
|
||||
"prefs.history_origin_bodies_note": "Vremenska traka lijevo ili ispod stabla modela počinje folderom Tijela aktivne komponente i Ishodištem, s njihovim okom i menijima kao u stablu modela. Vremenska traka na dnu ostaje nepromijenjena.",
|
||||
@@ -1888,6 +2190,8 @@
|
||||
"prefs.history_sketches_note": "Kada je Skice postavljeno na Ispod svoje operacije, vremenska traka lijevo prikazuje svaku skicu jedan nivo uvučeno, ispod prve operacije napravljene iz nje, kad god je dovoljno široka za nazive. Vremenska traka na dnu uvijek zadržava redoslijed nastanka.",
|
||||
"prefs.history_sketches_under_feature": "Ispod svoje operacije",
|
||||
"prefs.hold_stop_snapping": "Držite da isključite privlačenje",
|
||||
"prefs.home_fit_size": "Veličina za Početni prikaz i Uklopi",
|
||||
"prefs.home_fit_size_note": "Koliki dio prikaza model ispunjava kada ga uokviri Početni prikaz, Uklopi, otvaranje dizajna ili promjena prikaza u navigatoru, bez obzira na to je li postavka ispod uključena. 100 %: ispunjava prikaz između panela, kompasa i navigacijske trake.",
|
||||
"prefs.icons_only": "Samo ikone",
|
||||
"prefs.imported_file": "Uvezena datoteka",
|
||||
"prefs.in_browser": "U stablu modela",
|
||||
@@ -1896,13 +2200,20 @@
|
||||
"prefs.interface_language": "Jezik interfejsa",
|
||||
"prefs.interface_note": "Kako SolidCraft izgleda i gdje su njegovi dijelovi.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.invert_navigator_arrows": "Obrni strelice za rotaciju kocke prikaza",
|
||||
"prefs.invert_navigator_arrows_note": "Strelice oko kocke orbitiraju kameru u smjeru u kojem pokazuju, kao u programima Fusion i Inventor: strelica s lijeve strane pokazuje udesno i zakreće kameru udesno. Isključeno: strelica okreće kocku prema strani na kojoj se nalazi, pa lijeva strelica zakreće kameru ulijevo. Postavka ne utiče na zakrivljene strelice za valjanje.",
|
||||
"prefs.keep_work_on_tab_switch": "Zadrži dijaloge i alate otvorenima pri prebacivanju između dizajna",
|
||||
"prefs.keep_work_on_tab_switch_note": "Svaka kartica dizajna zadržava svoj otvoreni dijalog ili alat, s onim što ste u njemu izabrali i upisali, te svoju karticu trake s alatima. Kada se vratite na dizajn, ponovo su tu. Isključeno: prebacivanje između dizajna otkazuje ih i prikazuje karticu Model.",
|
||||
"prefs.labels": "Oznake",
|
||||
"prefs.language_system": "Sistem",
|
||||
"prefs.layout": "Raspored",
|
||||
"prefs.layout_note": "Kompaktno pričvršćuje panele uz ivice prozora. Prozirnost stabla modela dostupna je u opciji Plutajuće. Stranu Asistenta promijenite na stranici Asistent.",
|
||||
"prefs.left": "Lijevo",
|
||||
"prefs.left_click": "Lijevi klik",
|
||||
"prefs.line_drag_draws_one": "Pritisak i povlačenje crta jednu liniju",
|
||||
"prefs.line_drag_draws_one_note": "U alatu Linija pritisak, povlačenje i puštanje crta jednu liniju od mjesta pritiska do mjesta puštanja i tu se završava. Klikovi i dalje crtaju lanac. Isključeno: povlačenje postavlja tačku tamo gdje se pusti, kao i klik.",
|
||||
"prefs.line_hover_tangent_arc": "Zadržite pokazivač na posljednjoj tački linije da pređete na tangentni luk",
|
||||
"prefs.line_hover_tangent_arc_note": "U alatu Linija, nakon što je nacrtan segment, odmaknite pokazivač od posljednje tačke lanca, vratite ga na nju i držite ga mirno: sljedeći klik završava tangentni luk, a isti pokret vraća na linije. Isključeno: zadržavanje na toj tački ne radi ništa.",
|
||||
"prefs.linked_designs": "Povezani dizajni",
|
||||
"prefs.live_input": "Ulaz uživo",
|
||||
"prefs.live_preview": "Pregled uživo",
|
||||
@@ -1916,6 +2227,9 @@
|
||||
"prefs.max_file_size_step": "STEP i uvezene datoteke",
|
||||
"prefs.max_file_size_web": "Web aplikacija otvara STEP i uvezene datoteke do 512 MB i dizajne do 256 MB: kartica preglednika ne može držati veće datoteke. Desktop aplikacija može povećati ove granice.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.menu_grid": "Velike ikone",
|
||||
"prefs.menu_list": "Popis",
|
||||
"prefs.menus": "Meniji",
|
||||
"prefs.minutes": "minuta",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Miš i trackpad",
|
||||
@@ -1923,6 +2237,8 @@
|
||||
"prefs.mouse_mapping": "Mapiranje miša",
|
||||
"prefs.navigation": "Navigacija",
|
||||
"prefs.navigation_note": "Kako miš, trackpad i 3D miš pomjeraju prikaz.",
|
||||
"prefs.navigator_fit": "Uklopi model nakon promjene prikaza u navigatoru",
|
||||
"prefs.navigator_fit_note": "Lice, ugao ili strelica kocke prikaza, odnosno osa alata za orijentaciju, također usmjerava pogled na sredinu modela s jedne udaljenosti na kojoj se uklapa iz svakog smjera (njegova granična sfera). Početni prikaz koristi isti zum i centar dok je ova opcija uključena, pa se prikazi koje izaberete poslije njega samo okreću. Isključeno: prikaz se okreće na mjestu i zadržava svoj cilj i zum.",
|
||||
"prefs.network_host_remove": "Ukloni",
|
||||
"prefs.network_locations": "Dozvoli mrežne lokacije u dizajnima",
|
||||
"prefs.network_locations_note": "Isključeno: povezane datoteke dizajna na drugom računaru (\\\\server\\share) otvaraju se samo sa servera navedenih ovdje ili nakon što ga dozvolite kada se to zatraži.",
|
||||
@@ -1935,10 +2251,16 @@
|
||||
"prefs.origin_plane_names": "Nazivi ravni ishodišta",
|
||||
"prefs.origin_plane_names_note": "Nazivi prikazani u stablu modela, dijalozima i listama. {top} je XY, {front} je XZ, a {right} je YZ, sa Z prema gore. Komande, skripte i sačuvani dizajni zadržavaju XY, XZ i YZ.",
|
||||
"prefs.overall_speed": "Ukupna brzina",
|
||||
"prefs.parameter_list_rows": "Redovi popisa parametara",
|
||||
"prefs.parameter_list_rows_tip": "Strelica prema dolje u praznom polju vrijednosti navodi parametre, favorite prvo, do ovog broja. Manji broj održava popis laganim u velikom dizajnu.",
|
||||
"prefs.persistent_ribbon_pressed": "Istakni aktivne komande na traci",
|
||||
"prefs.persistent_ribbon_pressed_help": "Ističe komande na traci dok su njihovi efekti uključeni. Isključite da biste zadržali prethodni neutralni izgled i pomoć za komande.",
|
||||
"prefs.perspective_fov": "Vidno polje",
|
||||
"prefs.placement": "Postavljanje",
|
||||
"prefs.prefer_free_sketch_points": "Daj prednost pokretnim tačkama skice kada se tačke preklapaju",
|
||||
"prefs.preview_choices": "Pregled vaših izbora",
|
||||
"prefs.print_face_highlight": "Isticanje površine za štampu",
|
||||
"prefs.print_face_highlight_tip": "Koliko se jako površina za štampu tijela boji i obrubljuje u prikazu. 0 % ne prikazuje isticanje.",
|
||||
"prefs.profile_active": "Aktivni profil",
|
||||
"prefs.profile_applied": "Profil je primijenjen.",
|
||||
"prefs.profile_changed": "Postavke su promijenjene otkako je profil sačuvan. Sačuvaj ih upisuje u profil.",
|
||||
@@ -1960,9 +2282,13 @@
|
||||
"prefs.quality_fastest": "najbrže",
|
||||
"prefs.quality_slowest": "najsporije",
|
||||
"prefs.quality_smoother": "glađe",
|
||||
"prefs.quick_measure": "Brzo mjerenje u uglu prikaza",
|
||||
"prefs.quick_measure_note": "Veličina izbora u donjem desnom uglu prikaza: dužina ivice, ili Ø i R za okruglu, površina lica, položaj tačke, udaljenost i ugao dviju stavki, ukupna vrijednost više ivica ili lica. Klik je kopira.",
|
||||
"prefs.render_previous_geometry_budget": "Koristi prethodni budžet geometrije za pregled renderovanja",
|
||||
"prefs.render_previous_geometry_budget_help": "Vraća prethodno fiksno ograničenje geometrije (512 MiB na 64-bitnim desktop računarima, 128 MiB drugdje) umjesto budžeta prema uređaju.",
|
||||
"prefs.reports_saved": "Sačuvano: {n}",
|
||||
"prefs.ribbon_click_ends_tool": "Klik na dugme aktivnog alata završava ga",
|
||||
"prefs.ribbon_click_ends_tool_note": "Klik na istaknuto dugme na traci alata za skiciranje koji je u upotrebi završava taj alat, pa klikovi u prikazu ponovo služe za biranje. Isključeno: klik ponovo pokreće alat.",
|
||||
"prefs.ribbon_reveal": "Prikaži rezultate pretrage na traci",
|
||||
"prefs.ribbon_reveal_note": "Zadržavanje pokazivača iznad rezultata u Pretraži komande pokazuje gdje se komanda nalazi na traci: njenu karticu, njeno dugme ili njen red u padajućem meniju panela. Komanda pokrenuta iz pretrage tamo trepće, a pokrenuta komanda iz padajućeg menija označava svoj panel.",
|
||||
"prefs.right_click": "Desni klik",
|
||||
@@ -1973,6 +2299,8 @@
|
||||
"prefs.search_empty": "Nema odgovarajućih postavki",
|
||||
"prefs.select": "Izbor",
|
||||
"prefs.select_one_item": "Izaberi jednu stavku",
|
||||
"prefs.select_other_hidden": "Izaberi skrivenu geometriju pomoću Izaberi drugo",
|
||||
"prefs.select_other_hidden_help": "Uključeno: obuhvata skrivena tijela, skice i zaklonjene ivice u blizini pokazivača. Isključeno: koristi se izvorni popis vidljive geometrije ispod pokazivača.",
|
||||
"prefs.selecting": "Biranje",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift samo dodaje; Ctrl / Cmd uključuje i isključuje",
|
||||
@@ -1984,11 +2312,16 @@
|
||||
"prefs.sketch_align_points_note": "Tokom crtanja tačka se istovremeno poravnava vodoravno s jednom tačkom skice i okomito s drugom, uz tačkastu pomoćnu liniju, a klik zadržava oba poravnanja. Isključeno: samo s prethodnom tačkom, po jedna osa.",
|
||||
"prefs.sketch_conflict_colour": "Geometrija u sukobu u boji sukoba",
|
||||
"prefs.sketch_conflict_colour_note": "Dok su ograničenja skice u sukobu, krive i tačke koje drži ograničenje koje ne uspijeva crtaju se također u boji sukoba, ne samo njegov simbol ili kota. Isključeno: zadržavaju uobičajene boje.",
|
||||
"prefs.sketch_construction_lines": "Prošireni alati za pomoćne prave",
|
||||
"prefs.sketch_exit_view": "Kada se skica završi",
|
||||
"prefs.sketch_exit_view.keep": "Zadrži trenutni prikaz",
|
||||
"prefs.sketch_exit_view.previous": "Vrati se na prethodni prikaz",
|
||||
"prefs.sketch_exit_view.unless_rotated": "Vrati se osim ako je rotirano",
|
||||
"prefs.sketch_exit_view_note": "Kuda prikaz ide nakon Završi skicu, Otkaži ili dvostrukog Esc.",
|
||||
"prefs.sketch_face_activates_part": "Skiciranje na licu dijela čini taj dio aktivnim",
|
||||
"prefs.sketch_face_activates_part_note": "Skica započeta na licu tijela druge komponente prvo aktivira tu komponentu, pa skica pripada njoj i u stablu modela ostaje uz taj dio. Taj dio nakon toga ostaje aktivan. Isključeno: skica pripada aktivnoj komponenti.",
|
||||
"prefs.sketch_open_look_at": "Okreni kameru prema skici kad se otvori",
|
||||
"prefs.sketch_open_look_at_note": "Isključeno: kad napravite ili uredite skicu, prikaz ostaje onakav kakav jest. Pogledaj okreće prikaz prema skici kad to želite.",
|
||||
"prefs.sketch_recent_snap": "Automatski privlači na nedavne centre i središnje tačke",
|
||||
"prefs.sketch_recent_snap_note": "Tokom crtanja pokazivač se privlači tamo gdje se sijeku pomoćne linije dvaju posljednjih centara krugova ili središnjih tačaka pravih ivica. Isključeno: te pomoćne linije služe samo za prikaz i pokazivač se ne pomjera.",
|
||||
"prefs.sketch_selection": "Skica i izbor",
|
||||
@@ -2000,6 +2333,8 @@
|
||||
"prefs.sketch_tab_always_note": "Kartica Skica ostaje na traci, odmah iza Modela, dok nijedna skica nije otvorena. Njeni alati za crtanje tada pokreću skicu na izabranoj ravni ili licu, ili na ravni koju sljedeću izaberete, s alatom spremnim u njoj.",
|
||||
"prefs.sketch_tracking_guides": "Pomoćne linije poravnanja centra i središnje tačke",
|
||||
"prefs.sketch_tracking_guides_note": "Zadržite pokazivač iznad centra kruga ili središnje tačke prave ivice, pa se pomjerite vodoravno ili okomito da se prikaže privremena tačkasta pomoćna linija.",
|
||||
"prefs.sketch_tracking_points": "Pomoćne linije poravnanja iz krajnjih tačaka i uglova tokom crtanja",
|
||||
"prefs.sketch_tracking_points_note": "Dok alat za crtanje čeka tačku, prelazak mišem preko kraja linije ili luka, ugla pravougaonika ili slobodne tačke prikazuje istu privremenu tačkastu pomoćnu liniju kao centar ili središnja tačka. Isključeno: samo centri i središnje tačke.",
|
||||
"prefs.sketching": "Skiciranje",
|
||||
"prefs.snap_drawing_note": "Tokom crtanja ili povlačenja tačke skice.",
|
||||
"prefs.source": "Izvor",
|
||||
@@ -2016,6 +2351,24 @@
|
||||
"prefs.status_error": "Greška",
|
||||
"prefs.status_selected": "Izabrano",
|
||||
"prefs.status_warning": "Upozorenje",
|
||||
"prefs.structure.appearance": "Izgled",
|
||||
"prefs.structure.appearance_note": "Boje, tekst i pristupačnost.",
|
||||
"prefs.structure.in_profile": "U profilu",
|
||||
"prefs.structure.layout": "Raspored",
|
||||
"prefs.structure.layout_note": "Gdje se nalaze dijelovi prozora.",
|
||||
"prefs.structure.look": "Vizuelno",
|
||||
"prefs.structure.measure_pointer": "Mjerenje i pokazivač",
|
||||
"prefs.structure.mouse3d": "3D miš",
|
||||
"prefs.structure.mouse3d_note": "SpaceMouse i SpaceNavigator.",
|
||||
"prefs.structure.navigation_note": "Miš i trackpad. 3D miš ima vlastitu stranicu.",
|
||||
"prefs.structure.profile": "Profil",
|
||||
"prefs.structure.profile_edited": "Promijenjeno otkako je sačuvano",
|
||||
"prefs.structure.profile_hover": "Sačuvano u profilima postavki",
|
||||
"prefs.structure.scale_percent": "{percent}%",
|
||||
"prefs.structure.sketch": "Skica",
|
||||
"prefs.structure.tools": "Alati",
|
||||
"prefs.structure.tools_note": "Kako se ponašaju alati za modeliranje.",
|
||||
"prefs.structure.work": "Rad",
|
||||
"prefs.supersampling_brief": "Iscrtava prikaz većim i smanjuje ga filtriranjem: vrlo oštro i zahtjevnije.",
|
||||
"prefs.tab_accent_note": "Naglasna boja prati aktivnu karticu",
|
||||
"prefs.theme_file": "datoteka teme",
|
||||
@@ -2027,6 +2380,16 @@
|
||||
"prefs.theme_follow_off": "Isključeno",
|
||||
"prefs.theme_follow_stop": "Prestani pratiti",
|
||||
"prefs.theme_import_hint": "Kliknite da izaberete datoteku teme, ispustite je ovdje ili pritisnite Ctrl+V (Cmd+V na Macu) kada je tema kopirana",
|
||||
"prefs.theme_option_opacity": "Neprozirnost",
|
||||
"prefs.theme_option_opacity_help": "Podesite neprozirnost plutajućih panela. Donja granica čitljivosti štiti tekst preko boja prozora prikaza.",
|
||||
"prefs.theme_option_tab_colours": "Boje kartica",
|
||||
"prefs.theme_option_tab_colours_help": "Koristi boje panela i prozora prikaza ove teme za svaku karticu trake.",
|
||||
"prefs.theme_option_tab_fade_ms": "Prelaz kartica",
|
||||
"prefs.theme_option_tab_fade_ms_help": "Boje panela i prozora prikaza postepeno se mijenjaju pri prelasku na drugu karticu. Nula mijenja odmah.",
|
||||
"prefs.theme_options_badge": "Opcije",
|
||||
"prefs.theme_options_more": "Više · {count}",
|
||||
"prefs.theme_options_reset": "Resetuj",
|
||||
"prefs.theme_options_unavailable": "Nedostupno na ovom uređaju",
|
||||
"prefs.theme_paste": "Zalijepi temu",
|
||||
"prefs.theme_paste_none": "Međuspremnik ne sadrži SolidCraft temu",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
@@ -2048,13 +2411,23 @@
|
||||
"prefs.under_cursor": "Ispod pokazivača",
|
||||
"prefs.units_precision": "Jedinice i preciznost",
|
||||
"prefs.updates": "Ažuriranja",
|
||||
"prefs.value_boxes_in_view": "Prikaži vrijednosti pored njihovih ručica u prikazu",
|
||||
"prefs.value_boxes_in_view_tip": "Vrijednost komande (Udaljenost, Poluprečnik…) u polju pored njene ručice, preko modela. Isključeno: upisujete je u prozoru komande, koji preuzima tastaturu nakon izbora, i ništa ne prekriva model.",
|
||||
"prefs.view": "Ponašanje prikaza",
|
||||
"prefs.view_confirm_corner": "U redu i Otkaži u uglu prikaza",
|
||||
"prefs.view_confirm_corner_note": "Dok je otvoren dijalog ili skica, kvačica i krstić gore desno u prikazu rade isto što i dugmad U redu i Otkaži u dijalogu, odnosno završavaju i otkazuju skicu. Navigator prikaza pomjera se ispod njih.",
|
||||
"prefs.viewport": "Prozor prikaza",
|
||||
"prefs.viewport_analysis_status": "Prikaži status analize u prozoru prikaza",
|
||||
"prefs.viewport_analysis_status_help": "Prikazuje aktivne analize iznad prozora prikaza, otvara njihove legende iz svake stavke, a tipka Esc isključuje najnoviju analizu. Isključite da biste vratili legende u uglovima i prethodno ponašanje tipke Esc.",
|
||||
"prefs.viewport_background": "Pozadina",
|
||||
"prefs.viewport_background_colour": "Boja",
|
||||
"prefs.viewport_background_theme": "Gradijent teme",
|
||||
"prefs.viewport_section_status": "Prikaži status presjeka u prozoru prikaza",
|
||||
"prefs.viewport_section_status_help": "Prikazuje postavku presjeka u prozoru prikaza. Escape i aktivno dugme na traci je isključuju. Isključite da zadržite prethodne kontrole presjeka.",
|
||||
"prefs.viewport_status_details": "Detalji prozora prikaza uživo",
|
||||
"prefs.viewport_status_details_help": "Uređujte postavke aktivne analize i presjeka u standardnim karticama s detaljima. Isključite da vratite legende analize samo za čitanje i dijalog Presjek.",
|
||||
"prefs.viewport_status_summary": "Sažmi stavke statusa prozora prikaza",
|
||||
"prefs.viewport_status_summary_help": "Sažima tri ili više stavki statusa prozora prikaza u jednu sažetu oznaku. Isključite da svaka stavka ostane u punom redu.",
|
||||
"prefs.wheel": "Točkić",
|
||||
"prefs.wheel_trackpad": "Točkić i trackpad",
|
||||
"prefs.workspace": "Radni prostor",
|
||||
@@ -2064,6 +2437,10 @@
|
||||
"properties.title": "Svojstva: {name}",
|
||||
"recovery.days_ago": "prije {count} dana",
|
||||
"recovery.detail": "{file} · operacija: {features} · automatski sačuvano {when}",
|
||||
"recovery.discard_all": "Odbaci sve",
|
||||
"recovery.discard_all_detail": "Ovo se ne može poništiti.",
|
||||
"recovery.discard_all_title": "Odbaciti automatska čuvanja ({count})?",
|
||||
"recovery.discard_all_title_one": "Odbaciti 1 automatsko čuvanje?",
|
||||
"recovery.hours_ago": "prije {count} h",
|
||||
"recovery.just_now": "upravo sada",
|
||||
"recovery.min_ago": "prije {count} min",
|
||||
@@ -2929,7 +3306,9 @@
|
||||
"render.output_ready": "Spremno za renderovanje",
|
||||
"render.output_run_check": "Pokreni provjeru",
|
||||
"render.output_size_limit": "{method}: do {dimension} px po strani i {pixels} piksela. Najveća veličina koja staje: {width} × {height}.",
|
||||
"render.output_size_method_profile": "Za renderovanje {width} × {height} izaberite {method} s CPU profilom resursa {profile} ili manju veličinu.",
|
||||
"render.output_size_ok": "Veličina u redu · {width} × {height} px",
|
||||
"render.output_size_profile": "Za renderovanje {width} × {height} izaberite CPU profil resursa {profile} ili manju veličinu.",
|
||||
"render.output_swap_orientation": "Zamijeni širinu i visinu",
|
||||
"render.output_tools_inspector": "Veličinu i format uredite u inspektoru Izlaz.",
|
||||
"render.output_view_image": "Prikaži sliku",
|
||||
@@ -3142,6 +3521,7 @@
|
||||
"render.size_custom": "Prilagođeno…",
|
||||
"render.size_full_hd": "Full HD",
|
||||
"render.size_hd": "HD",
|
||||
"render.size_needs_profile": "{size} · potrebno: {profile}",
|
||||
"render.size_presets": "Predlošci veličine",
|
||||
"render.size_presets_pending": "Stiže s uređivačem izlaza",
|
||||
"render.size_preview": "Pregled",
|
||||
@@ -3429,6 +3809,24 @@
|
||||
"sheet.flange_type_contour": "Kontura",
|
||||
"sheet.flange_type_edge": "Ivica",
|
||||
"sheet.no_body": "nema limenog tijela: napravite ga pomoću Prirubnica > Osnova ili pretvorite ploču u lim pomoću Pretvori u lim (izaberite njeno veliko ravno lice)",
|
||||
"sheet.personal_rules.confirm_delete": "Potvrdi brisanje",
|
||||
"sheet.personal_rules.delete": "Izbriši",
|
||||
"sheet.personal_rules.delete_prompt": "Izbrisati ovo lično pravilo? Pravila već u dizajnima ostaju nepromijenjena.",
|
||||
"sheet.personal_rules.full": "Lična biblioteka već sadrži 64 pravila.",
|
||||
"sheet.personal_rules.invalid_name": "Unesite kratak naziv bez prijeloma reda ili kontrolnih znakova.",
|
||||
"sheet.personal_rules.invalid_values": "Dopunite vrijednosti pravila: svaka se mora izračunati u ovom dizajnu, s pozitivnom debljinom i K-faktorom od 0 do 1.",
|
||||
"sheet.personal_rules.missing": "Izabrano lično pravilo više nije dostupno.",
|
||||
"sheet.personal_rules.name": "Naziv pravila",
|
||||
"sheet.personal_rules.name_exists": "Lično pravilo s ovim nazivom već postoji. Koristite drugi naziv.",
|
||||
"sheet.personal_rules.name_hint": "Jedinstven naziv",
|
||||
"sheet.personal_rules.overwrite": "Sačuvaj promjene u izabranom ličnom pravilu",
|
||||
"sheet.personal_rules.rename": "Preimenuj",
|
||||
"sheet.personal_rules.rule": "Lično pravilo",
|
||||
"sheet.personal_rules.save": "Sačuvaj novo",
|
||||
"sheet.personal_rules.scope": "Čuva se u vašem profilu za sve dizajne.",
|
||||
"sheet.personal_rules.select": "Izaberite pravilo",
|
||||
"sheet.personal_rules.title": "Lična pravila",
|
||||
"sheet.personal_rules.unsupported": "Ovo pravilo ima vrijednosti koje ova verzija ne može koristiti.",
|
||||
"shortcut.json_error": "parametri moraju biti JSON: {error}",
|
||||
"shortcut.no_command": "nema komande `{id}`; tekst prije JSON-a mora biti id komande",
|
||||
"shortcut.pin": "Prikači na vrh S okvira",
|
||||
@@ -3465,8 +3863,10 @@
|
||||
"sketch_text.align_right": "Desno",
|
||||
"sketch_text.bold": "Podebljano",
|
||||
"sketch_text.builtin_font": "{family} (ugrađeno)",
|
||||
"sketch_text.formula": "Počnite znakom = da riječi složite iz parametara: ='W' + number(Width)",
|
||||
"sketch_text.italic": "Kurziv",
|
||||
"sketch_text.loading_fonts": "Traženje instaliranih fontova…",
|
||||
"sketch_text.no_bold": "Ugrađeni font nema podebljanu varijantu: izaberite instalirani font",
|
||||
"sketch_text.on_path": "Tekst prati svoju putanju: kriva ga postavlja",
|
||||
"sketch_text.path_clear": "Skini tekst s njegove krive",
|
||||
"sketch_text.path_hint": "Kriva skice koju tekst prati: kliknite ovdje, a zatim krivu",
|
||||
@@ -3498,10 +3898,7 @@
|
||||
"the two origins are on the same component": "oba ishodišta su na istoj komponenti",
|
||||
"thread.automatic_size": "Automatska veličina",
|
||||
"thread.designation": "Oznaka",
|
||||
"thread.direction": "Smjer navoja",
|
||||
"thread.iso_metric": "ISO metrički",
|
||||
"thread.left_hand": "Lijevi",
|
||||
"thread.right_hand": "Desni",
|
||||
"thread.select_size": "Izaberite veličinu",
|
||||
"timeline.delete_after_marker_one": "Izbriši 1 operaciju nakon oznake vraćanja",
|
||||
"timeline.delete_after_marker_other": "Izbriši operacije nakon oznake vraćanja: {count}",
|
||||
@@ -3605,6 +4002,8 @@
|
||||
"ui.3d_mouse_section": "3D miš",
|
||||
"ui.3d_mouse_show_in_view": "Prikaži 3D miš u 3D prikazu",
|
||||
"ui.3d_mouse_show_in_view_tip": "Slika vašeg 3D miša u donjem lijevom uglu, koja se kreće dok ga pomjerate.",
|
||||
"ui.3d_mouse_sketch_orbit": "Okreći skice 3D mišem",
|
||||
"ui.3d_mouse_sketch_orbit_tip": "3D miš okreće prikaz i dok je skica otvorena i gledana pod pravim uglom, kao što to može i miš. Isključeno: pomjera i zumira skicu dok ne nagnete prikaz.",
|
||||
"ui.3d_mouse_skip": "Moj uređaj ovo ne može",
|
||||
"ui.3d_mouse_skip_tip": "Preskače ovo kretanje u oba smjera, a kalibracija ga izostavlja.",
|
||||
"ui.3d_mouse_speed": "Brzina 3D miša",
|
||||
@@ -3633,6 +4032,8 @@
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Zakrenite kapicu suprotno od kazaljke na satu, držite je, a zatim pustite.",
|
||||
"ui.3d_mouse_switch_cap": "Kalibriši kao Povratak u centar",
|
||||
"ui.3d_mouse_switch_trackball": "Kalibriši kao trackball",
|
||||
"ui.3d_mouse_use_direct": "Koristi direktni drajver",
|
||||
"ui.3d_mouse_use_direct_tip": "Čitaj 3D miš direktno (hidraw) umjesto preko spacenavd.",
|
||||
"ui.3d_mouse_view_axis": "Osa prikaza",
|
||||
"ui.3d_mouse_view_off": "Isključeno",
|
||||
"ui.3d_mouse_view_off_tip": "Profil drajvera 3D miša isključuje ovu osu, pa njeno kretanje ne pomjera prikaz. Izbor kretanja druge ose ovdje ih zamjenjuje, a to kretanje prestaje pomjerati prikaz.",
|
||||
@@ -3671,6 +4072,7 @@
|
||||
"ui.agent_chat_auth_terminal": "Pokrenite ovo u terminalu, a zatim pritisnite Poveži ponovo:",
|
||||
"ui.agent_chat_auth_title": "Prijavi se kod agenta",
|
||||
"ui.agent_chat_cancel": "Otkaži",
|
||||
"ui.agent_chat_cannot_start": "Instalirano, ali se ne pokreće: {error}",
|
||||
"ui.agent_chat_changes": "Promjene: {n}, jedan korak poništavanja",
|
||||
"ui.agent_chat_changes_n": "Promjene: {n}",
|
||||
"ui.agent_chat_changes_of": "Promjene: {n}/{limit}",
|
||||
@@ -3763,9 +4165,12 @@
|
||||
"ui.agent_chat_queued_stopped": "U redu čekanja (zaustavljeno, nije poslano)",
|
||||
"ui.agent_chat_refresh": "Osvježi",
|
||||
"ui.agent_chat_refused": "Nije dozvoljeno agentu: {reason}",
|
||||
"ui.agent_chat_reinstall": "{agent} je instaliran ({program}), ali se nije pokrenuo kada ga je SolidCraft provjerio: {error}\nNjegova instalacija možda nije potpuna ili je napravljena za drugu vrstu računara. Instalirajte ga ponovo naredbom `{install}`, provjerite da li u terminalu ispisuje svoju verziju, a zatim pritisnite Osvježi.",
|
||||
"ui.agent_chat_remove": "Ukloni iz reda",
|
||||
"ui.agent_chat_remove_file": "Skini s ove poruke",
|
||||
"ui.agent_chat_remove_imported": "Skini s ove poruke; dio ostaje u dizajnu (Poništi ga uklanja)",
|
||||
"ui.agent_chat_reply_cut_off": "Odgovor je dostigao ograničenje dužine prije nego što je završen, pa može biti nepotpun. Zamolite agenta da nastavi ili da da kraći odgovor.",
|
||||
"ui.agent_chat_reply_declined": "Model je odbio odgovoriti na ovu poruku.",
|
||||
"ui.agent_chat_retry": "Pokušaj ponovo",
|
||||
"ui.agent_chat_right": "Desno",
|
||||
"ui.agent_chat_save": "Sačuvaj",
|
||||
@@ -3824,12 +4229,17 @@
|
||||
"ui.ai_copy_suffix": "kopiraj",
|
||||
"ui.ai_custom_url": "Prilagođeni URL",
|
||||
"ui.ai_default": "Podrazumijevano",
|
||||
"ui.ai_edit": "Uredi",
|
||||
"ui.ai_edit_agent_title": "Uredi agenta",
|
||||
"ui.ai_edit_gone": "Ovaj agent je uklonjen, pa nema šta da se sačuva.",
|
||||
"ui.ai_edit_tip": "Promijenite naziv, komandu ili krajnju tačku ovog agenta. Sačuvani ključ ostaje skriven.",
|
||||
"ui.ai_http_busy": "krajnja tačka je zauzeta ili je preko ograničenja; pokušajte ponovo kasnije",
|
||||
"ui.ai_http_key": "krajnja tačka je odbila ključ: provjerite ga u Postavke > AI",
|
||||
"ui.ai_http_not_found": "nema takve krajnje tačke ili modela: provjerite osnovni URL i naziv modela",
|
||||
"ui.ai_http_too_large": "razgovor je prevelik za ovu krajnju tačku; započnite novi razgovor",
|
||||
"ui.ai_key": "API ključ",
|
||||
"ui.ai_key_hint": "neobavezno; čuva se u sistemskom privjesku ključeva",
|
||||
"ui.ai_key_keep_hint": "ostavite prazno da zadržite sačuvani ključ",
|
||||
"ui.ai_key_needs_https": "Ključ se šalje samo preko HTTPS-a ili na ovaj računar.",
|
||||
"ui.ai_key_note": "Čuva se u sistemskom privjesku ključeva, nikad u postavkama SolidCrafta. Za lokalni server ostavite prazno.",
|
||||
"ui.ai_keychain_error": "Sistemski privjesak ključeva je odbio: {error}",
|
||||
@@ -3856,11 +4266,15 @@
|
||||
"ui.ai_permissions": "Dozvole",
|
||||
"ui.ai_preset": "Unaprijed postavljeno",
|
||||
"ui.ai_protocol": "Protokol",
|
||||
"ui.ai_reasoning_effort": "Nivo zaključivanja",
|
||||
"ui.ai_reasoning_effort_field_hint": "prazno = podrazumijevana vrijednost pružaoca",
|
||||
"ui.ai_reasoning_effort_hint": "Neobavezno. Šalje se krajnjoj tački kao `reasoning_effort`; prihvaćene vrijednosti razlikuju se od pružaoca do pružaoca (na primjer `low`, `high` ili `max`). SolidCraft nikad ne mijenja vrijednost koju ovdje postavite. Ostavite prazno da se ništa ne šalje i da se zadrži podrazumijevana vrijednost pružaoca.",
|
||||
"ui.ai_remove": "Ukloni",
|
||||
"ui.ai_request_timed_out": "Model je trajao duže od {seconds} s. Vremensko ograničenje zahtjeva za ovu krajnju tačku možete povećati u Postavke › Asistent.",
|
||||
"ui.ai_request_timeout": "Vremensko ograničenje zahtjeva",
|
||||
"ui.ai_request_timeout_hint": "Koliko dugo jedan zahtjev modelu na ovoj krajnjoj tački smije trajati, od 30 s do jednog sata. Povećajte za spori lokalni model.",
|
||||
"ui.ai_reset": "Resetuj",
|
||||
"ui.ai_save": "Sačuvaj",
|
||||
"ui.ai_settings": "AI postavke…",
|
||||
"ui.ai_show_features": "Prikaži AI funkcije",
|
||||
"ui.ai_show_features_note": "Razgovor s agentom: panel u kojem agent za kodiranje koji već koristite ili krajnja tačka modela koju izaberete radi na otvorenom dizajnu. Isključeno: nema AI dugmeta, prečice, stavke menija ni panela, i ništa se ne traži niti pokreće.",
|
||||
@@ -3949,6 +4363,7 @@
|
||||
"ui.circle": "Krug",
|
||||
"ui.circular_pattern": "Kružni niz",
|
||||
"ui.clear": "Očisti",
|
||||
"ui.clear_print_face": "Ukloni površinu za štampu",
|
||||
"ui.clear_recent": "Očisti nedavno",
|
||||
"ui.clear_the_selection": "Poništi izbor",
|
||||
"ui.click_selects_whole_bodies": "Klik bira cijela tijela",
|
||||
@@ -3983,6 +4398,14 @@
|
||||
"ui.constant": "Konstantno",
|
||||
"ui.constraints_conflict": "Ograničenja su u sukobu",
|
||||
"ui.construction": "Pomoćna",
|
||||
"ui.construction.angle_hint": "Pomoćna prava: kliknite tačku, pa upišite ugao ili izaberite smjer",
|
||||
"ui.construction.bisector_hint": "Simetrala: izaberite dvije prave linije koje se sijeku, pa željeno rješenje",
|
||||
"ui.construction.common_hint": "Zajednička tangenta: izaberite dva kruga ili luka, pa željeno rješenje",
|
||||
"ui.construction.contact_hint": "Tangenta: izaberite krug/luk ili otvoreni splajn/koniku, pa tačku dodira (krive slobodnog oblika: krajnje tačke)",
|
||||
"ui.construction.external_hint": "Tangenta: izaberite krug ili luk, vanjsku tačku, pa željeno rješenje",
|
||||
"ui.construction.free_hint": "Pomoćna prava: kliknite dvije tačke; desnim klikom počnite ispočetka",
|
||||
"ui.construction.point_hint": "Pomoćna prava: kliknite tačku kroz koju prolazi",
|
||||
"ui.construction.reference_hint": "Pomoćna prava: izaberite pravu liniju, pa tačku; desnim klikom promijenite referencu",
|
||||
"ui.construction.ribbon_label": "Pomoćne",
|
||||
"ui.context_menu": "Kontekstni meni",
|
||||
"ui.continuity_g0": "G0 pregib",
|
||||
@@ -4011,6 +4434,7 @@
|
||||
"ui.dark": "Tamna",
|
||||
"ui.default_modelling_orientation": "Podrazumijevana orijentacija modeliranja",
|
||||
"ui.delete": "Izbriši",
|
||||
"ui.delete_select_first": "Prvo izaberite šta želite izbrisati: operacije, tijela, komponente ili geometriju skice",
|
||||
"ui.depth": "Dubina",
|
||||
"ui.design": "Dizajn",
|
||||
"ui.design_properties.author": "Autor",
|
||||
@@ -4025,6 +4449,40 @@
|
||||
"ui.design_properties.project": "Projekat",
|
||||
"ui.design_properties.revision": "Revizija",
|
||||
"ui.design_saved": "Sačuvano: {path}",
|
||||
"ui.details_access_caption": "Lica pokazuju može li alat doći do njih iz izabranog smjera.",
|
||||
"ui.details_angle": "Ugao",
|
||||
"ui.details_continuity_caption": "Uredite izabrani šav. Ostali šavovi zadržavaju vlastite postavke.",
|
||||
"ui.details_continuity_stale": "Ranija geometrija: ponovo pokrenite analizu da uredite ili uokvirite ovaj šav.",
|
||||
"ui.details_copy_list": "Kopiraj popis",
|
||||
"ui.details_curvature": "Tolerancija zakrivljenosti",
|
||||
"ui.details_curvature_caption": "Lica su obojena prema svom lokalnom poluprečniku zakrivljenosti.",
|
||||
"ui.details_direction": "Smjer alata",
|
||||
"ui.details_draft_caption": "Lica su obojena prema svom uglu u odnosu na smjer vađenja iz kalupa.",
|
||||
"ui.details_edit_seam": "Uredi šav",
|
||||
"ui.details_fixture": "Uzorak komponente — samo probni podaci",
|
||||
"ui.details_fixture_caption": "Uzorak komponente s ilustrativnim vrijednostima; ovo nije rezultat modela.",
|
||||
"ui.details_fixture_problem": "Primjer nalaza s problemom",
|
||||
"ui.details_fixture_readout": "Primjer izmjerene vrijednosti",
|
||||
"ui.details_flip": "Obrni",
|
||||
"ui.details_gap": "Otvoreni kraj",
|
||||
"ui.details_hatch": "Šrafiraj presječena lica",
|
||||
"ui.details_jump": "Skok zakrivljenosti",
|
||||
"ui.details_key": "Legenda",
|
||||
"ui.details_offset": "Odmak",
|
||||
"ui.details_plane": "Ravan",
|
||||
"ui.details_pull": "Smjer vađenja iz kalupa",
|
||||
"ui.details_radius": "Poluprečnik",
|
||||
"ui.details_readout": "Očitanje",
|
||||
"ui.details_samples": "Uzorci",
|
||||
"ui.details_seam": "{body} · Ivica {edge}",
|
||||
"ui.details_settings": "Postavke",
|
||||
"ui.details_sharp_gap": "Oštro ili otvoreno",
|
||||
"ui.details_show_all": "Prikaži svih {count}",
|
||||
"ui.details_smooth": "Glatko",
|
||||
"ui.details_stripes": "Pruge",
|
||||
"ui.details_target": "Uokviri geometriju",
|
||||
"ui.details_worst_seams": "Najgori šavovi",
|
||||
"ui.details_zebra_caption": "Reflektovane pruge pokazuju koliko se glatko lica spajaju.",
|
||||
"ui.diameter": "Prečnik",
|
||||
"ui.dimension": "Kota",
|
||||
"ui.direct_interaction": "Direktna interakcija",
|
||||
@@ -4069,6 +4527,7 @@
|
||||
"ui.edit_in_place_saved_elsewhere": "Dio je sačuvan u drugu datoteku; sklop i dalje povezuje: {name}",
|
||||
"ui.edit_in_place_wait": "Pričekajte da se završi ono što se izvršava u dizajnu",
|
||||
"ui.edit_section_analysis": "Uredi analizu presjeka",
|
||||
"ui.edit_sketch_empty": "Nema skica za uređivanje",
|
||||
"ui.ellipse": "Elipsa",
|
||||
"ui.emboss_wrap_note": "Ortogonalni linijski profili koji tangiraju jedan podržani cilindar obavijaju se automatski. Izaberite zid da izaberete cilj.",
|
||||
"ui.end_radius": "Krajnji poluprečnik",
|
||||
@@ -4079,12 +4538,15 @@
|
||||
"ui.equation_explicit": "Eksplicitno",
|
||||
"ui.equation_form": "Oblik",
|
||||
"ui.equation_note": "Varijabla je običan broj: sin(t) je uzima u radijanima. Rezultat bez jedinice je u jedinici dizajna.",
|
||||
"ui.equation_note_range": "Varijabla je dužina, kao i njen raspon: kombinujte je s parametrima dužine, a za sin() je podijelite dužinom. Rezultat bez jedinice je u jedinici dizajna.",
|
||||
"ui.equation_parametric": "Parametarski",
|
||||
"ui.equation_variable_unit": "Varijabla ima jedinicu raspona",
|
||||
"ui.equation_x_of_t": "x(t)",
|
||||
"ui.equation_y_of_t": "y(t)",
|
||||
"ui.equation_y_of_x": "y(x)",
|
||||
"ui.equivalent_radius": "Ekvivalentni poluprečnik",
|
||||
"ui.expand": "Proširi",
|
||||
"ui.explode_needs_two_components": "Rastavljeni prikaz razdvaja komponente, pa dizajn treba imati dvije ili više njih (Nova komponenta, Umetni dizajn ili Napravi komponente iz tijela)",
|
||||
"ui.exportDxf": "Izvezi DXF…",
|
||||
"ui.export_ellipsis_keymap": "Izvezi…",
|
||||
"ui.export_keymap_tip": "Tipke, favoriti okvira S i traka s alatima, kao datoteka",
|
||||
@@ -4245,6 +4707,7 @@
|
||||
"ui.insert_design.saved_file": "{name} ima nesačuvane promjene, pa je umetnuta njegova sačuvana datoteka. Sačuvajte ga, a zatim pokrenite Ažuriraj veze da unesete promjene.",
|
||||
"ui.insert_design.unsaved": "Nesačuvane promjene",
|
||||
"ui.inspect": "Pregledaj",
|
||||
"ui.instances": "Instance",
|
||||
"ui.intersect": "Presjek",
|
||||
"ui.isolate": "Izoluj",
|
||||
"ui.join": "Spoji",
|
||||
@@ -4253,12 +4716,14 @@
|
||||
"ui.joints": "Spojevi",
|
||||
"ui.keyboard_shortcuts": "Prečice na tastaturi",
|
||||
"ui.language": "Jezik",
|
||||
"ui.lasso_select": "Laso (povucite petlju slobodnom rukom: uzima sve što je u cijelosti unutra)",
|
||||
"ui.layout_compact": "Kompaktno",
|
||||
"ui.layout_floating": "Plutajuće",
|
||||
"ui.length": "Dužina",
|
||||
"ui.length_decimals": "Decimale dužine",
|
||||
"ui.light": "Svijetla",
|
||||
"ui.line": "Linija",
|
||||
"ui.line_arc_hint": "Tangentni luk: kliknite kraj luka. Ponovo se zadržite na posljednjoj tački za linije; desni klik ili Esc za završetak",
|
||||
"ui.link_copies": "Poveži identične kopije",
|
||||
"ui.link_copies_keep": "Zadrži",
|
||||
"ui.link_copies_link": "Poveži",
|
||||
@@ -4347,6 +4812,7 @@
|
||||
"ui.part_size_shared": "Ponovo gradi dio, zamjenjujući sve dodane operacije. Svi primjerci ove komponente se ažuriraju.",
|
||||
"ui.paste": "Zalijepi",
|
||||
"ui.path": "Putanja",
|
||||
"ui.pattern_object_type_tip": "Tijela: puni oblici, biraju se u prikazu ili u stablu modela; svako se kopira u cijelosti.\nOperacije: koraci vremenske trake, poput ekstruzije ili rupe, biraju se na vremenskoj traci ili na licu koje su napravili; korak se ponavlja.\nSkica nije ni jedno ni drugo: ponovite njene krive pomoću alata Niz ili Preslikaj u samoj skici ili je prvo ekstrudirajte.",
|
||||
"ui.pattern_on_path": "Niz duž putanje",
|
||||
"ui.perforate_margin_default": "iz prečnika",
|
||||
"ui.perforate_pitch": "Korak",
|
||||
@@ -4373,12 +4839,17 @@
|
||||
"ui.plugins_skipped": "Foldere nije bilo moguće pročitati: {count}.",
|
||||
"ui.plugins_version": "{version}",
|
||||
"ui.point": "Tačka",
|
||||
"ui.point_cloud_add_filter": "Dodaj filter…",
|
||||
"ui.point_cloud_auto_range": "Automatski opseg",
|
||||
"ui.point_cloud_color_by": "Boja prema",
|
||||
"ui.point_cloud_file_colors": "Boje iz datoteke",
|
||||
"ui.point_cloud_filter_by": "Filtriraj prema",
|
||||
"ui.point_cloud_height_high": "Najviše",
|
||||
"ui.point_cloud_height_low": "Najniže",
|
||||
"ui.point_cloud_legend_filter": "Prikazano i skriveno u legendi iznad",
|
||||
"ui.point_cloud_no_file_colors": "Datoteka nema boje",
|
||||
"ui.point_cloud_pause": "Pauziraj",
|
||||
"ui.point_cloud_play": "Reprodukuj",
|
||||
"ui.point_cloud_single_color": "Jedna boja",
|
||||
"ui.point_cloud_size": "Veličina tačke",
|
||||
"ui.point_cloud_units": "Jedinice datoteke",
|
||||
@@ -4387,6 +4858,11 @@
|
||||
"ui.polygon": "Poligon",
|
||||
"ui.preferences": "Postavke",
|
||||
"ui.press_a_key": "pritisnite tipku…",
|
||||
"ui.print_face_body_not_sent": "Tijelo te površine se ne šalje: prvo ga dodajte u Tijela",
|
||||
"ui.print_face_cleared": "{body}: površina za štampu uklonjena",
|
||||
"ui.print_face_input": "Površina za štampu",
|
||||
"ui.print_face_pick": "Odaberite ravnu površinu punog tijela da postane površina za štampu",
|
||||
"ui.print_face_set": "{body}: površina za štampu postavljena; Pošalji u slajser polaže tijelo na nju",
|
||||
"ui.profile": "Profil",
|
||||
"ui.profile_plane": "Ravan profila",
|
||||
"ui.profiles": "Profili",
|
||||
@@ -4402,6 +4878,7 @@
|
||||
"ui.properties.inertia_y": "Iyx, Iyy, Iyz",
|
||||
"ui.properties.inertia_z": "Izx, Izy, Izz",
|
||||
"ui.properties.measuring": "Mjerenje…",
|
||||
"ui.properties.measuring_bodies": "Mjerenje… {done} od {total} tijela",
|
||||
"ui.properties.measuring_failed": "Mjerenje je zaustavljeno prije završetka.",
|
||||
"ui.properties.principal_axis": "Glavna osa {number}",
|
||||
"ui.properties.principal_moments": "Glavni momenti",
|
||||
@@ -4450,6 +4927,7 @@
|
||||
"ui.revolve": "Rotiraj",
|
||||
"ui.ribbon_exit": "Izađi",
|
||||
"ui.ribbon_labels": "Prikaži tekst ispod ikona na traci",
|
||||
"ui.ribbon_menus_skip_pinned": "Izostavi prikačene komande iz padajućih menija",
|
||||
"ui.ribbon_pin_footer": "Prikačene komande prikazuju se na traci. Zadržite pokazivač iznad reda da ga prikačite ili otkačite.",
|
||||
"ui.ribbon_workspace": "Radni prostor",
|
||||
"ui.right": "Desno",
|
||||
@@ -4468,6 +4946,7 @@
|
||||
"ui.selectOther": "Izaberi drugo…",
|
||||
"ui.selectTangentChain": "Izaberi tangentni lanac",
|
||||
"ui.select_all": "Izaberi sve",
|
||||
"ui.select_components_by_face": "Izaberi komponente (lice bira svoju komponentu)",
|
||||
"ui.selection": "Izbor",
|
||||
"ui.selection_modifiers": "Modifikatori izbora",
|
||||
"ui.selection_shift_add_only": "Shift+klik dodaje bez uklanjanja",
|
||||
@@ -4509,6 +4988,11 @@
|
||||
"ui.sketch_intersect_selection_filter": "Filter izbora",
|
||||
"ui.sketch_modifiers": "Modifikatori skice",
|
||||
"ui.sketch_project_geometry": "Geometrija",
|
||||
"ui.sketch_report.all_constrained": "Sve je potpuno ograničeno.",
|
||||
"ui.sketch_report.dof": "Stepeni slobode",
|
||||
"ui.sketch_report.loose": "Nije potpuno ograničeno",
|
||||
"ui.sketch_report.status": "Status",
|
||||
"ui.sketch_report.title": "Izvještaj o skici",
|
||||
"ui.sketch_scale": "Skaliranje skice",
|
||||
"ui.sketches": "Skice",
|
||||
"ui.slice": "Presijeci",
|
||||
@@ -4604,19 +5088,31 @@
|
||||
"ui.view_angles": "Uglovi prikaza",
|
||||
"ui.view_angles_convention": "Stepeni oko ose prema gore. Skretanje 0° je pogled sprijeda, 90° slijeva, −90° zdesna, 180° straga; nagib 90° je pogled odozgo; pozitivno valjanje okreće model u smjeru kazaljke na satu.",
|
||||
"ui.view_angles_help": "Prikazuje nagib, skretanje i valjanje prikaza u stepenima i njegovu projekciju ispod alata za orijentaciju. Kliknite očitanje da upišete tačne uglove.",
|
||||
"ui.view_angles_lock": "Zaključaj {axis}",
|
||||
"ui.view_angles_ortho": "Ortogr.",
|
||||
"ui.view_angles_persp": "Persp.",
|
||||
"ui.view_angles_pitch": "Nagib",
|
||||
"ui.view_angles_readout": "Nagib {pitch} · Skretanje {yaw} · Valjanje {roll}",
|
||||
"ui.view_angles_roll": "Valjanje",
|
||||
"ui.view_angles_unlock": "Otključaj {axis}",
|
||||
"ui.view_angles_yaw": "Skretanje",
|
||||
"ui.view_angles_zero": "Postavi {axis} na 0°",
|
||||
"ui.view_centre": "Centar prikaza",
|
||||
"ui.viewport_status_clear": "Isključi: {name}",
|
||||
"ui.viewport_status_clear_hint": "Isključi: {name} · Esc",
|
||||
"ui.viewport_status_edges": "Ivice: {count}",
|
||||
"ui.viewport_status_from": "iz smjera {direction}",
|
||||
"ui.viewport_status_key": "Prikaži legendu: {name}",
|
||||
"ui.viewport_status_minimum_radius": "Minimalni poluprečnik",
|
||||
"ui.viewport_status_pairs": "Parovi: {count}",
|
||||
"ui.viewport_status_section_hatch": "{setting} · šrafura",
|
||||
"ui.viewport_status_section_plane": "Trenutna ravan",
|
||||
"ui.viewport_status_section_tilted": "nagnuto · {offset}",
|
||||
"ui.viewport_status_stripes": "Pruge: {count}",
|
||||
"ui.viewport_status_summary_clear": "Isključi sve",
|
||||
"ui.viewport_status_summary_heading": "U ovom prikazu",
|
||||
"ui.viewport_status_summary_label": "{name} i još stavki: {count}",
|
||||
"ui.viewport_status_summary_more": "+{count}",
|
||||
"ui.viewport_theme_follow": "Prati temu",
|
||||
"ui.visual_shaded_hidden_edges": "Osjenčeno sa skrivenim ivicama",
|
||||
"ui.visual_shaded_with_edges": "Osjenčeno s ivicama",
|
||||
@@ -4653,7 +5149,9 @@
|
||||
"update.button": "Ažuriraj",
|
||||
"update.button_version": "Ažuriraj na {version}",
|
||||
"update.check_automatically": "Automatski provjeravaj ažuriranja",
|
||||
"update.check_for_updates": "Provjeri ažuriranja",
|
||||
"update.check_now": "Provjeri sada",
|
||||
"update.check_unavailable": "Provjera ažuriranja nije dostupna za ovu instalaciju.",
|
||||
"update.checking": "Provjera…",
|
||||
"update.copy": "Kopiraj",
|
||||
"update.dmg_opened": "Slika diska je otvorena i provjerena: prevucite SolidCraft u Applications, zamijenite staru kopiju, a zatim ponovo pokrenite SolidCraft.",
|
||||
|
||||
+606
-4
File diff suppressed because it is too large
Load Diff
+2816
-2
File diff suppressed because it is too large
Load Diff
+1404
-8
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,13 @@
|
||||
"color.edit": "Click to edit colour",
|
||||
"color.gamma": "Showing colour values in 0-255 gamma space",
|
||||
"color.linear": "Showing colour values in 0-1 linear space",
|
||||
"color.recent": "Recent colours",
|
||||
"color.recent_help": "Colours you pick in any colour field appear here",
|
||||
"color.saturation": "Saturation (per cent)",
|
||||
"color.selected": "Selected colour",
|
||||
"color.step_down": "Step down",
|
||||
"color.step_up": "Step up",
|
||||
"color.value": "Value, the brightness (per cent)",
|
||||
"dxf.cancelled": "DXF export cancelled",
|
||||
"dxf.cancelling": "Cancelling DXF export",
|
||||
"help.apache": "Apache licence 2.0",
|
||||
@@ -67,15 +73,16 @@
|
||||
"navbar.custom.position_centre": "Centre",
|
||||
"navbar.description.separator": "A thin line that groups neighbouring slots.",
|
||||
"pattern.alignment_hint": "Counts include the original. Centre balances copies; Flip chooses the extra side for even totals.",
|
||||
"prefs.appearance.colour_vision_help": "Changes the colours that carry a meaning: sketch states, axes, bends, analyses and icons.",
|
||||
"prefs.appearance.viewport_colours": "3D view colours",
|
||||
"prefs.bottom_centre": "Bottom centre",
|
||||
"prefs.logo_theme": "Theme's colours",
|
||||
"prefs.max_file_size": "Largest file to open",
|
||||
"prefs.max_file_size_design": "Design files",
|
||||
"prefs.max_file_size_flag": "This run started with --max-file-size {size}, which applies until SolidCraft closes. These settings apply when it starts without it.",
|
||||
"prefs.max_file_size_note": "Defaults: 512 MB and 256 MB; at most 16 GB. Opening a file takes several times its size in memory. A design that keeps a large import may need a higher design file limit to save and reopen.",
|
||||
"prefs.max_file_size_step": "STEP and import files",
|
||||
"prefs.max_file_size_web": "The web app opens STEP and import files up to 512 MB and designs up to 256 MB: a browser tab can't hold larger files. The desktop app can raise these limits.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.network_locations_note": "For organisations that keep designs on file servers. Off: only servers you allow.",
|
||||
"prefs.sketch_conflict_colour": "Conflicting geometry in the conflict colour",
|
||||
"prefs.sketch_conflict_colour_note": "While a sketch's constraints conflict, the curves and points a failing constraint holds are drawn in the conflict colour too, not only its glyph or dimension. Off: they keep their usual colours.",
|
||||
"prefs.sketch_construction_lines": "Extended construction line tools",
|
||||
@@ -86,12 +93,18 @@
|
||||
"prefs.sketch_tracking_guides": "Centre and midpoint alignment guides",
|
||||
"prefs.sketch_tracking_guides_note": "Hover over a circle centre or straight-edge midpoint, then move horizontally or vertically to show a temporary dotted guide.",
|
||||
"prefs.status_bar_note": "A thin bar along the window's bottom edge shows what is being edited, the open sketch's state in its colour and the design's units; a click on the units changes them. The sketch's state then no longer shows in the view's corner.",
|
||||
"prefs.structure.appearance_note": "Colours, text and accessibility.",
|
||||
"prefs.structure.tools_note": "How modelling tools behave.",
|
||||
"prefs.theme_follow": "Follow a theme file",
|
||||
"prefs.theme_follow_error": "Not applied: {error}. The last good theme stays on.",
|
||||
"prefs.theme_follow_launch": "Followed for this run only (--theme-file); reloaded whenever another tool rewrites it.",
|
||||
"prefs.theme_follow_note": "Reloaded whenever another tool rewrites it. Choosing a theme stops following it.",
|
||||
"prefs.theme_follow_off": "Off",
|
||||
"prefs.theme_follow_stop": "Stop following",
|
||||
"prefs.theme_option_opacity_help": "Adjust floating panel opacity. A readability floor protects text over viewport colours.",
|
||||
"prefs.theme_option_tab_colours": "Tab colours",
|
||||
"prefs.theme_option_tab_colours_help": "Use this theme’s panel and viewport colours for each ribbon tab.",
|
||||
"prefs.theme_option_tab_fade_ms_help": "Fade panel and viewport colours when switching tabs. Zero switches instantly.",
|
||||
"prefs.trackpad_zoom_modifier.help": "Hold only this key to zoom at the pointer with vertical scroll. Off keeps the existing scroll behaviour.",
|
||||
"prefs.view": "View behaviour",
|
||||
"prefs.viewport_background_colour": "Colour",
|
||||
@@ -234,7 +247,10 @@
|
||||
"ui.default_modelling_orientation": "Default modelling orientation",
|
||||
"ui.design_properties.license": "Licence",
|
||||
"ui.design_properties.organization": "Organisation",
|
||||
"ui.details_curvature_caption": "Faces are coloured by their local radius of curvature.",
|
||||
"ui.details_draft_caption": "Faces are coloured by their angle to the pull direction.",
|
||||
"ui.export_keymap_tip": "Keys, S box favourites and the toolbar, as a file",
|
||||
"ui.instances": "Instances",
|
||||
"ui.layout_compact": "Compact",
|
||||
"ui.layout_floating": "Floating",
|
||||
"ui.material_review.claims": "Sources and licences are unverified claims. Missing values remain unknown.",
|
||||
|
||||
+724
-14
File diff suppressed because it is too large
Load Diff
+1203
-14
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+476
-4
@@ -60,6 +60,7 @@
|
||||
"BLEND SURFACE": "SURFACE DE RACCORD",
|
||||
"BOUNDARY SURFACE": "SURFACE FRONTIÈRE",
|
||||
"BROWSER": "NAVIGATEUR",
|
||||
"Back to the original order": "Revenir à l'ordre d'origine",
|
||||
"Ball": "Rotule",
|
||||
"Bar": "Plat",
|
||||
"Base Center": "Centre de la base",
|
||||
@@ -181,6 +182,8 @@
|
||||
"Designs you open or save appear here.": "Les conceptions que vous ouvrez ou enregistrez apparaissent ici.",
|
||||
"Determined by To Object. Choose Angle to edit the angular extent.": "Déterminé par Jusqu'à l'objet. Choisir Angle pour modifier l'étendue angulaire.",
|
||||
"Diameter": "Diamètre",
|
||||
"Did this assembly open the wrong way up?": "Cet assemblage s'est-il ouvert dans le mauvais sens ?",
|
||||
"Did this part open the wrong way up?": "Cette pièce s'est-elle ouverte dans le mauvais sens ?",
|
||||
"Dimension new shapes": "Coter les nouvelles formes",
|
||||
"Direction": "Direction",
|
||||
"Discard": "Abandonner",
|
||||
@@ -593,6 +596,8 @@
|
||||
"Snap to face and edge centres": "Accrocher aux centres des faces et des arêtes",
|
||||
"SolidCraft cannot submit reports directly. Submit on GitHub; no token is needed in the app.": "SolidCraft ne peut pas soumettre directement les rapports. Soumettez-les sur GitHub ; aucun jeton n'est nécessaire dans l'application.",
|
||||
"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.",
|
||||
"Sort A to Z": "Trier de A à Z",
|
||||
"Sort Z to A": "Trier de Z à A",
|
||||
"Spacing 2 must be a non-zero number": "L'espacement 2 doit être un nombre non nul",
|
||||
"Spacing must be a non-zero number": "L'espacement doit être un nombre non nul",
|
||||
"Specialise": "Spécialiser",
|
||||
@@ -601,6 +606,7 @@
|
||||
"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 assembly up?": "Redresser cet assemblage ?",
|
||||
"Stand this part up?": "Redresser cette pièce ?",
|
||||
"Start angle": "Angle de départ",
|
||||
"Start bulge": "Renflement de début",
|
||||
@@ -772,6 +778,8 @@
|
||||
"at the face": "sur la face",
|
||||
"bom.group_children": "Sous-pièces dans la ligne de leur pièce",
|
||||
"browser.body_name": "Corps{number}",
|
||||
"browser.calibrate_click_a": "Cliquez sur l'image pour le point A, ou saisissez ses coordonnées.",
|
||||
"browser.calibrate_click_b": "Cliquez sur l'image pour le point B, ou saisissez ses coordonnées.",
|
||||
"browser.calibrate_title": "Calibrer : {name}",
|
||||
"browser.capture_many": "{count} composants ont été déplacés, mais leurs positions ne sont pas capturées.",
|
||||
"browser.capture_note": "Capturer la position conserve la nouvelle position ; Rétablir la position restaure la précédente.",
|
||||
@@ -814,6 +822,8 @@
|
||||
"cmd.appearance_pick_up": "Prélever l'apparence",
|
||||
"cmd.appearance_replace": "Remplacer l'apparence",
|
||||
"cmd.assembly_bom": "Nomenclature",
|
||||
"cmd.assembly_mass_properties": "Propriétés de masse",
|
||||
"cmd.body.print_face": "Face d’impression",
|
||||
"cmd.body_rename": "Renommer le corps",
|
||||
"cmd.browser_collapse_all": "Tout réduire",
|
||||
"cmd.browser_expand_all": "Tout développer",
|
||||
@@ -1185,6 +1195,7 @@
|
||||
"cmd.sketch_spline_control_point_5": "Spline par points de contrôle (degré 5)",
|
||||
"cmd.sketch_spline_fit_point": "Spline par points",
|
||||
"cmd.sketch_spline_insert_point": "Ajouter un point de spline",
|
||||
"cmd.sketch_spline_remove_point": "Supprimer un point de spline",
|
||||
"cmd.sketch_spline_reverse": "Inverser le sens de la spline",
|
||||
"cmd.sketch_spun_profile": "Profil de révolution",
|
||||
"cmd.sketch_toggle_diameter": "Basculer rayon / diamètre",
|
||||
@@ -1238,6 +1249,7 @@
|
||||
"cmd.view.render_browser": "Navigateur de rendu",
|
||||
"cmd.view.render_dock": "Déplacer le panneau de rendu",
|
||||
"cmd.view.render_focus": "Vue de rendu ciblée",
|
||||
"cmd.view.status_details": "Détails d'état de la vue 3D",
|
||||
"cmd.view.status_items": "Éléments d'état de la vue 3D",
|
||||
"cmd.view_ambient_occlusion": "Occlusion ambiante",
|
||||
"cmd.view_angle_readout": "Angles de vue",
|
||||
@@ -1276,8 +1288,14 @@
|
||||
"color.hex_hint": "#rrggbb",
|
||||
"color.hue": "Teinte",
|
||||
"color.linear": "Valeurs de couleur affichées en espace linéaire 0-1",
|
||||
"color.recent": "Couleurs récentes",
|
||||
"color.recent_help": "Les couleurs choisies dans n'importe quel champ de couleur apparaissent ici",
|
||||
"color.saturation": "Saturation (pour cent)",
|
||||
"color.saturation_value": "Saturation et valeur",
|
||||
"color.selected": "Couleur sélectionnée",
|
||||
"color.step_down": "Diminuer d'un pas",
|
||||
"color.step_up": "Augmenter d'un pas",
|
||||
"color.value": "Valeur, la luminosité (pour cent)",
|
||||
"config.column_filter_empty": "Aucune colonne correspondante",
|
||||
"contact.components_count": "{count} composants",
|
||||
"contact.enabled_label": "activé",
|
||||
@@ -1323,6 +1341,8 @@
|
||||
"dialog.dimension_insert_hint": "Double-cliquez sur une autre cote pilotante pour insérer son nom",
|
||||
"dialog.dimension_name": "Cote : {name}",
|
||||
"dialog.edit_title": "MODIFIER {title}",
|
||||
"dialog.no_shown_body": "Aucun corps affiché à modifier : affichez-en un ou choisissez Nouveau corps",
|
||||
"dialog.pattern_sketch_not_object": "Choisissez des corps ou des fonctions : une esquisse n'est ni l'un ni l'autre. Répétez ses courbes avec la Répétition ou la Symétrie de l'esquisse, ou extrudez-la d'abord.",
|
||||
"dialog.select": "↖ Sélectionner",
|
||||
"dialog.selected_count": "↖ {count} sélectionné(s)",
|
||||
"dialog.selection_guidance": "Sélectionner une entrée pour la modifier. Cliquer sur un objet sélectionné pour le retirer ; Ctrl/Cmd-clic le bascule aussi. Vérifier l'aperçu, puis appuyer sur OK.",
|
||||
@@ -1380,26 +1400,243 @@
|
||||
"from the rule": "selon la règle",
|
||||
"help.apache": "Licence Apache 2.0",
|
||||
"help.appearance.assign": "Donne aux corps, faces ou composants sélectionnés une couleur ou l'apparence d'un matériau, sans changer leur poids. À utiliser pour distinguer des pièces ou prévisualiser une finition.",
|
||||
"help.assembly.bom": "Liste les pièces de la conception ou d'un composant avec leurs quantités, par référence, et peut enregistrer la liste en CSV ou JSON. À utiliser comme nomenclature pour la commande et l'assemblage.",
|
||||
"help.attributions": "Attributions (ATTRIBUTION.md)",
|
||||
"help.build": "Version {version} · commit {commit} · compilée le {date}",
|
||||
"help.canvas.decal": "Place une image sur une face plane du modèle, comme un autocollant. À utiliser pour afficher une étiquette, un logo ou un motif imprimé sur une pièce.",
|
||||
"help.canvas.edit": "Modifie un canevas : sa position, sa taille, son angle, son opacité, sa symétrie, sa visibilité ou son nom. À utiliser pour aligner une image de référence sur l'esquisse que vous décalquez.",
|
||||
"help.canvas.insert": "Place une image PNG ou JPEG sur un plan ou une face comme référence translucide. À utiliser pour décalquer une photo ou un dessin dans une esquisse, l'image étant mise à une échelle connue.",
|
||||
"help.component.break_link": "Détache un composant lié de son fichier de conception en conservant ses corps tels quels. À utiliser lorsque la pièce ne doit plus suivre les modifications du fichier d'origine.",
|
||||
"help.component.capture_position": "Conserve les positions des pièces que vous avez déplacées à la main, qui deviennent leur nouvelle position. À utiliser après avoir fait glisser en place des pièces qu'aucune liaison ne maintient.",
|
||||
"help.component.create": "Ajoute un composant vide à la conception, dans le composant actif sauf si vous choisissez un autre parent. À utiliser pour donner à une nouvelle pièce d'un assemblage son propre espace pour les corps et les esquisses.",
|
||||
"help.component.finish_edit": "Enregistre la pièce que vous modifiez sur place, revient à son assemblage et met à jour l'assemblage à partir d'elle. À utiliser lorsque vos modifications de la pièce liée sont terminées.",
|
||||
"help.component.from_bodies": "Transforme chaque corps sélectionné en composant à part entière, portant le nom du corps. À utiliser lorsqu'une conception faite de corps isolés doit devenir un assemblage dont les pièces peuvent être reliées et déplacées.",
|
||||
"help.component.insert_design": "Insère une autre conception enregistrée comme composant lié à son fichier, avec ses corps solides placés comme dans ce fichier. À utiliser pour réutiliser une pièce dans un assemblage et récupérer les modifications ultérieures avec Mettre à jour les liens.",
|
||||
"help.component.properties": "Affiche le volume, la masse, le centre de gravité et les moments d'inertie d'un composant, d'après les matériaux de ses corps. À utiliser pour vérifier le poids et l'équilibre d'un assemblage.",
|
||||
"help.component.revert_position": "Remet à leur place les pièces que vous avez déplacées à la main, en abandonnant les déplacements non capturés. À utiliser pour annuler des essais de déplacement dans un assemblage.",
|
||||
"help.component.update_links": "Relit les fichiers de conception liés et remplace les corps qui ont changé. À utiliser après avoir modifié une pièce utilisée par cet assemblage.",
|
||||
"help.config.configure": "Ajoute des paramètres, des fonctions, des matériaux ou des apparences comme colonnes de la table des configurations. À utiliser pour créer des variantes d'une conception, par exemple les tailles d'une pièce.",
|
||||
"help.config.preview": "Vérifie qu'une ligne de la table des configurations se reconstruit, sans modifier la conception ni son historique. À utiliser avant de basculer vers une variante incertaine.",
|
||||
"help.config.rules": "Affiche les règles de la table des configurations, ses lignes et la ligne active. À utiliser pour voir comment les variantes d'une conception sont définies.",
|
||||
"help.config.table": "Affiche la table des configurations : une ligne par variante, une colonne par paramètre ou fonction, et si la conception correspond toujours à la ligne active. À utiliser pour passer en revue les variantes d'une conception.",
|
||||
"help.connect_note": "Redémarrez chaque assistant pour charger le serveur SolidCraft. Chaque fichier a été sauvegardé en .bak avant d'être modifié.",
|
||||
"help.connect_title": "Connecter les assistants IA",
|
||||
"help.construct.axis.edge": "Crée un axe de construction le long d'une arête droite ou d'une ligne d'esquisse. À utiliser comme axe pour une révolution, une répétition circulaire ou une liaison.",
|
||||
"help.construct.axis.normal_to_face": "Crée un axe de construction passant par un point, perpendiculaire à une face. À utiliser comme axe qui sort droit d'une surface.",
|
||||
"help.construct.axis.through_cylinder": "Crée un axe de construction passant par le centre d'une face cylindrique, conique ou torique, ou d'une arête circulaire. À utiliser pour faire tourner ou répéter des fonctions autour d'une pièce ronde.",
|
||||
"help.construct.axis.two_planes": "Crée un axe de construction là où deux plans ou faces planes se rencontrent. À utiliser lorsque l'axe voulu est la droite entre deux faces.",
|
||||
"help.construct.axis.two_points": "Crée un axe de construction passant par deux points, sommets ou points d'esquisse. À utiliser pour un axe qui relie deux positions connues.",
|
||||
"help.construct.plane.along_path": "Crée un plan de construction perpendiculaire à une arête ou une courbe, à une position choisie le long de celle-ci. À utiliser pour esquisser le profil d'un balayage ou une coupe d'une trajectoire.",
|
||||
"help.construct.plane.at_angle": "Crée un plan de construction tourné d'un angle par rapport à un plan ou une face, autour d'un axe ou d'une arête. À utiliser pour des esquisses inclinées, par exemple une équerre à angle.",
|
||||
"help.construct.plane.midplane": "Crée un plan de construction à mi-distance entre deux plans ou faces planes, ou bissecteur lorsqu'ils se rencontrent en formant un angle. À utiliser comme plan de symétrie.",
|
||||
"help.construct.plane.offset": "Crée un plan de construction parallèle à un plan ou une face plane, à une distance donnée ou passant par un point. À utiliser pour esquisser à une hauteur précise au-dessus d'une face.",
|
||||
"help.construct.plane.perpendicular": "Crée un plan de construction qui contient une droite ou une arête et qui est perpendiculaire à un plan ou une face. À utiliser pour esquisser à la verticale sur un modèle, le long d'une arête.",
|
||||
"help.construct.plane.tangent": "Crée un plan de construction qui touche un cylindre, un cône, une sphère ou un tore en un point. À utiliser pour esquisser sur le flanc d'une pièce ronde.",
|
||||
"help.construct.plane.three_points": "Crée un plan de construction passant par trois points. À utiliser pour un plan défini par des positions du modèle, comme trois coins.",
|
||||
"help.construct.plane.two_edges": "Crée un plan de construction passant par deux arêtes droites ou lignes situées dans un même plan. À utiliser pour un plan à travers une pièce, par exemple par deux arêtes opposées.",
|
||||
"help.construct.point.along_path": "Crée un point de construction à une position donnée le long d'une arête ou d'une courbe. À utiliser comme référence à mi-chemin sur une trajectoire.",
|
||||
"help.construct.point.center": "Crée un point de construction au centre d'une arête circulaire, d'un cercle ou arc d'esquisse, d'une sphère ou d'un tore. À utiliser comme référence pour des perçages, des axes et des liaisons.",
|
||||
"help.construct.point.edge_plane": "Crée un point de construction là où une arête ou une ligne traverse un plan ou une face plane. À utiliser pour repérer où une trajectoire coupe un plan.",
|
||||
"help.construct.point.three_planes": "Crée un point de construction là où trois plans ou faces planes se rencontrent. À utiliser comme référence de coin, même lorsque le modèle n'a pas de sommet à cet endroit.",
|
||||
"help.construct.point.two_edges": "Crée un point de construction là où deux arêtes ou lignes se rencontrent, ou se rapprochent le plus. À utiliser comme référence à un coin virtuel.",
|
||||
"help.construct.point.vertex": "Crée un point de construction sur un sommet ou un point d'esquisse. À utiliser comme référence qui subsiste lorsque l'esquisse ou le corps qui l'entoure est masqué.",
|
||||
"help.contact.check": "Liste les paires de pièces des jeux de contact qui se traversent dans leur position actuelle. À utiliser pour trouver les collisions avant de piloter les liaisons ou de lancer une étude de mouvement.",
|
||||
"help.contact.create": "Regroupe deux pièces ou plus dans un jeu de contact, dont les corps ne peuvent pas se traverser pendant le mouvement des liaisons. À utiliser pour des pièces qui doivent s'arrêter au contact, comme un levier contre sa butée.",
|
||||
"help.contact.disable_all": "Désactive le contact : les liaisons pilotées et les études de mouvement ne s'arrêtent plus là où les pièces se touchent. À utiliser pour déplacer à nouveau les pièces librement, ou pour garder un grand assemblage réactif.",
|
||||
"help.contact.edit": "Modifie un jeu de contact : les pièces qu'il contient, son nom, ou s'il est désactivé ; peut aussi supprimer le jeu. À utiliser pour ajuster les pièces qui ne doivent pas se traverser.",
|
||||
"help.contact.enable_all": "Place toutes les pièces de l'assemblage dans un seul jeu de contact, de sorte qu'aucune paire de pièces ne puisse se traverser pendant le mouvement des liaisons. À utiliser pour une vérification rapide d'un mécanisme entier.",
|
||||
"help.contact.enable_sets": "Active le contact pour les jeux de contact que vous avez définis, de sorte que les liaisons pilotées et les études de mouvement s'arrêtent là où leurs pièces se traverseraient. À utiliser pour retrouver vos jeux après avoir désactivé le contact.",
|
||||
"help.contact.list": "Liste les jeux de contact de la conception, les pièces de chacun et si le contact est activé. À utiliser pour vérifier la configuration du contact avant de piloter l'assemblage.",
|
||||
"help.document.inspect": "Affiche une vue d'ensemble de la conception : ses composants, corps, esquisses, fonctions et paramètres. À utiliser pour se repérer dans une conception que vous n'avez pas créée vous-même.",
|
||||
"help.document.units": "Change les unités dans lesquelles la conception affiche les longueurs, par exemple mm ou pouces. La géométrie garde sa taille ; seuls l'affichage et les nombres bruts suivent.",
|
||||
"help.edit.checkpoint": "Mémorise la conception, l'esquisse active et l'historique d'annulation dans leur état actuel. À utiliser avant un essai risqué dont vous voudrez peut-être revenir.",
|
||||
"help.edit.restore_checkpoint": "Revient à un point de restauration, historique d'annulation compris. À utiliser pour abandonner en une seule étape tout ce qui a été fait depuis ce point.",
|
||||
"help.explode.create": "Crée une vue éclatée qui écarte les pièces du centre de l'assemblage, ou selon les déplacements que vous définissez. À utiliser pour montrer comment un assemblage se monte, pour des notices ou des présentations.",
|
||||
"help.face.delete": "Supprime les faces sélectionnées et laisse les faces voisines refermer le vide, ou laisse le trou ouvert. À utiliser pour retirer un arrondi, un trou ou un bossage qui gêne.",
|
||||
"help.file.insert_mesh": "Ajoute un fichier 3MF, STL, OBJ, PLY ou glTF à la conception sous forme de corps maillés dans un nouveau composant. À utiliser pour placer à côté de votre conception un modèle téléchargé ou numérisé comme référence.",
|
||||
"help.file.insert_step": "Ajoute un fichier STEP, IGES, FreeCAD ou Rhino à la conception sous forme de corps solides dans un nouveau composant ; les pièces d'un assemblage deviennent des composants imbriqués. À utiliser pour importer des modèles de fournisseurs ou d'autres logiciels de CAO.",
|
||||
"help.file.save_mesh": "Enregistre des corps en maillage STL, 3MF ou OBJ, avec la finesse des faces courbes que vous choisissez. À utiliser pour préparer des pièces à l'impression 3D ou pour des logiciels qui lisent des maillages.",
|
||||
"help.file.send_to_slicer": "Enregistre les corps sélectionnés en fichier 3MF et l'ouvre dans le slicer de votre choix. À utiliser pour passer du modèle à l'impression en une seule étape.",
|
||||
"help.font_details": "L’interface utilise les polices intégrées d’egui (Ubuntu Light et Hack, sous les licences Ubuntu Font Licence et MIT/Bitstream Vera ; Noto Emoji et la police d’icônes emoji, sous OFL et MIT). Le texte des esquisses utilise Liberation Sans (SIL OFL 1.1). Le pack de polices facultatif de The Crafting Apps, craft-fonts (BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic ; SIL OFL 1.1), n’est pas inclus dans les versions de SolidCraft.",
|
||||
"help.fonts": "Polices",
|
||||
"help.frame.member": "Place un profilé, tel qu'un tube ou un profilé d'aluminium, le long des lignes d'une esquisse de structure, un élément par ligne. À utiliser pour construire des châssis, des racks et des structures soudées.",
|
||||
"help.inspect.accessibility": "Colore les faces qu'un outil peut atteindre depuis une direction, et celles qu'il ne peut pas atteindre. À utiliser pour vérifier l'usinabilité ou le moulage d'une pièce depuis ce côté.",
|
||||
"help.inspect.center_of_mass": "Affiche le centre de gravité des corps, de chacun séparément et de l'ensemble, à densité uniforme. À utiliser pour vérifier l'équilibre et l'endroit où une pièce basculera.",
|
||||
"help.inspect.colored_components": "Donne à chaque composant sa propre couleur temporaire, sans modifier les apparences. À utiliser pour distinguer d'un coup d'œil les pièces d'un assemblage.",
|
||||
"help.inspect.compare": "Compare deux corps et montre où l'un a de la matière que l'autre n'a pas, avec le volume et l'écart. À utiliser pour comparer une pièce modifiée ou importée à l'original.",
|
||||
"help.inspect.continuity": "Vérifie la douceur de raccord des faces le long de leurs arêtes : jeux, cassures ou sauts de courbure, et colore les raccords. À utiliser pour trouver où des surfaces ne se raccordent pas proprement.",
|
||||
"help.inspect.curvature_comb": "Trace des peignes le long de courbes, d'arêtes ou de faces, dont les dents montrent l'intensité de la courbure. À utiliser pour repérer les méplats, les cassures et les courbures irrégulières.",
|
||||
"help.inspect.curvature_map": "Colore les faces selon l'intensité de leur courbure, en rouge là où le rayon est petit. À utiliser pour trouver les zones serrées et juger la douceur d'une surface.",
|
||||
"help.inspect.draft": "Colore les faces selon leur angle de dépouille par rapport à une direction de démoulage, et signale celles qui en manquent. À utiliser pour vérifier une pièce avant moulage ou coulée.",
|
||||
"help.inspect.environment_map": "Affiche le modèle avec une finition réfléchissante, de type miroir. À utiliser pour juger la fluidité des surfaces d'après la façon dont les reflets les parcourent.",
|
||||
"help.inspect.interference": "Trouve les corps qui se chevauchent et montre chaque chevauchement avec son volume. À utiliser pour vérifier qu'aucune pièce d'un assemblage n'entre en collision.",
|
||||
"help.inspect.isocurve": "Trace les courbes isoparamétriques des faces, c'est-à-dire la grille sur laquelle la surface est construite. À utiliser pour voir comment une surface s'écoule et où elle se resserre.",
|
||||
"help.inspect.measure": "Mesure des longueurs, distances, angles, aires et rayons entre les éléments choisis, ou le volume et la masse de corps. À utiliser pour vérifier des dimensions et des jeux.",
|
||||
"help.inspect.minimum_radius": "Trouve le plus petit rayon de courbure de courbes, d'arêtes ou de faces, et son emplacement. À utiliser pour vérifier qu'un outil ou une buse peut suivre la forme.",
|
||||
"help.inspect.section": "Ouvre la vue selon un plan de coupe et hachure les faces coupées, sans modifier le modèle. À utiliser pour regarder à l'intérieur des pièces et des assemblages.",
|
||||
"help.inspect.zebra": "Affiche des bandes noires et blanches réfléchies sur les faces du modèle. À utiliser pour juger la douceur : des bandes qui se cassent ou se brisent sur une arête révèlent une mauvaise transition.",
|
||||
"help.joint.as_built": "Relie deux pièces là où elles se trouvent déjà, sans déplacer aucune des deux, et définit comment elles peuvent encore bouger. À utiliser pour des pièces déjà placées dans leur position finale.",
|
||||
"help.joint.cam": "Relie une came tournante à un suiveur, qui glisse ou pivote lorsque le contour de la came passe dessous. À utiliser pour des cames, des excentriques et des commandes de soupapes.",
|
||||
"help.joint.create": "Relie deux pièces aux origines de liaison que vous choisissez et déplace la seconde pièce en place ; le type de liaison définit comment elles peuvent encore bouger. À utiliser pour construire un assemblage dont les pièces se déplacent comme le vrai mécanisme.",
|
||||
"help.joint.drive": "Définit l'angle ou la distance d'une liaison et déplace les pièces en conséquence, dans ses limites ; les liaisons couplées et les boucles fermées suivent. À utiliser pour vérifier qu'un mécanisme se déplace comme prévu.",
|
||||
"help.joint.gear": "Couple deux liaisons rotatives avec un rapport d'engrenage, de sorte que piloter l'une fait tourner l'autre. À utiliser pour des engrenages droits et des couronnes, avec le rapport tiré des rayons primitifs ou des nombres de dents.",
|
||||
"help.joint.limits": "Définit jusqu'où une liaison peut tourner ou coulisser, par un minimum et un maximum, ou supprime ces limites. À utiliser pour garder une charnière, une glissière ou un levier dans sa course réelle.",
|
||||
"help.joint.motion_link": "Couple deux liaisons avec un rapport et un décalage, de sorte que piloter l'une déplace l'autre. À utiliser pour une relation fixe entre liaisons que Engrenage, Pignon et crémaillère ou Vis ne couvrent pas.",
|
||||
"help.joint.origin": "Place sur une pièce une origine de liaison nommée : au centre d'une face ou d'une extrémité ronde, au milieu d'une arête, à un coin ou en un point. À utiliser pour préparer l'emplacement des liaisons ; une origine choisie sur une face la suit lorsque la pièce change de taille.",
|
||||
"help.joint.rack_pinion": "Relie un pignon tournant à une crémaillère coulissante, de sorte que la crémaillère avance de la longueur de cercle primitif que le pignon parcourt en roulant. À utiliser pour des commandes à crémaillère, des directions et des axes linéaires.",
|
||||
"help.joint.reach": "Déplace un point d'une pièce vers une cible en modifiant les liaisons de sa chaîne, dans leurs limites. À utiliser pour positionner un mécanisme selon l'endroit où doit se trouver son extrémité, comme le bout d'un bras de robot.",
|
||||
"help.joint.rigid_group": "Verrouille ensemble les pièces sélectionnées telles qu'elles sont, pour qu'elles se déplacent d'un seul bloc. À utiliser pour des pièces qui vont ensemble et qui n'ont aucun mouvement entre elles.",
|
||||
"help.joint.screw": "Couple rotation et translation par un pas, de sorte que chaque tour avance de cette distance. À utiliser pour des vis mères, des boulons et des broches filetées.",
|
||||
"help.library.apply": "Applique un matériau ou une apparence d'un fichier de bibliothèque local à des corps, des faces ou des composants. À utiliser pour réutiliser des matériaux enregistrés depuis une autre conception.",
|
||||
"help.library.export": "Enregistre les matériaux et apparences de la conception dans un fichier de bibliothèque. À utiliser pour les partager ou les réutiliser dans d'autres conceptions.",
|
||||
"help.library.inspect": "Vérifie un fichier de bibliothèque et liste ses matériaux et apparences, sans modifier la conception. À utiliser pour voir ce que contient une bibliothèque avant de l'appliquer.",
|
||||
"help.licences": "Licences",
|
||||
"help.material.assign": "Applique le matériau choisi aux corps sélectionnés, par exemple acier, aluminium ou ABS. À utiliser pour que la masse et le poids correspondent à la pièce réelle.",
|
||||
"help.material.properties.validate": "Vérifie qu'une fiche de données de matériau a des valeurs cohérentes et qu'aucune n'est manquante. À utiliser avant de se fier aux données pour la masse ou la rigidité.",
|
||||
"help.mesh.convert": "Reconstruit un corps maillé, tel qu'un STL, en solide modifiable avec des faces planes et cylindriques. À utiliser pour modifier une pièce mécanique importée comme un corps modélisé.",
|
||||
"help.mit": "Licence MIT",
|
||||
"help.motion.study": "Définit des valeurs de liaison à des étapes le long d'une chronologie et les rejoue sous forme d'animation. À utiliser pour voir un mécanisme se déplacer selon une séquence et vérifier les jeux en chemin.",
|
||||
"help.motion.trace": "Trace le chemin que suit un point d'une pièce pendant la lecture d'une étude de mouvement ou le pilotage d'une liaison, et le conserve comme courbe d'esquisse 3D lorsque vous confirmez. À utiliser pour voir où arrive un mécanisme, ou pour construire à partir de sa trajectoire.",
|
||||
"help.notice": "NOTICE",
|
||||
"help.occurrence.ground": "Fixe une pièce en place, ou la libère, de sorte que les liaisons déplacent les autres pièces autour d'elle. À utiliser pour le châssis ou la base sur laquelle le reste de l'assemblage est construit.",
|
||||
"help.occurrence.move": "Déplace une pièce d’assemblage selon les seules libertés permises par ses liaisons. Sélectionnez une face de la pièce, puis faites glisser une flèche ou un arc disponible.",
|
||||
"help.parameters.change": "Ouvre le tableau des valeurs nommées et des expressions qui pilotent les cotes de la conception. À utiliser pour changer une dimension à un seul endroit et faire suivre toutes les fonctions qui l'utilisent.",
|
||||
"help.parameters.export": "Enregistre les paramètres de la conception en CSV ou JSON. À utiliser pour les modifier dans un tableur ou les transférer vers une autre conception.",
|
||||
"help.parameters.graph": "Montre quels paramètres et fonctions en utilisent quels autres, ainsi que les boucles ou erreurs entre eux. À utiliser pour voir ce que la modification d'un paramètre va affecter.",
|
||||
"help.parameters.import": "Lit des paramètres depuis un CSV ou un JSON dans la conception, tous ou aucun. À utiliser pour importer des valeurs d'un tableur ou d'une autre conception.",
|
||||
"help.parameters.move": "Déplace un paramètre utilisateur avant ou après un autre dans la liste. À utiliser pour garder ensemble les paramètres liés.",
|
||||
"help.parts.fastener": "Ajoute une vis, un boulon, un écrou, une rondelle, une goupille ou un roulement standard à la taille choisie. Placé sur un perçage, il s'y loge avec une liaison, ce qui évite de modéliser soi-même les fixations.",
|
||||
"help.parts.insert": "Insère une pièce standard de la famille, de la taille et de la longueur choisies. Placée sur le bord ou la paroi d'un perçage, elle s'y loge avec une liaison rigide.",
|
||||
"help.parts.library": "Ouvre les pièces standard : vis, boulons, écrous, rondelles, vis sans tête, goupilles cylindriques et roulements en tailles normalisées. À utiliser pour ajouter une pièce standard plutôt que de la modéliser soi-même.",
|
||||
"help.parts.resize": "Change la taille ou la longueur d'une pièce standard insérée et la reconstruit ; ses liaisons la maintiennent en place. À utiliser lorsqu'une vis se révèle trop longue ou qu'un perçage demande une autre taille.",
|
||||
"help.plastic.assign_rule": "Affecte une règle plastique à des corps, de sorte que leurs fonctions plastique prennent sa dépouille et son jeu par défaut. À utiliser pour configurer une pièce moulée une fois pour toutes ses fonctions.",
|
||||
"help.plastic.boss": "Ajoute un bossage de vis sur une face : un plot avec perçage, dépouille, congé de pied et nervures de soutien facultatifs. À utiliser pour les points de vissage des boîtiers moulés ou imprimés.",
|
||||
"help.plastic.lip": "Ajoute une lèvre ou une rainure correspondante le long du bord d'un corps évidé. À utiliser pour que deux moitiés de boîtier se positionnent l'une sur l'autre et se ferment hermétiquement.",
|
||||
"help.plastic.manage_rules": "Crée et modifie des règles plastique : matériau, épaisseur de paroi, rayons, jeu et dépouille. À utiliser pour garder cohérentes les fonctions d'une pièce moulée.",
|
||||
"help.plastic.rest": "Ajoute un appui sur une face : un plot rond ou rectangulaire, plein ou creux, avec dépouille. À utiliser comme support ou entretoise dans un boîtier moulé.",
|
||||
"help.plastic.snap_fit": "Ajoute un crochet de clip sur une face, avec sa longueur, son épaisseur et son ergot. À utiliser pour assembler des pièces par encliquetage sans vis.",
|
||||
"help.pointcloud.insert": "Insère un scan PLY, XYZ ou PTS sous forme de nuage de points à côté du modèle. Utilisez-le comme référence pour construire un modèle à partir de données mesurées.",
|
||||
"help.sheet.convert": "Transforme une plaque ou un corps évidé d'épaisseur uniforme en corps de tôlerie, avec des plis aux arêtes choisies. À utiliser pour rendre une pièce existante dépliable.",
|
||||
"help.sheet.export_bend_table": "Enregistre la table des plis d'un corps de tôlerie en CSV ou JSON : angles, rayons et positions des plis à plat. À utiliser pour la plieuse ou l'atelier.",
|
||||
"help.sheet.export_dxf": "Enregistre la mise à plat en DXF, avec contours, lignes de pli et annotations de pli facultatives. À utiliser pour la découpe laser, plasma ou jet d'eau.",
|
||||
"help.sheet.flange": "Ajoute un bord tombé : une plaque de base à partir d'une esquisse, une paroi le long d'un contour d'esquisse ouvert, ou une paroi pliée sur des arêtes de tôle. À utiliser pour construire des pièces de tôlerie.",
|
||||
"help.sheet.flat_pattern": "Crée la mise à plat d'un corps de tôlerie, avec sa taille, ses découpes et ses lignes de pli. À utiliser pour vérifier le flan avant de le découper.",
|
||||
"help.sheet.fold": "Plie une tôle le long d'une ligne d'esquisse selon un angle et un rayon. À utiliser pour plier une tôle plate après y avoir découpé des fonctions.",
|
||||
"help.sheet.hem": "Replie un bord de tôle sur lui-même. À utiliser pour obtenir des bords sûrs et rigides, sans bavures coupantes.",
|
||||
"help.sheet.manage_rules": "Crée et modifie des règles de tôlerie : épaisseur, rayon de pli, facteur K, dégagements et jeux. À utiliser pour s'adapter au matériau et aux outils de votre atelier.",
|
||||
"help.sheet.path_flange": "Ajoute une chaîne de bords tombés le long d'une arête de tôle, en suivant une chaîne ouverte de lignes d'esquisse. À utiliser pour des profils en U ou en Z en une seule étape.",
|
||||
"help.sheet.perforate": "Ajoute un champ de trous sur toute la tôle à plat, en motif décalé ou carré. À utiliser pour des aérations, des grilles et des panneaux allégés.",
|
||||
"help.sheet.refold": "Replie un corps de tôlerie déplié. À utiliser après avoir ajouté des fonctions à la tôle à plat.",
|
||||
"help.sheet.unfold": "Déplie tous les plis d'un corps de tôlerie. À utiliser pour ajouter des trous ou des découpes qui doivent traverser un pli, puis replier.",
|
||||
"help.sketch.arc.center_point": "Dessine un arc de cercle autour d'un centre, d'un point de départ à un point d'arrivée. À utiliser lorsque le centre de l'arc est connu, comme un rayon autour d'un perçage ou d'un pivot.",
|
||||
"help.sketch.arc.tangent": "Dessine un arc qui part de l'extrémité d'une ligne ou d'un arc et continue en tangence avec lui. À utiliser pour poursuivre une chaîne avec une courbe douce plutôt qu'un angle.",
|
||||
"help.sketch.arc.three_point": "Dessine un arc d'un point de départ à un point d'arrivée en passant par un troisième point. À utiliser lorsque l'arc doit passer par des points donnés plutôt qu'autour d'un centre connu.",
|
||||
"help.sketch.arc3d": "Dessine un arc de cercle exact dans l'espace par trois points, avec Esquisse 3D activée. À utiliser pour des courbes de trajectoires et de rails guides qui sortent du plan d'esquisse.",
|
||||
"help.sketch.auto_constrain": "Ajoute les contraintes que l'esquisse a presque déjà, comme horizontale, parallèle ou coïncidente, puis la cote jusqu'à ce qu'elle soit entièrement définie. À utiliser pour verrouiller en une étape une esquisse dessinée approximativement.",
|
||||
"help.sketch.auto_constrain_from_datum": "Fonctionne comme Contraintes automatiques, mais mesure les positions depuis un point de référence que vous choisissez plutôt que depuis l'origine. À utiliser lorsque la pièce est tracée à partir d'un point de référence, comme un coin ou un perçage.",
|
||||
"help.sketch.blend_curve": "Relie les extrémités de deux courbes par une spline douce, tangente aux deux, ou aussi continue en courbure si vous le choisissez. À utiliser pour combler un écart entre des courbes sans cassure.",
|
||||
"help.sketch.break": "Scinde une courbe en morceaux là où d'autres courbes la croisent, ou en des points que vous placez dessus. À utiliser lorsqu'une partie d'une courbe a besoin de sa propre contrainte ou doit être supprimée seule.",
|
||||
"help.sketch.cancel": "Quitte l'esquisse et abandonne tout ce qui a été modifié depuis sa création ou son ouverture pour modification. Annuler rétablit les modifications.",
|
||||
"help.sketch.centerline": "Bascule des lignes entre lignes normales et lignes d'axe, des axes que les profils ignorent. Utilisez une ligne d'axe comme axe pour une révolution, une symétrie ou une contrainte de symétrie.",
|
||||
"help.sketch.chamfer.distance_angle": "Biseaute le coin entre deux lignes, défini par une distance le long de la première ligne et un angle. À utiliser lorsqu'un dessin donne le chanfrein sous forme de longueur et d'angle, comme 2 mm × 30°.",
|
||||
"help.sketch.chamfer.equal_distance": "Biseaute le coin entre deux lignes sur la même distance le long des deux. À utiliser pour le chanfrein habituel à 45° sur un contour.",
|
||||
"help.sketch.chamfer.two_distance": "Biseaute le coin entre deux lignes, défini par une distance distincte le long de chaque ligne. À utiliser pour des chanfreins qui ne sont pas à 45°, donnés sous forme de deux longueurs.",
|
||||
"help.sketch.circle.center": "Dessine un cercle à partir de son centre et de son diamètre. À utiliser pour des perçages, des bossages et d'autres formes rondes dont le centre est connu.",
|
||||
"help.sketch.circle.three_point": "Dessine un cercle passant par trois points. À utiliser lorsque le cercle doit passer par des points donnés, par exemple de la géométrie existante.",
|
||||
"help.sketch.circle.three_tangent": "Dessine un cercle qui touche trois lignes, cercles ou arcs. À utiliser pour inscrire un cercle dans l'espace entre trois courbes, comme le cercle inscrit d'un triangle.",
|
||||
"help.sketch.circle.two_point": "Dessine un cercle à partir des deux extrémités de son diamètre. À utiliser lorsque les points du bord du cercle sont connus plutôt que son centre.",
|
||||
"help.sketch.circle.two_tangent": "Dessine un cercle de taille donnée qui touche deux lignes, cercles ou arcs. À utiliser pour un arrondi de rayon fixe entre deux courbes.",
|
||||
"help.sketch.conic": "Dessine une courbe conique entre deux extrémités, façonnée par un point sommet et une valeur rho. À utiliser pour des transitions douces dont vous voulez ajuster la tension, de presque plate à presque anguleuse.",
|
||||
"help.sketch.constrainer": "Applique la contrainte qu'une ou deux courbes choisies ont presque déjà, comme horizontale, parallèle, tangente ou égale. À utiliser pour mettre au propre une esquisse à main levée, une relation à la fois.",
|
||||
"help.sketch.constraint.coincident": "Place un point sur un autre point, une courbe ou un axe d'esquisse. À utiliser pour fermer les écarts d'un profil ou pour poser un point sur une ligne.",
|
||||
"help.sketch.constraint.collinear": "Place deux lignes sur une même droite. À utiliser pour aligner des arêtes interrompues, comme les deux côtés d'une encoche.",
|
||||
"help.sketch.constraint.concentric": "Donne le même centre à deux cercles ou arcs. À utiliser pour des bagues, des lamages et des arbres à épaulements.",
|
||||
"help.sketch.constraint.curvature": "Fait que deux courbes qui se rejoignent en une extrémité continuent avec la même courbure (G2). À utiliser lorsqu'un raccord tangent n'est pas assez doux, par exemple sur des surfaces visibles.",
|
||||
"help.sketch.constraint.equal": "Donne la même longueur à des lignes, ou le même rayon à des cercles et arcs. À utiliser pour qu'une seule cote pilote plusieurs formes identiques.",
|
||||
"help.sketch.constraint.fix": "Fixe des points ou des courbes là où ils sont, ou les libère. À utiliser pour la géométrie qui ne doit pas bouger lorsque vous faites glisser ou résolvez l'esquisse.",
|
||||
"help.sketch.constraint.horizontal_vertical": "Rend une ligne, ou la ligne entre deux points, horizontale ou verticale, selon la plus proche. À utiliser pour redresser un contour sur les axes de l'esquisse.",
|
||||
"help.sketch.constraint.midpoint": "Place un point au milieu d'une ligne, ou fait coïncider les milieux de deux lignes. À utiliser pour centrer de la géométrie sur une arête.",
|
||||
"help.sketch.constraint.parallel": "Rend deux lignes parallèles. À utiliser pour des côtés qui doivent rester parallèles lorsque d'autres cotes changent.",
|
||||
"help.sketch.constraint.perpendicular": "Fait se rencontrer deux lignes à angle droit. À utiliser pour des coins d'équerre entre des lignes qui ne sont pas sur les axes de l'esquisse.",
|
||||
"help.sketch.constraint.polygon": "Transforme une chaîne fermée de lignes en polygone régulier : côtés égaux, avec tous les coins sur un même cercle. À utiliser pour rendre régulier un polygone dessiné à main levée et le garder ainsi.",
|
||||
"help.sketch.constraint.symmetry": "Fait que deux points ou courbes sont symétriques par rapport à une ligne ou à un point. À utiliser pour garder un profil symétrique lorsque vous modifiez un côté.",
|
||||
"help.sketch.constraint.tangent": "Fait que deux courbes se touchent en tangence, comme une ligne et un arc. À utiliser pour des raccords doux sans cassure.",
|
||||
"help.sketch.construction": "Bascule des courbes entre géométrie normale et géométrie de construction, que les profils ignorent. Utilisez les courbes de construction comme aides pour l'implantation, les références et les cotes.",
|
||||
"help.sketch.construction_line": "Crée une droite de construction infinie passant par deux points. À utiliser pour structurer une esquisse sans diviser les profils.",
|
||||
"help.sketch.construction_line.angle": "Crée une droite de construction infinie inclinée d'un angle par rapport à l'axe X de l'esquisse. Choisissez le point puis saisissez l'angle, ou choisissez sa direction.",
|
||||
"help.sketch.construction_line.bisector": "Sélectionnez deux lignes droites sécantes de l'esquisse, puis choisissez l'une des deux bissectrices. Le résultat suit les lignes dont il provient.",
|
||||
"help.sketch.construction_line.horizontal": "Crée une droite de construction horizontale infinie passant par un point. Sa contrainte horizontale est conservée lors des modifications ultérieures.",
|
||||
"help.sketch.construction_line.parallel": "Sélectionnez une ligne droite de l'esquisse, puis placez une droite de construction infinie parallèle. Clic droit pour choisir une autre référence.",
|
||||
"help.sketch.construction_line.perpendicular": "Sélectionnez une ligne droite de l'esquisse, puis placez une droite de construction infinie perpendiculaire. Clic droit pour choisir une autre référence.",
|
||||
"help.sketch.construction_line.tangent_at": "Crée une tangente en un point d'un cercle ou d'un arc de cercle, ou à l'extrémité d'une spline ouverte ou d'une conique. Sélectionnez la courbe, puis son point de contact.",
|
||||
"help.sketch.construction_line.tangent_external": "Sélectionnez un cercle ou un arc de cercle, puis un point extérieur et une solution de tangente. Les aperçus n'affichent que les points de contact situés sur l'arc dessiné.",
|
||||
"help.sketch.construction_line.tangent_two": "Sélectionnez deux cercles ou arcs de cercle, puis choisissez une tangente commune dans l'aperçu. Les points de contact doivent se trouver sur les courbes dessinées.",
|
||||
"help.sketch.construction_line.vertical": "Crée une droite de construction verticale infinie passant par un point. Sa contrainte verticale est conservée lors des modifications ultérieures.",
|
||||
"help.sketch.copy": "Copie les courbes et points d'esquisse sélectionnés, avec les contraintes et cotes qui les relient. Collez-les dans cette esquisse ou dans une autre.",
|
||||
"help.sketch.create": "Démarre une nouvelle esquisse 2D sur un plan ou sur une face plane du modèle. Toute extrusion, révolution et balayage commence par une esquisse.",
|
||||
"help.sketch.cut": "Copie la géométrie d'esquisse sélectionnée et la supprime de l'esquisse, en une seule étape d'annulation. Collez-la ailleurs dans cette esquisse ou dans une autre.",
|
||||
"help.sketch.dimension": "Ajoute une cote pilotante : une longueur, une distance, un angle, un rayon ou un diamètre, selon ce que vous choisissez. Saisissez une valeur ou un nom de paramètre, et l'esquisse la suit.",
|
||||
"help.sketch.edit": "Ouvre une esquisse existante pour la modifier. Les fonctions construites dessus se mettent à jour lorsque vous terminez l'esquisse.",
|
||||
"help.sketch.ellipse": "Dessine une ellipse à partir de son centre, de l'extrémité de son grand axe et d'un point côté petit axe. À utiliser pour des ouvertures ovales, des cames et d'autres formes rondes allongées.",
|
||||
"help.sketch.equation_curve": "Dessine une courbe à partir de formules : x(t) et y(t), ou y = f(x), sur un intervalle. Les formules peuvent utiliser des paramètres et la courbe les suit : à utiliser pour une sinusoïde, une spirale ou la développante d'une dent d'engrenage.",
|
||||
"help.sketch.extend": "Prolonge une ligne ou un arc jusqu'à la courbe la plus proche sur son chemin, ou fait croître deux courbes jusqu'à ce qu'elles se rejoignent. À utiliser pour fermer des jeux dans un profil après ajustement ou modification.",
|
||||
"help.sketch.fillet": "Arrondit le coin où deux lignes ou arcs se rejoignent par un arc de rayon donné. Plusieurs coins arrondis ensemble partagent un même rayon.",
|
||||
"help.sketch.finish": "Quitte l'esquisse et revient au modèle. Ses profils fermés sont alors prêts pour Extrusion, Révolution et d'autres fonctions.",
|
||||
"help.sketch.finish_auto_constrain": "Lance Contraintes automatiques sur l'esquisse, puis la termine. À utiliser pour laisser une esquisse entièrement définie en une seule étape.",
|
||||
"help.sketch.fit_curves_to_section": "Coupe un corps maillé ou solide avec le plan d'esquisse et ajuste des lignes et des arcs à la section. À utiliser pour reconstruire une pièce numérisée ou importée sous forme d'esquisse modifiable.",
|
||||
"help.sketch.include_3d": "Amène des arêtes du modèle, des sommets et des courbes d'autres esquisses dans cette esquisse comme références liées, en aplatissant sur son plan celles qui en sont hors. À utiliser pour construire à partir d'une géométrie qui n'est pas dans le plan d'esquisse.",
|
||||
"help.sketch.infinite": "Bascule des lignes entre lignes normales et lignes de construction infinies qui traversent toute la vue. À utiliser comme références d'implantation qui ne s'arrêtent jamais trop court.",
|
||||
"help.sketch.insert_dxf": "Lit les courbes d'un dessin DXF dans une esquisse, mises à l'échelle, tournées et placées comme vous le définissez. À utiliser pour construire à partir d'un dessin 2D d'un autre logiciel, comme un contour de découpe laser.",
|
||||
"help.sketch.insert_svg": "Lit les tracés d'un dessin SVG dans une esquisse, mis à l'échelle, tournés et placés comme vous le définissez. À utiliser pour transformer un logo ou un contour vectoriel en courbes que vous pouvez extruder.",
|
||||
"help.sketch.inspect": "Ouvre un rapport sur l'esquisse ouverte : combien de degrés de liberté il reste et si elle est entièrement contrainte. À utiliser pour savoir ce qui bouge encore avant de terminer l'esquisse.",
|
||||
"help.sketch.intersect": "Ajoute les courbes ou points où des corps, faces, arêtes ou d'autres esquisses traversent le plan d'esquisse. Ils restent liés et suivent le modèle lorsqu'il change.",
|
||||
"help.sketch.intersection_curve": "Crée des courbes 3D là où deux corps ou faces se rencontrent, liées à eux. À utiliser comme trajectoires ou rails guides qui suivent le modèle.",
|
||||
"help.sketch.isoparametric_curve": "Crée une courbe 3D le long de la direction U ou V d'une face, par un point que vous choisissez, liée à la face. À utiliser comme trajectoire ou ligne de partage qui suit la surface.",
|
||||
"help.sketch.line": "Dessine des lignes connectées point par point, en chaîne ouverte ou en contour fermé. C'est l'outil de base de la plupart des profils.",
|
||||
"help.sketch.line.midpoint": "Dessine une ligne de son milieu vers une extrémité, l'autre extrémité étant son symétrique. À utiliser pour des lignes centrées sur un point, comme une arête symétrique.",
|
||||
"help.sketch.line3d": "Dessine des lignes droites connectées entre des points de l'espace, avec Esquisse 3D activée. À utiliser pour des trajectoires et des structures qui sortent du plan d'esquisse.",
|
||||
"help.sketch.mirror": "Symétrise des courbes et points d'esquisse par rapport à une ligne ou un axe et ajoute des contraintes de symétrie. À utiliser pour dessiner la moitié d'un profil symétrique et le compléter.",
|
||||
"help.sketch.move": "Déplace ou fait tourner des courbes et points d'esquisse, ou une copie. À utiliser pour repositionner de la géométrie en gardant sa forme et ses contraintes.",
|
||||
"help.sketch.offset": "Crée une copie d'une chaîne de courbes à une distance donnée, d'un côté ou des deux. À utiliser pour des parois, des contours et des jeux ; la distance reste une cote modifiable.",
|
||||
"help.sketch.paste": "Colle dans l'esquisse active la géométrie d'esquisse copiée ou coupée. Placez-la en un point ou décalez-la d'une valeur.",
|
||||
"help.sketch.pattern.circular": "Copie la géométrie d'esquisse autour d'un centre, sur un cercle complet ou un angle donné. À utiliser pour des cercles de perçages, des rayons et des dents.",
|
||||
"help.sketch.pattern.rectangular": "Copie la géométrie d'esquisse en lignes et colonnes à des espacements donnés. À utiliser pour des grilles de trous, de rainures ou d'aérations.",
|
||||
"help.sketch.point": "Place un point d'esquisse. À utiliser comme référence pour des cotes, des contraintes, des centres de perçage ou des fonctions ultérieures.",
|
||||
"help.sketch.point3d": "Place un point dans l'espace, avec Esquisse 3D activée. À utiliser comme référence pour des courbes et trajectoires 3D.",
|
||||
"help.sketch.polygon.circumscribed": "Dessine un polygone régulier autour d'un cercle, dimensionné du centre au milieu d'un côté. À utiliser lorsque la largeur sur plats est connue, comme pour un écrou hexagonal.",
|
||||
"help.sketch.polygon.edge": "Dessine un polygone régulier à partir des deux extrémités d'un côté. À utiliser lorsque la longueur et la position d'un côté sont connues.",
|
||||
"help.sketch.polygon.inscribed": "Dessine un polygone régulier à l'intérieur d'un cercle, dimensionné du centre à un coin. À utiliser lorsque la dimension sur coins est connue.",
|
||||
"help.sketch.project": "Amène des arêtes du modèle, des faces, des sommets et des courbes d'autres esquisses dans cette esquisse, projetés sur son plan. Ils restent liés et suivent le modèle lorsqu'il change.",
|
||||
"help.sketch.project_to_surface": "Projette des courbes d'autres esquisses selon la normale de cette esquisse sur une face, sous forme de courbes 3D liées. À utiliser pour poser un logo, une ligne de partage ou une trajectoire sur une surface courbe.",
|
||||
"help.sketch.rectangle.center": "Dessine un rectangle à partir de son centre et d'un coin. À utiliser pour des contours centrés sur un point, comme l'origine.",
|
||||
"help.sketch.rectangle.rounded": "Dessine un rectangle dont les quatre coins sont arrondis au même rayon. À utiliser pour des plaques, des couvercles et des ouvertures à coins arrondis.",
|
||||
"help.sketch.rectangle.three_point": "Dessine un rectangle à partir d'un côté et d'un point du côté opposé. À utiliser pour des rectangles inclinés par rapport aux axes de l'esquisse.",
|
||||
"help.sketch.rectangle.two_point": "Dessine un rectangle à partir de deux coins opposés. À utiliser pour les contours de plaques, de blocs et de poches.",
|
||||
"help.sketch.scale": "Met à l'échelle des courbes et points d'esquisse selon un facteur autour d'un point de base. À utiliser pour redimensionner une partie d'une esquisse sans la redessiner.",
|
||||
"help.sketch.slot.arc_center": "Dessine une rainure courbe de largeur donnée le long d'un arc défini par son centre, son début et sa fin. À utiliser pour des rainures de réglage autour d'un pivot.",
|
||||
"help.sketch.slot.arc_three_point": "Dessine une rainure courbe de largeur donnée le long d'un arc passant par trois points. À utiliser lorsque le tracé de la rainure doit passer par des points donnés.",
|
||||
"help.sketch.slot.center_point": "Dessine une rainure droite de largeur donnée de son centre au centre d'un arc d'extrémité. À utiliser pour des rainures centrées sur un point.",
|
||||
"help.sketch.slot.center_to_center": "Dessine une rainure droite de largeur donnée entre les centres de ses deux arcs d'extrémité. À utiliser pour des rainures cotées de centre à centre, comme les rainures de réglage de vis.",
|
||||
"help.sketch.slot.overall": "Dessine une rainure droite de largeur donnée d'une extrémité extérieure à l'autre. À utiliser lorsque c'est la longueur hors tout de la rainure qui compte.",
|
||||
"help.sketch.spline.control_point": "Dessine une spline cubique façonnée par des points de contrôle qu'elle ne traverse pas. À utiliser pour des courbes douces et fluides que vous façonnez en déplaçant les points.",
|
||||
"help.sketch.spline.control_point_5": "Dessine une spline de degré 5 façonnée par des points de contrôle. À utiliser lorsque des courbes doivent se raccorder avec une courbure plus douce, par exemple sur des surfaces de style visibles.",
|
||||
"help.sketch.spline.fit_point": "Dessine une spline douce qui passe par chaque point que vous placez, ouverte ou fermée. À utiliser pour décalquer une forme à travers des points connus.",
|
||||
"help.sketch.spline3d": "Dessine une courbe douce dans l'espace par des points, avec Esquisse 3D activée. À utiliser pour des trajectoires de balayage et des cheminements de câbles qui sortent du plan d'esquisse.",
|
||||
"help.sketch.spun_profile": "Crée dans le plan d'esquisse le contour qu'un corps balaie en tournant autour d'un axe. À utiliser pour obtenir la silhouette d'une pièce tournée pour une révolution ou un dessin.",
|
||||
"help.sketch.text": "Écrit du texte dans l'esquisse avec une police, une hauteur et un angle au choix ; ses mots peuvent suivre des paramètres. À extruder ou embosser pour des étiquettes et des lettrages.",
|
||||
"help.sketch.transform": "Met à l'échelle, fait tourner et déplace de la géométrie d'esquisse autour d'un centre en une seule étape. À utiliser pour repositionner et redimensionner une sélection d'un coup.",
|
||||
"help.sketch.trim": "Supprime le morceau d'une courbe entre les croisements les plus proches autour de l'endroit où vous cliquez. À utiliser pour nettoyer des lignes qui se chevauchent et obtenir un profil fermé.",
|
||||
"help.solid.align": "Déplace les corps sélectionnés pour qu'un de leurs points, faces ou plans rejoigne un point, une face ou un plan ailleurs. À utiliser pour poser une pièce contre une autre sans mesurer le décalage.",
|
||||
"help.solid.boundary_fill": "Crée des solides à partir des régions délimitées par des corps outils qui se chevauchent, en ne remplissant que les régions que vous choisissez. À utiliser pour construire une forme à partir de l'espace que plusieurs corps délimitent ensemble.",
|
||||
"help.solid.bounding_solid": "Crée un parallélépipède qui englobe les corps sélectionnés, avec une marge facultative. À utiliser pour voir l'encombrement d'une pièce, ou comme brut dans lequel elle est usinée.",
|
||||
"help.solid.box": "Crée un parallélépipède à partir d'une longueur, d'une largeur et d'une hauteur, depuis un coin ou centré sur un point. À utiliser pour un bloc rapide, un volume provisoire ou un corps dans lequel tailler des fonctions.",
|
||||
"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.coil": "Crée un corps hélicoïdal, comme un ressort, à partir d'un diamètre, d'un nombre de tours, d'une hauteur ou d'un pas et d'une section ronde ou carrée. À utiliser pour des ressorts, des spires et des nervures hélicoïdales.",
|
||||
"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.cone": "Crée un cône à partir d'un cercle de base et d'une hauteur, ou un tronc de cône lorsque le sommet a aussi un rayon. À utiliser pour des conicités, des entonnoirs, des fraisages et des pointes.",
|
||||
"help.solid.cylinder": "Crée un cylindre à partir d'un rayon ou d'un diamètre et d'une hauteur, posé sur un point selon un axe. À utiliser pour des arbres, des goupilles, des bossages et des bruts ronds.",
|
||||
"help.solid.draft": "Incline les faces sélectionnées de l'angle indiqué, mesuré depuis un plan neutre. À utiliser sur les pièces moulées ou coulées pour qu'elles sortent du moule.",
|
||||
"help.solid.emboss": "Fait saillir des profils d'esquisse, comme du texte ou un logo, hors d'une face, ou les enfonce dedans. À utiliser pour des lettrages, des étiquettes et des marquages sur des faces planes ou rondes.",
|
||||
"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.",
|
||||
@@ -1408,20 +1645,62 @@
|
||||
"help.solid.move": "Déplace ou fait tourner les corps sélectionnés, ou une copie, selon la distance et l'angle indiqués. À utiliser pour repositionner un corps ou en créer un second ailleurs.",
|
||||
"help.solid.offset_face": "Pousse les faces sélectionnées vers l'intérieur ou l'extérieur de la distance indiquée, et les faces voisines suivent. À utiliser pour épaissir une paroi, creuser une poche ou rehausser un bossage sans modifier l'esquisse.",
|
||||
"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.path": "Copie des fonctions ou des corps le long d'une trajectoire d'esquisse, selon un nombre et un espacement. À utiliser pour des trous le long d'une arête courbe ou des pièces le long d'un rail.",
|
||||
"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.pipe": "Crée un tube rond le long d'une trajectoire de courbes d'esquisse, plein ou creux avec une épaisseur de paroi. À utiliser pour des tubes, des fils, des câbles et des rails.",
|
||||
"help.solid.press_pull": "Ajoute ou retire de la matière quand on fait glisser une face, ou arrondit des arêtes quand on les fait glisser. À utiliser pour des modifications rapides directement sur le modèle.",
|
||||
"help.solid.remove": "Retire les corps sélectionnés de la conception à partir de ce point de la chronologie ; les étapes précédentes les gardent. À utiliser pour écarter un corps d'aide, comme un outil de découpe devenu inutile.",
|
||||
"help.solid.replace_face": "Amène les faces planes sélectionnées sur un plan ou une face parallèle, et le corps s'allonge ou se raccourcit pour la rejoindre. À utiliser pour mettre une face au ras d'une autre pièce ou d'un plan de construction.",
|
||||
"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.rib": "Ajoute une paroi mince le long d'une courbe d'esquisse ouverte, prolongée jusqu'aux faces suivantes du corps. À utiliser pour rigidifier des parois et des coins, par exemple dans des pièces moulées ou imprimées.",
|
||||
"help.solid.scale": "Agrandit ou réduit les corps sélectionnés d'un facteur, identique dans toutes les directions ou propre à chaque axe. À utiliser pour redimensionner une forme importée ou compenser un retrait.",
|
||||
"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.sphere": "Crée une sphère à partir d'un rayon ou d'un diamètre autour d'un centre. À utiliser pour des billes, des boutons et des extrémités arrondies.",
|
||||
"help.solid.split_body": "Scinde un corps en corps séparés avec un plan, la face d'un autre corps ou une ligne d'esquisse. À utiliser pour obtenir des pièces imprimées, usinées ou colorées séparément.",
|
||||
"help.solid.split_face": "Scinde les faces sélectionnées le long d'un plan, de la face d'un autre corps ou de courbes d'esquisse, sans scinder le corps. À utiliser pour donner à une partie de face sa propre dépouille, couleur ou décalage.",
|
||||
"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.solid.thread": "Ajoute un filetage métrique ISO sur une face cylindrique, soit sous forme de filetage cosmétique, soit taillé dans le corps. À utiliser pour des boulons, des trous taraudés et des pièces vissées imprimées.",
|
||||
"help.solid.torus": "Crée un anneau à partir d'un grand rayon autour de son axe et d'un petit rayon pour sa section ronde. À utiliser pour des joints toriques, des poignées et des perles rondes.",
|
||||
"help.solid.web": "Ajoute une paroi mince le long de chaque courbe d'esquisse ouverte, chacune prolongée jusqu'aux faces suivantes du corps. À utiliser pour une grille de parois de soutien à l'intérieur de boîtiers et de plateaux.",
|
||||
"help.surface.blend": "Crée une surface qui relie les arêtes de deux corps, en tangence ou en continuité de courbure avec les deux. À utiliser pour joindre en douceur deux surfaces à travers un écart.",
|
||||
"help.surface.boundary": "Crée une surface remplissant une boucle de deux à quatre courbes d'esquisse, ou un réseau de courbes. À utiliser pour des panneaux de forme libre définis par leurs bords.",
|
||||
"help.surface.extend": "Prolonge une surface au-delà de ses bords, d'une distance ou jusqu'à un plan ou une face. À utiliser pour que des surfaces se chevauchent avant de les ajuster ou de les coudre.",
|
||||
"help.surface.extrude": "Pousse des courbes ou profils d'esquisse sur une distance pour former une surface, parois seulement, sans fonds. À utiliser pour des parois minces et pour des outils de coupe ou de partage.",
|
||||
"help.surface.fair": "Lisse l'intérieur de faces de forme libre tandis que leurs bords restent en place. À utiliser pour atténuer les bosses et les reflets ondulés d'une surface.",
|
||||
"help.surface.loft": "Crée une surface qui passe par deux sections ou plus dans l'ordre, sans fonds. À utiliser pour des coques et des transitions entre des formes différentes.",
|
||||
"help.surface.match": "Reconstruit le bord d'une surface pour qu'il rejoigne le bord d'un autre corps, avec une tangence ou une courbure concordante. À utiliser pour fermer en douceur un raccord entre deux surfaces.",
|
||||
"help.surface.offset": "Crée une copie de faces à une distance le long de leurs normales, sous forme de nouveau corps surfacique. À utiliser pour des peaux, des jeux ou l'autre côté d'une paroi.",
|
||||
"help.surface.patch": "Remplit une région d'esquisse ou une boucle fermée d'arêtes par une face, plane ou courbe et douce. À utiliser pour boucher des trous dans un corps surfacique.",
|
||||
"help.surface.reverse_normal": "Retourne des faces d'un corps surfacique, de sorte que leur côté extérieur soit orienté dans l'autre sens. À utiliser avant d'épaissir ou de coudre lorsque les faces pointent vers l'intérieur.",
|
||||
"help.surface.revolve": "Fait tourner des courbes ou profils d'esquisse autour d'un axe pour former une surface, sans fonds. À utiliser pour des coques rondes comme des abat-jour, des bols et des buses.",
|
||||
"help.surface.ruled": "Crée une bande de droites à partir d'une chaîne d'arêtes, selon la normale de la face, la tangente ou un angle. À utiliser pour des parois de dépouille, des dégagements et des surfaces de joint.",
|
||||
"help.surface.stitch": "Coud des corps surfaciques le long de leurs arêtes communes, en un solide lorsqu'ils se referment. À utiliser pour transformer un ensemble de surfaces en un seul corps.",
|
||||
"help.surface.sweep": "Déplace des courbes ou profils d'esquisse le long d'une trajectoire pour former une surface, sans fonds. À utiliser pour des gouttières, des moulures et d'autres formes minces qui suivent une trajectoire.",
|
||||
"help.surface.thicken": "Donne une épaisseur à un corps surfacique et le transforme en solide. À utiliser pour fabriquer des pièces imprimables ou usinables à partir de surfaces.",
|
||||
"help.surface.trim": "Coupe une surface avec un plan, une autre surface ou des courbes d'esquisse, et garde le côté que vous choisissez. À utiliser pour ajuster des surfaces à leur forme avant de les coudre.",
|
||||
"help.surface.unstitch": "Sépare des faces d'un corps en corps surfaciques distincts ; un solide se décompose en surfaces. À utiliser pour retravailler des faces isolées d'un corps.",
|
||||
"help.surface.untrim": "Supprime des trous d'une face, ou rétablit son contour extérieur aux limites naturelles de la surface. À utiliser pour retrouver la surface complète après un ajustement.",
|
||||
"help.third_party": "Licences tierces",
|
||||
"help.timeline.compute_all": "Recalcule toutes les fonctions de la chronologie depuis le début. À utiliser pour rafraîchir le modèle et voir quelles fonctions échouent.",
|
||||
"help.timeline.delete": "Supprime les fonctions sélectionnées de la chronologie ; supprimer une esquisse supprime aussi les fonctions construites à partir d'elle. À utiliser pour retirer une étape devenue inutile.",
|
||||
"help.timeline.dependents": "Liste les fonctions qui seraient supprimées avec une fonction, et celles qui échoueraient sans elle. À utiliser avant de supprimer ou de modifier une fonction.",
|
||||
"help.timeline.play": "Rejoue les fonctions restantes une par une, avec une pause d'une demi-seconde après chaque reconstruction. Relancez cette commande ou appuyez sur Échap pour arrêter ; Aller à la fin saute immédiatement.",
|
||||
"help.view.apply": "Oriente la vue selon une direction standard comme face, dessus ou iso, ou selon une vue nommée enregistrée. À utiliser pour regarder le modèle sous un angle fixe.",
|
||||
"help.view.delete": "Supprime une vue nommée enregistrée. Le modèle n'est pas modifié.",
|
||||
"help.view.fit": "Zoome et déplace la vue pour que tout le modèle, ou la sélection, remplisse la vue. À utiliser lorsque le modèle est sorti du champ.",
|
||||
"help.view.grid": "Affiche ou masque la grille dans la vue 3D. À utiliser comme repère de taille et d'orientation pendant la modélisation.",
|
||||
"help.view.ground_shadow": "Affiche ou masque l'ombre du modèle sur le sol. À utiliser pour juger plus facilement la hauteur et la position.",
|
||||
"help.view.home": "Remet la vue dans la direction d'accueil et ajuste le modèle. À utiliser pour revenir rapidement à une vue connue.",
|
||||
"help.view.list": "Liste les vues nommées enregistrées dans la conception. À utiliser pour retrouver une vue enregistrée plus tôt.",
|
||||
"help.view.navigate": "Fait tourner, déplace ou zoome la vue par étapes, comme un quart de tour à gauche. À utiliser pour déplacer la caméra avec précision plutôt qu'en faisant glisser.",
|
||||
"help.view.navigation_bar": "Affiche ou masque la barre de navigation en bas de la vue 3D. Masquez-la pour gagner de la place, affichez-la pour accéder aux outils de vue à la souris.",
|
||||
"help.view.rename": "Donne un nouveau nom à une vue nommée enregistrée. La vue elle-même reste inchangée.",
|
||||
"help.view.save": "Enregistre la position actuelle de la caméra comme vue nommée dans la conception. À utiliser pour revenir plus tard au même angle, par exemple pour des captures d'écran.",
|
||||
"help.view.spin": "Fait orbiter la vue autour du modèle en continu jusqu'au prochain clic ou à la prochaine touche. À utiliser pour présenter un modèle sous tous les angles.",
|
||||
"help.view.tool": "Verrouille un outil de caméra, orbite, déplacement ou zoom, de sorte que faire glisser avec le bouton gauche de la souris déplace la vue jusqu'à ce que vous le désactiviez. À utiliser sur un trackpad ou sans bouton central de souris.",
|
||||
"help.view.visual_style": "Change le mode d'affichage du modèle : ombré avec ou sans arêtes, filaire, ou avec arêtes cachées. Utilisez le filaire pour voir à l'intérieur, l'ombré pour un rendu net.",
|
||||
"history.sketch_under_constrained": "Sous-contraint ({count} degrés de liberté)",
|
||||
"history.sketch_under_constrained_one": "Sous-contraint ({count} degré de liberté)",
|
||||
"hole_presets.confirm_delete": "Confirmer la suppression",
|
||||
"hole_presets.delete": "Supprimer",
|
||||
"hole_presets.delete_prompt": "Supprimer ce préréglage personnel ? Les perçages existants ne changent pas.",
|
||||
@@ -1596,6 +1875,10 @@
|
||||
"material.selection.mixed": "Matériaux différents",
|
||||
"measure.at": "À",
|
||||
"measure.item": "Élément {number}",
|
||||
"measure.quick_copied": "Mesure copiée",
|
||||
"measure.quick_copy": "Cliquer pour copier la mesure",
|
||||
"measure.quick_edges": "{count} arêtes",
|
||||
"measure.quick_faces": "{count} faces",
|
||||
"messages.ago_h": "il y a {n} h",
|
||||
"messages.ago_min": "il y a {n} min",
|
||||
"messages.ago_s": "il y a {n} s",
|
||||
@@ -1748,6 +2031,11 @@
|
||||
"not a move": "pas un déplacement",
|
||||
"not a number": "pas un nombre",
|
||||
"open a sketch first": "ouvrir d'abord une esquisse",
|
||||
"parameters.apply_live": "Appliquer chaque modification en quittant son champ",
|
||||
"parameters.apply_live_tip": "Désactivé : les modifications attendent Appliquer ou OK, puis le modèle est reconstruit une seule fois pour toutes. Annuler ou Échap les abandonne.",
|
||||
"parameters.held": "{count} modifications en attente d’Appliquer",
|
||||
"parameters.held_hint": "Modifiez les expressions et les unités. Les modifications attendent Appliquer ou OK.",
|
||||
"parameters.held_one": "1 modification en attente d’Appliquer",
|
||||
"parts.automatic_name": "automatique",
|
||||
"parts.dimension.across_flats": "Sur plats",
|
||||
"parts.dimension.bore": "Alésage",
|
||||
@@ -1766,10 +2054,19 @@
|
||||
"parts.family.button_head_cap_screw": "Vis à tête bombée à six pans creux",
|
||||
"parts.family.dowel_pin": "Goupille cylindrique",
|
||||
"parts.family.hex_bolt": "Vis à tête hexagonale",
|
||||
"parts.family.hex_cap_screw_inch": "Vis à tête hexagonale (pouces)",
|
||||
"parts.family.hex_nut": "Écrou hexagonal",
|
||||
"parts.family.hex_nut_inch": "Écrou hexagonal (pouces)",
|
||||
"parts.family.set_screw": "Vis sans tête à six pans creux, bout plat",
|
||||
"parts.family.socket_head_cap_screw": "Vis à tête cylindrique à six pans creux",
|
||||
"parts.family.washer": "Rondelle plate",
|
||||
"parts.family.washer_sae": "Rondelle plate, SAE (étroite)",
|
||||
"parts.family.washer_uss": "Rondelle plate, USS (large)",
|
||||
"parts.holes": "Trous (facultatif)",
|
||||
"parts.placement_hint": "choisissez un ou plusieurs trous pour y loger une pièce chacun, ou OK la place à l'origine",
|
||||
"parts.system": "Système d'unités",
|
||||
"parts.system.inch": "Pouces",
|
||||
"parts.system.metric": "Métrique",
|
||||
"pattern.alignment_2": "Alignement 2",
|
||||
"pattern.alignment_hint": "La quantité inclut l’original. Le centrage répartit les copies de chaque côté ; si la quantité est paire, l’inversion choisit le côté qui reçoit la copie supplémentaire.",
|
||||
"pattern.direction_1": "Direction 1",
|
||||
@@ -1810,6 +2107,8 @@
|
||||
"prefs.anti_aliasing_note": "Adoucit les bords crénelés.",
|
||||
"prefs.applies_on_restart": "Appliqué au redémarrage",
|
||||
"prefs.assistant": "Assistant",
|
||||
"prefs.auto_operation": "Choisir l'opération automatiquement",
|
||||
"prefs.auto_operation_note": "Les boîtes de dialogue des fonctions choisissent Joindre, Nouveau corps ou Couper d'après ce que touche la nouvelle matière, tant que vous n'en choisissez pas une vous-même. Désactivé : elles gardent l'opération avec laquelle elles s'ouvrent.",
|
||||
"prefs.axes": "Axes",
|
||||
"prefs.axis": "Axe",
|
||||
"prefs.bodies_folders": "Dossiers Corps",
|
||||
@@ -1820,6 +2119,10 @@
|
||||
"prefs.bottom_centre": "Bas centre",
|
||||
"prefs.bottom_right": "Bas droite",
|
||||
"prefs.browser": "Navigateur",
|
||||
"prefs.browser_components_open": "Composants dépliés au départ",
|
||||
"prefs.browser_components_open_note": "Chaque composant du navigateur commence déplié, comme avant, au lieu de rester replié jusqu'à ce que vous le dépliiez.",
|
||||
"prefs.browser_eye_left": "Œil à côté de la flèche de dépliage",
|
||||
"prefs.browser_eye_left_note": "Les lignes du navigateur affichent leur œil d'affichage et de masquage à côté de la flèche de dépliage, comme avant, au lieu d'une colonne à l'extrémité droite des lignes.",
|
||||
"prefs.browser_reveal_on_hover": "Révéler au survol",
|
||||
"prefs.browser_show_note": "Affiché garde le navigateur à l'écran. Révéler au survol le cache jusqu'à ce que le pointeur passe sur sa place ou sur le fin onglet à son bord gauche, et il reste ouvert tant que le pointeur est dedans ou qu'un de ses menus est ouvert.",
|
||||
"prefs.browser_shown": "Affiché",
|
||||
@@ -1870,6 +2173,8 @@
|
||||
"prefs.graphics_card_restart": "Prend effet après le redémarrage de SolidCraft. WGPU_ADAPTER_NAME prévaut sur ce choix.",
|
||||
"prefs.graphics_note": "Compromis entre vitesse et fluidité. Chaque choix montre son effet sur un exemple.",
|
||||
"prefs.graphics_presets_note": "Les préréglages définissent ensemble les choix ci-dessous.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "Masquer les symboles de contraintes en cas de zoom arrière",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "Les symboles de contraintes quittent la vue lorsque le zoom arrière sur l'esquisse est assez important pour qu'ils l'encombrent. Désactivé : ils gardent une taille fixe à l'écran quel que soit le zoom.",
|
||||
"prefs.history": "Historique",
|
||||
"prefs.history_origin_bodies": "Afficher l'origine et les corps en haut",
|
||||
"prefs.history_origin_bodies_note": "L'historique à gauche ou sous le navigateur commence par le dossier Corps et l'Origine du composant actif, avec leurs yeux et leurs menus comme dans le navigateur. L'historique en bas ne change pas.",
|
||||
@@ -1888,6 +2193,8 @@
|
||||
"prefs.interface_language": "Langue de l'interface",
|
||||
"prefs.interface_note": "L'apparence de SolidCraft et la place de ses éléments.",
|
||||
"prefs.interval_minutes": "{n} min",
|
||||
"prefs.keep_work_on_tab_switch": "Garder dialogues et outils ouverts en changeant de conception",
|
||||
"prefs.keep_work_on_tab_switch_note": "Chaque onglet de conception garde son dialogue ou son outil ouvert, avec ce que vous y avez sélectionné et saisi, ainsi que son onglet de barre d'outils. Vous les retrouvez en revenant à la conception. Désactivé : changer de conception les annule et affiche l'onglet Modèle.",
|
||||
"prefs.labels": "Libellés",
|
||||
"prefs.layout": "Disposition",
|
||||
"prefs.layout_note": "Compacte : ancre les panneaux aux bords de la fenêtre. La transparence du navigateur est disponible en mode Flottante. Changez le côté de l'assistant dans la page Assistant.",
|
||||
@@ -1908,6 +2215,9 @@
|
||||
"prefs.max_file_size_step": "Fichiers STEP et d’importation",
|
||||
"prefs.max_file_size_web": "L’application web ouvre les fichiers STEP et d’importation jusqu’à 512 Mo et les conceptions jusqu’à 256 Mo : un onglet du navigateur ne peut pas contenir de fichiers plus volumineux. L’application de bureau permet de relever ces limites.",
|
||||
"prefs.megabytes": "Mo",
|
||||
"prefs.menu_grid": "Grandes icônes",
|
||||
"prefs.menu_list": "Liste",
|
||||
"prefs.menus": "Menus",
|
||||
"prefs.minutes": "minutes",
|
||||
"prefs.minutes_suffix": " min",
|
||||
"prefs.mouse": "Souris et pavé tactile",
|
||||
@@ -1927,10 +2237,16 @@
|
||||
"prefs.origin_plane_names": "Noms des plans d'origine",
|
||||
"prefs.origin_plane_names_note": "Noms affichés dans le navigateur, les boîtes de dialogue et les listes. {top} est XY, {front} est XZ et {right} est YZ, avec Z vers le haut. Les commandes, les scripts et les conceptions enregistrées gardent XY, XZ et YZ.",
|
||||
"prefs.overall_speed": "Vitesse globale",
|
||||
"prefs.parameter_list_rows": "Paramètres listés",
|
||||
"prefs.parameter_list_rows_tip": "La flèche bas dans un champ de valeur vide liste les paramètres, favoris en premier, jusqu'à ce nombre. Moins allège la liste dans un gros design.",
|
||||
"prefs.persistent_ribbon_pressed": "Mettre en surbrillance les commandes actives du ruban",
|
||||
"prefs.persistent_ribbon_pressed_help": "Met en surbrillance les commandes du ruban tant que leurs effets sont actifs. Désactivez cette option pour conserver l'apparence neutre précédente et l'aide des commandes.",
|
||||
"prefs.perspective_fov": "Champ de vision",
|
||||
"prefs.placement": "Placement",
|
||||
"prefs.prefer_free_sketch_points": "Privilégier les points d'esquisse déplaçables quand des points se superposent",
|
||||
"prefs.preview_choices": "Aperçu de vos choix",
|
||||
"prefs.print_face_highlight": "Mise en évidence de la face d’impression",
|
||||
"prefs.print_face_highlight_tip": "Intensité de la teinte et du contour de la face d’impression d’un corps dans la vue. 0 % n’affiche aucune mise en évidence.",
|
||||
"prefs.profile_active": "Profil actif",
|
||||
"prefs.profile_applied": "Profil appliqué.",
|
||||
"prefs.profile_changed": "Les réglages ont changé depuis l'enregistrement du profil. Enregistrer les y conserve.",
|
||||
@@ -1952,6 +2268,8 @@
|
||||
"prefs.quality_fastest": "le plus rapide",
|
||||
"prefs.quality_slowest": "le plus lent",
|
||||
"prefs.quality_smoother": "plus fluide",
|
||||
"prefs.quick_measure": "Mesure rapide dans le coin de la vue",
|
||||
"prefs.quick_measure_note": "La taille de la sélection, dans le coin inférieur droit de la vue : la longueur d'une arête, ou Ø et R pour une arête ronde, l'aire d'une face, la position d'un point, la distance et l'angle entre deux éléments, le total de plusieurs arêtes ou faces. Un clic la copie.",
|
||||
"prefs.render_previous_geometry_budget": "Utiliser l'ancien budget de géométrie de l'aperçu Rendu",
|
||||
"prefs.render_previous_geometry_budget_help": "Rétablir l'ancienne limite fixe de géométrie (512 Mio sur un ordinateur 64 bits, 128 Mio ailleurs) au lieu du budget basé sur l'appareil.",
|
||||
"prefs.reports_saved": "Enregistrés : {n}",
|
||||
@@ -1965,6 +2283,8 @@
|
||||
"prefs.search_empty": "Aucun paramètre correspondant",
|
||||
"prefs.select": "Sélectionner",
|
||||
"prefs.select_one_item": "Sélectionner un élément",
|
||||
"prefs.select_other_hidden": "Sélectionner la géométrie masquée avec Sélectionner un autre",
|
||||
"prefs.select_other_hidden_help": "Activé : inclure les corps et les esquisses masqués ainsi que les arêtes occultées près du pointeur. Désactivé : utiliser la liste d'origine de la géométrie visible sous le pointeur.",
|
||||
"prefs.selecting": "Sélection",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Maj ajoute seulement ; Ctrl / Cmd bascule",
|
||||
@@ -1976,31 +2296,44 @@
|
||||
"prefs.sketch_align_points_note": "Pendant le dessin, un point s'aligne à la fois horizontalement sur un point d'esquisse et verticalement sur un autre, avec un guide en pointillés, et le clic conserve les deux alignements. Désactivé : uniquement sur le point précédent, un axe à la fois.",
|
||||
"prefs.sketch_conflict_colour": "Géométrie en conflit dans la couleur de conflit",
|
||||
"prefs.sketch_conflict_colour_note": "Tant que les contraintes d'une esquisse sont en conflit, les courbes et les points que retient une contrainte en échec sont aussi dessinés dans la couleur de conflit, et pas seulement son symbole ou sa cote. Désactivé : ils gardent leurs couleurs habituelles.",
|
||||
"prefs.sketch_construction_lines": "Outils étendus de droites de construction",
|
||||
"prefs.sketch_exit_view": "À la fin d'une esquisse",
|
||||
"prefs.sketch_exit_view.keep": "Garder la vue actuelle",
|
||||
"prefs.sketch_exit_view.previous": "Revenir à la vue précédente",
|
||||
"prefs.sketch_exit_view.unless_rotated": "Revenir, sauf si la vue a été tournée",
|
||||
"prefs.sketch_exit_view_note": "Où va la vue après Terminer l'esquisse, Annuler ou un double Échap.",
|
||||
"prefs.sketch_open_look_at": "Se placer face à l'esquisse à son ouverture",
|
||||
"prefs.sketch_open_look_at_note": "Désactivé : créer ou modifier une esquisse laisse la vue telle quelle. Vue normale place la vue face à l'esquisse quand vous le souhaitez.",
|
||||
"prefs.sketch_recent_snap": "Accrocher automatiquement aux centres et points milieux récents",
|
||||
"prefs.sketch_recent_snap_note": "Pendant le dessin, le curseur s'accroche là où se croisent les guides des deux derniers centres de cercle ou points milieux de segment droit. Désactivé : ces guides restent purement visuels et le curseur ne bouge pas.",
|
||||
"prefs.sketch_selection": "Esquisse et sélection",
|
||||
"prefs.sketch_selection_heading": "Esquisse et sélection",
|
||||
"prefs.sketch_selection_note": "Comment les esquisses s'accrochent et comment les clics sélectionnent.",
|
||||
"prefs.sketch_state_icons": "Icônes d'esquisse dans la couleur de leur état",
|
||||
"prefs.sketch_state_icons_note": "Dans l'historique et le navigateur, l'icône d'une esquisse prend la couleur de son état (sous-contraint, entièrement contrainte ou en conflit) et son infobulle nomme cet état. Désactivé : toutes les esquisses ont la même icône.",
|
||||
"prefs.sketch_tab_always": "Toujours afficher l'onglet Esquisse",
|
||||
"prefs.sketch_tab_always_note": "L'onglet Esquisse reste dans le ruban, juste après Modèle, tant qu'aucune esquisse n'est ouverte. Ses outils de dessin démarrent alors une esquisse sur le plan ou la face sélectionnés, ou sur le plan que vous choisissez ensuite, avec l'outil prêt à l'emploi.",
|
||||
"prefs.sketch_tracking_guides": "Guides d'alignement sur le centre et le point milieu",
|
||||
"prefs.sketch_tracking_guides_note": "Survolez le centre d'un cercle ou le point milieu d'un segment droit, puis déplacez le pointeur horizontalement ou verticalement pour afficher un guide pointillé temporaire.",
|
||||
"prefs.sketch_tracking_points": "Guides d'alignement depuis les extrémités et les coins pendant le dessin",
|
||||
"prefs.sketch_tracking_points_note": "Quand un outil de dessin attend un point, survoler l'extrémité d'une ligne ou d'un arc, le coin d'un rectangle ou un point libre affiche le même guide pointillé temporaire qu'un centre ou un point milieu. Désactivé : seulement les centres et les points milieux.",
|
||||
"prefs.sketching": "Esquisse",
|
||||
"prefs.snap_drawing_note": "Pendant le dessin ou le déplacement d'un point d'esquisse.",
|
||||
"prefs.source": "Source",
|
||||
"prefs.speed": "Vitesse",
|
||||
"prefs.stand_up_note": "Après l'ouverture d'un fichier STEP ou IGES en Z vers le haut, proposer un quart de tour autour de X.",
|
||||
"prefs.stand_up_unknown": "Pour les fichiers de programmes inconnus",
|
||||
"prefs.stand_up_unknown.ask": "Demander dans une boîte de dialogue",
|
||||
"prefs.stand_up_unknown.note": "Afficher une note",
|
||||
"prefs.stand_up_unknown.off": "Ne pas proposer",
|
||||
"prefs.stand_up_unknown_note": "Comment la proposition est faite quand l'en-tête du fichier ne nomme aucun programme connu pour enregistrer en Y vers le haut. Une note ne s'affiche que lorsque le fichier semble couché sur le dos ; Demander dans une boîte de dialogue pose la question pour chaque fichier de ce genre, comme avant.",
|
||||
"prefs.status_bar": "Barre d'état",
|
||||
"prefs.status_bar_note": "Une barre fine en bas de la fenêtre indique ce qui est en cours de modification, l'état de l'esquisse ouverte par sa couleur et les unités de la conception ; un clic sur les unités permet de les changer. L'état de l'esquisse n'apparaît alors plus dans le coin de la vue.",
|
||||
"prefs.status_done": "Terminé",
|
||||
"prefs.status_error": "Erreur",
|
||||
"prefs.status_selected": "Sélectionné",
|
||||
"prefs.status_warning": "Avertissement",
|
||||
"prefs.structure.scale_percent": "{percent} %",
|
||||
"prefs.supersampling_brief": "Dessine la vue en plus grand puis la réduit par filtrage : très net, mais plus lourd.",
|
||||
"prefs.tab_accent_note": "La couleur d'accent suit l'onglet actif",
|
||||
"prefs.theme_file": "fichier de thème",
|
||||
@@ -2012,6 +2345,16 @@
|
||||
"prefs.theme_follow_off": "Désactivé",
|
||||
"prefs.theme_follow_stop": "Arrêter le suivi",
|
||||
"prefs.theme_import_hint": "Cliquez pour choisir un fichier de thème, déposez-en un ici, ou appuyez sur Ctrl+V (Cmd+V sur Mac) avec un thème copié",
|
||||
"prefs.theme_option_opacity": "Opacité",
|
||||
"prefs.theme_option_opacity_help": "Règle l'opacité des panneaux flottants. Un seuil de lisibilité protège le texte placé sur les couleurs de la vue 3D.",
|
||||
"prefs.theme_option_tab_colours": "Couleurs des onglets",
|
||||
"prefs.theme_option_tab_colours_help": "Utilise les couleurs de panneau et de vue 3D de ce thème pour chaque onglet du ruban.",
|
||||
"prefs.theme_option_tab_fade_ms": "Transition des onglets",
|
||||
"prefs.theme_option_tab_fade_ms_help": "Fait passer progressivement les couleurs du panneau et de la vue 3D au changement d'onglet. Zéro bascule instantanément.",
|
||||
"prefs.theme_options_badge": "Options",
|
||||
"prefs.theme_options_more": "Plus · {count}",
|
||||
"prefs.theme_options_reset": "Réinitialiser",
|
||||
"prefs.theme_options_unavailable": "Indisponible sur cet appareil",
|
||||
"prefs.theme_paste": "Coller le thème",
|
||||
"prefs.theme_paste_none": "Le presse-papiers ne contient aucun thème SolidCraft",
|
||||
"prefs.timeline_half_hour": "−30 min",
|
||||
@@ -2033,13 +2376,23 @@
|
||||
"prefs.under_cursor": "Sous le curseur",
|
||||
"prefs.units_precision": "Unités et précision",
|
||||
"prefs.updates": "Mises à jour",
|
||||
"prefs.value_boxes_in_view": "Afficher les valeurs à côté de leurs poignées dans la vue",
|
||||
"prefs.value_boxes_in_view_tip": "La valeur d'une commande (distance, rayon…) dans une case à côté de sa poignée, par-dessus le modèle. Désactivé : vous la saisissez dans la fenêtre de la commande, qui prend le clavier après une sélection, et rien ne masque le modèle.",
|
||||
"prefs.view": "Comportement de la vue",
|
||||
"prefs.view_confirm_corner": "OK et Annuler dans le coin de la vue",
|
||||
"prefs.view_confirm_corner_note": "Lorsqu'une boîte de dialogue ou une esquisse est ouverte, une coche et une croix en haut à droite de la vue font la même chose que OK et Annuler, ou terminent et annulent l'esquisse. Le repère de navigation se place en dessous.",
|
||||
"prefs.viewport": "Vue 3D",
|
||||
"prefs.viewport_analysis_status": "Afficher l'état d'analyse de la vue 3D",
|
||||
"prefs.viewport_analysis_status_help": "Afficher les analyses actives au-dessus de la vue 3D, ouvrir leur légende depuis chaque élément et utiliser Échap pour désactiver l'analyse la plus récente. Désactiver pour rétablir les légendes dans les coins et l'ancien comportement d'Échap.",
|
||||
"prefs.viewport_background": "Fond",
|
||||
"prefs.viewport_background_colour": "Couleur",
|
||||
"prefs.viewport_background_theme": "Dégradé du thème",
|
||||
"prefs.viewport_section_status": "Afficher l'état de la coupe dans la vue 3D",
|
||||
"prefs.viewport_section_status_help": "Affiche le réglage de coupe dans la vue 3D. Échap et le bouton actif du ruban le désactivent. Désactivez cette option pour conserver les contrôles de coupe précédents.",
|
||||
"prefs.viewport_status_details": "Détails de la vue 3D modifiables",
|
||||
"prefs.viewport_status_details_help": "Modifie les réglages de l'analyse et de la coupe actives dans des fiches de détails standard. Désactivez cette option pour retrouver les légendes d'analyse en lecture seule et la boîte de dialogue Coupe.",
|
||||
"prefs.viewport_status_summary": "Regrouper les éléments d'état de la vue 3D",
|
||||
"prefs.viewport_status_summary_help": "Regroupe trois éléments d'état de la vue 3D ou plus dans un seul badge de synthèse. Désactivez cette option pour garder tous les éléments sur la ligne complète.",
|
||||
"prefs.wheel": "Molette",
|
||||
"prefs.wheel_trackpad": "Molette et pavé tactile",
|
||||
"prefs.workspace": "Espace de travail",
|
||||
@@ -2918,7 +3271,9 @@
|
||||
"render.output_ready": "Prêt à lancer le rendu",
|
||||
"render.output_run_check": "Lancer la vérification",
|
||||
"render.output_size_limit": "{method} : jusqu'à {dimension} px par côté et {pixels} pixels. Plus grande taille possible : {width} × {height}.",
|
||||
"render.output_size_method_profile": "Pour un rendu en {width} × {height}, choisissez {method} avec le profil de ressources CPU {profile}, ou une taille plus petite.",
|
||||
"render.output_size_ok": "Taille correcte · {width} × {height} px",
|
||||
"render.output_size_profile": "Pour un rendu en {width} × {height}, choisissez le profil de ressources CPU {profile}, ou une taille plus petite.",
|
||||
"render.output_swap_orientation": "Permuter largeur et hauteur",
|
||||
"render.output_tools_inspector": "Modifier la taille et le format dans l'inspecteur Sortie.",
|
||||
"render.output_view_image": "Afficher l'image",
|
||||
@@ -3131,6 +3486,7 @@
|
||||
"render.size_custom": "Personnalisé…",
|
||||
"render.size_full_hd": "Full HD",
|
||||
"render.size_hd": "HD",
|
||||
"render.size_needs_profile": "{size} · nécessite {profile}",
|
||||
"render.size_presets": "Préréglages de taille",
|
||||
"render.size_presets_pending": "Arrive avec l'éditeur Sortie",
|
||||
"render.size_preview": "Aperçu",
|
||||
@@ -3418,6 +3774,24 @@
|
||||
"sheet.flange_type_contour": "Contour",
|
||||
"sheet.flange_type_edge": "Arête",
|
||||
"sheet.no_body": "il n'y a pas de corps de tôlerie : commencez-en un avec Bord tombé > Base, ou transformez une plaque en tôlerie avec Convertir en tôlerie (choisissez sa grande face plane)",
|
||||
"sheet.personal_rules.confirm_delete": "Confirmer la suppression",
|
||||
"sheet.personal_rules.delete": "Supprimer",
|
||||
"sheet.personal_rules.delete_prompt": "Supprimer cette règle personnelle ? Les règles déjà présentes dans les conceptions ne changent pas.",
|
||||
"sheet.personal_rules.full": "La bibliothèque personnelle contient déjà 64 règles.",
|
||||
"sheet.personal_rules.invalid_name": "Saisissez un nom court, sans saut de ligne ni caractère de contrôle.",
|
||||
"sheet.personal_rules.invalid_values": "Complétez les valeurs de la règle : chacune doit pouvoir être calculée dans cette conception, avec une épaisseur positive et un facteur K compris entre 0 et 1.",
|
||||
"sheet.personal_rules.missing": "La règle personnelle sélectionnée n'est plus disponible.",
|
||||
"sheet.personal_rules.name": "Nom de la règle",
|
||||
"sheet.personal_rules.name_exists": "Une règle personnelle porte déjà ce nom. Choisissez-en un autre.",
|
||||
"sheet.personal_rules.name_hint": "Un nom unique",
|
||||
"sheet.personal_rules.overwrite": "Enregistrer les modifications de la règle personnelle sélectionnée",
|
||||
"sheet.personal_rules.rename": "Renommer",
|
||||
"sheet.personal_rules.rule": "Règle personnelle",
|
||||
"sheet.personal_rules.save": "Enregistrer comme nouvelle",
|
||||
"sheet.personal_rules.scope": "Enregistrée dans votre profil, pour toutes les conceptions.",
|
||||
"sheet.personal_rules.select": "Choisir une règle",
|
||||
"sheet.personal_rules.title": "Règles personnelles",
|
||||
"sheet.personal_rules.unsupported": "Cette règle contient des valeurs que cette version ne peut pas utiliser.",
|
||||
"shortcut.json_error": "les paramètres doivent être au format JSON : {error}",
|
||||
"shortcut.no_command": "la commande `{id}` n’existe pas ; le texte avant le JSON doit être un ID de commande",
|
||||
"shortcut.pin": "Épingler en haut de la boîte S",
|
||||
@@ -3454,6 +3828,7 @@
|
||||
"sketch_text.align_right": "Droite",
|
||||
"sketch_text.bold": "Gras",
|
||||
"sketch_text.builtin_font": "{family} (intégrée)",
|
||||
"sketch_text.formula": "Commencez par = pour composer le texte avec des paramètres : ='L' + number(Largeur)",
|
||||
"sketch_text.italic": "Italique",
|
||||
"sketch_text.loading_fonts": "Recherche des polices installées…",
|
||||
"sketch_text.on_path": "Le texte suit sa trajectoire : la courbe le positionne",
|
||||
@@ -3487,10 +3862,7 @@
|
||||
"the two origins are on the same component": "les deux origines se trouvent sur le même composant",
|
||||
"thread.automatic_size": "Taille automatique",
|
||||
"thread.designation": "Désignation",
|
||||
"thread.direction": "Sens du filetage",
|
||||
"thread.iso_metric": "Métrique ISO",
|
||||
"thread.left_hand": "À gauche",
|
||||
"thread.right_hand": "À droite",
|
||||
"thread.select_size": "Sélectionner la taille",
|
||||
"timeline.delete_after_marker_one": "Supprimer 1 fonction après le marqueur d'historique",
|
||||
"timeline.delete_after_marker_other": "Supprimer {count} fonctions après le marqueur d'historique",
|
||||
@@ -3507,8 +3879,10 @@
|
||||
"timeline.move_to_group": "Déplacer les fonctions vers le dossier",
|
||||
"timeline.rename_group": "Renommer le dossier de fonctions",
|
||||
"timeline.reorder_many": "Réordonner les fonctions",
|
||||
"timeline.suppress_features": "Désactiver les fonctions",
|
||||
"timeline.suppress_folders_tip": "Désactiver ou réactiver toutes les fonctions des dossiers et de leurs dossiers imbriqués",
|
||||
"timeline.ungroup": "Supprimer les dossiers de fonctions",
|
||||
"timeline.unsuppress_features": "Réactiver les fonctions",
|
||||
"to the face": "jusqu'à la face",
|
||||
"to the next face": "jusqu'à la face suivante",
|
||||
"toolbar.customize_note": "Onglet {tab}. Cochez une commande pour lui attribuer un bouton dans la barre d’outils. Vous pouvez aussi faire glisser une ligne du menu déroulant d’un panneau vers la barre d’outils, retirer un bouton en le faisant glisser hors de la barre ou utiliser l’épingle d’une ligne du menu déroulant.",
|
||||
@@ -3592,6 +3966,8 @@
|
||||
"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_sketch_orbit": "Faire pivoter les esquisses avec la souris 3D",
|
||||
"ui.3d_mouse_sketch_orbit_tip": "La souris 3D fait aussi pivoter la vue quand une esquisse est ouverte et vue de face, comme la souris. Désactivé : elle déplace et zoome l'esquisse jusqu'à ce que vous incliniez la vue.",
|
||||
"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",
|
||||
@@ -3620,6 +3996,8 @@
|
||||
"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_use_direct": "Utiliser le pilote direct",
|
||||
"ui.3d_mouse_use_direct_tip": "Lire la souris 3D directement (hidraw) au lieu de passer par spacenavd.",
|
||||
"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.",
|
||||
@@ -3658,6 +4036,7 @@
|
||||
"ui.agent_chat_auth_terminal": "Exécuter ceci dans un terminal, puis appuyer de nouveau sur Connecter :",
|
||||
"ui.agent_chat_auth_title": "Authentification auprès de l'agent",
|
||||
"ui.agent_chat_cancel": "Annuler",
|
||||
"ui.agent_chat_cannot_start": "Installé, mais ne démarre pas : {error}",
|
||||
"ui.agent_chat_changes": "Modifications : {n}, en une seule étape d'annulation",
|
||||
"ui.agent_chat_changes_n": "Modifications : {n}",
|
||||
"ui.agent_chat_changes_of": "{n}/{limit} modifications",
|
||||
@@ -3750,9 +4129,12 @@
|
||||
"ui.agent_chat_queued_stopped": "En file d'attente (arrêté, non envoyé)",
|
||||
"ui.agent_chat_refresh": "Actualiser",
|
||||
"ui.agent_chat_refused": "Interdit à l'agent : {reason}",
|
||||
"ui.agent_chat_reinstall": "{agent} est installé ({program}), mais n'a pas démarré quand SolidCraft l'a vérifié : {error}\nSon installation est peut-être incomplète, ou prévue pour un autre type d'ordinateur. Réinstallez-le avec `{install}`, vérifiez qu'il affiche sa version dans un terminal, puis cliquez sur Actualiser.",
|
||||
"ui.agent_chat_remove": "Retirer de la file d'attente",
|
||||
"ui.agent_chat_remove_file": "Retirer de ce message",
|
||||
"ui.agent_chat_remove_imported": "Retirer de ce message ; la pièce reste dans la conception (Annuler la retire)",
|
||||
"ui.agent_chat_reply_cut_off": "La réponse a atteint sa longueur maximale avant la fin : elle est peut-être incomplète. Demandez à l'agent de continuer, ou une réponse plus courte.",
|
||||
"ui.agent_chat_reply_declined": "Le modèle a refusé de répondre à ce message.",
|
||||
"ui.agent_chat_retry": "Réessayer",
|
||||
"ui.agent_chat_right": "Droite",
|
||||
"ui.agent_chat_save": "Enregistrer",
|
||||
@@ -3811,12 +4193,17 @@
|
||||
"ui.ai_copy_suffix": "copie",
|
||||
"ui.ai_custom_url": "URL personnalisée",
|
||||
"ui.ai_default": "Par défaut",
|
||||
"ui.ai_edit": "Modifier",
|
||||
"ui.ai_edit_agent_title": "Modifier l'agent",
|
||||
"ui.ai_edit_gone": "Cet agent a été retiré : il n'y a rien à enregistrer.",
|
||||
"ui.ai_edit_tip": "Modifier le nom, la commande ou le point d'accès de cet agent. La clé enregistrée reste masquée.",
|
||||
"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_keep_hint": "laisser vide pour garder la clé enregistrée",
|
||||
"ui.ai_key_needs_https": "Une clé n'est envoyée qu'en HTTPS, ou vers cet ordinateur.",
|
||||
"ui.ai_key_note": "Stockée dans le trousseau de clés du système, jamais dans les réglages de SolidCraft. Laisser vide pour un serveur local.",
|
||||
"ui.ai_keychain_error": "Le trousseau de clés du système a refusé : {error}",
|
||||
@@ -3848,6 +4235,7 @@
|
||||
"ui.ai_request_timeout": "Délai d'expiration des requêtes",
|
||||
"ui.ai_request_timeout_hint": "Durée maximale d'une requête au modèle de ce point d'accès, de 30 s à une heure. Augmentez-la pour un modèle local lent.",
|
||||
"ui.ai_reset": "Réinitialiser",
|
||||
"ui.ai_save": "Enregistrer",
|
||||
"ui.ai_settings": "Réglages IA…",
|
||||
"ui.ai_show_features": "Afficher les fonctionnalités IA",
|
||||
"ui.ai_show_features_note": "Le chat de l'agent : un panneau où un agent de programmation que vous utilisez déjà, ou un point d'accès de modèle de votre choix, travaille sur la conception ouverte. Désactivé : ni bouton IA, ni raccourci, ni entrée de menu, ni panneau, et rien n'est recherché ni lancé.",
|
||||
@@ -3926,6 +4314,8 @@
|
||||
"ui.cancel": "Annuler",
|
||||
"ui.canvases": "Canevas",
|
||||
"ui.center": "Centre",
|
||||
"ui.center_arc_follows_cursor": "L'arc par centre suit le sens du curseur",
|
||||
"ui.center_arc_follows_cursor_tip": "Un arc par centre tourne dans le sens horaire ou antihoraire selon la façon dont le curseur tourne autour de son centre. Désactivé : toujours dans le sens antihoraire depuis le début.",
|
||||
"ui.centerline": "Ligne de construction",
|
||||
"ui.chain": "Chaîne",
|
||||
"ui.chamfer": "Chanfrein",
|
||||
@@ -3934,6 +4324,7 @@
|
||||
"ui.circle": "Cercle",
|
||||
"ui.circular_pattern": "Répétition circulaire",
|
||||
"ui.clear": "Effacer",
|
||||
"ui.clear_print_face": "Effacer la face d’impression",
|
||||
"ui.clear_recent": "Effacer les récents",
|
||||
"ui.clear_the_selection": "Effacer la sélection",
|
||||
"ui.click_selects_whole_bodies": "Un clic sélectionne des corps entiers",
|
||||
@@ -3968,12 +4359,25 @@
|
||||
"ui.constant": "Constant",
|
||||
"ui.constraints_conflict": "Contraintes en conflit",
|
||||
"ui.construction": "Construction",
|
||||
"ui.construction.angle_hint": "Droite de construction : cliquez sur un point, puis saisissez un angle ou choisissez une direction",
|
||||
"ui.construction.bisector_hint": "Bissectrice : sélectionnez deux lignes droites sécantes, puis la solution souhaitée",
|
||||
"ui.construction.common_hint": "Tangente commune : sélectionnez deux cercles/arcs, puis la solution souhaitée",
|
||||
"ui.construction.contact_hint": "Tangente : sélectionnez un cercle/arc ou une spline/conique ouverte, puis le point de contact (courbes libres : extrémités)",
|
||||
"ui.construction.external_hint": "Tangente : sélectionnez un cercle ou un arc, un point extérieur, puis la solution souhaitée",
|
||||
"ui.construction.free_hint": "Droite de construction : cliquez sur deux points ; clic droit pour recommencer",
|
||||
"ui.construction.point_hint": "Droite de construction : cliquez sur le point de passage",
|
||||
"ui.construction.reference_hint": "Droite de construction : sélectionnez une ligne droite, puis un point ; clic droit pour changer la référence",
|
||||
"ui.construction.ribbon_label": "Lignes aux.",
|
||||
"ui.context_menu": "Menu contextuel",
|
||||
"ui.continuity_crease": "Angle vif",
|
||||
"ui.continuity_curvature": "Courbure",
|
||||
"ui.continuity_curvature_change": "Variation de courbure",
|
||||
"ui.continuity_g0": "G0 angle vif",
|
||||
"ui.continuity_g1": "G1 tangence",
|
||||
"ui.continuity_g2": "G2 courbure",
|
||||
"ui.continuity_gap": "Écart",
|
||||
"ui.continuity_position": "Position",
|
||||
"ui.continuity_tangent": "Tangence",
|
||||
"ui.copy": "Copier",
|
||||
"ui.corner": "Angle",
|
||||
"ui.corners": "Angles",
|
||||
@@ -4010,6 +4414,40 @@
|
||||
"ui.design_properties.project": "Projet",
|
||||
"ui.design_properties.revision": "Indice",
|
||||
"ui.design_saved": "Enregistré dans {path}",
|
||||
"ui.details_access_caption": "Les faces indiquent si un outil peut les atteindre depuis la direction choisie.",
|
||||
"ui.details_angle": "Angle",
|
||||
"ui.details_continuity_caption": "Modifie la jonction sélectionnée. Les autres jonctions gardent leurs propres réglages.",
|
||||
"ui.details_continuity_stale": "Géométrie antérieure : relancez l'analyse pour modifier ou cadrer cette jonction.",
|
||||
"ui.details_copy_list": "Copier la liste",
|
||||
"ui.details_curvature": "Tolérance de courbure",
|
||||
"ui.details_curvature_caption": "Les faces sont colorées selon leur rayon de courbure local.",
|
||||
"ui.details_direction": "Direction de l'outil",
|
||||
"ui.details_draft_caption": "Les faces sont colorées selon leur angle avec la direction de démoulage.",
|
||||
"ui.details_edit_seam": "Modifier la jonction",
|
||||
"ui.details_fixture": "Spécimen de composant — données d'exemple uniquement",
|
||||
"ui.details_fixture_caption": "Spécimen de composant avec des valeurs d'illustration ; ce n'est pas un résultat du modèle.",
|
||||
"ui.details_fixture_problem": "Exemple de constat signalant un problème",
|
||||
"ui.details_fixture_readout": "Exemple de valeur mesurée",
|
||||
"ui.details_flip": "Inverser",
|
||||
"ui.details_gap": "Écart",
|
||||
"ui.details_hatch": "Hachurer les faces coupées",
|
||||
"ui.details_jump": "Saut de courbure",
|
||||
"ui.details_key": "Légende",
|
||||
"ui.details_offset": "Décalage",
|
||||
"ui.details_plane": "Plan",
|
||||
"ui.details_pull": "Direction de démoulage",
|
||||
"ui.details_radius": "Rayon",
|
||||
"ui.details_readout": "Valeur affichée",
|
||||
"ui.details_samples": "Échantillons",
|
||||
"ui.details_seam": "{body} · Arête {edge}",
|
||||
"ui.details_settings": "Réglages",
|
||||
"ui.details_sharp_gap": "Vive ou ouverte",
|
||||
"ui.details_show_all": "Tout afficher ({count})",
|
||||
"ui.details_smooth": "Lisse",
|
||||
"ui.details_stripes": "Bandes",
|
||||
"ui.details_target": "Cadrer la géométrie",
|
||||
"ui.details_worst_seams": "Pires jonctions",
|
||||
"ui.details_zebra_caption": "Les bandes réfléchies montrent la régularité du raccord entre les faces.",
|
||||
"ui.diameter": "Diamètre",
|
||||
"ui.dimension": "Cote",
|
||||
"ui.direct_interaction": "Interaction directe",
|
||||
@@ -4070,6 +4508,7 @@
|
||||
"ui.equation_y_of_x": "y(x)",
|
||||
"ui.equivalent_radius": "Rayon équivalent",
|
||||
"ui.expand": "Développer",
|
||||
"ui.explode_needs_two_components": "La vue éclatée écarte les composants les uns des autres : la conception doit en contenir au moins deux (Nouveau composant, Insérer une conception ou Créer des composants à partir des corps)",
|
||||
"ui.exportDxf": "Exporter en DXF…",
|
||||
"ui.export_ellipsis_keymap": "Exporter…",
|
||||
"ui.export_keymap_tip": "Raccourcis, favoris de la boîte S et barre d'outils, dans un fichier",
|
||||
@@ -4230,6 +4669,7 @@
|
||||
"ui.insert_design.saved_file": "{name} contient des modifications non enregistrées ; c'est donc son fichier enregistré qui a été inséré. Enregistrez-le, puis utilisez Mettre à jour les liens pour intégrer les modifications.",
|
||||
"ui.insert_design.unsaved": "Modifications non enregistrées",
|
||||
"ui.inspect": "Inspecter",
|
||||
"ui.instances": "Instances",
|
||||
"ui.intersect": "Intersection",
|
||||
"ui.isolate": "Isoler",
|
||||
"ui.join": "Joindre",
|
||||
@@ -4332,6 +4772,7 @@
|
||||
"ui.part_size_shared": "Reconstruit la pièce en remplaçant les entités ajoutées. Toutes les occurrences de ce composant sont mises à jour.",
|
||||
"ui.paste": "Coller",
|
||||
"ui.path": "Trajectoire",
|
||||
"ui.pattern_object_type_tip": "Corps : formes pleines, choisies dans la vue ou le Navigateur ; chacun est copié en entier.\nFonctions : étapes de l'Historique, comme une extrusion ou un perçage, choisies dans l'Historique ou sur une face qu'elles ont créée ; l'étape est répétée.\nUne esquisse n'est ni l'un ni l'autre : répétez ses courbes avec la Répétition ou la Symétrie de l'esquisse, ou extrudez-la d'abord.",
|
||||
"ui.pattern_on_path": "Répétition sur trajectoire",
|
||||
"ui.perforate_margin_default": "depuis le diamètre",
|
||||
"ui.perforate_pitch": "Pas",
|
||||
@@ -4358,12 +4799,17 @@
|
||||
"ui.plugins_skipped": "{count} dossier(s) illisible(s).",
|
||||
"ui.plugins_version": "{version}",
|
||||
"ui.point": "Point",
|
||||
"ui.point_cloud_add_filter": "Ajouter un filtre…",
|
||||
"ui.point_cloud_auto_range": "Plage automatique",
|
||||
"ui.point_cloud_color_by": "Colorer par",
|
||||
"ui.point_cloud_file_colors": "Couleurs du fichier",
|
||||
"ui.point_cloud_filter_by": "Filtrer par",
|
||||
"ui.point_cloud_height_high": "Plus haut",
|
||||
"ui.point_cloud_height_low": "Plus bas",
|
||||
"ui.point_cloud_legend_filter": "Affichées et masquées dans la légende ci-dessus",
|
||||
"ui.point_cloud_no_file_colors": "Le fichier n'a pas de couleurs",
|
||||
"ui.point_cloud_pause": "Pause",
|
||||
"ui.point_cloud_play": "Lire",
|
||||
"ui.point_cloud_single_color": "Couleur unique",
|
||||
"ui.point_cloud_size": "Taille des points",
|
||||
"ui.point_cloud_units": "Unités du fichier",
|
||||
@@ -4372,6 +4818,11 @@
|
||||
"ui.polygon": "Polygone",
|
||||
"ui.preferences": "Préférences",
|
||||
"ui.press_a_key": "appuyez sur une touche…",
|
||||
"ui.print_face_body_not_sent": "Le corps de cette face n’est pas envoyé : ajoutez-le d’abord à Corps",
|
||||
"ui.print_face_cleared": "{body} : face d’impression effacée",
|
||||
"ui.print_face_input": "Face d’impression",
|
||||
"ui.print_face_pick": "Sélectionnez une face plane d’un corps solide pour en faire la face d’impression",
|
||||
"ui.print_face_set": "{body} : face d’impression définie ; Envoyer au slicer pose le corps sur cette face",
|
||||
"ui.profile": "Profil",
|
||||
"ui.profile_plane": "Plan du profil",
|
||||
"ui.profiles": "Profils",
|
||||
@@ -4387,6 +4838,7 @@
|
||||
"ui.properties.inertia_y": "Iyx, Iyy, Iyz",
|
||||
"ui.properties.inertia_z": "Izx, Izy, Izz",
|
||||
"ui.properties.measuring": "Mesure en cours…",
|
||||
"ui.properties.measuring_bodies": "Mesure en cours… {done} corps sur {total}",
|
||||
"ui.properties.measuring_failed": "La mesure s'est arrêtée avant la fin.",
|
||||
"ui.properties.principal_axis": "Axe principal {number}",
|
||||
"ui.properties.principal_moments": "Moments principaux",
|
||||
@@ -4435,6 +4887,7 @@
|
||||
"ui.revolve": "Révolution",
|
||||
"ui.ribbon_exit": "Quitter",
|
||||
"ui.ribbon_labels": "Afficher le texte sous les icônes du ruban",
|
||||
"ui.ribbon_menus_skip_pinned": "Laisser les commandes épinglées hors des menus déroulants",
|
||||
"ui.ribbon_pin_footer": "Les commandes épinglées s’affichent dans le ruban. Survolez une ligne pour l’épingler ou la détacher.",
|
||||
"ui.ribbon_workspace": "Espace de travail",
|
||||
"ui.right": "Droite",
|
||||
@@ -4453,6 +4906,7 @@
|
||||
"ui.selectOther": "Sélectionner un autre…",
|
||||
"ui.selectTangentChain": "Sélectionner la chaîne tangente",
|
||||
"ui.select_all": "Tout sélectionner",
|
||||
"ui.select_components_by_face": "Sélectionner des composants (une face sélectionne son composant)",
|
||||
"ui.selection": "Sélection",
|
||||
"ui.selection_modifiers": "Modificateurs de sélection",
|
||||
"ui.selection_shift_add_only": "Maj+clic ajoute sans retirer",
|
||||
@@ -4494,6 +4948,11 @@
|
||||
"ui.sketch_intersect_selection_filter": "Filtre de sélection",
|
||||
"ui.sketch_modifiers": "Modificateurs d’esquisse",
|
||||
"ui.sketch_project_geometry": "Géométrie",
|
||||
"ui.sketch_report.all_constrained": "Tout est entièrement contraint.",
|
||||
"ui.sketch_report.dof": "Degrés de liberté",
|
||||
"ui.sketch_report.loose": "Pas entièrement contrainte",
|
||||
"ui.sketch_report.status": "État",
|
||||
"ui.sketch_report.title": "Rapport d'esquisse",
|
||||
"ui.sketch_scale": "Échelle d'esquisse",
|
||||
"ui.sketches": "Esquisses",
|
||||
"ui.slice": "Trancher",
|
||||
@@ -4598,10 +5057,19 @@
|
||||
"ui.view_centre": "Centre de la vue",
|
||||
"ui.viewport_status_clear": "Désactiver {name}",
|
||||
"ui.viewport_status_clear_hint": "Désactiver {name} · Échap",
|
||||
"ui.viewport_status_edges": "{count} arêtes",
|
||||
"ui.viewport_status_from": "depuis {direction}",
|
||||
"ui.viewport_status_key": "Afficher la légende de {name}",
|
||||
"ui.viewport_status_minimum_radius": "Rayon minimum",
|
||||
"ui.viewport_status_pairs": "{count} paires",
|
||||
"ui.viewport_status_section_hatch": "{setting} · hachures",
|
||||
"ui.viewport_status_section_plane": "Plan actuel",
|
||||
"ui.viewport_status_section_tilted": "inclinée · {offset}",
|
||||
"ui.viewport_status_stripes": "{count} bandes",
|
||||
"ui.viewport_status_summary_clear": "Tout désactiver",
|
||||
"ui.viewport_status_summary_heading": "Dans cette vue",
|
||||
"ui.viewport_status_summary_label": "{name} et {count} autres éléments",
|
||||
"ui.viewport_status_summary_more": "+{count}",
|
||||
"ui.viewport_theme_follow": "Suivre le thème",
|
||||
"ui.visual_shaded_hidden_edges": "Ombré avec arêtes cachées",
|
||||
"ui.visual_shaded_with_edges": "Ombré avec arêtes",
|
||||
@@ -4638,10 +5106,12 @@
|
||||
"update.button": "Mettre à jour",
|
||||
"update.button_version": "Mettre à jour vers {version}",
|
||||
"update.check_automatically": "Rechercher automatiquement les mises à jour",
|
||||
"update.check_for_updates": "Rechercher des mises à jour",
|
||||
"update.check_now": "Vérifier maintenant",
|
||||
"update.check_unavailable": "La recherche de mises à jour n’est pas disponible pour cette installation.",
|
||||
"update.checking": "Vérification…",
|
||||
"update.copy": "Copier",
|
||||
"update.dmg_opened": "L'image disque est ouverte et vérifiée : faites glisser SolidCraft dans Applications en remplaçant l'ancienne copie, puis redémarrez SolidCraft.",
|
||||
"update.dmg_opened": "L'image disque est ouverte et vérifiée : quittez SolidCraft, faites glisser le nouveau SolidCraft dans Applications en remplaçant l'ancienne copie, puis rouvrez SolidCraft.",
|
||||
"update.download": "Télécharger",
|
||||
"update.download_open": "Télécharger et ouvrir",
|
||||
"update.downloading": "Téléchargement et vérification… {done} sur {total} Mo",
|
||||
@@ -4652,11 +5122,13 @@
|
||||
"update.installed": "SolidCraft {version} est installé. Redémarrez pour l'utiliser.",
|
||||
"update.installer_started": "L'installateur démarre à la fermeture de SolidCraft.",
|
||||
"update.later": "Me le rappeler plus tard",
|
||||
"update.managed": "Mise à jour disponible : mettez à jour SolidCraft dans {manager}",
|
||||
"update.no_notes": "Cette version n'a pas de notes.",
|
||||
"update.package_hint": "Cette copie provient d'un {kind}. Téléchargez le nouveau paquet, puis installez-le avec :",
|
||||
"update.prefs_dev": "Ceci est une version de développement ({version}) : elle ne vérifie pas les mises à jour.",
|
||||
"update.prefs_flatpak": "Flatpak met à jour cette copie ({version}) : exécutez {command} ou utilisez votre logithèque. SolidCraft ne vérifie pas.",
|
||||
"update.prefs_label": "Mises à jour",
|
||||
"update.prefs_managed": "Ceci est SolidCraft {version}, géré par {manager}. Une vérification interroge GitHub sur la dernière version et n'envoie rien d'autre ; mettez à jour SolidCraft dans {manager}.",
|
||||
"update.prefs_note": "Ceci est SolidCraft {version} ({kind}). Une vérification interroge GitHub sur la dernière version et n'envoie rien d'autre.",
|
||||
"update.prefs_web": "Rechargez la page pour obtenir la dernière version de l'application web. Si le réseau est indisponible ou si la mise à jour ne peut pas finir de se télécharger à temps, la version enregistrée s'ouvre à la place ; un téléchargement en cours se poursuit pour le prochain rechargement.",
|
||||
"update.refresh_checking": "Recherche de la dernière version…",
|
||||
|
||||
+606
-4
File diff suppressed because it is too large
Load Diff
+724
-14
File diff suppressed because it is too large
Load Diff
+579
-3
File diff suppressed because it is too large
Load Diff
+606
-4
File diff suppressed because it is too large
Load Diff
+606
-4
File diff suppressed because it is too large
Load Diff
+608
-4
File diff suppressed because it is too large
Load Diff
+724
-14
File diff suppressed because it is too large
Load Diff
+363
-6
@@ -407,6 +407,7 @@
|
||||
"Recover unsaved design?": "저장되지 않은 디자인을 복구할까요?",
|
||||
"Rectangular": "직사각형",
|
||||
"Reference face": "참조 면",
|
||||
"Reference plane": "참조 평면",
|
||||
"Relief Depth": "릴리프 깊이",
|
||||
"Relief Width": "릴리프 너비",
|
||||
"Remove Tools": "공구 제거",
|
||||
@@ -422,6 +423,8 @@
|
||||
"Rename Feature": "피처 이름 바꾸기",
|
||||
"Rename Folder": "폴더 이름 바꾸기",
|
||||
"Render Stage": "렌더링 단계",
|
||||
"Reopen the designs that were open when SolidCraft was last closed": "지난번에 열려 있던 디자인 다시 열기",
|
||||
"Repair Part": "부품 복구",
|
||||
"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에 붙여넣고 그곳에서 보고서를 제출하세요.",
|
||||
@@ -476,6 +479,7 @@
|
||||
"Second point": "두 번째 점",
|
||||
"Section": "단면",
|
||||
"Section position": "단면 위치",
|
||||
"Sections": "단면",
|
||||
"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).": "다른 항목 선택: 모델 위에서 왼쪽 버튼을 가만히 누르고 있거나(또는 오른쪽 클릭 › 다른 항목 선택…) 커서 아래의 모든 항목을 나열합니다. 같은 위치를 다시 클릭하면 다음 항목이 선택됩니다. 가운데 버튼을 더블 클릭하면 홈으로 이동합니다(환경설정 › 탐색).",
|
||||
@@ -679,6 +683,7 @@
|
||||
"browser.edit_canvas_title": "캔버스 편집: {name}",
|
||||
"browser.edit_point_cloud_title": "포인트 클라우드 편집: {name}",
|
||||
"browser.move_title": "이동: {name}",
|
||||
"browser.needs_repair": "{name}: 복구 필요",
|
||||
"browser.parameters": "매개변수 ({count})",
|
||||
"browser.precision": "정밀도: {precision}",
|
||||
"browser.sketch_attachment_lost": "{name} (부착 해제됨)",
|
||||
@@ -711,7 +716,10 @@
|
||||
"cmd.appearance_pick_up": "모양 가져오기",
|
||||
"cmd.appearance_replace": "모양 바꾸기",
|
||||
"cmd.assembly_bom": "자재 명세서",
|
||||
"cmd.assembly_mass_properties": "질량 특성",
|
||||
"cmd.body.print_face": "출력 바닥면",
|
||||
"cmd.body_rename": "바디 이름 변경",
|
||||
"cmd.body_repair": "부품 복구",
|
||||
"cmd.browser_group": "새 그룹",
|
||||
"cmd.browser_move": "브라우저 항목 이동",
|
||||
"cmd.browser_order": "브라우저 항목 정렬",
|
||||
@@ -756,6 +764,7 @@
|
||||
"cmd.config_rule": "구성 규칙",
|
||||
"cmd.config_rules": "구성 규칙",
|
||||
"cmd.config_table": "구성 표시",
|
||||
"cmd.construct.plane.axis_point": "축과 점을 지나는 평면",
|
||||
"cmd.construct_axis_edge": "모서리를 통과하는 축",
|
||||
"cmd.construct_axis_normal_to_face": "면에 수직인 축",
|
||||
"cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축",
|
||||
@@ -784,6 +793,8 @@
|
||||
"cmd.diagnostics_crash_reports": "충돌 보고서",
|
||||
"cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제",
|
||||
"cmd.diagnostics_crash_test": "충돌 테스트",
|
||||
"cmd.diagnostics_profile": "프로파일",
|
||||
"cmd.diagnostics_validation": "검증",
|
||||
"cmd.doc_recover": "설계 복구",
|
||||
"cmd.doc_recovery_discard": "복구된 설계 폐기",
|
||||
"cmd.doc_recovery_list": "복구 가능한 설계",
|
||||
@@ -902,6 +913,9 @@
|
||||
"cmd.pointcloud_edit": "포인트 클라우드 편집",
|
||||
"cmd.pointcloud_insert": "포인트 클라우드 삽입",
|
||||
"cmd.pointcloud_list": "포인트 클라우드",
|
||||
"cmd.render.compare.divider": "렌더 비교 구분선 이동",
|
||||
"cmd.render.result.discard": "렌더 결과 버리기",
|
||||
"cmd.render.result.select": "렌더 결과 선택",
|
||||
"cmd.render_backends": "렌더링 백엔드",
|
||||
"cmd.render_batch": "모든 샷 렌더링",
|
||||
"cmd.render_camera_fit": "렌더 대상 화면 맞춤",
|
||||
@@ -1090,9 +1104,11 @@
|
||||
"cmd.timeline_rollback": "타임라인 마커 이동",
|
||||
"cmd.timeline_stop": "기록 재생 중지",
|
||||
"cmd.timeline_suppress": "특성 비활성화",
|
||||
"cmd.ui.assistant": "도우미 표시 전환",
|
||||
"cmd.view.render_browser": "렌더 브라우저",
|
||||
"cmd.view.render_dock": "렌더 도크 이동",
|
||||
"cmd.view.render_focus": "렌더 집중 보기",
|
||||
"cmd.view.status_details": "뷰포트 상태 상세 정보",
|
||||
"cmd.view.status_items": "뷰포트 상태 항목",
|
||||
"cmd.view_ambient_occlusion": "앰비언트 오클루전",
|
||||
"cmd.view_angle_readout": "뷰 각도",
|
||||
@@ -1106,14 +1122,17 @@
|
||||
"cmd.view_render_frame": "렌더 프레임 가이드",
|
||||
"cmd.view_save": "새 이름 있는 보기",
|
||||
"cmd.view_shots_strip": "샷 스트립",
|
||||
"color.blue": "파랑",
|
||||
"color.copy": "클릭하여 색상 값 복사",
|
||||
"color.edit": "클릭하여 색상 편집",
|
||||
"color.gamma": "색상 값을 0-255 감마 공간으로 표시 중",
|
||||
"color.green": "초록",
|
||||
"color.hex": "Hex",
|
||||
"color.hex_help": "#rrggbb 또는 #rrggbbaa 입력. 6자리 값은 불투명도 유지",
|
||||
"color.hex_hint": "#rrggbb",
|
||||
"color.hue": "색조",
|
||||
"color.linear": "색상 값을 0-1 선형 공간으로 표시 중",
|
||||
"color.red": "빨강",
|
||||
"color.saturation_value": "채도 및 명도",
|
||||
"color.selected": "선택한 색상",
|
||||
"config.column_filter_empty": "일치하는 열이 없습니다",
|
||||
@@ -1150,6 +1169,7 @@
|
||||
"crash.report": "보고…",
|
||||
"crash.unclean_explanation": "이후 실행 시 감지되었으며 정확한 종료 시각은 기록되지 않았습니다. 시스템 종료나 강제 중지로도 비정상 종료가 발생할 수 있습니다.",
|
||||
"delete.and_more": "… 외 {count}개",
|
||||
"diagnostic.reselect_reference": "{feature}: 참조 `{reference}`이(가) 선택했을 때의 모습으로 더 이상 존재하지 않습니다. 다시 선택하세요",
|
||||
"dialog.bodies_count": "바디 {count}개",
|
||||
"dialog.clearance_invalid": "간격은 0 이상의 길이여야 합니다",
|
||||
"dialog.compute": "계산",
|
||||
@@ -1160,9 +1180,14 @@
|
||||
"dialog.dimension_insert_hint": "다른 구동 치수를 두 번 클릭하여 이름 삽입",
|
||||
"dialog.dimension_name": "치수: {name}",
|
||||
"dialog.edit_title": "{title} 편집",
|
||||
"dialog.pattern_sketch_not_object": "바디나 피처를 선택하세요. 스케치는 둘 다 아닙니다. 곡선은 스케치 안의 패턴이나 대칭 복사로 반복하거나, 먼저 돌출하세요.",
|
||||
"dialog.select": "↖ 선택",
|
||||
"dialog.selected_count": "↖ {count}개 선택됨",
|
||||
"dialog.selection_constraint": "구속 조건 {id}",
|
||||
"dialog.selection_external_curve": "{sketch} · 곡선 {id}",
|
||||
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
|
||||
"dialog.selection_midpoint": "중점 · {id}",
|
||||
"dialog.selection_profile": "{sketch} · 프로파일 {number}",
|
||||
"dialog.selection_step": "{step}/{total} 단계: {input} 선택",
|
||||
"dialog.step": "단계",
|
||||
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
|
||||
@@ -1195,6 +1220,14 @@
|
||||
"file_load.reading_percent": "파일 읽는 중... {percent}%",
|
||||
"file_load.rebuilding": "피처 다시 빌드 중 {done}/{total}",
|
||||
"file_load.recovering": "디자인 복구 중",
|
||||
"file_load.step_build": "모델 만드는 중",
|
||||
"file_load.step_feature": "피처 {done}/{total}",
|
||||
"file_load.step_part": "부품 {done}/{total}",
|
||||
"file_load.step_percent": "{percent}%",
|
||||
"file_load.step_read": "파일 읽는 중",
|
||||
"file_load.step_rebuild": "피처 다시 빌드 중",
|
||||
"file_load.step_repair": "부품 확인 및 복구 중",
|
||||
"file_load.step_show": "뷰 준비 중",
|
||||
"file_load.stopping": "이전 작업이 중지되기를 기다리는 중...",
|
||||
"file_load.waiting": "파일 작업 대기 중",
|
||||
"fits the face, e.g. M8": "면에 맞음, 예: M8",
|
||||
@@ -1203,6 +1236,7 @@
|
||||
"help.attributions": "저작자 표시 (ATTRIBUTION.md)",
|
||||
"help.build": "버전 {version} · 커밋 {commit} · 빌드 날짜 {date}",
|
||||
"help.component.finish_edit": "제자리에서 편집 중인 부품을 저장하고 어셈블리로 돌아가 그 부품으로 어셈블리를 업데이트합니다. 연결된 부품의 변경을 마쳤을 때 사용하세요.",
|
||||
"help.construct.plane.axis_point": "축과 축 위에 있지 않은 점을 포함하는 구성 평면을 만듭니다. 원점 축, 구성 축, 직선 모서리 또는 스케치 직선을 선택한 다음 꼭짓점, 스케치 점 또는 구성점을 선택합니다. 축 위의 점으로는 평면을 하나로 결정할 수 없습니다.",
|
||||
"help.font_details": "인터페이스는 egui의 내장 글꼴을 사용합니다(Ubuntu Font Licence 및 MIT/Bitstream Vera 라이선스의 Ubuntu Light와 Hack, OFL 및 MIT의 Noto Emoji와 이모지 아이콘 글꼴). 스케치 텍스트는 Liberation Sans(SIL OFL 1.1)를 사용합니다. The Crafting Apps의 선택적 글꼴 팩 craft-fonts(BIZ UDPGothic, Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC, Noto Sans Arabic; SIL OFL 1.1)는 SolidCraft 빌드에 포함되지 않습니다.",
|
||||
"help.fonts": "글꼴",
|
||||
"help.licences": "라이선스",
|
||||
@@ -1281,6 +1315,27 @@
|
||||
"home.start_hint": "설계를 만들거나, 최근 파일을 열거나, 예제를 살펴보세요.",
|
||||
"home.whats_new": "새로운 기능",
|
||||
"home.yesterday": "어제",
|
||||
"import_report.details": "세부 정보:",
|
||||
"import_report.file_one": "파일에 다른 문제가 1개 있었습니다.",
|
||||
"import_report.file_other": "파일에 다른 문제가 {count}개 있었습니다.",
|
||||
"import_report.hint": "이 부품을 만든 프로그램에서 다시 내보내면 해결될 수 있습니다. 세부 정보는 경고 메시지 복사에 있습니다.",
|
||||
"import_report.left_out_one": "면 1개를 복구하지 못해 빠져 있습니다.",
|
||||
"import_report.left_out_other": "면 {count}개를 복구하지 못해 빠져 있습니다.",
|
||||
"import_report.needs_repair_one": "부품 1개는 모델링하기 전에 복구해야 합니다.",
|
||||
"import_report.needs_repair_other": "부품 {count}개는 모델링하기 전에 복구해야 합니다.",
|
||||
"import_report.not_imported_one": "부품 1개를 가져오지 못했습니다.",
|
||||
"import_report.not_imported_other": "부품 {count}개를 가져오지 못했습니다.",
|
||||
"import_report.not_shown_one": "면 1개를 그리지 못해 숨겨져 있습니다.",
|
||||
"import_report.not_shown_other": "면 {count}개를 그리지 못해 숨겨져 있습니다.",
|
||||
"import_report.open_one": "부품 1개는 면 사이에 틈이 있어 열린 바디로 들어왔습니다.",
|
||||
"import_report.open_other": "부품 {count}개는 면 사이에 틈이 있어 열린 바디로 들어왔습니다.",
|
||||
"import_report.refused_one": "부품 1개를 솔리드로 만들지 못해 가져오지 않았습니다.",
|
||||
"import_report.refused_other": "부품 {count}개를 솔리드로 만들지 못해 가져오지 않았습니다.",
|
||||
"import_report.repaired_one": "부품 1개의 작은 결함을 복구했습니다.",
|
||||
"import_report.repaired_other": "부품 {count}개의 작은 결함을 복구했습니다.",
|
||||
"import_report.solids": "부품 {parts}개 중 {solids}개를 솔리드로 가져왔습니다.",
|
||||
"import_report.surfaces_one": "부품 1개를 솔리드로 만들지 못해 서피스로 들어왔습니다.",
|
||||
"import_report.surfaces_other": "부품 {count}개를 솔리드로 만들지 못해 서피스로 들어왔습니다.",
|
||||
"install.button_tip": "SolidCraft를 앱으로 설치합니다. 별도 창에서 열리며 오프라인에서도 작동합니다",
|
||||
"install.dock_1": "파일 › Dock에 추가를 선택합니다.",
|
||||
"install.dock_2": "추가를 클릭합니다.",
|
||||
@@ -1305,6 +1360,8 @@
|
||||
"joint.from_bodies_hint": "선택한 각 바디가 제자리에 그대로 있으면서 자체의 새 구성 요소로 옮겨집니다.",
|
||||
"joint.one_body": "두 선택이 모두 {body}에 있습니다. 두 번째는 다른 부품에서 선택하세요.",
|
||||
"joint.one_component": "이 부품들은 한 구성 요소의 바디입니다: {bodies}. 관절은 한 구성 요소를 다른 구성 요소에 대해 움직이므로 부품마다 자체 구성 요소가 필요합니다.",
|
||||
"joint_move.conflict": "이 부품의 조인트가 다른 조인트와 충돌합니다: {joint}.",
|
||||
"joint_move.conflict_hint": "충돌하는 조인트를 편집, 비활성화 또는 삭제하여 다른 조인트가 부품을 배치할 수 있게 하세요.",
|
||||
"keymap.command_id": "명령 ID",
|
||||
"keymap.empty": "일치하는 명령이 없습니다",
|
||||
"keymap.use_anyway": "그래도 여기서 {key} 사용",
|
||||
@@ -1345,6 +1402,8 @@
|
||||
"motion.track_name": "트랙 {number}",
|
||||
"nav.alias.help": "Shift+Alt+왼쪽 드래그는 회전, Shift+Alt+가운데 드래그는 이동, Shift+Alt+오른쪽 드래그는 확대/축소입니다. 가운데 드래그로도 이동하고 오른쪽 드래그로도 회전합니다",
|
||||
"nav.blender.help": "가운데 드래그는 회전, Shift+가운데 드래그는 이동, Ctrl+가운데 드래그는 확대/축소입니다. 오른쪽 드래그는 카메라에 영향을 주지 않습니다",
|
||||
"nav.creo.help": "가운데 버튼을 끌면 회전, Shift+가운데 버튼은 이동, Ctrl+가운데 버튼을 위아래로 끌면 확대/축소합니다(아래로 끌면 확대). 휠을 굴려도 확대/축소합니다",
|
||||
"nav.creo.label": "Creo",
|
||||
"nav.free_orbit.help": "마우스와 3D 마우스로 롤을 유지하며 양 극점을 넘어 회전합니다. Shift 축 스냅은 비활성화됩니다.",
|
||||
"nav.free_orbit.label": "자유 회전",
|
||||
"nav.free_orbit.snap_help": "마우스와 3D 마우스로 양쪽 극점을 지나 회전할 수 있으며 롤 각도가 유지됩니다. 궤도 회전 드래그 중 Shift를 누르면 보기 방향과 롤 각도가 스냅됩니다.",
|
||||
@@ -1513,6 +1572,18 @@
|
||||
"prefs.add_item": "항목 추가",
|
||||
"prefs.alt_frees_manipulators": "Alt / Option 키로 조작 화살표도 자유롭게 움직입니다",
|
||||
"prefs.anti_aliasing_note": "울퉁불퉁한 모서리를 부드럽게 합니다.",
|
||||
"prefs.appearance.colour_vision_help": "의미를 전달하는 색상을 변경합니다: 스케치 상태, 축, 굽힘, 분석 및 아이콘.",
|
||||
"prefs.appearance.desktop_only": "데스크톱 전용",
|
||||
"prefs.appearance.follow_help": "다른 도구가 파일을 다시 쓰면 다시 불러옵니다.",
|
||||
"prefs.appearance.interface_scale": "인터페이스 배율",
|
||||
"prefs.appearance.next_themes": "다음 테마",
|
||||
"prefs.appearance.option_count": "옵션 {count}개",
|
||||
"prefs.appearance.option_count_one": "옵션 {count}개",
|
||||
"prefs.appearance.previous_themes": "이전 테마",
|
||||
"prefs.appearance.theme_count": "테마 {count}개",
|
||||
"prefs.appearance.theme_own": "테마 자체",
|
||||
"prefs.appearance.viewport_colours": "3D 보기 색상",
|
||||
"prefs.appearance.viewport_logo": "뷰포트 및 로고",
|
||||
"prefs.applies_on_restart": "다시 시작하면 적용됩니다",
|
||||
"prefs.assistant": "어시스턴트",
|
||||
"prefs.axes": "축",
|
||||
@@ -1525,10 +1596,16 @@
|
||||
"prefs.bottom_centre": "하단 가운데",
|
||||
"prefs.bottom_right": "오른쪽 아래",
|
||||
"prefs.browser": "브라우저",
|
||||
"prefs.browser_components_open": "구성요소를 펼친 상태로 시작",
|
||||
"prefs.browser_components_open_note": "브라우저의 모든 구성요소가 이전처럼 펼쳐진 상태로 시작합니다. 끄면 직접 펼칠 때까지 접혀 있습니다.",
|
||||
"prefs.browser_eye_left": "표시 아이콘을 접기 화살표 옆에 표시",
|
||||
"prefs.browser_eye_left_note": "브라우저 행의 표시 아이콘을 행 오른쪽 끝의 열이 아니라 이전처럼 접기 화살표 옆에 표시합니다.",
|
||||
"prefs.built_in": "기본 제공",
|
||||
"prefs.button_left": "왼쪽 드래그",
|
||||
"prefs.button_middle": "가운데 드래그",
|
||||
"prefs.button_right": "오른쪽 드래그",
|
||||
"prefs.carousel_next": "다음",
|
||||
"prefs.carousel_previous": "이전",
|
||||
"prefs.choose_font": "글꼴 파일 선택…",
|
||||
"prefs.click": "클릭",
|
||||
"prefs.contact_shading": "부드러운 접촉 음영",
|
||||
@@ -1541,6 +1618,10 @@
|
||||
"prefs.ctrl_cmd": "Ctrl / Cmd",
|
||||
"prefs.curved_surfaces": "곡면",
|
||||
"prefs.curves": "곡면",
|
||||
"prefs.cut_preview_tint": "제거되는 재료를 빨간색으로 표시",
|
||||
"prefs.cut_preview_tint_help": "미리 보기는 피처가 제거하는 재료와 대체되는 형상을 빨간색 반투명 고스트로 채웁니다. 끄면 이 채움이 완전히 투명해지고 윤곽선만 그려집니다.",
|
||||
"prefs.dark_parts_shading": "어두운 부품의 형태 유지",
|
||||
"prefs.dark_parts_shading_help": "선형 조명과 부드러운 하늘광으로 뷰를 음영 처리하여 어두운 면이 검게 뭉개지지 않고 그라데이션을 유지합니다",
|
||||
"prefs.dead_zone_note": "빗금 친 영역이 데드존입니다. 그 안의 움직임은 무시됩니다.",
|
||||
"prefs.default": "기본값",
|
||||
"prefs.design": "설계 기본값",
|
||||
@@ -1561,19 +1642,35 @@
|
||||
"prefs.font_sample": "The quick brown fox · Ø12.50 mm · 45.0° · 0123456789",
|
||||
"prefs.font_unreadable": "선택한 글꼴을 읽을 수 없습니다.",
|
||||
"prefs.general": "일반",
|
||||
"prefs.general_note": "언어, 저장 및 업데이트.",
|
||||
"prefs.general.active": "사용 중",
|
||||
"prefs.general.crash_reports": "충돌 보고서",
|
||||
"prefs.general.edited": "수정됨",
|
||||
"prefs.general.held": "포함 항목",
|
||||
"prefs.general.import": "가져오기...",
|
||||
"prefs.general.last_check": "마지막 확인: {time}",
|
||||
"prefs.general.minutes": "분",
|
||||
"prefs.general.name_setup": "이 설정의 이름",
|
||||
"prefs.general.profile_name": "프로필 이름",
|
||||
"prefs.general.save_new": "새로 저장",
|
||||
"prefs.general.step_import": "STEP 및 가져오기",
|
||||
"prefs.general.this_computer": "이 컴퓨터",
|
||||
"prefs.general_note": "프로필, 언어, 파일 및 업데이트.",
|
||||
"prefs.gesture_orbit": "회전",
|
||||
"prefs.gesture_pan": "이동",
|
||||
"prefs.gesture_zoom": "확대/축소",
|
||||
"prefs.graphics": "그래픽",
|
||||
"prefs.graphics_note": "속도와 부드러움 중에서 선택합니다. 각 선택의 효과가 샘플에 표시됩니다.",
|
||||
"prefs.graphics_presets_note": "프리셋은 아래의 선택 항목을 한 번에 설정합니다.",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out": "축소 시 구속 조건 기호 숨기기",
|
||||
"prefs.hide_constraint_glyphs_zoomed_out_note": "스케치를 충분히 축소하면 구속 조건 기호가 가려지지 않도록 숨깁니다. 끄기: 확대/축소와 관계없이 고정된 화면 크기를 유지합니다.",
|
||||
"prefs.history": "이력",
|
||||
"prefs.history_origin_bodies": "원점과 바디를 맨 위에 표시",
|
||||
"prefs.history_origin_bodies_note": "왼쪽 또는 브라우저 아래의 히스토리가 활성 구성 요소의 바디 폴더와 원점으로 시작하며, 브라우저처럼 표시 아이콘과 메뉴가 있습니다. 아래쪽 히스토리는 바뀌지 않습니다.",
|
||||
"prefs.history_play_button": "기록 컨트롤에 기록 재생 버튼 표시",
|
||||
"prefs.history_play_button_note": "기록 컨트롤의 한 단계 뒤로와 한 단계 앞으로 사이에 기록 재생을 추가합니다. 컨트롤을 마우스 오른쪽 버튼으로 클릭해 표시하거나 숨길 수도 있습니다.",
|
||||
"prefs.hold_stop_snapping": "누르고 있으면 스냅 중지",
|
||||
"prefs.home_fit_size": "홈·맞춤 크기",
|
||||
"prefs.home_fit_size_note": "홈, 맞춤, 연 디자인 또는 뷰 탐색기의 뷰 변경으로 모델을 화면에 맞출 때 모델이 뷰에서 차지하는 비율. 아래 설정과 관계없이 적용됩니다. 100 %: 패널, 나침반, 내비게이션 바 사이의 뷰를 가득 채웁니다.",
|
||||
"prefs.icons_only": "아이콘만",
|
||||
"prefs.imported_file": "가져온 파일",
|
||||
"prefs.input": "입력",
|
||||
@@ -1582,19 +1679,22 @@
|
||||
"prefs.interface_note": "SolidCraft의 모양과 각 부분의 위치입니다.",
|
||||
"prefs.interval_minutes": "{n}분",
|
||||
"prefs.labels": "레이블",
|
||||
"prefs.language_system": "시스템",
|
||||
"prefs.layout": "레이아웃",
|
||||
"prefs.layout_note": "컴팩트는 패널을 창 가장자리에 고정합니다. 브라우저 반투명은 플로팅에서 사용할 수 있습니다. 어시스턴트 위치는 어시스턴트 페이지에서 바꿀 수 있습니다.",
|
||||
"prefs.left": "왼쪽",
|
||||
"prefs.left_click": "왼쪽 클릭",
|
||||
"prefs.line_drag_draws_one": "누른 채 드래그하면 선 하나 그리기",
|
||||
"prefs.line_drag_draws_one_note": "선 도구에서 누르고 드래그한 뒤 놓으면 누른 곳에서 놓은 곳까지 선 하나를 그리고 거기서 끝납니다. 클릭하면 이전처럼 이어지는 선을 그립니다. 끄면: 드래그는 클릭처럼 놓은 곳에 점을 둡니다.",
|
||||
"prefs.line_hover_tangent_arc": "선의 마지막 점에 머무르면 접선 호로 전환",
|
||||
"prefs.line_hover_tangent_arc_note": "선 도구에서 선분을 그린 뒤 체인의 마지막 점에서 벗어났다가 돌아와 포인터를 그 위에 멈추면, 다음 클릭으로 접선 호의 끝점을 지정합니다. 같은 동작으로 선으로 돌아갑니다. 끔: 머물러도 아무 일도 일어나지 않습니다.",
|
||||
"prefs.live_input": "실시간 입력",
|
||||
"prefs.live_preview": "실시간 미리보기",
|
||||
"prefs.max_file_size": "열 수 있는 최대 파일 크기",
|
||||
"prefs.max_file_size_design": "디자인 파일",
|
||||
"prefs.max_file_size_design": "열 파일의 최대 크기: 디자인",
|
||||
"prefs.max_file_size_flag": "이번 실행은 --max-file-size {size}로 시작했으며 SolidCraft를 닫을 때까지 적용됩니다. 이 설정은 해당 옵션 없이 시작할 때 적용됩니다.",
|
||||
"prefs.max_file_size_note": "기본값: 512 MB 및 256 MB, 최대 16 GB. 파일을 열려면 파일 크기의 몇 배에 달하는 메모리가 필요합니다. 큰 가져오기 데이터를 보관하는 디자인은 저장하고 다시 열기 위해 디자인 파일 한도를 높여야 할 수 있습니다.",
|
||||
"prefs.max_file_size_step": "STEP 및 가져오기 파일",
|
||||
"prefs.max_file_size_step": "열 파일의 최대 크기: STEP 및 가져오기",
|
||||
"prefs.max_file_size_web": "웹 앱은 STEP 및 가져오기 파일을 512 MB까지, 디자인을 256 MB까지 엽니다. 브라우저 탭은 더 큰 파일을 담을 수 없습니다. 데스크톱 앱에서는 이 한도를 높일 수 있습니다.",
|
||||
"prefs.megabytes": "MB",
|
||||
"prefs.minutes": "분",
|
||||
@@ -1604,6 +1704,9 @@
|
||||
"prefs.mouse_mapping": "마우스 매핑",
|
||||
"prefs.navigation": "탐색",
|
||||
"prefs.navigation_note": "마우스, 트랙패드, 3D 마우스로 뷰를 움직이는 방식입니다.",
|
||||
"prefs.navigator_fit": "뷰 탐색기로 뷰를 바꾼 뒤 모델 맞춤",
|
||||
"prefs.navigator_fit_note": "큐브의 면, 모서리, 화살표나 기즈모의 축을 클릭하면 어느 방향에서든 전체가 들어가는 한 거리(경계 구)에서 모델의 중심을 바라봅니다. 켜져 있는 동안 홈 뷰도 같은 확대/축소와 중심을 쓰므로 그 뒤에 고른 뷰는 방향만 바뀝니다. 끄면 뷰가 제자리에서 회전하며 대상과 확대/축소를 유지합니다.",
|
||||
"prefs.network_locations_note": "파일 서버에 디자인을 보관하는 조직용입니다. 끄면 허용한 서버만 사용합니다.",
|
||||
"prefs.not_on_gpu": "이 GPU에서는 사용할 수 없음",
|
||||
"prefs.on": "켜짐",
|
||||
"prefs.opaque": "불투명",
|
||||
@@ -1616,6 +1719,14 @@
|
||||
"prefs.persistent_ribbon_pressed_help": "효과가 켜져 있는 동안 리본 명령을 강조 표시합니다. 끄면 이전의 중립적인 모양과 명령 도움말을 유지합니다.",
|
||||
"prefs.perspective_fov": "시야각",
|
||||
"prefs.preview_choices": "선택 항목 미리보기",
|
||||
"prefs.preview_tint_new_faces": "미리 보기에서 새 면에 색 입히기",
|
||||
"prefs.preview_tint_new_faces_help": "바디를 변경하는 피처의 미리 보기에서 추가되는 면을 미리 보기 색으로 그릴 수 있습니다. 끄면 바디가 원래 색으로 그려집니다.",
|
||||
"prefs.print_face_highlight": "출력 바닥면 강조",
|
||||
"prefs.print_face_highlight_tip": "뷰에서 바디의 출력 바닥면을 색칠하고 윤곽을 그리는 강도입니다. 0 %이면 강조하지 않습니다.",
|
||||
"prefs.profile_export": "내보내기",
|
||||
"prefs.profile_imported": "프로필을 가져왔습니다.",
|
||||
"prefs.profile_operations": "작업 프로파일링",
|
||||
"prefs.profile_operations_note": "SMLib 호출, 커널 작업, 피처, 명령, 가져오기에 걸리는 시간을 기록합니다. 에이전트는 diagnostics.profile로 읽습니다.",
|
||||
"prefs.project_options_dialog": "Project 옵션 대화상자",
|
||||
"prefs.project_options_dialog_note": "켜기: 투영 전에 지오메트리 또는 바디와 투영 링크를 선택합니다. 끄기: 기존 직접 선택 작업 흐름을 유지합니다.",
|
||||
"prefs.quality_default": "기본값",
|
||||
@@ -1623,14 +1734,19 @@
|
||||
"prefs.quality_slowest": "가장 느림",
|
||||
"prefs.quality_smoother": "더 부드럽게",
|
||||
"prefs.reports_saved": "{n}개 저장됨",
|
||||
"prefs.restore_page_defaults": "페이지 기본값 복원",
|
||||
"prefs.ribbon_click_ends_tool": "실행 중인 도구의 버튼을 클릭하면 도구 종료",
|
||||
"prefs.ribbon_click_ends_tool_note": "사용 중인 스케치 도구의 강조된 리본 버튼을 클릭하면 도구가 종료되어 뷰에서 클릭하면 다시 선택됩니다. 끄기: 클릭하면 도구가 다시 시작됩니다.",
|
||||
"prefs.right_click": "오른쪽 클릭",
|
||||
"prefs.saving": "자동 저장 및 복구",
|
||||
"prefs.saving": "시작 및 저장",
|
||||
"prefs.scale_note": "창에서 Ctrl + / Ctrl −로도 조절합니다. 버튼은 100%에서 28 px입니다.",
|
||||
"prefs.scale_text": "배율 및 텍스트",
|
||||
"prefs.search": "설정 검색",
|
||||
"prefs.search_empty": "일치하는 설정이 없습니다",
|
||||
"prefs.select": "선택",
|
||||
"prefs.select_one_item": "항목 하나 선택",
|
||||
"prefs.select_other_hidden": "다른 항목 선택으로 숨겨진 형상 선택",
|
||||
"prefs.select_other_hidden_help": "켜짐: 포인터 근처의 숨겨진 바디, 스케치 및 가려진 모서리를 포함합니다. 꺼짐: 포인터 바로 아래에 보이는 형상만 포함하는 기존 목록을 사용합니다.",
|
||||
"prefs.selecting": "선택",
|
||||
"prefs.shift": "Shift",
|
||||
"prefs.shift_add_ctrl_toggle": "Shift는 추가만, Ctrl / Cmd는 토글",
|
||||
@@ -1656,6 +1772,8 @@
|
||||
"prefs.sketch_tab_always_note": "스케치가 열려 있지 않을 때도 스케치 탭이 리본에서 모델 바로 다음에 남아 있습니다. 그 그리기 도구는 선택한 평면이나 면, 또는 다음에 고르는 평면에서 스케치를 시작하고 그 안에서 도구를 바로 쓸 수 있게 합니다.",
|
||||
"prefs.sketch_tracking_guides": "중심 및 중점 정렬 안내선",
|
||||
"prefs.sketch_tracking_guides_note": "원 중심이나 직선 모서리의 중점에 마우스를 올린 다음 수평 또는 수직으로 이동하면 임시 점선 안내선이 표시됩니다.",
|
||||
"prefs.sketch_tracking_points": "그리는 동안 끝점과 모서리에서도 정렬 안내선",
|
||||
"prefs.sketch_tracking_points_note": "그리기 도구가 점을 기다리는 동안 선이나 호의 끝점, 직사각형 모서리, 자유 점에 마우스를 올리면 중심이나 중점과 같은 임시 점선 안내선이 표시됩니다. 끄면: 중심과 중점만.",
|
||||
"prefs.sketching": "스케치",
|
||||
"prefs.snap_drawing_note": "스케치 점을 그리거나 드래그하는 동안.",
|
||||
"prefs.source": "소스",
|
||||
@@ -1667,6 +1785,24 @@
|
||||
"prefs.status_error": "오류",
|
||||
"prefs.status_selected": "선택됨",
|
||||
"prefs.status_warning": "경고",
|
||||
"prefs.structure.appearance": "모양",
|
||||
"prefs.structure.appearance_note": "색상, 텍스트 및 접근성.",
|
||||
"prefs.structure.in_profile": "프로필에 포함",
|
||||
"prefs.structure.layout": "레이아웃",
|
||||
"prefs.structure.layout_note": "창 구성 요소의 위치.",
|
||||
"prefs.structure.look": "보기",
|
||||
"prefs.structure.measure_pointer": "측정 및 포인터",
|
||||
"prefs.structure.mouse3d": "3D 마우스",
|
||||
"prefs.structure.mouse3d_note": "SpaceMouse 및 SpaceNavigator.",
|
||||
"prefs.structure.navigation_note": "마우스와 트랙패드. 3D 마우스는 별도 페이지에 있습니다.",
|
||||
"prefs.structure.profile": "프로필",
|
||||
"prefs.structure.profile_edited": "저장 후 변경됨",
|
||||
"prefs.structure.profile_hover": "설정 프로필에 저장됨",
|
||||
"prefs.structure.scale_percent": "{percent}%",
|
||||
"prefs.structure.sketch": "스케치",
|
||||
"prefs.structure.tools": "도구",
|
||||
"prefs.structure.tools_note": "모델링 도구의 동작.",
|
||||
"prefs.structure.work": "작업",
|
||||
"prefs.supersampling_brief": "뷰를 더 크게 그린 뒤 줄여서 필터링합니다. 매우 선명하지만 더 무겁습니다.",
|
||||
"prefs.tab_accent_note": "강조 색상이 활성 탭을 따라갑니다",
|
||||
"prefs.theme_file": "테마 파일",
|
||||
@@ -1676,6 +1812,16 @@
|
||||
"prefs.theme_follow_note": "다른 도구가 파일을 다시 쓸 때마다 다시 불러옵니다. 테마를 선택하면 따르기를 중지합니다.",
|
||||
"prefs.theme_follow_off": "끄기",
|
||||
"prefs.theme_follow_stop": "따르기 중지",
|
||||
"prefs.theme_option_opacity": "불투명도",
|
||||
"prefs.theme_option_opacity_help": "플로팅 패널의 불투명도를 조정합니다. 뷰포트 색상 위에서도 텍스트를 읽을 수 있도록 최솟값이 적용됩니다.",
|
||||
"prefs.theme_option_tab_colours": "탭 색상",
|
||||
"prefs.theme_option_tab_colours_help": "각 리본 탭에 이 테마의 패널 및 뷰포트 색상을 사용합니다.",
|
||||
"prefs.theme_option_tab_fade_ms": "탭 전환",
|
||||
"prefs.theme_option_tab_fade_ms_help": "탭을 전환할 때 패널 및 뷰포트 색상을 서서히 변경합니다. 0이면 즉시 전환합니다.",
|
||||
"prefs.theme_options_badge": "옵션",
|
||||
"prefs.theme_options_more": "더 보기 · {count}",
|
||||
"prefs.theme_options_reset": "초기화",
|
||||
"prefs.theme_options_unavailable": "이 기기에서는 사용할 수 없음",
|
||||
"prefs.timeline_half_hour": "−30분",
|
||||
"prefs.timeline_now": "지금",
|
||||
"prefs.timeline_quarter_hour": "−15",
|
||||
@@ -1695,10 +1841,26 @@
|
||||
"prefs.under_cursor": "커서 아래",
|
||||
"prefs.units_precision": "단위 및 정밀도",
|
||||
"prefs.updates": "업데이트",
|
||||
"prefs.validation": "바디 검사",
|
||||
"prefs.validation.each_feature": "피처마다 한 번",
|
||||
"prefs.validation.each_step": "모든 단계 후",
|
||||
"prefs.validation.label": "커널 전체 검사",
|
||||
"prefs.validation.off": "끄기 (실험적)",
|
||||
"prefs.validation_note": "모든 단계 후: 스윕, 불리언, 블렌드마다 검사합니다.\n피처마다 한 번: 각 피처의 결과를 한 번 검사합니다.\n끄기 (실험적): 검사하지 않습니다.",
|
||||
"prefs.value_boxes_in_view": "뷰에서 핸들 옆에 값 표시",
|
||||
"prefs.value_boxes_in_view_tip": "명령의 값(거리, 반경 등)을 모델 위 핸들 옆 상자에 표시합니다. 끄면 값은 명령 창에 입력하며, 선택 후 키보드 입력이 그 창으로 가고 모델을 가리는 것이 없습니다.",
|
||||
"prefs.view": "뷰 동작",
|
||||
"prefs.view_confirm_corner": "뷰 모서리에 확인 및 취소",
|
||||
"prefs.view_confirm_corner_note": "대화 상자나 스케치가 열려 있는 동안 뷰 오른쪽 위의 체크와 X 표시가 대화 상자의 확인과 취소와 같은 동작을 하거나 스케치를 마치고 취소합니다. 뷰 탐색기는 그 아래로 내려갑니다.",
|
||||
"prefs.viewport": "뷰포트",
|
||||
"prefs.viewport_analysis_status": "뷰포트 해석 상태 표시",
|
||||
"prefs.viewport_analysis_status_help": "활성 해석을 뷰포트 위에 표시하고 각 항목에서 범례를 엽니다. Esc로 최신 해석을 끕니다. 끄면 모서리 범례와 이전 Esc 동작으로 돌아갑니다.",
|
||||
"prefs.viewport_section_status": "뷰포트 단면 상태 표시",
|
||||
"prefs.viewport_section_status_help": "뷰포트에 단면 설정을 표시합니다. Escape 키와 활성 리본 버튼으로 단면을 끕니다. 비활성화하면 이전 단면 조작을 유지합니다.",
|
||||
"prefs.viewport_status_details": "뷰포트 실시간 상세 정보",
|
||||
"prefs.viewport_status_details_help": "표준 상세 카드에서 활성 분석 및 단면 설정을 편집합니다. 끄면 읽기 전용 분석 범례와 단면 대화상자로 돌아갑니다.",
|
||||
"prefs.viewport_status_summary": "뷰포트 상태 항목 요약",
|
||||
"prefs.viewport_status_summary_help": "상태 항목이 3개 이상이면 하나의 요약 배지로 표시합니다. 비활성화하면 모든 항목을 기존 행에 표시합니다.",
|
||||
"prefs.wheel": "휠",
|
||||
"prefs.wheel_trackpad": "휠 및 트랙패드",
|
||||
"prefs.workspace": "작업 공간",
|
||||
@@ -1716,6 +1878,17 @@
|
||||
"recovery.just_now": "방금",
|
||||
"recovery.min_ago": "{count}분 전",
|
||||
"recovery.never_saved": "저장된 적 없음",
|
||||
"refused.change": "{body}을(를) 변경하지 못했습니다.",
|
||||
"refused.cut": "{body}을(를) 자르지 못했습니다.",
|
||||
"refused.face_curved": "곡면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_flat": "평면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_freeform": "자유 곡면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.face_rounded": "둥근 면 중 하나를 모델링에 사용할 수 없습니다.",
|
||||
"refused.faces_apart": "면들이 충분히 맞물리지 않아 모델링에 사용할 수 없습니다.",
|
||||
"refused.intersect": "{body}과(와) 교차하지 못했습니다.",
|
||||
"refused.join": "{body}에 결합하지 못했습니다.",
|
||||
"refused.repair": "부품 복구",
|
||||
"refused.show_face": "면 보기",
|
||||
"render.add_ground": "Add fitted ground to setup",
|
||||
"render.all_shots_label": "모든 샷",
|
||||
"render.appearance_editor.absorption": "흡수",
|
||||
@@ -1760,6 +1933,10 @@
|
||||
"render.appearances": "외관",
|
||||
"render.assign_help": "클릭하여 선택 항목에 적용하거나 드래그하여 미리 보고 놓으세요. 선택이 없으면 칠하기를 시작합니다.",
|
||||
"render.background": "장면 배경",
|
||||
"render.batch_all_shots": "모든 샷 ({count})",
|
||||
"render.batch_empty": "모든 샷을 렌더링하기 전에 샷을 저장하세요.",
|
||||
"render.batch_limit": "한 번에 최대 32개의 샷을 렌더링할 수 있습니다.",
|
||||
"render.batch_status": "{running}개 실행 중 · {waiting}개 대기 중",
|
||||
"render.bloom_enable": "표시 블룸",
|
||||
"render.bloom_help": "양의 선형 하이라이트를 세 번의 정규화 필터로 확산하는 출력 전용 효과입니다. 반경은 출력 픽셀 단위이며 sRGB 전에 더합니다. 렌즈·볼륨·수렴 개선이 아니며 원본 EXR은 변하지 않습니다.",
|
||||
"render.bloom_opaque": "블룸에는 불투명 출력이 필요합니다. 투명 PNG를 몰래 합성하지 않고 거부합니다.",
|
||||
@@ -1851,6 +2028,13 @@
|
||||
"render.children.workstation": "워크스테이션",
|
||||
"render.choose_look": "먼저 외관 라이브러리에서 외관을 선택하세요",
|
||||
"render.clear_stage": "Remove saved stage",
|
||||
"render.compare_divider": "비교 구분선",
|
||||
"render.compare_live": "라이브",
|
||||
"render.compare_live_preview": "라이브 미리보기",
|
||||
"render.compare_render": "렌더 {number}",
|
||||
"render.compare_side": "렌더 {number} · {shot}",
|
||||
"render.compare_summary": "{shot} · {width} × {height} · PNG · {seconds}초 · {renderer} · 리비전 {revision}에서 캡처",
|
||||
"render.compare_swap": "맞바꾸기",
|
||||
"render.cpu_budget": "CPU 리소스 프로필",
|
||||
"render.cpu_budget_bodies": "바디 수 상한",
|
||||
"render.cpu_budget_compact": "컴팩트 (기존 제한)",
|
||||
@@ -2263,7 +2447,9 @@
|
||||
"render.output_ready": "렌더링 준비 완료",
|
||||
"render.output_run_check": "검사 실행",
|
||||
"render.output_size_limit": "{method}: 한 변 최대 {dimension} px, 총 {pixels} 픽셀. 가능한 최대 크기: {width} × {height}.",
|
||||
"render.output_size_method_profile": "{width} × {height}을(를) 렌더링하려면 {method}와(과) {profile} CPU 리소스 프로필을 선택하거나 더 작은 크기를 선택하세요.",
|
||||
"render.output_size_ok": "크기 정상 · {width} × {height} px",
|
||||
"render.output_size_profile": "{width} × {height}을(를) 렌더링하려면 {profile} CPU 리소스 프로필이나 더 작은 크기를 선택하세요.",
|
||||
"render.output_tools_inspector": "출력 검사기에서 크기와 형식을 편집하세요.",
|
||||
"render.output_view_image": "이미지 보기",
|
||||
"render.paint_label": "칠하기",
|
||||
@@ -2317,6 +2503,10 @@
|
||||
"render.result_actual": "100%",
|
||||
"render.result_actual_size": "100%",
|
||||
"render.result_reduced_display": "표시 미리보기를 뷰포트 장치 또는 캐시 제한에 맞춰 축소했습니다. 보관된 PNG와 내보내기는 전체 해상도를 유지합니다. 이 축소 보기에서는 원본 100% 검사가 불가능합니다.",
|
||||
"render.results_choose": "보관된 결과",
|
||||
"render.results_label": "렌더 {number} · {shot} · {seconds}초",
|
||||
"render.results_stale_label": "{result} [이전 버전]",
|
||||
"render.results_unnamed": "이미지",
|
||||
"render.return_design": "설계로 돌아가기",
|
||||
"render.ribbon_backgrounds": "배경",
|
||||
"render.ribbon_check": "검사",
|
||||
@@ -2395,6 +2585,7 @@
|
||||
"render.size_custom": "사용자 지정…",
|
||||
"render.size_full_hd": "풀 HD",
|
||||
"render.size_hd": "HD",
|
||||
"render.size_needs_profile": "{size} · {profile} 필요",
|
||||
"render.size_presets": "크기 프리셋",
|
||||
"render.size_presets_pending": "출력 편집기와 함께 제공 예정",
|
||||
"render.size_preview": "미리보기",
|
||||
@@ -2616,6 +2807,24 @@
|
||||
"sheet.flange_type_base": "베이스",
|
||||
"sheet.flange_type_contour": "윤곽",
|
||||
"sheet.flange_type_edge": "모서리",
|
||||
"sheet.personal_rules.confirm_delete": "삭제 확인",
|
||||
"sheet.personal_rules.delete": "삭제",
|
||||
"sheet.personal_rules.delete_prompt": "이 개인 규칙을 삭제할까요? 설계에 이미 있는 규칙은 바뀌지 않습니다.",
|
||||
"sheet.personal_rules.full": "개인 라이브러리에 이미 규칙이 64개 있습니다.",
|
||||
"sheet.personal_rules.invalid_name": "줄 바꿈이나 제어 문자가 없는 짧은 이름을 입력하세요.",
|
||||
"sheet.personal_rules.invalid_values": "규칙 값을 모두 입력하세요. 각 값은 이 설계에서 계산할 수 있어야 하며, 두께는 양수, K 계수는 0에서 1 사이여야 합니다.",
|
||||
"sheet.personal_rules.missing": "선택한 개인 규칙을 더 이상 사용할 수 없습니다.",
|
||||
"sheet.personal_rules.name": "규칙 이름",
|
||||
"sheet.personal_rules.name_exists": "같은 이름의 개인 규칙이 이미 있습니다. 다른 이름을 사용하세요.",
|
||||
"sheet.personal_rules.name_hint": "고유한 이름",
|
||||
"sheet.personal_rules.overwrite": "선택한 개인 규칙에 변경 사항 저장",
|
||||
"sheet.personal_rules.rename": "이름 바꾸기",
|
||||
"sheet.personal_rules.rule": "개인 규칙",
|
||||
"sheet.personal_rules.save": "새로 저장",
|
||||
"sheet.personal_rules.scope": "모든 설계에서 쓸 수 있도록 프로필에 저장됩니다.",
|
||||
"sheet.personal_rules.select": "규칙 선택",
|
||||
"sheet.personal_rules.title": "개인 규칙",
|
||||
"sheet.personal_rules.unsupported": "이 규칙에는 이 버전에서 사용할 수 없는 값이 있습니다.",
|
||||
"shortcut.json_error": "매개변수는 JSON 형식이어야 합니다: {error}",
|
||||
"shortcut.no_command": "`{id}` 명령이 없습니다. JSON 앞의 텍스트는 명령 ID여야 합니다",
|
||||
"shortcut.pin": "S 상자 맨 위에 고정",
|
||||
@@ -2677,7 +2886,18 @@
|
||||
"the section's profiles must be in one sketch": "단면의 프로파일은 하나의 스케치 안에 있어야 합니다",
|
||||
"the two origins are on the same component": "두 원점이 같은 구성요소에 있습니다",
|
||||
"thread.automatic_size": "자동 크기",
|
||||
"thread.chamfer_depth_needed": "이 형상에 필요",
|
||||
"thread.designation": "호칭",
|
||||
"thread.drill_range": "탭 제조사의 드릴은 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
|
||||
"thread.drill_range_custom": "드릴은 바깥지름에서 피치를 뺀 {drill} mm입니다. 암나사의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
|
||||
"thread.drill_range_taper": "탭 제조사의 드릴은 {drill} mm입니다. 나사 끝에서의 규격 골지름은 {minor} mm입니다. {low}~{high} mm 드릴을 사용할 수 있습니다.",
|
||||
"thread.end_square": "직각",
|
||||
"thread.fade_short": "1.25 P (ISO 3508)",
|
||||
"thread.fade_standard": "2.5 P (ISO 3508)",
|
||||
"thread.form_bspp": "관용 평행나사 G (ISO 228-1)",
|
||||
"thread.form_bspt": "관용 테이퍼나사 R, Rc (ISO 7-1)",
|
||||
"thread.form_npt": "관용 테이퍼나사 NPT (ASME B1.20.1)",
|
||||
"thread.form_rp": "관용 평행 암나사 Rp (ISO 7-1)",
|
||||
"thread.iso_metric": "ISO 미터",
|
||||
"thread.select_size": "크기 선택",
|
||||
"timeline.feature_hint": "두 번 클릭하여 편집 · 드래그하여 순서 변경 · 마우스 오른쪽 버튼을 클릭하여 더 보기",
|
||||
@@ -2800,6 +3020,8 @@
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "캡을 시계 반대 방향으로 돌려 유지한 다음 놓으세요.",
|
||||
"ui.3d_mouse_switch_cap": "중앙 복귀형으로 보정",
|
||||
"ui.3d_mouse_switch_trackball": "트랙볼로 보정",
|
||||
"ui.3d_mouse_use_direct": "직접 드라이버 사용",
|
||||
"ui.3d_mouse_use_direct_tip": "spacenavd를 거치지 않고 3D 마우스를 직접(hidraw) 읽습니다.",
|
||||
"ui.3d_mouse_view_axis": "뷰 축",
|
||||
"ui.3d_mouse_view_off": "끔",
|
||||
"ui.3d_mouse_view_off_tip": "3D 마우스 드라이버 프로필이 이 축을 끄므로, 이 축의 동작으로는 뷰가 움직이지 않습니다. 여기서 다른 축의 동작을 고르면 둘이 바뀌고, 그 동작은 더 이상 뷰를 움직이지 않습니다.",
|
||||
@@ -2836,12 +3058,15 @@
|
||||
"ui.agent_chat_auth_terminal": "터미널에서 이것을 실행한 뒤 다시 연결을 누르세요:",
|
||||
"ui.agent_chat_auth_title": "에이전트에 로그인",
|
||||
"ui.agent_chat_cancel": "취소",
|
||||
"ui.agent_chat_cancel_timeout": "에이전트가 중지 요청에 응답하지 않아 연결이 끊어졌습니다. 계속하려면 다시 연결하세요.",
|
||||
"ui.agent_chat_cannot_start": "설치되어 있지만 시작되지 않습니다: {error}",
|
||||
"ui.agent_chat_changes": "변경 {n}개, 실행 취소 한 번",
|
||||
"ui.agent_chat_changes_of": "변경 {n}/{limit}",
|
||||
"ui.agent_chat_close": "닫기",
|
||||
"ui.agent_chat_command": "에이전트 명령",
|
||||
"ui.agent_chat_connect": "연결",
|
||||
"ui.agent_chat_copy": "복사",
|
||||
"ui.agent_chat_custom_heading": "사용자 지정 모델",
|
||||
"ui.agent_chat_custom_hint": "표준 입출력으로 Agent Client Protocol을 사용하는 모든 프로그램. 셸 없이 실행됩니다.",
|
||||
"ui.agent_chat_denied": "거부함",
|
||||
"ui.agent_chat_deny": "거부",
|
||||
@@ -2883,7 +3108,15 @@
|
||||
"ui.agent_chat_mode_all": "모두",
|
||||
"ui.agent_chat_mode_ask": "확인",
|
||||
"ui.agent_chat_mode_edits": "편집",
|
||||
"ui.agent_chat_model_custom": "직접 입력 사용: {model}",
|
||||
"ui.agent_chat_model_rejected": "에이전트가 {model}을(를) 받아들이지 않았습니다: {error}",
|
||||
"ui.agent_chat_model_search": "모델 검색…",
|
||||
"ui.agent_chat_model_tip": "에이전트가 사용하는 모델입니다. 선택은 이 에이전트에 기억됩니다.",
|
||||
"ui.agent_chat_models_failed": "모델 목록을 가져올 수 없습니다: {error}",
|
||||
"ui.agent_chat_models_heading": "사용 가능한 모델",
|
||||
"ui.agent_chat_models_listing": "모델 목록을 가져오는 중…",
|
||||
"ui.agent_chat_models_none": "엔드포인트에 나열된 모델이 없습니다",
|
||||
"ui.agent_chat_models_not_a_list": "응답이 모델 목록이 아닙니다",
|
||||
"ui.agent_chat_new": "새 채팅",
|
||||
"ui.agent_chat_new_confirm": "새 채팅을 시작할까요? 현재 채팅은 지워집니다.",
|
||||
"ui.agent_chat_next_picture": "다음 이미지",
|
||||
@@ -2891,6 +3124,7 @@
|
||||
"ui.agent_chat_no_control": "에이전트 채팅 엔드포인트가 시작되지 않아 에이전트가 디자인에 접근할 수 없습니다.",
|
||||
"ui.agent_chat_no_images": "이 에이전트는 이미지를 받지 않습니다",
|
||||
"ui.agent_chat_no_picture": "클립보드에 이미지가 없습니다",
|
||||
"ui.agent_chat_node_too_old": "Node.js {version}은 이 에이전트에 사용하기에는 너무 오래된 버전입니다. Node.js 20.10 이상을 설치한 후 새로 고침을 누르세요.",
|
||||
"ui.agent_chat_none_found": "코딩 에이전트를 찾지 못했습니다. 다음 중 하나를 설치한 뒤 새로 고침을 누르세요:",
|
||||
"ui.agent_chat_none_found_short": "에이전트를 찾지 못함",
|
||||
"ui.agent_chat_not_installed": "설치되지 않음",
|
||||
@@ -2912,9 +3146,12 @@
|
||||
"ui.agent_chat_queued_stopped": "대기 중 (중지됨, 보내지 않음)",
|
||||
"ui.agent_chat_refresh": "새로 고침",
|
||||
"ui.agent_chat_refused": "에이전트에게 허용되지 않음: {reason}",
|
||||
"ui.agent_chat_reinstall": "{agent}이(가) 설치되어 있지만({program}) SolidCraft가 확인할 때 시작되지 않았습니다: {error}\n설치가 완료되지 않았거나 다른 종류의 컴퓨터용일 수 있습니다. `{install}`(으)로 다시 설치하고, 터미널에서 버전이 출력되는지 확인한 뒤 새로 고침을 누르세요.",
|
||||
"ui.agent_chat_remove": "대기열에서 제거",
|
||||
"ui.agent_chat_remove_file": "이 메시지에서 빼기",
|
||||
"ui.agent_chat_remove_imported": "이 메시지에서 빼기. 부품은 디자인에 남습니다 (실행 취소하면 제거됨)",
|
||||
"ui.agent_chat_reply_cut_off": "응답이 끝나기 전에 길이 한도에 도달해 불완전할 수 있습니다. 에이전트에게 계속하거나 더 짧게 답하라고 요청하세요.",
|
||||
"ui.agent_chat_reply_declined": "모델이 이 메시지에 대한 답변을 거절했습니다.",
|
||||
"ui.agent_chat_right": "오른쪽",
|
||||
"ui.agent_chat_save": "저장",
|
||||
"ui.agent_chat_send": "보내기",
|
||||
@@ -2968,12 +3205,17 @@
|
||||
"ui.ai_copy_suffix": "복사본",
|
||||
"ui.ai_custom_url": "사용자 지정 URL",
|
||||
"ui.ai_default": "기본값",
|
||||
"ui.ai_edit": "편집",
|
||||
"ui.ai_edit_agent_title": "에이전트 편집",
|
||||
"ui.ai_edit_gone": "이 에이전트는 제거되었으므로 저장할 내용이 없습니다.",
|
||||
"ui.ai_edit_tip": "이 에이전트의 이름, 명령 또는 엔드포인트를 변경합니다. 저장된 키는 표시되지 않습니다.",
|
||||
"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_keep_hint": "저장된 키를 유지하려면 비워 두세요",
|
||||
"ui.ai_key_needs_https": "키는 HTTPS를 통해서만, 또는 이 컴퓨터로만 전송됩니다.",
|
||||
"ui.ai_key_note": "SolidCraft 설정이 아닌 시스템 키체인에 저장됩니다. 로컬 서버라면 비워 두세요.",
|
||||
"ui.ai_keychain_error": "시스템 키체인이 거부했습니다: {error}",
|
||||
@@ -3000,6 +3242,7 @@
|
||||
"ui.ai_preset": "프리셋",
|
||||
"ui.ai_remove": "제거",
|
||||
"ui.ai_reset": "재설정",
|
||||
"ui.ai_save": "저장",
|
||||
"ui.ai_settings": "AI 설정…",
|
||||
"ui.ai_show_features": "AI 기능 표시",
|
||||
"ui.ai_show_features_note": "에이전트 채팅: 이미 사용 중인 코딩 에이전트나 선택한 모델 엔드포인트가 열린 디자인을 다루는 패널입니다. 끄면 AI 버튼, 단축키, 메뉴 항목, 패널이 없고 아무것도 찾거나 시작하지 않습니다.",
|
||||
@@ -3063,6 +3306,7 @@
|
||||
"ui.circle": "원",
|
||||
"ui.circular_pattern": "원형 패턴",
|
||||
"ui.clear": "지우기",
|
||||
"ui.clear_print_face": "출력 바닥면 해제",
|
||||
"ui.clear_recent": "최근 항목 지우기",
|
||||
"ui.clear_the_selection": "선택 해제",
|
||||
"ui.click_selects_whole_bodies": "클릭하면 바디 전체 선택",
|
||||
@@ -3087,10 +3331,16 @@
|
||||
"ui.colour_vision_sample": "예시",
|
||||
"ui.colour_vision_tritan": "청색각이상 (Tritan)",
|
||||
"ui.colour_vision_tritan_tip": "파란색 감각이 약하거나 없는 경우",
|
||||
"ui.comb_length": "빗 길이",
|
||||
"ui.comb_length_tip": "빗살의 길이: 자동 길이의 배수입니다. 자동 길이에서는 가장 긴 빗살이 표시된 곡선 크기의 5분의 1이 됩니다.",
|
||||
"ui.comb_teeth": "빗살 수",
|
||||
"ui.comb_teeth_tip": "각 빗의 빗살 수이며, 곡선을 따라 같은 간격으로 놓입니다. 빗살이 많을수록 곡률이 어떻게 바뀌는지 더 자세히 보입니다.",
|
||||
"ui.combine": "결합",
|
||||
"ui.coming_later": "추후 지원 예정",
|
||||
"ui.command_done": "{command} 완료",
|
||||
"ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
|
||||
"ui.components_from_bodies_hint": "활성 구성요소 아래에 선택한 각 바디와 같은 이름의 구성요소를 만듭니다.",
|
||||
"ui.components_from_bodies_required": "바디를 하나 이상 선택하세요.",
|
||||
"ui.concentric": "동심",
|
||||
"ui.cone": "원뿔",
|
||||
"ui.conic_curve": "원뿔 곡선",
|
||||
@@ -3103,15 +3353,30 @@
|
||||
"ui.construction.contact_hint": "접선: 원/원호 또는 열린 스플라인/원뿔 곡선 선택 후 접점 선택 (자유 곡선은 끝점만)",
|
||||
"ui.construction.external_hint": "접선: 원 또는 원호, 외부 점, 원하는 해를 차례로 선택",
|
||||
"ui.construction.free_hint": "보조 직선: 두 점 클릭; 마우스 오른쪽 버튼으로 다시 시작",
|
||||
"ui.construction.normal_needs_length": "무한 직선은 항상 보조선입니다. 일반 선을 그리려면 길이를 입력하세요",
|
||||
"ui.construction.point_hint": "보조 직선: 통과할 점 클릭",
|
||||
"ui.construction.reference_hint": "보조 직선: 직선 선택 후 점 선택; 마우스 오른쪽 버튼으로 참조 변경",
|
||||
"ui.construction.ribbon_label": "보조선",
|
||||
"ui.construction.side_hint": "보조 직선: 선이 뻗어 나갈 쪽 클릭; 마우스 오른쪽 버튼으로 다시 시작",
|
||||
"ui.context_menu": "컨텍스트 메뉴",
|
||||
"ui.continuity_crease": "꺾임",
|
||||
"ui.continuity_curvature": "곡률",
|
||||
"ui.continuity_curvature_change": "곡률 변화",
|
||||
"ui.continuity_curvature_change_tip": "곡률 변화(G3): 이음매에서 곡률의 변화도 부드럽습니다.",
|
||||
"ui.continuity_curvature_tip": "곡률 연속(G2): 반사가 꺾임 없이 이어집니다.",
|
||||
"ui.continuity_g0": "G0 꺾임",
|
||||
"ui.continuity_g1": "G1 접선",
|
||||
"ui.continuity_g2": "G2 곡률",
|
||||
"ui.continuity_gap": "틈",
|
||||
"ui.continuity_position": "위치",
|
||||
"ui.continuity_position_tip": "위치(G0): 면이 만나지만, 만나는 곳에 꺾임이 있습니다.",
|
||||
"ui.continuity_smooth_curvature_change": "부드러운 곡률 변화",
|
||||
"ui.continuity_smooth_curvature_change_tip": "부드러운 곡률 변화(G4): 이음매에서 곡률이 변하는 방식까지 부드럽게 이어집니다.",
|
||||
"ui.continuity_tangent": "접선",
|
||||
"ui.continuity_tangent_tip": "접선(G1): 꺾임은 없지만, 이음매에서 곡률이 급변합니다.",
|
||||
"ui.copy": "복사",
|
||||
"ui.copy_details": "세부 정보 복사",
|
||||
"ui.copy_details_tip": "찾아보거나 보고할 수 있도록 전체 메시지를 복사합니다.",
|
||||
"ui.corner": "코너",
|
||||
"ui.corners": "코너",
|
||||
"ui.counterbore": "카운터보어",
|
||||
@@ -3131,6 +3396,7 @@
|
||||
"ui.dark": "어둡게",
|
||||
"ui.default_modelling_orientation": "기본 모델링 방향",
|
||||
"ui.delete": "삭제",
|
||||
"ui.delete_select_first": "먼저 삭제할 항목을 선택하세요: 피처, 바디, 컴포넌트 또는 스케치 형상",
|
||||
"ui.depth": "깊이",
|
||||
"ui.design": "디자인",
|
||||
"ui.design_properties.author": "작성자",
|
||||
@@ -3144,6 +3410,40 @@
|
||||
"ui.design_properties.part_number": "부품 번호",
|
||||
"ui.design_properties.project": "프로젝트",
|
||||
"ui.design_properties.revision": "리비전",
|
||||
"ui.details_access_caption": "선택한 방향에서 공구가 각 면에 도달할 수 있는지 표시합니다.",
|
||||
"ui.details_angle": "각도",
|
||||
"ui.details_continuity_caption": "선택한 이음부를 편집합니다. 다른 이음부는 각자의 설정을 유지합니다.",
|
||||
"ui.details_continuity_stale": "이전 형상입니다. 이 이음부를 편집하거나 화면에 맞추려면 분석을 다시 실행하세요.",
|
||||
"ui.details_copy_list": "목록 복사",
|
||||
"ui.details_curvature": "곡률 공차",
|
||||
"ui.details_curvature_caption": "각 면의 국소 곡률 반지름에 따라 색상이 표시됩니다.",
|
||||
"ui.details_direction": "공구 방향",
|
||||
"ui.details_draft_caption": "금형 분리 방향에 대한 각 면의 각도에 따라 색상이 표시됩니다.",
|
||||
"ui.details_edit_seam": "이음부 편집",
|
||||
"ui.details_fixture": "UI 구성 요소 예시 — 샘플 데이터만 표시",
|
||||
"ui.details_fixture_caption": "설명용 값을 사용하는 UI 구성 요소 예시입니다. 모델의 분석 결과가 아닙니다.",
|
||||
"ui.details_fixture_problem": "문제가 있는 결과 예시",
|
||||
"ui.details_fixture_readout": "측정값 예시",
|
||||
"ui.details_flip": "뒤집기",
|
||||
"ui.details_gap": "간격",
|
||||
"ui.details_hatch": "절단면 해칭",
|
||||
"ui.details_jump": "곡률 불연속",
|
||||
"ui.details_key": "범례",
|
||||
"ui.details_offset": "오프셋",
|
||||
"ui.details_plane": "평면",
|
||||
"ui.details_pull": "금형 분리 방향",
|
||||
"ui.details_radius": "반지름",
|
||||
"ui.details_readout": "측정값",
|
||||
"ui.details_samples": "샘플 수",
|
||||
"ui.details_seam": "{body} · 모서리 {edge}",
|
||||
"ui.details_settings": "설정",
|
||||
"ui.details_sharp_gap": "날카로운 이음 또는 간격",
|
||||
"ui.details_show_all": "전체 {count}개 표시",
|
||||
"ui.details_smooth": "매끄러움",
|
||||
"ui.details_stripes": "줄무늬",
|
||||
"ui.details_target": "형상에 맞게 보기 조정",
|
||||
"ui.details_worst_seams": "연속성이 가장 나쁜 이음부",
|
||||
"ui.details_zebra_caption": "반사된 줄무늬는 면이 얼마나 매끄럽게 이어지는지 보여 줍니다.",
|
||||
"ui.diameter": "지름",
|
||||
"ui.dimension": "치수",
|
||||
"ui.direct_interaction": "직접 조작",
|
||||
@@ -3188,8 +3488,10 @@
|
||||
"ui.edit_in_place_saved_elsewhere": "부품이 다른 파일에 저장되었습니다. 어셈블리는 여전히 {name}에 연결되어 있습니다",
|
||||
"ui.edit_in_place_wait": "디자인에서 실행 중인 작업이 끝날 때까지 기다리세요",
|
||||
"ui.edit_section_analysis": "단면 분석 편집",
|
||||
"ui.edit_sketch_empty": "편집할 스케치가 없습니다",
|
||||
"ui.ellipse": "타원",
|
||||
"ui.emboss_wrap_note": "지원되는 원통에 접하는 직교 선 프로파일은 자동으로 감깁니다. 대상을 고르려면 벽을 선택하세요.",
|
||||
"ui.end": "끝",
|
||||
"ui.end_radius": "끝 반지름",
|
||||
"ui.end_scale": "끝 배율",
|
||||
"ui.entities": "요소",
|
||||
@@ -3198,12 +3500,15 @@
|
||||
"ui.equation_explicit": "명시적",
|
||||
"ui.equation_form": "형식",
|
||||
"ui.equation_note": "변수는 단위가 없는 수입니다. sin(t)는 라디안으로 읽습니다. 단위가 없는 결과는 디자인 단위입니다.",
|
||||
"ui.equation_note_range": "변수는 범위와 같이 길이입니다. 길이 매개변수와 함께 쓸 수 있으며, sin()에는 길이로 나누어 넣습니다. 단위가 없는 결과는 디자인 단위로 읽습니다.",
|
||||
"ui.equation_parametric": "매개변수식",
|
||||
"ui.equation_variable_unit": "변수에 범위의 단위 사용",
|
||||
"ui.equation_x_of_t": "x(t)",
|
||||
"ui.equation_y_of_t": "y(t)",
|
||||
"ui.equation_y_of_x": "y(x)",
|
||||
"ui.equivalent_radius": "등가 반지름",
|
||||
"ui.expand": "펼치기",
|
||||
"ui.explode_needs_two_components": "분해 보기는 구성 요소를 서로 떼어 놓으므로 설계에 구성 요소가 두 개 이상 있어야 합니다(새 구성 요소, 설계 삽입 또는 바디로 구성 요소 만들기)",
|
||||
"ui.exportDxf": "DXF 내보내기…",
|
||||
"ui.export_ellipsis_keymap": "내보내기…",
|
||||
"ui.export_keymap_tip": "키, S 상자 즐겨찾기, 도구 모음을 파일로 내보내기",
|
||||
@@ -3211,11 +3516,39 @@
|
||||
"ui.extent_type": "범위 유형",
|
||||
"ui.extrude": "돌출",
|
||||
"ui.factor": "배율",
|
||||
"ui.fade": "페이드",
|
||||
"ui.fade_length": "페이드 길이",
|
||||
"ui.feature_history": "피처 기록",
|
||||
"ui.feature_options": "피처 옵션",
|
||||
"ui.features": "피처",
|
||||
"ui.file": "파일",
|
||||
"ui.fillet": "필렛",
|
||||
"ui.fillet_corner_ball": "볼",
|
||||
"ui.fillet_corner_ball_tip": "굴러가는 볼의 구면 일부로, 각 라운드에 접합니다(G1).",
|
||||
"ui.fillet_corner_curvature_tip": "곡률(G2): 부드러운 패치. 코너를 각 모서리를 따라 셋백만큼(입력하지 않으면 반지름의 {factor}배) 물려 패치로 채우므로, 반사가 꺾임 없이 지나갑니다.",
|
||||
"ui.fillet_corner_needs_constant": "부드러운 패치 코너(G2)에는 일정한 반지름과 정확한 반지름 맞춤이 필요합니다.",
|
||||
"ui.fillet_corner_patch": "부드러운 패치",
|
||||
"ui.fillet_corner_patch_tip": "곡률 연속(G2): 코너를 각 모서리를 따라 물려 패치로 채우므로, 반사가 꺾임 없이 지나갑니다.",
|
||||
"ui.fillet_corner_tangent_tip": "접선(G1): 굴러가는 볼 코너. 굴러가는 볼의 구면 일부로, 각 라운드에 접합니다.",
|
||||
"ui.fillet_error_corner": "여기에서는 부드러운 코너를 만들 수 없었습니다. 다른 셋백이나 접선 코너를 시도해 보세요.",
|
||||
"ui.fillet_error_failed": "이 모서리들에 이 필렛을 만들 수 없었습니다. 다른 반지름을 시도하거나 한 번에 둥글게 하는 모서리 수를 줄여 보세요.",
|
||||
"ui.fillet_error_setback_short": "이 코너의 패치에 비해 셋백이 너무 짧습니다. {least}부터 만들 수 있습니다. 더 긴 셋백을 시도해 보세요.",
|
||||
"ui.fillet_error_too_large": "이 모서리 옆의 면에 비해 반지름이 너무 큽니다. 더 작은 반지름을 시도하거나 '면 경계'를 '최대 반지름'으로 설정하세요.",
|
||||
"ui.fillet_fit": "면 경계",
|
||||
"ui.fillet_fit_exact": "정확한 반지름",
|
||||
"ui.fillet_fit_needs_constant": "반지름 맞춤에는 일정한 반지름이 필요합니다.",
|
||||
"ui.fillet_fit_reduce": "최대 반지름",
|
||||
"ui.fillet_fit_taper": "좁힌 반지름",
|
||||
"ui.fillet_fit_tip": "반지름이 모서리 옆 면이 허용하는 것보다 클 때.\n정확한 반지름: 지정한 반지름 그대로, 또는 둥글게 하지 않고 이유를 알리는 메시지.\n최대 반지름: 모든 모서리를 들어가는 가장 큰 반지름으로 둥글게 합니다.\n좁힌 반지름: 반지름을 유지하고 면이 모자란 곳에서만 라운드를 좁힙니다.",
|
||||
"ui.fillet_options_tip": "접선 체인: 선택한 모서리에서 부드럽게 이어지는 모서리도 둥글게 합니다.\n미리 보기: 편집하는 동안 결과를 뷰에 표시합니다.",
|
||||
"ui.fillet_setback": "셋백",
|
||||
"ui.fillet_setback_hint": "{factor} × 반지름",
|
||||
"ui.fillet_setback_least": "{least}보다 크게",
|
||||
"ui.fillet_setback_tip": "부드러운 코너 패치가 시작되는 위치: 코너에서 각 모서리를 따른 거리. 기본값은 반지름의 {factor}배. 최소 셋백(셋백 없이 라운드가 만나는 곳, 면이 서로 직각이면 반지름)보다 커야 합니다.",
|
||||
"ui.fillet_smooth_radius": "반지름 부드럽게 변화",
|
||||
"ui.fillet_type_choices_tip": "일정한 반지름: 모서리 전체에 하나의 반지름.\n현 길이: 면이 어떤 각도로 만나든 라운드의 폭이 같습니다.\n가변 반지름: 양 끝의 반지름과, 추가하면 모서리를 따라 더.\n곡률 연속: 평면과 만나는 곳에서 반사가 꺾이지 않는 단면(G2).\n스플라인: 모서리를 따라 반지름이 설정한 제어 반지름을 따릅니다.",
|
||||
"ui.fillet_type_curvature": "곡률 연속",
|
||||
"ui.fillet_type_tip": "모서리를 가로지르는 단면. 곡률 연속: 평면 사이의 곡률 연속(G2) 단면으로, 반사가 면에서 라운드로 꺾임 없이 이어집니다.",
|
||||
"ui.filter_commands_or_keys": "명령 또는 키 필터",
|
||||
"ui.findInTimeline": "타임라인에서 찾기",
|
||||
"ui.findInWindow": "창에서 찾기",
|
||||
@@ -3353,6 +3686,7 @@
|
||||
"ui.insert_design.saved_file": "{name}에 저장되지 않은 변경 사항이 있어 저장된 파일을 삽입했습니다. 저장한 다음 링크 업데이트로 변경 사항을 가져오세요.",
|
||||
"ui.insert_design.unsaved": "저장되지 않은 변경 사항",
|
||||
"ui.inspect": "검사",
|
||||
"ui.instances": "인스턴스",
|
||||
"ui.intersect": "교차",
|
||||
"ui.isolate": "분리 표시",
|
||||
"ui.join": "접합",
|
||||
@@ -3361,12 +3695,14 @@
|
||||
"ui.joints": "조인트",
|
||||
"ui.keyboard_shortcuts": "키보드 단축키",
|
||||
"ui.language": "언어",
|
||||
"ui.lasso_select": "올가미(자유롭게 둘러 그리기: 완전히 안쪽에 있는 것을 선택)",
|
||||
"ui.layout_compact": "컴팩트",
|
||||
"ui.layout_floating": "플로팅",
|
||||
"ui.length": "길이",
|
||||
"ui.length_decimals": "길이 소수점 자릿수",
|
||||
"ui.light": "밝게",
|
||||
"ui.line": "선",
|
||||
"ui.line_arc_hint": "접선 호: 호의 끝점을 클릭합니다. 선으로 돌아가려면 마지막 점에 다시 머무르고, 끝내려면 오른쪽 클릭 또는 Esc를 누릅니다",
|
||||
"ui.lock": "잠금",
|
||||
"ui.loft": "로프트",
|
||||
"ui.look_at": "바라보기",
|
||||
@@ -3445,7 +3781,9 @@
|
||||
"ui.part_size_needs_a_part": "크기를 바꿀 삽입된 표준 부품(나사, 너트, 와셔 또는 핀)을 선택하십시오. 바디를 키우거나 줄이려면 축척을 사용하십시오.",
|
||||
"ui.part_size_shared": "부품을 다시 빌드하며 추가된 피처는 모두 대체됩니다. 이 구성요소의 모든 인스턴스가 업데이트됩니다.",
|
||||
"ui.paste": "붙여넣기",
|
||||
"ui.patch_continuity_corner_tip": "패치는 각 변을 따라 주변 면과 선택한 연속성으로 이어집니다. 주변 면끼리 서로 다른 곡률로 만나는 모서리에서는 그곳에서 접선(G1)으로 유지됩니다.",
|
||||
"ui.path": "경로",
|
||||
"ui.pattern_object_type_tip": "바디: 뷰나 브라우저에서 선택하는 솔리드 형상이며, 각각 통째로 복사됩니다.\n피처: 돌출이나 구멍 같은 기록의 단계이며, 기록에서 또는 그 단계가 만든 면에서 선택합니다. 그 단계가 반복됩니다.\n스케치는 둘 다 아닙니다. 곡선은 스케치 안의 패턴이나 대칭 복사로 반복하거나, 먼저 돌출하세요.",
|
||||
"ui.pattern_on_path": "경로 패턴",
|
||||
"ui.perpendicular": "수직",
|
||||
"ui.perspective_view": "원근 투영 뷰",
|
||||
@@ -3477,6 +3815,11 @@
|
||||
"ui.polygon": "다각형",
|
||||
"ui.preferences": "환경 설정",
|
||||
"ui.press_a_key": "키를 누르세요…",
|
||||
"ui.print_face_body_not_sent": "이 면의 바디는 보내지지 않습니다. 먼저 바디에 추가하십시오",
|
||||
"ui.print_face_cleared": "{body}: 출력 바닥면을 해제했습니다",
|
||||
"ui.print_face_input": "출력 바닥면",
|
||||
"ui.print_face_pick": "솔리드 바디의 평면 하나를 선택하면 출력 바닥면이 됩니다",
|
||||
"ui.print_face_set": "{body}: 출력 바닥면을 설정했습니다. 슬라이서로 보내면 이 면이 아래로 놓입니다",
|
||||
"ui.profile": "프로파일",
|
||||
"ui.profile_plane": "프로파일 평면",
|
||||
"ui.profiles": "프로파일",
|
||||
@@ -3492,6 +3835,7 @@
|
||||
"ui.properties.inertia_y": "Iyx, Iyy, Iyz",
|
||||
"ui.properties.inertia_z": "Izx, Izy, Izz",
|
||||
"ui.properties.measuring": "측정 중…",
|
||||
"ui.properties.measuring_bodies": "측정 중… 바디 {total}개 중 {done}개",
|
||||
"ui.properties.measuring_failed": "측정이 끝나기 전에 중단되었습니다.",
|
||||
"ui.properties.principal_axis": "주축 {number}",
|
||||
"ui.properties.principal_moments": "주관성 모멘트",
|
||||
@@ -3553,6 +3897,7 @@
|
||||
"ui.selectOther": "기타 선택…",
|
||||
"ui.selectTangentChain": "접선 체인 선택",
|
||||
"ui.select_all": "모두 선택",
|
||||
"ui.select_components_by_face": "구성요소 선택(면을 클릭하면 해당 구성요소 선택)",
|
||||
"ui.selection": "선택",
|
||||
"ui.selection_modifiers": "선택 보조 키",
|
||||
"ui.selection_shift_add_only": "Shift+클릭은 추가만 함",
|
||||
@@ -3595,6 +3940,7 @@
|
||||
"ui.slice": "슬라이스",
|
||||
"ui.slicer": "슬라이서",
|
||||
"ui.slicer_delete_tip": "{program}\nDelete 키로 목록에서 제거",
|
||||
"ui.slicer_single_instance_tip": "실행 중인 창에 파일을 불러오는 슬라이서용입니다.",
|
||||
"ui.slot": "슬롯",
|
||||
"ui.smooth": "부드럽게",
|
||||
"ui.smooth_wheel_zoom": "부드러운 휠 확대/축소",
|
||||
@@ -3681,10 +4027,19 @@
|
||||
"ui.view_centre": "뷰 중심",
|
||||
"ui.viewport_status_clear": "{name} 끄기",
|
||||
"ui.viewport_status_clear_hint": "{name} 끄기 · Esc",
|
||||
"ui.viewport_status_edges": "모서리 {count}개",
|
||||
"ui.viewport_status_from": "{direction} 방향에서",
|
||||
"ui.viewport_status_key": "{name} 범례 표시",
|
||||
"ui.viewport_status_minimum_radius": "최소 반지름",
|
||||
"ui.viewport_status_pairs": "{count}쌍",
|
||||
"ui.viewport_status_section_hatch": "{setting} · 해칭",
|
||||
"ui.viewport_status_section_plane": "현재 평면",
|
||||
"ui.viewport_status_section_tilted": "기울어짐 · {offset}",
|
||||
"ui.viewport_status_stripes": "줄무늬 {count}개",
|
||||
"ui.viewport_status_summary_clear": "모두 끄기",
|
||||
"ui.viewport_status_summary_heading": "이 뷰에서",
|
||||
"ui.viewport_status_summary_label": "{name} 외 {count}개 항목",
|
||||
"ui.viewport_status_summary_more": "+{count}",
|
||||
"ui.viewport_theme_follow": "테마 따르기",
|
||||
"ui.visual_shaded_hidden_edges": "음영 처리 및 숨겨진 모서리",
|
||||
"ui.visual_shaded_with_edges": "음영 처리 및 모서리",
|
||||
@@ -3715,11 +4070,13 @@
|
||||
"update.available": "SolidCraft {version} 사용 가능",
|
||||
"update.button": "업데이트",
|
||||
"update.button_version": "{version}(으)로 업데이트",
|
||||
"update.check_automatically": "업데이트 자동 확인",
|
||||
"update.check_automatically": "시작 시 및 하루에 한 번 확인",
|
||||
"update.check_for_updates": "업데이트 확인",
|
||||
"update.check_now": "지금 확인",
|
||||
"update.check_unavailable": "이 설치에서는 업데이트를 확인할 수 없습니다.",
|
||||
"update.checking": "확인 중…",
|
||||
"update.copy": "복사",
|
||||
"update.dmg_opened": "디스크 이미지를 확인하고 열었습니다: SolidCraft를 응용 프로그램 폴더로 끌어 기존 버전을 바꾼 뒤 SolidCraft를 다시 시작하세요.",
|
||||
"update.dmg_opened": "디스크 이미지를 확인하고 열었습니다: SolidCraft를 종료하고, 새 SolidCraft를 응용 프로그램 폴더로 끌어 기존 버전을 바꾼 뒤 SolidCraft를 다시 여세요.",
|
||||
"update.download": "다운로드",
|
||||
"update.download_open": "다운로드 후 열기",
|
||||
"update.downloading": "다운로드 및 확인 중… {done} / {total} MB",
|
||||
|
||||
+606
-4
File diff suppressed because it is too large
Load Diff
+606
-4
File diff suppressed because it is too large
Load Diff
+602
-4
File diff suppressed because it is too large
Load Diff
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"en": {
|
||||
"prefs.viewport_analysis_status": "Show viewport analysis status",
|
||||
"prefs.viewport_analysis_status_help": "Show active analyses above the viewport, open their legends from each item, and use Esc to turn off the newest analysis. Turn off to restore corner legends and the previous Esc behaviour.",
|
||||
"ui.viewport_status_edges": "{count} edges",
|
||||
"ui.viewport_status_from": "from {direction}"
|
||||
},
|
||||
"ar": {
|
||||
"prefs.viewport_analysis_status": "إظهار حالة التحليل في منفذ العرض",
|
||||
"prefs.viewport_analysis_status_help": "عرض التحليلات النشطة أعلى منفذ العرض وفتح مفتاح كل تحليل من عنصره. ينهي Esc أحدث تحليل. أوقف الخيار لاستعادة المفاتيح في الزاوية وسلوك Esc السابق.",
|
||||
"ui.viewport_status_edges": "{count} حواف",
|
||||
"ui.viewport_status_from": "من {direction}"
|
||||
},
|
||||
"de": {
|
||||
"prefs.viewport_analysis_status": "Analysestatus im Ansichtsfenster anzeigen",
|
||||
"prefs.viewport_analysis_status_help": "Zeigt aktive Analysen über dem Ansichtsfenster und ihre Legenden am jeweiligen Eintrag. Esc beendet die neueste Analyse. Ausschalten stellt Ecklegenden und das bisherige Esc-Verhalten wieder her.",
|
||||
"ui.viewport_status_edges": "{count} Kanten",
|
||||
"ui.viewport_status_from": "aus {direction}"
|
||||
},
|
||||
"he": {
|
||||
"prefs.viewport_analysis_status": "הצגת מצב ניתוח בחלון התצוגה",
|
||||
"prefs.viewport_analysis_status_help": "הצגת ניתוחים פעילים מעל חלון התצוגה ופתיחת המקרא מכל פריט. Esc מכבה את הניתוח האחרון. כיבוי האפשרות משחזר את המקרא בפינה ואת פעולת Esc הקודמת.",
|
||||
"ui.viewport_status_edges": "{count} קצוות",
|
||||
"ui.viewport_status_from": "מכיוון {direction}"
|
||||
},
|
||||
"ja": {
|
||||
"prefs.viewport_analysis_status": "ビューポートの解析状態を表示",
|
||||
"prefs.viewport_analysis_status_help": "有効な解析をビューポートの上部に表示し、各項目から凡例を開きます。Esc で最新の解析を解除します。無効にすると隅の凡例と従来の Esc 動作に戻ります。",
|
||||
"ui.viewport_status_edges": "{count} エッジ",
|
||||
"ui.viewport_status_from": "方向 {direction}"
|
||||
},
|
||||
"ko": {
|
||||
"prefs.viewport_analysis_status": "뷰포트 해석 상태 표시",
|
||||
"prefs.viewport_analysis_status_help": "활성 해석을 뷰포트 위에 표시하고 각 항목에서 범례를 엽니다. Esc로 최신 해석을 끕니다. 끄면 모서리 범례와 이전 Esc 동작으로 돌아갑니다.",
|
||||
"ui.viewport_status_edges": "모서리 {count}개",
|
||||
"ui.viewport_status_from": "{direction} 방향에서"
|
||||
},
|
||||
"vi": {
|
||||
"prefs.viewport_analysis_status": "Hiện trạng thái phân tích trong khung nhìn",
|
||||
"prefs.viewport_analysis_status_help": "Hiện các phân tích đang bật phía trên khung nhìn và mở chú giải từ từng mục. Esc tắt phân tích mới nhất. Tắt tùy chọn để khôi phục chú giải ở góc và hành vi Esc trước đây.",
|
||||
"ui.viewport_status_edges": "{count} cạnh",
|
||||
"ui.viewport_status_from": "từ {direction}"
|
||||
},
|
||||
"zh": {
|
||||
"prefs.viewport_analysis_status": "显示视口分析状态",
|
||||
"prefs.viewport_analysis_status_help": "在视口上方显示启用的分析,从各项目打开图例,并按 Esc 关闭最近启用的分析。关闭此选项可恢复角落图例和原来的 Esc 行为。",
|
||||
"ui.viewport_status_edges": "{count} 条边",
|
||||
"ui.viewport_status_from": "从 {direction}"
|
||||
}
|
||||
}
|
||||
+1175
-2
File diff suppressed because it is too large
Load Diff
@@ -606,6 +606,8 @@
|
||||
"cmd.appearance_library": "Biblioteca de aparências",
|
||||
"cmd.appearance_list": "Listar aparências",
|
||||
"cmd.assembly_bom": "Lista de materiais",
|
||||
"cmd.assembly_mass_properties": "Propriedades de massa",
|
||||
"cmd.body.print_face": "Face de impressão",
|
||||
"cmd.body_rename": "Renomear corpo",
|
||||
"cmd.browser_group": "Novo grupo",
|
||||
"cmd.browser_move": "Mover itens do navegador",
|
||||
@@ -1217,6 +1219,8 @@
|
||||
"prefs.axis": "Eixo",
|
||||
"prefs.bottom": "Embaixo",
|
||||
"prefs.browser": "Navegador",
|
||||
"prefs.browser_components_open": "Componentes começam abertos",
|
||||
"prefs.browser_components_open_note": "Cada componente no navegador começa aberto, como antes, em vez de fechado até que você o abra.",
|
||||
"prefs.built_in": "Integrado",
|
||||
"prefs.button_left": "Arrastar com o esquerdo",
|
||||
"prefs.button_middle": "Arrastar com o meio",
|
||||
@@ -1292,6 +1296,8 @@
|
||||
"prefs.orientation": "Orientação",
|
||||
"prefs.overall_speed": "Velocidade geral",
|
||||
"prefs.preview_choices": "Prévia das suas escolhas",
|
||||
"prefs.print_face_highlight": "Destaque da face de impressão",
|
||||
"prefs.print_face_highlight_tip": "Intensidade da cor e do contorno da face de impressão de um corpo na vista. 0 % não mostra destaque.",
|
||||
"prefs.quality_default": "padrão",
|
||||
"prefs.quality_fastest": "mais rápido",
|
||||
"prefs.quality_slowest": "mais lento",
|
||||
@@ -1571,6 +1577,8 @@
|
||||
"ui.3d_mouse_step_twist_counterclockwise": "Gire a tampa no sentido anti-horário, segure e solte.",
|
||||
"ui.3d_mouse_switch_cap": "Calibrar como retorno ao centro",
|
||||
"ui.3d_mouse_switch_trackball": "Calibrar como trackball",
|
||||
"ui.3d_mouse_use_direct": "Usar driver direto",
|
||||
"ui.3d_mouse_use_direct_tip": "Ler o mouse 3D diretamente (hidraw) em vez de pelo spacenavd.",
|
||||
"ui.3d_mouse_view_axis": "Eixo da vista",
|
||||
"ui.3d_mouse_view_off": "Desligado",
|
||||
"ui.3d_mouse_view_off_tip": "O perfil do driver do mouse 3D desliga este eixo, então o movimento dele não move a vista. Escolher aqui o movimento de outro eixo troca os dois, e esse movimento deixa de mover a vista.",
|
||||
@@ -1840,6 +1848,7 @@
|
||||
"ui.circle": "Círculo",
|
||||
"ui.circular_pattern": "Padrão circular",
|
||||
"ui.clear": "Limpar",
|
||||
"ui.clear_print_face": "Limpar face de impressão",
|
||||
"ui.clear_recent": "Limpar recentes",
|
||||
"ui.clear_the_selection": "Limpar a seleção",
|
||||
"ui.click_selects_whole_bodies": "O clique seleciona corpos inteiros",
|
||||
@@ -1876,10 +1885,15 @@
|
||||
"ui.construction": "Construção",
|
||||
"ui.construction.ribbon_label": "Linhas aux.",
|
||||
"ui.context_menu": "Menu de contexto",
|
||||
"ui.continuity_crease": "Vinco",
|
||||
"ui.continuity_curvature": "Curvatura",
|
||||
"ui.continuity_curvature_change": "Variação de curvatura",
|
||||
"ui.continuity_g0": "G0 vinco",
|
||||
"ui.continuity_g1": "G1 tangente",
|
||||
"ui.continuity_g2": "G2 curvatura",
|
||||
"ui.continuity_gap": "Folga",
|
||||
"ui.continuity_position": "Posição",
|
||||
"ui.continuity_tangent": "Tangente",
|
||||
"ui.copy": "Copiar",
|
||||
"ui.corner": "Canto",
|
||||
"ui.corners": "Cantos",
|
||||
@@ -2085,6 +2099,7 @@
|
||||
"ui.infinite": "Infinita",
|
||||
"ui.insert": "Inserir",
|
||||
"ui.inspect": "Inspecionar",
|
||||
"ui.instances": "Instâncias",
|
||||
"ui.intersect": "Interseção",
|
||||
"ui.isolate": "Isolar",
|
||||
"ui.join": "Unir",
|
||||
@@ -2198,6 +2213,11 @@
|
||||
"ui.polygon": "Polígono",
|
||||
"ui.preferences": "Preferências",
|
||||
"ui.press_a_key": "pressione uma tecla…",
|
||||
"ui.print_face_body_not_sent": "O corpo dessa face não está sendo enviado: adicione-o primeiro a Corpos",
|
||||
"ui.print_face_cleared": "{body}: face de impressão removida",
|
||||
"ui.print_face_input": "Face de impressão",
|
||||
"ui.print_face_pick": "Selecione uma face plana de um corpo sólido para torná-la a face de impressão",
|
||||
"ui.print_face_set": "{body}: face de impressão definida; Enviar ao fatiador apoia o corpo sobre ela",
|
||||
"ui.profile": "Perfil",
|
||||
"ui.profile_plane": "Plano do perfil",
|
||||
"ui.profiles": "Perfis",
|
||||
@@ -2388,10 +2408,12 @@
|
||||
"update.button": "Atualizar",
|
||||
"update.button_version": "Atualizar para {version}",
|
||||
"update.check_automatically": "Verificar atualizações automaticamente",
|
||||
"update.check_for_updates": "Verificar atualizações",
|
||||
"update.check_now": "Verificar agora",
|
||||
"update.check_unavailable": "A verificação de atualizações não está disponível para esta instalação.",
|
||||
"update.checking": "Verificando…",
|
||||
"update.copy": "Copiar",
|
||||
"update.dmg_opened": "A imagem de disco está aberta e verificada: arraste o SolidCraft para Aplicativos, substituindo a cópia antiga, e reinicie o SolidCraft.",
|
||||
"update.dmg_opened": "A imagem de disco está aberta e verificada: encerre o SolidCraft, arraste o novo SolidCraft para Aplicativos, substituindo a cópia antiga, e abra o SolidCraft novamente.",
|
||||
"update.download": "Baixar",
|
||||
"update.download_open": "Baixar e abrir",
|
||||
"update.downloading": "Baixando e verificando… {done} de {total} MB",
|
||||
|
||||
+606
-4
File diff suppressed because it is too large
Load Diff
+579
-5
File diff suppressed because it is too large
Load Diff
+579
-3
File diff suppressed because it is too large
Load Diff
+606
-4
File diff suppressed because it is too large
Load Diff
+603
-4
File diff suppressed because it is too large
Load Diff
+537
-4
File diff suppressed because it is too large
Load Diff
+724
-14
File diff suppressed because it is too large
Load Diff
+609
-4
File diff suppressed because it is too large
Load Diff
+724
-14
File diff suppressed because it is too large
Load Diff
+276
-41
@@ -18,8 +18,11 @@ struct U {
|
||||
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
|
||||
tint: vec4<f32>, // xyz added to a whole hovered body's colour before lighting
|
||||
sel: vec4<f32>, // xyz a whole selected body's colour before lighting
|
||||
// the section hatch colours by part (`solidcraft_render::hatch`), the first the cap's own
|
||||
hatch: array<vec4<f32>, 3>,
|
||||
product_lights_meta: vec4<f32>, // explicit preview flag and bounded count
|
||||
product_lights: array<vec4<f32>,84>, // position/kind, radiance, axis/cone, attenuation
|
||||
sky: vec4<f32>, // xyz = world up; w = 1: shade in linear light with a sky fill (#7974)
|
||||
};
|
||||
|
||||
fn reversed() -> bool {
|
||||
@@ -34,21 +37,31 @@ fn near_z(c: vec4<f32>) -> f32 {
|
||||
|
||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||
@group(2) @binding(0) var ao_depth: texture_2d<u32>;
|
||||
@group(2) @binding(1) var ao_map: texture_2d<f32>;
|
||||
|
||||
fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
// The scene's colours, 2048 entries to a row: an R32Uint texture rather than a storage buffer,
|
||||
// so the viewport draws on WebGL2 (which has no storage buffers) as well.
|
||||
// The scene's colours, 2048 entries to a row: an Rg32Uint texture rather than a storage buffer,
|
||||
// so the viewport draws on WebGL2 (which has no storage buffers) as well. Each entry is the
|
||||
// colour's bytes, then 1 + the section hatch of the body it colours (0: the cap's own).
|
||||
@group(0) @binding(1) var pal: texture_2d<u32>;
|
||||
|
||||
/// The colour at palette entry `i`, as byte/255 (the old Unorm8x4 conversion, bit for bit).
|
||||
fn palette(i: u32) -> vec4<f32> {
|
||||
let v = textureLoad(pal, vec2<i32>(i32(i & 2047u), i32(i >> 11u)), 0).x;
|
||||
fn palette_entry(i: u32) -> vec2<u32> {
|
||||
return textureLoad(pal, vec2<i32>(i32(i & 2047u), i32(i >> 11u)), 0).xy;
|
||||
}
|
||||
|
||||
/// A palette entry's colour bytes as byte/255 (the old Unorm8x4 conversion, bit for bit).
|
||||
fn unpack_color(v: u32) -> vec4<f32> {
|
||||
return vec4<f32>(f32(v & 255u), f32((v >> 8u) & 255u), f32((v >> 16u) & 255u), f32((v >> 24u) & 255u)) / 255.0;
|
||||
}
|
||||
|
||||
/// The colour at palette entry `i`.
|
||||
fn palette(i: u32) -> vec4<f32> {
|
||||
return unpack_color(palette_entry(i).x);
|
||||
}
|
||||
|
||||
fn to_linear(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
|
||||
}
|
||||
@@ -61,10 +74,12 @@ fn out_color(c: vec4<f32>) -> vec4<f32> {
|
||||
}
|
||||
|
||||
struct TOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@invariant @builtin(position) pos: vec4<f32>,
|
||||
@location(0) n: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) wp: vec3<f32>,
|
||||
// 1 + the section hatch of the body (0: the cap's own), from the palette.
|
||||
@location(3) @interpolate(flat) h: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -72,7 +87,9 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.n = n;
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
@@ -94,7 +111,9 @@ fn vs_tri_inst(
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.n = vec3<f32>(dot(r0.xyz, n), dot(r1.xyz, n), dot(r2.xyz, n));
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
o.wp = wp;
|
||||
return o;
|
||||
}
|
||||
@@ -106,9 +125,10 @@ fn vs_tri_inst(
|
||||
/// stores every vertex's outputs: more of them cost more than the triangles' shared vertices
|
||||
/// save).
|
||||
struct FOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@invariant @builtin(position) pos: vec4<f32>,
|
||||
@location(0) rp: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) @interpolate(flat) h: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -124,7 +144,9 @@ fn vs_tri_inst_flat(
|
||||
var o: FOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.rp = wp - u.eye.xyz;
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -140,6 +162,7 @@ fn flat_fragment(i: FOut, front: bool) -> TOut {
|
||||
o.pos = i.pos;
|
||||
o.n = n;
|
||||
o.c = i.c;
|
||||
o.h = i.h;
|
||||
o.wp = i.rp + u.eye.xyz;
|
||||
return o;
|
||||
}
|
||||
@@ -208,6 +231,12 @@ fn fs_ao_depth_flat(i: FOut, @builtin(front_facing) front: bool) -> AoDepth {
|
||||
return opaque_depth(t, front);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_ao_term_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
return ao_term(t, front);
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
@@ -215,13 +244,43 @@ fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f3
|
||||
return c;
|
||||
}
|
||||
|
||||
/// A section's crossing count (`Cut::count` in `view.rs`): nothing but the cut itself, so the
|
||||
/// stencil counts only the faces behind the section plane. The colour is not written.
|
||||
@fragment
|
||||
fn fs_clip(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_clip_flat(i: FOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.rp + u.eye.xyz)) { discard; }
|
||||
return vec4<f32>(0.0);
|
||||
}
|
||||
|
||||
/// Faces where the section plane is outside every closed body (`Cut::rest`): a back face seen
|
||||
/// there is the inside of an open body (a surface body, a solid with faces left out), lit as it
|
||||
/// is without a section rather than hatched, since there is no cut face to show.
|
||||
@fragment
|
||||
fn fs_solid_open(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let c = solid(i, true);
|
||||
return c;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_solid_open_flat(i: FOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
let t = flat_fragment(i, front);
|
||||
let c = solid(t, true);
|
||||
return c;
|
||||
}
|
||||
|
||||
struct CutOut {
|
||||
@location(0) c: vec4<f32>,
|
||||
@builtin(frag_depth) depth: f32,
|
||||
};
|
||||
|
||||
/// The section cap (the inside seen through the cut, drawn on the far walls' back faces where
|
||||
/// they are the visible surface) again, at the depth of the section plane rather than of the
|
||||
/// The section cap (where the section plane lies inside a body, drawn on the far walls' back
|
||||
/// faces) again, at the depth of the section plane rather than of the
|
||||
/// far wall, so the cut face hides what lies inside the body instead of showing it like a
|
||||
/// pane of glass.
|
||||
@fragment
|
||||
@@ -266,11 +325,19 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour ("from component" colouring), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen; with the hatch off
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen; by part, toward the
|
||||
// body's own hatch colour and at its angle (45 or 135 degrees). With the hatch off
|
||||
// (`u.cap.a` 0), plain in the body's own colour.
|
||||
if (u.cap.a < 0.5) { return out_color(vec4<f32>(i.c.rgb, 1.0)); }
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
var tint = u.cap.rgb;
|
||||
var turn = 1.0;
|
||||
if (i.h > 0u) {
|
||||
let k = i.h - 1u;
|
||||
tint = u.hatch[min(k >> 1u, 2u)].rgb;
|
||||
turn = select(1.0, -1.0, (k & 1u) == 1u);
|
||||
}
|
||||
let base = mix(i.c.rgb, tint, 0.35);
|
||||
let h = fract((i.pos.x + turn * i.pos.y) / 9.0);
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
}
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
@@ -307,11 +374,89 @@ fn opaque_depth(i: TOut, front: bool) -> AoDepth {
|
||||
return AoDepth(depth, bitcast<u32>(depth));
|
||||
}
|
||||
|
||||
/// The term for an opaque surface's fragment, on the prepass's depth. A section's cut face has none.
|
||||
fn ao_term(i: TOut, front: bool) -> vec4<f32> {
|
||||
if (clipped(i.wp) || length(i.n) < 0.5) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) { return vec4<f32>(1.0); }
|
||||
return vec4<f32>(occlusion_term(i));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_ao_term(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
return ao_term(i, front);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_ao_blur(@builtin(vertex_index) v: u32) -> @builtin(position) vec4<f32> {
|
||||
let p = vec2<f32>(f32((v << 1u) & 2u), f32(v & 2u));
|
||||
return vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
|
||||
}
|
||||
|
||||
/// The depth map at `p`, reading 0 near and 1 far whichever way the buffer runs.
|
||||
fn map_depth(p: vec2<i32>) -> f32 {
|
||||
let at = clamp(p, vec2<i32>(0), vec2<i32>(textureDimensions(ao_depth)) - vec2<i32>(1));
|
||||
let d = bitcast<f32>(textureLoad(ao_depth, at, 0).x);
|
||||
return select(d, 1.0 - d, reversed());
|
||||
}
|
||||
|
||||
/// The raw term (`ao_map` in this pass) averaged over a 4 x 4 window, which holds every turn of
|
||||
/// [`ao_turn`]'s pattern once. A neighbour counts when it lies on this pixel's plane, the slope
|
||||
/// taken from the nearer side across each axis, so no surface's shade crosses an edge.
|
||||
@fragment
|
||||
fn fs_ao_blur(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
|
||||
let c = vec2<i32>(pos.xy);
|
||||
let d = map_depth(c);
|
||||
if (d >= 1.0) { return vec4<f32>(1.0); }
|
||||
let left = d - map_depth(c + vec2<i32>(-1, 0));
|
||||
let right = map_depth(c + vec2<i32>(1, 0)) - d;
|
||||
let above = d - map_depth(c + vec2<i32>(0, -1));
|
||||
let below = map_depth(c + vec2<i32>(0, 1)) - d;
|
||||
let dx = select(right, left, abs(left) < abs(right));
|
||||
let dy = select(below, above, abs(above) < abs(below));
|
||||
let tolerance = 1.5 * (abs(dx) + abs(dy)) + 1e-6;
|
||||
let last = vec2<i32>(textureDimensions(ao_map)) - vec2<i32>(1);
|
||||
var sum = 0.0;
|
||||
var weight = 0.0;
|
||||
for (var y = -2; y < 2; y = y + 1) {
|
||||
for (var x = -2; x < 2; x = x + 1) {
|
||||
let p = c + vec2<i32>(x, y);
|
||||
let s = map_depth(p);
|
||||
if (s < 1.0 && abs(s - (d + dx * f32(x) + dy * f32(y))) <= tolerance) {
|
||||
sum = sum + textureLoad(ao_map, clamp(p, vec2<i32>(0), last), 0).x;
|
||||
weight = weight + 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return vec4<f32>(sum / max(weight, 1.0));
|
||||
}
|
||||
|
||||
/// A pixel's turn of the sample pattern: every 4 x 4 window holds each of 16 turns once.
|
||||
fn ao_turn(pos: vec2<f32>) -> f32 {
|
||||
let x = u32(pos.x) & 3u;
|
||||
let y = u32(pos.y) & 3u;
|
||||
// WGSL won't mix shifts and `|` without parentheses; naga takes it, browsers refuse it (#11080).
|
||||
let v = (((x ^ y) & 1u) << 3u) | ((y & 1u) << 2u) | (((x ^ y) >> 1u) << 1u) | (y >> 1u);
|
||||
return (f32(v) + 0.5) / 16.0;
|
||||
}
|
||||
|
||||
/// The lit pass's occlusion: the blurred term where this point falls in the map, whatever the
|
||||
/// window's viewport offset and supersampling.
|
||||
fn occlusion(i: TOut) -> f32 {
|
||||
let size = vec2<i32>(textureDimensions(ao_map));
|
||||
// The maps are 1 × 1 stand-ins until a frame draws into them (the product preview draws no
|
||||
// prepass, and AO off releases them): they tell nothing, so leave the surface unoccluded.
|
||||
if (u.ao.x <= 0.0 || length(i.n) < 0.5 || size.x < 2 || size.y < 2) { return 1.0; }
|
||||
let q = u.vp * vec4<f32>(i.wp, 1.0);
|
||||
if (q.w <= 0.0) { return 1.0; }
|
||||
let uv = vec2<f32>(q.x / q.w * 0.5 + 0.5, 0.5 - q.y / q.w * 0.5);
|
||||
return textureLoad(ao_map, clamp(vec2<i32>(uv * vec2<f32>(size)), vec2<i32>(0), size - vec2<i32>(1)), 0).x;
|
||||
}
|
||||
|
||||
// Cosine-weighted hemisphere samples, the pattern turned by a different angle at each pixel.
|
||||
// Projecting the sample and a point one radius toward the eye supplies a depth range in both
|
||||
// orthographic and perspective views; distant foreground geometry cannot cast a dark halo onto
|
||||
// a background surface.
|
||||
fn occlusion(i: TOut) -> f32 {
|
||||
fn occlusion_term(i: TOut) -> f32 {
|
||||
let radius = u.ao.x;
|
||||
if (radius <= 0.0 || length(i.n) < 0.5) { return 1.0; }
|
||||
var n = normalize(i.n);
|
||||
@@ -325,10 +470,9 @@ fn occlusion(i: TOut) -> f32 {
|
||||
if (size.x < 2 || size.y < 2) { return 1.0; }
|
||||
// The same offsets at every pixel made each sample darken its own shifted copy of a nearby
|
||||
// part's outline: faint ghosts of the part where the radius is large on screen (#4167),
|
||||
// stepped bands along contacts where it is small (#3407). Turned by interleaved gradient
|
||||
// noise (Jimenez 2014), neighbouring pixels sample different points and the copies break up
|
||||
// into an even grain.
|
||||
let turn = fract(52.9829189 * fract(dot(i.pos.xy, vec2<f32>(0.06711056, 0.00583715))));
|
||||
// stepped bands along contacts where it is small (#3407). Turned, neighbouring pixels sample
|
||||
// different points, the copies break up into grain, and the blur takes the grain out (#9327).
|
||||
let turn = ao_turn(i.pos.xy);
|
||||
let t = tangent * cos(turn * 6.28318531) + bitangent * sin(turn * 6.28318531);
|
||||
let b = cross(n, t);
|
||||
// One comparison for both depth directions: depths read as 0 near / 1 far (mirrored when
|
||||
@@ -385,8 +529,13 @@ fn analysis(i: TOut, k: f32) -> vec4<f32> {
|
||||
let r = reflect(-v, n);
|
||||
var side = cross(v, vec3<f32>(0.0, 0.0, 1.0));
|
||||
if (length(side) < 1e-3) { side = vec3<f32>(1.0, 0.0, 0.0); }
|
||||
let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265);
|
||||
let c = select(0.08, 0.95, s > 0.0);
|
||||
let x = dot(r, normalize(side));
|
||||
let s = sin(x * u.ana.y * 3.14159265);
|
||||
// A face whose normal lies in the upright plane through the view (every horizontal
|
||||
// face) reflects straight ahead: its sideways part is zero, on the edge between a black
|
||||
// and a white stripe, and rounding picked one or the other from frame to frame while
|
||||
// the view turned (#2153). Straight ahead is white.
|
||||
let c = select(0.08, 0.95, s > 0.0 || abs(x) < 1e-4);
|
||||
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
|
||||
}
|
||||
if (u.ana.x > 4.5) {
|
||||
@@ -507,6 +656,31 @@ fn lit(i: TOut, visibility: f32) -> vec4<f32> {
|
||||
return lit_color(i, visibility, i.c);
|
||||
}
|
||||
|
||||
/// sRGB to linear light, exact (the straight segment too, which `to_linear` leaves out).
|
||||
fn srgb_decode(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(c / 12.92, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
|
||||
}
|
||||
|
||||
fn srgb_encode(c: vec3<f32>) -> vec3<f32> {
|
||||
let l = clamp(c, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
return select(l * 12.92, 1.055 * pow(l, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055), l > vec3<f32>(0.0031308));
|
||||
}
|
||||
|
||||
/// The studio light summed in linear light, encoded once (#7974): the key and the eye fill as in
|
||||
/// [`lit_color`], over a sky fill that grows as the face turns up, and the 4% every dielectric
|
||||
/// reflects of that sky (a little more toward grazing), so a near-black face keeps a gradient. The sky
|
||||
/// and the fill are what ambient occlusion (`visibility`) takes away. sRGB out.
|
||||
fn sky_lit(base: vec3<f32>, n: vec3<f32>, diff: f32, fill: f32, visibility: f32) -> vec3<f32> {
|
||||
let up = dot(n, normalize(u.sky.xyz)) * 0.5 + 0.5;
|
||||
let sky = mix(0.18, 0.42, up);
|
||||
// Schlick's rise toward grazing, tempered so mid-tones only gain a soft rim.
|
||||
let fresnel = 0.04 + 0.2 * pow(1.0 - fill, 5.0);
|
||||
let albedo = srgb_decode(base);
|
||||
// Scaled so light and mid-grey faces land near where the sRGB shading puts them.
|
||||
let light = albedo * (0.41 * diff + (0.66 * sky + 0.2 * fill) * visibility) + vec3<f32>(fresnel * sky * visibility);
|
||||
return srgb_encode(light);
|
||||
}
|
||||
|
||||
/// [`lit`] in a colour of its own: a whole highlighted body is lit in its colour lifted by the
|
||||
/// hover amount, or in the selection colour, from the model's own plan triangles.
|
||||
fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
|
||||
@@ -533,6 +707,9 @@ fn lit_color(i: TOut, visibility: f32, base: vec4<f32>) -> vec4<f32> {
|
||||
let fill = max(dot(n, v), 0.0);
|
||||
let h = normalize(key + v);
|
||||
let spec = pow(max(dot(n, h), 0.0), 40.0) * 0.25;
|
||||
if u.sky.w > 0.5 {
|
||||
return out_color(vec4<f32>(min(sky_lit(base.rgb, n, diff, fill, visibility) + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
|
||||
}
|
||||
let k = 0.42 + 0.38 * diff + 0.25 * fill - (0.42 + 0.25 * fill) * (1.0 - visibility);
|
||||
return out_color(vec4<f32>(min(base.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), base.a));
|
||||
}
|
||||
@@ -697,38 +874,96 @@ fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
// Point clouds: each point is a round dot a few pixels across, facing the screen, depth-tested
|
||||
// and written like the model's faces, so the model hides the points behind it and they hide it.
|
||||
// The colour's alpha carries the dot's size (eighths of a pixel).
|
||||
// Point clouds (`view/clouds.rs`): each point is a round dot a few pixels across, facing the
|
||||
// screen, depth-tested and written like the model's faces, so the model hides the points behind
|
||||
// it and they hide it. The cloud's style colours them by their own colour, a ramp or a palette,
|
||||
// and shows only those whose filtered values lie in its ranges or sets.
|
||||
|
||||
struct POut {
|
||||
struct Cloud {
|
||||
style: vec4<f32>, // x: colour (0 own, 1 ramp, 2 palette), y: dot size (px), z, w: ramp range
|
||||
filters: array<vec4<f32>, 4>, // x: kind (0 off, 1 range y…z, 2 whole values y + i for the bits i of w)
|
||||
};
|
||||
@group(1) @binding(3) var<uniform> cloud: Cloud;
|
||||
|
||||
struct CIn {
|
||||
@location(0) p: vec3<f32>,
|
||||
@location(1) v: f32,
|
||||
@location(2) f: vec4<f32>,
|
||||
@location(3) rgb: vec4<f32>,
|
||||
};
|
||||
|
||||
struct COut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) c: vec4<f32>,
|
||||
@location(1) wp: vec3<f32>,
|
||||
@location(2) at: vec2<f32>,
|
||||
@location(0) wp: vec3<f32>,
|
||||
@location(1) v: f32,
|
||||
@location(2) f: vec4<f32>,
|
||||
@location(3) rgb: vec3<f32>,
|
||||
@location(4) at: vec2<f32>, // the place on its disc
|
||||
@location(5) @interpolate(flat) has: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_point(@builtin(vertex_index) vi: u32, @location(0) p: vec3<f32>, @location(1) c: vec4<f32>) -> POut {
|
||||
fn vs_cloud_dot(@builtin(vertex_index) vi: u32, i: CIn) -> COut {
|
||||
var xs = array<f32, 6>(-1.0, 1.0, 1.0, -1.0, 1.0, -1.0);
|
||||
var ys = array<f32, 6>(-1.0, -1.0, 1.0, -1.0, 1.0, 1.0);
|
||||
let at = vec2<f32>(xs[vi], ys[vi]);
|
||||
let size = max(c.a * 255.0 / 8.0, 1.0);
|
||||
let centre = u.vp * vec4<f32>(p, 1.0);
|
||||
let half = u.screen.xy * 0.5;
|
||||
var o: POut;
|
||||
o.pos = vec4<f32>(centre.xy + at * size * 0.5 / half * centre.w, centre.z, centre.w);
|
||||
let size = max(cloud.style.y, 1.0);
|
||||
let centre = u.vp * vec4<f32>(i.p, 1.0);
|
||||
var o: COut;
|
||||
o.pos = vec4<f32>(centre.xy + at * size / u.screen.xy * centre.w, centre.z, centre.w);
|
||||
// Behind the eye: nothing.
|
||||
if (near_z(centre) < 0.0) { o.pos = vec4<f32>(0.0, 0.0, 0.0, -1.0); }
|
||||
o.c = vec4<f32>(c.rgb, 1.0);
|
||||
o.wp = p;
|
||||
o.wp = i.p;
|
||||
o.v = i.v;
|
||||
o.f = i.f;
|
||||
o.rgb = i.rgb.rgb;
|
||||
o.has = u32(round(i.rgb.a * 255.0));
|
||||
// Dots under two pixels are squares: a disc that small loses its centre pixel.
|
||||
o.at = at * select(1.0, 0.0, size < 2.0);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_point(i: POut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp) || dot(i.at, i.at) > 1.0) { discard; }
|
||||
return out_color(i.c);
|
||||
/// Whether filter `f` keeps the value `v`.
|
||||
fn cloud_keeps(f: vec4<f32>, v: f32) -> bool {
|
||||
if (f.x < 0.5) { return true; }
|
||||
if (f.x < 1.5) { return v >= f.y && v <= f.z; }
|
||||
let k = i32(round(v)) - i32(round(f.y));
|
||||
return k >= 0 && k < 32 && ((bitcast<u32>(f.w) >> u32(k)) & 1u) != 0u;
|
||||
}
|
||||
|
||||
fn cloud_ramp(t: f32) -> vec3<f32> {
|
||||
var stops = array<vec3<f32>, 5>(
|
||||
vec3<f32>(40.0, 70.0, 200.0), vec3<f32>(30.0, 170.0, 230.0), vec3<f32>(60.0, 200.0, 90.0),
|
||||
vec3<f32>(240.0, 220.0, 60.0), vec3<f32>(230.0, 70.0, 50.0));
|
||||
let k = clamp(t, 0.0, 1.0) * 4.0;
|
||||
let j = min(u32(k), 3u);
|
||||
return mix(stops[j], stops[j + 1u], k - f32(j)) / 255.0;
|
||||
}
|
||||
|
||||
fn cloud_palette(v: f32) -> vec3<f32> {
|
||||
var colours = array<vec3<f32>, 10>(
|
||||
vec3<f32>(240.0, 140.0, 40.0), vec3<f32>(245.0, 205.0, 65.0), vec3<f32>(230.0, 65.0, 65.0),
|
||||
vec3<f32>(165.0, 75.0, 190.0), vec3<f32>(65.0, 190.0, 115.0), vec3<f32>(75.0, 150.0, 220.0),
|
||||
vec3<f32>(240.0, 120.0, 180.0), vec3<f32>(140.0, 105.0, 75.0), vec3<f32>(120.0, 215.0, 215.0),
|
||||
vec3<f32>(190.0, 190.0, 80.0));
|
||||
if (v < -0.5) { return vec3<f32>(150.0, 160.0, 176.0) / 255.0; }
|
||||
return colours[u32(round(v)) % 10u] / 255.0;
|
||||
}
|
||||
|
||||
|
||||
@fragment
|
||||
fn fs_cloud(o: COut) -> @location(0) vec4<f32> {
|
||||
var out = clipped(o.wp) || dot(o.at, o.at) > 1.0;
|
||||
for (var k = 0u; k < 4u; k++) {
|
||||
out = out || (((o.has >> k) & 1u) != 0u && !cloud_keeps(cloud.filters[k], o.f[k]));
|
||||
}
|
||||
if (out) { discard; }
|
||||
var c = o.rgb;
|
||||
let mode = cloud.style.x;
|
||||
if ((o.has & 16u) != 0u && mode > 1.5) {
|
||||
c = cloud_palette(o.v);
|
||||
} else if ((o.has & 16u) != 0u && mode > 0.5) {
|
||||
c = cloud_ramp((o.v - cloud.style.z) / max(cloud.style.w - cloud.style.z, 1e-12));
|
||||
}
|
||||
return out_color(vec4<f32>(c, 1.0));
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ impl Tokens {
|
||||
font_compass: 13.5,
|
||||
font_subtitle: 15.0,
|
||||
font_intro: 16.0,
|
||||
font_page_heading: 20.0,
|
||||
font_article_heading: 17.0,
|
||||
font_title: 22.0,
|
||||
font_display: 26.0,
|
||||
@@ -203,6 +204,11 @@ impl Tokens {
|
||||
dimension_driven: Color32::from_rgb(160, 90, 120),
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
// A steel and a sage that tint a body about as strongly as the cap does and to within
|
||||
// a few points of its lightness, so the cut stays calm, while staying apart from the
|
||||
// cap, each other and the body in every colour vision mode (dark keeps them, as the
|
||||
// cap).
|
||||
section_hatch: [Color32::from_rgb(84, 138, 168), Color32::from_rgb(108, 180, 126)],
|
||||
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||
dialog_border: Color32::from_rgb(213, 219, 227),
|
||||
chip: Color32::from_rgb(38, 120, 218),
|
||||
@@ -226,6 +232,31 @@ impl Tokens {
|
||||
dialog_label: Color32::from_rgb(58, 70, 86),
|
||||
selected_ink: Color32::from_rgb(26, 80, 127),
|
||||
selected_bg: Color32::from_rgb(232, 241, 250),
|
||||
figure_ground: Color32::from_rgb(247, 248, 250),
|
||||
figure_line: Color32::from_rgb(111, 124, 143),
|
||||
figure_highlight: Color32::WHITE,
|
||||
figure_shadow: Color32::BLACK,
|
||||
figure_face_top: Color32::from_rgb(255, 255, 255),
|
||||
figure_face_left: Color32::from_rgb(228, 233, 240),
|
||||
figure_face_right: Color32::from_rgb(201, 210, 222),
|
||||
figure_card: Color32::from_rgb(255, 255, 255),
|
||||
figure_white: Color32::WHITE,
|
||||
figure_black: Color32::BLACK,
|
||||
figure_accent: Color32::from_rgb(35, 110, 173),
|
||||
figure_reference: Color32::from_rgb(138, 83, 0),
|
||||
figure_axis_x: Color32::from_rgb(214, 69, 69),
|
||||
figure_axis_y: Color32::from_rgb(46, 158, 91),
|
||||
figure_axis_z: Color32::from_rgb(47, 111, 208),
|
||||
figure_ok: Color32::from_rgb(47, 125, 91),
|
||||
figure_warn: Color32::from_rgb(201, 138, 0),
|
||||
figure_conflict: Color32::from_rgb(184, 74, 0),
|
||||
pref_well: Color32::from_rgb(238, 241, 245),
|
||||
pref_note: Color32::from_rgb(183, 74, 0),
|
||||
pref_note_fill: Color32::from_rgb(251, 235, 221),
|
||||
pref_on_accent: Color32::from_rgb(255, 255, 255),
|
||||
pref_ok: Color32::from_rgb(39, 107, 44),
|
||||
pref_ok_fill: Color32::from_rgb(229, 242, 230),
|
||||
pref_disabled: Color32::from_rgb(138, 148, 163),
|
||||
timeline_item: Color32::WHITE,
|
||||
timeline_item_error: Color32::from_rgb(250, 220, 220),
|
||||
timeline_marker: Color32::from_rgb(70, 76, 88),
|
||||
@@ -241,6 +272,8 @@ impl Tokens {
|
||||
bend_up: Color32::from_rgb(30, 140, 50),
|
||||
bend_down: Color32::from_rgb(200, 90, 20),
|
||||
interference: Color32::from_rgb(235, 45, 40),
|
||||
comb_envelope: Color32::from_rgb(130, 40, 175),
|
||||
comb_teeth: Color32::from_rgb(215, 105, 0),
|
||||
continuity: [
|
||||
Color32::from_rgb(0, 158, 115),
|
||||
Color32::from_rgb(0, 114, 178),
|
||||
@@ -345,6 +378,27 @@ impl Tokens {
|
||||
t.dialog_label = t.text;
|
||||
t.selected_ink = Color32::from_rgb(156, 199, 245);
|
||||
t.selected_bg = Color32::from_rgb(42, 70, 110);
|
||||
t.figure_ground = Color32::from_rgb(28, 31, 38);
|
||||
t.figure_line = Color32::from_rgb(163, 174, 194);
|
||||
t.figure_face_top = Color32::from_rgb(107, 119, 144);
|
||||
t.figure_face_left = Color32::from_rgb(81, 91, 114);
|
||||
t.figure_face_right = Color32::from_rgb(57, 66, 86);
|
||||
t.figure_card = Color32::from_rgb(47, 52, 62);
|
||||
t.figure_accent = Color32::from_rgb(140, 190, 245);
|
||||
t.figure_reference = Color32::from_rgb(232, 194, 74);
|
||||
t.figure_axis_x = Color32::from_rgb(242, 122, 122);
|
||||
t.figure_axis_y = Color32::from_rgb(98, 212, 142);
|
||||
t.figure_axis_z = Color32::from_rgb(108, 168, 247);
|
||||
t.figure_ok = Color32::from_rgb(127, 209, 174);
|
||||
t.figure_warn = Color32::from_rgb(232, 194, 74);
|
||||
t.figure_conflict = Color32::from_rgb(240, 162, 122);
|
||||
t.pref_well = Color32::from_rgb(47, 52, 62);
|
||||
t.pref_note = Color32::from_rgb(240, 162, 122);
|
||||
t.pref_note_fill = Color32::from_rgb(74, 51, 38);
|
||||
t.pref_on_accent = Color32::from_rgb(15, 22, 32);
|
||||
t.pref_ok = Color32::from_rgb(127, 209, 174);
|
||||
t.pref_ok_fill = Color32::from_rgb(31, 58, 49);
|
||||
t.pref_disabled = Color32::from_rgb(138, 148, 163);
|
||||
t.construction_geom = icon_primary(crate::icon_svg::CONSTRUCTION_PRIMARY[1]);
|
||||
t.error = Color32::from_rgb(240, 100, 96);
|
||||
t.sketch_conflict = t.error;
|
||||
@@ -387,6 +441,8 @@ impl Tokens {
|
||||
t.box_crossing = Color32::from_rgb(80, 200, 120);
|
||||
t.bend_up = Color32::from_rgb(110, 200, 120);
|
||||
t.bend_down = Color32::from_rgb(240, 150, 80);
|
||||
t.comb_envelope = Color32::from_rgb(195, 130, 255);
|
||||
t.comb_teeth = Color32::from_rgb(255, 160, 60);
|
||||
t.form_ink = t.text;
|
||||
t.form_dim = Color32::from_rgb(180, 187, 200);
|
||||
t.form_primary = t.accent;
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"chip": "#ffffff",
|
||||
"construction_geom": "#ffffff",
|
||||
"construction_plane": "#000000",
|
||||
"comb_envelope": "#ffffff",
|
||||
"comb_teeth": "#ffffff",
|
||||
"continuity": [
|
||||
"#ffffff",
|
||||
"#ffffff",
|
||||
@@ -132,6 +134,10 @@
|
||||
"render_soft": "#000000",
|
||||
"rubber_band": "#ffffff",
|
||||
"section_cap": "#ffffff",
|
||||
"section_hatch": [
|
||||
"#ffffff",
|
||||
"#ffffff"
|
||||
],
|
||||
"sel_edge": "#000000",
|
||||
"sel_edge_rim": "#000000",
|
||||
"sel_face": "#ffffff",
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"chip": "#000000",
|
||||
"construction_geom": "#000000",
|
||||
"construction_plane": "#ffffff",
|
||||
"comb_envelope": "#000000",
|
||||
"comb_teeth": "#000000",
|
||||
"continuity": [
|
||||
"#000000",
|
||||
"#000000",
|
||||
@@ -132,6 +134,10 @@
|
||||
"render_soft": "#ffffff",
|
||||
"rubber_band": "#000000",
|
||||
"section_cap": "#000000",
|
||||
"section_hatch": [
|
||||
"#000000",
|
||||
"#000000"
|
||||
],
|
||||
"sel_edge": "#ffffff",
|
||||
"sel_edge_rim": "#ffffff",
|
||||
"sel_face": "#000000",
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"chip": "#aa00aa",
|
||||
"construction_geom": "#aaaaaa",
|
||||
"construction_plane": "#ffff5522",
|
||||
"comb_envelope": "#ff55ff",
|
||||
"comb_teeth": "#55ffff",
|
||||
"continuity": [
|
||||
"#55ff55",
|
||||
"#5555ff",
|
||||
@@ -132,6 +134,10 @@
|
||||
"render_soft": "#000000",
|
||||
"rubber_band": "#55ffff",
|
||||
"section_cap": "#ffff55",
|
||||
"section_hatch": [
|
||||
"#555555",
|
||||
"#aaaaaa"
|
||||
],
|
||||
"sel_edge": "#ffff55",
|
||||
"sel_edge_rim": "#000000",
|
||||
"sel_face": "#ffff55",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user